I’ve always liked trains, and since working on projects like the Nerf Tank and HTCPCP, I’ve wanted to build remote control for my O-Guage model trains. In college, the couple of attempts I made were plauged by issues with power instability and faults in attempts to build my own motor controller with MOSFETs – I definitely did not have the electrical engineering skills for that.
A few years after I graduated college, I started looking into the problem again, and decided to largely avoid the problems I’d had with motor controls by simply buying a pre-made motorcontroller board that was definitely big enough to handle the load (actually it’s almost certainly completely overkill).
So after taking the guts out…
…I put together a Lego platform to keep the experimental parts insulated from the metal train body…
…and started working on figuring out the hardware and software.
The hardware I settled on includes a wildly overkill 10A motor controller (the power supplies for these model trains have absolute maximum ratings of around 3A), and a device called Moteino, which is a small board containing all the components of an Arduino on the same board as an RF69 wireless tranciever. This allows me to use one board for the radio and computer, and then the only parts I have to figure out are for providing power to the computer and wiring everything up.
For the power system and wiring connections, I designed a custom PCB that contains a rectifier (the trains use AC power, so that has to be converted to DC for any of the computer parts), a voltage regulator, and a bunch of capacitors for voltage smoothing as the train’s connection to the rails isn’t the most reliable, so balancing out spikes in power is very useful. The Moteino slots right on to my custom board, which also provides most of the external connections.
For the software to run the train, I built a server which keeps track of the trains that have contacted it over the radio and handles communication with them, including retrying command messages if the train doesn’t respond. For communication with the outside world, I created a gRPC protocol that could be used to get information about existing trains and send commands to them.
I originally built the server entirely in Python using Python’s async to deal with
independently tracking communication attempts, waiting, and retries on messages to and
from trains, without needing loads of threads. However, Python’s async uses independent
tasks, which can be as difficult to coordinate between as independent threads. This meant
a lot of coordination problems around sending commands and cancelling obsolete commands.
For example, if a task was started to send the command
set speed 10 and then the user
changed the speed again so we now need to send
set speed 100, the server would need to
set speed 10 task and start a new
set speed 100 task, which meant a lot of
In later experementation with Rust, I found that Rust’s async, which uses a polling model rather than a callback model, was better able to deal with the state changes involved, since you could easily cancel futures by dropping them. Originally I could not find a good gRPC library in Rust, so I used a rust library that let you write Python modules in Rust to bridge between a Rust core for managing trains and a Python server for talking gRPC, but I later found an acceptable Rust gRPC server and switched entirely to that.
On the client side, I wrote an app in Flutter for selecting trains and controlling them. I even made a custom control for a giant dial that you turn to set the train’s speed. Setting up network connections is sometimes problematic: I used Multicast DNS to advertise the train server on the local network, but not all routers support mDNS, so sometimes explicit connect-by-ip is needed.
The software on the train itself is pretty simple. I originally just hard-coded a radio-network address, and then had the software just broadcast its current speed and direction periodically, with an internal speed-ramp to prevent sudden speed changes even if, e.g., the user instantly changes speed from full reverse to full foward. I later updated the radio address system to have new trains announce themselves with a GUID and had the server assign them a radio address. If this controller were to become a real product, the radio address assignment from GUID means that every train can have a unique ID while still using far fewer bits in the radio packets to identify which train a command is for. Otherwise we would run out of radio addresses after a few thousand trains.
An issue that occurred in early versions was that commands were not originally sequenced in any way. Trains would Ack commands just by transmitting their current state, and the server would just check if the state matched the state it had most recently commanded the train into. However, this would cause issues especially in the event of very rapid command-state-changes, such as a short blast of the horn. Sometimes, the horn would get stuck on until the user pushed the button again.
The way this could happen is that if you pressed the horn twice in quick succession, delay in processing queues combined with wireless interference could mean that the server would see the acknowledgement for a previously sent message as if it was the acknowledgment for its most recent message. Here’s a sequence diagram showing how that would work:
To solve this, I just changed the acknowledgement system to include a short sequence number, so the server would generally be able to tell if an ack is from its most recent command or not, unless for some reason you’ve gone through 256 commands in a very short time.
Easy LAN Connections
As noted above, I used mDNS to allow the client app to discover the train server on the local network. However, this has some issues, particularly because some routers don’t allow the multicast messages needed for mDNS to work properly. I’m not sure what a good solution for this is, and I’m not sure how e.g. Chromecast works around these kinds of issues. I could try some other service discovery protocols like UPnP, though I think that also uses UDP broadcast and may have the same problems.
For now I work around the issue by using IP based connection on networks where mDNS doesn’t work.
One option I may consider in the future is a system where the server registers its LAN address on a server on the wider internet. Then the client can both scan mDNS and query the internet server for devices on its LAN. The internet server would use the public IP that it reads from the server/client to filter which devices it thinks are on the same network.
AC Waveform Control
I chose to use the original horn that came with the train, rather than replacing it with my own sound driver and sound effects. The way these train’s sound boards work is that they look at the AC power coming in and check for a DC offset. If the AC power has a DC offset, the horn is triggered (if a negative DC offset is detected, the bell is triggered, if one is available).
In the current version of the train, I decided to hack around this by using a second, very small motor controller to power the sound board with an AC square wave, which I could then apply a DC offset to by changing the midpoint of the square wave. This works for my train, but apparently trains from other years are more particular about wanting an AC sine wave and don’t like the square wave I give them.
On the second train I tried to build, the square wave output just makes the sound system scream in agony (lots of extra noise on the speaker, and unreliable random activation of sound effects). So to make this work for more types of trains, I need to actually produce something more like a sine wave with a DC offset.
One approach I have started working on is using a triac to gate the actual AC input in order to produce a DC offset. To do this, you just set it up so that when the horn should active, the sine wave is cut off partway in one direction of the cycle but not the other. Unfortunately I haven’t enough electrical engineering knowledge to figure out how to tune the resistor-capacitor delay circuits to get this timing right.
A slightly dumber version of the triac option which I have managed to make work is to just use a series of diodes in each direction (each diode produces a bit of voltage drop) with the triac set up to bypass the diodes. When the horn is off, the triac just stays on, but to get a DC offset, you just switch off the triac for half of the wave in order to get the voltage drop of the diodes in only one direction. This option is horrendously space-inefficient with the number of diodes needed to achieve an appropriate voltage drop. Here’s the circuit diagram for this approach:
Here is what the train looked like, operating in tests, with the hardware all still exposed.
And here’s a demo of the train running fully assembled.
I’d like to build a future version of this controller with the motor controller built into the same circuit board as the power system, horn controller, and the arduino/RF69 systems. Basically compact it all down to a single board. However, that requires me to solve the horn issues and basically design my own custom Arduino variant which is a lot more than I’ve done before.