Z Scale Layout – Arduino Control and Track Plan Updates
Actual trackwork on the layout has stalled recently. I failed to order all of the components I needed to build my previous plan and so used a few other off-cuts to, at least, make a circuit I could run trains on. First testing of trains was done with a 9v battery and the results were sub-optimal. Whilst browsing Jaycar recently, I saw a motor-controller shield for Arduino with 4 outputs and decided that was the way to go. It even has two servo controllers... I wonder what I can animate?
I also had an Arduino Uno Wifi, so decided, instead of building some physical box with switches and dials, I'd make a webpage to control the layout! I've programmed this Uno Wifi before, so go and check that post out for caveats when dealing with this unit. The main point being that you need to program the WIFI module independent from the Arduino itself, using the serial connection as the conduit to communicate between the units. This all then gets a little difficult as you may also want to use that Serial port for logging output.
The Web Server
Fortunately, there are hundreds of examples to run with when creating these mini-webservers. I used the example over here, adjusting the page to control two throttles. Whenever a user presses a button, it updates the internal throttle variable and then sends both throttle values out over the serial port.
#include <ESP8266WiFi.h> const char* ssid = "wifi accesspoint name"; const char* password = "wifi password"; WiFiServer server(80); String header; unsigned long currentTime = millis(); unsigned long previousTime = 0; const long timeoutTime = 2000; void setup() { delay(3500); Serial.begin(115200); Serial.print("AP:"); 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()); Serial.println("OK"); server.begin(); } int t1 = 100, t2 = 100; int lastT1, lastT2; 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) { currentTime = millis(); if (client.available()) { // if there's bytes to read from the client, char c = client.read(); // read a byte, then 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(); if (header.indexOf("GET /1/less") >= 0) { t1 -= 5; } else if (header.indexOf("GET /1/off") >= 0) { t1 = 100; } else if (header.indexOf("GET /1/more") >= 0) { t1 += 5; } else if (header.indexOf("GET /2/less") >= 0) { t2 -= 5; } else if (header.indexOf("GET /2/off") >= 0) { t2 = 100; } else if (header.indexOf("GET /2/more") >= 0) { t2 += 5; } if (t1 < 0) t1 = 0; if (t1 > 200) t1 = 200; if (t1 < 0 && t1 > -15) t1 = -15; if (t1 > 0 && t1 < 15) t1 = 15; if (t2 < 0) t2 = 0; if (t2 > 200) t2 = 200; if (t2 < 0 && t2 > -15) t1 = -15; if (t2 > 0 && t2 < 15) t1 = 15; if (lastT1 != t1 || lastT2 != t2) { Serial.print("THR:"); Serial.print(t1); Serial.print(":"); Serial.println(t2); lastT1 = t1; lastT2 = t2; } // Display the HTML web page client.println("<!DOCTYPE html><html>"); client.print("<head><meta name=\"viewport\" "); client.println("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.print("<style>html { font-family: Helvetica; display: inline-block;"); client.println("margin: 0px auto; text-align: center;}"); client.print(".button { background-color: #195B6A; border: none;"); client.println("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>Train Controller</h1>"); client.println("<h1>Throttle 1</h1>"); client.print("<p>Requested Speed: "); client.print(t1 - 100); client.println("</p>"); client.println("<p><a href=\"/1/less\"><button class=\"button button2\"><<</button></a> "); client.println("<a href=\"/1/off\"><button class=\"button button2\">Stop</button></a> "); client.println("<a href=\"/1/more\"><button class=\"button button2\">>></button></a></p>"); client.println("<h1>Throttle 2</h1>"); client.print("<p>Requested Speed: "); client.print(t2 - 100); client.println("</p>"); client.println("<p><a href=\"/2/less\"><button class=\"button button2\"><<</button></a> "); client.println("<a href=\"/2/off\"><button class=\"button button2\">Stop</button></a> "); client.println("<a href=\"/2/more\"><button class=\"button button2\">>></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 } } } header = ""; client.stop(); } }
Note that the ESP2866 throws out a stupid line of text when it switches on. It's at 74880 baud or somesuch and, if you're already listening on the arduino-side, you'll receive a random pile of junk. Due to this, I've added a 3 second delay to the start of both units.
Arduino Motor Control Code
So, we have the webserver sending the throttle over the serial port... let's now read it and control the motors as expected.
#include <AFMotor.h> AF_DCMotor innerLoop(1, MOTOR12_64KHZ); AF_DCMotor outerLoop(2, MOTOR12_64KHZ); bool is_ready = false; void setup() { //just to skip the bios text output from ESP8266 delay(1000); Serial.begin(115200); Serial.println("Starting..."); innerLoop.run(RELEASE); outerLoop.run(RELEASE); } void loop() { while(Serial.available() == 0) { } String in_str = Serial.readString(); Serial.print(in_str); while(in_str.length() > 0) { if (in_str.startsWith("OK")) { Serial.println("OKAY!"); } else if (in_str.startsWith("THR:")) { String this_str = in_str.substring(0, in_str.indexOf('\n')); in_str = in_str.substring(this_str.length()); Serial.print("Dealing with: "); Serial.println(this_str); int next_colon = this_str.indexOf(':', 4); int tt1 = this_str.substring(4, next_colon).toInt(); int tt2 = this_str.substring(next_colon + 1).toInt(); //Serial.println(next_colon); int t1 = (tt1 - 100) * 2.5; int t2 = (tt2 - 100) * 2.5; Serial.print("Throttles: "); Serial.print(t1); Serial.print(", "); Serial.println(t2); if (t1 == 0) { innerLoop.run(RELEASE); } else { innerLoop.run(t1 >= 0 ? FORWARD : BACKWARD); if (t1 < 0) t1 *= -1; innerLoop.setSpeed(t1); } if (t2 == 0) { outerLoop.run(RELEASE); } else { outerLoop.run(t2 >= 0 ? FORWARD : BACKWARD); if (t2 < 0) t2 *= -1; outerLoop.setSpeed(t2); } } in_str = in_str.substring(1); } }
The AFMotor library was fun to wrangle. You choose the polarity via the run call, passing either FORWARD or BACKWARD. Turns out though, if you pass a negative value into the speed, it'll reverse the direction automatically. I initially was setting both and wondering why my train wasn't reversing!
Layout Updates
With the hobbled-together track, I'd made the basic figure-of-eight dual-track loop and was nearly bored of it once the controller was working properly. Friends also then came over for dinner that evening and asked how more complicated I was going to make the track... slightly insulted, I went back to the drawing board. Instead of dual-track all the way round, let's reduce it to single with a few loops and a yard.
The result is ridiculous, but nicely uses the track I'd accumulated... only needing a little bit more. No problems with that as I got to order more trains at the same time. That track is still in the mail, so instead here's a video of the current trains.
Excuse the audio. How nice is that Marklin Santa Fe F7A? Love it. The Akia 485-Series hated the incline straight away and no amount of throttle got it going. I also need to fix the throttle increments as you can see the trains only start moving after 4 presses. Work to do!