Skip to content

Firmware: wireless upload, several custom patterns per build, and rebuilding only what changed #232

Description

@engmung

Three firmware-side pieces of work, now that building and flashing from the browser works end to end (#230). They are listed together because they touch the same files, but they are independent and can land separately.

1. Wireless upload — the device pulls, nothing pushes

Flashing over USB works and takes about twenty seconds, so this is convenience rather than a blocker. It matters for a Patternflow mounted on a wall, an installation, or anyone with more than one device.

What cannot work, and why — worth writing down, because two of these look plausible until you try them:

  • OSC is not a transport. It carries knob and state messages over UDP. It can trigger an update ("there is one waiting"), but it is not how 1.1 MB travels.
  • ArduinoOTA — what the firmware has today — is LAN push. The uploader has to reach the device on UDP 3232. A browser cannot send UDP at all, and the build server cannot reach a device behind someone's home NAT.
  • Browser straight to the device over the LAN is blocked. An HTTPS page cannot fetch http://192.168.x.x; mixed content stops it, and the community is HTTPS.

What does work: the device fetches over HTTPS. An outbound connection traverses NAT with nothing to configure, and neither the browser nor the server needs to reach the device.

The hardware makes the pairing pleasant — there is a display and four encoders:

  1. Long-press into an update screen. The device shows a short code and starts polling.
  2. On the web, after a build, enter that code.
  3. The server maps the code to the build; the device's next poll returns a URL, downloads, flashes, reboots.

Device-shows / human-types is the right direction: typing six characters on a keyboard beats turning a knob to dial them in.

What it needs

  • HTTPUpdate (or esp_https_ota) in the firmware, plus the update-mode screen and polling loop
  • A code→build endpoint on the server, short-lived and rate-limited
  • TLS done properly. setInsecure() is not acceptable here — a man in the middle could push arbitrary firmware. Pin the root CA and verify the image hash (HTTPUpdate supports an MD5 check).
  • Rollback on a failed update: the partition table already has app0/app1 and otadata, but the app must mark itself valid after a successful boot or a bad image can strand a device that is hard to reach.

Note the chicken-and-egg: this capability has to arrive by USB once. It ships in a stock release, and everyone who flashes that gets wireless updates from then on.

2. More than one custom pattern per build

Worth correcting a common assumption: the firmware already has three custom slots (custom1.hcustom3.h, three entries in customPatterns[]), and the build service's assembler already handles up to three. The reason only one arrives is that the web UI sends one — see #233.

So this is mostly a web change. On this side:

  • Decide whether three is the right number. Nothing structural stops more: the app image is ~1.1 MB in a 3 MB partition, so space is not the constraint. Build time is — every additional pattern lands in the same translation unit (see below).
  • Make sure the on-device pattern list stays sensible with several customs — ordering, and long-press navigation not becoming tedious.

3. Rebuild only the pattern that changed

A warm build is 14–16 s on the Pi, nearly all of it recompiling one translation unit. Arduino concatenates the sketch, and every pattern header is textually included into it, so changing one custom slot recompiles all ~34 presets along with it. Extra cores do not help: it is one file.

That is fine today. It stops being fine as the preset library grows, since build time tracks the total amount of pattern code rather than what changed.

A shape worth considering. Splitting every pattern into its own .cpp is the obvious answer, but it is a large refactor and would break the header-only .h format that community patterns are already written and shared in. A smaller change gets most of the win:

  • Give the custom slots fixed namespaces (Custom1, Custom2, Custom3) and have the assembler rename the submitted pattern into its slot. The displayed name comes from the NAME constant, not the namespace, so nothing changes for the author.
  • With fixed namespaces, pattern_registry.h never changes between builds — today it is regenerated every time, which is itself enough to force a full recompile.
  • Compile each custom slot as its own translation unit (a generated customN.cpp that includes the submitted .h), leaving the presets in the main unit where they belong, since they do not change.

Result: a build recompiles one small file and relinks, instead of recompiling the entire pattern library. Rough guess 15 s → 5 s, most of what remains being the link.

  • Confirm the assumption first by timing a link-only rebuild — if linking is 10 of the 15 seconds, this is not worth doing
  • Keep accepting the existing .h format; community patterns already in circulation must not need editing
  • Sanity check: separate translation units mean no accidental reliance on other patterns' symbols leaking through the shared unit

Trigger for doing it at all: when a warm build passes ~30 s. There is no reason to touch this while it sits at 15.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:firmwareESP32 firmware running on the devicetype:epicA large piece of work, broken into and tracked as sub-issuestype:featureA new capability or addition

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions