An LCN Protocol

In this installment of the Layout Control Nodes (LCN) series, we will look at the methods and language nodes can use to communicate and coordinate activity. First, let’s briefly review where we are.

The Layout Control Node (LCN) is a microcontroller platform for controlling sections of a physical layout.  Leaving locomotive control to other technologies, the LCN focuses on layout objects and processes including block occupancy detection and management, turnout control, signals, lighting and other aspects of the layout environment.

I started thinking about this generally in Building Blocks for Layout Control. I started to more specifically explore the LCN concept in Basic Layout Control Nodes  with further discussion of hardware issues in Additional LCN Components.

In Layout Control Nodes I pulled everything together, describing use of an RF24Network.  So at this point, we have the means to set up communication between nodes of an RF24Network; all that’s lacking is a protocol for node-to-node communication.

What’s a Protocol?

When talking about a protocol, we generally mean the specific methods, rules and symbols for node communication. Protocols can be simple or complex, depending on the environment and your goals.

To start with, we need to define the scope of communication and the language or symbols to be employed. In other words, what will be the subject matter of communication and how will we represent information?

Scope

We will start by setting the scope of our protocol to layout operation, e.g., the operation of layout objects when trains are running. Some of you will see immediately that this is a limited scope of communication. Since operating trains is the whole reason we do this, focusing our protocol on that functionality will meet the needs of the majority of modelers … for now.

Let’s enumerate the objects and information or commands we have to manage:

  1. Blocks — reports occupancy state; optionally accepts “close” command to prevent transit
  2. Turnouts — reports alignment (close, throw); accepts alignment commands
  3. Signals — reports aspect (clear, approach, stop); responds to block and turnout state; accepts aspect commands

Blocks and turnouts are the most basic elements of a layout. Other elements are built in whole or part from those elements.  ABS Signals, for example, are a compound object that logically uses block and turnout state information to determine (whether determined at the node, or at some other processor) the correct aspect to show.

Symbols

How are we going to represent data in messages? You have two basic choices:

  • Strings
  • Binary values

Novices should start with Strings so that messages are human readable, easing the debugging process.  A production system should use binary values because you can communicate vastly more information with fewer bytes of data, directly impacting performance.

We have 3 layout object types listed. To start with we need a symbol to represent each object type. That symbol, plus an id, will allow us to identify and address specific layout objects.

Conventions: “” denote a string or character converted to a string. No quote marks means literal binary value. ALLCAPS denotes a constant whose value is defined as shown in the tables.

  • String version:
    • “1” or “a” or “b” for block
    • “2” or “b” or “s” for signal
    • “3” or “c” or “t” for turnout
  • Binary version:
    • the value 1 (00000001) for block
    • the value 2 (00000010) for signal
    • the value 3 (00000011) for turnout

For each object type, we need values to represent the state of the object, and values to represent commands the object can accept and act on.

Object State Values Command Values
Block CLEAR == 0
OCCUPIED == 1
NO_ENTRY == 2
Get State == 1
Set State== 2
Turnout ALIGN_NONE == 0
CLOSED == 1
THROWN == 2
Get Alignment == 1
Set Alignment == 2
Signal OFF == 0
STOP == 1
APPROACH == 2
CLEAR == 3
Get Aspect == 1
Set Aspect == 2


In String based messaging, you would send the numeric character representing a value: “1”, “2”, “3” etc. In a binary message, you send the actual value: 1, 2, 3 and so on.

Isn’t this Arbitrary?

Of course it is.  But there should be “method” in the madness (with apologies to Will Shakespeare).

There are a few basic considerations

  • Size (in bytes) of symbols and data
  • Usage semantics

Byte Sized Pieces

The importance of size is pretty obvious.  The size of your message payload affects performance.  The fewer bytes that have to be assembled, transmitted, received and then re-parsed into usable data, the better the system will perform.

I like to size everything except extended data (data involving numbers larger than 255) as single bytes. Using the symbol/value/command tables above, the actionable portion of each message will be 3 or 4 bytes whether the data is binary or String. The underlying reason for selecting low values should be obvious.

This approach runs into trouble when integrating with Java.  The problem is that “byte” values are unsigned in the C/C++ languages, which natively support both integer and floating point mathematics. The Java language only natively supports floating point math, and integer math is a construct within a floating point system. One the differences this causes is that bytes are signed in java. The practical effect is in C a byte can be a value from 0 to 255; in java a byte can be a value from -127 to 127. The underlying bits are the same; the difference is interpretation. In your java code, you have to force the VM to interpret byte values as unsigned.

In an RF24Network there are more than 256 possible addresses, so a node address is always 2 bytes.

I’m careful about how the value 0 is used in the protocol. I use 0 to representing null or empty states — a block is unoccupied, a turnout is neither closed nor thrown (probably moving), or a signal is showing no aspect. The Address 00 has special significance and can only be used for the MASTER in an RF24Network. Otherwise the only other use of 0 is as the first in a sequence of ids in a zero based numbering system.

C, C++ and most languages descended from them use zero-based indices for arrays.  It is best to avoid converting an id to or from a zero-based system; those transformations regularly lead to obscure, hard to find errors.  The object id and the container array index should be the same.

Semantics

Semantics, how you construct meaningful “phrases” in a computer language, is an important consideration. The right semantics can make programming and maintenance much easier.

For example, lets assume we have an array of turnout objects called turnouts, and the index of each object in the array is used as its id. That produces the following semantics:

if(turnouts[id].isClosed()){ // method isClosed() returns true or false
... do something
} else if(turnouts[id].isThrown()){ // method isThrown() returns true or false
... do something else
} else {
... turnout out of position; handle it
}

Or, if you prefer, you could phrase the same logic tree this way (my personal preference):

switch(turnouts[id].getAlignment()){
case ALIGN_NONE:
// oops; stop the loco before its too late!
break;
case CLOSED:
// if this is OK, continue
// otherwise stop and fix
break;
case THROWN:
// if this is OK, continue
// otherwise stop and fix
break;
}

Lets assume the turnout object contains a private property

byte _alignment;

which contains the current alignment of the turnout as a value from the table of state values above. The public methods using that property might look like this:

boolean turnout::isClosed() { return _alignment == CLOSED; }
boolean turnout::isThrown() { return _alignment == THROWN; }
boolean turnout::canTransit() { return _alignment > ALIGN_NONE; }
byte turnout::getAlignment() { return _alignment; }

A Signal Example

One last example where multiple objects interact.  Consider a pair of signals that monitor two turnouts, arranged so that the frogs are facing each other, like this:

The Signals face away from each other, protecting the approaches to and transit through block 23. Both turnouts are protected by other signals at their frog end, so we can allow a train to approach the farther turnout.

From the point of view of the West signal, in order to show clear, T1 must be closed, block 23 must be unoccupied, and T2 must be open to transit (it can be closed or thrown). From the point of view of the East signal, T1 must be open to transit and T2 must be closed. For the purposes of this illustration, each signal will show APPROACH if the distant turnout is not in position.

To keep this illustration simple, I’m ignoring the turnouts west of T1’s diverging leg. In a real world implementation, if T1 is thrown, then all turnouts west of T1 must be either thrown or closed, depending on position and orientation, creating a clear path though the ladder.

So let’s look at the decision tree for each signal (this references a signal object containing this decision tree).

West:

if(turnouts[1].isClosed() && !blocks[23].isOccupied()){
if(turnouts[2].canTransit()) {
this.setAspect(CLEAR);
} else {
this.setAspect(APPROACH);
}
} else {
this.setAspect(STOP);
}

East

if(turnouts[2].isClosed() && !blocks[23].isOccupied()){
  if(turnouts[1].canTransit()) {
    this.setAspect(CLEAR);
  } else {
    this.setAspect(APPROACH);
  }
} else {
  this.setAspect(STOP);
}

Simplicity + Readability = Reliability

As you can see, the semantics I’ve adopted are clear, unambiguous and simple.  Every choice made to this point, no matter how small, leads to this result.

Notice that blocks have a single method for obtaining current occupancy, implemented to return true if and only if a block is occupied.  To flip the logic and determine if a block is NOT occupied, just reverse the return with ‘!’- the logical NOT operator. It reads naturally: “If turnout 1 is closed and block 23 IS NOT occupied” …

What About Messages

It seems like I’ve drifted away from node-to-node messaging in this post, but appearances are deceiving. Object level semantics should guide messaging semantics.  So now we come full circle and consider message content.

Message Payload

First, we’ll adopt the String format for message content. We’ll use “b”, “s” and “t” as the object type symbols. That helps make messages human readable for debugging purposes.

Second, will the message string be fixed length or delimited?

If the message is fixed length, then each component of the string has to be fixed length. If all the symbols are a value that can be represented with a single character, then fixed length works. If there will be any variation in the data where more than one character will be needed to represent a single value, then fixed length doesn’t work as well, forcing you to pad elements with spaces or zeros in case more than one character is needed.

A delimited String inserts a special character in between the elements of the message, to mark the element boundaries. This makes it easy to accommodate variable length data in message strings.

We’ll do delimited Strings and use commas as the delimiter.

Third, we’ll adopt a simple message pattern with 4 elements:

  • object type, object id, update (0) or a command (1, 2, etc), state or 0

Message semantics should efficiently communicate the information needed by layout objects. It should be easy to parse so that the process of turning a message into action has the fewest possible steps.

Lets look at example messages, using the 4 element semantics:

Message Delimited String
Block 3 is Occupied “b,3,0,1”
Turnout 7 is Changing Position “t,7,0,0”
Turnout 13 is Thrown “t,13,0,2”
Set Turnout 15 to Closed “t,15,2,1”
Get Signal 5 Aspect “s,5,1,0”
Signal 5 Aspect is CLEAR “s,5,0,1”

What’s My Address?

The message strings above do not fully identify the objects in question. The full address of an object in a multi-node RF24Network is the ID of the host node, plus the internal id of the object (a zero-based index). In an RF24Network, the message header contains the addresses of the source and destination nodes, so we need not include that information in the message body.

In node-to-node communication, a message is read only by the destination node, so other nodes won’t act on the message. In this system, a message containing a command is always directed to the specific node containing the object you wish to command.

On the other hand, status change messages, such as block occupancy and turnout alignment messages, are generally intended to be broadcast to all nodes. Its up to each listening node to determine if the status message is of interest by comparing the source node address in the header, plus the id in the message body, to a table of objects the node is following.

Parsing Messages

Delimited strings are easy to parse using String find and substring methods. I like to use my own wrapper method for sub-strings to avoid common errors:

String safeSubString(String *subject, int start, int end){
  int size = subject->length();
  if(end == -1 && size > 0 && start < size){
    return subject->substring(start);
  } else if(size == 0 || start >= size || end >= size || start >= end){
    return "";
  }
  return subject->substring(start, end);
}

With that function, each element can be extracted this way :

String delimiter = ",";
String sub;
int pos;
byte element;

// find the next delimiter; pos == -1 if not found
pos = inString.indexOf(delimiter);
// extract substring up to, but excluding, the delimiter
sub = safeSubString(&inString, 0, pos);
// convert the substring to an byte value
if(sub.length() > 0){
  element = byte(sub.toInt());
} else {
  element = 0;
}
// strip the delimiter, and loop until returned string == ""
if(pos > -1) inString = inString.substring(pos + 1);

Next Steps

At this point, all the basic theory and considerations on the table. We have a network where each node has its own address, we can address a specific object using its host node ID plus the object’s internal index, and we have an easy to parse, human readable message format for conveying status information and commands.

In the next post on this thread I’ll create a simple example demonstrating practical network communication.

Until then, Happy Model Railroading!

Leave a Reply

Your email address will not be published. Required fields are marked *