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.

This page provides a comprehensive overview of the Spotify MacroBoard source code, explaining the main components, program flow, and key functions.

Code structure

The Spotify MacroBoard code is organized into several functional sections:
  • Setup and initialization: Hardware configuration and WiFi connection
  • Display management: OLED screen updates and graphics
  • LED control: RGB LED strip animations and color transitions
  • Button handling: Input detection and action triggering
  • API communication: HTTP requests to Spotify control server
  • State management: Tracking playback state and metadata

Libraries and dependencies

The code uses several libraries for hardware control and networking:
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <FastLED.h>
#include <SMBCredentials.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <Wire.h>
LibraryPurpose
Adafruit_GFXGraphics primitives for drawing
Adafruit_SSD1306OLED display driver (128x64, I2C)
FastLEDWS2812B LED strip control
SMBCredentials.hUser credentials and pin definitions
WiFiESP32 WiFi connectivity
WiFiClientSecureHTTPS/TLS client for secure API calls
WireI2C communication protocol

Global objects and variables

Hardware objects

WiFiClientSecure wifiClient;
TwoWire I2C = TwoWire(0);
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &I2C, OLED_RESET);
CRGB LEDs[RGB_LED_NUM];
  • wifiClient: Secure HTTPS client for API communication
  • I2C: I2C bus interface for OLED display
  • display: SSD1306 OLED display object (128x64 pixels)
  • LEDs: Array of RGB LED objects (20 LEDs)

Button state tracking

const int keys[7] = {SHUFFLE, VOLUME_DEC, VOLUME_INC, LOOP,
                     BACK,    PAUSE_PLAY, SKIP};
bool keyPrevState[7] = {HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH};
bool keyState[7] = {HIGH, HIGH, HIGH, HIGH, HIGH, HIGH, HIGH};
Buttons are configured with internal pull-up resistors, so they read HIGH when not pressed and LOW when pressed.

Timing control

const unsigned long RSSIDelay = 10000;    // WiFi signal check: 10 seconds
const unsigned long currentDelay = 5000;   // Track info update: 5 seconds
const unsigned long timeDelay = 1000;      // Progress bar update: 1 second
const unsigned long fadeDelay = 4;         // LED fade step: 4 milliseconds

unsigned long nextRSSICheck, nextCurrentCheck, nextTimeCheck, nextFade;
The code uses non-blocking delays to handle multiple tasks concurrently.

Playback state

String title, artist, album, color, durationRaw, progressRaw, pausedRaw, volumeRaw;
int progress, duration, volume;
bool paused;
int rssi;  // WiFi signal strength

Setup function

The setup() function runs once when the ESP32 boots and initializes all hardware components.
1

Configure GPIO pins

for (int i = 0; i < 7; i++) {
    pinMode(keys[i], INPUT_PULLUP);
}
pinMode(LED, OUTPUT);
pinMode(SCL, INPUT_PULLUP);
pinMode(SDA, INPUT_PULLUP);
Buttons use internal pull-up resistors to eliminate the need for external resistors.
2

Initialize LED strip

FastLED.addLeds<CHIP_SET, LED, COLOR_CODE>(LEDs, RGB_LED_NUM);
FastLED.setBrightness(BRIGHTNESS);
FastLED.setMaxPowerInVoltsAndMilliamps(5, 500);

for (int i = 0; i < RGB_LED_NUM; i++) LEDs[i] = CRGB::White;
FastLED.show();
LEDs start white to indicate power-on. Power is limited to 500mA for safety.
3

Initialize OLED display

I2C.begin(SDA, SCL, 400000);
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    for (int i = 0; i < RGB_LED_NUM; i++) {
        LEDs[i] = CRGB::OrangeRed;
    }
    FastLED.show();
    for (;;) {;}
}
I2C runs at 400kHz for faster display updates. If the display fails to initialize (wrong address, disconnected, etc.), LEDs turn orange-red and the system halts.
4

Connect to WiFi

WiFi.mode(WIFI_STA);
WiFi.begin(SSID, SSID_PASS);

while (WiFi.status() != WL_CONNECTED) {
    for (int times = 0; times < 20; times++) {
        for (int i = 0; i < RGB_LED_NUM; i++) {
            byte brightness = 140 + 110 * sin(millis() / 250.0);
            LEDs[i] = CRGB::White;
            LEDs[i].fadeToBlackBy(255 - brightness);
        }
        delay(25);
        FastLED.show();
    }
}
LEDs pulse white while connecting. The pulsing animation provides visual feedback during connection.
5

Indicate successful connection

for (int step = 0; step < 256; step++) {
    for (int i = 0; i < RGB_LED_NUM; i++) {
        LEDs[i] = blend(CRGB::White, CRGB::Green, step);
    }
    delay(3);
    FastLED.show();
}
LEDs smoothly transition from white to green over 768ms to indicate successful WiFi connection.
6

Configure SSL certificate

wifiClient.setCACert(benzServerCert);
Sets the server certificate for HTTPS verification.

Main loop

The loop() function continuously monitors buttons, updates the display, and synchronizes playback state.
void loop() {
    // 1. Check button states
    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];
    }

    // 2. Update WiFi signal strength (every 10 seconds)
    if (millis() > nextRSSICheck) {
        nextRSSICheck = millis() + RSSIDelay;
        rssi = WiFi.RSSI();
        updateScreen();
    }

    // 3. Fetch current track info (every 5 seconds)
    if (millis() > nextCurrentCheck) {
        nextCurrentCheck = millis() + currentDelay;
        updateCurrent();
    }

    // 4. Update progress bar (every 1 second)
    if (millis() > nextTimeCheck) {
        nextTimeCheck = millis() + timeDelay;
        updateTime(duration, progress);
        if (!paused) {
            progress++;
        }
    }

    // 5. Animate LED color transition (every 4 milliseconds)
    if (shouldFade && millis() > nextFade) {
        nextFade = millis() + fadeDelay;
        shouldFade = fadeLED();
    }
}

Loop execution flow

  1. Button detection: Scans all 7 buttons for state changes (edge detection)
  2. WiFi monitoring: Updates signal strength indicator every 10 seconds
  3. Track sync: Fetches current track metadata every 5 seconds
  4. Progress tracking: Updates progress bar every second (increments locally if playing)
  5. LED animation: Smoothly fades LEDs when track changes (4ms per step)
The loop uses non-blocking delays with millis() timing. This allows multiple tasks to run concurrently without freezing the system.

Key functions

Button handling

Each button triggers a specific function through a function pointer array:
void (*funcs[7])() = {
    shuffle, volumeDecrease, volumeIncrease, repeat, back, pausePlay, skip
};
Button functions call updateState() with an action character:
void skip() {
    updateState('s');
    nextCurrentCheck = millis() + 100;
    updateCurrent();
}
The skip function sends a skip command, then immediately requests updated track info.

API communication

The updateState() function sends control commands to the server:
void updateState(char action, int subAction = 0) {
    String actionString = "";
    if (action == 'p') {
        actionString = "playPause";
    } else if (action == 'b') {
        actionString = "back";
    } else if (action == 's') {
        actionString = "skip";
    } else if (action == 'v') {
        if (subAction == 0) {
            actionString = "vdec";
        } else {
            actionString = "vinc";
        }
    } else if (action == 'l') {
        actionString = "loop";
    } else if (action == 'f') {
        actionString = "shuffle";
    }
    
    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;
        }
    }
    
    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();
}
API endpoint format: GET /api/manageState/{PASSWORD}/{ACTION}Actions: playPause, back, skip, vdec, vinc, loop, shuffle

Fetching current track

The updateCurrent() function retrieves track metadata:
void updateCurrent() {
    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;
        }
    }

    wifiClient.print("GET /api/getCurrent/" + PASSWORD + " HTTP/1.1\r\n" +
                     "Host: benzhou.tech\r\n" +
                     "Connection: Keep-Alive\r\n\r\n");

    // Wait for response (5 second timeout)
    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;
        }
    }

    // Skip HTTP headers
    char endOfHeaders[] = "\r\n\r\n";
    if (!wifiClient.find(endOfHeaders)) {
        for (int i = 0; i < RGB_LED_NUM; i++) LEDs[i] = CRGB::Orange;
        FastLED.show();
        return;
    }
    
    // Read JSON response
    wifiClient.find("{\"ti");
    String response = "{\"ti";
    while (wifiClient.available()) {
        char c = wifiClient.read();
        response += c;
    }
    wifiClient.flush();

    // Parse response
    extractValue("title", response, title);
    extractValue("artist", response, artist);
    extractValue("album", response, album);
    extractValue("duration", response, durationRaw);
    extractValue("progress", response, progressRaw);
    extractValue("paused", response, pausedRaw);
    extractValue("volume", response, volumeRaw);
}
{
  "title": "Song Title",
  "artist": "Artist Name",
  "album": "Album Name",
  "duration": "240",
  "progress": "120",
  "paused": "false",
  "volume": "75",
  "color": [255, 128, 64]
}

JSON parsing

The code uses a simple string-based JSON parser:
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);  // Remove opening quote
        result.remove(result.length() - 1);  // Remove closing quote
    }
}
This lightweight parser extracts values by finding key patterns and extracting substrings, avoiding the overhead of a full JSON library.

LED color transitions

When a new track starts, LEDs smoothly transition to the album’s dominant color:
bool fadeLED() {
    if (step > 255) {
        step = 0;
        return false;  // Fade complete
    }
    
    for (int i = 0; i < RGB_LED_NUM; i++) {
        LEDs[i] = blend(CRGB(prevRed, prevGreen, prevBlue),
                        CRGB(red, green, blue), step);
    }
    FastLED.show();
    step += 1;
    return true;  // Continue fading
}
The fade takes 256 steps × 4ms = 1,024ms (about 1 second) to complete.

Display updates

The updateScreen() function draws the top status bar with WiFi signal and volume:
void updateScreen() {
    display.setCursor(0, 0);
    display.setTextColor(WHITE);
    display.setTextSize(2);

    // Draw WiFi icon
    if (rssi == 0) {
        display.drawBitmap(2, 0, wifi_0, 16, 16, WHITE);
    } else if (rssi < -70) {
        display.drawBitmap(2, 0, wifi_1, 16, 16, WHITE);
    } else {
        display.drawBitmap(2, 0, wifi_2, 16, 16, WHITE);
    }

    // Draw volume icon
    if (volume > 66) {
        display.drawBitmap(108, 0, volume_3, 16, 16, WHITE);
    } else if (volume > 33) {
        display.drawBitmap(108, 0, volume_2, 16, 16, WHITE);
    } else if (volume > 0) {
        display.drawBitmap(108, 0, volume_1, 16, 16, WHITE);
    } else {
        display.drawBitmap(108, 0, volume_0, 16, 16, WHITE);
    }

    display.display();
}
The status bar uses 16×16 pixel bitmap icons for WiFi signal strength and volume level.

Progress bar rendering

The updateTime() function draws the playback progress bar:
void updateTime(int total, int current) {
    display.fillRect(0, 48, 128, 16, BLACK);  // Clear area

    if (current > total) current = total;

    int barWidth = 100;
    int barHeight = 6;
    int barX = 14;
    int barY = 56;

    // Draw progress bar outline
    display.drawRect(barX, barY, barWidth, barHeight, WHITE);
    
    // Calculate and draw progress
    if (total <= 0) total = 1;
    int percent = (100 * current) / total;
    int progressWidth = (barWidth * percent) / 100;
    display.fillRect(barX, barY, progressWidth, barHeight, WHITE);

    // Draw time stamps
    int currentMinutes = current / 60;
    int currentSeconds = current % 60;
    int totalMinutes = total / 60;
    int totalSeconds = total % 60;

    display.setTextSize(1);
    display.setCursor(32, 48);
    display.print(currentMinutes);
    display.print(":");
    if (currentSeconds < 10) display.print("0");
    display.print(currentSeconds);
    display.print("/");
    display.print(totalMinutes);
    display.print(":");
    if (totalSeconds < 10) display.print("0");
    display.println(totalSeconds);

    display.display();
}

Error indication

The code uses LED colors to indicate different error states:
LED ColorMeaningCause
Orange-redDisplay initialization failedOLED not detected at I2C address 0x3C
RedConnection failedCannot connect to server
YellowRequest timeoutServer not responding within 5 seconds
OrangeInvalid responseMalformed HTTP headers
GreenNormal operationSuccessfully connected and syncing

Memory usage

The compiled sketch has the following resource utilization on ESP32:
Program storage: 886,189 bytes (67% of 1,310,720 bytes)
Dynamic memory: 45,808 bytes (13% of 327,680 bytes)
Free memory: 281,872 bytes for local variables
The code leaves 33% of flash storage free, allowing for future feature additions or larger assets.

Performance considerations

Non-blocking architecture

All timing uses millis() instead of delay(), allowing:
  • Responsive button input
  • Smooth LED animations
  • Concurrent display updates
  • Uninterrupted network communication

Update frequencies

TaskFrequencyJustification
Button scanEvery loop (~1000 Hz)Ensures no button presses are missed
Track metadata5 secondsBalances freshness with API load
Progress bar1 secondSufficient for visual feedback
WiFi signal10 secondsRarely changes quickly
LED fade4 millisecondsSmooth visual transition

Connection management

HTTP connections use Keep-Alive to reduce overhead:
if (!wifiClient.connected()) {
    if (!wifiClient.connect("benzhou.tech", 443)) {
        // Handle error
        return;
    }
}
Connections are only established when needed and reused across requests.

Customization opportunities

Adjust update intervals

Modify timing constants to change update frequencies:
const unsigned long RSSIDelay = 10000;    // Change WiFi check interval
const unsigned long currentDelay = 5000;   // Change track sync interval
const unsigned long timeDelay = 1000;      // Change progress update rate
const unsigned long fadeDelay = 4;         // Change LED fade speed

Change LED brightness

Adjust LED brightness in SMBCredentials.h:
#define BRIGHTNESS 230  // 0-255 (230 is ~90%)

Modify button actions

Reorder or replace button functions:
void (*funcs[7])() = {
    shuffle,        // Button on GPIO 4
    volumeDecrease, // Button on GPIO 5
    volumeIncrease, // Button on GPIO 12
    repeat,         // Button on GPIO 13
    back,           // Button on GPIO 14
    pausePlay,      // Button on GPIO 25
    skip            // Button on GPIO 26
};

Next steps

Now that you understand the code structure:

Hardware assembly

Build the physical MacroBoard

Troubleshooting

Resolve common issues

Build docs developers (and LLMs) love