Super Earth · Firmware v1.2.0

Helldivers 2Major Order Desk Monitor

An always-on desk display that shows the war as it happens: the current Major Order, its target planet, liberation percentage, time remaining and live player counts — drawn as a Super Earth command terminal on a 4″ 480×320 panel.

Plug it in, join it to WiFi once, and it runs. It updates its own firmware over the air, so USB is only needed for the very first flash.

Device HUD: liberation Device HUD: defence under assault Device HUD: eradication Device HUD: extraction Device HUD: campaign feed Device HUD: defence — standing by Device HUD: no active order Device HUD: offline / stale
Liberation 1 / 8

Drag to page · advances every 7s, like the device

Panel
480×320ST7796S, 4″ landscape
MCU
ESP32D0WD-V3, no PSRAM
Poll
5 minapi.helldivers2.dev
Static RAM
17.3%56,704 bytes
Updates
OTAhourly release check

Event screens

Two things get the whole panel

A new Major Order arriving, and the verdict when one ends. They interrupt the carousel, stay up until they are acknowledged, and are the only screens on the device with no chrome. Each one plays a clip through the board's speaker as it appears.

Full-screen NEW MAJOR ORDER briefing on the device

New Major Order

The order's name and as much of High Command's briefing as fits in four lines. Raised when the assignment id changes — gated so a reboot does not re-announce an order that was already running.

Full-screen ORDER COMPLETE screen with a green band

Order complete

Every task was done at the last observation. The green band is the whole design: from across a room the colour has already said it before any of the words have.

Full-screen ORDER FAILED screen

Order failed

The order ended incomplete, or its deadline passed with tasks outstanding. The objective count is here because “3 of 4 objectives met” is a materially different evening from “0 of 4”.

The API has no outcome field

An assignment that ends simply stops being listed, so the verdict is inferred from the last state the device saw before the order vanished: gone with every task complete is a success, gone otherwise is a failure. Completion is tested before the deadline, so an order finished with an hour to spare is not called a failure the moment its clock runs out — and the check only runs on a poll that actually succeeded, because a dropped connection must never read as a lost war.

Hardware

One board, no soldering

A Hosyond 4″ ESP32-32E display module — the LCDwiki E32R40T. Panel, touch controller, amplifier and MicroSD slot are all already on it. Power is USB-C.

PartDetail
BoardHosyond 4″ ESP32-32E display module (LCDwiki E32R40T)
MCUESP32-D0WD-V3, dual core, no PSRAM, 520 KB SRAM, 4 MB flash
PanelST7796S, 320×480 native, driven at rotation 1 → 480×320 landscape
TouchXPT2046 resistive, on the panel's SPI bus (CS 33) — swipe to page, tap to dismiss
AudioGPIO26 internal DAC → FM8002E amp, GPIO4 enable (active low)
StorageOnboard MicroSD on VSPI (18/19/23/5). Entirely optional.
PowerUSB-C, 5 V. No battery circuitry.

Pin definitions live in platformio.ini under build_flags, not in a TFT_eSPI User_Setup.h — nothing in the library folder needs editing.

Verify the pinout first

Hosyond does not publish its own datasheet, so the mapping is taken from the LCDwiki listing and cross-checked against the ESP32-Cheap-Yellow-Display community pinouts. Blank screen with the backlight on usually means the driver, not the pins; blank with the backlight off means TFT_BL — the single most likely pin to differ. The README walks through all five failure modes.


Under the hood

Decisions worth writing down

Most of what is interesting here is not the feature list — it is what the constraints of a 4 MB part with no PSRAM forced.

The touch panel was never miscalibrated

TFT_eSPI's own touch layer produced a panel where calibration appeared not to hold — taps landed erratically, swipes barely registered. The cause is in validTouch(), which requires two successive samples to agree within 20 ADC counts.

That is a stationarity test, and a moving finger cannot pass it. It is a reasonable filter for buttons and a fatal one for gestures: most of a swipe is rejected outright, and the readings that survive are whichever ones caught the finger momentarily still — which is why the symptom presents as bad calibration rather than as dropped input.

So the firmware keeps TFT_eSPI for bus arbitration and raw conversions, and replaces only the validation layer: a pressure gate first, a median of five instead of a deadband, the burst bracketed by the pressure gate at both ends, and a release that has to stay quiet for 70 ms.

Two app slots, or no OTA

Flash sits at 96.1% — high because the partition table splits 4 MB into two 1.9375 MiB app slots, which is what over-the-air updates need: the running image stays put while an update is written into the other.

Adding the SD/FAT stack pushed the stock min_spiffs.csv to 98.5%, so the project carries its own table that drops a SPIFFS partition it never mounted and gives 64 KB of it to each slot. A unit already in the field needs one USB reflash to actually get that room — an OTA writes the app slot and nothing else.

The previews cannot drift

Every screen on this page was produced by tools/preview.sh, which compiles the firmware's own renderer against a small host shim and rasterises the result to a PNG. Nothing in the shim describes the layout, so there is no second copy to keep in step.

Its predecessor was a hand-maintained Python reimplementation of the same layout. It fell two commits behind and started rendering a HUD the device no longer drew — exactly the thing a preview exists to prevent. Between the renderer harness and check_layout.py, six real layout bugs were caught before any hardware existed.

Direct drawing, not LVGL

A full 480×320 16-bpp framebuffer is 307 KB against 520 KB of SRAM, most already spoken for by the WiFi stack and TLS. The HUD is a fixed, nearly-static arrangement of text and rectangles with no scrolling or animation, so the abstraction buys little.

Instead the renderer redraws only what changed, via a content signature rebuilt each frame; the countdown, WiFi indicator and footer each keep their own. Text boxes are composited in small transient sprites and freed immediately. Result: 48 KB static RAM, leaving roughly 280 KB of heap for TLS and sprites.

Stale data beats no data

The screen is never blanked or replaced by an error message. A failed fetch keeps the last good data up and switches the footer to a dimmed STALE — LAST hh:mm. Repeated failures back off from 15 seconds to a 10-minute ceiling; a dropped WiFi link turns the header indicator red and nudges a reconnect every 15 seconds.

Deploy

USB once, then never again

Everything — platform, board, libraries, display config — is pinned in platformio.ini. The first build downloads the toolchain and takes a few minutes; later builds take seconds.

# requires PlatformIO Core
git clone https://github.com/codydoerfler/hd2-desk-monitor.git
cd hd2-desk-monitor

pio run                 # build
pio run -t upload       # build + flash over USB-C
pio device monitor      # serial log @ 115200 baud
  1. Flash over USB-C

    The only time a cable is required. This also writes the partition table, which an OTA cannot.

  2. Join it to WiFi

    No credentials are compiled in. On first boot the device raises a captive portal — join HD2-Monitor, password helldive, pick your network and set a UTC offset. Stored in NVS, survives power cycles. If nobody configures it within five minutes it reboots and retries the saved network, so an unattended unit recovers on its own after a router outage.

  3. Set a real contact header

    The community API requires every request to identify its caller and returns HTTP 400 without it. HD2_CONTACT_HEADER ships as a placeholder — replace it with your GitHub handle or an email. This is the courtesy that keeps a free community API open.

  4. Leave it alone

    Once an hour the device asks GitHub for the newest release and, if the tag is a higher version than the running image, downloads firmware.bin into the inactive slot and reboots into it. A download that fails halfway leaves the running firmware untouched.

Optional — the SD card

Without a card the device behaves exactly as it always has; the only difference is one [sd] no card line in the serial log. With one, the event screens get their own audio from /audio/mo_new.wav, mo_success.wav and mo_failure.wav. A clean checkout synthesises plain terminal tones, so it produces a working card without shipping game audio nobody can redistribute.