Repository navigation
Description
Consolidated 2026-10-27 against firmware 0.3.0 (firmware-v0.3.0).
Firmware refs are on OpenSkyhawk f9fed409, the 0.3.0 release commit. FP = Firmware/ScratchPad/FirmwarePlan/. The release layout (zip, manifest.json schema 2, path rules, SHA-256) is in #192; this ticket only takes images from its cache.
Goal
Update one board on the bus (target 1–63) from a firmware release, from its row in the Firmware sheet or from its Cockpit panel. Today the release images are the PDUs only: in 0.3.0 the entries with a nodeId are nodes 3 / 4 / 5, plus node 0, which is #187's (.github/workflows/firmware-release-assets.yml:106-115). No release has PanelGroup firmware yet, so today a PanelGroup node updates from a file in #177 (OpenSkyHawk/OpenSkyhawk#380). The flow is driven by the manifest's nodeId: a future release that ships a PanelGroup image for a NODE_ID uses the same flow unchanged.
Design
The Cockpit board of the client design (Firmware sheet, node / PDU sheet → Firmware, the update dialog): https://claude.ai/artifact/R8DXWWC4uyAby54tBQLo8v. Markers show this ticket's parts. The design dictates the UI and its interactivity. This ticket names the design's states, and specifies only what the design can't show: firmware facts, logic and tests.
- Final boards (2026-10-28; line refs are to the artifact's version
1793186986-be4e):- node / PDU sheet › Status › Firmware:
CockpitMap.dc.html:340-345,:403-408,fwDetail:877-907; - Firmware sheet rows:
:514-518; - the dialog's board path:
:551-624,upVals:1018-1220; - fail texts:
:955-1001; - footers:
:1199.
- node / PDU sheet › Status › Firmware:
- The DESIGN-ONLY injections (
CockpitMap.dc.html:1181-1182): Write error · Retries exhausted · Crash loop · No response · Wrong board · Too big · No bootloader · Wrong version · No confirm · Client cut off · Gateway power-cycled.
Scope
-
Confirm dialog: the shared update dialog from feat: update the Gateway from a firmware release #173 PR 2, confirm step as drawn (
upVals, a node's path).- Shown as drawn: title and sub (from → to · release), the
checks, the notes (PDU calibration, panels pause), the mixed-majorwarnswith theirneedAckacknowledgement, and the footer and buttons. Update is disabled until every check passes. - Logic behind the checks:
- DCS relay stopped (offers Stop relay). While flashing, the Gateway drops the client's export bytes without a NACK or a counter (FP03:433-435).
- Gateway connected and able to update boards.
UPL_INFOanswers: a Gateway older than 0.3.0 has no uploader and lets the request time out (FP03:405-407, :441). Its cockpit side isn't behind (feat: update the Gateway's cockpit side from a firmware release #187's check: the node-0 row =bridgeFw), because the relay to the boards is in the cockpit side's firmware. Order: USB side, then cockpit side, then boards (docs: bootloader rollout, first-setup page and video script OpenSkyhawk#380).- As drawn (
CockpitMap.dc.html:1052-1054): no uploader → "The Gateway needs firmware 0.3.0 or newer to update boards — update the Gateway first." with Update the Gateway; cockpit side behind → "Update the Gateway's cockpit side first — it's behind its USB side." with Update cockpit side.
- As drawn (
- Board has a bootloader. Its row reports a version and its
bootloader≠ 00.BOOTLOADER00 also means not reported (NodeStatus.h:95-96): a board that was running before the bridge booted reads 00 until its READY or bootloader announcement arrives, so a row with version000000reads not reported first (fix: firmware version handling — supported major 1, honest "not reported" copy #191). In the design a reported 00 is the row's manual update only chip, and Update isn't offered there (fwInfocan). Boards on 0.2.0 or older have no bootloader and need the release's-full.hexby ST-Link once (Firmware/release/README-node.md:7-9). Flashing them anyway costs 10 s and endsNO_BOOTLOADER(feat(firmware): streaming bootloader transfer and the Gateway uploader (#366) OpenSkyhawk#379). - Image from the cache: feat: firmware release check and local cache #192
getFile({ kind: 'release', version }, { nodeId, format: 'bin' }), SHA-256 checked there. The entry is found bynodeId, whatever the board is. No image for this NODE_ID (today, every PanelGroup node) → not offered: the row's custom firmware chip. - Built for this node ID and kind (the design's "Built for #n · kind — matches this board"). Both come from the image's identity block (read with feat: update the Gateway's cockpit side from a firmware release #187's identity-block parser), since the manifest has no kind. Compare them with the row's NODE_ID and
nodeKind(NodeStatus.h:174-180). The Gateway also refuses a wrong kind or ID before erasing. - Fits. The client can only check the image against
UPL_INFO_ACK.maxImageKB(124,UploadProtocol.h:24). The board's flash size is known only to the Gateway, which reportsTOO_BIG_FOR_BOARDbefore erasing (room = flash − 4 KB; FP07:420). As drawn: "Fits the 124 KB limit; the board checks its own flash before anything is erased" (CockpitMap.dc.html:1059).
- Facts for the notes: the board is out of action for the whole flash, about 3–5 s. That's 3.2 s measured for a 27.7 KB image and 4.4 s with one retry (FP07:457-459). The 33 KB PDU image hasn't been timed.
- A board is about 5 s (measured 3–5 s). The rounding rule is feat: update the Gateway from a firmware release #173's. PDU calibration stays on the board: an update erases only the pages the image needs, and the calibration is in the last 1 KB page (
BootProtocol.h:47,PduConfig.h:117). - As drawn: "Fits the 124 KB limit; the board checks its own flash before anything is erased"; the notes "PDU calibration — stays on the board — a firmware update doesn't touch it." and "Panels pause — the board restarts once — its panels stop for about 5 s."; the foot "33 KB · about 5 s".
- A board is about 5 s (measured 3–5 s). The rounding rule is feat: update the Gateway from a firmware release #173's. PDU calibration stays on the board: an update erases only the pages the image needs, and the calibration is in the last 1 KB page (
- Shown as drawn: title and sub (from → to · release), the
-
Stage and flash through the uploader client feat: update the Gateway's cockpit side from a firmware release #187 lands, which drives the Gateway's uploader (firmware feat(firmware): Gateway uploader — staging and CAN flash OpenSkyhawk#366; FP03 § Firmware upload, FP07 § Uploader). What's board-specific here:
- target 1–63,
newNodeId=UPL_NODE_ID_KEEP; - the image from feat: firmware release check and local cache #192
getFile(above), staged withUPL_BEGIN{imageLength,imageCrcfrom the identity block}; - relay export is paused for the session, and
_NODE_STATUSkeeps flowing (FP07:433-436).
The messages, NACKs,
KEEPALIVE,UPL_ABORTon close (the image stays staged after a result, so Retry re-sendsUPL_FLASHwithout re-uploading), and the calibration / upload exclusion are feat: update the Gateway's cockpit side from a firmware release #187's uploader client. - target 1–63,
-
Live phases: the design's step list for a node (
phasesFor):stage→boot→write(with %) →verify→restart→ver.setidis feat: install a specific firmware version or a file — rollback, node-ID checks, history #177's. TheUplPhase→ step mapping is feat: update the Gateway's cockpit side from a firmware release #187's.block/blockscount the bootloader's 64-byte blocks, not the upload chunks (517 for a 33 KB image). That's the source of Writing · n%.UPL_STATUScomes on each phase change and every 250 ms while writing (Uploader.cpp:183,:479).- Confirming waits for the board's row to show the app running, newer than GO: same version →
OK, other version →VERSION_MISMATCH, 10 s →TIMEOUT(FP07:426). - The result comes from
UPL_RESULT: result, target, version, attempts, seconds (UploadProtocol.h:118-126). The done screen reads " reports ". - The board's tile shows ↻ while it's updated: the row's updating chip (
fwInforun).
-
Failures: the design's states (
tickUpinjections,failUp, the fail step's Close / Install another version… / Retry). The logic behind them:- Retries: the Gateway retries 2 times by itself (
UploadProtocol.h:68). The retry note and its rule are feat: update the Gateway's cockpit side from a firmware release #187's ("Write interrupted — retrying automatically (n of 2)"). - Failed after the erase: the design's
exhaust→ Stuck in its bootloader.WRITE_FAILED/VERIFY_FAILEDafter the retries. Copy (CockpitMap.dc.html:970-973): "writing failed after 2 automatic retries. Its bootloader is intact and waits for an image — nothing is lost. Retry, or install another version." - After restarting:
- The design's
crash→ App unstable. It shows up asTIMEOUT: an app that crashes before it reports healthy endsAPP_UNSTABLEafter 3 resets (BootProtocol.h:183-187). Copy (:992): "the new firmware restarted 3 times without starting up properly, so its bootloader stopped starting it. Install another version — the earlier versions saved on this computer are listed in its Settings tab.", or with no release image "…Install a working build with From file… in its Settings tab." VERSION_MISMATCH→ Wrong version — "it reports 1.0.0, not 1.0.1. Nothing is retried automatically." Foot: "It runs the version it reports." (:993).TIMEOUT→ No confirmation — "It didn't report its new version after the update." Foot: "Check its version on the Firmware sheet." Confirmation is the board's version report after it restarts (user, 2026-10-28). In Update all's queue (feat: Update all — the Gateway first, then the boards you pick #176): "didn't update — check its version".
- The design's
- Stuck / unstable → the dialog offers Close · Install another version… · Retry, or Close · Install from a file… · Retry for a board with no release image (
CockpitMap.dc.html:1203-1205), and the board shows in Attention until it's fixed. A committed image or a power cycle clears the crash counter (FP12:107-113). - Refused, board untouched:
NO_RESPONSE,NO_BOOTLOADER,ID_MISMATCH,KIND_MISMATCH,TOO_BIG_FOR_BOARD, all decided before the erase (Uploader.cpp:233-243). The board waits in its bootloader (rowbootState01) and restarts its old app by itself within 30 s (FP12:81-83). A node or kind mismatch atENDcomes after the erase (Uploader.cpp:281-282), so trust the row.- As drawn (
CockpitMap.dc.html:987-992), each with the foot "Nothing was written. The board goes back to its old firmware within 30 s.": Refused — wrong board — "the board's bootloader reports a different node ID or kind than the image."; Refused — too big — "the image doesn't fit this board's flash."; No bootloader — "the board didn't enter a bootloader. Flash it once with a programmer; after that it updates from here."
- As drawn (
NO_RESPONSE: an app that ignores the request is caught only by aCONNECTin the 200 ms after its own reset, inside the Gateway's 10 s attempt (BootProtocol.h:189;Uploader.cpp:28-29). So the dialog shows the instruction first, with no clock running, and Retry starts a new, counted 10 s attempt. The instruction's wording is written after the bench check in Done when; until then the design shows a visible placeholder (decision 2026-10-28): No response — "[wording follows the bench test] the board didn't answer the update request." (CockpitMap.dc.html:988).- The client is cut off mid-flash while the Gateway stays powered (the client closed, or the PC asleep while the Gateway keeps power): the design's "Back online — the Gateway carried on by itself while the client was cut off" note.
- The Gateway finishes on its own. It stops sending
UPL_STATUSafter 5 s without a client frame and starts again when the client is back. - On reconnect,
UPL_INFO_ACKstate3 = still flashing, thenlastTarget/lastResult/lastFwin bytes 5–9 give the result (UploadProtocol.h:73-87;Uploader.cpp:26,:173).
- The Gateway finishes on its own. It stops sending
- The Gateway is power-cycled mid-flash: the Gateway unplugged (the USB side runs from USB,
docs/hardware/base-boards.md:51), or the RP2040-Zero's RESET pressed (it reboots the USB side, where the uploader runs). The uploader stops, and its session and result are lost: they're kept in RAM only (Uploader.cpp:39,:57).- On reconnect,
UPL_INFO_ACKreportsstate0 (idle),lastResult0 (NONE),lastTarget0 andlastFw0.0.0. So there's no result for this board (Uploader.cpp:354-369). - The client judges from the board's row. The cockpit side keeps it: it runs from the bus, not from USB.
bootState02APP_INVALID→ stuck in bootloader, Retry. Once BEGIN has erased the app the board stays in its bootloader. There's no idle timeout after a BEGIN, and the state stays 02 even when the cut came afterEND(Firmware/Bootloader/src/main.cpp:502,:599-600; FP12:81-83). The 0x80 bit lapses 3 s after the traffic stops (FP06:57).- Cut before the erase:
bootState01 for up to 30 s, then the app is running at the old version → nothing changed, Retry. Copy (CockpitMap.dc.html:963,:1199): Interrupted — "the Gateway lost power before anything was written." Foot: "Nothing was written. The board goes back to its old firmware within 30 s." - Cut after the erase: Stuck in its bootloader — "the Gateway lost power during the update, so the result was lost. The board waits in its bootloader — Retry finishes it." (
:962). - The board stays
presentthroughout, because its bootloader announces every second (BootProtocol.h:63-64; FP06:40-45).
- On reconnect,
- The Gateway PCB's RESET button (SW1, the STM32 / cockpit side) resets only the cockpit side's STM32, the relay. Its
NRSTgoes to the SWD header and SW1 only, and there's no reset line from the USB side (feat(firmware): Gateway updates its own bridge over USART2 OpenSkyhawk#367;PCB/Base/Gateway_Bridge/Gateway_Bridge.kicad_schSW1 "RESET").- Pressed mid-flash, the uploader keeps its session. It sees the stall and retries like a power cut, while the cockpit side restarts.
- Not benched for a board on the bus: FP07:460 is a cockpit-side reset during the cockpit side's own update, which retried to
OK. - The RP2040-Zero's BOOT button is its BOOTSEL. It's needed only for a USB side that hangs before USB comes up (feat: update the Gateway from a firmware release #173;
Firmware/release/README-gateway.md:31-33). It plays no part here.
- A power cut mid-update can recover by itself if the board is back within about 20 s. The stall is noticed after about 3.5 s, then come two 10 s retries (
Uploader.cpp:32-33,:516-520; worked out from the code). Otherwise the user follows the manual recipe indocs/guides/bring-up.md#recovering-from-a-power-cut-during-an-update.
- Retries: the Gateway retries 2 times by itself (
-
Status chips for the states this adds: the design's
fwInfochips updating (run), stuck in bootloader (stuck), app unstable (unstable) and manual update only (manual). manual update only already shipped in feat: update the Gateway from a firmware release #173 PR 1 (PR feat(cockpit): firmware button, sheet and Overview banner (#173 PR 1) #198,noBootloader()inmain/gateway/firmware.ts), for boards and the Gateway's cockpit side; this ticket adds the other three. The other chips are feat: update the Gateway from a firmware release #173's. Logic, from the row'sbootState, firmware feat(firmware): node and bridge support for CAN updates OpenSkyhawk#365 (NodeStatus.h:62-67; FP04:83-91):- updating: bit 0x80 set.
- stuck in bootloader: 02
APP_INVALIDwithout 0x80. - app unstable: 04
APP_UNSTABLEwithout 0x80. - manual update only:
bootloader00 on a row that reports a version. A row with version000000reads not reported first (fix: firmware version handling — supported major 1, honest "not reported" copy #191), sinceBOOTLOADER00 also means not reported (NodeStatus.h:95-96). The design orders them the same way (CockpitMap.dc.html:830-831). - 01
REQUESTED/ 05CAUGHTare short waits with the app still valid, so they get no chip. - A board in its bootloader stays
presentand never reads as offline (FP06:40-45). - What a stuck or unstable row offers:
- With a release image: Retry on the Firmware sheet row, and Retry the update in the board's Firmware section (
CockpitMap.dc.html:1362,:890). - With no release image (every PanelGroup board): From file… on the sheet row, or Install a build with From file… in its section. It opens the board's Settings tab: its staged file was dropped (
UPL_ABORT) when the dialog closed. - Retry exists only inside the dialog that failed for such a board (decision 2026-10-28;
CockpitMap.dc.html:853-854,:868-875).
- With a release image: Retry on the Firmware sheet row, and Retry the update in the board's Firmware section (
-
Constants (message types, NACK / result codes, CAN-side states) only via
npm run sync. The identity-block layout (FirmwareImage.h) andUploadProtocol.hare synced in feat: update the Gateway's cockpit side from a firmware release #187; this ticket reuses them.- New to the sync here:
BootProtocol.hBootStateandNodeStatus.hNODE_BOOT_STATE_*(for the status chips).
- New to the sync here:
-
Tests against a simulated Gateway on the serial seam:
- success, retry (attempt counter), stuck, unstable;
- link lost mid-flash: port closed, reconnect to
state3, thenlastResult; - Gateway power-cycled mid-flash: reconnect to
state0 /lastResultNONE; the row'sbootState02 → stuck, or the old version → nothing changed; - each pre-erase refusal;
bootloader00 → manual update only, nothing sent;- no-uploader Gateway (
UPL_INFOtimeout → update the Gateway first); NO_RESPONSE→ instruction → Retry with the same staged image;BUSYduring calibration;UPL_ABORTafter the result.
Design (resolved)
Everything the old gaps list named is drawn (Design, and the copy in Scope):
- Pre-erase refusals: No response (visible placeholder until the bench test), Refused — wrong board, Refused — too big, No bootloader, each with the foot "Nothing was written. The board goes back to its old firmware within 30 s." (
CockpitMap.dc.html:987-992,:1199). - After the restart: Wrong version and No confirmation (
:993-994; the No confirmation text is the ticket's, decided 2026-10-28). - Confirm:
- "Firmware 1.0.1 · saved on this computer, checked" (
:1057); - "Fits the 124 KB limit; the board checks its own flash before anything is erased" (
:1059); - the no-uploader and cockpit-side-behind rows (
:1052-1054); - "about 5 s" notes and the foot "33 KB · about 5 s".
- "Firmware 1.0.1 · saved on this computer, checked" (
- Client cut off vs. Gateway power-cycled: two injections (
:1182). Power-cycled after the erase → Stuck in its bootloader (:962); before → Interrupted (:963). Client cut off → Back online. The run footer "Don't unplug the Gateway. Closing the client is fine — the Gateway finishes on its own." stays. - Release images: every PanelGroup board reads custom firmware (
hasImg:798).
Out of scope
- The Gateway's cockpit side (node 0, not responding → Retry the update included): moved to feat: update the Gateway's cockpit side from a firmware release #187 on 2026-10-27. It updates over USART2 (feat(firmware): Gateway updates its own bridge over USART2 OpenSkyhawk#367), not over the bus.
- Forget also clears the board on the Gateway: moved to feat: Forget also clears the board on the Gateway #188 on 2026-10-27. It isn't part of updating firmware.
- Changing a board's NODE_ID (
SETTING_ID), installing from a file, and PanelGroup images: feat: install a specific firmware version or a file — rollback, node-ID checks, history #177.
Blocked by
#192 (the image), #173 PR 2 (the shared update dialog), #187 (the uploader client and the identity-block sync). The firmware has shipped in 0.3.0: OpenSkyHawk/OpenSkyhawk#362–#367 are closed.
Blocks
#177 (the board flow it installs through), #176 (the board step).
Build order (approved 2026-10-27): #192 → #173 PR 1 → #173 PR 2 → #187 → #175 → #177 → #176.
Done when
On the bench, a board on an older release is updated from the client end to end and reports the new version. Each failure path above is reachable and recoverable from the UI.
Bench checks this depends on:
- Catching a hung app through the Gateway is unproven. PR feat(firmware): bootloader self-protection, NODE_ID rule, crash-loop counter, catch at boot (#364) OpenSkyhawk#377 used a CANable and a board reset. A whole-cockpit power cycle also restarts the cockpit side, which is the relay (
docs/architecture/power.md:114). TheNO_RESPONSEinstruction's wording is written after this test. - Recovery from a lost frame or ack (go-back-N) through the Gateway is still open in test(firmware): v1.0 stability gate — multi-node live-DCS soak + API/wire freeze review OpenSkyhawk#290 § Carried bench checks.
Activity
mottihoresh commentedon Oct 27, 2026
Design update (2026-10-26, design artifact v178). Where this ticket's parts sit on the node / PDU sheet has changed:
Markers on the board:
CHANGED · #175 — the update sits in Status, only while one is pending,CHANGED · #175 — no Bootloader row; only a board without one says so,NEW · #175 — opens the Bootloader page of the docs in the browser….feat(releases): firmware release check and local cache (#196)
feat(cockpit): firmware button, sheet and Overview banner (#173 PR 1) (…
feat(update): update the Gateway USB side from a saved release (#173 …
feat(update): uploader client and firmware image identity sync (#187 …