Model train layout control app using Blynk

I have created a phone app for controlling my model train layout…

It handles train control, special functions, real-time track views, signals, events, troubleshooting and dispatching trains… plus some admin type things too…

https://cabin-layout.mixmox.com/2019/03/mobile-phone-layout-control-app.html

#include "Cab.h" // auth token SSID, private urls etc.

// Blynk
#include <BlynkSimpleEsp8266.h>
#include "C:\ard\common\Blynkhelper.h"

// common files, do not add to sketch, they get duplicated if you do
#include "C:\ard\common\utils.h"
#include "C:\ard\common\RemoteSign.h"
#include "C:\ard\common\RemoteSign.cpp"

// hardware setup -----------------------------------------------------
// none
//--------------------------------------------------------------------------


// general constants
const int MAXSPEED = 200; //max of slider

RemoteSign rs(D4, LOW, HIGH); //create an instance of RemoteSign, using D4 as the connection status light
bool forward = true;
int speed = 0;  // current speed in km/h
int dspeed = 0; // desired speed in km/h
int limit = 0; // speedlimit in km/h
int train=-1; // current train
int k83adr = 1; // k83 address
int s88 = 0; //s88 to be monitored

const String BLYNK_GREEN = "#23C48E";
const String BLYNK_RED = "#D3435C";
const String BLYNK_YELLOW = "#ED9D00";
const String BLYNK_BLUE  =  "#04C0F8";
const String BACKGROUND = "#212226";

// log screen
WidgetTerminal terminal(V22);
// event screen
int currentevent = -1;
// dispatch screen
int currentstation = -1;
int currentstationtrack = -1;
int currentdestination = -1;


void shows88(bool onoff, String Descrip){ // display the state of the LED
  Blynk.virtualWrite(V26,onoff ? 255:25); // show the led or not
  Blynk.virtualWrite(V19,Descrip); // put description into display widget

}
void ShowDisconnect() {
      String SpaceString=" ";
      Blynk.setProperty(V1, "labels", SpaceString); // clear trains
      Blynk.virtualWrite(V1,0);
      train=0;
      mywriteV1(); //, sets limit, clears track image, signal, etc.
      
      Blynk.setProperty(V30, "labels", SpaceString); // clear stations
      Blynk.virtualWrite(V30,0);
      ResetStation(); // resets tracks and destinations
      Blynk.setProperty(V27, "labels", SpaceString); // clear events
      Blynk.virtualWrite(V27,0);

      setBothLabels(V40, SpaceString); // clear red button
      setBothLabels(V41, SpaceString); // clear green button
      
      s88=0;
      Blynk.virtualWrite(V39,s88); // s88 address
      shows88(0,SpaceString);
      
}

void showLimit() {
   setBothLabels(V6, String(limit / 10)); // speed limit
}

void showSpeed() {
  Blynk.virtualWrite(V5,String(speed) + "/" + String(dspeed));
  Blynk.setProperty(V5,"color",speed > limit ? BLYNK_RED:BLYNK_GREEN);
  Blynk.virtualWrite(V7,speed);
  Blynk.virtualWrite(V2,MAXSPEED - dspeed);
}

void setBothLabels(int pin,String label){
  Blynk.setProperty(pin,"offLabel",label);
  Blynk.setProperty(pin,"onLabel",label);
}

void mywriteV1() {
  if (train < 2) {
    //  to clear an image, set property opacity 0
     Blynk.setProperty(V0,"opacity",0); //clear the track image
     Blynk.setProperty(V4,"opacity",0); //clear the signal image
     limit=0;
     showLimit();
     for (byte i=V10; i < V18;i++) {  // hide functions
        setBothLabels(i," ");
        SetBackground(i);
     }
     Blynk.virtualWrite(V5," "); // speed indicator
     Blynk.virtualWrite(V2,MAXSPEED); // MAXSPEED is inverted
     Blynk.virtualWrite(V7,0); // current speed
  }
}
BLYNK_WRITE(V1) {    // Train selection
  train = param.asInt() ;  // note: there is a <none>=1 
  mywriteV1();
  rs.sendCabData('T',train);
}  // V1

BLYNK_WRITE(V2) {    // speed control
  dspeed = MAXSPEED - param.asInt() ;
  rs.sendCabData('G',dspeed);
  showSpeed();
}  // V2

BLYNK_WRITE(V3) {    // direction control
  int dir = param.asInt();
  forward =  1 == dir ;
  speed = 0;
  rs.sendCabData('D',dir);
  showSpeed();
  
}  // V3

void showPower(byte p) {
  switch (p) {
    case 1: {Blynk.setProperty(V21,"color", BLYNK_RED );Blynk.setProperty(V38,"offBackColor", BLYNK_RED );Blynk.virtualWrite(V38,0 );     break;} // off
    case 2: {Blynk.setProperty(V21,"color", BLYNK_GREEN );Blynk.setProperty(V38,"onBackColor", BLYNK_GREEN );Blynk.virtualWrite(V38,1 ); break;}  // on
    case 3: {Blynk.setProperty(V21,"color", BLYNK_YELLOW );Blynk.setProperty(V38,"offBackColor", BLYNK_YELLOW );Blynk.virtualWrite(V38,0 ); break;}  // halt
  } // power mode
  Blynk.virtualWrite(V21,p ); // ensure segment switch is aso set
}

BLYNK_WRITE(V8) { // acceleration
    int x = param[0].asFloat();
    int y = param[1].asFloat();
   // int z = param[2].asFloat(); // z always > 9 on my phone
   const byte alimit = 15;
  
   if (x > alimit || y > alimit) { // violent shake, shut off power!
       rs.sendCabData('P',0);
       Blynk.virtualWrite(V21,1); // update the segmented switch
       showPower(1);  // set the colors of the switch
      
      //Serial.println(F("STOP"));
    }
    
}
BLYNK_WRITE(V9) { // request log data
  int mode =  param.asInt() ;
    if (mode == 1) { // when pressed only
        rs.RScommand("{LOG}25");
    }
}

BLYNK_WRITE(V18) { // refresh track list
  int mode =  param.asInt() ;
    if (mode == 1) { // when pressed only
        rs.RScommand("{LST?}" + String(currentstation));
    }
}

BLYNK_WRITE(V20) { // update button
  
}

BLYNK_WRITE(V21) {    // power mode
  int mode =  param.asInt() ;
  rs.sendCabData('P',mode - 1);
  showPower(mode); 
  
}  // V21

/*
BLYNK_WRITE(V22) { // log
  
} //22
*/

void mywriteV23(){
  // clear existing labels
         showK83(1,"red","green"); // default value before getting data from Bw
        rs.RScommand("{K83?}" + String(k83adr)); // ask for the data
}

BLYNK_WRITE(V23) { // k83 address
   k83adr =  param.asInt() ;
   mywriteV23();     
} // 23

void showK83(byte rg, String Red, String Green) { // show the colors according to the rg (redgreen) value
  // dont change labels if red/green are empty
  const int rpin = V40;
  const int gpin = V41;
  static String lastRed;
  static String lastGreen;
  if ( Red != "") {lastRed = Red; Blynk.setProperty(rpin,"onLabel",lastRed);Blynk.setProperty(rpin,"offLabel",lastRed);} ;
  if (Green != "") {lastGreen = Green;  Blynk.setProperty(gpin,"onLabel",lastGreen);Blynk.setProperty(gpin,"offLabel",lastGreen);} ;

  if (rg ==1) {
    Blynk.virtualWrite(rpin,rg );Blynk.virtualWrite(gpin,0 );   // red
  }else{
    Blynk.virtualWrite(rpin,0 );Blynk.virtualWrite(gpin,1 );  //  green
  } // red/green mode
  
} // showK83

BLYNK_WRITE(V24) { // fine adjustment to k83 address
  k83adr = k83adr + param.asInt() ;
  Blynk.virtualWrite(V23,k83adr);
  mywriteV23();
  
  
} // 24

BLYNK_WRITE(V25) { // s88 fine adjustment
  s88 = s88 + param.asInt() ;
  Blynk.virtualWrite(V39,s88);
  mywriteV39();
} // 25

BLYNK_WRITE(V27) { // event list
  currentevent = param.asInt() ;
}

BLYNK_WRITE(V28) { // execute event
  int press = param.asInt() ; // assigning incoming value 
  if (press == 1 && currentevent != -1) {
    rs.RScommand("{CAB}E\21" + String(currentevent));
  }
}

BLYNK_WRITE(V29) {    // Network status
  int ask = param.asInt() ; // assigning incoming value 
  if (ask == 1) {
    showNetworkStatus(V29);
  }
}  // V29

void ResetStation() { // reset the track and destination controls, also called when we disconnect
  String SpaceString = " ";
  Blynk.setProperty(V31,"labels",SpaceString); // clear track list
  Blynk.virtualWrite(V31,0);
  Blynk.setProperty(V32, "labels", SpaceString); // clear destination list
  Blynk.virtualWrite(V32,0);
  Blynk.setProperty(V33, "offLabel", SpaceString);
  Blynk.setProperty(V42, "offLabel", SpaceString);  // train to cab button
  Blynk.setProperty(V18, "offLabel", SpaceString);  // refresh button
}

BLYNK_WRITE(V30) { // station
  currentstation = param.asInt() ;
  currentstationtrack = -1;
  currentdestination  = -1;
  ResetStation();
  rs.RScommand("{LST?}" + String(currentstation)); // get list of tracks and trains on them
}
BLYNK_WRITE(V31) { // station track
  currentstationtrack = param.asInt() ;
  currentdestination  = -1;
  Blynk.setProperty(V32, "labels", ""); // clear destination list
  Blynk.virtualWrite(V32,0);
  Blynk.setProperty(V33, "offLabel", " ");
  Blynk.setProperty(V18, "offLabel", "♻️");  // ♻️ refresh button
  Blynk.setProperty(V42, "offLabel", "🚇");  //🚇 put train into cab button
  rs.RScommand("{LSTD?}" + String(currentstation) + "\21" + String(currentstationtrack)); // get list of destinations
}

BLYNK_WRITE(V32) { // station track destination
  currentdestination  = param.asInt() ;
  Blynk.setProperty(V33, "offLabel", "➡️");
}

BLYNK_WRITE(V33) { // dispatch
   int press = param.asInt() ;
   if (press == 1 && currentdestination > 0 ) {
    rs.RScommand("{CAB}d\21" + String(currentstation) + "\21" + String(currentstationtrack) + "\21" + String(currentdestination)); 
  }
}

BLYNK_WRITE(V34) {    // Ignore s88s
   int mode =  param.asInt() ;
   rs.sendCabData('8',mode);
}  // V34

BLYNK_WRITE(V35) {    // manual control
   int mode =  param.asInt() ;
   rs.sendCabData('M',mode);
}  // V35

BLYNK_WRITE(V37) {    // stop train
   int mode =  param.asInt() ;
   if (mode == 1) { // only do anything on the initial press
    rs.sendCabData('S',0);
   } // 1
} 

BLYNK_WRITE(V38) {    // cab tab power button
  int mode =  param.asInt() ;
  rs.sendCabData('P',mode);
  showPower(mode+ 1); 
} 

void mywriteV39(){
     rs.RScommand("{S88?}" + String(s88)); // ask for the data
}

BLYNK_WRITE(V39) {    // s88 slider
  s88 =  param.asInt() ;
  mywriteV39();
} 

BLYNK_WRITE(V40) {    // k83 - red
  int press =  param.asInt() ;
  if (press == 1) {
    rs.RScommand("{CAB}k" + sDC1 + String(k83adr) + sDC1 + "R");
    mywriteV23();
  }
} 
BLYNK_WRITE(V41) {    // k83 - green
  int press =  param.asInt() ;
  if (press == 1) {
    rs.RScommand("{CAB}k" + sDC1 + String(k83adr) + sDC1 + "G");
    mywriteV23();
  }
} 

BLYNK_WRITE(V42) {    // get track train into cab
  int press = param.asInt() ;
   if (press == 1 && currentstationtrack > 0 ) {
    rs.RScommand("{CAB}d\21" + String(currentstation) + "\21" + String(currentstationtrack)); 
  }
} 



BLYNK_WRITE_DEFAULT() {  //  function keys
   int pin = request.pin - 10;   
   if (pin > 7 || pin < 0) return; // we only support f0 - f7
   int mode =  param.asInt() ;
   rs.sendCabData('n',pin, mode);
}  // fn buttons

void SetBackground(int pin) {
    Blynk.setProperty(pin, "onBackColor", BACKGROUND);
    Blynk.setProperty(pin, "offBackColor", BACKGROUND);
}

void setup() {

  Serial.begin(115200);
   Blynk.begin(auth, ssid, password); // get onto wifi network with Blynk
   printWifiStatus();

   Blynk.virtualWrite(V36, " "); // clear connection info
   ShowDisconnect();
   
   // set default k83 slider etc
   Blynk.virtualWrite(V23,k83adr);
   mywriteV23();
   // set default s88
   Blynk.virtualWrite(V39,s88);
   mywriteV39();

/*
  // set up button colors - only need to be run once
  SetBackground(V3);
  SetBackground(V37);
  SetBackground(V38);
  SetBackground(V29);
 SetBackground(V28);

 SetBackground(V18);
SetBackground(V33);
SetBackground(V42);
 */
 

   // show version of firmware
   showversion(V20);
   showNetworkStatus(V29); // and network status

  
  // set up RemoteSign
  rs.setFirmwareVersion(String(FW_VERSION));

  // define channel data
  rs.setChannelData(1, "CAB", "Phone Cab"); // cab control
 // rs.setBlynkPin(1, V1) ; // Train picker widget
  
  rs.begin(); // start server listening
} // setup

void loop() {
  rs.run();
  Blynk.run();

} // loop()

void BlynkVWriteInt_helper(uint8_t pin, uint8_t value) {
   Blynk.virtualWrite(pin, value);
   if (pin = 1) {
      train = value;
      mywriteV1();
      rs.sendCabData('T',train);
      };
}
void BlynkVWriteStr_helper(uint8_t pin, String text) { // char const *pchar) {
   if (pin == V36) { // disconnect notice
      train= -1;
      Blynk.virtualWrite(pin, 1);  // select the <none> train
   }
   Blynk.virtualWrite(pin, text);  
}
void BlynkCabData_helper(uint8_t type, String thedata) {
 //Serial.println(thedata);
 switch (type) {
  case 1: 
    thedata = "<none>" + sDC2 + thedata;
    // train list fallthrough
  case 17:
  case 18:
  case 19:  
  case 2: { // event list
       int r=0;
       int i;
       BlynkParamAllocated items(1023); // list length, in bytes 
        for (i=0; i < thedata.length(); i++) { 
         if(thedata.charAt(i) == '\x12')   {    // x12 is hex 18 DC2
            items.add(thedata.substring(r, i)); 
            r= i + 1; 
           } // DC1
        } // for
         items.add(thedata.substring(r, i)); // get the last item also
      // put the array into the virtual pin
      switch (type) { 
          case 1 : { Blynk.setProperty(V1, "labels", items);  break;   } // train list
          case 2 : { Blynk.setProperty(V27, "labels", items); break;  } // event data
          case 17: { Blynk.setProperty(V30, "labels", items); break;  } // station list
          case 18: { Blynk.setProperty(V31, "labels", items); break;  } // stationtrack list
          case 19: { Blynk.setProperty(V32, "labels", items); break;  } // destination list
      } // switch train/events
      break;
  } // 1 & 2
  case 3 : {  // function list
      // function names must come in F0 through f7
      String label; 
      byte vpin = V10;
      int r=0;
      int i;
      for (i=0; i < thedata.length(); i++) { 
         if(thedata.charAt(i) == '\x12')   {    // x12 is hex 18 DC2
            label = thedata.substring(r, i);
            if (label =="") label = " "; // treat nuls as blank
            if (label == " ") { // no function
              SetBackground(vpin); // blank it
            }else{
              Blynk.setProperty(vpin,"onBackColor",BLYNK_BLUE);
              Blynk.setProperty(vpin,"offBackColor",BLYNK_GREEN);
            }
            setBothLabels(vpin++,label);
            r= i + 1; 
           } // DC2
        } // for
        label = thedata.substring(r, i); // and the last one
        if (label =="") label = " "; // treat nuls as blank
            if (label == " ") { // no function
              SetBackground(vpin); // blank it
            }else{
              Blynk.setProperty(vpin,"onBackColor",BLYNK_BLUE);
              Blynk.setProperty(vpin,"offBackColor",BLYNK_GREEN);
            }
        setBothLabels(vpin++ , label);  
        
        // fill unspecified functions empty
        label = " ";
        for (i=vpin; i < 18; i++) { 
           SetBackground(vpin);
           setBothLabels(vpin++, label);  
        }
        
        break; 
  } // function names
 case 4 : {  // function states
      // function states must come in F0 through f7
      String label; 
      byte vpin = V10;
      int r=0;
      int i;
      byte state;
      for (i=0; i < thedata.length(); i++) { 
         if(thedata.charAt(i) == '\x12')   {    // x12 is hex 18 DC2
            label = thedata.substring(r, i);
            state = label.toInt();
            Blynk.virtualWrite(vpin++, state);
            r= i + 1; 
           } // DC2
        } // for
        label = thedata.substring(r, i); // and the last one
        state = label.toInt();
        Blynk.virtualWrite(vpin, state);
        
        break; 
  } // function
  case 5 : { // image file
    if (thedata == "" ) {
       Blynk.setProperty(V0,"opacity",0); //clear the track image
    }else{

      Blynk.setProperty(V0, "urls",imagebase + thedata );
      Blynk.setProperty(V0,"opacity",100);
    }
    break;
  }
  case 6 : { // signal aspect
    if (thedata == "" ) {
      Blynk.setProperty(V4,"opacity",0); //clear the track image
    }else{

      Blynk.setProperty(V4, "urls",signalbase + thedata );
      Blynk.setProperty(V4,"opacity",100);
    }
    break;
  }
   case 7 : { // speed limit
    limit = thedata.toInt();
    showLimit();
    break;
  }
   case 8 : { // speed and desired speed
    String s = rs.getValue(thedata, DC1, 0) ;
    speed= s.toInt();
    s = rs.getValue(thedata, DC2, 1) ;
    dspeed= s.toInt();
    showSpeed();
    break;
  }
  case 9 : { // direction
    uint8_t dir = thedata.toInt();
    Blynk.virtualWrite(V3, dir);
    break;
  }
  case 10 : { // Power settings
    uint8_t p = thedata.toInt() ;  // 0 = off  1  = on 2 = halt
    Blynk.virtualWrite(V21, ++p );  // 1 = off  2  = on 3 = halt
    showPower(p);
    break;
  }
  case 11 : { // Ignore s88s
    uint8_t p = thedata.toInt() ;  // 0 = off  1  = on
    Blynk.virtualWrite(V34, p );  
    break;
  }
  case 12 : { // Manual control
   // uint8_t p = thedata.toInt() ;  // 0 = off  1  = on
    Blynk.virtualWrite(V35, 0 );  //always turning off
    break;
  } 
  case 13 : { // update one function
    String s = rs.getValue(thedata, DC1, 0) ;
    int f = s.toInt(); // funtion number
    s = rs.getValue(thedata, DC1, 1) ;
    int v = s.toInt();  // new value
    Blynk.virtualWrite(f + 10, v); // f0 uses V10 so we add 10
    break;
  } 
  case 14 : { // shows RemoteSign version
    Blynk.virtualWrite(V36,rs.getValue(thedata, DC1, 0));
    if (thedata == "Disconnected") {
        // reset things to indicate no connection
        ShowDisconnect();
    }else{
    
      if (train > 0) { 
        rs.sendCabData('T',train); // refresh current train
      } 
    }
    break;
  }

  case 15 : { // LOG data
    terminal.println(thedata);
    //terminal.flush(); // do this later perhaps using a timer?
    break;
  }
  case 16 : { // flush LOG data. Called when Bw has finished sending log data
    terminal.flush(); 
    break;
  }

  // 17 is station list (handled above)
  // 18 is stationtrack list (handled above)
  // 19 is destination list  (handled above)

  case 20 : {  // K83 address state and description   thedata 0 = R/G  1 = red name 2 = green name
       byte rg;
       if (rs.getValue(thedata, DC1, 0) == "R") {
          rg=1; // red
       }else{
          rg=2; // green
       }
       showK83(rg,rs.getValue(thedata, DC1, 1),rs.getValue(thedata, DC1, 2));
       break;
  }
   case 21 : { // s88 monitor status - we get state and description
      String param = rs.getValue(thedata, DC1, 0);
      shows88(param.toInt()==1,rs.getValue(thedata, DC1, 1));
      break;
   }
  
  default :
    Serial.print(F("Unknown type for BlynkCabData_helper "));Serial.println(type);
 } // switch type

} //BlynkCabData_helper

void executeEvent(int channel,String param) {
} // executeEvent
void showversion(int pin){
  Blynk.setProperty(pin,"label","Ver " + String(FW_VERSION));
}

void showNetworkStatus(int pin){
  long rssi = WiFi.RSSI();
  String IP = WiFi.localIP().toString();
  Blynk.setProperty(pin,"label",IP); // put IP address onto Network button
  Blynk.setProperty(pin,"offLabel","📶 " + String(rssi) + "dBm");

}
6 Likes

This is the correct url:

Pete.

1 Like

thanks, Pete, I fixed it.

That’s spectacular. The Blynk app is impressive, but the model train layout is spectacular. Thank you for taking the time to put together this blog.

Are you sure we can’t justify embedding an ESP8266 on each locomotive? :wink:

Congratulations, Dale!

Joe

1 Like

Thanks Joe,

Well actually each loco does already have a processor. Called decoders, each with its own unique address, and that is how we control the speeds and functions - via a digital signal in the track power.

I have been thinking of adding some ESP8266s to control lighting along the length of the trains though, but the current draw is rather substantial, so I would probably have to include a MOSFET and gate driver circuit too… so perhaps another day…

I figured as much, but I’m pretty sure the HO train layout on my ping pong table as a kid didn’t have a digital signal in the track power. :wink:

What other functions are communicated via the track power digital signal?

the digital bus sends commands for setting any address to a binary value (red/green) which allows turnouts, signals and almost anything imaginable to be switched.
Another bus returns sensor signals to the controller - which I use to monitor the progress of the train as it moves about so I can maintain a state model of the layout.

My software talks to the controller over a serial port exchanging commands and sensor data.

This creates a system that allows full automation with prototypical accelerations, sound effects, light transitions, etc.

Some system use a CAN BUS and the ESP32s have a CAN bus controller built in! Some guys are now driving everything with Arduino type processors!

I am now running three ESP8266s with my layout - one for the room lights, one for driving a miniature screen and now also the cab controller.

Warning: it is an amazing world of capabilities and if you look too closely you may be getting your old trains out and retrofitting decoders in them. You might not be seen for months.

What you have done is inspiring. I have used an ESP32 to control an 0 guage engine.
Would you be interested in helping me control twin coil turnouts with Blynk? I found this article and would like to do this. https://www.alfray.com/trains/arduino_turnouts.html
https://www.alfray.com/trains/arduino_turnouts.html

Hi peter,

glad you liked the project.

It looks like the links you have for the twin coil are very comprehensive. I have not done anything like that myself.

From the Blynk you would need two buttons to set the two modes.

Dale

Dale,
I got it done. I now have a twin coil controller with LED widget indicators.
Peter

1 Like