September 13, 2020

FORMATDISPLAY

The only positive thing to come out of this absolutely trash year is the extra time that I’ve had on working on dumb projects. This project in particular has no purpose whatsoever other than being the realization of an in-joke about file formats. Last few years I’ve switched to almost exclusively buying new music in FLAC or other lossless formats to get closer to technical medium transparency for audio (bandcamp is great). Of course the times you hear anything different between an MP3 encoded in 320 or V0 compared to a FLAC is quite few but we thought it would be funny to only play FLAC at ANDERSTORPSFESTIVALEN and highlight it in a dumb way.

Idea is simple: Display what Format is currently playing (not what song is currently playing) in the ATP bar using some 90s style display technology.

There is this dumb movie prop from the movie Tomorrow Never Dies that is supposed to be a master GPS encoder”. What struck me about this prop is not the idea that a box would hold some offline cryptography key, rather that it comes with a huge nonsense segment display. Just seeing this makes me think this is the dumbest prop I’ve seen, which is why I wanted to use segment displays to show the format.

segment

Reading the current track

To start with, we need to create some sort of API that we can poll to get the currently playing track from the playback MacBook Pro. I personally use Swinsian but there is often Spotify being used in the bar as well, so supporting both of these is a must. Neither Swinsian nor Spotify has a REST/DBUS/Whatever API to get song data out of but it turns out that they both expose AppleScript variables to be compatible with iTunes plugins. Knowing that, creating some sort of shim around AppleScript to publish the variables with a HTTP API would solve a lot of problems.

AppleScript is absolutely terrible. It’s the worst scripting language I’ve had the pleasure to deal with but after some massaging we have this code that gets the state of both Swinsian and Spotify and outputs as some sort of mocked JSON.

on is_running(appName)
    tell application "System Events" to (name of processes) contains appName
end is_running

on psm(state)
    if state is «constant ****kPSP» then
        set ps to "playing"
    else if state is «constant ****kPSp» then
        set ps to "paused"
    else if state is «constant ****kPSS» then
        set ps to "stopped"
    else
        set ps to "unknown"
    end if
    return ps
end psm

if is_running("Swinsian") then
    tell application "Swinsian"
        set wab to my psm(player state)
        set sfileformat to kind of current track
        set strackname to name of current track
        set strackartist to artist of current track
        set strackalbum to album of current track
        set sws to "{\"format\": \"" & sfileformat & "\",\"state\": \"" & wab & "\",\"song\": \"" & strackname & "\",\"artist\": \"" & strackartist & "\",\"album\": \"" & strackalbum & "\"}"
    end tell
end if

if is_running("Spotify") then
    tell application "Spotify"
        set playstate to my psm(player state)
        set trackname to name of current track
        set trackartist to artist of current track
        set trackalbum to album of current track
        set spf to "{\"format\": \"" & "OGG" & "\",\"state\": \"" & playstate & "\",\"song\": \"" & trackname & "\",\"artist\": \"" & trackartist & "\",\"album\": \"" & trackalbum & "\"}"
    end tell
end if

set output to "{ \"spotify\": " & spf & ", \"swinsian\": " & sws & "}"

With this out of the way, we can easily invoke this AppleScript from Go with osascript and capture the output of stdout and serve this over HTTP with gin. I’ve published the end result as go-swinsian-state on my Github if you ever find yourself needing to do something this ugly.

Displaying this on alphanumeric segment displays

Working with segment displays is annoying. It’s essentially one LED per segment and they are wired as a crosspoint matrix meaning that you need tons of pins to drive this. Luckily for me, people have been annoyed at this before and created driver chips, such as the MAX7219 which allows you to control the entire display using just one serial connection. This takes a lot of the headache off the table and allows me to use much smaller headroom microcontrollers.

For this project, a key feature is that the display can’t be connected using Serial/SPI/I2C to the host computer, rather it has to pull data over WLAN. There’s an array of microcontrollers out there now with WiFi support but my personal favorite is the Espressif series of microcontrollers. The ESP8266/ESP32 is in my mind almost a revolution in microcontrollers, offering a huge amount of connectivity with an insane amount of I/O at an unbeatable price (around $3-$4 per chip). Since this project is a one-off, designing a logic board seemed too much effort so I shopped around for an ESP32 in a nice form factor.

Adafruit has a nice series of Feathers” which is essentially a smaller format Arduino with focus on battery power and size. They ship the ESP32-WROOM model for around $20 which is a fair price for the design and ecosystem. It also turns out they have this Featherwing” which is akin to Arduino shields with both the LED segment driver and displays. $14 is a reasonable price for this as well so BOM so far is $34 which gives us a microcontroller with WiFi and a alphanumeric segment display that can fit the words FLAC/MP3/OGG/AAC.

The fun thing about working with the Arduino framework is that there are good libraries for these components available, so stringing all this together is less than 125 lines of code. I published the PlatformIO project on my github as FORMATDISPLAY but the main code is so short that I’ll include it here.

#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <Adafruit_I2CDevice.h>
#include <Adafruit_GFX.h>
#include "Adafruit_LEDBackpack.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include "Settings.h"

Adafruit_AlphaNum4 alpha4 = Adafruit_AlphaNum4();

IPAddress ip;
HTTPClient http;
String url = String(storedURL);
int errCount = 0;

void display(uint8_t a, uint8_t b, uint8_t c, uint8_t d)
{
    alpha4.writeDigitAscii(0, a);
    alpha4.writeDigitAscii(1, b);
    alpha4.writeDigitAscii(2, c);
    alpha4.writeDigitAscii(3, d);
    alpha4.writeDisplay();
}

void displayC(const char *s) {
    for (int i = 0; i < 4; i++) {
        if (s[i] == 0x00) {
            alpha4.writeDigitAscii(i, ' ');
        } else {
            alpha4.writeDigitAscii(i, s[i]); 
        }
    }
    alpha4.writeDisplay();
}

void setup()
{
    //Initialize serial and alphanumeric driver.
    Serial.begin(115200);
    Serial.print("START");
    alpha4.begin(0x70);
    alpha4.setBrightness(brightness);

    // WIFI CONNECTION
    display('C', 'O', 'N', 'N');
    WiFi.begin(storedSSID, storedPASSWORD);

    int wifiRetry = 0;

    while (WiFi.status() != WL_CONNECTED)
    {
        delay(500);
        wifiRetry++;

        if (wifiRetry > 100) {
            ESP.restart();
        }
    }

    // PRINT IP 
    display('W', 'I', 'F', 'I');
    ip = WiFi.localIP();

    char fip[3];
    itoa(ip[3], fip, 10);
    char veb[4] = {'-', fip[0], fip[1], fip[2]};
    displayC(veb);
}

void loop()
{
    if (WiFi.status() == WL_CONNECTED)
    {
        http.begin(url);
        int httpCode = http.GET();
        if (httpCode > 0)
        {
            const size_t capacity = JSON_OBJECT_SIZE(2) + 2 * JSON_OBJECT_SIZE(5) + 940;
            DynamicJsonDocument doc(capacity);

            deserializeJson(doc, http.getString());

            JsonObject spotify = doc["spotify"];
            const char *spotify_format = spotify["format"]; // "OGG"
            const char *spotify_state = spotify["state"];   // "paused"

            JsonObject swinsian = doc["swinsian"];
            const char *swinsian_format = swinsian["format"]; // "MP3"
            const char *swinsian_state = swinsian["state"];   // "playing"

            if (strcmp(swinsian_state, "playing")==0) {
                displayC(swinsian_format);
            } else if (strcmp(spotify_state, "playing")==0) {
                displayC(spotify_format);
            } else {
                display(' ', ' ', ' ', ' ');
            }
            errCount = 0;
        } else {
            errCount++;
        }
        http.end();
    }
    else
    {
        display('E', 'N', 'E', 'T');
    }

    if (errCount > 10) {
        display('E', 'R', 'R', 'D');
    }

    delay(500);
}

It’s as simple as that and it displays the currently playing format. The code is basically 4 distinct parts. The first is bootstrapping the WiFi and the segment display. Second is acquiring an IP address from the network. Third is generating a HTTP request against the chosen endpoint and the last is parsing the JSON that’s returned and sending it to the display.

result

There is of course a discussion to be had about the wastefulness of storing an entire JSON blob on the heap of a microcontroller. The design above returns a pretty massive JSON blob with a lot of unwanted data which the microcontroller has to poll. When considering these tradeoffs I think it’s important to remember just how fast the ESP32-WROOM actually is. This microprocessor is a dual core design running at 240 MHz, compare this to a Arduino Uno (ATmega328P) which runs at 16 MHz and it’s obvious that we don’t have to be as careful with wasting cycles here. Building solutions this way allows one to easily prototype the end result and experiment, since everything is just JSON APIs.

Enclosure

Even though it already looks pretty neat, you really only want to see the segment display and hide away the rest of the feather. I designed this extremely simple enclosure in Fusion360 in which the segment display pushed through an opening and is constrained by the footprint of the featherwing. The back cover is designed to snap into the chassi and stay in place using the friction from the PLA, a design I usually do for smaller enclosures like this. There is a platform extruded in the middle to push the Feather into the hole.

fl

3D printed this small enclosure in about 2 hours on the Prusa with using the Prusament Galaxy Black.

enclosure

This project is really meaningless. At the same time sometimes these projects does not have to be more than fun to work on. If you think this project is meaningless, just wait until my next post on the other project.


Projects


Previous post
California Wildfires When I moved to San Francisco, dealing with wildfires on a yearly cadence was not one of the challenges I expected. That has turned out to be a
Next post
Autumn