Playrail, upgraded
A short tour of my experience upgrading Playrail to allow remote control for the trains

So a little while ago, my wife picked up a huge collection of Playrail train, track pieces and scenery. Playrail is a great track and train system for kids, my daughter and I have had loads of fun with it. It's very similar to the Trackmaster system.

Fun times with Playrail, notice the cool 3d printed trees and buildings painted by me and my 3 year old daughter

The trains are very simple electrically, just a motor connected to a battery through a switch.

I decided it would be fun to try to create a system to be able to remotely control the trains.

Time to upgrade!

I had a goal of making the electronics fit inside the existing train body if possible, as such getting the smallest possible components was critical. The train itself is powered by a AA battery and there is a small amount of extra space in the battery area. The space is limited to about 4.5cm by 2.5cm with a couple of centimeters of height available.

The battery compartment on the left is the only place to put the electronics
There is some space in the body of the train just above the battery slot

With these constraints in mind I settled on the idea of having a wifi capable microcontroller in the train with the ability to send instructions to it via http requests. After a bit of trail and error, I settled on the following components for the build:

NodeMcu-Esp8266 microcontroller ~$15 AUD

This board is super cheap and easily programmable using the built in micro usb port and the Arduino IDE. Crucially, it includes Wifi and just about fits in to the battery compartment of the train. It has plenty of gpio pins and also allows PWM modulation of the pin output. This will be important later as the motor control board uses PWM to allow variable speed control of the motor.

Initially I was experimenting with the Omega2 board which is a full fledged Linux based system. It includes SD card + wifi capability, something similar to a Raspberry Pi but with cut down performance. I destroyed 2 of these boards before giving up on it.

The first Omega2 I destroyed by shorting out the 5v to 3.3v rails of my power supply thus frying the board with 5v of power. The second one may not be destroyed, but it seemed to get bricked while doing the firmware upgrade. I think the issue with the 2nd board was the LDO regulator I was using for testing required at least 4.5v in order to regulate it down to a stable 3.3v, the wall wart I had was providing less than 4.5v under load. As such I think during the firmware upgrade the voltage dipped and the board reset itself. It never came back up after that. I may order the Omega2 dock at some point and try to reflash the omega firmware..

After reading on the forums it seems many people seem to have similar issues with the Omega2. I needed a system that was a bit more bulletproof against noob errors and finally settled on the NodeMcu-Esp8266

SparkFun Motor Driver - Dual TB6612FNG (1A) ~ $5AUD

I needed a tiny motor driver that would allow me to have variable speed control, this was available at my local electronics shop so I picked it. This particular board allows driving 2 motors. The single driver version of this board if it exists may be a little smaller but this fit in to my space requirements.

This board comes with headers pre-soldered and a headerless version, I chose headerless as the headers themselves are absolutely massive and would totally blow my space budget.

350mah adafruit lipo battery ~$13AUD

This was the smallest battery I could find that looked like it would have enough juice to power the system.

AP3429A 3.3V Buck converter ~$5AUD

Powering off 1 single lipo cell was a bit tricky, this was the only board I could find that would regulate the 3.7-4.2v of the lipo cell down to a stable 3.3v for the microcontroller, luckily it is absolutely tiny.

Extras

On top of these components I picked up a few other items to help me with the build:

  • Heat shrink tubing - allows me to tidy up the cabling a bit
  • Lipo battery charger - so I can charge the battery after each play
  • Jst connectors so I can plug on to the NodeMcu-Esp8266 rather than soldering to it directly

All up the components and extras cost was about $50. If I built a second one I wouldn't need the extras again and it would only be about $37, quite reasonable! Using a raw Esp8266 rather than the NodeMcu development board would drop another $10 off the price, but that might be a bit advanced for me!

Putting it all together

This is how everything needs to be wired up:

  • The ground pins of the battery, voltage regulator, motor controller and NodeMcu-Esp8266 need to be connected together.
  • The battery's positive needs to go to the Vin of the regulator, I used a JST connector here to allow the battery to be disconnected for charging.
  • The 3.3v out of the regulator is connected to the Vcc of the motor controller and 3.3v of the NodeMcu-Esp8266. The motor controller can actually provide a different voltage for the motor rather than 3.3v that is used to drive the logic part of the board. This is what the Vm pin is for on the motor controller board. Since I decided to save one wire I just used a solder bridge to join the Vm to the Vcc pins and have the motor be driven by 3.3v along with the rest of the electronics. This is slightly less efficient as the motor power will be coming out of the voltage regulator instead of coming directly from the battery. The advantage of this approach though is that as the battery voltage drops, the motor will still receive a constant voltage thus the motor speed for a given PWM duty cycle should be a bit more deterministic.
  • The motor controller has a 'stand by' pin, this must be pulled high for the board to do anything. I used a small wire to jumper this pin to the Vcc pin since I always want the motor driver enabled when the battery is plugged in.
  • The NodeMcu-Esp8266 has multiple options to power it, you can provide 5v to the Vin pin, 3.3v to the 3.3v pin, or simply plug in a micro usb cable, I chose to use the 3.3v pin since that is what the regulator outputs. I used a 2pin jst connector to plug in to the 3.3v/gnd of the NodeMcu-Esp8266.
  • The output pins of the NodeMcu-Esp8266 D5, D6,D7 are connected to PWMA, Ain1, Ain2 of the motor controller, these are used to control the speed and direction of the motor. You could use any 3 pins for this, though I picked these 3 as they were next to each other and didn't have any special requirements to be floating/pulled low/high during boot. I used a 3pin jst here connector on the NodeMcu-Esp8266 side to prevent having to solder anything on the NodeMcu-Esp8266.
  • Finally, the Ao1/Ao2 of the motor controller board connect to the motor, since we can control motor direction with the NodeMcu-Esp8266 pins the polarity here doesn't matter, whichever way it is connected we can correct for it in software.

The original Playrail motors are designed to run off a single 1.5v battery, with this design the motor will be fed 3.7v and run way too fast to be usable. This can be taken care of in software in the next step, pulse width modulation will reduce the effective voltage being fed to the motor.

When reprogramming the board, I'm not sure if the voltage regulator will be damaged by plugging in the micro usb cable, to be safe, I disconnect the 3v/gnd pins of the NodeMcu-Esp8266 to prevent the voltage regulator board from seeing a voltage. If anyone knows if it is safe to leave these plugged in while programming please let me know!

Here's what it looks like after assembly:

It took me a while to get everything compact enough to fit in there, a bit of trial and error with a lot of double sided mounting tape finally led me to a layer cake of

  • NodeMcu-Esp8266 on the bottom with the legs pointing up.
  • The battery sitting on the NodeMcu-Esp8266 in between its legs.
  • The motor controller and voltage regulator board sitting on the battery

With a bit of fiddling, it just manages to fit inside the train. The original on/off switch for the train shown in blue does not disconnect the power any more. It does however disconnect the motor physically from the wheels. It's best just to leave the switch in the original on position.

With the train cover on, you can't really tell there is any difference except the train being slightly heavier

This is how it looks with the charger attached to the battery, that tiny charger plugs in to any usb port or wall wart

I was initially planning to 3d print an enclosure around the electronics so it would be tidy even after taking the train cover off, unfortunately there is just no extra space to fit it in.

The code!

With the hardware part out of the way, I wanted to do the absolute minimum to get everything up and running. I wasn't sure if it would all work reliably enough and be strong enough to survive a session with my daughter. It really needed a good play test before I invest more time in to writing too much software for it.

I decided on the simplest possible software solution. The NodeMcu-Esp8266 will run a tiny web server that exposes a small api to change the status of the 3 motor driving pins D5, D6, D7. It's then simple enough to use a browser to connect to the server and manipulate the pins directly to control the train.

The NodeMcu provides a web api that a regular browser can call in to and change pin voltage

I found almost exactly what I needed for an initial POC here, thanks to Rui Santos for such a great tutorial on writing a web server. I modified this code to drive the correct pins. Furthermore, the code only controlled 2 pins where I need 3 so I made these changes too.

The pins D6, D7 need to have one of them driven high and the other one low to control the motor direction, flipping them around will reverse the motor direction.

Pin D5 is connected to PWMA of the motor control board, this is the pin we need to use pulse width modulation on to control the motor speed. I hard coded it to drive it at a 25% duty cycle leaving the default 1024 hz frequency. This measured as approximately 0.8v which is a little lower than the 1.2-1.5v the motor normally needs, but was good enough for an initial test.

This is the full code I used after my modifications, if you wish to use this code, make sure you change the wifi ssid and password before uploading to your NodeMcu:

/*********
  Based on the work of Rui Santos
  Complete project details at http://randomnerdtutorials.com  
*********/

// Load Wi-Fi library
#include <ESP8266WiFi.h>

// Replace with your network credentials
const char* ssid     = "yourssid";
const char* password = "yourpassword";

// Set web server port number to 80
WiFiServer server(80);

// Variable to store the HTTP request
String header;

// Auxiliar variables to store the current output state
String output14State = "off";
String output12State = "off";
String output13State = "off";

// Assign output variables to GPIO pins
const int output14 = 14;
const int output12 = 12;
const int output13 = 13;

// Current time
unsigned long currentTime = millis();
// Previous time
unsigned long previousTime = 0; 
// Define timeout time in milliseconds (example: 2000ms = 2s)
const long timeoutTime = 2000;

void setup() {
  Serial.begin(115200);
  // Initialize the output variables as outputs
  pinMode(output14, OUTPUT);
  pinMode(LED_BUILTIN, OUTPUT);     // Initialize the LED_BUILTIN pin as an output
  pinMode(output12, OUTPUT);
  pinMode(output13, OUTPUT);
  // Set outputs to LOW
  digitalWrite(output14, LOW);
  digitalWrite(output12, LOW);
  digitalWrite(output13, LOW);

  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // Print local IP address and start web server
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  server.begin();
}

void loop(){
  WiFiClient client = server.available();   // Listen for incoming clients

  if (client) {                             // If a new client connects,
    Serial.println("New Client.");          // print a message out in the serial port
    String currentLine = "";                // make a String to hold incoming data from the client
    currentTime = millis();
    previousTime = currentTime;
    while (client.connected() && currentTime - previousTime <= timeoutTime) { // loop while the client's connected
      currentTime = millis();         
      if (client.available()) {             // if there's bytes to read from the client,
        char c = client.read();             // read a byte, then
        Serial.write(c);                    // print it out the serial monitor
        header += c;
        if (c == '\n') {                    // if the byte is a newline character
          // if the current line is blank, you got two newline characters in a row.
          // that's the end of the client HTTP request, so send a response:
          if (currentLine.length() == 0) {
            // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
            // and a content-type so the client knows what's coming, then a blank line:
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println("Connection: close");
            client.println();
            
            // turns the GPIOs on and off
            if (header.indexOf("GET /14/on") >= 0) {
              Serial.println("GPIO 14 on");
              
              analogWrite(LED_BUILTIN, 1000);   // Turn the LED on by making the voltage LOW
              output14State = "on";
              analogWrite(output14, 256);
              // digitalWrite(output14, HIGH);
            } else if (header.indexOf("GET /14/off") >= 0) {
              Serial.println("GPIO 14 off");
              digitalWrite(LED_BUILTIN, HIGH);   // Turn the LED on by making the voltage LOW
              output14State = "off";
              analogWrite(output14, LOW);
            } else if (header.indexOf("GET /12/on") >= 0) {
              Serial.println("GPIO 12 on");
              output12State = "on";
              digitalWrite(output12, HIGH);
            } else if (header.indexOf("GET /12/off") >= 0) {
              Serial.println("GPIO 12 off");
              output12State = "off";
              digitalWrite(output12, LOW);
            } else if (header.indexOf("GET /13/on") >= 0) {
              Serial.println("GPIO 13 on");
              output13State = "on";
              digitalWrite(output13, HIGH);
            } else if (header.indexOf("GET /13/off") >= 0) {
              Serial.println("GPIO 13 off");
              output13State = "off";
              digitalWrite(output13, LOW);
            }
            
            // Display the HTML web page
            client.println("<!DOCTYPE html><html>");
            client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
            client.println("<link rel=\"icon\" href=\"data:,\">");
            // CSS to style the on/off buttons 
            // Feel free to change the background-color and font-size attributes to fit your preferences
            client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
            client.println(".button { background-color: #195B6A; border: none; color: white; padding: 16px 40px;");
            client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
            client.println(".button2 {background-color: #77878A;}</style></head>");
            
            // Web Page Heading
            client.println("<body><h1>ESP8266 Web Server</h1>");
            
            // Display current state, and ON/OFF buttons for GPIO 14  
            client.println("<p>GPIO 14 - State " + output14State + "</p>");
            // If the output14State is off, it displays the ON button       
            if (output14State=="off") {
              client.println("<p><a href=\"/14/on\"><button class=\"button\">ON</button></a></p>");
            } else {
              client.println("<p><a href=\"/14/off\"><button class=\"button button2\">OFF</button></a></p>");
            } 
               
            // Display current state, and ON/OFF buttons for GPIO 4  
            client.println("<p>GPIO 12 - State " + output12State + "</p>");
            // If the output12State is off, it displays the ON button       
            if (output12State=="off") {
              client.println("<p><a href=\"/12/on\"><button class=\"button\">ON</button></a></p>");
            } else {
              client.println("<p><a href=\"/12/off\"><button class=\"button button2\">OFF</button></a></p>");
            }

            client.println("<p>GPIO 13 - State " + output13State + "</p>");
            // If the output13State is off, it displays the ON button       
            if (output13State=="off") {
              client.println("<p><a href=\"/13/on\"><button class=\"button\">ON</button></a></p>");
            } else {
              client.println("<p><a href=\"/13/off\"><button class=\"button button2\">OFF</button></a></p>");
            }

            
            client.println("</body></html>");
            
            // The HTTP response ends with another blank line
            client.println();
            // Break out of the while loop
            break;
          } else { // if you got a newline, then clear currentLine
            currentLine = "";
          }
        } else if (c != '\r') {  // if you got anything else but a carriage return character,
          currentLine += c;      // add it to the end of the currentLine
        }
      }
    }
    // Clear the header variable
    header = "";
    // Close the connection
    client.stop();
    Serial.println("Client disconnected.");
    Serial.println("");
  }
}

Results

Happy to say after all the effort it finally did exceed all my expectations!

I wasn't sure if the battery I used would last long enough but it did last a good 45 minutes before we had to turn it off and go to bed, I will be doing some more tests on the longevity next time we play to see how long it goes until the battery fully dies.

I was also happy the whole set up withstood a proper play test with my daughters rough handling, derailments and train crashes. It would have been useless if it didn't stand up to the punishment.

Another thing I was worried about was if the NodeMcu-Esp8266 would drop off the wifi or randomly not respond to my start/stop requests. Happy to say it worked flawlessly with no noticeable lag between clicking the start/stop button and seeing the train start/stop moving.

First successful test!
Full play test, success!

There are still a couple of issues to solve.

When the NodeMcu-Esp8266 powers up, it gets assigned a random IP address, in order to browse to it, I currently need to manually figure out what the IP address is using the admin interface of my router. This is not ideal. The problem will become even worse when I build a 2nd train with this same set up. I will be writing a central web server to run off my home server that will do discovery of all the NodeMcu-Esp8266 trains. I'm hoping I can do a port scan of the subnet to accomplish this.

I would like to have a variable speed control with a reverse option so I'll be putting together a small react based front end that will display all trains on the network and allow control over all of them centrally.

Stay tuned for the next installment where I've hopefully solved these issues!

Leave a comment

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