C++ Objects for Layout Control, Part 2 — Turnouts

In C++ Objects for Layout Control, Part 1 I did an introduction to C++ objects, and demonstrated the basics of OOP with a simple “fire” object that I use to run an LED to simulate a fire in the turntable operator’s hut on the L&NC. In the demo sketch I did two instances of the fire object with two LED’s to demonstrate object independence and scalability.

In this post, we’ll take the basics of C++ objects for animation and extend them to the most common animated object on our layouts: turnouts.

L&NC Progress

Before I go hardcore programmer on you, I thought’ I’d share a few progress notes & pictures. Last weekend we rejoined the three lower level modules together for the first time in over a year.

All Three lower level sections reassembled.

Crossing from module 1 to module 2 for the first time.

Red Bluffs Yard

Another view of Modules ! & 2

I was especially pleased that the three sections came back together perfectly, even though they have been apart for over a year. I attribute the stability of the structures and the accuracy of the alignment to the style of framing, use of hardwoods and the McMaster-Carr alignment pins I use to assure the sections align correctly when put back to together. It all looked good on paper; but until proven you can’t be completely sure. Now I’m confident I can take the layout apart and put it back together again reliably.

I’m getting the basic wiring and interconnections with modules 2 & 3 completed, following the same basic methods as on module 1 (the main difference from where I began is I’ve moved to current transformers for occupancy detection). Once the basic wiring is in and the turnouts are mated with servos, I’ll start in with scenicking. I’ll cover and further shape the mountain with plaster cloth and sculptamold. Since I am doing more roads (using the Woodland Scenics road system which I think works pretty well; more about that at the end of this previous post) on both modules 2 & 3, I’ll pour those first. I find it best to get the roads in before applying any paint or other material. All that should keep me busy for a while.

Turnout Issues & Requirements

Like the fire object, the turnout object requires certain basic parameters to run, and has to be able to keep track of its own state. The turnout object has to respond to positioning commands and manage the turnouts’ motion to achieve a slow, scale appropriate movement from one alignment to the other.

The added complication is that the turnout class will need to deal with different hardware configurations and interfaces in different situations. For example, on the L&NC module I’ve been working on there are 9 turnouts and I’m using the Adafruit PWM Driver to run the servos and additional PWM devices, which I discussed in L&NC Update: Running Lots of Turnouts. The adjacent module has only two turnouts, so I’ll use the standard servo library and a couple of pins there. But regardless of the hardware interface, I want to use the same software objects in both places so they perform the same way.

Properties

The turnout class is a little more complicated than the fire class. Its basic properties include the pin (in the case of the Adafruit driver, the channel) the servo is attached to, the current alignment of the turnout, the default alignment and the servo settings for the main and divergent positions (pos_main and pos_divergent). Then, to facilitate motion, there are a variety of properties that work together: delay between moves, the increment (1 or more units) of each move, the current position, the target position and the target alignment.

class turnout
{
private:
 // object properties
 int pin;
 int pos_main;
 int pos_div;
 int align_default;
 int alignment;
 
 // motion data
 bool is_moving;
 int move_delay;
 int increment;
 int pos_now;
 int target_pos;
 int target_alignment;
 unsigned long last_move;
}

I mentioned that I am using the turnout class in different situations where the hardware that runs the servos could be a standard pin, or an external driver board such as the Adafruit PWM Driver. To complicate matters, while the standard Servo Library positions by degrees, the Adafruit PWM Driver positions by the “tick” value of the desired PWM setting.  These are different units and different scales; but it doesn’t matter. So long the turnout class is initialized with a correct set of values (in the same units) for the hardware interface in question, it works consistently.

Methods

For the turnout class I have created an interface of public methods for interacting with the class. Additionally, the class has a private hardware interface for interacting with the hardware environment.

The Constructor

The constructor is straight forward with one twist. Because a lot of arguments are required to set up the object, I’m using a data structure to pass most of the arguments.

typedef struct TURNOUT_PARAMS {
 int pin;
 int pos_main;
 int pos_div;
 int align_default;
 int move_delay;
};

On the L&NC Module1, Lower Level the array of turnout parameters looks like this (STANDARD_DELAY is 20):

TURNOUT_PARAMS tdef[NUM_TURNOUTS] = {
 {0, 375, 310, ALIGN_MAIN, STANDARD_DELAY},
 {1, 335, 408, ALIGN_MAIN, STANDARD_DELAY},
 {2, 330, 370, ALIGN_MAIN, 30},
 {3, 284, 345, ALIGN_MAIN, STANDARD_DELAY},
 {4, 355, 415, ALIGN_MAIN, STANDARD_DELAY},
 {5, 291, 390, ALIGN_MAIN, STANDARD_DELAY},
 {6, 285, 373, ALIGN_MAIN, STANDARD_DELAY},
 {7, 355, 285, ALIGN_MAIN, STANDARD_DELAY},
 {8, 305, 372, ALIGN_MAIN, STANDARD_DELAY}
 };

The constructor takes a pointer to a TURNOUT_PARAMS variable, plus an optional movement_increment argument. I added the movement increment parameter (and the corresponding class property) as an additional factor in turnout motion after encountering problems integrating multiple processes on the Uno on Module 1. Manipulating both delay and increment values improves control over the movement of servos in different situations. On the L&NC Module 1, Lower Level there are both a lot of turnouts and a lot of block detectors. I’ll discuss the issues more in an upcoming post, but the resources required for block detection make it difficult to move the servos fast enough without adjusting the movement increment.

// Constructor
 turnout(TURNOUT_PARAMS *parameters, int movement_increment = 1){
   pin = parameters->pin;
   pos_main = parameters->pos_main;
   pos_div = parameters->pos_div;
   align_default = parameters->align_default;
   move_delay = parameters->move_delay;
   is_moving = false;
   increment = movement_increment;
   init_servo();
 }

After capturing the parameter values, the constructor invokes the private init_servo() method which I will talk about below in the hardware interface section.

Using the array of parameters above, the turnout objects are instantiated with this bit of code in setup, taking the default value for movement_increment (turnouts is global, declared before setup):

 turnout *turnouts[NUM_TURNOUTS];
 for(int i = 0; i < NUM_TURNOUTS; i++){
   turnouts[i] = new turnout(&tdef[i]);
 }

What’s all this Pointer Stuff?

If you’ve been working with Arduino for a while you’ve likely dealt with pointers before. For those who are unfamiliar with the term (and it is a complex subject, worth learning), pointers are the memory address of a variable. Pointers are another way to access variables unique to the C/C++ languages. In the case of the Constructor, passing a pointer to the parameters array grants access to a complex data structure without having to copy it first. The Reference (&) operator returns a pointer to an variable (&tdef[i]); the dereference (*) operator declares a pointer variable used to access a pointer’s value — such as TURNOUT_PARAMS *parameters in the Constructor. With pointers to data structures or objects, use “->” to access members instead of “.”; eg: params->data instead of params.data or object->method() instead of object.method().

The Public Interface

The public interface is comprised for four methods. The getAlignment() method is a good example of the correct way to share the value of an internal property with an external process. Its correct because it outputs the value of the alignment property without breaking its protection as a private property (and thus exposing it to being changed externally).

 int getAlignment(){
   return alignment;
 }

Another simple method provides a way to toggle the position of the turnout back and forth.

void toggle(){
   if(alignment == ALIGN_MAIN){
     set(ALIGN_DIVERGENT);
   } else {
     set(ALIGN_MAIN);
   }
 }

The alignment can be one of three values at any given time.

#define ALIGN_NONE 0
#define ALIGN_MAIN 1
#define ALIGN_DIVERGENT 2

As you’ll see, ALIGN_NONE indicates that the turnout is in motion.

The heart of the logic for moving the turnout is in the set() and update() methods.

void set(int align){
 if(align != alignment){
   is_moving = true;
   last_move = 0;
   target_alignment = align;
   alignment = ALIGN_NONE;
   switch(align){
     case ALIGN_MAIN:
       target_pos = pos_main;
       break;
     case ALIGN_DIVERGENT:
       target_pos = pos_div;
       break;
     }
   }
 }

 void update(unsigned long curMillis) {
   if(is_moving){ 
     if((curMillis - last_move) >= move_delay){
       last_move = curMillis;
       if (pos_now < target_pos) { // if the new position is higher
         pos_now = min(pos_now + increment, target_pos);
         setServo(pos_now);
       } else { // otherwise the new position is equal or lower
         if (pos_now != target_pos) { // not already at destination
           pos_now = max(pos_now - increment, target_pos);
           setServo(pos_now);
         }
       }
       if (pos_now == target_pos) {
          is_moving = false;
          last_move = 0;
          alignment = target_alignment;
       }
     }
   }
 }

Motion works by setting motion variables in the set() method then calling update() repeatedly to execute the motion. Update() is intended to be called continually as part of the main loop() while the sketch is running—my multitasking model is to get the current time (millis()) at the start of every iteration of the main loop(), then pass that value to every object that uses time to manage its own state. It is very important that the very first logic test within the update() method is whether or not the turnout is in motion; if not in motion the method exits immediately. With nine turnouts on the lower lever of module 1, efficiency is necessary or the sketch bogs down.

Hardware Interface

Up to this point there has been no direct interaction with hardware. Instead there have been calls to two private methods: init_servo() and setServo(). These two private methods interact directly with the servo hardware.

I should emphasize that any motor type can be used with appropriate connections, even 12 volt stall motors. I like servos because they are cheap and easily supported in the Arduino world. But don’t feel that just because you have a different motor type you can’t run them with an Arduino. The point of creating a private, protected hardware interface is to isolate all the hardware specific stuff in one place while presenting a more general public interface. That makes it much easier to drive a wide variety of hardware while behaving consistently.

private:
 void init_servo(){
   int data;
   switch(align_default){
     case ALIGN_MAIN:
     data = pos_main;
     break;
   case ALIGN_DIVERGENT:
     data = pos_div;
     break;
   }
   setServo(data);
   is_moving = false;
   pos_now = data;
   alignment = align_default;
 }
 // hardware interface 
 void setServo(int data){
   // use compiler directives to determine which
   // method is used to drive the actual hardware
   #ifdef ADAF_DRIVER
   extern Adafruit_PWMServoDriver pwm;
   pwm.setPWM(pin, 0, data);
   #endif
   #ifdef SERVO_LIB
   extern servo servos;
   servos[pin]->write(data);
   #endif
 }

Init_servo() is for doing whatever your motor needs to set up. Here the method sets the default alignment and commands the servo to move to that position immediately. This is private so that it can never be called from outside a turnout object.

setServo() moves the servo to a specific position. I use a compiler directive — by defining either ADAF_DRIVER or SERVO_LIB (but not both)— to determine which hardware system is in use. In some cases I’ll be using the Adafruit 16 Channel PWM Driver and its library; in other cases regular pins with the native servo library.

The Whole Enchilada

I’m posting the turnout class on the github site.

This time I’m posting it in the form of a header — *.h — file. To use it, copy it into your sketch directory. The Arduino IDE will recognize and allow you to edit the “h” file, but it won’t automatically include it in your build.  To in include it in the build you must explicitly include it near the top of your main sketch this way:

#include "turnout.h"

Why do it this way? Because the class definition has to be seen by the compiler before it can be used to create run-time objects. Therefore, in a single file sketch you’d have to put the class definition at the top of the sketch. If you have a lot of header material of that sort (class definitions, typdefs, etc.), the top of your sketch can get long and the whole thing harder to maintain.  By putting the class definition in a header file you can segregate different elements of your sketch, control exactly when the compiler sees it during the build process, ensure all dependencies are satisfied and keep your main sketch file clean and uncluttered. The bigger your sketch gets the more important this becomes.

Coming Soon

Back to block occupancy detection with 24 blocks and one Arduino to rule them all! To say scale started to be a problem would be an understatement. More about that in the next post.

Until then, Happy Railroading!

 

 

20 thoughts on “C++ Objects for Layout Control, Part 2 — Turnouts”

  1. Robin,
    Do you have a sketch that is using “turnout.h” I can’t it to compile at all…

    Thanks
    Greg

    • What error is the compiler giving you? I can put together a simple one-turnout sketch to test everything, but lets see if we can figure out what the problem is from the error messages. RS

  2. Robin… This is very simple…
    #include “turnout.h”
    void setup() {
    }

    void loop() {
    }
    ….…..
    C:\Users\gregh\AppData\Local\Temp\arduino_build_270881\sketch\turnout.h:21:1: warning: ‘typedef’ was ignored in this declaration

    };

    ^

    C:\Users\gregh\AppData\Local\Temp\arduino_build_270881\sketch\turnout.h: In member function ‘void turnout::toggle()’:

    turnout.h:62: error: ‘ALIGN_MAIN’ was not declared in this scope

    if (alignment == ALIGN_MAIN) {

    ^

    turnout.h:63: error: ‘ALIGN_DIVERGENT’ was not declared in this scope

    set(ALIGN_DIVERGENT);

    ^

    C:\Users\gregh\AppData\Local\Temp\arduino_build_270881\sketch\turnout.h: In member function ‘void turnout::set(int)’:

    turnout.h:74: error: ‘ALIGN_NONE’ was not declared in this scope

    alignment = ALIGN_NONE;

    ^

    turnout.h:76: error: ‘ALIGN_MAIN’ was not declared in this scope

    case ALIGN_MAIN:

    ^

    turnout.h:79: error: ‘ALIGN_DIVERGENT’ was not declared in this scope

    case ALIGN_DIVERGENT:

    ^

    C:\Users\gregh\AppData\Local\Temp\arduino_build_270881\sketch\turnout.h: In member function ‘void turnout::init_servo()’:

    turnout.h:113: error: ‘ALIGN_MAIN’ was not declared in this scope

    case ALIGN_MAIN:

    ^

    turnout.h:116: error: ‘ALIGN_DIVERGENT’ was not declared in this scope

    case ALIGN_DIVERGENT:

    ^

    exit status 1
    ‘ALIGN_MAIN’ was not declared in this scope

    • In order to work, you need to define three macro-constants:

      #define ALIGN_NONE 0
      #define ALIGN_MAIN 1
      #define ALIGN_DIVERGENT 2

      Put those at the beginning of the sketch before including turnout.h

      Best,
      Robin

      • Hey Robin,
        Any chance of getting some demo code… I just dont understand how to move the servers
        Thanks
        Greg

          • #include
            #include

            Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();

            #define ADAF_DRIVER
            #define ALIGN_NONE 0
            #define ALIGN_MAIN 1
            #define ALIGN_DIVERGENT 2
            #include “turnout.h”

            #define NUM_TURNOUTS 2
            #define STANDARD_DELAY 20

            TURNOUT_PARAMS tdef[NUM_TURNOUTS] = {
            {0, 160, 575, ALIGN_DIVERGENT, STANDARD_DELAY},
            {1, 150, 530, ALIGN_MAIN, STANDARD_DELAY}
            };

            turnout *turnouts[NUM_TURNOUTS];

            void setup() {
            Serial.begin(9600);
            Serial.println(“Load Parms”);

            pwm.begin();
            pwm.setPWMFreq(60); // Analog servos run at ~60 Hz updates

            for (int i = 0; i toggle();
            turnouts[1]->toggle();

            Serial.println();
            }

            void loop() {
            unsigned long current_millis = millis();

            turnouts[0]->update(current_millis);
            turnouts[1]->update(current_millis);

            }

            And I got this error…

            sketch\turnout.h:21:1: warning: ‘typedef’ was ignored in this declaration

            };
            so I changed… it to…
            typedef struct {
            int pin;
            int pos_main;
            int pos_div;
            int align_default;
            int move_delay;
            }TURNOUT_PARAMS;

            Is this the correct way of using your code?

            Thanks
            Greg

          • You are not too far off, but modifying the typedef shouldn’t be necessary. An odd error (ignoring a typedef is wierd) usually means a parsing problem such that the compiler isn’t understanding the code. One thing in setup() sticks out:

            for (int i = 0; i toggle();
            turnouts[1]->toggle();

            That’s not a properly formatted for loop and I can’t tell what is intended here. What should be happening in setup at that point is initializing the turnouts like this:

            for(int i = 0; i < NUM_TURNOUTS; i++){ turnouts[i] = new turnout(&tdef[i]); }

            The turnout objects don't exist until this happens. Only after initialization is it possible to use the toggle() and update() functions. For testing I suggest you wire up a couple of buttons and monitoring them in your main loop -- running toggle() every time a button is pushed. Program flow in the main loop should look like this:

            -> Get Milliseconds
            -> run update method on all turnouts
            -> read buttons
            -> if a button[i] == HIGH call turnout[i].toggle()

            R

  3. Thanks Robin
    so what did you decide on for block detection? (ACS712 SENSOR or coils)

    Greg

  4. Hi Rob,
    Reading all the initiated comments on your superb programming, I’m beginning to feel a bit “out of the sketch”. I think I understand the basic idea using millis to define servoposition/movements, but can’t get it to function. As already stated, your earlier “5 servo demo” works ok, but the goal now is using PCA9685. You did post, some time ago, a description on what had to be changed to use this unit; adding the Adafruit library and replacing “servo.write” calls with the two lines
    pulselen = map(turnouts[i].pos_now, 0, 180, SERVOMIN, SERVOMAX);
    pwm.setPWM(turnouts[i].data.servo_port, 93, pulselen);

    Other than that I have changed the servo pin definition in the turnout data array:

    TURNOUT_DATA turnouts[NUMBER_OF_TURNOUTS] = {
    {A0, 0, 93, 117, 0, 1, 2, 3},
    {A1, 1, 93, 117, 4, 5, 6, 7},
    {A2, 2, 93, 117, 8, 9, 10, 11},
    {A3, 3, 93, 117, 12, 13, 14, 15},
    I suspect this may be a problem, will the new library map Arduino output pins into PCA9685 outputs ? And, if so, should I have “2,3,4,5” instead ?
    If possible, could you have a look and see if something sticks out ?
    Rgds
    Peter Unger

  5. Evening guys.
    i have downloaded the turnouts.h file but get an error with ALIGN_MAIN not defined. I tried copying
    #define ALIGN_NONE 0
    #define ALIGN_MAIN 1
    #define ALIGN_DIVERGENT 2
    at the start of the sketch but then got other errors.
    I have read and read…
    Is there a file for the 9685 that responds to a button press to actuate the servo.
    Many thanks for the stimulus, my head is kind of melting

    • That file just contains the object definition and is not a complete implementation. I looked at the file and added in those three items for convenience — but what you did is fine.

      To use the file, you need to have one (but not both) of these defines at the top of the file before including turnouts.h –

      #define ADAF_DRIVER
      OR
      #define SERVO_LIB

      This determines which PWM driver is used — if using ADAF_DRIVER the Adafruit PWM driver has to be properly installed in your libraries.

      All your standard includes, including the Adafruit driver, should be first in the sketch; only include turnouts.h after the other files have been included.

      That should eliminate most sources of errors other than typos.

      There is no special code for using buttons with PC9685 cards and the Adafruit driver. The turnout object handles the PWM side of the equation. What button presses should do is call a turnout object method like this: turnout.set(ALIGN_MAIN), turnout.set(ALIGN_DIVERGENT) or turnout.toggle().

      Assuming your button is on port D6, here is a simple version of the coding that would toggle the position of the turnout if the button is pressed:

      if(digitalRead(D6) == HIGH){
      turnout.toggle();
      }

      A more sophisticated approach uses a millis() timer to check buttons at a certain rate — say 10 times per second. Also, you want to save button state between readings and act only when the state changes. Those additional measures help avoid false or repeated actions.

      Don’t forget the 10k pulldown resistor on each button; without it button state will gyrate.

      Cheers,

      Rob

      • Thanks for that Rob,
        I have edited and removed the second line and that part is now compiling.
        Sorry for the dumb question(s)
        Should I put the TURNOUT_PARAMS tdef array in the same sketch?
        I am reading as if the turnouts.h is the main script calling other objects in to it?
        I am reading and reading but little is sticking!!

        • I would put the TURNOUT_PARAMS array near the top of your main sketch.

          turnouts.h is a class definition file. It defines object you can create and manipulate in your main script.

          R

  6. Hi Rob,
    Firstly sorry.., but I am learning and picking bits up..
    To explain: I was/am planning to use a separate uno to run the turnouts, stand alone. I have an arduino mega running DCCp for my dcc control of the locos.
    As such I was putting all the defs into the turnout.h and then operate like an old fashioned mimic board, pulse to actuate (as per the hint you gave me above) I have achieved this (I think) but now I get an error.
    To others this may seem a crow bar to break an egg, but it lets me play without killing the mega??
    “Arduino: 1.8.13 (Windows 10), Board: “Arduino Uno”
    C:\Users\Ellie\AppData\Local\Temp\cc1gDM4b.ltrans0.ltrans.o: In function `main’:
    C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/main.cpp:43: undefined reference to `setup’
    C:\Program Files (x86)\Arduino\hardware\arduino\avr\cores\arduino/main.cpp:46: undefined reference to `loop’
    collect2.exe: error: ld returned 1 exit status
    exit status 1
    Error compiling for board Arduino Uno.”

    • Taken literally, the error says that the compiler can’t find your setup() and loop() functions. Either those two required functions are in fact missing, or there is a code structure issue that is fooling the compiler.

      You don’t want to mess with DCC++ code; it will probably break. Yes, you want separate boards for other things.

      Rob

  7. Thanks Rob,
    I have another machine and have tonight installed arduino on that so will use that to evolve the turnout project.. will update in next days
    Cheers
    Ian

Leave a Reply

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