ML2016-3

Controlling a servo (or 4) from JMRI using LCC messages:

We are going to create a fake LCC node to act upon the messages sent by LCC turnouts in JMRI over the CAN bus. I claim fake, since the node does not do even close to what is required by a real LCC node. No configuration, no status, no firmware updates and no CDI at all, simply receiving an 8 byte message and if one of the 8 hard coded, move one of 4 servos CW or CCW.
 
Both Arduinos are connected to MCP2515 SPI CAN transceivers with only the CANHI, CANLO and Ground wires connected between the two sets.


*Fritzing drawing of the board hookup...ARduino on the left is also connected to the PC, giving JMRI the LCC serial connection.

First piece of code is for the Arduino connected to the computer and JMRI, COMx or /dev/ttyUSB00x, sending all messages from JMRI onto the CAN bus (the toughest part in this and the next piece of code, is to understand that an 8 bit byte is send as two nibbles (4 bits) in hexadecimal as ASCII characters across the serial port, and in order to make it a number again, you need to subtract either '30h' from the 0-9 or '41h' from the A-F (and add 10 again, since 'A' is 10), once you get the idea, or ignore it, the code is simple! ):
/* 
  Sending CAN messages from Serial port as received from JMRI.
  JMRI will send a ':'; some bytes; an 'N'; up to 8 data bytes we want
  to send and then a ';'. We are going to ignore everything except the data
  between 'N' and ';', and then send the bytes out on the CAN bus.
  If we did receive more than 8 bytes when ';' comes, we don't send and wait 
  for the next data.
  
  The data shows up over the serial port as two hexadecimal characters, (nibbles)
  0x00 thru 0xFF, so we need to take care of making the two characters  into 
  one 8 bit byte. Of course, a message like:
  03.00.01.02.03.02.01.01 does not have A-F, but we are going to avoid running
  into the problem when the message ends in something like .02.5F.AE.
  Examples :XCACB4000N0300010203020101; or :XCACB4000N5AA500FFDEADBEEF;
  
  Put an LED and 1k resistor between 5V and STATUSLEDPIN to see some action when
  sending a message.
  
  Author: Speed Muller, Date:   2017.03.28
*/

#include <mcp_can.h>
#include <SPI.h>

#define VERSION "canJMRISendLCC ver 009"

#define CANPIN       10
#define CANINTPIN    11
#define STATUSLEDPIN  4              // PIN 13 is in use by SPI
#define CANMSGSIZE    8

MCP_CAN       myCan( CANPIN );       // CAN object with CS as CANPIN
bool          initSuccess = false;   // keep track if CAN initialized 
unsigned char data[CANMSGSIZE];      // send CANMSGSIZE bytes max
char          inChar;                // char from Serial port
unsigned char value;                 // data value with HI and LO nibble
byte          charPosition;          // track of where next Serial char goes

enum States { BEFORE, COLON, NHI, NLO, WAIT4SEMI, SEMICOLON };
States        state = BEFORE;

void setup() {
  pinMode( STATUSLEDPIN, OUTPUT );
  digitalWrite( STATUSLEDPIN, HIGH );
  Serial.begin( 115200 );
  // init can bus with a baudrate = 500k 
  initSuccess = CAN_OK == myCan.begin( CAN_125KBPS, MCP_8MHz ); 
  //.. Serial.println( VERSION );  // avoid talking to JMRI this round 
} // setup

void loop() {
  if ( !initSuccess ) { // only flash LED if CAN did not initialize
    delay( 100 ); digitalWrite( STATUSLEDPIN, HIGH ); 
    delay( 100 ); digitalWrite( STATUSLEDPIN,  LOW );
    return;   // end of loop()
  } // if CAN init failed

  while ( Serial.available() > 0 ) {
    inChar = Serial.read();
    switch ( state ) {
      case BEFORE:    if ( inChar == ';' ) { state = BEFORE;
                                           digitalWrite( STATUSLEDPIN, HIGH ); }
                      if ( inChar == ':' ) { state = COLON;  
                                           digitalWrite( STATUSLEDPIN,  LOW ); }
                      break;
                      
      // only when a COLON showed up, we reset the data and position
      case COLON:     if ( inChar == ';' ) { state = BEFORE; break; }
                      if ( inChar == ':' ) { state = COLON; break; }
                      if ( inChar == 'N' ) state = NHI;
                      for ( int i = 0; i < CANMSGSIZE; i++ )
                        data[i] = 0x00;  // clear data buffer
                      charPosition = 0;  // next char goes to 0
                      break;
                      
      // only when an N showed, do we copy the incoming data, HI nibble first
      case NHI:       if ( inChar == ';' ) {
                        if ( charPosition > 0 ) {
                          state = SEMICOLON; 
                        } else {
                          state = BEFORE;    break;
                        } // if
                      } else {
                        if ( charPosition < CANMSGSIZE ) {
                          // HEX is 0-9 and A-F (caps assumed) remove '0' or 
                          // 'A' to get value, get HI => val * 16
                          if ( inChar < 'A' ) value = ( inChar - '0' ) * 16;  
                          else value = ( 10 + inChar - 'A' ) * 16;
                           
                          state = NLO;                    // LO Nibble next
                        } else { 
                          state = BEFORE; break; 
                        } // else
                        break;
                      }
                      // no break here                      
      case WAIT4SEMI: if ( inChar == ';' ) { state = BEFORE; /*break;*/ }
      
      case SEMICOLON: digitalWrite( STATUSLEDPIN, HIGH );
                      // send CAN data: ID = 127, Standard frame, 
                      // data len = CANMSGSIZE, data: buffer
                      myCan.sendMsgBuf( 127, 0, charPosition, data );  
                      state = BEFORE;
                      delay( 100 );                       // delay a bit 
                      break;

      // copy the incoming data LO nibble
      case NLO:       if ( inChar == ';' ) { state = BEFORE; break; } // if
                      if ( charPosition < CANMSGSIZE ) {
                        // HEX is 0-9 and A-F (caps assumed) remove '0' or 
                        // 'A' to get value, val = val + LO
                        if ( inChar < 'A' ) value += ( inChar - '0' ) ;
                        else                value += ( 10 + inChar - 'A' );
                          
                        state = NHI;
                        data[charPosition++] = value;     // add value to buffer
                        value = 0; 
                        if ( charPosition == CANMSGSIZE ) {
                          state = WAIT4SEMI; break;  
                        }
                      } else { 
                        state = BEFORE; break; 
                      } // else
                      break;

      // anything else shows up? we reset
      default:        state = BEFORE; 
                        break;
    } // switch
  } // while serial.available
} // loop

/******************************* END OF FILE **********************************/

The second piece of code, will listen to messages on the CAN bus and do something with them if recognized:
/* 
  CAN receive from JMRI over LCC
  When receiving one of 8 messages over CAN from JMRI's LCC turnouts,
  move one of the 4 servos into the correct position 
  
  Ctrl-Shift-M to open the Serial Monitor to see your messages coming in.

 :XCACB4000N0300010203020101;

  Author: Speed Muller, Date:   2017.03.28
*/

#include <mcp_can.h>
#include <SPI.h>
#include <Servo.h>

#define VERSION   "canJMRIServoReceiveLCC ver 006"

#define OURID 0x03000102
#define DEBUG      false
#define IMALIVE     3000
#define CHECKIN      300

#define CANINTPIN      2
#define CANPIN        10
#define STATUSLEDPIN   4
#define CANMSGSIZE     8

#define SERVO1PIN      3 
#define SERVO2PIN      5
#define SERVO3PIN      6
#define SERVO4PIN      9

/* skipping these until we can send events back to JMRI
* #define SERVO1INPIN    2
* #define SERVO2INPIN    4
* #define SERVO3INPIN    7
* #define SERVO4INPIN    8
* #define UPDATETIME   100
*/
// could also use 14,15,16 and 17 here:
#define SERVO1LEDPIN  A0
#define SERVO2LEDPIN  A1
#define SERVO3LEDPIN  A2
#define SERVO4LEDPIN  A3

#define SERVOLEFT     75
#define SERVORIGHT   105
#define SERVOCNTR     90

/* 
And of course you need to copy the whole previous 
"class SlowerServo : public Servo { };" in here from before if you want slower 
moving servos like SlowerServo myServo1( UPDATETIME/2 ); and 
SlowerServo myServo2( 2*UPDATETIME );
*/

Servo myServo1; Servo myServo2; Servo myServo3; Servo myServo4;
MCP_CAN       myCan( CANPIN );       // CAN object with CS as CANPIN
bool          initSuccess = false;   // keep track if CAN initialized 

long unsigned int rxId;
unsigned char     len = 0;
unsigned char     rxBuf[8];
unsigned long     incomingEvent;
byte              value;

bool              lightOn = false;
int               pin1    = HIGH;

void setup() {
  pinMode( STATUSLEDPIN, OUTPUT );
  delay( 500 ); digitalWrite( STATUSLEDPIN, HIGH ); 
  delay( 500 ); digitalWrite( STATUSLEDPIN,  LOW );
  delay( 500 ); digitalWrite( STATUSLEDPIN, HIGH ); 
  delay( 500 ); digitalWrite( STATUSLEDPIN,  LOW );
  delay( 500 ); digitalWrite( STATUSLEDPIN, HIGH );
  
  Serial.begin( 115200 );
  initSuccess = ( CAN_OK == myCan.begin( CAN_125KBPS, MCP_8MHz ) ); // init can bus : baudrate = 125k 
  pinMode( CANINTPIN, INPUT );          // Setting pin CANINTPIN for /INT input

  pinMode( SERVO1LEDPIN, OUTPUT ); pinMode( SERVO2LEDPIN, OUTPUT ); 
  pinMode( SERVO3LEDPIN, OUTPUT ); pinMode( SERVO4LEDPIN, OUTPUT ); 
  
  incomingEvent = 0;  
  Serial.println( VERSION );                                  // since you are not using the serial port to JMRI
 
  myServo1.attach( SERVO1PIN ); myServo2.attach( SERVO2PIN ); // attach to pins
  myServo3.attach( SERVO3PIN ); myServo4.attach( SERVO4PIN ); // attach to pins
  myServo1.write( SERVOCNTR );  myServo2.write( SERVOCNTR );  // move to middle
  myServo3.write( SERVOCNTR );  myServo4.write( SERVOCNTR );  // move to middle
} // setup

void loop() {
  if ( !initSuccess ) {                 // flash LED if CAN did not initialize
    delay( 100 ); digitalWrite( STATUSLEDPIN, HIGH ); 
    delay( 100 ); digitalWrite( STATUSLEDPIN,  LOW );
    Serial.println( "no CAN ..." );
    initSuccess = ( CAN_OK == myCan.begin( CAN_125KBPS, MCP_8MHz ) ); // init can bus : baudrate = 125k 
    return;   // end of loop()
  } // if CAN init failed

  if ( !digitalRead( CANINTPIN ) ) {    // If pin 2 is low, read receive buffer
    myCan.readMsgBuf( &len, rxBuf );    // Read data: len = data length, 
                                        // buf = data byte(s)
    rxId = myCan.getCanId( );           // Get message ID

    int i = 0;   // declare outside "for", we are breaking and continuing... 
    for ( i = 0; i < len; i++ ) { // add each byte to 16 x 16 of the previous
      incomingEvent = incomingEvent * 16  * 16 + rxBuf[i];
      if ( i == 3 ) break;  // only need the first 4 bytes here to match ID
    } // for
      
    if ( incomingEvent != OURID ) // is it for us?
      len = 0;                // bail then
                              // it is not our message
    incomingEvent = 0;        // start over, unsigned long int is not big enough
    for ( i; i < len; i++ ) { // add each byte to 16 x 16 of the previous
      incomingEvent = incomingEvent * 16 * 16 + rxBuf[i];        
    } // for
    Serial.println( );
    if ( len == 8 ) {              // this node only do 8 byte messages!
      switch ( incomingEvent ) {   // of course the messages are hard coded
                                   // here, but you could easily store them
                                   // in EEPROM and update them as you
                                   // configure your nodes          
        case 0x03020101:  // 1st servo 
          Serial.println( "1-closed" ); myServo1.write( SERVORIGHT );
          digitalWrite( SERVO1LEDPIN, LOW ); 
          break;
        case 0x03020100:           
          Serial.println( "1-thrown" ); myServo1.write( SERVOLEFT );
          digitalWrite( SERVO1LEDPIN, HIGH );
          break;               
        case 0x03020201:  // 2nd servo
          Serial.println( "2-closed" ); myServo2.write( SERVORIGHT );
          digitalWrite( SERVO2LEDPIN, LOW );
          break;
        case 0x03020200:
          Serial.println( "2-thrown" ); myServo2.write( SERVOLEFT );
          digitalWrite( SERVO2LEDPIN, HIGH );
          break;                    
        case 0x03020301:  // 3rd servo         
          Serial.println( "3-closed" ); myServo3.write( SERVORIGHT );
          digitalWrite( SERVO3LEDPIN, LOW );
          break;
        case 0x03020300:           
          Serial.println( "3-thrown" ); myServo3.write( SERVOLEFT );
          digitalWrite( SERVO3LEDPIN, HIGH );
          break;          
        case 0x03020401:  // 4th servo         
          Serial.println( "4-closed" ); myServo4.write( SERVORIGHT );
          digitalWrite( SERVO4LEDPIN, LOW );
          break;
        case 0x03020400:
          Serial.println( "4-thrown" ); myServo4.write( SERVOLEFT );
          digitalWrite( SERVO4LEDPIN, HIGH );
          break;            
        default:
          break;
      } // switch incomingEvent
    } // if len == 8
  } // if CANINTPIN
} // loop

/******************************* END OF FILE **********************************/

And that is all it takes to create a "fake" LCC node, doing something "real"!



The files attached below are a from August 2016, and the code shown above is newer, so use that instead until the files below are updated.

Enjoy

ċ
ML2016-3_canJMRISendLCC.ino
(6k)
Gert (TxAdmin) Muller,
Aug 4, 2016, 5:32 AM
ċ
ML2016-3_canJMRIServoReceiveLCC.ino
(6k)
Gert (TxAdmin) Muller,
Aug 4, 2016, 5:35 AM
ċ
ML2016_07.xml
(11k)
Gert (TxAdmin) Muller,
Aug 22, 2016, 5:58 AM
Comments