Monday, November 20, 2017

Building a Seeburg Wall-O-Matic Interface (Part 6)

Developing the Software

This post is definitely not going to completely describe the software. The software is always going to be a work-in-progress, so follow the Git repository for more specific updates. Instead, its going to describe a bit about the development setup, and some basics about what the software is doing.

Wall-O-Matic Microcontroller Interface ESP8266 Code [github.com]

Preparing a development rig

The first step in this stage of the project was to actually have a development board to work with. While I could have simply used my real circuit board (as described in the previous post), doing so had a number of disadvantages:
  • Somewhat tedious to switch in-and-out of flash download mode
  • Required an extra adapter to connect to USB
  • Designed to be powered by a bulky transformer connected to AC mains
  • No way to test the coin switch outputs without a multimeter
  • No way to test the pulse decoding input without a real wallbox
In other words, using the real circuit board was not a solution well suited to an office desk environment that was outside of my electronics workbench.

To do the majority of the software development, I really only needed to test the ESP-12S itself and basic interactions on its I/O pins. Thankfully, this did not actually require the full circuit board. So, to make my life easier, I cobbled together a little breadboard development rig.

Development Rig Breadboard

For the ESP-12S part of this rig, I used an Adafruit Feather HUZZAH. Its a convenient breakout board that includes a USB port, reset button, and some nifty wiring that allows the USB serial controller to automatically switch the ESP-12S in and out of flash download startup mode.

To simulate the signal pulses of the wallbox, I used an Arduino Nano. This module also has its own USB port, and I/O pins that could easily be programmed to simulate any digital pulse stream I needed. Since I'm not testing the full filtering path, I really only need to simulate a clean series of logic-level pulses. The code that runs on this Arduino can be found here: [PulseSim.ino]

For the coin switches, three LEDs in different colors were enough. After all, I just needed to test that I could make them blink.

Since the Arduino is a 5V system, and the ESP8266 is a 3.3V system, I leveraged my existing bin of components to link these together. I used a spare opto-isolator and inverter, which did the trick. This had the benefit of making the two microcontroller modules completely isolated from each other, so they could be powered independently.

In schematic form, it looked like this:
Development Rig Schematic

Setting up the toolchain

The easiest way to get the development tools up and running was to leverage the "esp-open-sdk" project.  This project is essentially a Makefile and some scripts that downloads, compiles, and configures everything you need to write code for the ESP8266. This part was surprisingly painless, and decently covered in that project's README.

Decoding Pulses (For Real)


When I was originally figuring out the pulse protocol and filtering circuitry, I used an Arduino-based test decoder. The code for this basically used the "pulseIn()" function in a loop. Upon further investigation, I found that "pulseIn()" just busy-waits on the I/O pin. While perfectly fine for basic testing, this was definitely not a good solution for a complete system.

The approach I took here involved configuring an interrupt to trigger on any transition on the signal pulse's input pin. Inside the interrupt handler function, I then populated an array with the pulse durations and pulse gap widths and kicked off a timer.  If no pulses happened for a certain amount of time, the timer callback function would then inspect the array and determine what the song selection was. This approach ended up working quite well.

The code that implements this is here: user_wb_selection.c

Inserting Coins (For Real)

While precise timing is really not necessary for the wallbox's coin switch mechanism, I still went and measured how long a real coin would actually trip the switches for (40-60ms). I then wrote some simple timer functions that would trigger the selected coin switch for the appropriate amount of time.

The code that implements this is here: user_wb_credit.c

Interfacing with Sonos

This is the step where things started to get complicated. Some of the blog posts I read at the outset of this project had led me to believe it was just a matter of spitting the right HTTP POST at a Sonos speaker's IP address, but is anything actually that simple?  Especially if you want a robust and reliable solution?

As I worked through this process, I had decided that there were certain things I definitely wanted to have:
  • Sonos device configuration should be easy (i.e. no hard-coded IP addresses)
  • Can't make any assumptions as to the state of the Sonos speaker
  • The Sonos device's playlist should be treated as if it was a jukebox
The goal was not only to make setup easy, but to also ensure that I should never have to fiddle the Sonos app on my phone to get things into (or back into) a functional state. The Wall-O-Matic interface should function as standalone as technically possible.
Of course doing all of this meant that I needed to figure out quite a bit more of the Sonos protocol than a single enqueue request.

To figure all this out, I spent a lot of time staring at Wireshark while fiddling with both the official Sonos app and the node-sonos-http-api project's utility commands.

I'm not going to even attempt to document the Sonos protocols here, as that would take way too much time and effort to write up. I'll just be giving a short summary of the parts I used, then linking to my code that implements them.

Discovery Protocol

The Sonos discovery protocol is based on SSDP, and involves two kinds of device discovery: explicit searches and notifications.

Explicit searches begin by sending a UDP broadcast to IP address 239.255.255.250 (port 1900) with the following payload:

M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:reservedSSDPport
MAN: ssdp:discover
MX: 1
ST: urn:schemas-upnp-org:device:ZonePlayer:1

All Sonos devices on the network will then reply to the sender, by sending something like this to the source port of the discovery request:

HTTP/1.1 200 OK
CACHE-CONTROL: max-age = 1800
EXT:
LOCATION: http://192.168.1.42:1400/xml/device_description.xml
SERVER: Linux UPnP/1.0 Sonos/37.12-45270 (ZP120)
ST: urn:schemas-upnp-org:device:ZonePlayer:1
USN: uuid:RINCON_D6E477CEC684A1400::urn-schemas-upnp-org:device:ZonePlayer:1
. . .

Additionally, Sonos devices will periodically broadcast their presence on port 1900 without any discovery requests. When that happens, the payload looks similar:

NOTIFY * HTTP/1.1
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age = 1800
LOCATION: http://192.168.1.42:1400/xml/device_description.xml
NT: upnp:rootdevice
NTS: ssdp:alive
SERVER: Linux UPnP/1.0 Sonos/37.12-45270 (ZP120)
USN: uuid:RINCON_D6E477CEC684A1400::upnp:rootdevice
. . .

Regardless of how it came in, I collected the IP address, port, and UUID from these payloads. Then, to get the zone name itself, I made an HTTP GET to the following URL:

http://192.168.1.42:1400/status/zp

The response to this was a big XML blob that contained the zone name, as well as other device information.

This info was then collected in a data structure, and later used as follows:
  • IP and Port - Used to actually connect to the device and to send it commands
  • UUID - Used as the reference to the device kept in the software configuration, and used as part of certain control requests. 
  • Zone Name - Used for display purposes, so the UI could give friendly information on which zone was selected.
The code that implements this is here: user_sonos_discovery.c

Control Requests

To instruct the Sonos device to actually do things (e.g. add song to queue, play, query status, etc), I had to implement the following control requests:
  • AddURIToQueue - Adds a file or stream URI to the playback queue
  • GetPositionInfo - Gets position info on the current track
  • SetAVTransportURI - Sets the transport URI to play (e.g. stream source or local file queue)
  • Seek - Select the queued track to play
  • Play - Start or resume playback
The code that implements this is here: user_sonos_request.c

Event Subscriptions

Unfortunately, the normal control interface provides no way to distinguish between the playing and paused states. The only way to do this, was to listen for event notifications. There's another protocol Sonos devices support where you can tell them to notify you whenever there's a state change. Thankfully, these notifications include the play/paused state.

The code that implements this is here: user_sonos_listener.c

Putting it all together

At boot, the device discovery listener is started and a discovery request is broadcast. As soon as the configured Sonos device UUID is detected, it is set as the active device and an event subscription request is made.

When a song selection is made on the wallbox, the following sequence of operations are initiated:
  • Construct a track URI for the selected song
  • Add the track URI to the Sonos device queue (AddURIToQueue)
  • Get the current position info (GetPositionInfo)
  • If the current transport is not a file URI, then switch to the local queue (SetAVTransportURI)
  • If the current transport is a file URI, and currently playing, then do nothing
  • Otherwise:
    • If not currently playing, start playing
    • If on a previous track, and not currently playing, then seek to the added track and start playing
    • If on a previous track, and paused, then seek to the next (or added) track and start playing.

The code that implements this is here: user_sonos_client.c

Providing a User Interface

At runtime, the wallbox itself kinda is the user interface for the project. Well, with one exception: inserting coins. I needed some way to instruct my board to "insert a nickel/dime/quarter" remotely, either via a manual interaction (i.e. click a button on an app/website) or via some sort of home automation setup. To accomplish this, I decided I'd simply expose some sort of basic HTTP API for triggering the coin switch relays.

At setup time, it was a different story. There was actually a lot to configure for this thing to work correctly:
  • Wi-Fi Network Setup
  • Selecting the Sonos device to play through
  • Selecting the type of the connected wallbox
  • Configuring which song file to play for each wallbox selection code
Some might simply bake all this into the firmware. I really didn't want to do this, since you'd never design a "real" product like that.

So the first thing I did, was to embed a web server into the device. To do this, I leveraged the libesphttpd project (and its sample esphttpd app). This project provides a simple webserver written against the ESP APIs, and a fair amount of sample/utility code to handle support functions (Wi-Fi setup and firmware updating) that I knew I was going to need.

As I began to explore this project, I soon noticed that it hadn't been updated in a while. However, there seemed to be a number of forks that had been picking up improvements and fixes. Ultimately, I settled on using Chris Morgan's fork of the project.

After several iterations, I ended up with a (mobile friendly) landing page that looked like this:
Local Web Interface

The setup and firmware pages were pretty much straight lifts from the esphttpd sample project, albeit with some minor style tweaks. The Sonos zone selection page was also fairly simple.  The most complex of these pages was the "Wallbox Configuration" page:

Wallbox Configuration Page
The "wallbox type" determines how many song selections there are, how they're numbered, and what the format of the wallbox's signal pulses are. (Contrary to a previous post, I did eventually get the model "200" mostly working and added code to support its signal pulse format.  The format is different from the "100", and actually slightly simpler.)

The (long) "base folder path" and the (short) "song" filenames are combined to form the playback URI for each song selection. Buttons help automate the otherwise-tedious entry process.

The layout of this page reflects the approach I took towards configuring the playback URIs. While I could have allowed a completely custom per-song-selection URI, doing so would have required quite a lot of configuration memory.

The ESP SDK has APIs for "safe" persistent data work in triplets of 4KB flash pages. So 50KB of URI data (for a 200-selection wallbox) would inflate to 150KB of flash.  With a custom replacement for those built-in APIs, I could probably drop this down a bit, but it would still be more complexity than I wanted to deal with.

So instead, I decided that it made more sense to prepare a folder on the file server just for wallbox-triggered playback. Given that you'd have to organize your music by song codes anyways (to print title strips), it didn't seem like that big of a deal. This way you'd have a single "wallbox" folder with simple and short file names,

By doing this, on-device configuration was mostly automatic. In my own setup, I really only had to configure the base path. For the songs, I just went with the default names and only changed them if my files were something other than MP3s (like M4A or FLAC).

The code that implements this is here: user_webserver.c
The associated HTML and assets are here: html/

Conclusion

Combining everything above with some nasty looking Perl CGI on my home webserver, and fumbling my way through Google Assistant and IFTTT, I eventually ended up with something like this:


No comments: