Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/benz206/SpotifyMacroBoard/llms.txt

Use this file to discover all available pages before exploring further.

Base URL

All endpoints are accessed via HTTPS on your personal server:
https://benzhou.tech/api/
Replace benzhou.tech with your own domain when setting up your personal server.

Authentication

All endpoints use password-based authentication via URL parameter:
/api/{endpoint}/{password}/{...}
The password is defined in SampleCredentials.h and validated by the server before processing requests.

Endpoints

Manage playback state

Controls Spotify playback by executing various actions.
GET /api/manageState/{password}/{action}
password
string
required
Authentication password matching server configuration
action
string
required
Action to perform. Must be one of: playPause, skip, back, loop, vdec, vinc, shuffle

Actions

playPause
action
Toggle play/pause state. Mapped from pause/play button (GPIO 25).Source: src/src.ino:366-369
void pausePlay() {
    updateState('p');
    nextCurrentCheck = millis() + 100;
}
skip
action
Skip to next track. Mapped from skip button (GPIO 26).Source: src/src.ino:371-375
void skip() {
    updateState('s');
    nextCurrentCheck = millis() + 100;
    updateCurrent();
}
back
action
Go to previous track or restart current track. Mapped from back button (GPIO 14).Source: src/src.ino:361-364
void back() {
    updateState('b');
    nextCurrentCheck = millis() + 100;
}
loop
action
Toggle repeat/loop mode. Mapped from loop button (GPIO 13).Source: src/src.ino:359
void repeat() { updateState('r'); }
vdec
action
Decrease volume. Mapped from volume decrease button (GPIO 5).Source: src/src.ino:349-352
void volumeDecrease() {
    updateState('v', 0);
    updateScreen();
}
vinc
action
Increase volume. Mapped from volume increase button (GPIO 12).Source: src/src.ino:354-357
void volumeIncrease() {
    updateState('v', 1);
    updateScreen();
}
shuffle
action
Toggle shuffle mode. Mapped from shuffle button (GPIO 4).Source: src/src.ino:347
void shuffle() { updateState('f'); }

Request example

// From src/src.ino:202-205
wifiClient.print("GET /api/manageState/" + PASSWORD + "/" + actionString +
                 " HTTP/1.1\r\n" + "Host: benzhou.tech\r\n" +
                 "Connection: Keep-Alive\r\n\r\n");
wifiClient.flush();
HTTP request:
GET /api/manageState/your-password/playPause HTTP/1.1
Host: benzhou.tech
Connection: Keep-Alive

Response

The server should return an HTTP 200 status on success. The ESP32 does not parse the response body for this endpoint.
{
  "success": true
}

Get current playback

Retrieves the current playback state including track information, progress, and album art color.
GET /api/getCurrent/{password}
password
string
required
Authentication password matching server configuration

Request example

// From src/src.ino:218-220
wifiClient.print("GET /api/getCurrent/" + PASSWORD + " HTTP/1.1\r\n" +
                 "Host: benzhou.tech\r\n" +
                 "Connection: Keep-Alive\r\n\r\n");
HTTP request:
GET /api/getCurrent/your-password HTTP/1.1
Host: benzhou.tech
Connection: Keep-Alive

Response

Returns JSON with current playback information:
title
string
required
Current track title. Displayed on OLED screen (line 2-3).Example: "Bohemian Rhapsody"Extracted at: src/src.ino:248
extractValue("title", response, title);
artist
string
required
Artist name. Displayed on OLED screen (line 4).Example: "Queen"Extracted at: src/src.ino:249
extractValue("artist", response, artist);
album
string
required
Album name. Stored but not displayed on screen due to space constraints.Example: "A Night at the Opera"Extracted at: src/src.ino:250
extractValue("album", response, album);
duration
integer
required
Track duration in seconds.Example: 354 (5:54)Extracted at: src/src.ino:251, 259
extractValue("duration", response, durationRaw);
duration = durationRaw.toInt();
progress
integer
required
Current playback position in seconds. Used to render progress bar.Example: 120 (2:00)Extracted at: src/src.ino:252, 260
extractValue("progress", response, progressRaw);
progress = progressRaw.toInt();
paused
boolean
required
Whether playback is currently paused. When true, progress counter stops incrementing.Example: falseExtracted at: src/src.ino:253, 256
extractValue("paused", response, pausedRaw);
paused = pausedRaw == "true";
volume
integer
required
Current volume level (0-100). Displayed as icon in top-right corner.Example: 75Icon mapping (src/src.ino:140-148):
  • 67-100: volume_3 (full)
  • 34-66: volume_2 (medium)
  • 1-33: volume_1 (low)
  • 0: volume_0 (muted)
Extracted at: src/src.ino:254, 258
extractValue("volume", response, volumeRaw);
volume = volumeRaw.toInt();
color
array
required
RGB color array [R, G, B] extracted from album artwork. Each value is 0-255.Example: [45, 52, 71]Used to set RGB LED strip color for ambient lighting that matches the album art.Extracted at: src/src.ino:263-276
color = response.substring(response.indexOf("[") + 1,
                           response.indexOf("]"));

int comma1 = color.indexOf(",");
int comma2 = color.indexOf(",", comma1 + 1);

red = color.substring(0, comma1 + 1).toInt();
green = color.substring(comma1 + 1, comma2 + 1).toInt();
blue = color.substring(comma2 + 1, color.length()).toInt();
The color is faded smoothly over 256 steps when the track changes (src/src.ino:292-304).

Response example

{
  "title": "Bohemian Rhapsody",
  "artist": "Queen",
  "album": "A Night at the Opera",
  "duration": 354,
  "progress": 120,
  "paused": false,
  "volume": 75,
  "color": [45, 52, 71]
}

Response parsing

The ESP32 uses a custom JSON parser to extract values:
// From src/src.ino:158-171
void extractValue(const String& key, const String& json, String& result) {
    String quoteKey = "\"" + key + "\":";
    int start = json.indexOf(quoteKey);
    if (start != -1) {
        int end = json.indexOf(",", start + quoteKey.length());
        if (end == -1) {
            end = json.indexOf("}", start + quoteKey.length());
        }
        result = json.substring(start + quoteKey.length(), end);
        result.trim();
        result.remove(0, 1);
        result.remove(result.length() - 1);
    }
}
This lightweight parser avoids the memory overhead of a full JSON library like ArduinoJson.
The response JSON must be properly formatted with keys in this exact order: title, artist, album, duration, progress, paused, volume, color. The parser is not order-independent.

Polling intervals

The ESP32 polls endpoints at different intervals:
EndpointIntervalConstantSource
/api/getCurrent5 secondscurrentDelay = 5000src/src.ino:25, 395-398
RSSI check10 secondsRSSIDelay = 10000src/src.ino:24, 389-393
Progress update1 secondtimeDelay = 1000src/src.ino:26, 400-406
LED fade step4 msfadeDelay = 4src/src.ino:27, 408-411
// From src/src.ino:395-398
if (millis() > nextCurrentCheck) {
    nextCurrentCheck = millis() + currentDelay;
    updateCurrent();
}

Connection handling

The ESP32 maintains a persistent HTTPS connection using HTTP Keep-Alive:
// From src/src.ino:194-201
if (!wifiClient.connected()) {
    if (!wifiClient.connect("benzhou.tech", 443)) {
        for (int i = 0; i < RGB_LED_NUM; i++) LEDs[i] = CRGB::Red;
        FastLED.show();
        return;
    }
    yield();
}
Connection parameters:
  • Protocol: HTTPS (TLS/SSL)
  • Port: 443
  • Host: benzhou.tech (configurable)
  • Keep-Alive: Enabled for connection reuse
  • Timeout: 5000ms for response (src/src.ino:222-229)

Error responses

The ESP32 provides visual feedback for errors using the RGB LED strip:
Error conditionLED colorSource
Connection failedRedsrc/src.ino:196-198
Response timeout (>5s)Yellowsrc/src.ino:225-228
Invalid response formatOrangesrc/src.ino:234-237
WiFi connectingPulsing whitesrc/src.ino:79-87
WiFi connectedFade to greensrc/src.ino:89-95

Timeout handling

// From src/src.ino:222-229
unsigned long timeout = millis();
while (!wifiClient.available()) {
    if (millis() - timeout > 5000) {
        for (int i = 0; i < RGB_LED_NUM; i++) LEDs[i] = CRGB::Yellow;
        FastLED.show();
        return;
    }
}

Button mapping

Buttons are mapped to actions in the following order:
// From src/src.ino:19-20
const int keys[7] = {SHUFFLE, VOLUME_DEC, VOLUME_INC, LOOP,
                     BACK,    PAUSE_PLAY, SKIP};

// From src/src.ino:377-378
void (*funcs[7])() = {
    shuffle, volumeDecrease, volumeIncrease, repeat, back, pausePlay, skip};
GPIOFunctionActionAPI call
4ShuffleToggle shuffle/api/manageState/{password}/shuffle
5Volume -Decrease volume/api/manageState/{password}/vdec
12Volume +Increase volume/api/manageState/{password}/vinc
13LoopToggle repeat/api/manageState/{password}/loop
14BackPrevious track/api/manageState/{password}/back
25Play/PauseToggle playback/api/manageState/{password}/playPause
26SkipNext track/api/manageState/{password}/skip
Buttons are read with INPUT_PULLUP, meaning they read LOW when pressed:
// From src/src.ino:381-387
for (int i = 0; i < 7; i++) {
    keyState[i] = digitalRead(keys[i]);
    if (keyState[i] == LOW && keyPrevState[i] == HIGH) {
        funcs[i]();
    }
    keyPrevState[i] = keyState[i];
}

Implementation notes

Why GET instead of POST?

The ESP32 uses GET requests for simplicity. The WiFiClientSecure library makes GET requests straightforward, and since the password is sent over HTTPS, it’s encrypted in transit.

Why custom JSON parsing?

The ESP32 has limited RAM (320KB). Using a full JSON library like ArduinoJson would consume significant memory. The custom extractValue() function is lightweight and sufficient for the simple JSON structure.

Why Keep-Alive?

TLS handshakes are computationally expensive on the ESP32. By maintaining a persistent connection, the device avoids repeated handshakes, significantly improving response time and reducing power consumption.

Next steps

Server setup

Set up your personal proxy server

Hardware assembly

Assemble the MacroBoard hardware

Build docs developers (and LLMs) love