ww_dfu: A/B scenarios¶
The four ww_dfu scenarios for a flash that holds two images: the update goes into the inactive slot
while the app keeps running, and an image that doesn't confirm itself is reverted at the next reset. 2C (Swap)
when the bootloader copies the new image into place, 2R (Flip) when the hardware or boot ROM remaps the boot
address. Picking yours says which one fits; each section below is complete on its own.
2C-APP-STEP: Swap Shuttle¶
A/B with a copying bootloader; the running app receives the plan one call per step over a fast link.
Setup. Two slots of the same size; the image always runs from the active one, and the bootloader copies the new image into place at the next reset, page by page through a swap page, so a cut during the copy resumes (embassy-boot on STM32 F1, G0 and other single-bank parts).
| bootloader (no transport) | ACTIVE slot: runs here | DFU slot: the update goes here | swap / state pages |
The app implements UpdateNode, BootControl, Inventory and Stage (Profile::AB(SlotSwitch::Copy)). The
bootloader has no transport.
Choose it when the flash holds two images, the part can't remap banks, and the link answers within milliseconds (USB, Ethernet, a LAN). The device keeps running during the whole download.
Flow.
UpdateNode::node: profile, version, boot state, manifest digest. A known digest means the host knows every chunk and can plan a delta; otherwise it readsInventory::chunks.Stage::limits:max_piece(chunks larger than this come in pieces),in_flight.Stage::begin(Target::Live, header): every check that can be made before writing (format, geometry, version floor, signature, boot state).Stage::erase/copy/writefor every step, into the DFU slot; nothing the app runs from is touched.Stage::finish: the image in the DFU slot is checked against the signed descriptor and markedStaged.BootControl::reboot(BootTarget::App): the bootloader swaps the slots and starts the new image on trial.- The host reconnects; the new image checks what matters to it and calls
BootControl::confirm. Without a confirm, the next reset swaps back (BootState::Reverted).
Failures and power cuts. A lost link or a cut during the download costs only time: the DFU slot is scratch. A cut during the swap is resumed by the bootloader from its swap state. A refused step changes nothing and names why.
Resume. After a lost link, begin with the same header answers resumed: true, resume_from: <next step>. After
a reset during the download the session is gone (it lives in RAM); the host starts over.
What the device checks in this scenario: a COPY source hash before copying, each WRITE's chunk hash once all its
pieces are in, every index against the slot, and the whole image against the descriptor in finish.
Transcript¶
The host plans a delta from version 1 to 2 (four ERASE, three COPY, four WRITE), resends one step as if its reply
was lost, finishes, reboots, and confirms the trial image. Test: swap_shuttle_2c_app_step over simulated USB, 64 B
packets.
── device runs the app
── link up: dfu_app_api (link setup not shown, same on every link)
→ node.node()
5 B 01 40 00 00 02
← { role: App, profile: AB(Copy), product: "sim-dfu", geometry_id: 4242, product_id: 1, seal: Unsigned,
app_version: Some(1), version_floor: 1, manifest_digest: Some(1dd95effd1e00214), boot_state: Confirmed,
last_result: { outcome: Nothing, version: 0 }, offers: { boot_control: true, inventory: true,
stage: true, stage_bulk: true } }
46 B 01 80 00 00 73 69 6d 2d 64 66 …
→ stage.limits()
5 B 02 40 30 00 02
← { max_piece: 64, in_flight: 1, live: true, stored: false, plan_area: 0, writes_in_background: true }
13 B 02 80 40 00 00 00 01 80 00 00 …
→ stage.begin(target: Live, header: [35 B: 57 57 44 50 01 92 …])
42 B 03 40 31 00 00 57 57 44 50 01 …
← Ok({ steps: 11, resumed: false, resume_from: 0 })
13 B 03 80 80 0b 00 00 00 00 00 00 …
→ stage.erase(index: 0, block: 0)
11 B 04 40 32 00 00 00 00 00 00 00 …
← Ok(())
3 B 04 80 80
→ stage.erase(index: 1, block: 1)
11 B 05 40 32 00 01 00 00 00 01 00 …
← Ok(())
3 B 05 80 80
→ stage.erase(index: 2, block: 2)
11 B 06 40 32 00 02 00 00 00 02 00 …
← Ok(())
3 B 06 80 80
→ stage.erase(index: 3, block: 3)
11 B 07 40 32 00 03 00 00 00 03 00 …
← Ok(())
3 B 07 80 80
→ stage.write(index: 4, dst: 0, hash: 2780790d97efcff7, offset: 0, data: [64 B: 46 57 02 00 00 00 …])
89 B 08 40 34 00 04 00 00 00 00 00 …
← Ok(())
3 B 08 80 80
→ stage.copy(index: 5, dst: 1, src: 1, hash: 3ce293643207b87c)
21 B 09 40 33 00 05 00 00 00 01 00 …
← Ok(())
3 B 09 80 80
→ stage.write(index: 6, dst: 2, hash: edcc83d5b3974a90, offset: 0, data: [64 B: 61 70 70 20 76 32 …])
89 B 0a 40 34 00 06 00 00 00 02 00 …
← Ok(())
3 B 0a 80 80
→ stage.copy(index: 7, dst: 3, src: 5, hash: 1ec70fb257d1befc)
21 B 0b 40 33 00 07 00 00 00 03 00 …
← Ok(())
3 B 0b 80 80
→ stage.copy(index: 8, dst: 4, src: 4, hash: a809c61b42942960)
21 B 0c 40 33 00 08 00 00 00 04 00 …
← Ok(())
3 B 0c 80 80
→ stage.write(index: 9, dst: 5, hash: 3bf9a7bd49c0f887, offset: 0, data: [64 B: 6e 65 74 20 76 32 …])
89 B 0d 40 34 00 09 00 00 00 05 00 …
← Ok(())
3 B 0d 80 80
→ stage.write(index: 10, dst: 6, hash: d18f7ce680781257, offset: 0, data: [64 B: 6f 74 61 94 9a 86 …])
89 B 0e 40 34 00 0a 00 00 00 06 00 …
← Ok(())
3 B 0e 80 80
── host resends step 5 as if its reply was lost: skipped
→ stage.copy(index: 5, dst: 1, src: 1, hash: 3ce293643207b87c)
21 B 0f 40 33 00 05 00 00 00 01 00 …
← Ok(())
3 B 0f 80 80
→ stage.finish()
5 B 10 40 36 00 02
← Ok(Staged)
3 B 10 80 82
→ boot.reboot(into: App)
6 B 11 40 13 00 00 02
← Ok(())
3 B 11 80 80
← Disconnect(RequestByUser)
1 B 02
── device resets (into the app)
── bootloader switches to the staged image: swaps the slots' contents page by page
── device runs the app
── host reconnects to the same device
── link up: dfu_app_api (link setup not shown, same on every link)
→ node.node()
5 B 01 40 00 00 02
← { role: App, profile: AB(Copy), product: "sim-dfu", geometry_id: 4242, product_id: 1, seal: Unsigned,
app_version: Some(2), version_floor: 1, manifest_digest: Some(5673030c91c1ef0d), boot_state: Trial,
last_result: { outcome: Nothing, version: 0 }, offers: { boot_control: true, inventory: true,
stage: true, stage_bulk: true } }
46 B 01 80 00 00 73 69 6d 2d 64 66 …
→ boot.confirm()
5 B 02 40 11 00 02
← Ok(())
3 B 02 80 80
→ node.node()
5 B 03 40 00 00 02
← { role: App, profile: AB(Copy), product: "sim-dfu", geometry_id: 4242, product_id: 1, seal: Unsigned,
app_version: Some(2), version_floor: 1, manifest_digest: Some(5673030c91c1ef0d), boot_state: Confirmed,
last_result: { outcome: Confirmed, version: 2 }, offers: { boot_control: true, inventory: true,
stage: true, stage_bulk: true } }
46 B 03 80 00 00 73 69 6d 2d 64 66 …
Wire bytes: 44 USB packets (full speed, 64 B max) (link setup and pings left out)
── device runs the app
→ 6 B 05 01 40 00 00 02
← 48 B 08 2e 01 80 00 00 73 69 6d 2d 64 66 75 92 10 00
00 01 00 00 00 08 01 00 00 00 01 00 00 00 80 1d
d9 5e ff d1 e0 02 14 00 00 00 00 00 00 f0 01 57
→ 6 B 05 02 40 30 00 02
← 15 B 08 0d 02 80 40 00 00 00 01 80 00 00 00 00 80
→ 44 B 08 2a 03 40 31 00 00 57 57 44 50 01 92 10 00 00
02 00 00 00 0b 00 67 01 00 00 86 ba 52 71 00 2c
c3 5d c1 92 3c 26 b1 01 e5 86 3c 02
← 15 B 08 0d 03 80 80 0b 00 00 00 00 00 00 00 00 19
→ 13 B 08 0b 04 40 32 00 00 00 00 00 00 00 02
← 4 B 03 04 80 80
→ 13 B 08 0b 05 40 32 00 01 00 00 00 01 00 02
← 4 B 03 05 80 80
→ 13 B 08 0b 06 40 32 00 02 00 00 00 02 00 02
← 4 B 03 06 80 80
→ 13 B 08 0b 07 40 32 00 03 00 00 00 03 00 02
← 4 B 03 07 80 80
→ 64 B 48 59 08 40 34 00 04 00 00 00 00 00 27 80 79 0d
97 ef cf f7 00 00 00 00 46 57 02 00 00 00 2f 86
5d 24 dc d1 cf 67 47 9e 75 3c f4 e9 e7 7f 5f b6
8d 54 0c 01 ff 97 77 ce a5 6c 24 19 17 af 8f e6
→ 29 B c8 1b bd 84 3c 31 2f c7 a7 fe d5 9c 54 49 47 df
bf 16 ed b4 6c 61 5f f7 d7 2e 00 89 02
← 4 B 03 08 80 80
→ 23 B 08 15 09 40 33 00 05 00 00 00 01 00 01 00 3c e2
93 64 32 07 b8 7c 02
← 4 B 03 09 80 80
→ 64 B 48 59 0a 40 34 00 06 00 00 00 02 00 ed cc 83 d5
b3 97 4a 90 00 00 00 00 61 70 70 20 76 32 11 db
28 16 4c de 9f 28 29 f3 40 2e 64 f6 b7 40 41 0b
58 46 7c 0e cf 58 59 23 70 5e 94 26 e7 70 71 3b
→ 29 B c8 1b 88 76 ac 3e ff 88 89 53 a0 8e c4 56 17 a0
a1 6b b8 a6 dc 6e 2f b8 b9 83 00 89 02
← 4 B 03 0a 80 80
→ 23 B 08 15 0b 40 33 00 07 00 00 00 03 00 05 00 1e c7
0f b2 57 d1 be fc 02
← 4 B 03 0b 80 80
→ 23 B 08 15 0c 40 33 00 08 00 00 00 04 00 04 00 a8 09
c6 1b 42 94 29 60 02
← 4 B 03 0c 80 80
→ 64 B 48 59 0d 40 34 00 09 00 00 00 05 00 3b f9 a7 bd
49 c0 f8 87 00 00 00 00 6e 65 74 20 76 32 87 6d
7c 5d 3c 65 75 24 9f 85 94 75 54 7d 8d 3c b7 9d
ac 8d 6c 95 a5 54 cf b5 c4 a5 84 ad bd 6c e7 cd
→ 29 B c8 1b dc bd 9c c5 d5 84 ff e5 f4 d5 b4 dd ed 9c
17 fd 0c ed cc f5 05 b4 2f 15 00 89 02
← 4 B 03 0d 80 80
→ 64 B 48 59 0e 40 34 00 0a 00 00 00 06 00 d1 8f 7c e6
80 78 12 57 00 00 00 00 6f 74 61 94 9a 86 af 33
c8 a6 ea ac b2 9e c7 4b e0 be 02 c4 ca b6 df 63
f8 d6 1a dc e2 ce f7 7b 10 ee 32 f4 fa e6 0f 93
→ 29 B c8 1b 28 06 4a 0c 12 fe 27 ab 40 1e 62 24 2a 16
3f c3 58 36 7a 3c 42 2e 57 db 00 89 02
← 4 B 03 0e 80 80
── host resends step 5 as if its reply was lost: skipped
→ 23 B 08 15 0f 40 33 00 05 00 00 00 01 00 01 00 3c e2
93 64 32 07 b8 7c 02
← 4 B 03 0f 80 80
→ 6 B 05 10 40 36 00 02
← 4 B 03 10 80 82
→ 7 B 06 11 40 13 00 00 02
← 7 B 03 11 80 80 30 c1 02
── device resets (into the app)
── bootloader switches to the staged image: swaps the slots' contents page by page
── device runs the app
── host reconnects to the same device
→ 6 B 05 01 40 00 00 02
← 48 B 08 2e 01 80 00 00 73 69 6d 2d 64 66 75 92 10 00
00 01 00 00 00 08 02 00 00 00 01 00 00 00 80 56
73 03 0c 91 c1 ef 0d 30 00 00 00 00 00 f0 01 57
→ 6 B 05 02 40 11 00 02
← 4 B 03 02 80 80
→ 6 B 05 03 40 00 00 02
← 48 B 08 2e 03 80 00 00 73 69 6d 2d 64 66 75 92 10 00
00 01 00 00 00 08 02 00 00 00 01 00 00 00 80 56
73 03 0c 91 c1 ef 0d 00 10 02 00 00 00 f0 01 57
2C-APP-BULK: Swap Freight¶
A/B with a copying bootloader; the running app receives the whole plan as one stream over a slow link.
Setup. Two slots of the same size; the image always runs from the active one, and the bootloader copies the new image into place at the next reset, page by page through a swap page, so a cut during the copy resumes (embassy-boot on single-bank parts).
| bootloader (no transport) | ACTIVE slot: runs here | DFU slot: the update goes here | swap / state pages |
The app implements UpdateNode, BootControl, Stage and StageBulk (Inventory too if hosts may not know its
manifest). The bootloader has no transport.
Choose it when the flash holds two images, the part can't remap banks, and a round trip takes long (LoRa, LTE-M, satellite, a store-and-forward gateway): a call per step would cost more than the delta saves.
Flow.
UpdateNode::node(andStage::limitsformax_piece, unless the host knows the device type).StageBulk::begin_bulk(Target::Live, header, total_len, ack_every): the checks ofStage::begin;ack_every: 0asks for one acknowledgement at the end.- The host opens the
plancredit sink and sends the plan after its header asPlanSegment { offset, data }. The device grants credit in bytes as it frees its buffer, so the host never sends more than fits; lost segments are sent again from the device's position (go-back-N). The device applies the steps into the DFU slot as they arrive. - When all bytes are in, the device sends
PlanAck { have: total_len, status: Complete }onplan_ack. Stage::finish: checked against the descriptor,Staged.BootControl::reboot(BootTarget::App): the bootloader swaps the slots and starts the new image on trial; the host reconnects and callsBootControl::confirm.
Failures and power cuts. A lost link or a cut during the download costs only time: the DFU slot is scratch. A
step that fails ends the session with PlanAck { status: Failed(error) }; a segment that doesn't continue what the
device has is answered with BulkStatus::Gap and the position to resend from. A cut during the swap is resumed by
the bootloader.
Resume. begin_bulk with the same header answers resumed: true and resume_from in bytes after the header;
the host sends from there. The plan sink continues its own session too: a host that saved Sink::cursor() opens it
again with plan_resume(cursor), which answers how many segments the device has in it (one unit per segment), so
nothing the device already took is sent again and a device that lost the session (a reset) is reported as
StreamError::SinkSessionLost: the host starts over. See credit sinks. The
transcript shows a host that restarts half way.
What the device checks: each segment's offset against what it has, each step as it completes (COPY source hash,
WRITE hash, index ranges), the image against the descriptor in finish.
Transcript¶
The host sends three segments, restarts (a new connection), resumes at byte 192 and continues the plan sink's
session at segment 3, gets one Complete, finishes, reboots and confirms. Test: swap_freight_2c_app_bulk over UDP on localhost.
── device runs the app
── link up: dfu_app_api (link setup not shown, same on every link)
→ node.node()
5 B 01 40 00 00 02
← { role: App, profile: AB(Copy), product: "sim-dfu", geometry_id: 4242, product_id: 1, seal: Unsigned,
app_version: Some(1), version_floor: 1, manifest_digest: Some(1dd95effd1e00214), boot_state: Confirmed,
last_result: { outcome: Nothing, version: 0 }, offers: { boot_control: true, inventory: true,
stage: true, stage_bulk: true } }
46 B 01 80 00 00 73 69 6d 2d 64 66 …
→ stage.limits()
5 B 02 40 30 00 02
← { max_piece: 64, in_flight: 1, live: true, stored: false, plan_area: 0, writes_in_background: true }
13 B 02 80 40 00 00 00 01 80 00 00 …
→ bulk.begin_bulk(target: Live, header: [35 B: 57 57 44 50 01 92 …], total_len: 359, ack_every: 0)
50 B 03 40 40 00 00 57 57 44 50 01 …
← Ok({ steps: 11, resumed: false, resume_from: 0 })
13 B 03 80 80 0b 00 00 00 00 00 00 …
→ bulk.plan: open its session at 0
8 B 04 40 41 88 01 00 00 02
← bulk.plan: ack: have 0 units, may send 2062 more bytes
8 B 00 83 41 50 00 90 0e 02
← bulk.plan: opened at 0, buffer of 2062 bytes
9 B 04 80 01 00 00 40 90 0e 00
→ bulk.plan ⇐ { offset: 0, data: [64 B: 01 00 00 01 01 00 …] } (sink position 0, 1 unit)
76 B 00 40 41 a0 00 00 00 00 00 01 …
→ bulk.plan ⇐ { offset: 64, data: [64 B: e6 bd 84 3c 31 2f …] } (sink position 1, 1 unit)
76 B 00 40 41 a0 01 40 00 00 00 e6 …
→ bulk.plan ⇐ { offset: 128, data: [64 B: 28 29 f3 40 2e 64 …] } (sink position 2, 1 unit)
76 B 00 40 41 a0 02 80 00 00 00 28 …
→ bulk.plan: query: sent 3 units
7 B 05 40 41 67 00 03 02
← bulk.plan: ack: have 3 units, may send 2062 more bytes
8 B 05 83 41 50 03 90 0e 02
── host restarts; the device keeps its session
→ Disconnect(RequestByUser)
1 B 02
── link up: dfu_app_api (link setup not shown, same on every link)
→ bulk.begin_bulk(target: Live, header: [35 B: 57 57 44 50 01 92 …], total_len: 359, ack_every: 0)
50 B 01 40 40 00 00 57 57 44 50 01 …
← Ok({ steps: 11, resumed: true, resume_from: 192 })
13 B 01 80 80 0b 00 00 00 80 c0 00 …
→ bulk.plan_ack: StreamOpen { resume: None, max_age_us: None, codecs: None, args: tail(0:[]) }
5 B 02 40 42 80 02
← ()
6 B 02 80 00 00 00 00
── the plan sink continues its session where the device is
→ bulk.plan: open its session at 3
8 B 03 40 41 88 01 03 00 02
← bulk.plan: ack: have 3 units, may send 2062 more bytes
8 B 00 83 41 50 03 90 0e 02
← bulk.plan: opened at 3, buffer of 2062 bytes
9 B 03 80 01 03 03 40 90 0e 00
→ bulk.plan ⇐ { offset: 192, data: [64 B: 02 04 00 04 00 a8 …] } (sink position 3, 1 unit)
76 B 00 40 41 a0 03 c0 00 00 00 02 …
→ bulk.plan ⇐ { offset: 256, data: [64 B: e7 cd dc bd 9c c5 …] } (sink position 4, 1 unit)
76 B 00 40 41 a0 04 00 01 00 00 e7 …
→ bulk.plan ⇐ { offset: 320, data: [39 B: d6 1a dc e2 ce f7 …] } (sink position 5, 1 unit)
50 B 00 40 41 a0 05 40 01 00 00 d6 …
← bulk.plan_ack ⇒ { have: 359, status: Complete }
9 B 00 82 42 67 01 00 00 20 02
→ bulk.plan: query: sent 6 units
7 B 04 40 41 67 00 06 02
← bulk.plan: ack: have 6 units, may send 2062 more bytes
8 B 04 83 41 50 06 90 0e 02
→ stage.finish()
5 B 05 40 36 00 02
← Ok(Staged)
3 B 05 80 82
→ boot.reboot(into: App)
6 B 06 40 13 00 00 02
← Ok(())
3 B 06 80 80
← Disconnect(RequestByUser)
1 B 02
── device resets (into the app)
── bootloader switches to the staged image: swaps the slots' contents page by page
── device runs the app
── link up: dfu_app_api (link setup not shown, same on every link)
→ boot.confirm()
5 B 01 40 11 00 02
← Ok(())
3 B 01 80 80
→ node.node()
5 B 02 40 00 00 02
← { role: App, profile: AB(Copy), product: "sim-dfu", geometry_id: 4242, product_id: 1, seal: Unsigned,
app_version: Some(2), version_floor: 1, manifest_digest: Some(5673030c91c1ef0d), boot_state: Confirmed,
last_result: { outcome: Confirmed, version: 2 }, offers: { boot_control: true, inventory: true,
stage: true, stage_bulk: true } }
46 B 02 80 00 00 73 69 6d 2d 64 66 …
Wire bytes: 27 UDP datagrams (link setup and pings left out)
── device runs the app
→ 11 B 03 05 01 40 00 00 02 f1 44 33 b4
← 53 B 03 08 2e 01 80 00 00 73 69 6d 2d 64 66 75 92 10
00 00 01 00 00 00 08 01 00 00 00 01 00 00 00 80
1d d9 5e ff d1 e0 02 14 00 00 00 00 00 00 f0 01
57 e8 8a 34 ed
→ 11 B 04 05 02 40 30 00 02 3f e5 5f 12
← 20 B 04 08 0d 02 80 40 00 00 00 01 80 00 00 00 00 80
06 e4 55 c3
→ 57 B 05 08 32 03 40 40 00 00 57 57 44 50 01 92 10 00
00 02 00 00 00 0b 00 67 01 00 00 86 ba 52 71 00
2c c3 5d c1 92 3c 26 b1 01 e5 86 67 01 00 00 00
00 00 00 3c 02 a9 a3 1f f3
← 20 B 05 08 0d 03 80 80 0b 00 00 00 00 00 00 00 00 19
19 4f f6 d8
→ 15 B 06 08 08 04 40 41 88 01 00 00 02 13 c6 5c f8
← 30 B 06 08 08 00 83 41 50 00 90 0e 02 7c 8b 4a e3 08
09 04 80 01 00 00 40 90 0e 00 21 bb 1a 82
→ 259 B 07 08 4c 00 40 41 a0 00 00 00 00 00 01 00 00 01
01 00 01 02 00 01 03 00 03 00 00 27 80 79 0d 97
ef cf f7 40 00 46 57 02 00 00 00 2f 86 5d 24 dc
d1 cf 67 47 9e 75 3c f4 e9 e7 7f 5f b6 8d 54 0c
01 ff 97 77 ce a5 6c 24 19 17 af 8f 00 89 02 f5
10 45 fa 08 4c 00 40 41 a0 01 40 00 00 00 e6 bd
84 3c 31 2f c7 a7 fe d5 9c 54 49 47 df bf 16 ed
b4 6c 61 5f f7 d7 2e 02 01 00 01 00 3c e2 93 64
32 07 b8 7c 03 02 00 ed cc 83 d5 b3 97 4a 90 40
00 61 70 70 20 76 32 11 db 28 16 4c de 9f 00 89
02 ea 1e 97 12 08 4c 00 40 41 a0 02 80 00 00 00
28 29 f3 40 2e 64 f6 b7 40 41 0b 58 46 7c 0e cf
58 59 23 70 5e 94 26 e7 70 71 3b 88 76 ac 3e ff
88 89 53 a0 8e c4 56 17 a0 a1 6b b8 a6 dc 6e 2f
b8 b9 83 02 03 00 05 00 1e c7 0f b2 57 d1 be fc
00 89 02 ba e0 e7 ee 07 05 40 41 67 00 03 02 19
d9 8c d3
← 15 B 07 08 08 05 83 41 50 03 90 0e 02 36 f5 91 78
── host restarts; the device keeps its session
→ 8 B 08 30 c1 02 40 95 74 fc
→ 57 B 03 08 32 01 40 40 00 00 57 57 44 50 01 92 10 00
00 02 00 00 00 0b 00 67 01 00 00 86 ba 52 71 00
2c c3 5d c1 92 3c 26 b1 01 e5 86 67 01 00 00 00
00 00 00 3c 02 cd 31 ae fd
← 20 B 0b 08 0d 01 80 80 0b 00 00 00 80 c0 00 00 00 19
fa f0 8d 6f
→ 11 B 04 05 02 40 42 80 02 4a c1 a9 7e
← 12 B 0c 06 02 80 00 00 00 00 85 ee 73 ad
── the plan sink continues its session where the device is
→ 15 B 05 08 08 03 40 41 88 01 03 00 02 52 17 3d 69
← 30 B 0d 08 08 00 83 41 50 03 90 0e 02 d6 a3 de d2 08
09 03 80 01 03 03 40 90 0e 00 98 7a dd 56
→ 233 B 06 08 4c 00 40 41 a0 03 c0 00 00 00 02 04 00 04
00 a8 09 c6 1b 42 94 29 60 03 05 00 3b f9 a7 bd
49 c0 f8 87 40 00 6e 65 74 20 76 32 87 6d 7c 5d
3c 65 75 24 9f 85 94 75 54 7d 8d 3c b7 9d ac 8d
6c 95 a5 54 cf b5 c4 a5 84 ad bd 6c 00 89 02 00
a1 d3 84 08 4c 00 40 41 a0 04 00 01 00 00 e7 cd
dc bd 9c c5 d5 84 ff e5 f4 d5 b4 dd ed 9c 17 fd
0c ed cc f5 05 b4 2f 15 03 06 00 d1 8f 7c e6 80
78 12 57 40 00 6f 74 61 94 9a 86 af 33 c8 a6 ea
ac b2 9e c7 4b e0 be 02 c4 ca b6 df 63 f8 00 89
02 3d 3b 20 69 08 32 00 40 41 a0 05 40 01 00 00
d6 1a dc e2 ce f7 7b 10 ee 32 f4 fa e6 0f 93 28
06 4a 0c 12 fe 27 ab 40 1e 62 24 2a 16 3f c3 58
36 7a 3c 42 2e 57 db 7c 02 20 a4 eb ca 07 04 40
41 67 00 06 02 ab 32 f7 1f
← 30 B 0e 08 09 00 82 42 67 01 00 00 20 02 d5 6a a3 45
08 08 04 83 41 50 06 90 0e 02 1f 3b a8 f8
→ 11 B 07 05 05 40 36 00 02 00 5f 1a 95
← 9 B 0f 03 05 80 82 8e 77 39 68
→ 12 B 08 06 06 40 13 00 00 02 ef d3 1f f7
← 16 B 00 03 06 80 80 2a 3f 21 06 30 c1 02 af bd c0 39
── device resets (into the app)
── bootloader switches to the staged image: swaps the slots' contents page by page
── device runs the app
→ 11 B 03 05 01 40 11 00 02 b6 8d d7 a9
← 9 B 03 03 01 80 80 7f 53 ce 44
→ 11 B 04 05 02 40 00 00 02 af 00 34 36
← 53 B 04 08 2e 02 80 00 00 73 69 6d 2d 64 66 75 92 10
00 00 01 00 00 00 08 02 00 00 00 01 00 00 00 80
56 73 03 0c 91 c1 ef 0d 00 10 02 00 00 00 f0 01
57 eb da 23 57
2R-APP-STEP: Flip Shuttle¶
A/B with a hardware remap; the running app receives the plan one call per step over a fast link.
Setup. Two banks or partitions; the hardware or boot ROM maps the chosen one to the address the image runs from, so nothing is copied and the switch is instant (STM32 H7, G4, L4 bank swap; RP2350 partition table with an A/B pair and try-before-you-buy; ESP32 OTA partitions; nRF dual bank).
bank 1: | image A: runs (mapped to the boot address) |
bank 2: | image B: the update goes here | boot ROM / option bytes choose the bank
The app implements UpdateNode, BootControl, Inventory and Stage (Profile::AB(SlotSwitch::Remap)).
Choose it when the part can remap its banks and the link answers within milliseconds. The first boot of the new image takes no longer than any other boot, and so does a revert.
Flow.
UpdateNode::node,Stage::limits.Stage::begin(Target::Live, header).Stage::erase/copy/writefor every step, into the inactive bank.Stage::finish: checked against the descriptor,Staged.BootControl::reboot(BootTarget::App): the bootloader (or boot ROM) maps the other bank and starts it on trial.- The host reconnects,
BootControl::boot_stateisTrial; the image callsBootControl::confirm.
Failures and power cuts. Nothing the app runs from is touched until the switch, and the switch is a single write (an option byte, a partition flag), so a cut or a lost link at any point costs nothing but time. An image that doesn't confirm is mapped out again at the next reset.
Resume. After a lost link, begin with the same header continues at resume_from; after a reset the host
starts over.
What the device checks: COPY source hashes, WRITE chunk hashes, every index against the bank, the image against
the descriptor in finish.
Transcript¶
A plain full image: four ERASE and seven WRITE, no COPY, so the host needs no manifest. Then the reboot, the
trial and the confirm. Test: flip_shuttle_2r_app_step over WebSocket on localhost.
── device runs the app
── link up: dfu_app_api (link setup not shown, same on every link)
→ node.node()
5 B 01 40 00 00 02
← { role: App, profile: AB(Remap), product: "sim-dfu", geometry_id: 4242, product_id: 1, seal: Unsigned,
app_version: Some(1), version_floor: 1, manifest_digest: Some(1dd95effd1e00214), boot_state: Confirmed,
last_result: { outcome: Nothing, version: 0 }, offers: { boot_control: true, inventory: true,
stage: true, stage_bulk: true } }
46 B 01 80 00 10 73 69 6d 2d 64 66 …
→ stage.limits()
5 B 02 40 30 00 02
← { max_piece: 64, in_flight: 1, live: true, stored: false, plan_area: 0, writes_in_background: true }
13 B 02 80 40 00 00 00 01 80 00 00 …
── host sends a plain full image: ERASE and WRITE only, no manifest needed
→ stage.begin(target: Live, header: [35 B: 57 57 44 50 01 92 …])
42 B 03 40 31 00 00 57 57 44 50 01 …
← Ok({ steps: 11, resumed: false, resume_from: 0 })
13 B 03 80 80 0b 00 00 00 00 00 00 …
→ stage.erase(index: 0, block: 0)
11 B 04 40 32 00 00 00 00 00 00 00 …
← Ok(())
3 B 04 80 80
→ stage.erase(index: 1, block: 1)
11 B 05 40 32 00 01 00 00 00 01 00 …
← Ok(())
3 B 05 80 80
→ stage.erase(index: 2, block: 2)
11 B 06 40 32 00 02 00 00 00 02 00 …
← Ok(())
3 B 06 80 80
→ stage.erase(index: 3, block: 3)
11 B 07 40 32 00 03 00 00 00 03 00 …
← Ok(())
3 B 07 80 80
→ stage.write(index: 4, dst: 0, hash: 2780790d97efcff7, offset: 0, data: [64 B: 46 57 02 00 00 00 …])
89 B 08 40 34 00 04 00 00 00 00 00 …
← Ok(())
3 B 08 80 80
→ stage.write(index: 5, dst: 1, hash: 3ce293643207b87c, offset: 0, data: [64 B: 68 61 6c d3 da 10 …])
89 B 09 40 34 00 05 00 00 00 01 00 …
← Ok(())
3 B 09 80 80
→ stage.write(index: 6, dst: 2, hash: edcc83d5b3974a90, offset: 0, data: [64 B: 61 70 70 20 76 32 …])
89 B 0a 40 34 00 06 00 00 00 02 00 …
← Ok(())
3 B 0a 80 80
→ stage.write(index: 7, dst: 3, hash: 1ec70fb257d1befc, offset: 0, data: [64 B: 66 6f 6e 74 b0 4c …])
89 B 0b 40 34 00 07 00 00 00 03 00 …
← Ok(())
3 B 0b 80 80
→ stage.write(index: 8, dst: 4, hash: a809c61b42942960, offset: 0, data: [64 B: 75 69 83 ee 6e 08 …])
89 B 0c 40 34 00 08 00 00 00 04 00 …
← Ok(())
3 B 0c 80 80
→ stage.write(index: 9, dst: 5, hash: 3bf9a7bd49c0f887, offset: 0, data: [64 B: 6e 65 74 20 76 32 …])
89 B 0d 40 34 00 09 00 00 00 05 00 …
← Ok(())
3 B 0d 80 80
→ stage.write(index: 10, dst: 6, hash: d18f7ce680781257, offset: 0, data: [64 B: 6f 74 61 94 9a 86 …])
89 B 0e 40 34 00 0a 00 00 00 06 00 …
← Ok(())
3 B 0e 80 80
→ stage.finish()
5 B 0f 40 36 00 02
← Ok(Staged)
3 B 0f 80 82
→ boot.reboot(into: App)
6 B 10 40 13 00 00 02
← Ok(())
3 B 10 80 80
← Disconnect(RequestByUser)
1 B 02
── device resets (into the app)
── bootloader switches to the staged image: maps bank 1 to the boot address, nothing is copied
── device runs the app
── link up: dfu_app_api (link setup not shown, same on every link)
→ boot.boot_state()
5 B 01 40 10 00 02
← Trial
3 B 01 80 30
→ boot.confirm()
5 B 02 40 11 00 02
← Ok(())
3 B 02 80 80
→ node.node()
5 B 03 40 00 00 02
← { role: App, profile: AB(Remap), product: "sim-dfu", geometry_id: 4242, product_id: 1, seal: Unsigned,
app_version: Some(2), version_floor: 1, manifest_digest: Some(5673030c91c1ef0d), boot_state: Confirmed,
last_result: { outcome: Confirmed, version: 2 }, offers: { boot_control: true, inventory: true,
stage: true, stage_bulk: true } }
46 B 03 80 00 10 73 69 6d 2d 64 66 …
Wire bytes: 39 WebSocket frames (host frames unmasked, mask key zeroed) (link setup and pings left out)
── device runs the app
→ 12 B 82 86 00 00 00 00 00 01 40 00 00 02
← 49 B 82 2f 00 01 80 00 10 73 69 6d 2d 64 66 75 92 10
00 00 01 00 00 00 08 01 00 00 00 01 00 00 00 80
1d d9 5e ff d1 e0 02 14 00 00 00 00 00 00 f0 01
57
→ 12 B 82 86 00 00 00 00 00 02 40 30 00 02
← 16 B 82 0e 00 02 80 40 00 00 00 01 80 00 00 00 00 80
── host sends a plain full image: ERASE and WRITE only, no manifest needed
→ 49 B 82 ab 00 00 00 00 00 03 40 31 00 00 57 57 44 50
01 92 10 00 00 02 00 00 00 0b 00 27 02 00 00 86
ba 52 71 00 2c c3 5d 65 c6 3a 85 91 25 c9 ad 3c
02
← 16 B 82 0e 00 03 80 80 0b 00 00 00 00 00 00 00 00 19
→ 18 B 82 8c 00 00 00 00 00 04 40 32 00 00 00 00 00 00
00 02
← 6 B 82 04 00 04 80 80
→ 18 B 82 8c 00 00 00 00 00 05 40 32 00 01 00 00 00 01
00 02
← 6 B 82 04 00 05 80 80
→ 18 B 82 8c 00 00 00 00 00 06 40 32 00 02 00 00 00 02
00 02
← 6 B 82 04 00 06 80 80
→ 18 B 82 8c 00 00 00 00 00 07 40 32 00 03 00 00 00 03
00 02
← 6 B 82 04 00 07 80 80
→ 96 B 82 da 00 00 00 00 00 08 40 34 00 04 00 00 00 00
00 27 80 79 0d 97 ef cf f7 00 00 00 00 46 57 02
00 00 00 2f 86 5d 24 dc d1 cf 67 47 9e 75 3c f4
e9 e7 7f 5f b6 8d 54 0c 01 ff 97 77 ce a5 6c 24
19 17 af 8f e6 bd 84 3c 31 2f c7 a7 fe d5 9c 54
49 47 df bf 16 ed b4 6c 61 5f f7 d7 2e 00 89 02
← 6 B 82 04 00 08 80 80
→ 96 B 82 da 00 00 00 00 00 09 40 34 00 05 00 00 00 01
00 3c e2 93 64 32 07 b8 7c 00 00 00 00 68 61 6c
d3 da 10 e3 4c 3b 40 72 eb f2 28 fb 64 53 58 8a
03 0a 40 13 7c 6b 70 a2 1b 22 58 2b 94 83 88 ba
33 3a 70 43 ac 9b a0 d2 4b 52 88 5b c4 b3 b8 ea
63 6a a0 73 dc cb d0 02 7b 82 b8 8b f4 00 89 02
← 6 B 82 04 00 09 80 80
→ 96 B 82 da 00 00 00 00 00 0a 40 34 00 06 00 00 00 02
00 ed cc 83 d5 b3 97 4a 90 00 00 00 00 61 70 70
20 76 32 11 db 28 16 4c de 9f 28 29 f3 40 2e 64
f6 b7 40 41 0b 58 46 7c 0e cf 58 59 23 70 5e 94
26 e7 70 71 3b 88 76 ac 3e ff 88 89 53 a0 8e c4
56 17 a0 a1 6b b8 a6 dc 6e 2f b8 b9 83 00 89 02
← 6 B 82 04 00 0a 80 80
→ 96 B 82 da 00 00 00 00 00 0b 40 34 00 07 00 00 00 03
00 1e c7 0f b2 57 d1 be fc 00 00 00 00 66 6f 6e
74 b0 4c 1a 87 91 79 c1 10 c8 64 32 9f a9 91 d9
28 e0 7c 4a b7 c1 a9 f1 40 f8 94 62 cf d9 c1 09
58 10 ac 7a e7 f1 d9 21 70 28 c4 92 ff 09 f1 39
88 40 dc aa 17 21 09 51 a0 58 f4 c2 2f 00 89 02
← 6 B 82 04 00 0b 80 80
→ 96 B 82 da 00 00 00 00 00 0c 40 34 00 08 00 00 00 04
00 a8 09 c6 1b 42 94 29 60 00 00 00 00 75 69 83
ee 6e 08 b6 b8 eb 0a 9b 06 86 20 ce d0 03 22 b3
1e 9e 38 e6 e8 1b 3a cb 36 b6 50 fe 00 33 52 e3
4e ce 68 16 18 4b 6a fb 66 e6 80 2e 30 63 82 13
7e fe 98 46 48 7b 9a 2b 96 16 b0 5e 60 00 89 02
← 6 B 82 04 00 0c 80 80
→ 96 B 82 da 00 00 00 00 00 0d 40 34 00 09 00 00 00 05
00 3b f9 a7 bd 49 c0 f8 87 00 00 00 00 6e 65 74
20 76 32 87 6d 7c 5d 3c 65 75 24 9f 85 94 75 54
7d 8d 3c b7 9d ac 8d 6c 95 a5 54 cf b5 c4 a5 84
ad bd 6c e7 cd dc bd 9c c5 d5 84 ff e5 f4 d5 b4
dd ed 9c 17 fd 0c ed cc f5 05 b4 2f 15 00 89 02
← 6 B 82 04 00 0d 80 80
→ 96 B 82 da 00 00 00 00 00 0e 40 34 00 0a 00 00 00 06
00 d1 8f 7c e6 80 78 12 57 00 00 00 00 6f 74 61
94 9a 86 af 33 c8 a6 ea ac b2 9e c7 4b e0 be 02
c4 ca b6 df 63 f8 d6 1a dc e2 ce f7 7b 10 ee 32
f4 fa e6 0f 93 28 06 4a 0c 12 fe 27 ab 40 1e 62
24 2a 16 3f c3 58 36 7a 3c 42 2e 57 db 00 89 02
← 6 B 82 04 00 0e 80 80
→ 12 B 82 86 00 00 00 00 00 0f 40 36 00 02
← 6 B 82 04 00 0f 80 82
→ 13 B 82 87 00 00 00 00 00 10 40 13 00 00 02
← 6 B 82 04 00 10 80 80
← 4 B 82 02 0c 02
── device resets (into the app)
── bootloader switches to the staged image: maps bank 1 to the boot address, nothing is copied
── device runs the app
→ 12 B 82 86 00 00 00 00 00 01 40 10 00 02
← 6 B 82 04 00 01 80 30
→ 12 B 82 86 00 00 00 00 00 02 40 11 00 02
← 6 B 82 04 00 02 80 80
→ 12 B 82 86 00 00 00 00 00 03 40 00 00 02
← 49 B 82 2f 00 03 80 00 10 73 69 6d 2d 64 66 75 92 10
00 00 01 00 00 00 08 02 00 00 00 01 00 00 00 80
56 73 03 0c 91 c1 ef 0d 00 10 02 00 00 00 f0 01
57
2R-APP-BULK: Flip Freight¶
A/B with a hardware remap; the running app receives the whole plan as one stream.
Setup. Two banks or partitions; the hardware or boot ROM maps the chosen one to the boot address, nothing is copied, the switch is instant (dual-bank STM32, RP2350 partitions, ESP32 OTA, nRF dual bank).
bank 1: | image A: runs (mapped to the boot address) |
bank 2: | image B: the update goes here | boot ROM / option bytes choose the bank
The app implements UpdateNode, BootControl, Stage and StageBulk (Profile::AB(SlotSwitch::Remap)).
Choose it when the part can remap its banks and round trips are long or metered, or simply when the whole plan should go in one stream (a high-speed link where the host wants to keep the pipe full).
Flow.
UpdateNode::node,Stage::limits.StageBulk::begin_bulk(Target::Live, header, total_len, ack_every: 0).- The plan after its header on the
plancredit sink; the device applies steps into the inactive bank as they complete. PlanAck { status: Complete }onplan_ack, thenStage::finish:Staged.BootControl::reboot(BootTarget::App): the other bank starts on trial.- The new image confirms with
BootControl::confirm, or gives up withBootControl::revert(or never confirms): the next reset maps the previous bank back,BootState::Reverted,LastResult { outcome: Reverted }.
Failures and power cuts. As for any A/B: nothing the app runs from is touched until the switch, a cut costs only
time. A failed step ends the session with PlanAck { status: Failed(error) }.
Resume. begin_bulk with the same header answers the byte to continue from.
What the device checks: segment offsets, each completed step (COPY source hash, WRITE hash, ranges), the image
against the descriptor in finish.
Transcript¶
The delta in one stream, one Complete, reboot into the trial; the new image fails its own checks and calls
revert, the reset maps version 1 back. Test: flip_freight_2r_app_bulk over simulated USB, 512 B packets.
── device runs the app
── link up: dfu_app_api (link setup not shown, same on every link)
→ node.node()
5 B 01 40 00 00 02
← { role: App, profile: AB(Remap), product: "sim-dfu", geometry_id: 4242, product_id: 1, seal: Unsigned,
app_version: Some(1), version_floor: 1, manifest_digest: Some(1dd95effd1e00214), boot_state: Confirmed,
last_result: { outcome: Nothing, version: 0 }, offers: { boot_control: true, inventory: true,
stage: true, stage_bulk: true } }
46 B 01 80 00 10 73 69 6d 2d 64 66 …
→ stage.limits()
5 B 02 40 30 00 02
← { max_piece: 64, in_flight: 1, live: true, stored: false, plan_area: 0, writes_in_background: true }
13 B 02 80 40 00 00 00 01 80 00 00 …
→ bulk.begin_bulk(target: Live, header: [35 B: 57 57 44 50 01 92 …], total_len: 359, ack_every: 0)
50 B 03 40 40 00 00 57 57 44 50 01 …
← Ok({ steps: 11, resumed: false, resume_from: 0 })
13 B 03 80 80 0b 00 00 00 00 00 00 …
→ bulk.plan_ack: StreamOpen { resume: None, max_age_us: None, codecs: None, args: tail(0:[]) }
5 B 04 40 42 80 02
← ()
6 B 04 80 00 00 00 00
→ bulk.plan: open its session at 0
8 B 05 40 41 88 01 00 00 02
← bulk.plan: ack: have 0 units, may send 2062 more bytes
8 B 00 83 41 50 00 90 0e 02
← bulk.plan: opened at 0, buffer of 2062 bytes
9 B 05 80 01 00 00 40 90 0e 00
→ bulk.plan ⇐ { offset: 0, data: [64 B: 01 00 00 01 01 00 …] } (sink position 0, 1 unit)
76 B 00 40 41 a0 00 00 00 00 00 01 …
→ bulk.plan ⇐ { offset: 64, data: [64 B: e6 bd 84 3c 31 2f …] } (sink position 1, 1 unit)
76 B 00 40 41 a0 01 40 00 00 00 e6 …
→ bulk.plan ⇐ { offset: 128, data: [64 B: 28 29 f3 40 2e 64 …] } (sink position 2, 1 unit)
76 B 00 40 41 a0 02 80 00 00 00 28 …
→ bulk.plan ⇐ { offset: 192, data: [64 B: 02 04 00 04 00 a8 …] } (sink position 3, 1 unit)
76 B 00 40 41 a0 03 c0 00 00 00 02 …
→ bulk.plan ⇐ { offset: 256, data: [64 B: e7 cd dc bd 9c c5 …] } (sink position 4, 1 unit)
76 B 00 40 41 a0 04 00 01 00 00 e7 …
→ bulk.plan ⇐ { offset: 320, data: [39 B: d6 1a dc e2 ce f7 …] } (sink position 5, 1 unit)
50 B 00 40 41 a0 05 40 01 00 00 d6 …
← bulk.plan_ack ⇒ { have: 359, status: Complete }
9 B 00 82 42 67 01 00 00 20 02
→ bulk.plan: query: sent 6 units
7 B 06 40 41 67 00 06 02
← bulk.plan: ack: have 6 units, may send 2062 more bytes
8 B 06 83 41 50 06 90 0e 02
→ stage.finish()
5 B 07 40 36 00 02
← Ok(Staged)
3 B 07 80 82
→ boot.reboot(into: App)
6 B 08 40 13 00 00 02
← Ok(())
3 B 08 80 80
← Disconnect(RequestByUser)
1 B 02
── device resets (into the app)
── bootloader switches to the staged image: maps bank 1 to the boot address, nothing is copied
── device runs the app
── link up: dfu_app_api (link setup not shown, same on every link)
→ node.node()
5 B 01 40 00 00 02
← { role: App, profile: AB(Remap), product: "sim-dfu", geometry_id: 4242, product_id: 1, seal: Unsigned,
app_version: Some(2), version_floor: 1, manifest_digest: Some(5673030c91c1ef0d), boot_state: Trial,
last_result: { outcome: Nothing, version: 0 }, offers: { boot_control: true, inventory: true,
stage: true, stage_bulk: true } }
46 B 01 80 00 10 73 69 6d 2d 64 66 …
── the new image fails its own checks: gives up the trial instead of confirming
→ boot.revert()
5 B 02 40 12 00 02
← Ok(())
3 B 02 80 80
→ boot.reboot(into: App)
6 B 03 40 13 00 00 02
← Ok(())
3 B 03 80 80
← Disconnect(RequestByUser)
1 B 02
── device resets (into the app)
── bootloader the trial image did not confirm: switches back: maps bank 0 to the boot address, nothing is copied
── device runs the app
── link up: dfu_app_api (link setup not shown, same on every link)
→ node.node()
5 B 01 40 00 00 02
← { role: App, profile: AB(Remap), product: "sim-dfu", geometry_id: 4242, product_id: 1, seal: Unsigned,
app_version: Some(1), version_floor: 1, manifest_digest: Some(1dd95effd1e00214), boot_state: Reverted,
last_result: { outcome: Reverted, version: 2 }, offers: { boot_control: true, inventory: true,
stage: true, stage_bulk: true } }
46 B 01 80 00 10 73 69 6d 2d 64 66 …
Wire bytes: 24 USB packets (high speed, 512 B max) (link setup and pings left out)
── device runs the app
→ 6 B 05 01 40 00 00 02
← 48 B 08 2e 01 80 00 10 73 69 6d 2d 64 66 75 92 10 00
00 01 00 00 00 08 01 00 00 00 01 00 00 00 80 1d
d9 5e ff d1 e0 02 14 00 00 00 00 00 00 f0 01 57
→ 6 B 05 02 40 30 00 02
← 15 B 08 0d 02 80 40 00 00 00 01 80 00 00 00 00 80
→ 52 B 08 32 03 40 40 00 00 57 57 44 50 01 92 10 00 00
02 00 00 00 0b 00 67 01 00 00 86 ba 52 71 00 2c
c3 5d c1 92 3c 26 b1 01 e5 86 67 01 00 00 00 00
00 00 3c 02
← 15 B 08 0d 03 80 80 0b 00 00 00 00 00 00 00 00 19
→ 6 B 05 04 40 42 80 02
← 7 B 06 04 80 00 00 00 00
→ 10 B 08 08 05 40 41 88 01 00 00 02
← 21 B 08 08 00 83 41 50 00 90 0e 02 08 09 05 80 01 00
00 40 90 0e 00
→ 450 B 08 4c 00 40 41 a0 00 00 00 00 00 01 00 00 01 01
00 01 02 00 01 03 00 03 00 00 27 80 79 0d 97 ef
cf f7 40 00 46 57 02 00 00 00 2f 86 5d 24 dc d1
cf 67 47 9e 75 3c f4 e9 e7 7f 5f b6 8d 54 0c 01
ff 97 77 ce a5 6c 24 19 17 af 8f 00 89 02 08 4c
00 40 41 a0 01 40 00 00 00 e6 bd 84 3c 31 2f c7
a7 fe d5 9c 54 49 47 df bf 16 ed b4 6c 61 5f f7
d7 2e 02 01 00 01 00 3c e2 93 64 32 07 b8 7c 03
02 00 ed cc 83 d5 b3 97 4a 90 40 00 61 70 70 20
76 32 11 db 28 16 4c de 9f 00 89 02 08 4c 00 40
41 a0 02 80 00 00 00 28 29 f3 40 2e 64 f6 b7 40
41 0b 58 46 7c 0e cf 58 59 23 70 5e 94 26 e7 70
71 3b 88 76 ac 3e ff 88 89 53 a0 8e c4 56 17 a0
a1 6b b8 a6 dc 6e 2f b8 b9 83 02 03 00 05 00 1e
c7 0f b2 57 d1 be fc 00 89 02 08 4c 00 40 41 a0
03 c0 00 00 00 02 04 00 04 00 a8 09 c6 1b 42 94
29 60 03 05 00 3b f9 a7 bd 49 c0 f8 87 40 00 6e
65 74 20 76 32 87 6d 7c 5d 3c 65 75 24 9f 85 94
75 54 7d 8d 3c b7 9d ac 8d 6c 95 a5 54 cf b5 c4
a5 84 ad bd 6c 00 89 02 08 4c 00 40 41 a0 04 00
01 00 00 e7 cd dc bd 9c c5 d5 84 ff e5 f4 d5 b4
dd ed 9c 17 fd 0c ed cc f5 05 b4 2f 15 03 06 00
d1 8f 7c e6 80 78 12 57 40 00 6f 74 61 94 9a 86
af 33 c8 a6 ea ac b2 9e c7 4b e0 be 02 c4 ca b6
df 63 f8 00 89 02 08 32 00 40 41 a0 05 40 01 00
00 d6 1a dc e2 ce f7 7b 10 ee 32 f4 fa e6 0f 93
28 06 4a 0c 12 fe 27 ab 40 1e 62 24 2a 16 3f c3
58 36 7a 3c 42 2e 57 db 7c 02 07 06 40 41 67 00
06 02
← 21 B 08 09 00 82 42 67 01 00 00 20 02 08 08 06 83 41
50 06 90 0e 02
→ 6 B 05 07 40 36 00 02
← 4 B 03 07 80 82
→ 7 B 06 08 40 13 00 00 02
← 7 B 03 08 80 80 30 c1 02
── device resets (into the app)
── bootloader switches to the staged image: maps bank 1 to the boot address, nothing is copied
── device runs the app
→ 6 B 05 01 40 00 00 02
← 48 B 08 2e 01 80 00 10 73 69 6d 2d 64 66 75 92 10 00
00 01 00 00 00 08 02 00 00 00 01 00 00 00 80 56
73 03 0c 91 c1 ef 0d 30 00 00 00 00 00 f0 01 57
── the new image fails its own checks: gives up the trial instead of confirming
→ 6 B 05 02 40 12 00 02
← 4 B 03 02 80 80
→ 7 B 06 03 40 13 00 00 02
← 7 B 03 03 80 80 30 c1 02
── device resets (into the app)
── bootloader the trial image did not confirm: switches back: maps bank 0 to the boot address, nothing is copied
── device runs the app
→ 6 B 05 01 40 00 00 02
← 48 B 08 2e 01 80 00 10 73 69 6d 2d 64 66 75 92 10 00
00 01 00 00 00 08 01 00 00 00 01 00 00 00 80 1d
d9 5e ff d1 e0 02 14 10 20 02 00 00 00 f0 01 57