Skip to content
Advanced

ww_dfu: one-slot scenarios

The four ww_dfu scenarios for a flash that holds only one image: the app is rewritten in place and there is no revert. 1S (Parcel) stores the plan in a plan area while the app runs and lets the bootloader apply it; 1L (Pit stop) reboots into an updater that applies the plan live as it arrives. Picking yours says which one fits; each section below is complete on its own.

1S-APP-STEP: Parcel Shuttle

One app slot and a plan area; the running app stores the plan, step by step, and the bootloader applies it in place after a reset.

Setup. One app slot, updated in place, and a plan area big enough for the largest plan with its WRITE data. The bootloader has no transport; it applies a stored plan offline and keeps an apply journal (a bit per step, cleared without an erase) so a cut while applying resumes.

| bootloader (no transport, applies plans) | APP slot: runs here, rewritten in place | plan area (≥ largest plan) |

The app implements UpdateNode, BootControl, Inventory and Stage (Profile::InPlace, StageLimits { stored: true, plan_area }).

Choose it when the flash can't hold a second image but has room for a plan area, the bootloader can't have a transport, and the downtime may be the apply time only (no revert: the old image is gone once applied).

Flow.

  1. UpdateNode::node. If the host doesn't know the manifest digest (a development build, an image changed by an interrupted update), it reads Inventory::layout and Inventory::chunks and plans against that.
  2. Stage::limits: stored: true and plan_area.
  3. Stage::begin(Target::Stored, header): also checks that the plan fits the plan area (Error::PlanTooBig).
  4. Stage::erase / copy / write: stored in the plan area in the plan's byte form, nothing in the app slot changes.
  5. Stage::finish: the stored plan is checked before anything is touched: complete, header signature, every COPY source still has its hash, every WRITE's data its hash. The state is PlanStored.
  6. BootControl::reboot(BootTarget::App): the bootloader applies the plan in place, checks the image against the descriptor and starts it. The update is confirmed as applied (LastResult { outcome: Confirmed }), there is no trial.

Failures and power cuts. During the download nothing but the plan area changes; the app keeps running. During the apply, the bootloader journals every finished step; after a cut it redoes the step in flight and continues (every step's result is checked by its hash, the WRITE data is still in the plan area). A plan that fails while applying leaves the app slot unbootable; the device needs an updater then (Outcome::Recovery), so keep a recovery path for this case.

Resume. After a lost link or an app reset during the download, begin with the same header continues where the plan area ends (resume_from: the next step). After a cut while applying, the journal resumes; the host sees nothing but a longer reboot.

What the device checks: COPY source hashes and WRITE chunk hashes as the steps arrive and again in finish, the plan area size, the header signature; the applied image against the descriptor.

Transcript

The host doesn't know the manifest and reads it (layout, chunks), stores the in-place delta (ten steps, one COPY from a block not yet erased), reboots; the power fails while the bootloader applies step 5; the journal resumes, and version 2 boots. Test: parcel_shuttle_1s_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: InPlace, 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 } }
        45 B  01 80 01 73 69 6d 2d 64 66 75 …
   ── host doesn't know this manifest digest: reads the manifest
→ inventory.layout(slot: Active)
         6 B  02 40 20 00 00 02
← Ok({ chunk_size: 64, chunk_count: 8, erase_size: 128, erase_count: 4 })
        16 B  02 80 80 40 00 00 00 08 00 80 …
→ inventory.chunks(slot: Active, first: 0, count: 8)
        10 B  03 40 22 00 00 00 00 08 00 02
← Ok([Hash(eb848dd0f44cb2da), Hash(3ce293643207b87c), Hash(f34a24ddef9136f0), Hash(bbff91d8f49d34d7),
      Hash(a809c61b42942960), Hash(1ec70fb257d1befc), Blank, Blank])
        58 B  03 80 80 eb 84 8d d0 f4 4c b2 …
→ stage.limits()
         5 B  04 40 30 00 02
← { max_piece: 64, in_flight: 1, live: false, stored: true, plan_area: 1024, writes_in_background: true }
        13 B  04 80 40 00 00 00 01 40 00 04 …
→ stage.begin(target: Stored, header: [35 B: 57 57 44 50 01 92 …])
        42 B  05 40 31 00 10 57 57 44 50 01 …
← Ok({ steps: 10, resumed: false, resume_from: 0 })
        13 B  05 80 80 0a 00 00 00 00 00 00 …
→ stage.erase(index: 0, block: 0)
        11 B  06 40 32 00 00 00 00 00 00 00 …
← Ok(())
         3 B  06 80 80
→ stage.write(index: 1, dst: 0, hash: 2780790d97efcff7, offset: 0, data: [64 B: 46 57 02 00 00 00 …])
        89 B  07 40 34 00 01 00 00 00 00 00 …
← Ok(())
         3 B  07 80 80
→ stage.write(index: 2, dst: 1, hash: 3ce293643207b87c, offset: 0, data: [64 B: 68 61 6c d3 da 10 …])
        89 B  08 40 34 00 02 00 00 00 01 00 …
← Ok(())
         3 B  08 80 80
→ stage.erase(index: 3, block: 1)
        11 B  09 40 32 00 03 00 00 00 01 00 …
← Ok(())
         3 B  09 80 80
→ stage.write(index: 4, dst: 2, hash: edcc83d5b3974a90, offset: 0, data: [64 B: 61 70 70 20 76 32 …])
        89 B  0a 40 34 00 04 00 00 00 02 00 …
← Ok(())
         3 B  0a 80 80
→ stage.copy(index: 5, dst: 3, src: 5, hash: 1ec70fb257d1befc)
        21 B  0b 40 33 00 05 00 00 00 03 00 …
← Ok(())
         3 B  0b 80 80
→ stage.erase(index: 6, block: 2)
        11 B  0c 40 32 00 06 00 00 00 02 00 …
← Ok(())
         3 B  0c 80 80
→ stage.write(index: 7, dst: 4, hash: a809c61b42942960, offset: 0, data: [64 B: 75 69 83 ee 6e 08 …])
        89 B  0d 40 34 00 07 00 00 00 04 00 …
← Ok(())
         3 B  0d 80 80
→ stage.write(index: 8, dst: 5, hash: 3bf9a7bd49c0f887, offset: 0, data: [64 B: 6e 65 74 20 76 32 …])
        89 B  0e 40 34 00 08 00 00 00 05 00 …
← Ok(())
         3 B  0e 80 80
→ stage.write(index: 9, dst: 6, hash: d18f7ce680781257, offset: 0, data: [64 B: 6f 74 61 94 9a 86 …])
        89 B  0f 40 34 00 09 00 00 00 06 00 …
← Ok(())
         3 B  0f 80 80
→ stage.finish()
         5 B  10 40 36 00 02
← Ok(PlanStored)
         3 B  10 80 84
→ 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 applies the stored plan in place: 10 steps
   ── power cut while the bootloader applies step 5
   ── bootloader finds the apply journal at step 5: redoes step 5, continues
   ── bootloader: image verified against the descriptor, plan area cleared
   ── 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: InPlace, 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 } }
        45 B  01 80 01 73 69 6d 2d 64 66 75 …
Wire bytes: 42 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
←   47 B  08 2d 01 80 01 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
   ── host doesn't know this manifest digest: reads the manifest
→    7 B  06 02 40 20 00 00 02
←   18 B  08 10 02 80 80 40 00 00 00 08 00 80 00 00 00 04
          00 49
→   12 B  08 0a 03 40 22 00 00 00 00 08 00 02
←   60 B  08 3a 03 80 80 eb 84 8d d0 f4 4c b2 da 00 3c e2
          93 64 32 07 b8 7c 00 f3 4a 24 dd ef 91 36 f0 00
          bb ff 91 d8 f4 9d 34 d7 00 a8 09 c6 1b 42 94 29
          60 00 1e c7 0f b2 57 d1 be fc 50 09
→    6 B  05 04 40 30 00 02
←   15 B  08 0d 04 80 40 00 00 00 01 40 00 04 00 00 80
→   44 B  08 2a 05 40 31 00 10 57 57 44 50 01 92 10 00 00
          02 00 00 00 0a 00 e4 01 00 00 86 ba 52 71 00 2c
          c3 5d 10 9b 6b 17 9f ae b3 bd 3c 02
←   15 B  08 0d 05 80 80 0a 00 00 00 00 00 00 00 00 19
→   13 B  08 0b 06 40 32 00 00 00 00 00 00 00 02
←    4 B  03 06 80 80
→   64 B  48 59 07 40 34 00 01 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 07 80 80
→   64 B  48 59 08 40 34 00 02 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
→   29 B  c8 1b 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
←    4 B  03 08 80 80
→   13 B  08 0b 09 40 32 00 03 00 00 00 01 00 02
←    4 B  03 09 80 80
→   64 B  48 59 0a 40 34 00 04 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 05 00 00 00 03 00 05 00 1e c7
          0f b2 57 d1 be fc 02
←    4 B  03 0b 80 80
→   13 B  08 0b 0c 40 32 00 06 00 00 00 02 00 02
←    4 B  03 0c 80 80
→   64 B  48 59 0d 40 34 00 07 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
→   29 B  c8 1b 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
←    4 B  03 0d 80 80
→   64 B  48 59 0e 40 34 00 08 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 0e 80 80
→   64 B  48 59 0f 40 34 00 09 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 0f 80 80
→    6 B  05 10 40 36 00 02
←    4 B  03 10 80 84
→    7 B  06 11 40 13 00 00 02
←    7 B  03 11 80 80 30 c1 02
   ── device resets (into the app)
   ── bootloader applies the stored plan in place: 10 steps
   ── power cut while the bootloader applies step 5
   ── bootloader finds the apply journal at step 5: redoes step 5, continues
   ── bootloader: image verified against the descriptor, plan area cleared
   ── device runs the app
→    6 B  05 01 40 00 00 02
←   47 B  08 2d 01 80 01 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

1S-APP-BULK: Parcel Freight

One app slot and a plan area; the running app receives the whole plan in one stream into the plan area, keeps doing its job, and the bootloader applies the plan after a reset.

Setup. One app slot, updated in place, and a plan area big enough for the largest plan. The bootloader has no transport; it applies a stored plan offline, with an apply journal.

| bootloader (no transport, applies plans) | APP slot: runs here, rewritten in place | plan area (≥ largest plan) |

The app implements UpdateNode, BootControl, Stage and StageBulk (Profile::InPlace, StageLimits { stored: true, plan_area }).

Choose it when the link is slow or metered and the device must keep working while the download takes hours (a sensor on LoRa), the flash can't hold a second image, and the bootloader has no transport. This is the scenario that ww_dfu's bulk path was made for: the bytes after the header are the plan area's content, unchanged.

Flow.

  1. UpdateNode::node (and Stage::limits, unless the host knows the device type).
  2. StageBulk::begin_bulk(Target::Stored, header, total_len, ack_every): checks the header and that the plan fits.
  3. The plan after its header on the plan credit sink, written to the plan area as it arrives. The app answers anything else meanwhile.
  4. PlanAck { status: Complete } on plan_ack.
  5. Stage::finish: the stored plan is checked before anything is touched (complete, signature, COPY sources, WRITE hashes); PlanStored.
  6. BootControl::reboot(BootTarget::App): the bootloader applies the plan in place and starts the new image.

Failures and power cuts. A cut during the download loses nothing stored: after the reboot, begin_bulk with the same header resumes after the bytes in the plan area. A cut during the apply is resumed from the journal. No revert once applied.

Resume. begin_bulk with the same header: resume_from is the number of plan bytes already in the plan area. With ack_every the device also reports its position every so many bytes, for very long downloads.

What the device checks: segment offsets and the plan area size as bytes arrive; in finish the whole stored plan (complete, signature, COPY source hashes, WRITE hashes); while applying, every step's result by its hash and the image against the descriptor.

Transcript

Half the plan, then the app answers node in the middle of the download (still version 1, Confirmed), the rest, one Complete, finish into PlanStored, the reboot and the bootloader's apply. Test: parcel_freight_1s_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: InPlace, 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 } }
        45 B  01 80 01 73 69 6d 2d 64 66 75 …
→ stage.limits()
         5 B  02 40 30 00 02
← { max_piece: 64, in_flight: 1, live: false, stored: true, plan_area: 1024, writes_in_background: true }
        13 B  02 80 40 00 00 00 01 40 00 04 …
→ bulk.begin_bulk(target: Stored, header: [35 B: 57 57 44 50 01 92 …], total_len: 484, ack_every: 0)
        50 B  03 40 40 00 10 57 57 44 50 01 …
← Ok({ steps: 10, resumed: false, resume_from: 0 })
        13 B  03 80 80 0a 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 03 00 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: d5 9c 54 49 47 df …] } (sink position 1, 1 unit)
        76 B  00 40 41 a0 01 40 00 00 00 d5 …
→ bulk.plan ⇐ { offset: 128, data: [64 B: 33 3a 70 43 ac 9b …] } (sink position 2, 1 unit)
        76 B  00 40 41 a0 02 80 00 00 00 33 …
→ bulk.plan ⇐ { offset: 192, data: [64 B: f6 b7 40 41 0b 58 …] } (sink position 3, 1 unit)
        76 B  00 40 41 a0 03 c0 00 00 00 f6 …
→ bulk.plan: query: sent 4 units
         7 B  06 40 41 67 00 04 02
← bulk.plan: ack: have 4 units, may send 2062 more bytes
         8 B  06 83 41 50 04 90 0e 02
   ── half way: the app keeps doing its job and answers anything else
→ node.node()
         5 B  07 40 00 00 02
← { role: App, profile: InPlace, 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 } }
        45 B  07 80 01 73 69 6d 2d 64 66 75 …
→ bulk.plan: open its session at 0
         9 B  08 40 41 88 82 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
        10 B  08 80 82 01 00 00 40 90 0e 00
→ bulk.plan ⇐ { offset: 256, data: [64 B: a8 09 c6 1b 42 94 …] } (sink position 0, 1 unit)
        76 B  00 40 41 a0 00 00 01 00 00 a8 …
→ bulk.plan ⇐ { offset: 320, data: [64 B: 46 48 7b 9a 2b 96 …] } (sink position 1, 1 unit)
        76 B  00 40 41 a0 01 40 01 00 00 46 …
→ bulk.plan ⇐ { offset: 384, data: [64 B: bd 9c c5 d5 84 ff …] } (sink position 2, 1 unit)
        76 B  00 40 41 a0 02 80 01 00 00 bd …
→ bulk.plan ⇐ { offset: 448, data: [36 B: e2 ce f7 7b 10 ee …] } (sink position 3, 1 unit)
        47 B  00 40 41 a0 03 c0 01 00 00 e2 …
← bulk.plan_ack ⇒ { have: 484, status: Complete }
         9 B  00 82 42 e4 01 00 00 20 02
→ bulk.plan: query: sent 4 units
         7 B  09 40 41 67 00 04 02
← bulk.plan: ack: have 4 units, may send 2062 more bytes
         8 B  09 83 41 50 04 90 0e 02
→ stage.finish()
         5 B  0a 40 36 00 02
← Ok(PlanStored)
         3 B  0a 80 84
→ boot.reboot(into: App)
         6 B  0b 40 13 00 00 02
← Ok(())
         3 B  0b 80 80
← Disconnect(RequestByUser)
         1 B  02
   ── device resets (into the app)
   ── bootloader applies the stored plan in place: 10 steps
   ── bootloader: image verified against the descriptor, plan area cleared
   ── 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: InPlace, 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 } }
        45 B  01 80 01 73 69 6d 2d 64 66 75 …
Wire bytes: 24 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
←   52 B  03 08 2d 01 80 01 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
          e9 30 23 33
→   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 40 00 04 00 00 80
          e5 6d df 5d
→   57 B  05 08 32 03 40 40 00 10 57 57 44 50 01 92 10 00
          00 02 00 00 00 0a 00 e4 01 00 00 86 ba 52 71 00
          2c c3 5d 10 9b 6b 17 9f ae b3 bd e4 01 00 00 00
          00 00 00 3c 02 69 6a 43 f5
←   20 B  05 08 0d 03 80 80 0a 00 00 00 00 00 00 00 00 19
          27 24 34 37
→   11 B  06 05 04 40 42 80 02 c3 25 76 66
←   12 B  06 06 04 80 00 00 00 00 50 11 ea ea
→   15 B  07 08 08 05 40 41 88 01 00 00 02 4d 19 78 f5
←   30 B  07 08 08 00 83 41 50 00 90 0e 02 bc 54 c4 22 08
          09 05 80 01 00 00 40 90 0e 00 0d e3 c4 0e
→  341 B  08 08 4c 00 40 41 a0 00 00 00 00 00 01 00 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 e6 bd 84 3c 31 2f c7 a7 fe 00 89 02 7a
          89 0f 7d 08 4c 00 40 41 a0 01 40 00 00 00 d5 9c
          54 49 47 df bf 16 ed b4 6c 61 5f f7 d7 2e 03 01
          00 3c e2 93 64 32 07 b8 7c 40 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 00 89
          02 be 4a 5b 2a 08 4c 00 40 41 a0 02 80 00 00 00
          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 01 01 00
          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 28 29 f3 40 2e 64
          00 89 02 a0 a2 cb 64 08 4c 00 40 41 a0 03 c0 00
          00 00 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 01 02 00 03
          04 00 00 89 02 9f c6 b7 14 07 06 40 41 67 00 04
          02 92 9a 9c 72
←   15 B  08 08 08 06 83 41 50 04 90 0e 02 aa 3e 31 f8
   ── half way: the app keeps doing its job and answers anything else
→   11 B  09 05 07 40 00 00 02 1f ee 03 9f
←   52 B  09 08 2d 07 80 01 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
          da 74 28 01
→   16 B  0a 08 09 08 40 41 88 82 01 00 00 02 b7 89 7a c8
←   31 B  0a 08 08 00 83 41 50 00 90 0e 02 bb 19 50 e9 08
          0a 08 80 82 01 00 00 40 90 0e 00 35 0a e7 eb
→  312 B  0b 08 4c 00 40 41 a0 00 00 01 00 00 a8 09 c6 1b
          42 94 29 60 40 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 00 89 02 fe
          0b 9d 4c 08 4c 00 40 41 a0 01 40 01 00 00 46 48
          7b 9a 2b 96 16 b0 5e 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 e7 cd dc 00 89
          02 a4 6e 26 41 08 4c 00 40 41 a0 02 80 01 00 00
          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 d6 1a dc
          00 89 02 d9 dd 7d 8c 08 2f 00 40 41 a0 03 c0 01
          00 00 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 4c 02 ab e2 98 58 07 09 40 41
          67 00 04 02 be d6 59 bd
←   30 B  0b 08 09 00 82 42 e4 01 00 00 20 02 b3 7b 24 45
          08 08 09 83 41 50 04 90 0e 02 07 4a f3 b8
→   11 B  0c 05 0a 40 36 00 02 2b 9c 4d 15
←    9 B  0c 03 0a 80 84 56 ef a6 cd
→   12 B  0d 06 0b 40 13 00 00 02 55 ce 61 03
←   16 B  0d 03 0b 80 80 c8 68 69 f6 30 c1 02 72 65 aa cb
   ── device resets (into the app)
   ── bootloader applies the stored plan in place: 10 steps
   ── bootloader: image verified against the descriptor, plan area cleared
   ── device runs the app
→   11 B  03 05 01 40 00 00 02 f1 44 33 b4
←   52 B  03 08 2d 01 80 01 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
          a4 0d 04 13

1L-UPD-STEP: Pit-stop Shuttle

One app slot; the app reboots into an updater (a bootloader with a transport, or a recovery app), which receives the plan one call per step and applies it in place as it arrives.

Setup. One app slot, no extra flash. The updater is either the bootloader with its own transport (USB, CAN) or a small write-protected recovery app at its own address; it must come back up after any cut and finish the job.

| bootloader + transport = updater | APP slot: rewritten in place |
| bootloader | recovery app (updater, write-protected) | APP slot: rewritten in place |

The app implements UpdateNode and BootControl only (often just reboot(BootTarget::Updater) matters). The updater implements UpdateNode, BootControl, Inventory and Stage (NodeInfo::role is Bootloader or Recovery).

Choose it when the flash has no room for a second image or a plan area, and the bootloader (or a recovery app) can have a transport. It is the fastest end to end and needs no extra flash, but the device does nothing else from the reboot into the updater until the new app starts.

Flow.

  1. To the app: UpdateNode::node: offers has no stage, so the host calls BootControl::reboot(BootTarget::Updater).
  2. The host reconnects to the same device; the updater answers, with a different API.
  3. UpdateNode::node, Stage::limits: an updater usually has a small buffer, chunks come in pieces of max_piece.
  4. Stage::begin(Target::Live, header): from here on the app slot is not bootable until the session finishes (BootState::Updating); the updater records the session in flash.
  5. Stage::erase / copy / write applied in place as they arrive; the planner never copies from a chunk an earlier step has overwritten.
  6. Stage::finish: the new image is checked against the descriptor and marked bootable (Confirmed).
  7. BootControl::reboot(BootTarget::App): the new app runs.

Failures and power cuts. A cut leaves no bootable app: the bootloader sees that and starts the updater again (BootState::Updating, app_version: None), and the host continues. A lost link the same. The updater must be able to resume any session it started, which is why it keeps its session record (header and next step) in flash.

Resume. begin with the same header: resumed: true, resume_from the step after the last one that finished; the step in flight is redone from its first piece (Stage::progress shows the piece offset while the link is up).

What the device checks: the header (format, geometry, floor, signature) before erasing anything, every index against the app slot, COPY source hashes, WRITE chunk hashes after the last piece, the image against the descriptor.

Transcript

The lean app reboots into the updater, which takes chunks in 32-byte pieces. The power fails half way into step 7 (no reply); the device comes back in the updater with no valid app; begin resumes at step 7; the update finishes and the new app starts. Test: pit_stop_shuttle_1l_upd_step over simulated USB, 64 B packets.

   ── device runs the app
   ── link up: dfu_lean_app_api (link setup not shown, same on every link)
→ node.node()
         5 B  01 40 00 00 02
← { role: App, profile: InPlace, 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: false,
      stage: false, stage_bulk: false } }
        45 B  01 80 01 73 69 6d 2d 64 66 75 …
→ boot.reboot(into: Updater)
         6 B  02 40 13 00 10 02
← Ok(())
         3 B  02 80 80
← Disconnect(RequestByUser)
         1 B  02
   ── device resets (into the updater)
   ── device runs the updater
   ── host reconnects to the same device: the updater answers
   ── link up: dfu_updater_api (link setup not shown, same on every link)
→ node.node()
         5 B  01 40 00 00 02
← { role: Bootloader, profile: InPlace, 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 } }
        45 B  01 80 11 73 69 6d 2d 64 66 75 …
→ stage.limits()
         5 B  02 40 30 00 02
← { max_piece: 32, in_flight: 1, live: true, stored: false, plan_area: 0, writes_in_background: false }
        13 B  02 80 20 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: 10, resumed: false, resume_from: 0 })
        13 B  03 80 80 0a 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.write(index: 1, dst: 0, hash: 2780790d97efcff7, offset: 0, data: [32 B: 46 57 02 00 00 00 …])
        56 B  05 40 34 00 01 00 00 00 00 00 …
← Ok(())
         3 B  05 80 80
→ stage.write(index: 1, dst: 0, hash: 2780790d97efcff7, offset: 32, data: [32 B: a5 6c 24 19 17 af …])
        56 B  06 40 34 00 01 00 00 00 00 00 …
← Ok(())
         3 B  06 80 80
→ stage.write(index: 2, dst: 1, hash: 3ce293643207b87c, offset: 0, data: [32 B: 68 61 6c d3 da 10 …])
        56 B  07 40 34 00 02 00 00 00 01 00 …
← Ok(())
         3 B  07 80 80
→ stage.write(index: 2, dst: 1, hash: 3ce293643207b87c, offset: 32, data: [32 B: 83 88 ba 33 3a 70 …])
        56 B  08 40 34 00 02 00 00 00 01 00 …
← Ok(())
         3 B  08 80 80
→ stage.erase(index: 3, block: 1)
        11 B  09 40 32 00 03 00 00 00 01 00 …
← Ok(())
         3 B  09 80 80
→ stage.write(index: 4, dst: 2, hash: edcc83d5b3974a90, offset: 0, data: [32 B: 61 70 70 20 76 32 …])
        56 B  0a 40 34 00 04 00 00 00 02 00 …
← Ok(())
         3 B  0a 80 80
→ stage.write(index: 4, dst: 2, hash: edcc83d5b3974a90, offset: 32, data: [32 B: 70 5e 94 26 e7 70 …])
        56 B  0b 40 34 00 04 00 00 00 02 00 …
← Ok(())
         3 B  0b 80 80
→ stage.copy(index: 5, dst: 3, src: 5, hash: 1ec70fb257d1befc)
        21 B  0c 40 33 00 05 00 00 00 03 00 …
← Ok(())
         3 B  0c 80 80
→ stage.erase(index: 6, block: 2)
        11 B  0d 40 32 00 06 00 00 00 02 00 …
← Ok(())
         3 B  0d 80 80
→ stage.write(index: 7, dst: 4, hash: a809c61b42942960, offset: 0, data: [32 B: 75 69 83 ee 6e 08 …])
        56 B  0e 40 34 00 07 00 00 00 04 00 …
← Ok(())
         3 B  0e 80 80
→ stage.write(index: 7, dst: 4, hash: a809c61b42942960, offset: 32, data: [32 B: 33 52 e3 4e ce 68 …])
        56 B  0f 40 34 00 07 00 00 00 04 00 …
   ── power cut while the device applies step 7: no reply
   ── bootloader: an update is under way, the app slot is not bootable: starts the updater
   ── device runs the updater
   ── host: the call fails (link lost), reconnects when the device is back
   ── link up: dfu_updater_api (link setup not shown, same on every link)
→ node.node()
         5 B  01 40 00 00 02
← { role: Bootloader, profile: InPlace, product: "sim-dfu", geometry_id: 4242, product_id: 1, seal: Unsigned,
      app_version: None, version_floor: 1, manifest_digest: None, boot_state: Updating,
      last_result: { outcome: Nothing, version: 0 }, offers: { boot_control: true, inventory: true,
      stage: true, stage_bulk: true } }
        32 B  01 80 11 73 69 6d 2d 64 66 75 …
→ stage.begin(target: Live, header: [35 B: 57 57 44 50 01 92 …])
        42 B  02 40 31 00 00 57 57 44 50 01 …
← Ok({ steps: 10, resumed: true, resume_from: 7 })
        13 B  02 80 80 0a 00 00 00 80 07 00 …
→ stage.write(index: 7, dst: 4, hash: a809c61b42942960, offset: 0, data: [32 B: 75 69 83 ee 6e 08 …])
        56 B  03 40 34 00 07 00 00 00 04 00 …
← Ok(())
         3 B  03 80 80
→ stage.write(index: 7, dst: 4, hash: a809c61b42942960, offset: 32, data: [32 B: 33 52 e3 4e ce 68 …])
        56 B  04 40 34 00 07 00 00 00 04 00 …
← Ok(())
         3 B  04 80 80
→ stage.write(index: 8, dst: 5, hash: 3bf9a7bd49c0f887, offset: 0, data: [32 B: 6e 65 74 20 76 32 …])
        56 B  05 40 34 00 08 00 00 00 05 00 …
← Ok(())
         3 B  05 80 80
→ stage.write(index: 8, dst: 5, hash: 3bf9a7bd49c0f887, offset: 32, data: [32 B: c4 a5 84 ad bd 6c …])
        56 B  06 40 34 00 08 00 00 00 05 00 …
← Ok(())
         3 B  06 80 80
→ stage.write(index: 9, dst: 6, hash: d18f7ce680781257, offset: 0, data: [32 B: 6f 74 61 94 9a 86 …])
        56 B  07 40 34 00 09 00 00 00 06 00 …
← Ok(())
         3 B  07 80 80
→ stage.write(index: 9, dst: 6, hash: d18f7ce680781257, offset: 32, data: [32 B: 10 ee 32 f4 fa e6 …])
        56 B  08 40 34 00 09 00 00 00 06 00 …
← Ok(())
         3 B  08 80 80
→ stage.finish()
         5 B  09 40 36 00 02
← Ok(Confirmed)
         3 B  09 80 80
→ boot.reboot(into: App)
         6 B  0a 40 13 00 00 02
← Ok(())
         3 B  0a 80 80
← Disconnect(RequestByUser)
         1 B  02
   ── device resets (into the app)
   ── device runs the app
   ── link up: dfu_lean_app_api (link setup not shown, same on every link)
→ node.node()
         5 B  01 40 00 00 02
← { role: App, profile: InPlace, 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: false,
      stage: false, stage_bulk: false } }
        45 B  01 80 01 73 69 6d 2d 64 66 75 …
Wire bytes: 55 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
←   47 B  08 2d 01 80 01 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 80 01 57
→    7 B  06 02 40 13 00 10 02
←    7 B  03 02 80 80 30 c1 02
   ── device resets (into the updater)
   ── device runs the updater
   ── host reconnects to the same device: the updater answers
→    6 B  05 01 40 00 00 02
←   47 B  08 2d 01 80 11 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 20 00 00 00 01 80 00 00 00 00 00
→   44 B  08 2a 03 40 31 00 00 57 57 44 50 01 92 10 00 00
          02 00 00 00 0a 00 e4 01 00 00 86 ba 52 71 00 2c
          c3 5d 10 9b 6b 17 9f ae b3 bd 3c 02
←   15 B  08 0d 03 80 80 0a 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
→   58 B  08 38 05 40 34 00 01 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 0c 02
←    4 B  03 05 80 80
→   58 B  08 38 06 40 34 00 01 00 00 00 00 00 27 80 79 0d
          97 ef cf f7 20 00 00 00 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 0c 02
←    4 B  03 06 80 80
→   58 B  08 38 07 40 34 00 02 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 0c 02
←    4 B  03 07 80 80
→   58 B  08 38 08 40 34 00 02 00 00 00 01 00 3c e2 93 64
          32 07 b8 7c 20 00 00 00 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 0c 02
←    4 B  03 08 80 80
→   13 B  08 0b 09 40 32 00 03 00 00 00 01 00 02
←    4 B  03 09 80 80
→   58 B  08 38 0a 40 34 00 04 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 0c 02
←    4 B  03 0a 80 80
→   58 B  08 38 0b 40 34 00 04 00 00 00 02 00 ed cc 83 d5
          b3 97 4a 90 20 00 00 00 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 0c 02
←    4 B  03 0b 80 80
→   23 B  08 15 0c 40 33 00 05 00 00 00 03 00 05 00 1e c7
          0f b2 57 d1 be fc 02
←    4 B  03 0c 80 80
→   13 B  08 0b 0d 40 32 00 06 00 00 00 02 00 02
←    4 B  03 0d 80 80
→   58 B  08 38 0e 40 34 00 07 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 0c 02
←    4 B  03 0e 80 80
→   58 B  08 38 0f 40 34 00 07 00 00 00 04 00 a8 09 c6 1b
          42 94 29 60 20 00 00 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 0c 02
   ── power cut while the device applies step 7: no reply
   ── bootloader: an update is under way, the app slot is not bootable: starts the updater
   ── device runs the updater
   ── host: the call fails (link lost), reconnects when the device is back
→    6 B  05 01 40 00 00 02
←   34 B  08 20 01 80 11 73 69 6d 2d 64 66 75 92 10 00 00
          01 00 00 00 00 01 00 00 00 05 00 00 00 00 00 f0
          01 57
→   44 B  08 2a 02 40 31 00 00 57 57 44 50 01 92 10 00 00
          02 00 00 00 0a 00 e4 01 00 00 86 ba 52 71 00 2c
          c3 5d 10 9b 6b 17 9f ae b3 bd 3c 02
←   15 B  08 0d 02 80 80 0a 00 00 00 80 07 00 00 00 19
→   58 B  08 38 03 40 34 00 07 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 0c 02
←    4 B  03 03 80 80
→   58 B  08 38 04 40 34 00 07 00 00 00 04 00 a8 09 c6 1b
          42 94 29 60 20 00 00 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 0c 02
←    4 B  03 04 80 80
→   58 B  08 38 05 40 34 00 08 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 0c 02
←    4 B  03 05 80 80
→   58 B  08 38 06 40 34 00 08 00 00 00 05 00 3b f9 a7 bd
          49 c0 f8 87 20 00 00 00 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 0c 02
←    4 B  03 06 80 80
→   58 B  08 38 07 40 34 00 09 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 0c 02
←    4 B  03 07 80 80
→   58 B  08 38 08 40 34 00 09 00 00 00 06 00 d1 8f 7c e6
          80 78 12 57 20 00 00 00 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 0c 02
←    4 B  03 08 80 80
→    6 B  05 09 40 36 00 02
←    4 B  03 09 80 80
→    7 B  06 0a 40 13 00 00 02
←    7 B  03 0a 80 80 30 c1 02
   ── device resets (into the app)
   ── device runs the app
→    6 B  05 01 40 00 00 02
←   47 B  08 2d 01 80 01 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 80 01 57

1L-UPD-BULK: Pit-stop Freight

One app slot; the app reboots into an updater, which receives the whole plan as one stream and applies it in place as the steps complete.

Setup. One app slot, no extra flash; the updater is the bootloader with a transport or a recovery app.

| bootloader + transport = updater | APP slot: rewritten in place |
| bootloader | recovery app (updater, write-protected) | APP slot: rewritten in place |

The app implements UpdateNode and BootControl. The updater implements UpdateNode, BootControl, Stage and StageBulk.

Choose it when there is no room for a second image or a plan area, the updater has a transport, and round trips are long, or the link is fast and the host just wants to stream the plan without waiting on every step.

Flow.

  1. To the app: UpdateNode::node, then BootControl::reboot(BootTarget::Updater).
  2. The host reconnects; the updater answers. UpdateNode::node, Stage::limits.
  3. StageBulk::begin_bulk(Target::Live, header, total_len, ack_every: 0): the app slot stops being bootable.
  4. The plan after its header on the plan credit sink, in segments of at most max_piece; the updater parses the steps from the stream and applies each in place as soon as it is complete.
  5. PlanAck { status: Complete } on plan_ack, then Stage::finish: checked, Confirmed.
  6. BootControl::reboot(BootTarget::App): the new app runs.

Failures and power cuts. A cut leaves no bootable app; the bootloader starts the updater again and the host resumes. A step that fails ends the session with PlanAck { status: Failed(error) }; the device stays in the updater until a session finishes.

Resume. begin_bulk with the same header answers the byte after the last applied step (the updater keeps it in its session record); bytes of a step that was not complete are sent again.

What the device checks: segment offsets, the header before erasing anything, each completed step (ranges, COPY source hash, WRITE hash), the image against the descriptor.

Transcript

The lean app reboots into the updater; the in-place delta goes in 32-byte segments in one stream, one Complete, finish into Confirmed, and the new app starts. Test: pit_stop_freight_1l_upd_bulk over WebSocket on localhost.

   ── device runs the app
   ── link up: dfu_lean_app_api (link setup not shown, same on every link)
→ node.node()
         5 B  01 40 00 00 02
← { role: App, profile: InPlace, 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: false,
      stage: false, stage_bulk: false } }
        45 B  01 80 01 73 69 6d 2d 64 66 75 …
→ boot.reboot(into: Updater)
         6 B  02 40 13 00 10 02
← Ok(())
         3 B  02 80 80
← Disconnect(RequestByUser)
         1 B  02
   ── device resets (into the updater)
   ── device runs the updater
   ── link up: dfu_updater_api (link setup not shown, same on every link)
→ node.node()
         5 B  01 40 00 00 02
← { role: Bootloader, profile: InPlace, 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 } }
        45 B  01 80 11 73 69 6d 2d 64 66 75 …
→ stage.limits()
         5 B  02 40 30 00 02
← { max_piece: 32, in_flight: 1, live: true, stored: false, plan_area: 0, writes_in_background: false }
        13 B  02 80 20 00 00 00 01 80 00 00 …
→ bulk.begin_bulk(target: Live, header: [35 B: 57 57 44 50 01 92 …], total_len: 484, ack_every: 0)
        50 B  03 40 40 00 00 57 57 44 50 01 …
← Ok({ steps: 10, resumed: false, resume_from: 0 })
        13 B  03 80 80 0a 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: [32 B: 01 00 00 03 00 00 …] } (sink position 0, 1 unit)
        43 B  00 40 41 a0 00 00 00 00 00 01 …
→ bulk.plan ⇐ { offset: 32, data: [32 B: 75 3c f4 e9 e7 7f …] } (sink position 1, 1 unit)
        43 B  00 40 41 a0 01 20 00 00 00 75 …
→ bulk.plan ⇐ { offset: 64, data: [32 B: d5 9c 54 49 47 df …] } (sink position 2, 1 unit)
        43 B  00 40 41 a0 02 40 00 00 00 d5 …
→ bulk.plan ⇐ { offset: 96, data: [32 B: d3 da 10 e3 4c 3b …] } (sink position 3, 1 unit)
        43 B  00 40 41 a0 03 60 00 00 00 d3 …
→ bulk.plan ⇐ { offset: 128, data: [32 B: 33 3a 70 43 ac 9b …] } (sink position 4, 1 unit)
        43 B  00 40 41 a0 04 80 00 00 00 33 …
→ bulk.plan ⇐ { offset: 160, data: [32 B: 03 02 00 ed cc 83 …] } (sink position 5, 1 unit)
        43 B  00 40 41 a0 05 a0 00 00 00 03 …
→ bulk.plan ⇐ { offset: 192, data: [32 B: f6 b7 40 41 0b 58 …] } (sink position 6, 1 unit)
        43 B  00 40 41 a0 06 c0 00 00 00 f6 …
→ bulk.plan ⇐ { offset: 224, data: [32 B: 56 17 a0 a1 6b b8 …] } (sink position 7, 1 unit)
        43 B  00 40 41 a0 07 e0 00 00 00 56 …
→ bulk.plan ⇐ { offset: 256, data: [32 B: a8 09 c6 1b 42 94 …] } (sink position 8, 1 unit)
        43 B  00 40 41 a0 08 00 01 00 00 a8 …
→ bulk.plan ⇐ { offset: 288, data: [32 B: e6 e8 1b 3a cb 36 …] } (sink position 9, 1 unit)
        43 B  00 40 41 a0 09 20 01 00 00 e6 …
→ bulk.plan ⇐ { offset: 320, data: [32 B: 46 48 7b 9a 2b 96 …] } (sink position 10, 1 unit)
        43 B  00 40 41 a0 0a 40 01 00 00 46 …
→ bulk.plan ⇐ { offset: 352, data: [32 B: 5d 3c 65 75 24 9f …] } (sink position 11, 1 unit)
        43 B  00 40 41 a0 0b 60 01 00 00 5d …
→ bulk.plan ⇐ { offset: 384, data: [32 B: bd 9c c5 d5 84 ff …] } (sink position 12, 1 unit)
        43 B  00 40 41 a0 0c 80 01 00 00 bd …
→ bulk.plan ⇐ { offset: 416, data: [32 B: 12 57 40 00 6f 74 …] } (sink position 13, 1 unit)
        43 B  00 40 41 a0 0d a0 01 00 00 12 …
→ bulk.plan ⇐ { offset: 448, data: [32 B: e2 ce f7 7b 10 ee …] } (sink position 14, 1 unit)
        43 B  00 40 41 a0 0e c0 01 00 00 e2 …
→ bulk.plan ⇐ { offset: 480, data: [4 B: 42 2e 57 db] } (sink position 15, 1 unit)
        15 B  00 40 41 a0 0f e0 01 00 00 42 …
← bulk.plan_ack ⇒ { have: 484, status: Complete }
         9 B  00 82 42 e4 01 00 00 20 02
→ bulk.plan: query: sent 16 units
         7 B  06 40 41 67 00 10 02
← bulk.plan: ack: have 16 units, may send 2062 more bytes
         8 B  06 83 41 50 10 90 0e 02
→ stage.finish()
         5 B  07 40 36 00 02
← Ok(Confirmed)
         3 B  07 80 80
→ 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)
   ── device runs the app
   ── link up: dfu_lean_app_api (link setup not shown, same on every link)
→ node.node()
         5 B  01 40 00 00 02
← { role: App, profile: InPlace, 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: false,
      stage: false, stage_bulk: false } }
        45 B  01 80 01 73 69 6d 2d 64 66 75 …
Wire bytes: 42 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
←   48 B  82 2e 00 01 80 01 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 80 01 57
→   13 B  82 87 00 00 00 00 00 02 40 13 00 10 02
←    6 B  82 04 00 02 80 80
←    4 B  82 02 0c 02
   ── device resets (into the updater)
   ── device runs the updater
→   12 B  82 86 00 00 00 00 00 01 40 00 00 02
←   48 B  82 2e 00 01 80 11 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 20 00 00 00 01 80 00 00 00 00 00
→   57 B  82 b3 00 00 00 00 00 03 40 40 00 00 57 57 44 50
          01 92 10 00 00 02 00 00 00 0a 00 e4 01 00 00 86
          ba 52 71 00 2c c3 5d 10 9b 6b 17 9f ae b3 bd e4
          01 00 00 00 00 00 00 3c 02
←   16 B  82 0e 00 03 80 80 0a 00 00 00 00 00 00 00 00 19
→   12 B  82 86 00 00 00 00 00 04 40 42 80 02
←    9 B  82 07 00 04 80 00 00 00 00
→   15 B  82 89 00 00 00 00 00 05 40 41 88 01 00 00 02
←   11 B  82 09 00 00 83 41 50 00 90 0e 02
←   12 B  82 0a 00 05 80 01 00 00 40 90 0e 00
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 00 00 00 00 00
          01 00 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
          0c 02
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 01 20 00 00 00
          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
          0c 02
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 02 40 00 00 00
          d5 9c 54 49 47 df bf 16 ed b4 6c 61 5f f7 d7 2e
          03 01 00 3c e2 93 64 32 07 b8 7c 40 00 68 61 6c
          0c 02
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 03 60 00 00 00
          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
          0c 02
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 04 80 00 00 00
          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 01 01 00
          0c 02
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 05 a0 00 00 00
          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 28 29 f3 40 2e 64
          0c 02
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 06 c0 00 00 00
          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
          0c 02
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 07 e0 00 00 00
          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 01 02 00 03 04 00
          0c 02
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 08 00 01 00 00
          a8 09 c6 1b 42 94 29 60 40 00 75 69 83 ee 6e 08
          b6 b8 eb 0a 9b 06 86 20 ce d0 03 22 b3 1e 9e 38
          0c 02
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 09 20 01 00 00
          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
          0c 02
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 0a 40 01 00 00
          46 48 7b 9a 2b 96 16 b0 5e 60 03 05 00 3b f9 a7
          bd 49 c0 f8 87 40 00 6e 65 74 20 76 32 87 6d 7c
          0c 02
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 0b 60 01 00 00
          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
          0c 02
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 0c 80 01 00 00
          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
          0c 02
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 0d a0 01 00 00
          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 d6 1a dc
          0c 02
→   50 B  82 ac 00 00 00 00 00 00 40 41 a0 0e c0 01 00 00
          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
          0c 02
→   22 B  82 90 00 00 00 00 00 00 40 41 a0 0f e0 01 00 00
          42 2e 57 db 04 02
→   14 B  82 88 00 00 00 00 00 06 40 41 67 00 10 02
←   12 B  82 0a 00 00 82 42 e4 01 00 00 20 02
←   11 B  82 09 00 06 83 41 50 10 90 0e 02
→   12 B  82 86 00 00 00 00 00 07 40 36 00 02
←    6 B  82 04 00 07 80 80
→   13 B  82 87 00 00 00 00 00 08 40 13 00 00 02
←    6 B  82 04 00 08 80 80
←    4 B  82 02 0c 02
   ── device resets (into the app)
   ── device runs the app
→   12 B  82 86 00 00 00 00 00 01 40 00 00 02
←   48 B  82 2e 00 01 80 01 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 80 01 57