Skip to content

fix: classify bare metal from the flash descriptor when no ROM-only alt is listed - #198

Open
boringethan wants to merge 2 commits into
nextfrom
fix/197-baremetal-single-alt
Open

fix: classify bare metal from the flash descriptor when no ROM-only alt is listed#198
boringethan wants to merge 2 commits into
nextfrom
fix/197-baremetal-single-alt

Conversation

@boringethan

Copy link
Copy Markdown
Contributor

Problem

Flashing a console running firmware older than OW_CMD_BOOT_INFO (console FW <= 1.8.0) could fail with:

could not tell whether this device has the bootloader installed; refusing to flash, because the wrong address would brick it

The unit is an ordinary bare-metal device, but it cannot be updated at all. Old firmware has no other way to be classified — the boot-info command is the only non-DFU route, and it does not exist there (0x0B is unassigned in the 1.8.0 command enum, which jumps 0x0A -> 0x0D).

Root cause

parse_boot_mode() recognised bare metal only via the ROM-only alt settings (@Option Bytes, @OTP Memory, @Device Feature). When dfu-util -l reports just the @Internal Flash alt, none of those markers are present and classification falls through to UNKNOWN:

Found DFU: [0483:df11] ... alt=0, name="@Internal Flash /0x08000000/16*128Kg"
-> BootMode.UNKNOWN        (should be BARE_METAL)

Deterministic, not a race. How many alts the ROM loader reports varies — a bench console showed two, not the four the module documented.

Fix

The ROM-only alts are still checked first, but are no longer required. Failing that, the @Internal Flash alt at 0x08000000 is classified from its DfuSe access letters:

Descriptor Before After
any read-only (a) run BOOTLOADER BOOTLOADER (unchanged)
runs present, all writable UNKNOWN BARE_METAL
no parseable runs UNKNOWN UNKNOWN (unchanged)
writable, but not at 0x08000000 UNKNOWN UNKNOWN (unchanged)

Why this is evidence, not a guess

Both bootloaders hard-code their DFU flash descriptor, and both always mark sector 0 (the bootloader itself) and the reserved/config region read-only — that clamp is the security property they exist to enforce:

Build FLASH_DESC_STR
openmotion-bl (sensor) @Internal Flash/0x08000000/01*128Ka,04*128Kg,11*128Ka
open-motion-console-bl @Internal Flash/0x08000000/01*128Ka,08*128Kg,07*128Ka

A descriptor at 0x08000000 with no read-only run therefore cannot be an openmotion bootloader, and the ROM loader is the only other thing it can be. Both signals are positive evidence; neither infers a mode from the absence of the other, so the "never guess an address" posture is preserved.

Behaviour change to review carefully

This intentionally overturns test_single_fully_writable_alt_is_unknown_not_bootloader, which asserted the conservative UNKNOWN. That test predates the verification above — the invariant makes the case decidable, so it now asserts BARE_METAL with the reasoning recorded in the docstring. This is the one judgement call in the PR and the thing most worth a second opinion.

Test coverage

The existing update-flow tests stub detect_boot_mode() outright, so they could never have caught a classification bug. Added coverage drives a real listing through the genuine detect_boot_mode -> parse_boot_mode chain — the path the apps actually take:

  • test_update_flashes_bare_metal_when_only_the_flash_alt_is_listed — the regression
  • test_update_still_routes_a_converted_console_to_the_signed_slot — the other half: relaxing bare-metal detection must not send a bootloader unit a bare-metal image at the base of flash
  • plus unit cases for the console-bl layout, no parseable runs, and a writable descriptor at a different address

Both new tests were confirmed to fail with the fix reverted. Full software suite: 769 passed, 207 deselected.

pytest tests/ -m "not console and not sensor and not destructive and not slow and not fpga and not imu"

Test app

No change needed. openmotion-test-app resolves omotion from disk via PYTHONPATH and delegates all boot-mode policy to the SDK ("All of the policy — which asset suits which boot mode, and what address it goes to — lives in the SDK; this app only supplies buttons"), so its flash thread picks this up through FirmwareUpdater.update(). The second test above covers that path directly.

Hardware verification

Bench console (HWID 23004c00065133333735383300000000) on FW 1.8.0:

  • confirmed bare metal from its DFU alt-setting layout
  • updated to 1.8.1-rc.2 at 0x08000000 with motion-console-fw-baremetal.bin
  • re-queried after reboot: get_boot_mode() returns BARE_METAL from OW_CMD_BOOT_INFO (vtor 0x08000000)

The DFU-derived answer and the firmware's own report agree, which is the cross-check this fix relies on. Note 1.8.1-rc.2 does implement OW_CMD_BOOT_INFO, so that console no longer needs the fallback — but units still on <= 1.8.0 do.

Refs #197

…lt is listed

A unit whose `dfu-util -l` output carried only the `@Internal Flash` alt was
reported as BootMode.UNKNOWN, so FirmwareUpdater.update() refused it with
"could not tell whether this device has the bootloader installed". Consoles
running firmware older than OW_CMD_BOOT_INFO have no other way to be
classified, which left them unflashable.

parse_boot_mode() recognised bare metal only via the ROM-only alt settings
(@option Bytes / @otp Memory / @device Feature). Those are still checked first,
but they are no longer required: failing that, the Internal Flash descriptor at
0x08000000 now decides, from its DfuSe access letters.

That is positive evidence, not a guess. Both bootloaders hard-code a descriptor
that marks sector 0 and the reserved/config region read-only, because that
clamp is the security property they exist to enforce:

  openmotion-bl           01*128Ka,04*128Kg,11*128Ka
  open-motion-console-bl  01*128Ka,08*128Kg,07*128Ka

So a descriptor with no read-only run cannot be a bootloader, and the ROM
loader is the only other thing it can be. A descriptor with no parseable runs,
or one at some other address, still reports UNKNOWN.

This intentionally overturns test_single_fully_writable_alt_is_unknown_not_
bootloader, which asserted the conservative UNKNOWN before the invariant above
was verified against both bootloader sources; it now asserts BARE_METAL with
the reasoning recorded.

The existing update-flow tests stub detect_boot_mode() outright and so could
never have caught this, so the new coverage drives a real listing through the
genuine detect_boot_mode -> parse_boot_mode chain — the path the apps use.

Refs #197

Co-Authored-By: Claude Opus 5 <[email protected]>
@boringethan

Copy link
Copy Markdown
Contributor Author

Hardware verification on a genuine pre-boot-info console — and an important limitation

OW_CMD_BOOT_INFO landed in console FW 1.8.1-rc.1, so the bench console was downgraded to 1.8.1-rc.0 (the last release without it) to test the real case.

Confirmed it was a genuine old unit:

version         : 1.8.1-rc.0
get_boot_mode() : BootMode.UNKNOWN
raw 0x0B reply  : OW_RESP, data_len=0

Then drove the real production pathcheck_latest(include_prerelease=True) -> download_firmware() -> FirmwareUpdater.update(), the same chain the test app's flash thread uses:

latest         : 1.8.1-rc.2 | primary asset: motion-console-fw-baremetal.bin
candidates     : motion-console-fw-baremetal.bin, motion-console-fw-signed.bin
flash success  : True
last_boot_mode : BootMode.BARE_METAL

Console came back on 1.8.1-rc.2, and get_boot_mode() now independently reports BARE_METAL (vtor 0x08000000). So a pre-boot-info console updates cleanly end-to-end, and UNKNOWN over the wire never blocks the flash.

The limitation — this run does NOT exercise the new code path

The listing detection actually classified carried both alts:

alt=1, name="@Option Bytes   /0x5200201C/01*128 e"
alt=0, name="@Internal Flash   /0x08000000/16*128Kg"

So it was classified by the pre-existing ROM-only marker, not the new descriptor fallback. Running both parsers over that captured listing:

Listing old parse_boot_mode new
bench listing (both alts) BARE_METAL BARE_METAL
Internal Flash alt alone UNKNOWN BARE_METAL

This bench console does not reproduce #197. Its ROM loader always reports the Option Bytes alt, so DFU detection worked here before this change and still does.

What that means for review

  • The fix is sound and the single-alt listing is genuinely unclassifiable today — but the single-alt listing itself remains unobserved on this hardware.
  • The original user-reported refusal is therefore not explained by this change. Something else produced that UNKNOWN, and I have not reproduced it.
  • Reviewers should weigh this as a robustness improvement with verified reasoning, not as a confirmed fix for the reported symptom. If the refusal recurs, the verbatim message and the dfu-util -l output at that moment are what would pin it down.

@boringethan

Copy link
Copy Markdown
Contributor Author

Attempted reproduction through the test app — the reported symptom does NOT reproduce

Staged the exact reported scenario end to end:

  • console downgraded to 1.8.1-rc.0 (last release without OW_CMD_BOOT_INFO), confirmed get_boot_mode() -> UNKNOWN and a zero-byte 0x0B reply
  • openmotion-sdk on next @ 263ec90, without this PR's fix
  • openmotion-test-app on next @ 598e940
  • flash driven from the app UI, not a script

App log:

11:24:44  console boot mode: unknown            <- pre-boot-info unit, as intended
11:24:54  beginConsoleFirmwareDownload 1.8.1-rc.2
11:24:59  console CONNECTED -> DISCONNECTING    (entering DFU)
11:25:25  console DISCONNECTED -> CONNECTING
11:25:27  Version: 1.8.1-rc.2                   <- flash succeeded
11:25:27  console boot mode: bare_metal

The update completed normally on the unfixed SDK. No FirmwareUpdateError, no refusal. UNKNOWN over the wire never gated anything, because the flash path does not consult OW_CMD_BOOT_INFO at all — it classifies from the DFU alt-setting layout, and this console's ROM loader reports the @Option Bytes alt, which the existing ROM-only marker already handles.

That matches the parser comparison posted earlier: for this hardware's listing, old and new agree on BARE_METAL.

What this means

  • The premise behind this issue as originally written — that a missing boot-info command blocks flashing on old units — is falsified for this hardware. Pre-boot-info consoles flash fine today.
  • The single-alt listing that this PR fixes is still genuinely unclassifiable, but it remains unobserved on real hardware. It is a latent edge case, not the reported failure.
  • The user-reported refusal is therefore still unexplained. Whatever produced it was not this.

Recommendation

Judge this PR purely as defensive hardening of parse_boot_mode — a listing without a ROM-only alt should not be a dead end — and not as a fix for the reported symptom. The title and body oversell it; happy to retitle to something like "harden boot-mode detection when no ROM-only alt is listed", or to park the PR until a real reproduction turns up.

Reopening the question of what actually caused the original refusal. The things that would pin it: the verbatim error text, the dfu-util -l output at that moment, and which app/SDK build was in use.

FirmwareUpdater.update() raised "could not tell whether this device has the
bootloader installed; refusing to flash, because the wrong address would brick
it" whenever detect_boot_mode() came back UNKNOWN. That is the error reported
from the field, and it strands the device: there is no way to flash it at all,
so the unit is stuck on whatever firmware it already has.

Fall back to bare metal instead. This is safe because the hardware enforces it,
not because we are confident in the guess: both bootloaders clamp their DFU
write window to the application slot and mark sector 0 read-only

  openmotion-bl           01*128Ka,04*128Kg,11*128Ka
  open-motion-console-bl  01*128Ka,08*128Kg,07*128Ka

so a bare-metal write at 0x08000000 against a bootloader unit is rejected by the
bootloader and fails loudly rather than bricking. The opposite default has no
such backstop: a signed image at 0x08020000 writes happily into the middle of a
bare-metal device's flash and leaves it unbootable.

last_boot_mode still reports what was actually detected (UNKNOWN), so neither
callers nor the apps' lock indicator can mistake the fallback for an
observation, and the fallback is logged as a warning.

install_bootloader() is deliberately unchanged and still aborts on UNKNOWN.
Converting a device is irreversible over USB, so an assumption is not
acceptable there.

Refs #197

Co-Authored-By: Claude Opus 5 <[email protected]>
@boringethan

Copy link
Copy Markdown
Contributor Author

The reported error is confirmed, and this PR now actually addresses it

The verbatim message from the failing unit is this SDK's own FirmwareUpdateError:

Console: could not tell whether this device has the bootloader installed, refusing to flash, because the wrong address would brick it

So detect_boot_mode() returned UNKNOWN with the device already in DFU. That rules out the boot-info command (the flash path never calls it) and rules out a missing bundled dfu-util (which fails with a different message).

The stale SDK is not the cause

The failing unit runs test-app 1.4.2-dev.1 (= commit 598e940). Being a -dev.* tag, it bundled openmotion-sdk@next at build time — commit 19e50c7, 32 commits behind current next. But every module in the flash path is byte-identical to next: boot_mode.py, firmware_update.py, DFUProgrammer.py, MotionConsole.py, and the vendored dfu-util binary. Those 32 commits are all db_* / contact-quality / comms work. Updating the SDK would not have fixed this.

New commit: fall back to bare metal instead of refusing

Refusing strands the device — there is no way to flash it at all, so it stays on whatever firmware it has. update() now proceeds as bare metal when the mode is unclassifiable.

This is safe because the hardware enforces it, not because the guess is confident. Both bootloaders clamp the DFU write window to the application slot and mark sector 0 read-only, so a bare-metal write at 0x08000000 against a bootloader unit is rejected by the bootloader and fails loudly. The opposite default has no backstop: a signed image at 0x08020000 writes happily into the middle of a bare-metal device and leaves it unbootable. The asymmetry is what makes bare metal the correct default.

Guard rails kept:

  • last_boot_mode still reports what was detected (UNKNOWN), so neither callers nor the apps' lock indicator can mistake the fallback for an observation
  • the fallback is logged as a warning
  • install_bootloader() is unchanged and still aborts on UNKNOWN — converting a device is irreversible over USB, so an assumption is not acceptable there
  • a test asserts directly that an unclassifiable device is never handed a signed/slot image

Scope of the PR now

Two layers, in order:

  1. classify more (first commit) — a listing with no ROM-only alt is decided by the flash descriptor, so fewer devices land on UNKNOWN at all
  2. fail safe when we still cannot (this commit) — UNKNOWN flashes as bare metal rather than stranding the unit

Full software suite: 771 passed, 207 deselected.

Still open

What made that machine's listing unclassifiable is not yet known — the fallback makes it flashable either way, but the underlying cause is worth finding. dfu-util -l from that PC with the console in DFU would answer it. A leading candidate is a driver issue on that host: wait_for_dfu_device() returns True on a bare 0483:df11 substring match, so output like Cannot open DFU device 0483:df11 or a Found Runtime: line satisfies the wait but yields no parseable alt entries — which would produce exactly this UNKNOWN. If that is confirmed, it deserves its own fix (a clearer error distinguishing "device unreadable" from "device ambiguous").

@boringethan

Copy link
Copy Markdown
Contributor Author

Root cause CONFIRMED: a USB driver problem on the failing host

Confirmed by the reporter. The console was in DFU, but 0483:df11 was not bound to WinUSB/libusbK on that PC, so dfu-util could see the device and not read its descriptors.

UNKNOWN was never a property of the device. It is always either the ST ROM loader or openmotion-bl; UNKNOWN only ever meant we failed to observe which.

The code path that turned an unreadable device into a brick warning

wait_for_dfu_device() gates on a bare substring match:

if self.vidpid and self.vidpid.lower() in out.lower():
    return True

Cannot open DFU device 0483:df11 - skipping contains 0483:df11, so an error message is accepted as proof the device is ready. Control then reaches classification, which legitimately cannot parse any alt entries, and the user is shown a message about bootloaders and bricking — pointing at entirely the wrong subsystem.

Demonstrated against the real functions:

scenario wait_for_dfu_device detect_boot_mode
wrong driver (not WinUSB) True UNKNOWN
permission / in use True UNKNOWN
runtime mode, not DFU True UNKNOWN
genuinely absent False UNKNOWN

Only the last row produces the correct "DFU device did not appear" error. The first three are the trap.

What this means for the two commits here

Neither commit fixes the reported failure, and this should not be merged as though it does:

  • classify from the flash descriptor — still a real latent gap, still unobserved on hardware
  • fall back to bare metal on UNKNOWN — makes an unclassifiable device flashable rather than stranded, but on a driver-broken host the flash still fails, because dfu-util cannot open the device to write it either. It converts a misleading error into an honest one; it is not a cure.

Both remain defensible on their own merits. The actual fix for this class of failure is separate: require a parseable Found DFU: entry before declaring the device ready, and report an unreadable device as a driver problem instead of a boot-mode ambiguity. Raw dfu-util -l output should also be logged, since discarding it is why this took a full diagnostic session to pin down.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant