From e79592c7af7b3476d2e51f5859c586b9ff8f5381 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Tue, 2 Dec 2025 11:20:43 +0100 Subject: feat: support OTA commands in esp-hosted driver * Expose OTA functionality in control * Handle OTA swap wait in runner --- embassy-net-esp-hosted/src/control.rs | 46 ++++++++++++++++++++++++++++++++++- embassy-net-esp-hosted/src/ioctl.rs | 17 +++++++++++++ embassy-net-esp-hosted/src/lib.rs | 5 ++++ embassy-net-esp-hosted/src/proto.rs | 12 ++++----- 4 files changed, 73 insertions(+), 7 deletions(-) diff --git a/embassy-net-esp-hosted/src/control.rs b/embassy-net-esp-hosted/src/control.rs index 38ec648b4..d96a62daf 100644 --- a/embassy-net-esp-hosted/src/control.rs +++ b/embassy-net-esp-hosted/src/control.rs @@ -24,6 +24,11 @@ pub struct Control<'a> { shared: &'a Shared, } +/// Handle for managing firmware update. +pub struct UpdateControl<'a, 'd> { + control: &'a mut Control<'d>, +} + /// WiFi mode. #[allow(unused)] #[derive(Copy, Clone, PartialEq, Eq, Debug)] @@ -146,6 +151,13 @@ impl<'a> Control<'a> { Ok(()) } + /// Initiate a firmware update. + pub async fn update(&mut self) -> Result, Error> { + let req = proto::CtrlMsg_Req_OTABegin {}; + ioctl!(self, ReqOtaBegin, RespOtaBegin, req, resp); + Ok(UpdateControl { control: self }) + } + /// duration in seconds, clamped to [10, 3600] async fn set_heartbeat(&mut self, duration: u32) -> Result<(), Error> { let req = proto::CtrlMsg_Req_ConfigHeartbeat { @@ -175,7 +187,8 @@ impl<'a> Control<'a> { async fn ioctl(&mut self, msg: &mut CtrlMsg) -> Result<(), Error> { debug!("ioctl req: {:?}", &msg); - let mut buf = [0u8; 128]; + // Theoretical max overhead is 29 bytes. Biggest message is OTA write with 256 bytes. + let mut buf = [0u8; 256 + 29]; let buf_len = buf.len(); let mut encoder = PbEncoder::new(&mut buf[..]); @@ -216,6 +229,37 @@ impl<'a> Control<'a> { } } +impl<'a, 'd> UpdateControl<'a, 'd> { + /// Write slice of firmware to a device. + /// + /// The slice is split into chunks that can be sent across + /// the ioctl protocol to the wifi adapter. + pub async fn write(&mut self, data: &[u8]) -> Result<(), Error> { + let this = &mut self.control; + for chunk in data.chunks(256) { + let req = proto::CtrlMsg_Req_OTAWrite { + ota_data: heapless::Vec::from_slice(chunk).unwrap(), + }; + ioctl!(this, ReqOtaWrite, RespOtaWrite, req, resp); + } + Ok(()) + } + + /// End the OTA session. + /// + /// NOTE: Will reset the wifi adapter after 5 seconds. + pub async fn finish(self) -> Result<(), Error> { + let this = self.control; + let req = proto::CtrlMsg_Req_OTAEnd {}; + ioctl!(this, ReqOtaEnd, RespOtaEnd, req, resp); + // Ensures that run loop awaits reset + this.shared.ota_done(); + // Wait for re-init + this.init().await?; + Ok(()) + } +} + // WHY IS THIS A STRING? WHYYYY fn parse_mac(mac: &str) -> Result<[u8; 6], Error> { fn nibble_from_hex(b: u8) -> Result { diff --git a/embassy-net-esp-hosted/src/ioctl.rs b/embassy-net-esp-hosted/src/ioctl.rs index a516f80c7..7f462d528 100644 --- a/embassy-net-esp-hosted/src/ioctl.rs +++ b/embassy-net-esp-hosted/src/ioctl.rs @@ -24,6 +24,7 @@ pub struct Shared(RefCell); struct SharedInner { ioctl: IoctlState, is_init: bool, + is_ota: bool, control_waker: WakerRegistration, runner_waker: WakerRegistration, } @@ -33,6 +34,7 @@ impl Shared { Self(RefCell::new(SharedInner { ioctl: IoctlState::Done { resp_len: 0 }, is_init: false, + is_ota: false, control_waker: WakerRegistration::new(), runner_waker: WakerRegistration::new(), })) @@ -99,11 +101,26 @@ impl Shared { } } + // ota + pub fn ota_done(&self) { + let mut this = self.0.borrow_mut(); + this.is_ota = true; + this.is_init = false; + this.runner_waker.wake(); + } + + // check if ota is in progress + pub fn is_ota(&self) -> bool { + let this = self.0.borrow(); + this.is_ota + } + // // // // // // // // // // // // // // // // // // // // pub fn init_done(&self) { let mut this = self.0.borrow_mut(); this.is_init = true; + this.is_ota = false; this.control_waker.wake(); } diff --git a/embassy-net-esp-hosted/src/lib.rs b/embassy-net-esp-hosted/src/lib.rs index d882af8cf..7236e73e8 100644 --- a/embassy-net-esp-hosted/src/lib.rs +++ b/embassy-net-esp-hosted/src/lib.rs @@ -234,6 +234,11 @@ where tx_buf[..PayloadHeader::SIZE].fill(0); } Either4::Fourth(()) => { + // Extend the deadline if OTA + if self.shared.is_ota() { + self.heartbeat_deadline = Instant::now() + HEARTBEAT_MAX_GAP; + continue; + } panic!("heartbeat from esp32 stopped") } } diff --git a/embassy-net-esp-hosted/src/proto.rs b/embassy-net-esp-hosted/src/proto.rs index 74c67bd61..09bec8984 100644 --- a/embassy-net-esp-hosted/src/proto.rs +++ b/embassy-net-esp-hosted/src/proto.rs @@ -16,7 +16,7 @@ Switch to a proper script when https://github.com/YuhanLiin/micropb/issues/30 is // Special config for things that need to be larger g.configure( ".CtrlMsg_Req_OTAWrite.ota_data", - micropb_gen::Config::new().max_bytes(1024), + micropb_gen::Config::new().max_bytes(256), ); g.configure( ".CtrlMsg_Event_ESPInit.init_data", @@ -4296,28 +4296,28 @@ impl ::micropb::MessageEncode for CtrlMsg_Resp_OTABegin { #[derive(Debug, Default, PartialEq, Clone)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct CtrlMsg_Req_OTAWrite { - pub r#ota_data: ::micropb::heapless::Vec, + pub r#ota_data: ::micropb::heapless::Vec, } impl CtrlMsg_Req_OTAWrite { ///Return a reference to `ota_data` #[inline] - pub fn r#ota_data(&self) -> &::micropb::heapless::Vec { + pub fn r#ota_data(&self) -> &::micropb::heapless::Vec { &self.r#ota_data } ///Return a mutable reference to `ota_data` #[inline] - pub fn mut_ota_data(&mut self) -> &mut ::micropb::heapless::Vec { + pub fn mut_ota_data(&mut self) -> &mut ::micropb::heapless::Vec { &mut self.r#ota_data } ///Set the value of `ota_data` #[inline] - pub fn set_ota_data(&mut self, value: ::micropb::heapless::Vec) -> &mut Self { + pub fn set_ota_data(&mut self, value: ::micropb::heapless::Vec) -> &mut Self { self.r#ota_data = value.into(); self } ///Builder method that sets the value of `ota_data`. Useful for initializing the message. #[inline] - pub fn init_ota_data(mut self, value: ::micropb::heapless::Vec) -> Self { + pub fn init_ota_data(mut self, value: ::micropb::heapless::Vec) -> Self { self.r#ota_data = value.into(); self } -- cgit From 741b09ac8a0cb47285568e4e317ef8e4f7ac566e Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Tue, 2 Dec 2025 11:51:46 +0100 Subject: chore: update changelog --- embassy-net-esp-hosted/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/embassy-net-esp-hosted/CHANGELOG.md b/embassy-net-esp-hosted/CHANGELOG.md index d8b912295..6991b39fd 100644 --- a/embassy-net-esp-hosted/CHANGELOG.md +++ b/embassy-net-esp-hosted/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add an `Interface` trait to allow using other interface transports. - Switch to `micropb` for protobuf. - Update protos to latest `esp-hosted-fg`. +- Add support for OTA firmware updates. ## 0.2.1 - 2025-08-26 -- cgit From 4e56caa71035ab236b11b31af618a7ae45792358 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Tue, 2 Dec 2025 12:30:53 +0100 Subject: chore: refactor api to allow other control commands --- embassy-net-esp-hosted/src/control.rs | 81 +++++++++++++++++++---------------- embassy-net-esp-hosted/src/ioctl.rs | 1 - embassy-net-esp-hosted/src/lib.rs | 3 +- 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/embassy-net-esp-hosted/src/control.rs b/embassy-net-esp-hosted/src/control.rs index d96a62daf..255ad7045 100644 --- a/embassy-net-esp-hosted/src/control.rs +++ b/embassy-net-esp-hosted/src/control.rs @@ -1,3 +1,4 @@ +use core::marker::PhantomData; use embassy_net_driver_channel as ch; use embassy_net_driver_channel::driver::{HardwareAddress, LinkState}; use heapless::String; @@ -24,9 +25,15 @@ pub struct Control<'a> { shared: &'a Shared, } -/// Handle for managing firmware update. -pub struct UpdateControl<'a, 'd> { - control: &'a mut Control<'d>, +/// Token required for doing an update +pub struct OtaToken { + _d: PhantomData<()>, +} + +impl OtaToken { + fn new() -> Self { + Self { _d: PhantomData } + } } /// WiFi mode. @@ -152,10 +159,43 @@ impl<'a> Control<'a> { } /// Initiate a firmware update. - pub async fn update(&mut self) -> Result, Error> { + /// + /// Returns a token needed for writing and finishing. + pub async fn ota_begin(&mut self) -> Result { let req = proto::CtrlMsg_Req_OTABegin {}; ioctl!(self, ReqOtaBegin, RespOtaBegin, req, resp); - Ok(UpdateControl { control: self }) + Ok(OtaToken::new()) + } + + /// Write slice of firmware to a device. + /// + /// Token is required as proof that ota_begin was called. + /// + /// The slice is split into chunks that can be sent across + /// the ioctl protocol to the wifi adapter. + pub async fn ota_write(&mut self, _token: &OtaToken, data: &[u8]) -> Result<(), Error> { + for chunk in data.chunks(256) { + let req = proto::CtrlMsg_Req_OTAWrite { + ota_data: heapless::Vec::from_slice(chunk).unwrap(), + }; + ioctl!(self, ReqOtaWrite, RespOtaWrite, req, resp); + } + Ok(()) + } + + /// End the OTA session. + /// + /// Token is required as proof that ota_begin was called. + /// + /// NOTE: Will reset the wifi adapter after 5 seconds. + pub async fn ota_end(&mut self, _token: OtaToken) -> Result<(), Error> { + let req = proto::CtrlMsg_Req_OTAEnd {}; + ioctl!(self, ReqOtaEnd, RespOtaEnd, req, resp); + // Ensures that run loop awaits reset + self.shared.ota_done(); + // Wait for re-init + self.init().await?; + Ok(()) } /// duration in seconds, clamped to [10, 3600] @@ -229,37 +269,6 @@ impl<'a> Control<'a> { } } -impl<'a, 'd> UpdateControl<'a, 'd> { - /// Write slice of firmware to a device. - /// - /// The slice is split into chunks that can be sent across - /// the ioctl protocol to the wifi adapter. - pub async fn write(&mut self, data: &[u8]) -> Result<(), Error> { - let this = &mut self.control; - for chunk in data.chunks(256) { - let req = proto::CtrlMsg_Req_OTAWrite { - ota_data: heapless::Vec::from_slice(chunk).unwrap(), - }; - ioctl!(this, ReqOtaWrite, RespOtaWrite, req, resp); - } - Ok(()) - } - - /// End the OTA session. - /// - /// NOTE: Will reset the wifi adapter after 5 seconds. - pub async fn finish(self) -> Result<(), Error> { - let this = self.control; - let req = proto::CtrlMsg_Req_OTAEnd {}; - ioctl!(this, ReqOtaEnd, RespOtaEnd, req, resp); - // Ensures that run loop awaits reset - this.shared.ota_done(); - // Wait for re-init - this.init().await?; - Ok(()) - } -} - // WHY IS THIS A STRING? WHYYYY fn parse_mac(mac: &str) -> Result<[u8; 6], Error> { fn nibble_from_hex(b: u8) -> Result { diff --git a/embassy-net-esp-hosted/src/ioctl.rs b/embassy-net-esp-hosted/src/ioctl.rs index 7f462d528..8b9d582be 100644 --- a/embassy-net-esp-hosted/src/ioctl.rs +++ b/embassy-net-esp-hosted/src/ioctl.rs @@ -106,7 +106,6 @@ impl Shared { let mut this = self.0.borrow_mut(); this.is_ota = true; this.is_init = false; - this.runner_waker.wake(); } // check if ota is in progress diff --git a/embassy-net-esp-hosted/src/lib.rs b/embassy-net-esp-hosted/src/lib.rs index 7236e73e8..f0cf04d04 100644 --- a/embassy-net-esp-hosted/src/lib.rs +++ b/embassy-net-esp-hosted/src/lib.rs @@ -238,8 +238,9 @@ where if self.shared.is_ota() { self.heartbeat_deadline = Instant::now() + HEARTBEAT_MAX_GAP; continue; + } else { + panic!("heartbeat from esp32 stopped") } - panic!("heartbeat from esp32 stopped") } } -- cgit From 4e69d44d184f4920f2f167a6ba9411cea0759dab Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Tue, 2 Dec 2025 12:53:42 +0100 Subject: chore: refactor to not use token --- embassy-net-esp-hosted/src/control.rs | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/embassy-net-esp-hosted/src/control.rs b/embassy-net-esp-hosted/src/control.rs index 255ad7045..227db0d09 100644 --- a/embassy-net-esp-hosted/src/control.rs +++ b/embassy-net-esp-hosted/src/control.rs @@ -1,4 +1,3 @@ -use core::marker::PhantomData; use embassy_net_driver_channel as ch; use embassy_net_driver_channel::driver::{HardwareAddress, LinkState}; use heapless::String; @@ -25,17 +24,6 @@ pub struct Control<'a> { shared: &'a Shared, } -/// Token required for doing an update -pub struct OtaToken { - _d: PhantomData<()>, -} - -impl OtaToken { - fn new() -> Self { - Self { _d: PhantomData } - } -} - /// WiFi mode. #[allow(unused)] #[derive(Copy, Clone, PartialEq, Eq, Debug)] @@ -159,21 +147,19 @@ impl<'a> Control<'a> { } /// Initiate a firmware update. - /// - /// Returns a token needed for writing and finishing. - pub async fn ota_begin(&mut self) -> Result { + pub async fn ota_begin(&mut self) -> Result<(), Error> { let req = proto::CtrlMsg_Req_OTABegin {}; ioctl!(self, ReqOtaBegin, RespOtaBegin, req, resp); - Ok(OtaToken::new()) + Ok(()) } /// Write slice of firmware to a device. /// - /// Token is required as proof that ota_begin was called. + /// [`ota_begin`] must be called first. /// /// The slice is split into chunks that can be sent across /// the ioctl protocol to the wifi adapter. - pub async fn ota_write(&mut self, _token: &OtaToken, data: &[u8]) -> Result<(), Error> { + pub async fn ota_write(&mut self, data: &[u8]) -> Result<(), Error> { for chunk in data.chunks(256) { let req = proto::CtrlMsg_Req_OTAWrite { ota_data: heapless::Vec::from_slice(chunk).unwrap(), @@ -185,10 +171,10 @@ impl<'a> Control<'a> { /// End the OTA session. /// - /// Token is required as proof that ota_begin was called. + /// [`ota_begin`] must be called first. /// /// NOTE: Will reset the wifi adapter after 5 seconds. - pub async fn ota_end(&mut self, _token: OtaToken) -> Result<(), Error> { + pub async fn ota_end(&mut self) -> Result<(), Error> { let req = proto::CtrlMsg_Req_OTAEnd {}; ioctl!(self, ReqOtaEnd, RespOtaEnd, req, resp); // Ensures that run loop awaits reset -- cgit From 220e8b5973efe0562f5d96cdd92b87badd313f00 Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Tue, 2 Dec 2025 13:54:42 +0100 Subject: chore: unify ota and init states --- embassy-net-esp-hosted/src/control.rs | 1 - embassy-net-esp-hosted/src/ioctl.rs | 29 ++++++++++++++++------------- embassy-net-esp-hosted/src/lib.rs | 7 +++---- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/embassy-net-esp-hosted/src/control.rs b/embassy-net-esp-hosted/src/control.rs index 227db0d09..eb79593f6 100644 --- a/embassy-net-esp-hosted/src/control.rs +++ b/embassy-net-esp-hosted/src/control.rs @@ -177,7 +177,6 @@ impl<'a> Control<'a> { pub async fn ota_end(&mut self) -> Result<(), Error> { let req = proto::CtrlMsg_Req_OTAEnd {}; ioctl!(self, ReqOtaEnd, RespOtaEnd, req, resp); - // Ensures that run loop awaits reset self.shared.ota_done(); // Wait for re-init self.init().await?; diff --git a/embassy-net-esp-hosted/src/ioctl.rs b/embassy-net-esp-hosted/src/ioctl.rs index 8b9d582be..de0f867e8 100644 --- a/embassy-net-esp-hosted/src/ioctl.rs +++ b/embassy-net-esp-hosted/src/ioctl.rs @@ -23,18 +23,23 @@ pub struct Shared(RefCell); struct SharedInner { ioctl: IoctlState, - is_init: bool, - is_ota: bool, + state: ControlState, control_waker: WakerRegistration, runner_waker: WakerRegistration, } +#[derive(Clone, Copy)] +pub(crate) enum ControlState { + Init, + Reboot, + Ready, +} + impl Shared { pub fn new() -> Self { Self(RefCell::new(SharedInner { ioctl: IoctlState::Done { resp_len: 0 }, - is_init: false, - is_ota: false, + state: ControlState::Init, control_waker: WakerRegistration::new(), runner_waker: WakerRegistration::new(), })) @@ -104,29 +109,27 @@ impl Shared { // ota pub fn ota_done(&self) { let mut this = self.0.borrow_mut(); - this.is_ota = true; - this.is_init = false; + this.state = ControlState::Reboot; } + // // // // // // // // // // // // // // // // // // // // + // // check if ota is in progress - pub fn is_ota(&self) -> bool { + pub(crate) fn state(&self) -> ControlState { let this = self.0.borrow(); - this.is_ota + this.state } - // // // // // // // // // // // // // // // // // // // // - pub fn init_done(&self) { let mut this = self.0.borrow_mut(); - this.is_init = true; - this.is_ota = false; + this.state = ControlState::Ready; this.control_waker.wake(); } pub fn init_wait(&self) -> impl Future + '_ { poll_fn(|cx| { let mut this = self.0.borrow_mut(); - if this.is_init { + if let ControlState::Ready = this.state { Poll::Ready(()) } else { this.control_waker.register(cx.waker()); diff --git a/embassy-net-esp-hosted/src/lib.rs b/embassy-net-esp-hosted/src/lib.rs index f0cf04d04..2c7377281 100644 --- a/embassy-net-esp-hosted/src/lib.rs +++ b/embassy-net-esp-hosted/src/lib.rs @@ -234,13 +234,12 @@ where tx_buf[..PayloadHeader::SIZE].fill(0); } Either4::Fourth(()) => { - // Extend the deadline if OTA - if self.shared.is_ota() { + // Extend the deadline if initializing + if let ioctl::ControlState::Reboot = self.shared.state() { self.heartbeat_deadline = Instant::now() + HEARTBEAT_MAX_GAP; continue; - } else { - panic!("heartbeat from esp32 stopped") } + panic!("heartbeat from esp32 stopped") } } -- cgit