C++ Objects for Layout Control, Part 1

The programming language supported by the Arduino IDE is C++, an object-oriented super-set of the C language (one of the fundamental languages of the computer programming world). This post is geared to newbies who have little or no previous experience with object-oriented programming. Experienced hands can skip past the basic explanations and see what I’m doing with it in a practical example.

Object-oriented programming (OOP) allows the programmer to conceptualize and write software in a way that is less like how the machine runs it (a list of instructions) and more like how humans think (objects, abstractions, relationships, etc.). Of course, you still have to write specific procedures, but they are placed in context within a software object. Don’t worry if this doesn’t make sense yet.

Without going into the more exotic aspects of OOP theory (we may get to some of those eventually, but not today), the point of OOP is to create the software equivalent of a “black box”—something that you can give data to and get a result from without knowing anything about how it works internally. This makes code highly reusable, the second major point of OOP.

OOP and the Arduino IDE

The Arduino IDE is a simplified environment within which to write C++ code. You cannot create and edit the formal file structures of C++ (.h header files and .cpp implementation files) within the IDE. However, within the IDE you can include C++ OOP code in your sketch with minimal formalities, so its a good place to get to know C++ objects. Those who write in C++ (whether professionally or as a dedicated amateur) can include their own work by installing it as a library the Arduino IDE can compile and include.

Given the context, from here on I’ll limit myself to how to do things in the Arduino IDE. To find out more about formal C++, here is one of many sites devoted to the subject: http://www.cplusplus.com/doc/tutorial/


Consider the things you are trying to control around the layout; would it be easier to manage turnouts, signals or animated/lit things as C++ objects? I think you’ll agree that it would. Lets explore an example.

A Basic Animated Object

My turntable bridge includes an operator’s hut containing a small stove with a fire for warmth.

If you Google Arduino fire simulations, you’ll find all kinds of algorithms and methods for simulating fire with LEDS and an Arduino. This fire is intended as a low, banked fire; the kind that has a variable glow and occasional sparks with no flame; probably coal is the fuel. So I’m keeping it simple: a single red LED on a PWM pin, its brightness rising and falling over time with random bright flashes. To do that, the code has to track the state of the led and whether brightness is rising or falling, periodically increment or decrement the brightness within a defined range, and randomly go high brightness for one cycle to represent a spark.

A First Class

The code that defines an object and its implementation is called a CLASS. Begin with the keyword class, give it a name and setup a pair of curly braces and a trailing semicolon to contain the definition:

class fire
{
};

In the Arduino IDE we can do this exactly the same way we create a TYPEDEF to define a data structure: put it in the head of your INO file (the sketch) before you use it and before the setup() function.

Now then, we need some content for this class: what data does a fire object need to do its job? It needs the know the pin it will use, the low and high values for brightness it will use, and the rate or frequency of updates. In order to run, it has to also keep track of its own state and when it was last updated.

class fire
{
  private:
   int ledPin;
   int high;  // value for maximum brightness
   int low;   // value for minimum brightness
   int rate;  // update frequency in milliseconds

   bool dirUp; // true if brightness is increasing
   int state;
   unsigned long lastUpdate;  // in milliseconds; can be a big number
  
};

That looks like a bunch of variable definitions, right? Exactly; just like local variables within a function. By convention we refer to these variables as the properties of the object. The private keyword that precedes the properties tells the compiler that these properties can only be accessed from within the object. While you can make properties public and thus directly accessible from outside the object, you should not do that without good cause because it could break the integrity of the “black box”. In general, the best practice is to create a public method to retrieve a private property from outside the object.

Methods

In order to do something useful a class has to have methods, the OOP term for class internal functions. Public methods are accessible from outside the object and constitute the interface you use to manipulate the object. Classes can also contain private methods, accessible only from within the objects. All objects require a special public method called the constructor which is called once when an object is instantiated—an instance is created in memory and initialized.

The Constructor

The constructor is a public method, with the same name as the class.

 public:
 
 fire(int pin, int zhigh, int zlow, int zrate){
   ledPin = pin;
   high = zhigh;
   low = zlow;
   rate = zrate;
 
   state = low;
   dirUp = true;
   lastUpdate = 0; 
   analogWrite(ledPin, state);
 }

The constructor takes 4 arguments. Upon entering the constructor, the first step is to transfer argument values to the object properties. Then the method sets the initial LED state to low brightness, sets the direction of brightness change to up (increasing brightness), initializes the lastUpdate property and turns the LED on at the desired PWM level.

The Update Method

In addition to the constructor, the fire object requires a public update method that is called regularly to allow the object to update itself and its LED.

void Update(unsigned long curMillis) {
   if(curMillis - lastUpdate >= rate){
     lastUpdate = curMillis;
     dirUp ? state++ : state--;
     if(random(80) == 1){ // do a spark at random intervals
       analogWrite(ledPin, 255);
     } else { // otherwise write the updated state to the LED
       analogWrite(ledPin, state);
     }
     if(state == high || state == low){ //if at end of range
       dirUp = !dirUp; // boolean logic flip
     }
   }
}

The update method takes a single argument, the current “time” (since the program started) in milliseconds. The intention here is that this method will get called more frequently than absolutely necessary, terminating immediately if it is not yet time to update. Note that here I need to use the random() function to trigger the spark, but up to this point I have not seeded the psuedo random number generator. In the final sketch, I include a call to randomSeed() in the constructor so that each instance will reseed the generator when it starts.

You might wonder why the update method doesn’t retrieve the time value on its own. Efficiency is important when running a stack of simultaneous animations, or your sketch will bog down and perform poorly. The best practice is to retrieve the time value at the beginning of your main loop() then pass that value to each animated object to allow it to determine what it should do at that time value. This also gives the function calling the update methods some flexibility in allocating time among competing priorities (for example, favoring a high priority object over a low priority one). If the update methods are efficient everything runs smoothly.

Putting it All Together

Now that we have defined a CLASS, lets create some objects and see how it works.

To try this out, create a simple double LED circuit with an UNO and a breadboard, like so — I put the resistors on the cathode side as a matter of habit because I’m using common anode wiring everywhere; put them on the anode side of the LEDs if you prefer:

A simple double LED circuit on PWM pins 5 & 6. The 220Ω resistors are on the ground side.

Why two LEDs? We are going to further enhance the class by generating random values for the initial state and direction of the fire object. That, plus instantiating each object with slightly different parameters, guarantees that each fire instance starts and progresses differently. Once started, each instance will do its own thing.

Here’s the sketch (download from the github site):

///////////////////////////////////////
// CLASS fire
// A demonstration C++ class 
// for simulating a fire with LEDS
//
// Author: Robin Simonds, theNscaler.com
// License: CC BY-SA 4.0
// https://creativecommons.org/licenses/by-sa/4.0/
///////////////////////////////////////

class fire {
 private:
   int ledPin;
   int high; // maximum brightness 
   int low; // minimum brightness 
   int rate; // update frequency in milliseconds 
   bool dirUp; // true if brightness is increasing 
   int state;
   unsigned long lastUpdate; // in milliseconds; can be a big number

public: 
 fire(int pin, int zhigh, int zlow, int zrate){
   ledPin = pin;
   high = zhigh;
   low = zlow;
   rate = zrate;

   lastUpdate = 0;
   // seed the psuedo random number generator by
   // reading an unconnected analog pin
   randomSeed(analogRead(0));
   // randomize the starting state of the object
   state = random(low + 1, high);
   dirUp = random(2) == 1;
   analogWrite(ledPin, state);
 }
 // call the Update method frequently to run the animation
 void Update(unsigned long curMillis) {
   if(curMillis - lastUpdate >= rate){
     lastUpdate = curMillis;
     dirUp ? state++ : state--;
     if(random(80) == 1){ // a possible spark
       analogWrite(ledPin, 255);
     } else {
       analogWrite(ledPin, state);
     }
     if(state == high || state == low){
       dirUp = !dirUp; // boolean logic flip
     }
   }
 } 
};

// create global instances of the fire class
fire demo_fire1 = fire(6, 80, 20, 30);
fire demo_fire2 = fire(5, 75, 15, 35);

void setup() {
 // no setup currently needed
}

void loop() {
 unsigned long current_millis = millis();
 
 demo_fire1.Update(current_millis);
 demo_fire2.Update(current_millis);
}

When you run it, it should look something like this:

Want to do a two or three LED fire algorithm (maybe red with amber/yellow for more of flame effect)? Its just a matter of additional properties and additional instructions in the Update method to implement the algorithm. Give it a try if you are so inclined. You’ll see first hand how OOP can make code more reuseable and easier to modify. For animation with Arduino, OOP provides just the right framework to create and manipulate multiple animated objects of all kinds.

In part 2 of this post, we’ll tackle a common and more complex layout object: turnouts.

Basic Signaling for a Small Layout

Continuing with the theme of controlling a small layout with an UNO, I thought I’d accept my own challenge from the last post and talk about how one might implement signals on a small layout as I did on the Test Loop.

Signals on the Test Loop

While I was actively testing block occupancy detection on the test loop, I set up three sets of signals as part of that effort. I wanted to both test some off-the-shelf signals from Tomar and take a crack at building my own searchlight signals using BLMA unlit signal heads. The former turned out to work well, but because they are wired for common anode, they sent me on a quest to tame common anode wiring. The latter also came out well, once I learned how to reliably solder magnet wire to SMD micro LEDS!

Block and Signal layout on the Test Loop

Block and Signal layout on the Test Loop

I did this primarily to see the block detection system working (block detection is also shown on on my programmable control panel, but that is another story) as a train moves around the track.  You can see it in action in this video—note that only the locomotive is detectable by the block occupancy detection system; the rolling stock is not set up for detection.

Since the test loop is just an oval with a single turnout and siding, the system is fairly simple.  As you watch the train go around the oval you will see signals change state as the train moves in and out of blocks, and as the turnout changes state.  The logic is imperfect in a few cases but good enough to show the various parts of the system working as a whole under the control of an UNO, which was the point of the exercise.

A Framework for ABS

Automatic Block Signalling (ABS) is straight forward and prototypical for late nineteenth / early twentieth century railroads. ABS signals are autonomous and react to block occupancy and turnout status; the red (next block obstructed), yellow (a subsequent block is obstructed) and green (no obstruction) indicators are near universal. Typical implementations handle blocks in groups of three or four, depending on how far ahead the system sensors extend.

Because each signal is an autonomous object that responds to specific environmental conditions, ABS lends itself well to a data-oriented approach. As with turnouts in the previous post, the best starting point is to devise a data structure that will encapsulate and represent everything that needs to be known about each signal in your system. Here is what I came up with for the test loop (see this post for an explanation of my Duino nodes, and this post for the addressing system in use).

typedef struct SIGNAL_DEF {
  byte type; // bit mask indicating available signal aspects
             // bit 1 = Red; bit 2 = Green; bit 3 = Yellow)
             // 1=R; 2=G; 3=RG; 4=Y; 5=RY; 6=GY; 7=RGY
             // 3 and 7 are the two types on the test loop
  nodeAddress addr; // base address of the Duino node for this signal
  byte R_ID; // pin/bit id for red indication
  byte G_ID; // pin/bit id for green indication
  byte Y_ID; // pin/bit id for yellow indication
  
  // data elements for running the signal
  byte state; // current signal state
  T_ALIGN *turnouts; // these turnouts must be aligned as defined to get SIGNAL_GREEN
  byte numTurnouts;
  byte *following; // additional blocks ahead (depends on direction signal faces)
  // watched for occupancy resulting in SIGNAL_YELLOW caution   
  byte numFollowing; 
};

By now you should recognize that this is my preferred approach to dealing with complex, interactive objects in the system.  As always, I define a compound data structure (the structure contains other structures as elements, in this case the T_ALIGN and NODEADDRESS types)  to collect all relevant data for each signal. Feel free to reinvent any of this — the point is to collect all necessary data in one place for each signal.

“T_ALIGN *turnouts”  is an example of pointer notation which allows for an array of zero (empty array) or more of the respective types; the “numTurnouts” element indicates how many items the T_ALIGN array contains. The “byte *following” and “numFollowing” do the same thing for a list of subsequent block IDs that are watched for occupancy.

Here is the declaration of a signals array from the Test Loop encapsulating all the signals in use, using the data types discussed. Notice how structures and arrays within the SIGNAL_DEF structure are defined inside their own curly braces:

// Signals definitions and data
SIGNAL_DEF signals[NUM_SIGNALS] = {
  {3, {3, 0}, 0, 1, -1, SIGNAL_OFF,{0, ALIGN_DIVERGENT}, 1,{2}, 1 },
  {3, {3, 0}, 2, 3, -1, SIGNAL_OFF,{0, ALIGN_MAIN}, 1, {2}, 1 },
  {7, {1, 0}, 0, 1,  2, SIGNAL_OFF,{}, 0, {0}, 1},
  {7, {0, 0}, 0, 1,  2, SIGNAL_OFF,{0, ALIGN_DIVERGENT}, 1, {}, 0 },
  {3, {0, 0}, 3, 4, -1, SIGNAL_OFF,{0, ALIGN_MAIN}, 1,{2}, 1}
};

Signal logic is handled by one function that gets called at the end of each loop cycle on the UNO, after block occupancy has been established.

void refreshSignals() {
  // First pass, set Stop (RED) state; default is GREEN
  // from block occupancy or turnout states
  for(int i = 0; i < NUM_SIGNALS; i++){ // for each signal
    // default state
    int state = SIGNAL_GREEN;
    SIGNAL_DEF sig = signals[i];
    for(int j = 0; j < max(sig.numTurnouts, sig.numBlocks); j++){ 
      if(j < sig.numTurnouts){
        // if the turnout is in motion OR 
        // if turnout alignment does not equal the required alignment
        if(turnout[sig.turnouts[j].id].is_moving || 
            turnout[sig.turnouts[j].id].alignment != sig.turnouts[j].align){
         state = SIGNAL_RED;
        } 
      }
      if(j < sig.numBlocks){ // for each linked block in the SIGNAL_DEF
        if(blocks[sig.blocks[j]].occ){ // if occupied
          state = SIGNAL_RED;
        }
      }
    }
    setSignalBits(i, state);
  }
  
  // Second pass to set caution states on
  // signals that support it and are currently set to GREEN
  
  for(int i = 0; i < NUM_SIGNALS; i++){ // for each signal
    SIGNAL_DEF sig = signals[i];
    if(bitRead(sig.type, 2)){ // if the signal supports the caution state
      if(sig.numFollowing > 0 && sig.state == SIGNAL_GREEN){
        // check occupancy of following block(s) if any
        for(int j = 0; j < sig.numFollowing; j++){
          if(blocks[sig.following[j]].occ){
            setSignalBits(i, SIGNAL_YELLOW);
          }
        }
      }
    }
  }
  // Refresh the nodes to show signals in their updated state
  nodeRefresh();
}

void setSignalBits(int signalID, byte signalState) {
  SIGNAL_DEF sig = signals[signalID];
  if (sig.state != signalState) {
    signals[signalID].state = signalState;
    byte nodeBits = nodeGet(sig.addr);
    switch (signalState) {
      case SIGNAL_OFF:
        if(bitRead(sig.type, 0)) bitWrite(nodeBits, sig.R_ID, LOW);
        if(bitRead(sig.type, 1)) bitWrite(nodeBits, sig.G_ID, LOW);
        if(bitRead(sig.type, 2)) bitWrite(nodeBits, sig.Y_ID, LOW);
        break;
      case SIGNAL_RED:
        if(bitRead(sig.type, 0)) bitWrite(nodeBits, sig.R_ID, HIGH);
        if(bitRead(sig.type, 1)) bitWrite(nodeBits, sig.G_ID, LOW);
        if(bitRead(sig.type, 2)) bitWrite(nodeBits, sig.Y_ID, LOW);
        break;
      case SIGNAL_GREEN:
        if(bitRead(sig.type, 0)) bitWrite(nodeBits, sig.R_ID, LOW);
        if(bitRead(sig.type, 1)) bitWrite(nodeBits, sig.G_ID, HIGH);
        if(bitRead(sig.type, 2)) bitWrite(nodeBits, sig.Y_ID, LOW);
        break;
      case SIGNAL_YELLOW:
        if(bitRead(sig.type, 0)) bitWrite(nodeBits, sig.R_ID, LOW);
        if(bitRead(sig.type, 1)) bitWrite(nodeBits, sig.G_ID, LOW);
        if(bitRead(sig.type, 2)) bitWrite(nodeBits, sig.Y_ID, HIGH);
        break;
    }
    nodeSet(sig.addr, nodeBits);
  }
  return;
}

void setSignal(int signalID, byte signalState) {
  setSignalBits(signalID, signalState);
  nodeRefresh();
}

For more in-depth discussion of my Duino Node devices, node functions and how they are used,  see Adding Signals to the Test Loop and Adding Signals to the Test Loop Part 2.

The idea here is that the logic of the signal system is executed in the refreshSignals() function. That function, in turn, calls setSignalBits() to interface with the hardware, using the hardware specific Duino Node functions to  drive the hardware.

How to Integrate Signals into Your Small Layout

Adding signals to your small layout consists of two basic steps: 1) setup your signal hardware so that it can be turned on and off in some way; either by direct connection to your UNO or using a shift register chain along similar lines to what I do with Duino Nodes. The choice of common anode vs. common cathode wiring is yours to make, but will depend on how your signal gear is wired. 2) Integrate signal handling in the sketch using turnout state and (if you have it) block occupancy data. Call your signal logic function at the end of each iteration of the main loop, and let that function interact with the signal hardware. Your signal logic should be in one place, and should be written as an “abstraction” that doesn’t know how to change signal display, but relies on other hardware specific functions to do that job.

Tomar Signals

Tomar N Scale Signals

I consider signals to be the most basic form of animation you can add to your layout to bring it to life. Its a little bit of trouble, but I hope you can see its really not hard.  The advantage of the Arduino approach over a hardware/hardwired approach (eg, connecting signals to the outputs of a stand alone block occupancy device) is the flexibility you gain in implementing signals while keeping wiring to an absolute minimum. Adding Absolute Permissive Block signalling is just as matter of additional logic to the sketch for the stretch of track you are trying to protect. Even full CTC functionality can be readily supported by responding to messages from a CTC control panel or system.

You’ll be amazed that how much work a single UNO can actually do for you.

Running a Small Layout with an Uno

Arduino Uno R3

Arduino Uno R3

A reader on another thread had questions about running multiple turnouts on a small layout with an Uno. Primarily he wants to control turnouts with a control panel and feedback indicators.  I thought that his needs are pretty typical for small layouts, and would be a good example for others planning and building on a similar scale.

So this post is bottom-up exercise in planning and implementing single UNO control on a small layout. I hope it will be helpful in planning and building your layout.

The Requirements

The layout has 5 turnouts that will be controlled with push buttons. Control is toggle-style: each button push causes the turnout to toggle between positions. Turnouts are run by micro servos. The control panel will have a layout map with turnout push buttons and LED position indicators.

First Decisions

The UNO is the first board many of us encounter and is certainly up to the task of managing 5 turnouts and a control panel, with capacity to spare. There are other Arduino boards suitable for model railroading use; but the UNO is cost-effective and easy to work with.

That said, the issue you have to contend with eventually is the number of connections required to support all the devices and LEDS that will be attached to the microcontroller.

Arduino boards provide three different types of connections/pins:

  • Digital: digital pins are basic on/off binary connections. In output mode they are either fully on (emitting 5 volts, with a max current of 40 mA) or off. In input mode they read the incoming current as either HIGH (on) or LOW (off). When used as inputs, digital pins may need pull-up or pull-down resistors so that they will function correctly.
  • Digital PWM: some, but not all, digital pins are also capable of PWM – pulse width modulation.  The output of a PWM pin, if not fully on or off, is a series of pulses that cause the output to to on a percentage of the time.  The pulses are used to control servos and other PWM devices. PWM with LEDS lets you vary their brightness between 0 and 100%, supporting a variety of interesting lighting effects.
  • Analog: Analog pins are primarily used to read sensors that produce a variable output. Pin inputs are fed to the on-board ADC (analog-to-digital converter) for conversion to a number between 0 and 1024. Current and temperature sensors are example of analog devices read through analog pins / ADC. What you may not know about analog pins is that they can also be used as basic digital pins.

It is essential in early planning to determine all the devices to be connected to the microcontroller and the type of connection each requires to determine what connection issues you will need to resolve. In some cases, going to the UNO’s bigger cousin, the MEGA, will solve connection limits.  But even the MEGA has limits. In some cases it makes sense to use pin-multiplying techniques using external chips to drive banks of LEDS, servos or other devices, even when you otherwise have enough pins.

A Connection Plan

The UNO has 14 digital connections, numbered 0 to 13, of which 6 (pins 3, 5, 6, 9, 10, and 11 ) are PWM. It also has 6 analog connections that can be used as digital connections with digitalRead() and digitalWrite() when referenced in the sketch as A0 through A5. That is a grand total of 20 digital pins, of which 6 are PWM.

Of those, two pins are generally off limits on an UNO: digital pins 0 and 1. They are Serial RX and TX respectively, and should be avoided for other uses on any UNO where you expect to use the USB interface — that would be most of the time. On boards without the built-in USB, pins 0 and 1 are fair game.

For five servos we need five digital pins. Even though PWM is used to control servos, the Arduino servo library creates the necessary pulses on any digital pin. However, using the library disables PWM functionality on pins 9 & 10, so some of the UNO’s native PWM capacity is sacrificed in any case.

For the five turnout control buttons we need five basic digital pins. For the control panel indicators, assuming one LED for each leg of each turnout, we need 10 basic digital pins.

That means we need 20 connections; but with pins 0 & 1 reserved for the USB interface, we’re short 2 connections. Many would want to use bi-color red/green LEDS so the state of each leg is continuously shown. That would require 10 additional basic digital connections.

Need Connections? No Problem!

That’s OK. Its exactly what I expected to happen when I started in on this exercise. Both inputs and outputs can be multiplied with external chips and boards; multiplying the basic digital outputs is far and away the easiest to implement, because the circuits are simple and the compiler includes native software support without add-on libraries.

I would use shift registers to control all the control panel indicators. That reduces the connection load on the UNO to just 3 connections for all LED indicators, no matter how many you end up with. With that, and the 10 pins needed to read the buttons and control the servos, the total digital pins requirement is 13. That leaves 5 available pins, one analog and up to three digital PWM, for other uses.

There is a great tutorial on controlling LEDs with shift registers, and chaining multiple registers together, on the Arduino website. That tutorial is mandatory if you are unfamiliar with shift registers; the circuit(s) shown are what you will be using for control panel LED indicators. The balance of this post assumes the basic knowledge contained in that tutorial.

Build A Control Board

Use a shift register chain to run your panel indicators. I use a TI 74HC595 chips that I buy from DigiKey. At less than $.50 each in lots of 10, they are a bargain. $5.00 worth of chips supports 80 outputs. Its the cheapest way I know of to extend the capabilities of an Arduino and run a lot of low current devices.

Decide how many shift registers you need — the 74HC595 has 8 outputs — and create a board based on the tutorial circuits to hold the chips and provide connection points for your LEDS. A single LED for each leg of each turnout (to show which leg is open to traffic) will need 10 connections—use two shift registers (use the two-chip circuit in the tutorial). Add a third chip (after the tutorial you will know how to do that) and your connection count rises to 24, enough to support red/green bi-color LEDS on each turnout leg on the panel. With 4 connections left, who knows what else you might light up?

Each push button will need a 10kΩ pull-down resistor connecting its pin to ground.  The purpose of the pull-down resistor is to drain stray current and keep the pin LOW unless the button is pushed and full current flow is applied.  See the diagram below. The resistor should be placed as close to the pin as possible.

Servo Control by Button

Basic Servo Control by Button

Power Issues

While you can run one or two servos off the power supplied by the UNO’s +5 volt pin, more servos than that would exceed the board’s power handling capabilities. Accordingly, you will need to supply power to the servos from a separate power source. VERY IMPORTANT: the ground for the servo power source must be tied to UNO ground or the servos will not work correctly.

One Sketch To Run it All

Alright. The control panel is built and wired; the servos are installed (see this post for my latest methods) and connected to power and the UNO. What you need now is a sketch to run the control panel and the servos.

Here is a demonstration sketch to get you started:

//////////////////////////////////////////
//
// Small Layout turnout control
// Demonstration Sketch
//
//////////////////////////////////////////

#include <Servo.h> // include the Servo library

////////////////////////////////////////
//
// Definitions
//
////////////////////////////////////////

////////////////////////////////////////
// Basic parameters; adjust for your actual setup
#define NUMBER_OF_TURNOUTS 5
#define NUMBER_OF_SHIFT_REGISTERS 3
#define STEP_DELAY 70  // servo movement step delay, in milliseconds
///////////////////////////////////////

///////////////////////////////////////
// Data Structures
///////////////////////////////////////

//////////////////////////////////////
// TURNOUT_DEF holds all configuration
// information about turnouts and panel LEDS
//////////////////////////////////////
typedef struct TURNOUT_DEF {
  uint8_t button_pin; // Digital or analog pin for the button associated with this turnout
  uint8_t servo_pin; // Digital pin for the servo associated with this turnout
  int pos_main; // servo position for the MAIN leg, in degrees
  int pos_div; // servo position for the DIVERGENT leg, in degrees
  int panel_LED_main_green; // The position(s)of panel LEDS in the shift register chain
  int panel_LED_main_red; // Example assumes a bi-color (red/green) LED for each turnout leg
  int panel_LED_div_green; // modify these elements to reflect the actual LEDS you are using
  int panel_LED_div_red;
};

/////////////////////////////////////
// TURNOUT_DATA is wrapper structure holding
// both configuration and runtime data for turnout operation
/////////////////////////////////////
typedef struct TURNOUT_DATA {
  TURNOUT_DEF data; // configuration
  bool is_moving;
  byte alignment;
  int pos_now;
  int target_pos;
  unsigned long last_move;
};

// Alignment state values
#define ALIGN_NONE 0
#define ALIGN_MAIN  1
#define ALIGN_DIVERGENT 2


// pin ids for shift register chain controlling panel LEDS
#define LATCH_PIN 7
#define CLOCK_PIN 8
#define DATA_PIN 9

//////////////////////////////////////////
//
// Global variables
//
//////////////////////////////////////////

//////////////////////////////////////////
// TURNOUT_DATA Array
// * A0, A1, etc refer to analog pins which are used for buttons in this example
// * Replace pos_main (93) and pos_div (117) with real values for each turnout
// * LEDS are identified by their output position in the shift register chain;
// the identifier is a number between 0 and (NUMBER_OF_SHIFT_REGISTERS * 8) - 1. 
// Example assumes LEDS are connected to shift register outputs sequentially 
// from the first output of first register. You can connect LEDS to any output in
// any order; just set the identifiers accordingly.
//
// Only the TURNOUT_DEF part of the TURNOUT_DATA structure has to be initialized here; 
// The remaining elements are managed internally and are initialized automatically
//////////////////////////////////////////

TURNOUT_DATA turnouts[NUMBER_OF_TURNOUTS] = {
  {{A0, 2, 93, 117, 0, 1, 2, 3}},
  {{A1, 3, 93, 117, 4, 5, 6, 7}},
  {{A2, 4, 93, 117, 8, 9, 10, 11}},
  {{A3, 5, 93, 117, 12, 13, 14, 15}},
  {{A4, 6, 93, 117, 16, 17, 18, 19}}
};

// servo objects
Servo servos[NUMBER_OF_TURNOUTS];

// array to hold shift register state bytes
byte panel_LEDS[NUMBER_OF_SHIFT_REGISTERS];

void setup() 
{
  // Setup pins for shift register chain
  pinMode(LATCH_PIN, OUTPUT);
  pinMode(CLOCK_PIN, OUTPUT);
  pinMode(DATA_PIN, OUTPUT);
    
  // initialize each turnout 
  for(int i = 0; i < NUMBER_OF_TURNOUTS; i++){
    // attach the servo
    servos[i].attach(turnouts[i].data.servo_pin);
    // set the pin mode for the button pin
    pinMode(turnouts[i].data.button_pin, INPUT);
    // test and position the turnout by moving
    // to divergent then to main positions
    servos[i].write(turnouts[i].data.pos_div);
    turnouts[i].pos_now = turnouts[i].data.pos_div;
    setTurnout(i, ALIGN_MAIN);
    }
} // end of setup

void loop() 
{ 
  // get elapsed milliseconds at loop start
  unsigned long currentMillis = millis();

  // loop through the turnouts array
  for(int i = 0; i < NUMBER_OF_TURNOUTS; i++){
    if (turnouts[i].is_moving) {
      // if sufficient time has elapsed since the last move
      if ( (currentMillis - turnouts[i].last_move) >= STEP_DELAY ) {
        // move the turnout one degree
        turnouts[i].last_move = currentMillis;
        if (turnouts[i].pos_now < turnouts[i].target_pos) { // if the new angle is higher
          servos[i].write(++turnouts[i].pos_now);
        } else {  // otherwise the new angle is equal or lower
          if (turnouts[i].pos_now != turnouts[i].target_pos) { // not already at destination
            servos[i].write(--turnouts[i].pos_now);
          }
        }
      }
      // if target position reached, stop turnout motion
      if (turnouts[i].pos_now == turnouts[i].target_pos) {
        turnouts[i].is_moving = false;
        turnouts[i].last_move = 0;
        setIndicators(i);
      }
    } else {
      // if a turnout is NOT in motion, check to see if its button is pressed
      int button_state = digitalRead(turnouts[i].data.button_pin);
      if(button_state == HIGH){
        // toggle position
        if(turnouts[i].alignment == ALIGN_MAIN){
          setTurnout(i, ALIGN_DIVERGENT);
        } else {
          setTurnout(i, ALIGN_MAIN);
        }
      }
    }
  }
}// end of main loop

////////////////////////////////////////////////////////////////
// Supporting Functions
////////////////////////////////////////////////////////////////

void setTurnout(int id, int align){
    // Set indicators to show turnout in motion
    turnouts[id].alignment = ALIGN_NONE;
    setIndicators(id);
    // Set values to trigger motion on next loop iteration
    switch(align){
        case ALIGN_MAIN:
          turnouts[id].is_moving = true;
          turnouts[id].last_move = 0;
          turnouts[id].target_pos = turnouts[id].data.pos_main;
          turnouts[id].alignment = ALIGN_MAIN;
          break;
        case ALIGN_DIVERGENT:
          turnouts[id].is_moving = true;
          turnouts[id].last_move = 0;
          turnouts[id].target_pos = turnouts[id].data.pos_div;
          turnouts[id].alignment = ALIGN_DIVERGENT;
          break;
      }
}

void setIndicators(int id){
  switch(turnouts[id].alignment){
    case ALIGN_NONE: // means the turnout is in motion and not aligned
      panelWrite(turnouts[id].data.panel_LED_main_red, HIGH);
      panelWrite(turnouts[id].data.panel_LED_main_green, LOW);
      panelWrite(turnouts[id].data.panel_LED_div_red, HIGH);
      panelWrite(turnouts[id].data.panel_LED_div_green, LOW);
      break;
    case ALIGN_MAIN:
      panelWrite(turnouts[id].data.panel_LED_div_green, LOW);
      panelWrite(turnouts[id].data.panel_LED_div_red, HIGH);
      panelWrite(turnouts[id].data.panel_LED_main_green, HIGH);
      panelWrite(turnouts[id].data.panel_LED_main_red, LOW);
      break;
    case ALIGN_DIVERGENT:
      panelWrite(turnouts[id].data.panel_LED_div_green, HIGH);
      panelWrite(turnouts[id].data.panel_LED_div_red, LOW);
      panelWrite(turnouts[id].data.panel_LED_main_green, LOW);
      panelWrite(turnouts[id].data.panel_LED_main_red, HIGH);
      break;
  }
}
/////////////////////////////////////////////////
// Shift Register Functions
/////////////////////////////////////////////////
void panelWrite(int id, byte state) {
  int reg = floor(id / 8);
  int pos = id % 8;
  bitWrite(panel_LEDS[reg], pos, state);
  panelRefresh();
}

void panelRefresh(){
  // Prepare to shift by turning off the output
  digitalWrite(LATCH_PIN, LOW);
  // shift all bits out in MSB (most significant bit first) order
  for(int i = (NUMBER_OF_SHIFT_REGISTERS - 1); i>=0; i--) {
    // shift out the bits
    shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, panel_LEDS[i]);
  }
  // turn on the output to activate
  digitalWrite(LATCH_PIN, HIGH);
}

The sketch compiles into a compact package leaving plenty of memory and resources for additional functionality.

The main loop is built around a simple multitasking model, allowing you to control task timing and balance multiple competing tasks.  In this case, the main benefit of this methodology is control of the movement rate of turnout servos. On my test loop this methodology allows block occupancy detection to work in the background along with signal logic.

UP995 at Signal 34. Dual Searchlight Signals, Scratch Made with BLMA Signal Heads.

UP995 at Signals 3 & 4 on the Test Loop. Dual Searchlight Signals, Scratch Made with BLMA Signal Heads.

Troubleshooting

If you build the circuits accurately, everything should work out of the box (as it were). I am an obsessive tester — so I test things at critical stages as I build them.  Build and test your shift register circuits on a breadboard first, before soldering everything to a prototyping board.

To test the sketch I set up a simulation on a breadboard.

To test the sketch for this post I set up a 3 servo simulation on a breadboard.

Getting the connections to the UNO correct is key. If the LEDs don’t light at all, check all three connections, plus power connections; LEDS are polarized, so make sure the anodes are connected to incoming power and the cathodes to ground. Tracing power flow with a multi-tester should help you find problem areas quickly.

If your LEDs light but alternately flash odd/even, you probably reversed the LATCH_PIN and CLOCK_PIN connections.

For 3 or more servos, you must have an independent power supply to power them. BE SURE TO CONNECT GROUND FROM THE SERVO POWER SUPPLY TO ARDUINO GROUND.

Do Something with Unused Outputs

Old style hard wiring, without the help of a microcontroller, is a messy business at best, which tends to limit what you can do. DCC can help bridge the gap, but layout control is something of an afterthought for DCC (it was designed for locomotive control only) and is, frankly, awkward to use (my opinion; your mileage may vary).

To get the kind of functionality we have here without Arduinos — turnout control with a synchronous control panel — it would have to be hard wired. Or you could connect a full computer and run JMRI; that would require DCC (plus stationary decoders for the turnouts and a USB->DCC interface) to work . You would have to use stall-motor type turnout motors or add microcontrollers (Peco now sells a “smart” turnout system using servos and custom controllers) to interface with servo motors. So if you want to use servos instead of stall-motors, you can go Peco or do it the Arduino way from the start. As much as I appreciate Peco’s efforts here, the openness of the Arduino platform is a big part of what makes it useful and cost-effective.

Adding another function to a small layout — lets say a signal to protect a turnout — is a matter of connecting the signal leads to unused outlets and writing some code to run it.  If you’ve gotten this far, you’ll spend far more time thinking about exactly how the signal should run in conjunction with the turnout it is protecting, than you will connecting the device and adding code to the sketch.  Once you get into this way to doing things, I guarantee you’ll never look back.

So what will it be? Signals?  A fire simulation (using PWM and LEDS)? A lighted structure? Let your imagination run wild!


UPDATE

I starting working with shift registers a while back.  They are so effective my habit is to always use shift registers with LEDs, such as for signals, structure lighting and control panel applications.  The habit is so ingrained that I forget to mention the other important reason for using them: power management.

The power handling capability of an UNO is limited to 40 mA per pin and 400 mA for the entire board. LEDs typically draw between 20 and 30 mA.  So if you are directly powering 20 LEDs from an UNO, you are exceeding its power handling capacity and will burn out the board.

In the example given in the post, no more than half the LEDs well be lit at any one time so we are unlikely to exceed the 400 mA limit overall. A few more lit LEDS, though, would put it over the top.  Shift registers duck the problem altogether because each register is powered—and supplies power to connected devices—independently.  Other than the total load on your power supply, adding devices/lights to shift registers or even adding additional registers, puts no significant power load on the UNO.

Above I mentioned that an independent power supply is needed to run the servos.  From a “best practices” perspective, one should always power external devices with an independent power supply. Arduino’s current handling limits are tight; always using external power supplies avoids that problem.


UPDATE

Several readers asked if I have a fritzing image of the 3 servo demo.  I did not, but I put one together for those who might find this helpful.

3 Servo Demo

This circuit is intended to work with the above sketch, with the turnouts variable shortened to 3 elements instead of 5:

TURNOUT_DATA turnouts[NUMBER_OF_TURNOUTS] = {
  {{A0, 2, 93, 117, 0, 1, 2, 3}},
  {{A1, 3, 93, 117, 4, 5, 6, 7}},
  {{A2, 4, 93, 117, 8, 9, 10, 11}}
};

Always cross check all connections on a circuit like this against appropriate data sheets before powering it up for the first time! Be sure to connect the ground of your Arduino to the ground of your external power supply.

Adding Signals to the Test Loop, Part 2

Using shift register/darlington driver nodes for attaching lighting (and other devices) to the layout is easily justified by the conservation of precious Arduino pins. But that’s not necessarily the most important benefit of this approach.

Duino node for station lighting.

Duino nodes for station signal and lighting on the Test Loop.

The principal advantage of organizing lighting control into chains of shift registers—with or without Darlington Drivers to sink instead of source current (at this level it doesn’t matter what the shift register is attached to)—is to create an addressable architecture for lighting and animation.

What do I mean?

An addressable architecture is a system where individual objects can be identified by an “address” that provides sufficient information, in proper context, to allow the object to be accessed (my definition). An “address” should be a direct access mechanism and should not require conversion to a different form in order to use it. However, an address does not necessarily have to be complete if the context within which it is used has “knowledge” of the missing address information.

By that definition a pin number is a valid address. But, since we want to control more objects that we have pins, we need to think on a larger scale.

Arrays

Going beyond pin numbers, an address could be an offset (index) into an array. This works for objects attached to a shift register because you need to have memory set aside to represent state of the shift register; the values in memory are used to generate the stream of bits that sets the shift register state.

An 8-bit shift register could be represented in memory as an array of 8 bytes, either as plain 8-bit values or as boolean values (boolean is the better idea), occupying one byte for each bit on the register:

byte MyRegister[8];
or
bool MyRegister[8];

Having done that, every object on the shift register can be referred to by its position in the MyRegister array, a number from 0 to 7.  When you want to know or change the state of a particular bit on the shift register, you access the bit via its array index, making the index an address.

What about the Pins?

I am cheating in a sense. I’m relying on the fact that the first shift register in a chain has to be attached to three pins, you need to know which pin is which, and we can embed that information in the method that pushes bits out to the shift register. With that housekeeping squared away, all we need to do to turn shift register outputs on or off is maintain the array representing the shift register in memory, using the array indices as addresses for specific objects.

Here is an example method (derived from the Arduino Shift-Out tutorials, and used in the Roundhouse to run stall lamps) that writes to shift registers, using a static byte array to hold the register state information:

const int clockPIN = 3;
const int latchPIN = 4;
const int dataPIN = 5;
void registerWrite(int output, int state) {
  static byte outputStates[8];
  byte bitsToSend = 0;

  // update the outputStates array
  outputStates[output] = state;
  
 // Set bits according to the outputStates Array
 for(int i = 0; i < 8; i++){
     bitWrite(bitsToSend, i, outputStates[i]);
 } 
 // turn off the output while shifting bits
 digitalWrite(latchPIN, LOW);
 // shift the bits out
 shiftOut(dataPIN, clockPIN, MSBFIRST, bitsToSend);
 // turn on the output
 digitalWrite(latchPIN, HIGH);
}

This is what I mean by proper context. Here the context is a known “base address” for the shift register that never changes: the LatchPin, ClockPin and DataPin used to access it. Once context is established with a method that knows the “base address” and writes to the shift register, we no longer need to worry about that aspect of the problem and can supply the unique part of the address — the position of the output we wish to change — as an argument to the registerWrite method. In this instance, the address supplied to the method should be considered a relative address, because it is relative to the static portion of the address stored in the method code.

This simple approach works great for a shift register or two, but gets unwieldy when trying to manage the state of multiple shift register outputs with a single array. Further, memory is a precious resource and using a byte of memory to represent the state of a single bit on a shift register is inefficient at best. For a layout with hundreds of controllable lights for signals, crossings, street lights, building lighting and whatever else you can think of, something more sophisticated is called for.

The Data

So lets start with a better way to hold the values of the 8 bits of a shift register; I call shift registers “nodes”:

typedef struct nodeState {
  byte pins;
};

The 8 bits constituting a single byte of memory represent the 8 bits of a node. This is a big improvement in both memory efficiency and execution efficiency, since the pins byte can be sent directly to the target register without any processing.

Next we need a type to hold all information relevant to a chain of nodes:

typedef struct nodeGroup {
  byte clockPin;
  byte latchPin;
  byte dataPin;
  byte numNodes;
  nodeState *nodes;
};

Now we have all the data we need to handle a chain of shift register nodes in one place. Notice how the nodeState type is used. Once instantiated,  the *nodes element will point to an array of bytes, one for each node.

Notice something else: the door is now open to run multiple, distinct chains of nodes easily by creating a nodeGroup data instance for each chain. This confers even more freedom to structure resources for the convenience and logic of the sketch.

What’s The Address?

So that is how the nodes are attached and accessed. Now we’re ready for an address form that will work with this architecture:

typedef struct nodeAddress { 
  byte chain;
  byte node;
  byte pin;
};

With all node data organized into nodeGroup structures, the nodeAddress type provides what amounts to an absolute address to any resource defined in this way. An absolute address contains all necessary parts of an address, and is processed by methods without having any hidden address elements embedded in the method code.

Station and Signal 5

Practical Application

The test loop has one chain of four nodes.  For that sketch, a single node group is instantiated this way:

nodeState ndata0[] = {0,0,0,0};
nodeGroup nodeGroups[] = {{7, 6, 5, 4, ndata0}};

Here I’ve defined an array of nodeGroups with one member. The node group uses pins 5, 6 and 7 on the Uno, and has four member nodes. An array of nodeState structures, one for each node, is defined as ndata0[], and incorporated into the nodeGroup via the ndata0 pointer.

Signals on the test loop are defined by a different data structure that includes this element

nodeAddress addr;

capturing the base address of a particular signal. When the sketch is ready to set the state of a signal, it passes the nodeAddress to the node setting method, like this:

nodeSet(signal.addr, nodeBits);

All manipulation of the nodes can be accomplished with 4 methods:

void nodeWrite(struct nodeAddress addr, byte state) {
  // write the state to the pin bit defined by nodeAddress addr
  bitWrite(nodeGroups[addr.chain].nodes[addr.node].pins, addr.pin, state);
  nodeRefresh();
}
void nodeSet(struct nodeAddress addr, byte state) {
  // set the pins element with a byte value
  nodeGroups[addr.chain].nodes[addr.node].pins = state;
  nodeRefresh();
}
byte nodeGet(struct nodeAddress addr){
  // get the pins element for a node
  return nodeGroups[addr.chain].nodes[addr.node].pins;
}
void nodeRefresh(){
  // Shift out current node data, one node group at a time
  for(int i = (NODE_GROUPS - 1); i>=0; i--) {
    // shift all bits out in MSB order (last node first):
    // Prepare to shift by turning off the output
    digitalWrite(nodeGroups[i].latchPin, LOW);
    // for each node in the group
    for(int j = (nodeGroups[i].numNodes - 1); j>=0; j--) {
      shiftOut(nodeGroups[i].dataPin, nodeGroups[i].clockPin, MSBFIRST, nodeGroups[i].nodes[j].pins);
    }
    // turn on the output to activate
    digitalWrite(nodeGroups[i].latchPin, HIGH);
  }
}

The nodeSet method used by the signal system—primarily because a signal requires an output for each aspect (red, green, yellow)—sets the pins byte for a random node in one operation.

NodeWrite sets a single output bit on any random node.

NodeGet retrieves the current pin byte for a random node. The signal system uses this method to retrieve the bits on a signal node, before modifying the values and applying them with nodeSet.

NodeRefresh writes the pins bytes to the nodes.

There is, of course, more going on with the signals on the test loop. Deciding what aspect a signal should show at any given time is a matter of logic and data. In the next post in this series I’ll do a deep dive into my first take on making signals operational within an Arduino environment.

Until then, happy railroading!

Block Occupancy Detection for DC and DCC, Part 3

In Part 1 of this series I demonstrated the basics of current sensing with ACS712 sensors and an Arduino microcontroller. In Part 2, I demonstrated the test loop wired for four blocks, occupancy detection and signals.

In this post I’ll put on my programmer’s hat and talk about the code behind block occupancy detection on the test loop in Part 2. Some is an improved version of code used in Block Occupancy Detection for DC and DCC, Part 1.  The rest is new; and I’m scaling from a single to multiple detectors. I’ve also changed the way I calculate the occupancy threshold so that it is calibrated for each sensor.

ACS712 Basics

Current Sensors and Block Feeder Connections.

ACS712 Current Sensors and Block Feeder Connections on the Test Loop.

The basic concepts for working with an ACS712 sensor board are discussed in  Block Occupancy Detection for DC and DCC, Part 1, but lets review them here:

–The ACS712 sensor senses current flow regardless of polarity up to its rated maximum (5, 20 or 30 amps). That means that it can be used with both DC and AC current. DCC track power is an AC type wave form alternating polarity at about 8 kHz. Raw sensor readings are noisy, requiring some mathematical processing to be useful.

–For maximum accuracy sensors have to be calibrated at start up. This is a software process by which you determine the value produced by each sensor when there is no current present, the average quiescent voltage also called adc_zero. In addition, I now calculate the average quiescent current reading  (AQC) at adc_zero; this value is used to calculate the occupancy detection threshold as explained below.

–The current detected by a sensor is represented by the difference between the reading and adc_zero for that sensor, converted to milliamps by the sensitivity factor for the version of the chip in use. Here’s the formula:

CURRENT (in mA) = (RAW_VALUE  – ADC_ZERO) / SENSITIVITY

–Accurately measuring an AC waveform requires multiple readings over a defined sampling period, using a sampling interval that is shorter than the frequency of the current you are trying to measure in order to sample the entire cycle. The Root Mean Square of the data set is calculated to arrive at an rms current reading.

Determining Occupancy

The heart of the occupancy detection system is a function to read current from a sensor and return an rms current value. This function is intended to be called once for each sensor during the detection cycle, so the target sensor’s pin and adc_zero are passed as arguments.

float readCurrent(int PIN, float adc_zero)
{
  float currentAcc = 0;
  unsigned int count = 0;
  unsigned long prevMicros = micros() - sampleInterval;
  while (count < numSamples)
  {
    if (micros() - prevMicros >= sampleInterval)
    {
      float adc_raw = (float) analogRead(PIN) - adc_zero;
      // convert to amperes
      adc_raw /= SENSITIVITY;
      // accumulate the sum of the squares of the readings
      currentAcc += (adc_raw * adc_raw);
      ++count;
      prevMicros += sampleInterval;
    }
  }
  //https://en.wikipedia.org/wiki/Root_mean_square
  // square root of the sum of the squares / number of samples
  float rms = sqrt((float)currentAcc / (float)numSamples);
  return rms;
}

During the detection cycle, readCurrent() is called by another function that determines if the block is occupied by comparing the rms current reading to a threshold value, returning either true or false.

bool isOccupied(int block, float adc_zero, float threshold) {
  float current = readCurrent(block, adc_zero);
  return (current > threshold);
}

Sampling Parameters

Here are the key sampling parameters I’m currently using.

// constant variables for block occupancy detection

// sample over 58 ms; 100 times the nominal pulse width for binary 1 in DCC
// sample time is expressed in microseconds
const unsigned long sampleTime = 58000UL;

// number of samples taken during a read cycle 
const unsigned long numSamples = 200UL;

// ADC conversion time is about 50µs (+/-)
// the sampling interval-here 290µs-should be longer than then ADC conversion time 
const unsigned long sampleInterval = sampleTime / numSamples;

I originally started with 100 ms sample time, then later realized I could shorten it, which helped compensate for the time required for two step detection as described below. I’ve only just begun to play with the variables and see how time efficient I can make the detection process.

Managing Block Data

I should back up a bit and explain my data handling strategy.  I represent each block with this data structure:

typedef struct BLOCK_DEF {
  int pin;
  int aqv;
  float aqc;
  bool occ;
  bool chg_pending;
};

Each block is a sensor, so the first element is its pin assignment, and the next two elements (aqv & aqc) of the structure are the calibration data for the sensor calculated at startup.  The fourth element, occ is the current occupancy state of the block (true if occupied, otherwise false). The last element indicates whether a change in occupancy state has been detected but not finalized (true or false).

The test loop sketch defines an array of BLOCK_DEF structures for the four blocks, initializing the array with the pin numbers and starting values for the remaining elements; An additional BLOCK_DEF structure is defined and initialized for for the master current sensor:

BLOCK_DEF blocks[NUM_BLOCKS] = {
    {0, 0, 0, false, false},
    {1, 0,0, false, false},
    {2, 0, 0, false, false},
    {3, 0, 0, false, false}}; 
    
BLOCK_DEF master = {4, 0, 0, false, false};

Calibration

My calibration routine has evolved to require two functions. The first is the same as in the original demonstration; the second function is new. Remember that track power must be OFF during calibration.

int determineVQ(int PIN) {
  float VQ = 0;
  //read a large number of samples to stabilize value
  for (int i = 0; i < CALIBRATION_READS; i++) {
    VQ += analogRead(PIN);
    delayMicroseconds(100);
  }
  VQ /= CALIBRATION_READS;
  return int(VQ);
}

float determineCQ(int pin, float aqv) {
  float CQ = 0;
  // set reps so the total actual analog reads == CALIBRATION_READS
  int reps = (CALIBRATION_READS / numSamples);
  for (int i = 0; i < reps; i++) {
    CQ += readCurrent(pin, aqv);
  }
  CQ /= reps;
  return CQ;
}

The first function determines the quiescent voltage, adc_zero, which is saved in the aqv element of the BLOCK_DEF structure. With adc_zero calculated, the second function determines the average quiescent current reading at adc_zer0, which is saved in the aqc element of the BLOCK_DEF data structure.

Here’s the calibration procedure from setup():

  master.aqv = determineVQ(master.pin);
  master.aqc = determineCQ(master.pin, master.aqv);
  
  for (i = 0; i < NUM_BLOCKS; i++) { // for each block 
    blocks[i].aqv = determineVQ(blocks[i].pin);
    blocks[i].aqc = determineCQ(blocks[i].pin, blocks[i].aqv);
  }

Why the second step? Because even though there is no current to be sensed, the sensor still produces a small reading. The sensors on the test loop produce an AQC reading from 9 to 24 mA. By capturing the average quiescent current reading, the detection threshold can be automatically adjusted for each sensor.

Instead of using a hard current threshold (like .0259, about 26 mA, as I did in the first demo), I now use a multiplier that I apply to the AQC reading for each sensor to set the detection threshold for that sensor.

Detection sensitivity is controlled by the multiplier. The multiplier I used during the demo was 1.5, which produces satisfactory results on the test loop, but is insensitive to low power devices.

Set the multiplier too low, and block states will flicker because of the variance in sensor readings. On the test loop I’m inhibited because of the Atlas bumper light and its draw on track power on block 3. I’m able to bring the multiplier down to 1.095; below that block 3 flickers mercilessly.  But the other blocks do not, so I think even lower multipliers and higher sensitivities are possible. I’ll find out on the L&NC.

The Detection Cycle

A detection cycle occurs on every iteration of the main loop. The first step is to check the master power. If the master power is on, then each block is checked in turn. If a block’s occupancy state is changed during the cycle, a notification is sent out to “subscribers,” other devices that have asked to receive block occupancy data. That is how my control panel gets updated.

  power_state = isOccupied(master.pin, master.aqv, 
           master.aqc * DETECTION_MULTIPLIER, master.chg_pending);
  if(power_state != master.occ) {
    if(master.chg_pending) {
      master.occ = power_state;
      master.chg_pending = false;
    } else {
      master.chg_pending = true;
    }
  } else {
    master.chg_pending = false;
  }
  if(master.occ){
    for (int i = 0; i < NUM_BLOCKS; i++) {
      block_state = isOccupied(blocks[i].pin, blocks[i].aqv, 
               blocks[i].aqc * DETECTION_MULTIPLIER, false);
      if (block_state != blocks[i].occ) { // if occupancy state has changed
      // if a pending change has been previously detected
      if(blocks[i].chg_pending && !master.chg_pending) {
           blocks[i].occ = block_state;
           blocks[i].chg_pending = false;
           String id = String(i);
           notifySubscribers(1, id, String(block_state));
         } else { // initial change detection
           blocks[i].chg_pending = true;
         }
       } else {
         blocks[i].chg_pending = false;       
       }
    } 
  }

As stable as the current sensing system is now, I have found that when a locomotive is crossing a block boundary and drawing from two blocks you can get some jitter causing the newly occupied block’s state to flicker.

My current (pardon the pun) solution is to implement a system rule requiring 2 consecutive readings to confirm a block state change.  So, the first time a state change is detected, the block (or the master) is marked pending. On the next cycle, if the state change is detected again, the state change is accepted. Otherwise the pending flag is reset. The additional time lag in detection this creates is unnoticeable, and it completely eliminates detection jitter.

What’s Next?

Dual Searchlight Signals, Scratch Made with BLMA Signal Heads.

Dual Searchlight Signals, Scratch Made with BLMA Signal Heads.

The whole point of block occupancy detection is to put that data to use in some way. The most basic use for it is to run signals, which I used on the test loop as a way of showing occupancy detection in action. That can be done through JMRI, or as part of the layout’s intrinsic Arduino processes which is they way I’m approaching it.

I think of signals as the simplest form of animation a layout can and should have. As you can see from the demo, signals done the Arduino way are equally functional in a DC or DCC environment. It simply doesn’t matter how you run your trains: if you want signals then you should have them!

But, as always, there are unique complications that arise when doing things the Arduino way, not the least of which is the finite number of pins available to run lights or other external devices.

Here’s a hint: running signals the Arduino way works best when the signals are tied together in a physical and logical chain.

That, fellow railroaders, is the subject for another post.

 

A Programmable Layout Controller

Programming an Arduino to run turnouts, lights or animation on the layout is only part of the challenge. The other part is how do you control the board and tell it what you want it to do?

Servo Control with LED Feedback

Servo Control with LED Feedback

From an Arduino point of view, any sensor attached to a pin can trigger action in a sketch. As shown in Turnout Control with Arduino & Servos, mechanical buttons and switches can be attached to pins to tell the board what to do. In the example circuit, a single button triggers servo action. If you want to include feedback indicators as in this example circuit — these could be layout signals or panel indicators — you can hard-wire everything together to the same Arduino board.

Until you run out of pins

Pin management is critical as you ask the Arduino to do more and more. Every new sensor or triggering device consumes pins (as does every new actuator or output device). While learning what I could do with an Arduino on the layout, I realized that I needed get beyond the hardwired controls used in experiments and demos to a generic, software-based control system. To do that I was going to have to network everything together.

Networking Arduinos

Uno with Ethernet Shield

Uno with Ethernet Shield

In Roundhouse Rebuild Part 2 I mentioned, without explanation, that I was using Ethernet, and went on to discuss the evolving Simple Network Command System. I decided to go with wired Ethernet because of the easy availability of inexpensive Ethernet shields based on the WIZnet W5100 Ethernet Controller chip (under 10 dollars per shield), and an easy to use Arduino library included in the IDE. It is as close to plug & play as networking gets on an Arduino. The only additional equipment required are one or more inexpensive 10/100 switches (for example: TP-LINK TL-SF1005D 5-port 10/100Mbps Desktop Switch; don’t get gigabit switches to work with these shields, you’re just asking for trouble) to  interconnect the devices. I use a per-device assigned address system which helps keep the equipment roster simple (no router or DHCP required).

Why not just use the Digital Command Control system for the Arduino net? The short answer is that while it is clearly doable, for the purposes of this project I am going to keep the Arduino net separate because:

  1. Everything I do here has to work for both DC and DCC layouts. I own both DCC and unconverted DC locomotives; the layout has to work the same in either mode.
  2. Compared to conventional networking, DCC is a relatively difficult way to conduct bidirectional communications between Arduino boards.
  3. Keeping them separate does not preclude enabling communication between the two systems down the road.

If you want to pursue DCC communication and Arduino, the Model Railroading with Arduino site is a great place to start. The biggest impediment for most modelers will be the lack of commercial interface hardware to connect an Arduino to either track power or the command bus (although the circuits are easy enough to build); the closest commercial solution would be to use a USB interface, like the Digitrax PR3XTRA USB Programmer, RR-CirKits LocoBuffer-USB or the SPROG II USB, to tap into the DCC command system just as you would with JMRI.

My ultimate goal is to build the layout’s electronic and mechanical foundation around a network of Arduino boards. For communication among Arduino boards, Ethernet makes the most sense right now because it is the most “frictionless” route to achieve my goals (a wireless form would be even better, but would be a little more difficult to implement, so I’m holding that option for the future); communication between the Arduino net and the DCC system is a topic for the future—and the possibilities go way beyond treating Arduinos as decoders.

Building The Controller

2050-04

Adafruit 3.5″ TFT screen displaying a bitmap.

The concept for a prototype controller was simple enough: start with an Uno, add an Ethernet shield and add a small touchscreen for display and user input. Put it all in a box with an Ethernet jack, a USB jack and power connector. Software generates screen displays, interprets touches and communicates with devices it is controlling.

For the screen I chose the Adafruit 3.5″ TFT Touchscreen, seen here attached to an Uno via a breadboard (NB: The wiring shown is the minimum required to run the screen; the touch overlay and the SD Card reader require additional connections). It is capable of full 16bit color with a resolution of 320 x 480 pixels. The Adafruit library provides basic graphic primitive functions, basic text functions and bitmap functions allowing image display. It has a resistive touch overlay. Adafruit has an excellent tutorial on using this screen with their library.

Back of 3.8" TFT Screen

Back of 3.5″ TFT Touchscreen

The screen comes with a choice of interfaces: you can use the SPI bus interface in order to use the fewest pins on your Arduino, or you can devote more pins to use the faster 8 bit interface. You select the interface and solder the header pins on the appropriate side.  A  solder jumper on the back determines which interface is active; the decision is reversible. An SD Card reader is included for convenient storage of bitmap files.

On an Uno, the Ethernet shield dictates that the TFT screen has to be run via SPI; there aren’t enough pins otherwise. The application does not require the SD Card Reader so I don’t connect it to the UNO.

I fabricated a wiring harness for attaching the screen to the Uno\Ethernet combo, then mounted everything in a Radio Shack project box as shown below.

Wiring Harness

Wiring Harness

The connectors on the wiring harness are male or female PCB Headers; I solder the wires to the PCB side of the fittings, then cover each connection with heat shrink tubing. White wires connect to digital pins 7 through 13 (except 10, which is reserved for the Ethernet shield) and are for the TFT interface. Green wires are for the touch overlay and connect to Analog pins A2 – A5. Red supplies 5v, and black ground, to the TFT screen. The Ethernet extension cable and the USB extension cable both came from Adafruit.

Inside the Controller

Inside the Controller

Controller with Screen Wiring Attached

Controller with Screen Wiring Attached

 

 

 

 

 

 

 

Here it is in operation:

The Programmable Controller

The Programmable Controller

 

 

 

 

 

 

You may have guessed the fan ( on the left side ) was an afterthought. The cheap Ethernet shields I use are heat sensitive; they will crash when put in a confined space with poor air circulation.  Out in the open no problem; in a box, its a problem. Found that out the hard way. So I added a little fan to pull the air through the box (if you look closely, you’ll see there are holes around the bottom); works fine if noisily. Obviously, I will plan for air circulation when I build the main layout control panel. Such is prototyping!

What it Does

The controller sketch displays menus with buttons that, when touched, will cause the controller to either go to a different menu or send a command packet to the target device. Command packets are strings, formatted thus: function / option / data. For more about my protocol and the network polling process, see the Simple Network Command System section near the end of Roundhouse Rebuild Part 2.

The Main Menu provides access to sub-menus that I’ve created to support parts of the project.

Controller Main Menu

Controller Main Menu

All menus are built with buttons. A structure type called button_t holds button data:

typedef struct {
  int x;
  int y;
  int txtX;
  int txt;
} button_t;

X and y are the coordinates of the upper left corner of the button; the width and height are the same for all buttons in this version of the system. txtX is the x coordinate for the button text; the y coordinate is calculated and there is no text centering function. Finally, txt is an offset into a button_labels array pointing to the button text.

For the main menu, the button set definition looks like this:

const button_t buttons_main[SIZE_MAIN_SET] = {
  {90, 80, 115, 0 },
  {250, 80, 254, 1},
  {175, 140, 185, 16}};

Determining if a button has been touched is fairly straight forward. The coordinates of a touch p are compared to each button, as b, in the current set to see if it is on or within the button boundaries.

p.x >= b.x && p.x <= (b.x + BUTTON_WIDTH) && p.y >= b.y && p.y <= (b.y + BUTTON_HEIGHT)

Whacking My Head on the Memory Ceiling

The graphics libraries contain a lot of code. With the newest Arduino IDE, the controller sketch compiles to 27,030 bytes, about 83% of available program space; it was about 29k bytes with the previous IDE.

That is still tight enough that I cannot include SD Card access and a function to draw a bitmap from a file without going 15% over the absolute memory limit for an UNO. In the future I’ll use an Arduino MEGA 2560 Board instead of an UNO for control panel applications because of its vastly superior memory resources (and it has a lot more pins to work with). The remaining 17% with the current sketch gives me plenty of room for now.

The trickier bit of memory management is “dynamic memory,” which (on an UNO) is 2,048 bytes of shared memory space used for local variables. Local variables are created when functions are called and destroyed when they are exited. Global variables–variables declared outside of any function that are always in scope and available wherever you are in your sketch–are also stored in the same space. Global variables reduce the amount of dynamic memory available for local variables and, if not managed, can strangle your sketch.

Fortunately, the majority of global variables turn out to be constants — unchanging values or text used by the application. This kind of data can be stored in the program space instead of dynamic memory; the limitations are that

  • you can’t change the value stored in program space while the sketch is running, and
  • you have to copy a value from program space to dynamic memory in order to use it.

The PROGMEM keyword is used to tell the compiler to store something in program space instead of dynamic memory. To park menu titles and button text in program space, I did this:

const char mstr_0[] PROGMEM = "Main Menu";
const char mstr_1[] PROGMEM = "Lighting Menu";
const char mstr_2[] PROGMEM = "Roundhouse Menu";
const char mstr_3[] PROGMEM = "Test Loop Menu";

const char* const menus[] PROGMEM = {mstr_0, mstr_1, mstr_2, mstr_3};

const char str_0[] PROGMEM = "Lights";
const char str_1[] PROGMEM = "Roundhouse";
const char str_2[] PROGMEM = "<-Back";
const char str_3[] PROGMEM = "  Night";
const char str_4[] PROGMEM = "   Day";
const char str_5[] PROGMEM = " Mid-Day";
const char str_6[] PROGMEM = " Sunrise";
const char str_7[] PROGMEM = " Sunset";
const char str_8[] PROGMEM = "   Low";
const char str_9[] PROGMEM = "  High";
const char str_10[] PROGMEM = " Stall 1";
const char str_11[] PROGMEM = " Stall 2";
const char str_12[] PROGMEM = " Stall 3";
const char str_13[] PROGMEM = " Stall 4";
const char str_14[] PROGMEM = " Stall 5";
const char str_15[] PROGMEM = "Afternoon";
const char str_16[] PROGMEM = "Test Loop";
const char str_17[] PROGMEM = "Main";
const char str_18[] PROGMEM = "Siding";
const char str_19[] PROGMEM = "Occupancy";

const char* const button_labels[] PROGMEM = {str_0, str_1, str_2, str_3,
 str_4, str_5, str_6, str_7, str_8, str_9, str_10, str_11, str_12,
 str_13, str_14, str_15, str_16, str_17, str_18, str_19};

Copying the title of the main menu into a local variable text looks like this:

strcpy_P(text, (char*)pgm_read_word((&menus[0])));

For getting the button labels:

 strcpy_P(text, (char*)pgm_read_word((&button_labels[b.txt])));

An alternate way to store static data in program memory is to use the F macro, as in this declaration of a local variable that initializes with a static value that is stored in and retrieved from program memory:

String readyStr = F("Ready");

At this point I find it useful to make it a habit to use these tools in all sketches to tame dynamic memory space. Currently the controller sketch uses only 771 bytes or 37% of dynamic memory for global variables, leaving plenty of space for locals.

Menus

The Lighting and Roundhouse menus look like this:

lighting_menu

Lighting Menu

 

Roundhouse Menu

Roundhouse Menu

These are the controls I used off screen to control lighting when making the Roundhouse demo video. Overhead lighting was supplied by 4 led light bars (152 RGB ALEDS total) controlled by a networked UNO.


I’ve been busy at the test loop trying out various ideas.  Turnout control, signals and block occupancy detection (I have a method that works for both DC and DCC layouts), all play a part in the next step toward the layout. I’ll leave you to ponder the test loop menu until next time.

Test Loop Controls

Test Loop Controls

 


 


 

Roundhouse Rebuild Part 2 – The Sketch

Completed roundhouse close up exterior 1The sketch that currently runs the Roundhouse lighting (it will become part of a larger Yard & Turntable sketch that will be developed down the road) includes the ALED control methods discussed in An Introduction to Arduino & Addressable RGB LEDs, so I won’t discuss that part at length here. What’s new is the use of a shift register to control multiple outputs and the introduction of Ethernet networking to receive commands from a separate control device.

First, the included libraries:

#include <Adafruit_NeoPixel.h>
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#include <EEPROM.h>
#include <SystemCommon.h>

SPI.h is required for all spi bus functionality, such as accessing the Ethernet shield and EEPROM memory. Ethernet.h supports the basic functionality of an Ethernet shield. In addition, since I have elected to use UDP (a simple, short datagram protocol) for messaging, I have to include the EthernetUDP.h library as well. For more about network addresses and using EEPROM, see Ethernet Shields, Addresses & EEPROM. SystemCommon.h is my personal library containing (among other things) a function to retrieve address information from EEPROM (see Ethernet Shields, Addresses & EEPROM).

Next. the predefined values used by the sketch, including pin assignments.

#define NUMSTALLS 5
#define NUMLEDS 10

// Stall Settings
#define STALL_HIGH 1
#define STALL_LOW -1
#define STALL_OFF 0

// pin assignments
const int stripPIN = 2; 
const int clockPIN = 3;
const int latchPIN = 4;
const int dataPIN = 5;

A few global variables are needed. This sketch uses preset RGB values for the LEDs; these are set in three global arrays:

// RGB color arrays
 byte base_high[] = {128, 76, 25};
 byte base_low[] = {32, 19 , 6};
 byte blue_low[] = {0, 0, 8};

We need a global object to manage the LED strip, and a global object for handling UDP communication.

// An EthernetUDP instance to let us send and receive packets over UDP
 EthernetUDP Udp;
 // Global LED strip object
 Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUMLEDS, stripPIN, NEO_GRB + NEO_KHZ800);

Using Shift Registers

Before looking at setup() and loop(), we will need a method for handling shift register(s). For an excellent explanation and tutorial on shift registers, see the Arduino – ShiftOut Tutorial.

The short explanation of how they work is that each register has 8 outputs that can be ON or OFF. A pattern of bits is sent to each register via a single data connection, determining which outputs go on (binary 1) or off (binary 0). For example, sending the byte (8 bits) value of 6 to a register — 0110000 binary — turns the 2nd the 3rd outputs on and all the rest off. When there are multiple registers in series, a byte has to be sent out for each,

An important point about using shift registers: you must keep track of their state independently. This typically requires a global or static array representing all the shift register outputs and their current state.

This function maintains a static array of bytes representing the intended output state of all shift register outputs being tracked, and generates a corresponding bit mask to send to the registers. It is called every time you want to change the state of a register output.

void registerWrite(int whichOut, int whichState) {
  static byte outputStates[NUMSTALLS];
  outputStates[whichOut] = whichState;
  // the bits to send
  byte bitsToSend = 0;

  // turn off the output while shifting bits
  digitalWrite(latchPIN, LOW);  

  // Set bits according to the outputStates Array
  for(int i = 0; i < NUMSTALLS; i++){
    bitWrite(bitsToSend, i, outputStates[i]);
  }

  // shift the bits out:
  shiftOut(dataPIN, clockPIN, MSBFIRST, bitsToSend);

  // turn on the output 
  digitalWrite(latchPIN, HIGH);
}

Setting Lighting

All lighting is set by stall number using another function, which has to manage both the shift register and the LED strip to set the light in a given stall.

void stall_set(int stall, int state){
  byte *led_rgb; // pointer to a color array
  int lamp_state = HIGH;

  // set the led pointer to the correct color array for the requested state
  // set lamp off if new state is STALL_OFF
  switch(state){
    case STALL_HIGH:
      led_rgb = base_high;
      break;
    case STALL_OFF:
      led_rgb = blue_low;
      lamp_state = LOW;
      break;
    case STALL_LOW:
      led_rgb = base_low;
      break;    
  }
  registerWrite(stall, lamp_state); 
  strip.setPixelColor(4 - stall, strip.Color(led_rgb[0], led_rgb[1], led_rgb[2])); //rear LED
  strip.setPixelColor(stall + 5, strip.Color(led_rgb[0], led_rgb[1], led_rgb[2])); //front LED
  strip.show();
}

Setup()

Those preliminaries out of the way, here is the setup function:

void setup(){
  // start Ethernet and UDP:
  IPMAC ipm = readIPMAC(); 
  Ethernet.begin(ipm.mac, ipm.ip);
  Udp.begin(UDP_PORT);
  // set pins used by the shift register
  pinMode(clockPIN, OUTPUT);
  pinMode(latchPIN, OUTPUT);
  pinMode(dataPIN, OUTPUT);
  // initialize the lighting system
  strip.begin();
  strip.show(); // Initialize all pixels to 'off'
  delay(500);
  for(int i = 0; i < NUMSTALLS; i++){ // set stalls to off / night mode
    stall_set(i, STALL_OFF); 
  }
}

Loop()

The main loop is conceptually simple. On each repeat of the loop, the sketch checks to see if it has received a UDP packet. If it has a packet, the sketch executes the requested command.

void loop(){
  // static array to hold stall states
  static int stalls[5] = {0, 0, 0, 0, 0};
  int i, stall, req;
  // poll the network
  // if no actual packet, pkt.function == "0"
  PKT_DEF pkt = pollNet();
  
  // process the packet
  // case pkt.function == 0 will fall through
  switch(pkt.function.toInt()){
    case 1: //night/off [low blue ambient light]
      for(i = 0; i < NUMSTALLS; i++){
        stall_set(i, STALL_OFF);
        stalls[i] = STALL_OFF;
      }
      break;
    case 2: //all lights low
      for(i = 0; i < NUMSTALLS; i++){
        stall_set(i, STALL_LOW);
        stalls[i] = STALL_LOW;
      }
      break;
    case 3: //all lights high
      for(i = 0; i < NUMSTALLS; i++){
        stall_set(i, STALL_HIGH);
        stalls[i] = STALL_HIGH;
      }
      break;
    case 4: //Stall Control 
      // stall id is in pkt.option
      // pkt.data is used to convey a specific state request
      stall = pkt.option.toInt();
      // stall id should be 1 - 5
      // convert to internal numbering, 0 - 4
      if(stall > 0) stall--;
      if(stall >= 0 && stall < NUMSTALLS){
        if(pkt.data.length() > 0){
          // specific state request was received
          req = pkt.data.toInt();
          if(req >= -1 && req <= 1){
            stall_set(stall, req);
            stalls[stall] = req;
          }
        } else {
          // general toggle request
          switch(stalls[stall]){ // act based on current state
            case STALL_LOW:
              stall_set(stall, STALL_HIGH);
              stalls[stall] = STALL_HIGH;
              break;
            case STALL_OFF:
            case STALL_HIGH:
              stall_set(stall, STALL_LOW);
              stalls[stall] = STALL_LOW;
              break;
          }
        }        
      }    
      break;
  }
}

Simple Network Command System

UDP is a simple protocol. You can send a small (up to 24 bytes) packet containing anything you want. Its up to the receiving sketch to interpret the packet.

I’ve decided on a numerical command system. For now, commands are structured as a function, an option for executing the function if needed, plus a third optional data segment. The function and option are numbers; the data segment could be anything, but in this sketch is treated as a number.

The simplest way to build or interpret UDP packets is to use strings — a more advanced and useful form of character arrays intended to support human readable text. That is because packets are handled with character arrays and string semantics makes manipulation easier. Using strings also makes debugging easier!

For example, lets say that you want to tell the roundhouse to turn on stall 3 at full brightness. Stall control is function “4”, the stall id “3” is the option, and “1” (the value of STALL_HIGH in the sketch) requests the high brightness state. That packet would be a string of characters looking like this:

4/3/1/

The slash (“/”) is a delimiter (used to mark the end of a field) to allow interpretation of a packet where the elements are variable length. To go to low brightness state, the data field contains the value of STALL_LOW:

4/3/-1/

The call to pollNet() returns data in the form of a structure that separates out the three possible components of a packet. Here’s the type definition (I keep this in my library instead of individual sketches):

typedef struct PKT_DEF {
  String function; 
  String option;
  String data;
};

The pollNet() function checks to see if a new packet is available, and if so reads it into local memory, then passes the packet to the parsePKT() function which returns the packet data in the form of a PKT_DEF structure. PollNet() then sends a packet containing the string “ack” back to the originator of the command.

struct PKT_DEF pollNet(){
  char packetBuffer[UDP_TX_PACKET_MAX_SIZE + 1]; 
  String reply, strpkt;
  PKT_DEF pkt;
  int packetSize;
  // check for a new packet
  packetSize = Udp.parsePacket();
  if(packetSize)
  {     
    // read the packet into packetBuffer
    Udp.read(packetBuffer,UDP_TX_PACKET_MAX_SIZE);
    // convert packet to a string
    for(int i = 0; i < packetSize; i++){
      strpkt += packetBuffer[i];
    }
    // parse the packet
    pkt = parsePKT(strpkt);
    // send a reply, to the IP address and port that sent us the command
    reply = "ack";
    reply.toCharArray(packetBuffer, UDP_TX_PACKET_MAX_SIZE);
    Udp.beginPacket(Udp.remoteIP(), Udp.remotePort());
    Udp.write(packetBuffer);
    Udp.endPacket();
  } else {
    // no new packet
    pkt.function = "0";
  }
  return pkt;
 }
struct PKT_DEF parsePKT(String packet) {
   PKT_DEF pkt;
   int segment = 1;
   char delimiter = '/';
   int delimIndex = packet.indexOf(delimiter);
   if(delimIndex == 0) { 
    // drop leading delimiter, if any
    // some older experimental sketches may still be using a leading delimiter
    packet.remove(0,1);
    delimIndex = packet.indexOf(delimiter);
   }
   int lastDelim = -1;
   while(delimIndex >= 0 && delimIndex < packet.length()) {
     switch(segment){
       case 1:
         pkt.function = packet.substring(lastDelim + 1, delimIndex);
         break;
       case 2:
         pkt.option = packet.substring(lastDelim + 1, delimIndex);
         break;
       case 3: 
         pkt.data = packet.substring(lastDelim + 1, delimIndex);
         break;
       default:      
         break;
     }
     segment++;
     lastDelim = delimIndex;
     delimIndex = packet.indexOf(delimiter, lastDelim + 1);  
   }
   // if we don't already have a data field  
   // any trailing element without a deliminter is data
   // makes the third delimiter optional
   if(pkt.data.length() == 0 && lastDelim < packet.length()){ 
     pkt.data = packet.substring(lastDelim + 1);
   }  
   return pkt;
 }

That is the entire sketch. Basically this is the first version of the command and control systems that will be running on the layout. As you can see, 24 bytes is more than enough to convey any command plus parameters I wish. This system, while rudimentary, is more than enough to command an entire layout full of Arduino controllers.

I’m already working on improvements.