Skip to content

Commit f68f586

Browse files
committed
Fix access-controller OTA bricking on first update after USB flash
On a freshly USB-flashed device, otadata is erased and Ota::current_slot() returns Slot::None. With no factory partition, the IDF bootloader falls back to booting ota_0 - but Slot::None.next() returns Slot0, so OtaWriter picked ota_0 (the running slot) as the destination and overwrote the live app in place before pointing otadata at the half-written result. Normalize Slot::None to Slot0 before computing the next slot so the first OTA after a USB flash lands in ota_1.
1 parent 1018dd7 commit f68f586

1 file changed

Lines changed: 27 additions & 8 deletions

File tree

access-controller/src/ota.rs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ pub fn status() -> Result<OtaStatus, OtaError> {
115115
let current = {
116116
let mut region = otadata.as_embedded_storage(&mut flash);
117117
let mut ota = Ota::new(&mut region).map_err(|_| OtaError::Ota)?;
118-
ota.current_slot().map_err(|_| OtaError::Ota)?
118+
effective_current(ota.current_slot().map_err(|_| OtaError::Ota)?)
119119
};
120120

121121
let next = current.next();
@@ -154,7 +154,7 @@ pub fn rollback() -> Result<Slot, OtaError> {
154154

155155
let mut region = otadata.as_embedded_storage(&mut flash);
156156
let mut ota = Ota::new(&mut region).map_err(|_| OtaError::Ota)?;
157-
let cur = ota.current_slot().map_err(|_| OtaError::Ota)?;
157+
let cur = effective_current(ota.current_slot().map_err(|_| OtaError::Ota)?);
158158
let other = cur.next();
159159
ota.set_current_slot(other).map_err(|_| OtaError::Ota)?;
160160
Ok(other)
@@ -209,7 +209,7 @@ impl OtaWriter {
209209
let current = {
210210
let mut region = otadata.as_embedded_storage(&mut flash);
211211
let mut ota = Ota::new(&mut region).map_err(|_| OtaError::Ota)?;
212-
ota.current_slot().map_err(|_| OtaError::Ota)?
212+
effective_current(ota.current_slot().map_err(|_| OtaError::Ota)?)
213213
};
214214
let next = current.next();
215215

@@ -341,13 +341,32 @@ pub fn slot_label(slot: Slot) -> &'static str {
341341
}
342342
}
343343

344+
/// Normalize `Slot::None` (returned when `otadata` is erased, e.g. on a
345+
/// fresh USB flash) into the slot the IDF bootloader actually boots from
346+
/// in that state.
347+
///
348+
/// This firmware is built without a factory partition, so when otadata
349+
/// is invalid the bootloader falls back to `ota_0`. Crucially,
350+
/// `Slot::None.next()` in `esp-bootloader-esp-idf` returns `Slot0`,
351+
/// which would cause `OtaWriter::begin` to pick `ota_0` as the "next"
352+
/// slot - i.e. write the new image directly over the currently
353+
/// executing app and then point otadata at the half-written result.
354+
/// Normalizing here ensures the first OTA after a USB flash lands in
355+
/// `ota_1` instead of bricking the device.
356+
fn effective_current(slot: Slot) -> Slot {
357+
match slot {
358+
Slot::None => Slot::Slot0,
359+
s => s,
360+
}
361+
}
362+
344363
fn app_subtype_for(slot: Slot) -> AppPartitionSubType {
345364
match slot {
346-
// Slot::None means "no OTA has happened yet"; the bootloader
347-
// will boot the factory image if present, else ota_0. We treat
348-
// it as ota_0 here since the firmware is built without a
349-
// factory partition.
350-
Slot::None | Slot::Slot0 => AppPartitionSubType::Ota0,
365+
Slot::Slot0 => AppPartitionSubType::Ota0,
351366
Slot::Slot1 => AppPartitionSubType::Ota1,
367+
// Callers must normalize `Slot::None` via `effective_current`
368+
// before reaching here; if they don't, fall back to ota_0 so we
369+
// at least describe a real partition rather than panicking.
370+
Slot::None => AppPartitionSubType::Ota0,
352371
}
353372
}

0 commit comments

Comments
 (0)