deskradar-matrixportals3

CircuitPython firmware for the Adafruit MatrixPortal S3 — the LED matrix display device.

Hardware

The Adafruit MatrixPortal S3 is an ESP32-S3 based board designed for driving HUB75 RGB LED matrices over WiFi.

SpecValue
MicrocontrollerESP32-S3 (dual-core, 240 MHz)
RAM8 MB
WiFi802.11b/g/n (integrated)
Matrix connector16-pin HUB75 header
Power (matrix)Separate 5V supply required for the LED panel
USBUSB-C (programming, REPL console)
FirmwareCircuitPython

The board connects directly to a standard 64×64 HUB75 LED matrix panel. The matrix requires its own 5V power supply — the MatrixPortal cannot power the panel from USB alone.

How it works

On boot, the device:

  1. Initialises the RGB matrix display (64×64, 5-bit color depth)
  2. Reads WiFi credentials from wifi.txt on the CIRCUITPY filesystem
  3. Connects to WiFi (up to 5 attempts)
  4. Advertises itself via mDNS as deskradar-portal.local
  5. Starts an HTTP server on port 80
  6. Enters the main loop: polls for incoming HTTP requests, manages watchdog timers

Once running, it passively waits for draw commands from the deskradar application on the Pi. A watchdog timer reboots the device if no request arrives within 60 seconds.

WiFi Credentials

Credentials are stored in src/wifi.txt on the CIRCUITPY volume:

deskradar
deskradaradsb

Line 1 is the SSID, line 2 is the password. When the Pi configurator switches WiFi networks, it writes new credentials to this file automatically. The device reads wifi.txt on every boot.

If wifi.txt is missing or malformed, the device reboots. Ensure the file always has exactly two non-empty lines.

HTTP API

All endpoints are on port 80. The device's IP is assigned by DHCP; it is also reachable as deskradar-portal.local on the same network.

GET /ping

Health check. Returns the device's IP address.

GET /ping HTTP/1.1

HTTP/1.1 200 OK
Content-Type: text/plain

10.42.0.51

POST /draw

Clears the display and renders a new frame. Accepts a JSON array of pixel objects.

POST /draw HTTP/1.1
Content-Type: application/json

[
  {"x": 32, "y": 32, "r": 255, "g": 255, "b": 255},
  {"x": 33, "y": 32, "r": 200, "g": 200, "b": 200}
]

Each object requires x, y (0–63), and r, g, b (0–255). Coordinates outside the 64×64 bounds are silently ignored. Colors are converted to RGB565 internally for matrix rendering.

POST /empty

Clears the entire display to black. Used when there are no aircraft in the store.

POST /empty HTTP/1.1

HTTP/1.1 200 OK

POST /no-change

Keepalive. Resets the watchdog timer without modifying the display. Sent when the pixel data is identical to the previous cycle.

POST /no-change HTTP/1.1

HTTP/1.1 200 OK

POST /add-constant

Registers pixels that persist across all subsequent /draw calls. Useful for reference marks or calibration points that should always be visible regardless of what is drawn.

POST /add-constant HTTP/1.1
Content-Type: application/json

[
  {"x": 32, "y": 32, "r": 100, "g": 100, "b": 100}
]

Constant pixels are layered on top of each rendered frame. To clear them, send an empty array.

Watchdog Behavior

The device monitors two conditions and reboots if either triggers:

ConditionAction
No HTTP request received for 60 secondsReboot (re-initialise WiFi and server)
WiFi connection dropsReboot (reconnect and re-advertise mDNS)

The 60-second timeout means the deskradar app must send at least a /no-change request each cycle to keep the device alive. With the default 1-second POLL_CADENCE, this is well within the limit.

Configuration Constants

These are hardcoded in src/code.py and require a firmware update to change:

ConstantDefaultDescription
SERVER_PORT80HTTP server port
REQUEST_BUFFER_SIZE32768HTTP request buffer (32 KB)
DISPLAY_WIDTH64Matrix width in pixels
DISPLAY_HEIGHT64Matrix height in pixels
BIT_DEPTH5Color bit depth (5-bit = 32,768 colors)
WIFI_CONNECT_TIMEOUT_S10Per-attempt WiFi connect timeout (seconds)
MAX_WIFI_CONNECT_ATTEMPTS5Max WiFi connection retries before reboot
REQUEST_TIMEOUT_S60Watchdog: reboot if no request in this window
SCROLL_DELAY_S0.05Status text animation frame delay

Color Format

The device accepts standard 8-bit RGB values (0–255) in the JSON payload. Internally, these are converted to RGB565 (5 bits red, 6 bits green, 5 bits blue) for the matrix framebuffer:

def rgb_to_rgb565(r, g, b):
    return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)

Installation

Requirements

Steps

  1. Connect the MatrixPortal S3 to your Mac via USB-C
  2. Wait for it to appear as CIRCUITPY in Finder (or /Volumes/CIRCUITPY/)
  3. Edit src/wifi.txt with your WiFi credentials if needed
  4. Run the install script:
    cd /path/to/deskradar-matrixportals3
    ./install.sh
  5. The script copies everything in src/ to the device. The device reboots automatically and begins executing code.py.

install.sh is a single command:

cp -r src/* /Volumes/CIRCUITPY/

Verifying the install

Connect to the CircuitPython serial console (e.g. via screen /dev/tty.usbmodem* 115200) to see boot output including the assigned IP address. You can also ping deskradar-portal.local from any device on the same network once it has connected.

Source Structure

PathContents
src/code.pyMain application (476 lines)
src/wifi.txtWiFi SSID and password
src/lib/adafruit_httpserver/HTTP server library (11 compiled modules)
src/lib/adafruit_display_text/Text rendering (5 compiled modules)
src/lib/adafruit_matrixportal/Matrix display abstraction (4 compiled modules)
src/lib/adafruit_bitmap_font/Font support (7 compiled modules)
src/lib/adafruit_ticks.mpyTiming utilities
src/lib/neopixel.mpyNeoPixel LED support

All libraries are pre-compiled (.mpy bytecode) and included in the repository — no additional package management is required.