LED Ticker Build Log · 02

[IoT DIY Part 2] Separating the Microcontroller and API Server: API Key Security, Asynchronous Rotation, and Network Freeze Troubleshooting

[IoT DIY Part 2] Separating the Microcontroller and API Server: API Key Security, Asynchronous Rotation, and Network Freeze Troubleshooting

One common mistake in embedded-device projects is trying to perform every calculation and every external API call directly inside the microcontroller, such as an ESP8266 or ESP32. For a ticker display that connects to cryptocurrency exchanges or market-data APIs, that design can create serious security exposure and system failures.

This article explains why I separated the system into a Python broker server and an ESP display client to overcome the compute and security limits of a single microcontroller. It also documents the asynchronous data-rotation logic used in practice and the network-freeze troubleshooting that appeared after firmware updates.

[Demo video] Asynchronous display rotation test where an ESP ticker renders market data prepared by the Python broker server.

1. Why server-client decoupling is required for security

1) API key leakage risk from firmware dumps

To calculate live returns through exchange APIs such as Upbit or Binance, the system needs an Access Key and a Secret Key. If those keys are hard-coded into ESP8266/ESP32 source code for short-term convenience and then flashed to the device, the risk is not theoretical.

  • Security weakness: Microcontrollers do not provide perfect flash-memory protection. If a third party physically obtains the device, dumps the flash memory, and extracts plaintext strings, the exchange API key can be exposed. That can lead to unauthorized trading or, depending on key permissions, asset loss.
  • Architectural fix: I defined the rule as “authentication and calculation happen on a safer internal server, such as a PC or Raspberry Pi, while the ticker is only a display monitor.” The Python broker server handles API authentication, return calculation against purchase history, and data preparation. The ticker device only asks over the local LAN, through HTTP polling, “what string should I display now?”

2) Full separation of business logic and view

This separation sharply improves maintainability. When an API specification changes or a new stock index needs to be added, the ticker firmware does not need to be recompiled and updated by OTA or USB every time. Only the Python server logic changes, and the display automatically shows the new data on the next polling cycle.

2. Dynamic information rotation and the Skip-on-Empty rule

A dot-matrix ticker with limited screen space, such as an 8x16 or 2x20 layout, needs a sequential rotation system to show time, cryptocurrency prices, returns, exchange rates, weather, and other information.

The key rule that determines the user experience is the Skip-on-Empty algorithm.

Rotation itemData source(Python Server)Skip-on-Empty condition exampleDisplay behavior
1. Real-time clockNTP / local system timeAlways validShown as the default HH:MM screen
2. Crypto / stock pricesExchange REST APIMarket closed for stocks, or delayed API responseSkip immediately without showing an error
3. Personal return rateAccount lookup + calculated resultNo holdings or key-authentication errorSkip silently instead of showing blank text or “0%”
4. Exchange rate and weatherOpenWeather / exchange-rate APINo RSS news during late-night hours, or empty text frameIgnore the empty frame and show the next valid item

Many makers design devices to show Error 404, No Data, or Null when API communication fails. For a desk-side interior device, that lowers the perceived quality. The server should return an empty value or an explicit skip flag such as "skip": true when data is missing or invalid. The display-side finite state machine then detects that flag and smoothly advances to the next valid rotation index.

3. [Practical code block 1] Python API broker server(Flask + ticker integration)

The following Python boilerplate keeps exchange API secret keys safely on the server and returns only cleaned JSON data when the ticker asks for it.

Python API broker server(Flask + ticker integration)
import os
import requests
from flask import Flask, jsonify

app = Flask(__name__)

# [Security rule] Never hard-code API keys. Load them from OS environment variables.
UPBIT_ACCESS_KEY = os.getenv("UPBIT_ACCESS_KEY")
UPBIT_SECRET_KEY = os.getenv("UPBIT_SECRET_KEY")

def get_crypto_ticker(market="KRW-BTC"):
    """
    Calls the exchange API and calculates the current price and daily change rate.
    """
    try:
        url = f"https://api.upbit.com/v1/ticker?markets={market}"
        response = requests.get(url, timeout=3.0)
        response.raise_for_status()
        data = response.json()[0]

        price = f"{data['trade_price']:,} KRW"
        change_rate = round(data['signed_change_rate'] * 100, 2)

        return {
            "valid": True,
            "display_text": f"BTC {price} ({change_rate:+}%)",
            "type": "CRYPTO"
        }
    except Exception as e:
        # On API failure, timeout, or network error, return a skip flag instead of raising.
        print(f"[Error] ticker fetch failed: {str(e)}")
        return {"valid": False, "skip": True}

@app.route('/api/display/current', methods=['GET'])
def get_display_data():
    """
    A single endpoint that the ESP ticker polls periodically.
    The server dynamically decides what should be displayed at the current time.
    """
    # [Example] Fetch ticker data and apply the Skip-on-Empty rule on failure.
    crypto_data = get_crypto_ticker("KRW-BTC")

    if crypto_data.get("valid"):
        return jsonify(crypto_data), 200
    else:
        # Tell the display to move to the next rotation item when this item is invalid.
        return jsonify({"valid": False, "skip": True, "fallback": "CLOCK"}), 200

if __name__ == '__main__':
    # Bind to 0.0.0.0 so ESP boards on the same Wi-Fi LAN can connect.
    app.run(host='0.0.0.0', port=5000, debug=False)

4. [Practical code block 2] ESP8266/ESP32 non-blocking HTTP polling and rotation renderer

When a microcontroller performs HTTP communication, using delay() can stop the display scan and cause blinking or broken text. This C++ boilerplate uses millis()-based non-blocking polling and JSON parsing.

ESP8266/ESP32 non-blocking HTTP polling and rotation renderer
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include <ArduinoJson.h>

const char* ssid = "MY_WIFI_SSID";
const char* password = "MY_WIFI_PASSWORD";
const char* serverUrl = "http://192.168.1.100:5000/api/display/current";

unsigned long lastRequestTime = 0;
const unsigned long POLLING_INTERVAL = 5000; // Refresh display data every 5 seconds
String currentDisplayText = "BOOTING...";

// ==============================================================================
// 1. Non-blocking HTTP polling and Skip-on-Empty handling
// ==============================================================================
void fetchDisplayData() {
    if (WiFi.status() != WL_CONNECTED) return;

    WiFiClient client;
    HTTPClient http;

    // [Troubleshooting key] Limit timeout to 1.5 seconds to prevent Freeze.
    http.setTimeout(1500);

    if (http.begin(client, serverUrl)) {
        int httpCode = http.GET();

        if (httpCode == HTTP_CODE_OK) {
            String payload = http.getString();

            // Use a static JSON document to reduce heap fragmentation.
            StaticJsonDocument<256> doc;
            DeserializationError error = deserializeJson(doc, payload);

            if (!error) {
                bool isValid = doc["valid"] | false;
                bool shouldSkip = doc["skip"] | false;

                // [Skip-on-Empty rule] Ignore rendering when data is missing or skip is true.
                if (!isValid || shouldSkip) {
                    Serial.println("[System] Skip-on-Empty - keep previous screen or switch to clock");
                    // Optionally force the rotation index to the next item.
                } else {
                    currentDisplayText = doc["display_text"].as<String>();
                    Serial.println("[Display update] " + currentDisplayText);
                }
            }
        } else {
            Serial.printf("[HTTP] request failed, code: %d
", httpCode);
        }
        http.end();
    }
}

void setup() {
    Serial.begin(115200);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
}

void loop() {
    unsigned long currentMillis = millis();

    // Run an asynchronous HTTP request every 5 seconds without delay().
    if (currentMillis - lastRequestTime >= POLLING_INTERVAL) {
        lastRequestTime = currentMillis;
        fetchDisplayData();
    }

    // The display rendering loop keeps running at 60+ FPS independently of network I/O.
    // renderLedMatrix(currentDisplayText);
}

5. Field troubleshooting: rotation freeze and Wi-Fi lockup after firmware updates

Even after the rotation rule was carefully designed, the real device hit a serious issue: after a firmware update, the entire screen could freeze during long-running operation. It took several days to isolate the cause. The failure came down to two main issues.

Case 1: Main loop interruption caused by blocking HTTP requests

  • Symptom: When Wi-Fi became temporarily unstable or the router changed channels, the scrolling ticker text stopped in the middle and the device did not respond for 10 to 20 seconds.
  • Cause: A basic HTTP GET request on a microcontroller is synchronous and blocks the main loop until a response arrives. If packets are dropped because the router connection is unstable, the CPU waits for the default TCP timeout, often tens of seconds to a minute. During that time, display scanning and animation stop completely.
  • Fix:
    1. As shown in the C++ code block, I explicitly set http.setTimeout(1500); so the socket is closed if the server does not respond within 1.5 seconds, then control returns to the display-rendering loop.
    2. Before communication, the firmware checks WiFi.status() == WL_CONNECTED. After three consecutive communication failures, it quietly reconnects only the Wi-Fi module through a soft-reset recovery routine.

Case 2: Heap fragmentation from accumulating String objects

  • Symptom: The rotation was smooth for the first 24 hours after boot, but after two or three days the device could suddenly reboot or lock up regardless of whether the firmware had just been updated.
  • Cause: When the firmware parses JSON text from the Python server and repeatedly concatenates Arduino String objects with +, heap fragmentation becomes severe inside the ESP8266, which has only about 40KB of usable heap memory. Once contiguous free memory is exhausted, JSON allocation can trigger an Out of Memory crash.
  • Fix:
    1. Instead of DynamicJsonDocument, which causes dynamic allocation, I used fixed-size StaticJsonDocument<256> so parsing completes safely within a known memory boundary.
    2. I preallocated the global String buffer with .reserve(64) to prevent uncontrolled growth and reduce garbage-collection pressure.

6. Conclusion and scalability: evolving into a 2x20 large dot-matrix display

Because the server handles all authentication and calculation while the display only shows prepared text, the project became surprisingly easy to expand.

When I upgraded the early 8x16 small ticker into a wider 2x20 dot-matrix display, I did not need to rewrite the business logic or the communication protocol. I only changed the display-width parameter on the ESP board, and the price, exchange-rate, and clock data from the Python server began flowing across the larger matrix.

In 2022, when AI coding assistants were not yet common, I had to read API documentation directly and build this server-client split by trial and error. Several years later, that architecture is still the strongest technical backbone that keeps the device running on my desk without a security incident or recurring crash.

Based on this server-client architecture, the next article covers how I solved night-time LED glare using Doppler radar and an FSM state machine: Part 3: multi-sensor fusion for reducing night-time visual fatigue.