I had thread going before the pandemic about Layout Control Nodes — I’ve been ruminating on this subject for a while: see Building Blocks for Layout Control, Basic Layout Control Nodes and Additional LCN Components. So let’s pick that back up and continue on. Put on your goggles, we’ll review old material then dive a little deeper into the subject.
Layout Control Nodes (LCNs) are dedicated to the physical layout — its turnouts, signals, lighting and animated accessories — leaving train control to other technologies.
That does not mean that LCNs and train control cannot interact, only that they are independent of each other. Properly implemented, LCNs can work cooperatively with any train control system: DC, DCC, WiFi, Bluetooth, etc.
Here’s the concept: take a basic microcontroller, such as an Uno or Nano, give it peripheral devices for communication and to provide ports for attaching your layout lighting, motors and electronics. That controller would then manage everything in a limited physical area — lets say 1 – 2 square meters. On any layout larger than a square meter or so, you can deploy multiple control nodes with reasonably localized wiring doing the work on your layout, instead of having all wiring leading back to a single central control point.
Why would you want to do that?
This simple answer is that it enables you to do more, and, with good coding, allows you to create a layout that organically responds to trains and the passage of fast clock time.
But, wait. Can’t I do that now? Fast clocks, DCC, JRMI, what’s missing?
I suppose the best concepts to use here are ACCESSIBILITY, DEPTH and SCALING.
- ACCESSIBILITY is a measure of how easy it is to use a tool, or more broadly, how easy it is to accomplish a functional goal, without having special expertise.
- DEPTH refers to the variety of objects and simultaneous actions supported.
- SCALING is the ability of a system to grow or shrink while maintaining the same level of performance.
A fully animated museum layout is a wonder to behold. So many things going on in perfect coordination. It takes substantial computing power with sophisticated communication and electrical switching technologies to make all that work. Add the ability to handle random elements, such as a decision made by a visitor by pushing a button, and the degree of complexity in decision making multiplies exponentially.
Its hard to replicate that, even on a small scale, in your own basement. Why? Because available tools are built around a single process — e.g., a single program, such as JRMI, running on a central computer — doing all the work. That is a classic strategy, going back to the roots of computing and, coupled with “event driven” software implementations, it will get you a long way.
But there are costs to that approach:
- Accessibility is a big issue with centralized systems. How easy or difficult is it to program, use or later change? What level of programming knowledge, if any, is required to use or configure the system? Are the management tools graphical (GUI) or textual?
- Depth in 2022 and beyond requires an automation system to support more than just turnouts and signals. A system with depth includes automation support for layout lighting and a wide variety of layout accessories, including motorized accessories.
- Centralized systems usually deal with dumb attached objects, so it must be connected to everything and must micromanage everything. From the user perspective, you are potentially dealing with long lists of objects, with associated functional data, that are hard to organize and manage, impacting Accessibility, Scalability and Depth.
- Scalability and Depth are limited by the speed and memory capacity of the controlling computer. If the central system has to manage too many objects, it will tend to become saturated and unresponsive.
- Significant changes to an established, centralized system can be difficult.
The Principle of the Layout Control Node
The principle of the Layout Control Node is to subdivide the layout into smaller, semi-independent sections that are easier to create, wire, program, combine and use. The result:
- Each section can be managed independently, with shorter object lists and the capacity to do more with the objects. Whatever you do in one section should not directly affect other sections. Just breaking your layout down into smaller independent modules vastly improves accessibility.
- While the scope of a section is limited by its physical size, the potential automation depth of each section is not limited by that. The depth of each section depends on the number and variety of supporting ports available for attaching layout objects. The more ports available, the more the processor can do.
- It is much easier to build your layout electronics one independent section at a time. Since each section has its own processor, the system automatically scales as you add or remove sections. Because each node is logically and functionally independent, adding a node does not impose a significant burden on other nodes. Instead, each new node increases the amount of simultaneous activity the system can support.
- Each section manages its objects according to the rules you provide. Since each section has its own processor and rules set, the combined behaviors can be unexpectedly organic.
An array of Layout Control Nodes, deployed in layout sections, is a multi-threaded, multi-processing engine that can give layout behavior an organic, natural quality because each node is responding to layout events in its own way. Behaviors like these aren’t easily programmed centrally; they are an emergent property of this kind of system.
Communication Method
To paraphrase an old expression, “no person is an island,” the same is true of LCNs. When a train is running, what happens on an adjacent node may matter to another node. So our LCNs must communicate with each other.
You want node-to-node communication to be wireless. Wired communication between multiple nodes would require require additional hardware to route communications; that sort of excess wiring and hardware is exactly what we want to avoid.
Your wireless choices include WiFi, XBee and nRF24. WiFi and XBee can transit significant distances, and to do that they have fairly high power consumption. nRF24 communications use the 2.4 GHz band, which is intended for short range use, giving good short range performance with lower power consumption. In a typical basement or even a 5000+ square foot club layout, the nRF radio provides the necessary range while saving power and leaving your WiFi for other uses.
Accordingly, a Layout Control Node should include an nRF24L01+ radio as a basic component.
Why Not Use WiFi?
Some will argue for an ESP32 or similar IOT device as a node using built in WiFi. That is a viable alternative, but not necessarily the best approach. Here’s why:
- WiFi is a relatively complex, processor intensive collection of protocols; the processing and memory burden impose by the network drivers is substantial. The design of the ESP32 is optimized for those tasks, but they still have an impact on the device’s overall performance as you add in additional tasks.
- WiFi requires an Access Point and Router. You can fabricate one, but most people will just use a standard WiFi router; for many, that means mixing your layout and household traffic unless you have a special, correctly configured AP/Router for your trains. You would have the option of creating a mesh network, adding fault tolerance and simplifying adding and removing devices. So there are interesting options if you go WiFi.
- Nevertheless, the big reason to avoid WiFi for this application is power consumption: WiFi hardware is relatively power hungry, typically requiring 300 mA or more power to support each WiFi device. That’s a lot of amps to consume before you’ve run a turnout motor or lit up some LEDS. On a small layout with two control nodes talking to each other, no big deal. On a larger layout with 10 control nodes, you are burning 3 amps of layout power just to support WiFi. 3 amps could support over 100 LED devices, a far more entertaining use of the power!
In contrast to WiFi, an nRFNetwork using nRF24L01+ radios:
- is self routing so it does not require an access point or router;
- Is a simple protocol requiring minimal processor support; and
- each device draws a mere 12 mA while listening, and 120 mA when transmitting. Overall power savings is 60 to 70% compared to WiFi. On a layout with lots of lighting, signals and turnout motors, the power savings matter.
So I chose nRF for my layout network. You may choose differently.
nRF24L01+ Hardware
The nRF24L01+ chip is available on small, plug in cards with a built-in antenna. For greater range, you can use a version of the card with a connection for an external antenna. While outdoor railroading enthusiasts might need the extra range, for indoor use the cards with an integrated antenna are the right choice.
Connecting an nRF radio is straight forward. MISO, MOSI and SCK are connected to the SPI terminals with the same names on your board. CSN and CE can be any two other digital pins. Since the 3 SPI pins are shared, only two pins are used exclusively — CE & CSN. If you use a Nano carrier board that includes a connector for an nRF radio, like the one shown below:
… then the 3.3 volt terminal is connected to the 3.3v output from the Nano, CSN will be connected to D9 and CE will be connected to D10.
Notice the capacitor below the lower right corner of the yellow header. This is here primarily because the source of the 3.3 volts is the USB chip on the underside of the Nano. That source has very limited current capacity and, while the nRF uses little power when idle or receiving, it needs 120 mA current to drive the transmitter, more current than the USB chip can provide. Without the capacitor reservoir, radio transmission would fail. The capacitor also serves to filter power supply noise. You need the capacitor when using the 3.3v output of any Arduino board.
Precautions
1. These devices are really static sensitive. Make sure you discharge all body static before handling one. The surest way to kill the card is a static discharge from your finger to the radio chip. I have a pile to dead radios to prove it.
2. Along that line, these boards rarely survive being plugging in incorrectly. Make sure all pins are in the terminal block, in the right direction, before applying power.
3. You may find these do not work well with some knock-off Arduinos. That is because one of the ways the knock-offs save costs is by using inferior USB chips. Some USB chips cannot support the power requirements of the nRF radio, even with the reservoir capacitor. If the USB chip tends to get hot when using an nRF radio, that is a clue that its a poor quality chip.
4. If you create a custom board to host your radio, give it an independent circuit to provide regulated 3.3v power to the radio. That is the best way to ensure adequate power regardless of who made the Arduino board you are using.
Required Libraries
To use the radio you there are several required libraries:
- RF24 – Radio Driver, OSI layer 2 library for nrf24L01+ modules. To use the driver, include RF24.h in your sketch after installing the library. Documentation.
- RF24Network – OSI Layer 3 Networking for nrf24L01+ devices. Provides addressing and routing for your layout network. To use the driver, include RF24Network.h. Documentation.
Use the library manager in your Arduino IDE to obtain and manage these libraries. Other choices are available for OSI layer 3 networking, but for a model railroad the RF24Network is just right.
Creating a network
The RF24Network library allows you to create a routed mesh network for your nodes.
The topology of the RF24Network is hub-and-spoke, using a MASTER node as the top hub uniting all the branches beneath it. The MASTER can have up to 5 children, as can each child. The depth of the topology can be up to 5 levels (including the MASTER), which results in up to 781 addressable nodes on one radio channel. Really large networks can be achieved by running multiple parallel networks on different radio channels.
Addressing
The address of a layout control node on an RF24Network is a number in OCTAL format that describes the route from the MASTER to reach the node.
The MASTER Node is always 00.
The Master Node can have up to 5 children. The address of each child node is 1 through 5 with a leading 0 — 01, 02, 03 , 04 and 05. These are referred to as Level 1 addresses.
Each node can have 5 children of its own, whose addresses are Level 2. The address of each child is 01 through 05 preceding the address of its parent. So, the children of node 01 are 011, 021, 031, 041 and 051 respectively. The entire tree can have up to 4 levels below the Master. This can be illustrated as follows:
The number of nodes you’ll use depends on the size and complexity of your layout. In most cases you’ll want to assign all 5 level 1 addresses before using level 2 or deeper addresses. On many if not most home layouts, you won’t need more than the 5 level 1 nodes
This HO layout would work well with 5 client nodes. The illustrations shows one way to divvy up the work.
On a large layout, the assignment of node addresses should be directly related to the physical placement of the nodes, creating physical paths for messages to transit the layout from the Master out to the furthest edge and back. Level 1 nodes should be physically closest to the Master, while Level 2 nodes should be closer to their level 1 parents than to the Master, level 3 nodes be closer to level 2 parents than anything else, and so on.
Using the RF24 Radio Drivers
Put these lines at the top of your main sketch, before setup() and loop():
#include <SPI.h>
// include the radio driver
#include <RF24.h>
// include the network driver
#include <RF24Network.h>
// create the radio object; CE and CSN can be any digital pins you choose
RF24 radio(10, 9); // nRF24L01 (CE,CSN)
// create the network object and link the radio
RF24Network network(radio);
// address for this node
const uint16_t this_node = 01;
// channel chosen for this network
const uint8_t channel = 90;
In setup(), start the network with these lines:
// start SPI communications
SPI.begin();
// start the radio
radio.begin();
// start the network, specifying a channel and the address of this node
network.begin(channel, this_node); //(channel, node address)
// set speed to highest supported by the nRF24L01+
radio.setDataRate(RF24_2MBPS); // Max baud rate
// set power amp level to high; default power level is too low
// For maximum range, use RF24_PA_MAX
radio.setPALevel(RF24_PA_HIGH);
In loop(), call the update function at least once per iteration of the loop:
network.update();
At this point you are ready to begin communicating with other nodes. Here is a basic function for sending a message to another node:
void sendMessage(uint16_t to_node, byte *data, int bytes){
RF24NetworkHeader header(to_node);
network.write(header, data, bytes); // Send the data
}
Here is a basic function for receiving messages:
boolean getNextMessage(uint16_t to_node, byte *data, int maxBytes){
boolean msgReceived = false;
network.update(); // Process network traffic
while( network.available() ) { // Is there any incoming data?
RF24NetworkHeader header;
network.read(header, data, maxBytes); // Read the incoming data
msgReceived = true;
}
return msgReceived;
}
Readers with a lot coding experience may question the use of a while() loop; wouldn’t that cause multiple messages to overwrite each other?
No, it will retrieve just one message. I can’t tell you exactly why you have to do it this way, but if you try the obvious if() clause instead, receiving fails. My best guess is that multiple passes may be required to fetch all the data, and the header is used to differentiate messages and prevent overwrites. In any case, the while() loop is mandatory.
How Do You Use the Network?
What are you going to say and how are you going to say it? A network communications system has many moving parts; we have the OSI layers in place, and now we need a protocol for message payloads.
I’ll talk about that next time.
Until then, Happy Model Railroading!
This is what I’ve been waiting for, I think. Thanks.
I have been linking through some of your previous postings on RF24 and LCN and block detection, not sure which ones came first and which were follow-ups. I think this one helps to tie things together, but there is still a lot of helpful information on the other posts. I wonder if you could put together a linked list of posts that a reader could follow that would give them the key information in the correct order. Like: “Read these posts in this order …”
I still have not come across the one missing piece that I’ve been looking for; that is the .ino code that takes the inputs, applies logic and then sends instructions to other nodes to react to the input. I’m imagining one node to have block detection sensors, but another node has the servo that controls the correct turnout. Do you have the same code in all nodes? Or is each node’s code specific to its devices? You may well have posted complete code, but I haven’t run across it yet. I’m capable of figuring it out myself, but why reinvent the wheel? Programmers are basically lazy – that’s why they write code!
Thanks again,
Randy
Hi Randy,
I haven’t done a communications protocol yet. That’s coming and essential to doing anything useful with multiple nodes. I’ll see about including a post list as you suggest.
Once you have a communications protocol, the remaining question is how to divide up the tasks. You could do something like assign block detection to one node, and turnout control to another. Then a third node might be a command node attached to a control panel that listens to and controls the other nodes. We’ll call that model a “functional” division of tasks. In this scenario, each node has a unique software package tailored to its assigned task.
The other model I’ll call a “geographic” division of tasks. In that model, each node is assigned a specific area of the layout where it has complete control. Having control over blocks, turnouts and other objects in a specific zone makes each zone semi-independent, equipped with the necessary logic to run all the objects in its area. It has 3 main jobs 1) run its objects;, 2) broadcast information every time the state of one of its objects changes; and 3) to listen for messages from other nodes. In this scenario, each client node (the MASTER required for RF24 networking will always be unique) can have the same software load, with behavior controlled by configuration data.
If the node runs any signals, it may need to know block and turnout status from adjacent nodes. Buttons for commanding turnouts can be either connected directly to the node owning the turnouts, or buttons and switches can be attached to other nodes and cause command messages to be sent to target turnouts.
On a small layout the functional model would work fine. On larger layouts, you would have multiple nodes for each function (3 for blocks, 4 for turnouts, etc.), so the geographic model probably works better.
Best, Rob
Thanks. I will stop looking for that missing piece. Based on your groundwork, I will do some coding and testing of my own. Here’s my plan for now. Testing will determine if it is viable. The “00” master node will have one sketch and all slave nodes will have a second sketch. The slave sketches will simply take any inputs and pass them up the chain and take any commands from the master and pass them down to the target slave. The slaves will differ from each other with a table that defines for each of its inputs a target ID (single or group) along with a desired command code. The master will receive the target and desired command, handle any potential conflicts, and send the command down the target chain. Even if the input and target are on the same node, letting the master node make all decisions should simplify code on all nodes, although there would be a slight delay to go up and down the chain. I will try to mitigate the delay by giving highest priority to passing on messages intended for other nodes. Any slave node can have any kind of input and any kind of output, in any combination. The table will define what will be done. And the master will keep track of statuses and handle any conflicting actions. Or I may have the master update JMRI and get statuses from JMRI.
I will keep you posted on my findings.
Thanks, again,
Randy
Sounds like good plan to start with. I am curious how you would handle a JRMI interface.
Best, Rob
My layout runs on an NCE Power Cab, which is connected to JMRI via the NCE USB interface board. Here is a Python script for JMRI. It listens for serial input from the USB port. I realize this is only one side of the conversation between Arduino and JMRI, but it will take a little longer to get you the Arduino code because it currently contains a lot of stuff that is not pertinent (e.g., reading bar codes to ID trains).
# JMRI_listener.py
# Listen to IR train detection sensors
# Based on SensorLog.py:
# Listen to all sensors, printing a line when they change state.
# Author: Bob Jacobsen, copyright 2005, 2008
# Part of the JMRI distribution
import jmri
import java
import java.beans
import jarray
# Routine to return appropriate text for an event state (oldValue or newValue)
def stateName(state) :
if (state == ACTIVE) :
return “ACTIVE”
if (state == INACTIVE) :
return “INACTIVE”
if (state == INCONSISTENT) :
return “INCONSISTENT”
if (state == UNKNOWN) :
return “UNKNOWN”
return “(invalid)”
# Define the sensor listener: Print some
# information on the status change.
class SensorListener(java.beans.PropertyChangeListener):
def propertyChange(self, event):
if (event.propertyName == “KnownState”) :
uName = event.source.userName
if (uName.startswith(“CCTA”) and event.newValue == ACTIVE) :
track = uName[4:5] # M = main; S = siding
turnOut = uName[5:]
# throw a Walthers turnout
t = turnouts.provideTurnout(turnOut)
if (track == “M”) :
print(“Set turnout ” + turnOut + ” to CLOSED”)
t.commandedState = CLOSED
if (track == “S”) :
print(“Set turnout ” + turnOut + ” to THROWN”)
t.commandedState = THROWN
return
listener = SensorListener()
# Define a Manager listener. When invoked, a new
# item has been added, so go through the list of items removing the
# old listener and adding a new one (works for both already registered
# and new sensors)
class ManagerListener(java.beans.PropertyChangeListener):
def propertyChange(self, event):
list = event.source.getNamedBeanSet()
for sensor in list :
sensor.removePropertyChangeListener(listener)
sensor.addPropertyChangeListener(listener)
# Attach the sensor manager listener
sensors.addPropertyChangeListener(ManagerListener())
#
# For the sensors that exist, attach a sensor listener
list = sensors.getNamedBeanSet()
for sensor in list :
sensor.addPropertyChangeListener(listener)
Sorry, the essential indentation for the Python script was removed when I submitted the comment. In this version, I’ve inserted one dash for each indentation, so you can replace each dash with 2 spaces:
# JMRI_listener.py
#
# Based on SensorLog.py:
# Listen to all sensors, printing a line when they change state.
# Author: Bob Jacobsen, copyright 2005, 2008
# Part of the JMRI distribution
import jmri
import java
import java.beans
import jarray
# Routine to return appropriate text for an event state (oldValue or newValue)
def stateName(state) :
–if (state == ACTIVE) :
—-return “ACTIVE”
–if (state == INACTIVE) :
—-return “INACTIVE”
–if (state == INCONSISTENT) :
—-return “INCONSISTENT”
–if (state == UNKNOWN) :
—-return “UNKNOWN”
–return “(invalid)”
# Define the sensor listener: Print some
# information on the status change.
class SensorListener(java.beans.PropertyChangeListener):
-def propertyChange(self, event):
–if (event.propertyName == “KnownState”) :
—-uName = event.source.userName
——if (uName.startswith(“CCTA”)) :
——–print(“CCTA? : TRUE”)
——print(“ACTIVE? ” + stateName(event.newValue))
—-#if (uName.startswith(“CCTA”) and stateName(event.newValue) == “ACTIVE”) :
—-if (uName.startswith(“CCTA”) and event.newValue == ACTIVE) :
——track = uName[4:5] # M = main; S = siding
——turnOut = uName[5:]
——# throw a Walthers turnout
——t = turnouts.provideTurnout(turnOut)
——if (track == “M”) :
——–print(“Set turnout ” + turnOut + ” to CLOSED”)
——–t.commandedState = CLOSED
——if (track == “S”) :
——–print(“Set turnout ” + turnOut + ” to THROWN”)
——–t.commandedState = THROWN
–return
listener = SensorListener()
# Define a Manager listener. When invoked, a new
# item has been added, so go through the list of items removing the
# old listener and adding a new one (works for both already registered
# and new sensors)
class ManagerListener(java.beans.PropertyChangeListener):
-def propertyChange(self, event):
–list = event.source.getNamedBeanSet()
–for sensor in list :
—sensor.removePropertyChangeListener(listener)
—sensor.addPropertyChangeListener(listener)
# Attach the sensor manager listener
sensors.addPropertyChangeListener(ManagerListener())
#
# For the sensors that exist, attach a sensor listener
list = sensors.getNamedBeanSet()
for sensor in list :
-sensor.addPropertyChangeListener(listener)
Thanks. I’m not currently working with JRMI so I’m always interested in seeing examples of how others are using it. This is a great example of well formed code. May I assume that “sensors” in this case are buttons/switches to command turnouts?
What’s interesting is the number lines of code it takes to setup turnouts with a command mechanism when doing it in a centralized way. On a small layout, it doesn’t matter. But expand the number of turnouts and sensors to 50, 60 70 or more of each and the code simply iterates more and consumes more cycles in that repetition. None of that would matter except that there is only one primary process to handle everything. A personal computer can make up for the workload with raw speed and system resources (memory in particular), but eventually the saturation point is inevitably reached. On an Arduino or other microcontroller device, saturation comes that much sooner for lack of resources.
The theory of the Layout Control Node is to decentralize and redistribute work to simpler, cheaper processors, and to make the system self-scaling because a new processor comes with each new layout section.
One more wrinkle: consider the possibilities of cooperatively joining a fully decentralized system to JRMI, and combining the automated processes of the Layout Control Nodes with the top-down view of JRMI. Now JRMI can focus on controlling trains within a smart, responsive environment. What could you do with that?
Yes, “sensors” are any kind of input – IR, pushbutton, etc.
Decentralization is great, but my code to read barcodes is very big. I would have to buy a Mega for each node. So to stay on the cheap, I will put all that code in one or a few master nodes. I will play with the possibility of the first layer of slave nodes being a master and all lower levels being simple Nano slaves. But complete decentralization will eventually have to somehow deal with turnouts on one node/chain (section of layout) needing input info from a different node/chain (section of layout). JMRI controlling inputs/outputs won’t work for me because my barcode reading needs low-level high-speed processing not available in JMRI. I’m going to start with something that I think has a better chance of working, keeping in mind that decentralization is a good target to work for.
And here’s some Arduino code for conversing with JMRI that has been working for me. Pay special attention to “cmri” statements.:
// My Arduino is a Mega because it has 64 ports. It has a PCA9685 shield attached for outputs (turnouts).
// I was initially thinking that all my turnouts could be controlled by this one Arduino.
// But I now realize that I will need many more ports. That’s why the LCN network appeals to me.
// I will have to redo a lot of code to accomodate LCN, but I’ll try to show what is needed to
// communicate with JMRI. Look for “cmri” for commands that communicate with JMRI.
// After starting up the communication connection in “Setup”,
// – each loop:
// – initiates a cmri.process // I don’t know why this is needed for each loop
// – calls pollSensors() for each turnout to:
// – call digitalRead() to get Arduino input statuses
// – call cmri.get_bit() to get the current turnout status from JMRI
// JMRI
// – Sensors (CMRI)
// – 1st: address 1001 – name CS1 (or your choice)
// – 2nd: address 1002 – name CS2 (or your choice)
// – 3rd: address 1003 – name CS3 (or your choice)
// – Turnouts (CMRI)
// – 1st: address 1001 – name CT1 (or your choice)
// One sensor, CS3 (or whatever you named it)
// – 2nd: address 1002 – name CT2 (or your choice)
// I set the Arduino up to behave like a piece of CMRI hardware called SUSIC:
// Up to 64 slots
// Each slot has either a 24 or 32 bit input/output card
// 64 x 32 bit cards gives up to 2048 input/outputs!
// However, it’s best to only set up the SUSIC with the required inputs/outputs to make the process more efficient.
// We will set cards 0 and 1 to be the sensor inputs (up to 64 inputs) and card 2 to support 32 servo outputs
// Include libraries
#include
#include
#include
// CMRI Settings
#define CMRI_ADDR 1 //CMRI node address in JMRI
#define DE_PIN 2
// The number of servos connected
#define numServos 2
// Define the PCA9685 board addresses (This assumes there’s only one PCA9685 board attached)
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(); //setup the board address – defaults to 0x40 if not specified
// Setup serial communication
// On a Auto485 board (which we are not using):
// DE = Driver Enable pin (set HIGH to indicate transmitting – RE must be LOW)
// RE = Receive Enable pin (set HIGH to incicate receiving – DE must be LOW)
// (Randy – I’ve read that it is acceptible to combine DE and RE, so that HIGH is transmit and LOW is receive. Perhaps that is what the Arduino does.)
Auto485 bus(DE_PIN); // Arduino pin 2 -> MAX485 DE and RE pins
// Define CMRI connection with 64 inputs and 32 outputs (As indicated above, the total could be 2048 inputs/outputs.)
CMRI cmri(CMRI_ADDR, 64, 32, bus);
// turnout tables
// ————–
// 0 = address 1001 (“1″=PCA9685 board; “001”=pin 0 on that board
// this is the correct value for servos
//int Turnout[] = { 0, 1 }; // zero-relative number of the turnout and the PCA9685 pin # of the servo
// this is to test a Walthers turnout
int Turnout[] = { 0, 2 }; // zero-relative number of the turnout and the PCA9685 pin # of the servo
int ThrowPos[] = { 1015, 800 }; // the “thrown/siding” position for each servo
int ClosePos[] = { 1800, 2200 }; // the “closed/mainline” position for each servo
bool Tstatus[] = { LOW, LOW }; // initial/current status of turnouts (LOW = Closed and HIGH = Thrown)
int Ttype[] = { 0, 0 }; // Types: 0=SG90-servo 1=Walthers
// sensor tables
// ————-
// NOTE: Arduino Mega allows only these pins:
// 3 – 12
// 14 – 19
// 22 – 69
// – for a total of 64 input pins
// – pins 0, 1, 2 are reserved by either the Arduino or the shield board
// – pin 13 is for the board’s LED as output, which can be turned on/off by code
// – pins 20 & 21 are reserved for connecting to the PCA9685 servo output board(s)
// 3 = address 1001 (“1″=CMRI_ADDR 1 for first Arduino board; “001”=first input pin (3) on that board
char incomingByte = 0; // for initial transmission from JMRI
int TcomgMain[] = { 3, 7 }; // Arduino pin # of “Train coming” sensors – main line
int TcomgSide[] = { 4, 8 }; // Arduino pin # of “Train coming” sensors – side track
int Fback[] = { 5, 9 }; // Arduino pin # of “Feedback” sensors – to confirm switch position
int Pbutn[] = { 6, 10 }; // Arduino pin # of “Push buttons” to toggle switchs
int Fstatus[] = { LOW, LOW }; // Status of feedback sensors
int SensorStatMain[] = { LOW, LOW }; // table of saved signal status
int SensorStatSide[] = { LOW, LOW }; // table of saved signal status
int PBstatus[] = { LOW, LOW }; // table of saved pushbutton status
int SignalA[] = { 30, 32 }; // Address of pushbutton light A
int SignalB[] = { 31, 33 }; // Address of pushbutton light B
int SensorStat[] = { LOW, LOW }; // this is temporary for code that will be replaced
int control = LOW;
unsigned long lastDebounceTime = 0;
unsigned long debounceDelay = 50;
int aMainComing; // setting for a single turnout
int aSideComing; // setting for a single turnout
int aJmriFeedback; // setting for a single turnout
int aJmriTurnout; // setting for a single turnout
int aPushButton; // setting for a single turnout pushbutton
void setup() {
// Start the serial connection
Serial.begin(9600); //Baud rate of 19200, ensure this matches the baud rate in JMRI, using a faster rate can make processing faster but can also result in incomplete data
bus.begin(9600); // Randy changed this from 19200 to 9600 to try to fix the startup polling issue
for (int i=0; i<numServos; i++) {
pinMode(Pbutn[i], INPUT_PULLUP);
pinMode(Fback[i], INPUT_PULLUP);
pinMode(TcomgMain[i], INPUT_PULLUP);
pinMode(TcomgSide[i], INPUT_PULLUP);
Tservo[i].attach(Turnout[i]); // attach the Servo servo to the assigned pin
}
// Initialise PCA9685 board
pwm.begin();
//pwm.setOscillatorFrequency(25000000); // Randy added this to try to fix a problem with the servos not moving, but it didn't help
pwm.setPWMFreq(50); // This is the maximum PWM frequency
}
void loop(){
//Serial.println("Looping");
cmri.process();
// POLL ALL SENSORS
for (int i=0; i<numServos; i++) {
pollSensors(i);
}
}
void pollSensors(int i) {
// collect all sensor information related to a given servo turnout
// poll all sensors for the given servo
aMainComing = !digitalRead(TcomgMain[i]); // is there a train coming on the mainline?
aSideComing = !digitalRead(TcomgSide[i]); // is there a train coming on the siding?
aPushButton = digitalRead(Pbutn[i]); // has the turnout's push button been pressed?
aJmriFeedback = !digitalRead(Fback[i]); // get the status of the JMRI turnout (Thrown or Closed)
aJmriTurnout = cmri.get_bit(Turnout[i]); // get the status of the JMRI turnout (Thrown or Closed)
// Check JMRI for changed turnout setting
// ========== =======
if (aJmriTurnout != Tstatus[i]) { // JMRI has changed the status of the turnout
if (Tstatus[i] == LOW) { // JMRI has set the turnout to HIGH (closed?)
Tstatus[i] = HIGH;
pwm.writeMicroseconds(Turnout[i], ThrowPos[i]); // throw servo
cmri.set_bit(Fback[i]-3, HIGH); // set the sensor flag in JMRI to Active (2 is the CT1 feedback sensor CS3)
}
else { // JMRI has set the turnout to LOW (thrown?)
Tstatus[i] = LOW;
pwm.writeMicroseconds(Turnout[i], ClosePos[i]); // close servo
cmri.set_bit(Fback[i]-3, LOW); // set the sensor flag in JMRI to Active (2 is the CT1 feedback sensor CS3)
}
}
// Check JMRI for changed feedback setting
// ===== ========
if (aJmriFeedback != Fstatus[i]) { // JMRI has changed the status of the turnout
if (Fstatus[i] == LOW) { // JMRI has set the turnout to HIGH (closed?)
Fstatus[i] = HIGH;
pwm.writeMicroseconds(Turnout[i], ThrowPos[i]); // throw servo
cmri.set_bit(Fback[i]-3, HIGH); // set the sensor flag in JMRI to Active (2 is the CT1 feedback sensor CS3)
}
else { // JMRI has set the turnout to LOW (thrown?)
Fstatus[i] = LOW;
pwm.writeMicroseconds(Turnout[i], ClosePos[i]); // close servo
cmri.set_bit(Fback[i]-3, LOW); // set the sensor flag in JMRI to Active (2 is the CT1 feedback sensor CS3)
}
}
. . .
}
Thanks. I think others will find this useful.
Barcode reading does add challenges, so I can see how your needs exceed what JRMI can do. One way to deal with that would be to use dedicated processors for barcode reading. Reader nodes would transmit data to other nodes that are interested. This is a situation where functional division of labor would work.
Best, Rob
That makes sense, and is probably the way I will have to go. I will keep you posted on my progress once I get a few nodes talking to each other.
Randy