A live XRP Ledger ticker rendered on a 1983 Apple green-phosphor CRT.
An ESP32 synthesizes composite video straight from its DAC pin — no PC, no video card.
The microcontroller is the video source.
An ESP32-WROOM-32 drives a vintage Apple Monitor II (12" green monochrome CRT, 1983) over a single RCA cable, showing the live XRP/USD price and the XRP Ledger closing in real time.
ESP32 GPIO25 (DAC1) ──yellow──> RCA center pin ─┐
ESP32 GND ───────────black────> RCA shield ─────┴─> Apple Monitor II composite in
That is the entire interface — one signal wire and one ground. The ESP32's DAC generates a
real NTSC composite signal; the monitor just paints it. Wiring detail:
media/wiring.svg · docs/HARDWARE.md.
Live green-phosphor dashboard (price is read from the on-chain XRPL DEX over the same WebSocket as the ledger feed — no API key, no second connection):
┌──────────────────────────────────────────────────────┐
│ [X] XRP / USD XRPL DEX │
│ $2.1416 │
│ ──────────────────────────────────────────────────── │
│ LEDGER INDEX │
│ #105,088,566 │
│ │
│ TXNS 60 TPS 17.1 │
│ CLOSE 3.8s SEEN 1,204 │
│ OPEN FOR 2.3s │
│ ====================| │
│ IN-FLIGHT 247 │
│ #### #### ##/ │
│ ############## │
│ ──────────────────────────────────────────────────── │
│ * LIVE · s1.ripple.com 14:29:57 │
└──────────────────────────────────────────────────────┘
At power-on it plays a one-shot, filmable boot sequence — a CRT warm-up, a stacked
LEDGER PHOSPHOR / by ShaneOnChain splash, a real self-test that ticks each phase to
[OK] as the live connection comes up, a READY hold, and a channel-change wipe into the
dashboard. Full layout and data-source mapping: docs/DESIGN.md.
Left: the first_light composite test pattern. Right: the build on the bench.
The dashboard is the easy part. The interesting problem is making a no-PSRAM microcontroller be a stable composite-video source and a live blockchain client at the same time — on two cores, in ~45 KB of contiguous heap.
- Composite NTSC from a bare DAC pin. GPIO25 swings the 0–1 V luma of a real NTSC signal directly out of the ESP32's DAC (via the legacy I²S→DAC path). No display controller, no scaler.
- A dual-core jitter fix. The composite line-refill ISR installs at the lowest interrupt
level, and any FreeRTOS critical section masks it — so a busy TLS/WebSocket loop on the
same core starved it and scanlines wobbled under load. The fix: pin all networking to
core 0 and leave core 1 for video + render alone, so no network critical section can ever
mask the line ISR. Cross-core state is lock-free (
volatileatomic scalars — a portMUX would re-introduce the very critical sections we moved off-core). - Fitting WSS + TLS + a video framebuffer in a no-PSRAM WROOM-32. A persistent TLS WebSocket, WiFi/lwIP, and a double composite framebuffer leave ~45 KB of contiguous heap. The network-task stack is allocated before TLS so the TLS arena lands around it, not squeezed by it.
- Price from the on-chain DEX over the existing WebSocket. A second HTTPS/TLS context for a price API doesn't fit beside the firehose, so XRP/USD is derived from the XRP/RLUSD DEX order book on the socket that's already open — zero extra TLS, no API key.
- An anti-burn pixel orbiter. To protect the 40-year-old phosphor, the whole UI drifts a few pixels along a slow precessing Lissajous — invisible in normal viewing, held dead-center during the filmed boot intro.
- Self-healing networking. A wedged public-node firehose is cleared by a core-0 WiFi-stack bounce without a reboot or an intro replay; a stale watchdog is the last-resort backstop.
Full design rationale and the data-source mapping live in docs/DESIGN.md.
| Part | Notes |
|---|---|
| Apple Monitor II (A2M2010) | 12" green-phosphor CRT, 1983. Composite in via a single RCA jack. Monitor /// behaves the same. |
| ESP32-WROOM-32 dev board | Must be the original ESP32 — the composite I²S→DAC path runs only on the classic chip, not the S2/S3/C3. |
| Slim RCA pigtail (RCA → bare wires) | The whole connection. Slim barrel seats in the recessed Apple jack. |
Two wires: GPIO25 → RCA center, GND → RCA shield — that's the whole connection. A
direct wire usually syncs fine; if the picture is washed out or unstable, add the small
attenuator shown above. Bill of materials, connector notes, and the signal-level math:
docs/HARDWARE.md.
⚠️ You never need to open the monitor for this project — it's all on the external RCA jack. A CRT stores a dangerous high-voltage charge even unplugged; never go inside one unless you know how to discharge it.
Composite video uses the ESP32's legacy I²S→DAC path, which Espressif removed in Arduino core 3.x — so the esp32 core is pinned to 2.0.14. The sketch also relies on two non-static globals in ESP_8_BIT_composite 1.3.2 to lift the video ISR to LEVEL3. Both are pinned below, and the CI workflow rebuilds both sketches from a clean toolchain on every push.
Prerequisite: arduino-cli (brew install arduino-cli).
# 1. One-time toolchain (pinned). esp32 core 2.0.14 + the three registry libraries.
arduino-cli config set library.enable_unsafe_install true
arduino-cli config add board_manager.additional_urls \
https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
arduino-cli core update-index
arduino-cli core install esp32:esp32@2.0.14
arduino-cli lib install "Adafruit GFX Library@1.12.6" "ArduinoJson@7.4.3" "WebSockets@2.7.2"
# 2. ESP_8_BIT_composite is not in the Library Manager — install from GitHub at the 1.3.2 tag.
arduino-cli lib install --git-url "https://github.com/Roger-random/ESP_8_BIT_composite.git#v1.3.2"PORT=/dev/cu.usbserial-0001
FQBN=esp32:esp32:esp32
# Stage 1 — first light: a test pattern + big text, no networking. Proves GPIO25 → RCA →
# monitor actually paints pixels, and calibrates the overscan safe area.
arduino-cli compile --fqbn $FQBN firmware/01_first_light
arduino-cli upload --fqbn $FQBN -p $PORT firmware/01_first_light
# Stage 2 — the live ticker. Copy the config template and add your 2.4 GHz WiFi first.
cp firmware/phosphor_ticker/config.h.example firmware/phosphor_ticker/config.h
# edit config.h: set WIFI_SSID / WIFI_PASSWORD. config.h is gitignored.
arduino-cli compile --fqbn $FQBN firmware/phosphor_ticker
arduino-cli upload --fqbn $FQBN -p $PORT firmware/phosphor_ticker
arduino-cli monitor -p $PORT -c baudrate=115200Everything tunable lives in config.h (copied from
config.h.example): WiFi, the WebSocket host,
the NTSC/PAL toggle, the overscan margins, TXNS_PER_BLOCK (the block-grid calibration
knob), and the BOOT_* wordmark/credit. BOOT_SHOW_SSID defaults to off so your network
name stays out of any filmed clip.
The default config points at Ripple's free public cluster (s1.ripple.com) and subscribes
to the full transactions_proposed firehose. That's shared community infrastructure — if
you run this continuously or build several units, point WS_HOST at your own rippled/Clio
node (the config supports plain ws:// to a node on your LAN).
The composite + UI engine is data-source-agnostic — the same hardware could just as easily
drive a morning news ticker, a weather board, or a home-automation panel. See Possible
extensions in docs/DESIGN.md.
Phosphor's XRPL streaming + blocks-landing engine grew out of an ESP32-C3 LED-matrix build, which is based on Little Ledger by Andy (Handy4ndy) / Xspence — an educational open-source project for real-time XRP Ledger monitoring on embedded hardware. Thanks to Andy for the groundwork and for sharing it openly.
Built on Roger Cheng's ESP_8_BIT_composite
(composite video + Adafruit GFX on the ESP32 DAC; signal path after Peter Barrett's
ESP_8_BIT), ArduinoJson, and the arduinoWebSockets library. Full attribution and license
details: NOTICE.md.
MIT © ShaneOnChain. Use it, fork it, put a green CRT on your desk.


