Twilight Zone -> Bluesky

I’ve owned a Twilight Zone pinball machine for a couple of decades now. My friend Grue and I used to play one in the mall way back when, and it’s always been my favorite pinball.

The operator’s manual contains references to an “optional printer kit, part number 63110.” I’d kept an eye out for one of these over the years, but never had any luck. Apparently they were mostly just produced for pre-release machines, to get diagnostics back to Williams when they were still working out all the details, so they’re very rare. Operators could dump all of the audit data – several pages worth! – to a serial printer, all at a screaming 9600bps.

Then last year, I came across a thread on Pinside where some clever fellow had designed a drop-in replacement for the board that provided a nice modern USB port instead of a DB9. I reached out to him and he still had a couple on hand, so I bought one from him in July 2024. It took a while to get it sorted out due to a flaky USB cable, but I finally had it hooked up and running in December, with help from Grue. Here’s a photo of what the installation looks like. The printer board is the blue board on J501 on the Dot Matrix Controller board. (The yellow test probe provides power to the printer board.)

So with the board installed and working, now came the fun part of the project – writing the software to do something useful with it!

I originally thought of exposing the data via SNMP and just pulling it into an off-the-shelf piece of software like LibreNMS to graph it, but I abandoned that idea when I realized that the data from the TZ just doesn’t change often enough for that to be interesting. I did write a quick perl script to do it anyway, as a way to start parsing the audit output from the Pi, and just for the visual :

I then decided that maybe posting to a blog post, or to bluesky would be a more suitable approach.

I’m leaving the Pi on all the time. The TZ is only powered on when it’s going to be played. This gives us 4 basic states that we need to detect/navigate :

  1. TZ Powered off
    • Pi just loops looking for the TZ to power on. (Look for activity on the USB hub)
  2. TZ Powered on, PI does not recognize the serial device
    • One requirement for the new printer board is that the TZ needs to be powered on before the Pi, otherwise the Pi doesn’t see the serial device. Annoying.
  3. TZ Powered on, PI recognizes the serial device
    • The TZ will only dump audit data to the serial board when prompted to do so via the built-in menu, or when it receives a carriage return on the serial board while the coin door is open.
  4. TZ Powered on, PI recognizes the serial device, and the coin door is open
    • We know we’re in this state when we get serial data after sending a CR to the board. The TZ will only send audit data once, then it will wait for a game to be played or a power off event before sending audit data again.

The most difficult problem was #2 – how to get the Pi to properly initialize/detect the USB device when the Pi was already on when the TZ was powered on. I spent a lot of time flailing around with various USB reset methods I found on the web, most of which were outdated or otherwise non-workable. I finally found the right utility in “uhubctl“, which supports the Pi5’s hub and resets it in a way that brings up the printer board properly.

Here is what is in the log when the TZ is powered on while the Pi is already on. It doesn’t negotiate properly for some reason (the error -71), so no device is created in /dev.

Jan 26 14:29:47 tz kernel: usb 1-1: new full-speed USB device number 103 using xhci-hcd
Jan 26 14:29:47 tz kernel: usb 1-1: device descriptor read/64, error -71

But after we bounce the Pi’s USB hubs using uhubctl, the USB device is initialized and brought up properly.

Jan 26 14:30:40 tz kernel: usb usb1-port1: attempt power cycle
Jan 26 14:30:43 tz pinmon[55324]: powering on usb hub
Jan 26 14:30:44 tz kernel: usb 1-1: new full-speed USB device number 127 using xhci-hcd
Jan 26 14:30:44 tz kernel: usb 1-1: New USB device found, idVendor=0403, idProduct=6001, bcdDevice= 6.00
Jan 26 14:30:44 tz kernel: usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
Jan 26 14:30:44 tz kernel: usb 1-1: Product: FT245R USB FIFO
Jan 26 14:30:44 tz kernel: usb 1-1: Manufacturer: FTDI
Jan 26 14:30:44 tz kernel: usb 1-1: SerialNumber: AB0K1N2L
Jan 26 14:30:44 tz kernel: ftdi_sio 1-1:1.0: FTDI USB Serial Device converter detected
Jan 26 14:30:44 tz kernel: usb 1-1: Detected FT232R
Jan 26 14:30:44 tz kernel: usb 1-1: FTDI USB Serial Device converter now attached to ttyUSB0
Jan 26 14:30:44 tz mtp-probe[56032]: checking bus 1, device 127: "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb1/1-1"
Jan 26 14:30:44 tz mtp-probe[56032]: bus: 1, device: 127 was not an MTP device
Jan 26 14:30:44 tz mtp-probe[56064]: checking bus 1, device 127: "/sys/devices/platform/axi/1000120000.pcie/1f00200000.usb/xhci-hcd.0/usb1/1-1"
Jan 26 14:30:44 tz mtp-probe[56064]: bus: 1, device: 127 was not an MTP device
Jan 26 14:30:44 tz pinmon[55324]: usb hub power cycled, retrying
Jan 26 14:30:44 tz pinmon[55324]: opened serial device on /dev/ttyUSB0

So now that the Pi sees the USB device properly, we can mark the start of a session of pinball gaming, and start sending a CR to the printer board regularly to see when it sends data back. (The paradigm being – turn on the pinball machine, play a bunch of pinball, then when you’re done, open the coin door so the audit data can get dumped to the Pi, wait for that, then turn off the pinball machine.)

Jan 26 14:30:44 tz pinmon[55324]: usb hub power cycled, retrying
Jan 26 14:30:44 tz pinmon[55324]: opened serial device on /dev/ttyUSB0
Jan 26 14:30:44 tz pinmon[55324]: new session started : Sun Jan 26 14:30:44 2025
Jan 26 14:30:44 tz pinmon[55324]: empty read buffer - pinging WPC for audit data

Gaming done, coin door opened, we wait a minute for the loop to come around again and send a CR to the serial board, and voila, we start getting the audit data :

an 26 14:50:36 tz pinmon[55324]: timeout in serial read loop, 9 loops remaining
Jan 26 14:50:36 tz pinmon[55324]: empty read buffer - pinging WPC for audit data
Jan 26 14:50:37 tz pinmon[55324]: got data in serial read loop, buffer length is now 255
Jan 26 14:50:37 tz pinmon[55324]: got data in serial read loop, buffer length is now 510
Jan 26 14:50:38 tz pinmon[55324]: got data in serial read loop, buffer length is now 765

I could look for text in the last line of the audit output to determine when it’s done printing, or look at the length of data received, but it was easiest to just set up a short timeout loop where it assumes it’s complete after receiving no new data for 10 loop iterations (1 second each) :

Jan 26 14:51:07 tz pinmon[55324]: timeout in serial read loop, 2 loops remaining
Jan 26 14:51:08 tz pinmon[55324]: timeout in serial read loop, 1 loops remaining
Jan 26 14:51:09 tz pinmon[55324]: timeout in serial read loop, 0 loops remaining
Jan 26 14:51:09 tz pinmon[55324]: done with serial read loop
Jan 26 14:51:09 tz pinmon[55324]: wrote 8364 bytes to /root/pinmon-data/buffer-2025-01-26-14-30.txt, session start reset
Jan 26 14:51:09 tz pinmon[55324]: calling pinmon2sql.pl...
Jan 26 14:51:09 tz pinmon[55324]: calling pinmon-report.pl...
Jan 26 14:51:12 tz pinmon[55324]: done with pinmon-report.pl

We write that out to a text file for safe keeping. Then we kick off pinmon2sql.pl, which reads the text file and stores it in a simple MySQL database for easier access/processing.

Then we call pinmon-report.pl, which generates the data (basically figures out deltas for relevant/interesting data), and then posts it to BlueSky. I used the excellent Bluesky perl examples from this Medium post to connect up to my Bluesky account and post the data. I had to hack it up a bit to get it to create threads and to add hashtag handling. (The AT API pushes a lot of things I would have expected the back end to handle on to the thing calling the API, but it’s not too bad.)

All told, it’s about a thousand lines of somewhat sloppy perl code. You can see an example of the final result here on BlueSky.