use core::fmt::Write as _;
use core::num::NonZeroU8;
use crate::bdx::{BdxHandler, BdxResponder, BdxStatus};
use crate::dm::{Cluster, Dataver, InvokeContext};
use crate::error::{Error, ErrorCode};
use crate::tlv::{Octets, TLVBuilderParent};
use crate::transport::exchange::MAX_EXCHANGE_RX_BUF_SIZE;
use crate::utils::storage::pooled::Buffers;
use crate::with;
pub use crate::bdx::BdxBuffer;
pub use crate::dm::clusters::decl::ota_software_update_provider::*;
#[cfg(feature = "ota-dcl")]
pub mod dcl;
const MAX_FILE_DESIGNATOR: usize = 128;
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct OtaImageMeta<'a> {
pub version: u32,
pub file_designator: &'a str,
pub update_token: &'a [u8],
pub size: Option<u64>,
pub user_consent_needed: bool,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum OtaQueryOutcome<'a> {
Available(OtaImageMeta<'a>),
Busy {
delay_secs: u32,
},
NotAvailable,
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum OtaApplyOutcome {
Proceed {
delay_secs: u32,
},
Await {
delay_secs: u32,
},
Discontinue,
}
pub trait OtaImagesRegistry {
async fn query<'b>(
&self,
vendor_id: u16,
product_id: u16,
current_version: u32,
requestor_can_consent: bool,
designator_buf: &'b mut [u8],
) -> OtaQueryOutcome<'b>;
async fn apply(&self, _update_token: &[u8], _new_version: u32) -> OtaApplyOutcome {
OtaApplyOutcome::Proceed { delay_secs: 0 }
}
}
impl<T> OtaImagesRegistry for &T
where
T: OtaImagesRegistry,
{
async fn query<'b>(
&self,
vendor_id: u16,
product_id: u16,
current_version: u32,
requestor_can_consent: bool,
designator_buf: &'b mut [u8],
) -> OtaQueryOutcome<'b> {
T::query(
self,
vendor_id,
product_id,
current_version,
requestor_can_consent,
designator_buf,
)
.await
}
async fn apply(&self, update_token: &[u8], new_version: u32) -> OtaApplyOutcome {
T::apply(self, update_token, new_version).await
}
}
pub trait OtaImages {
async fn size(&self, file_designator: &[u8]) -> Option<u64>;
async fn read(
&self,
file_designator: &[u8],
offset: u64,
buf: &mut [u8],
) -> Result<usize, Error>;
}
impl<T> OtaImages for &T
where
T: OtaImages,
{
async fn size(&self, file_designator: &[u8]) -> Option<u64> {
T::size(self, file_designator).await
}
async fn read(
&self,
file_designator: &[u8],
offset: u64,
buf: &mut [u8],
) -> Result<usize, Error> {
T::read(self, file_designator, offset, buf).await
}
}
const UPDATE_TOKEN_LEN: core::ops::RangeInclusive<usize> = 8..=32;
pub struct OtaProviderHandler<I> {
dataver: Dataver,
images: I,
}
impl<I> OtaProviderHandler<I> {
pub const fn new(dataver: Dataver, images: I) -> Self {
Self { dataver, images }
}
pub const fn adapt(self) -> HandlerAsyncAdaptor<Self> {
HandlerAsyncAdaptor(self)
}
}
impl<I: OtaImagesRegistry> ClusterAsyncHandler for OtaProviderHandler<I> {
const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required));
fn dataver(&self) -> u32 {
self.dataver.get()
}
fn dataver_changed(&self) {
self.dataver.changed();
}
async fn handle_query_image<P: TLVBuilderParent>(
&self,
ctx: impl InvokeContext,
request: QueryImageRequest<'_>,
response: QueryImageResponseBuilder<P>,
) -> Result<P, Error> {
let vendor_id = request.vendor_id()?;
let product_id = request.product_id()?;
let current_version = request.software_version()?;
let requestor_can_consent = request.requestor_can_consent()?.unwrap_or(false);
let mut designator_buf = [0u8; MAX_FILE_DESIGNATOR];
let image = match self
.images
.query(
vendor_id,
product_id,
current_version,
requestor_can_consent,
&mut designator_buf,
)
.await
{
OtaQueryOutcome::Available(image) => image,
OtaQueryOutcome::Busy { delay_secs } => {
return response
.status(StatusEnum::Busy)?
.delayed_action_time(Some(delay_secs))?
.image_uri(None)?
.software_version(None)?
.software_version_string(None)?
.update_token(None)?
.user_consent_needed(None)?
.metadata_for_requestor(None)?
.end();
}
OtaQueryOutcome::NotAvailable => {
return response
.status(StatusEnum::NotAvailable)?
.delayed_action_time(None)?
.image_uri(None)?
.software_version(None)?
.software_version_string(None)?
.update_token(None)?
.user_consent_needed(None)?
.metadata_for_requestor(None)?
.end();
}
};
let fab_idx = NonZeroU8::new(ctx.cmd().fab_idx).ok_or(ErrorCode::Invalid)?;
let node_id = ctx
.exchange()
.with_state(|state| Ok(state.fabrics.fabric(fab_idx)?.node_id()))?;
let mut uri = heapless::String::<200>::new();
write!(uri, "bdx://{:016X}/{}", node_id, image.file_designator)
.map_err(|_| ErrorCode::NoSpace)?;
let mut version_str = heapless::String::<16>::new();
write!(version_str, "{}", image.version).map_err(|_| ErrorCode::NoSpace)?;
if !UPDATE_TOKEN_LEN.contains(&image.update_token.len()) {
return Err(ErrorCode::ConstraintError.into());
}
response
.status(StatusEnum::UpdateAvailable)?
.delayed_action_time(None)?
.image_uri(Some(uri.as_str()))?
.software_version(Some(image.version))?
.software_version_string(Some(version_str.as_str()))?
.update_token(Some(Octets(image.update_token)))?
.user_consent_needed(Some(image.user_consent_needed))?
.metadata_for_requestor(None)?
.end()
}
async fn handle_apply_update_request<P: TLVBuilderParent>(
&self,
_ctx: impl InvokeContext,
request: ApplyUpdateRequestRequest<'_>,
response: ApplyUpdateResponseBuilder<P>,
) -> Result<P, Error> {
let update_token = request.update_token()?;
let new_version = request.new_version()?;
let (action, delay) = match self.images.apply(update_token.0, new_version).await {
OtaApplyOutcome::Proceed { delay_secs } => (ApplyUpdateActionEnum::Proceed, delay_secs),
OtaApplyOutcome::Await { delay_secs } => {
(ApplyUpdateActionEnum::AwaitNextAction, delay_secs)
}
OtaApplyOutcome::Discontinue => (ApplyUpdateActionEnum::Discontinue, 0),
};
response.action(action)?.delayed_action_time(delay)?.end()
}
async fn handle_notify_update_applied(
&self,
_ctx: impl InvokeContext,
_request: NotifyUpdateAppliedRequest<'_>,
) -> Result<(), Error> {
Ok(())
}
}
pub struct OtaBdxHandler<B, I> {
buffers: B,
images: I,
}
impl<B, I> OtaBdxHandler<B, I> {
pub const fn new(buffers: B, images: I) -> Self {
Self { buffers, images }
}
}
impl<B, I: OtaImages> OtaBdxHandler<B, I> {
async fn fill(&self, fd: &[u8], offset: u64, buf: &mut [u8]) -> Result<usize, Error> {
let mut filled = 0;
while filled < buf.len() {
let read_offset = offset
.checked_add(filled as u64)
.ok_or(ErrorCode::Invalid)?;
let n = self
.images
.read(fd, read_offset, &mut buf[filled..])
.await?;
if n == 0 {
break;
}
if n > buf.len() - filled {
return Err(ErrorCode::Invalid.into());
}
filled += n;
}
Ok(filled)
}
}
impl<B, I> BdxHandler for OtaBdxHandler<B, I>
where
B: Buffers<BdxBuffer>,
I: OtaImages,
{
async fn handles(&self, responder: &BdxResponder<'_>) -> bool {
matches!(responder, BdxResponder::Download(_))
&& self.images.size(responder.fd()).await.is_some()
}
async fn handle(&self, responder: BdxResponder<'_>) -> Result<(), Error> {
let responder = match responder {
BdxResponder::Download(responder) => responder,
other => return other.reject(BdxStatus::FileDesignatorUnknown).await,
};
let mut fd = heapless::Vec::<u8, MAX_FILE_DESIGNATOR>::new();
if fd.extend_from_slice(responder.fd()).is_err() {
return responder.reject(BdxStatus::FileDesignatorUnknown).await;
}
let Some(size) = self.images.size(&fd).await else {
return responder.reject(BdxStatus::FileDesignatorUnknown).await;
};
let start_offset = responder.start_offset();
if start_offset > size {
return responder.reject(BdxStatus::StartOffsetNotSupported).await;
}
let remaining = size - start_offset;
let Some(mut buf) = self.buffers.get().await else {
return responder.reject(BdxStatus::ResponderBusy).await;
};
unwrap!(buf.resize_default(MAX_EXCHANGE_RX_BUF_SIZE));
let mut writer = responder.reply(buf.as_mut_slice(), Some(remaining)).await?;
let mut offset = start_offset;
loop {
let n = self.fill(&fd, offset, writer.block_buf()).await?;
if n == 0 {
break;
}
writer.commit(n).await?;
offset += n as u64;
}
writer.finish().await
}
}