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 8MCP_CAN myCan( CANPIN ); // CAN object with CS as CANPINbool initSuccess = false; // keep track if CAN initialized unsigned char data[CANMSGSIZE]; // send CANMSGSIZE bytes maxchar inChar; // char from Serial portunsigned char value; // data value with HI and LO nibblebyte charPosition; // track of where next Serial char goesenum 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 } // setupvoid 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 CANPINbool 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} // setupvoid 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