Skip to main content

rs_matter/dm/clusters/
ota_req.rs

1/*
2 *
3 *    Copyright (c) 2026 Project CHIP Authors
4 *
5 *    Licensed under the Apache License, Version 2.0 (the "License");
6 *    you may not use this file except in compliance with the License.
7 *    You may obtain a copy of the License at
8 *
9 *        http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *    Unless required by applicable law or agreed to in writing, software
12 *    distributed under the License is distributed on an "AS IS" BASIS,
13 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *    See the License for the specific language governing permissions and
15 *    limitations under the License.
16 */
17
18//! The OTA Software Update Requestor cluster.
19//!
20//! The device hosts the cluster (server role) so an Administrator can configure
21//! its provider list and observe its update state. Rather than a ready-made
22//! update loop, this exposes the building blocks and leaves the policy (when to
23//! check, how to download and apply) to the application:
24//!
25//! - [`Providers`] keeps the persistent, fabric-scoped `DefaultOTAProviders` list
26//!   (at most one entry per fabric) plus a transient cache of providers learned
27//!   via `AnnounceOTAProvider`; [`Providers::wait_changed`] lets the app react.
28//! - [`Provider::query`] asks one provider whether a newer image is available.
29//! - [`OtaState`] holds the reported update state; [`OtaState::initiate_update`]
30//!   is an RAII session the app uses to report progress.
31//! - [`parse_bdx_url`] turns a `bdx://` image URI into a `(node, file-designator)`
32//!   pair for a BDX download via [`Exchange::download`](crate::bdx::BdxDownloadInitiator::download).
33
34use core::num::NonZeroU8;
35
36use crate::crypto::Crypto;
37use crate::dm::{
38    ArrayAttributeRead, ArrayAttributeWrite, AttrChangeNotifier, Cluster, Dataver, HandlerContext,
39    InvokeContext, LifecycleOp, ReadContext, WriteContext,
40};
41use crate::dm::{AttrId, EndptId, NodeId};
42use crate::error::{Error, ErrorCode};
43use crate::fabric::MAX_FABRICS;
44use crate::persist::{KvBlobStore, KvBlobStoreAccess, Persist, OTA_PROVIDERS_KEY};
45use crate::tlv::{FromTLV, Nullable, Octets, TLVArray, TLVBuilderParent, TLVElement, ToTLV};
46use crate::transport::exchange::Exchange;
47use crate::utils::cell::RefCell;
48use crate::utils::init::{init, Init};
49use crate::utils::storage::Vec;
50use crate::utils::sync::blocking::Mutex;
51use crate::utils::sync::Notification;
52use crate::with;
53use crate::Matter;
54
55pub use crate::dm::clusters::decl::ota_software_update_requestor::*;
56
57use crate::dm::clusters::decl::ota_software_update_provider::{
58    ApplyUpdateActionEnum, DownloadProtocolEnum, OtaSoftwareUpdateProviderClient,
59    QueryImageResponse,
60};
61use crate::dm::clusters::ota_prov::OtaApplyOutcome;
62
63/// The number of transient providers (learned via `AnnounceOTAProvider`) cached
64/// at once. These are one-shot hints, deduplicated by `(fabric, node)`; the
65/// oldest is evicted when the cache is full.
66const ANNOUNCED_PROVIDERS: usize = 4;
67
68/// One OTA Provider, scoped to the fabric that registered (or announced) it.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, FromTLV, ToTLV)]
70#[cfg_attr(feature = "defmt", derive(defmt::Format))]
71pub struct Provider {
72    /// The fabric this provider belongs to.
73    pub fab_idx: NonZeroU8,
74    /// The provider node's id.
75    pub node_id: NodeId,
76    /// The endpoint on the provider node hosting the OTA Provider cluster.
77    pub endpoint: EndptId,
78}
79
80impl Provider {
81    /// Ask this provider whether a software image newer than `current_version`
82    /// (defaulting to this device's `sw_ver` from `BasicInfoConfig` when `None`)
83    /// is available, advertising the given download `protocols`.
84    ///
85    /// The request's `VendorID`, `ProductID`, `SoftwareVersion` and
86    /// `HardwareVersion` are taken from the node's [`BasicInfoConfig`], and
87    /// `Location` from the configured Basic Information settings - the spec
88    /// requires each to equal the corresponding Basic Information cluster
89    /// attribute. `requestor_can_consent` declares whether this requestor can
90    /// obtain user consent on its own (via built-in UI); pass `true` only if so,
91    /// as it lets the provider delegate consent (see [`OtaImagesRegistry`]).
92    ///
93    /// Opens a CASE exchange to the provider, sends `QueryImage`, and invokes `f`
94    /// with the [`QueryImageResponse`] *before* the exchange is released - so `f`
95    /// reads `image_uri`/`software_version`/`update_token` straight off the RX
96    /// buffer without copying them onto the stack. `f`'s return value is returned.
97    ///
98    /// `query` does not interpret the response (it checks neither `status` nor the
99    /// returned version); that is left to `f`. On a `bdx://` `image_uri`, use
100    /// [`parse_bdx_url`] + [`Exchange::download`](crate::bdx::BdxDownloadInitiator::download) to fetch.
101    ///
102    /// [`BasicInfoConfig`]: crate::dm::clusters::basic_info::BasicInfoConfig
103    /// [`OtaImagesRegistry`]: crate::dm::clusters::ota_prov::OtaImagesRegistry
104    pub async fn query<C, F, R>(
105        &self,
106        matter: &Matter<'_>,
107        crypto: C,
108        protocols: &[DownloadProtocolEnum],
109        current_version: Option<u32>,
110        requestor_can_consent: bool,
111        f: F,
112    ) -> Result<R, Error>
113    where
114        C: Crypto,
115        F: FnOnce(&QueryImageResponse<'_>) -> Result<R, Error>,
116    {
117        let dev = matter.dev_det();
118        let version = current_version.unwrap_or(dev.sw_ver);
119
120        // `Location`, per spec, mirrors the Basic Information cluster Location
121        // attribute (a 2-char region code) when one is configured. Copy it out so
122        // the state lock is not held across the exchange.
123        let location = matter.with_state(|state| state.basic_info_settings.location.clone());
124        let location = location.as_deref();
125
126        let exchange = Exchange::initiate(matter, crypto, self.fab_idx, self.node_id).await?;
127
128        let handle = exchange
129            .ota_software_update_provider()
130            .query_image(self.endpoint, |b| {
131                let mut protos = b
132                    .vendor_id(dev.vid)?
133                    .product_id(dev.pid)?
134                    .software_version(version)?
135                    .protocols_supported()?;
136                for proto in protocols {
137                    protos = protos.push(proto)?;
138                }
139                protos
140                    .end()?
141                    .hardware_version(Some(dev.hw_ver))?
142                    .location(location)?
143                    .requestor_can_consent(Some(requestor_can_consent))?
144                    .metadata_for_provider(None)?
145                    .end()
146            })
147            .await?;
148
149        // Hand the response to `f` while it is still valid (borrows the RX buffer),
150        // then release the exchange.
151        let result = {
152            let response = handle.response()?;
153            f(&response)
154        };
155
156        handle.complete().await?;
157
158        result
159    }
160
161    /// Ask this provider how to apply the already-downloaded image identified by
162    /// `update_token` (the `UpdateToken` from the provider's
163    /// [`QueryImageResponse`]), which upgrades to `new_version`.
164    ///
165    /// Opens a CASE exchange and sends `ApplyUpdateRequest`. The returned
166    /// [`OtaApplyOutcome`] is the provider's decision: [`Proceed`] (apply, after
167    /// its delay), [`Await`] (wait the delay and call this again), or
168    /// [`Discontinue`] (discard the image).
169    ///
170    /// [`Proceed`]: OtaApplyOutcome::Proceed
171    /// [`Await`]: OtaApplyOutcome::Await
172    /// [`Discontinue`]: OtaApplyOutcome::Discontinue
173    pub async fn apply_update<C>(
174        &self,
175        matter: &Matter<'_>,
176        crypto: C,
177        update_token: &[u8],
178        new_version: u32,
179    ) -> Result<OtaApplyOutcome, Error>
180    where
181        C: Crypto,
182    {
183        let exchange = Exchange::initiate(matter, crypto, self.fab_idx, self.node_id).await?;
184
185        let handle = exchange
186            .ota_software_update_provider()
187            .apply_update_request(self.endpoint, |b| {
188                b.update_token(Octets(update_token))?
189                    .new_version(new_version)?
190                    .end()
191            })
192            .await?;
193
194        let outcome = {
195            let response = handle.response()?;
196            let delay_secs = response.delayed_action_time()?;
197
198            match response.action()? {
199                ApplyUpdateActionEnum::Proceed => OtaApplyOutcome::Proceed { delay_secs },
200                ApplyUpdateActionEnum::AwaitNextAction => OtaApplyOutcome::Await { delay_secs },
201                ApplyUpdateActionEnum::Discontinue => OtaApplyOutcome::Discontinue,
202            }
203        };
204
205        handle.complete().await?;
206
207        Ok(outcome)
208    }
209
210    /// Tell this provider that the image identified by `update_token` has been
211    /// applied, now running `software_version`. Opens a CASE exchange and sends
212    /// `NotifyUpdateApplied`; there is no response payload.
213    pub async fn notify_applied<C>(
214        &self,
215        matter: &Matter<'_>,
216        crypto: C,
217        update_token: &[u8],
218        software_version: u32,
219    ) -> Result<(), Error>
220    where
221        C: Crypto,
222    {
223        let exchange = Exchange::initiate(matter, crypto, self.fab_idx, self.node_id).await?;
224
225        exchange
226            .ota_software_update_provider()
227            .notify_update_applied(self.endpoint, |b| {
228                b.update_token(Octets(update_token))?
229                    .software_version(software_version)?
230                    .end()
231            })
232            .await
233    }
234}
235
236/// The OTA Requestor's provider registry: the persistent, fabric-scoped
237/// `DefaultOTAProviders` list (at most one entry per fabric) and a transient
238/// cache of providers learned via `AnnounceOTAProvider`.
239///
240/// Modeled on the Binding cluster's registry: the default list is persisted as a
241/// single TLV blob under [`OTA_PROVIDERS_KEY`]; announced providers are never
242/// persisted. [`wait_changed`](Self::wait_changed) signals the application when
243/// the set changes (an admin write or an announcement).
244pub struct Providers {
245    state: Mutex<RefCell<ProvidersState>>,
246    changed: Notification,
247}
248
249struct ProvidersState {
250    /// The persisted `DefaultOTAProviders` list - at most one entry per fabric.
251    default: Vec<Provider, MAX_FABRICS>,
252    /// Transient providers learned via `AnnounceOTAProvider` (not persisted).
253    announced: Vec<Provider, ANNOUNCED_PROVIDERS>,
254}
255
256impl ProvidersState {
257    fn init() -> impl Init<Self> {
258        init!(Self {
259            default <- Vec::init(),
260            announced <- Vec::init(),
261        })
262    }
263}
264
265impl Providers {
266    /// Create an empty registry. Prefer [`Self::init`] for a large `MAX_FABRICS`.
267    pub const fn new() -> Self {
268        Self {
269            state: Mutex::new(RefCell::new(ProvidersState {
270                default: Vec::new(),
271                announced: Vec::new(),
272            })),
273            changed: Notification::new(),
274        }
275    }
276
277    /// An in-place initializer for an empty registry.
278    pub fn init() -> impl Init<Self> {
279        init!(Self {
280            state <- Mutex::init(RefCell::init(ProvidersState::init())),
281            changed <- Notification::init(),
282        })
283    }
284
285    /// Re-hydrate the default provider list from `store`.
286    ///
287    /// Called on startup via the [`LifecycleOp::Startup`] lifecycle operation
288    /// delivered to the [`OtaRequestorHandler`] borrowing this registry.
289    pub fn load_persist<S: KvBlobStore>(&self, mut store: S, buf: &mut [u8]) -> Result<(), Error> {
290        let Some(data) = store.load(OTA_PROVIDERS_KEY, buf)? else {
291            self.state.lock(|cell| cell.borrow_mut().default.clear());
292            return Ok(());
293        };
294
295        let loaded = Vec::<Provider, MAX_FABRICS>::from_tlv(&TLVElement::new(data))?;
296        self.state.lock(|cell| cell.borrow_mut().default = loaded);
297
298        info!("Loaded OTA provider entries from storage");
299
300        Ok(())
301    }
302
303    /// Reset the registry - both the persisted default provider list and the
304    /// transient announced-provider cache - and remove the persisted blob from
305    /// `store` (under [`OTA_PROVIDERS_KEY`]).
306    ///
307    /// Called on factory reset via the [`LifecycleOp::FactoryReset`] lifecycle
308    /// operation delivered to the [`OtaRequestorHandler`] borrowing this registry.
309    pub fn reset_persist<S: KvBlobStore>(&self, mut store: S, buf: &mut [u8]) -> Result<(), Error> {
310        self.state.lock(|cell| {
311            let mut state = cell.borrow_mut();
312
313            state.default.clear();
314            state.announced.clear();
315        });
316
317        store.remove(OTA_PROVIDERS_KEY, buf)
318    }
319
320    /// Drop the default and announced providers belonging to `fab_idx`,
321    /// returning whether anything was removed. Signals
322    /// [`wait_changed`](Self::wait_changed) when so.
323    ///
324    /// Called on fabric removal via the [`LifecycleOp::FabricRemoval`]
325    /// lifecycle operation delivered to the [`OtaRequestorHandler`] borrowing
326    /// this registry. Idempotent.
327    pub fn remove_for_fabric(&self, fab_idx: NonZeroU8) -> bool {
328        let removed = self.state.lock(|cell| {
329            let mut state = cell.borrow_mut();
330            let before = state.default.len() + state.announced.len();
331
332            state.default.retain(|provider| provider.fab_idx != fab_idx);
333            state
334                .announced
335                .retain(|provider| provider.fab_idx != fab_idx);
336
337            state.default.len() + state.announced.len() < before
338        });
339
340        if removed {
341            self.changed.notify();
342        }
343
344        removed
345    }
346
347    /// Persist the default provider list to `ctx.kv()`.
348    fn store_persist<C: HandlerContext>(&self, ctx: &C) -> Result<(), Error> {
349        let mut persist = Persist::new(ctx.kv());
350
351        self.state.lock(|cell| {
352            let state = cell.borrow();
353            persist.store_tlv(OTA_PROVIDERS_KEY, &state.default)
354        })?;
355
356        persist.run()
357    }
358
359    /// The number of configured default providers (across all fabrics).
360    pub fn len(&self) -> usize {
361        self.state.lock(|cell| cell.borrow().default.len())
362    }
363
364    /// Whether there are no configured default providers.
365    pub fn is_empty(&self) -> bool {
366        self.len() == 0
367    }
368
369    /// The `index`-th default provider, cloned out so the registry lock is not
370    /// held across the caller's subsequent (likely `async`) work.
371    pub fn get(&self, index: usize) -> Option<Provider> {
372        self.state
373            .lock(|cell| cell.borrow().default.get(index).copied())
374    }
375
376    /// The number of cached announced providers.
377    pub fn announced_len(&self) -> usize {
378        self.state.lock(|cell| cell.borrow().announced.len())
379    }
380
381    /// The `index`-th announced provider, cloned out.
382    pub fn announced(&self, index: usize) -> Option<Provider> {
383        self.state
384            .lock(|cell| cell.borrow().announced.get(index).copied())
385    }
386
387    /// Drop all cached announced providers (e.g. once the app has queried them).
388    pub fn clear_announced(&self) {
389        self.state.lock(|cell| cell.borrow_mut().announced.clear());
390    }
391
392    /// Atomically remove and return all cached announced providers.
393    ///
394    /// Prefer this to iterating [`announced`](Self::announced) and then calling
395    /// [`clear_announced`](Self::clear_announced) in an update loop: providers
396    /// learned via `AnnounceOTAProvider` *while the loop is busy* processing this
397    /// batch land in a fresh `announced` set (and re-arm [`wait_changed`](Self::wait_changed))
398    /// instead of being discarded unprocessed by a trailing clear.
399    pub fn take_announced(&self) -> Vec<Provider, ANNOUNCED_PROVIDERS> {
400        self.state.lock(|cell| {
401            let mut state = cell.borrow_mut();
402            let taken = state.announced.clone();
403            state.announced.clear();
404            taken
405        })
406    }
407
408    /// Wait until the provider set changes (a `DefaultOTAProviders` write or an
409    /// `AnnounceOTAProvider` command).
410    pub async fn wait_changed(&self) {
411        self.changed.wait().await;
412    }
413
414    /// Replace the (single) default provider for `fab_idx` with `provider`, or
415    /// clear it when `None`.
416    fn replace_default<C: WriteContext>(
417        &self,
418        ctx: &C,
419        fab_idx: NonZeroU8,
420        provider: Option<Provider>,
421    ) -> Result<(), Error> {
422        self.state.lock(|cell| {
423            let mut state = cell.borrow_mut();
424            state.default.retain(|p| p.fab_idx != fab_idx);
425            if let Some(provider) = provider {
426                state
427                    .default
428                    .push(provider)
429                    .map_err(|_| ErrorCode::ResourceExhausted)?;
430            }
431            Ok::<_, Error>(())
432        })?;
433
434        self.changed.notify();
435
436        self.store_persist(ctx)
437    }
438
439    /// Add a default provider for `fab_idx`, failing with `CONSTRAINT_ERROR` if it
440    /// already has one (at most one entry per fabric).
441    fn add_default<C: WriteContext>(
442        &self,
443        ctx: &C,
444        fab_idx: NonZeroU8,
445        provider: Provider,
446    ) -> Result<(), Error> {
447        self.state.lock(|cell| {
448            let mut state = cell.borrow_mut();
449            if state.default.iter().any(|p| p.fab_idx == fab_idx) {
450                return Err(ErrorCode::ConstraintError.into());
451            }
452            state
453                .default
454                .push(provider)
455                .map_err(|_| ErrorCode::ResourceExhausted)?;
456            Ok::<_, Error>(())
457        })?;
458
459        self.changed.notify();
460
461        self.store_persist(ctx)
462    }
463
464    /// Cache a transient provider learned via `AnnounceOTAProvider` and wake any
465    /// waiter. Deduplicated by `(fabric, node)`; the oldest is evicted if full.
466    fn add_announced(&self, provider: Provider) {
467        self.state.lock(|cell| {
468            let mut state = cell.borrow_mut();
469            state
470                .announced
471                .retain(|p| !(p.fab_idx == provider.fab_idx && p.node_id == provider.node_id));
472            if state.announced.is_full() {
473                state.announced.remove(0);
474            }
475            // Cannot fail: we just made room.
476            let _ = state.announced.push(provider);
477        });
478
479        self.changed.notify();
480    }
481
482    /// Render the (fabric-filtered) default list into the attribute builder.
483    fn render<P: TLVBuilderParent>(
484        &self,
485        fab_filter: Option<NonZeroU8>,
486        builder: ArrayAttributeRead<ProviderLocationArrayBuilder<P>, ProviderLocationBuilder<P>>,
487    ) -> Result<P, Error> {
488        self.state.lock(|cell| {
489            let state = cell.borrow();
490            let mut iter = state
491                .default
492                .iter()
493                .filter(|p| fab_filter.is_none_or(|f| p.fab_idx == f));
494
495            match builder {
496                ArrayAttributeRead::ReadAll(mut array) => {
497                    for p in iter {
498                        array = array
499                            .push()?
500                            .provider_node_id(p.node_id)?
501                            .endpoint(p.endpoint)?
502                            .fabric_index(Some(p.fab_idx.get()))?
503                            .end()?;
504                    }
505                    array.end()
506                }
507                ArrayAttributeRead::ReadOne(index, item) => {
508                    let Some(p) = iter.nth(index as usize) else {
509                        return Err(ErrorCode::ConstraintError.into());
510                    };
511                    item.provider_node_id(p.node_id)?
512                        .endpoint(p.endpoint)?
513                        .fabric_index(Some(p.fab_idx.get()))?
514                        .end()
515                }
516                ArrayAttributeRead::ReadNone(array) => array.end(),
517            }
518        })
519    }
520}
521
522impl Default for Providers {
523    fn default() -> Self {
524        Self::new()
525    }
526}
527
528/// The OTA Requestor's reported update state: the `UpdateState`,
529/// `UpdateStateProgress` and `UpdatePossible` attributes.
530///
531/// Held separately from [`Providers`] because it is transient runtime state, not
532/// configuration. The application reports progress through an [`OtaUpdate`]
533/// session obtained from [`initiate_update`](Self::initiate_update).
534///
535/// Because progress is reported from the application's own update loop (outside
536/// any cluster-handler `ctx`), the reporting calls take the data model's
537/// [`AttrChangeNotifier`] (typically the `InteractionModel`) so each change bumps the
538/// cluster's data version and wakes subscribers. It is passed in rather than
539/// stored because the `InteractionModel` is constructed *after* this state (it borrows
540/// it), so there is never a moment at which it could be stored here.
541pub struct OtaState {
542    endpoint_id: EndptId,
543    reported: Mutex<RefCell<Reported>>,
544}
545
546struct Reported {
547    update_state: UpdateStateEnum,
548    progress: Option<u8>,
549    update_possible: bool,
550}
551
552impl OtaState {
553    /// Create idle, update-possible state for the endpoint hosting the OTA
554    /// Requestor cluster (the one whose [`OtaRequestorHandler`] reads this state),
555    /// so change notifications target the right cluster instance.
556    pub const fn new(endpoint_id: EndptId) -> Self {
557        Self {
558            endpoint_id,
559            reported: Mutex::new(RefCell::new(Reported {
560                update_state: UpdateStateEnum::Idle,
561                progress: None,
562                update_possible: true,
563            })),
564        }
565    }
566
567    /// Set the `UpdatePossible` attribute (e.g. `false` when the battery is too
568    /// low to apply an update), notifying `notifier` of the change.
569    pub fn set_update_possible(&self, notifier: &dyn AttrChangeNotifier, possible: bool) {
570        self.reported
571            .lock(|cell| cell.borrow_mut().update_possible = possible);
572
573        self.notify(notifier, AttributeId::UpdatePossible as _);
574    }
575
576    fn update_state(&self) -> UpdateStateEnum {
577        self.reported.lock(|cell| cell.borrow().update_state)
578    }
579
580    fn progress(&self) -> Option<u8> {
581        self.reported.lock(|cell| cell.borrow().progress)
582    }
583
584    fn update_possible(&self) -> bool {
585        self.reported.lock(|cell| cell.borrow().update_possible)
586    }
587
588    /// Update the reported `UpdateState`/`UpdateStateProgress` and notify.
589    fn report(
590        &self,
591        notifier: &dyn AttrChangeNotifier,
592        state: UpdateStateEnum,
593        progress: Option<u8>,
594    ) {
595        self.reported.lock(|cell| {
596            let mut reported = cell.borrow_mut();
597            reported.update_state = state;
598            reported.progress = progress;
599        });
600
601        // Both `UpdateState` and `UpdateStateProgress` changed; a single
602        // cluster-level notification bumps the data version once and re-reports
603        // any subscriber interested in either attribute.
604        notifier.notify_cluster_changed(self.endpoint_id, FULL_CLUSTER.id);
605    }
606
607    /// Notify the data model that `attr_id` of this cluster instance changed.
608    fn notify(&self, notifier: &dyn AttrChangeNotifier, attr_id: AttrId) {
609        notifier.notify_attr_changed(self.endpoint_id, FULL_CLUSTER.id, attr_id);
610    }
611
612    /// Begin an update session, reporting changes through `notifier`. The returned
613    /// [`OtaUpdate`] reports progress; on [`complete`](OtaUpdate::complete) - or if
614    /// dropped without it - the reported state returns to `Idle` (`UpdateStateEnum`
615    /// has no dedicated failure value).
616    pub fn initiate_update<'a>(&'a self, notifier: &'a dyn AttrChangeNotifier) -> OtaUpdate<'a> {
617        OtaUpdate {
618            state: self,
619            notifier,
620            done: false,
621        }
622    }
623}
624
625/// An in-progress update session (RAII). Report progress with
626/// [`querying`](Self::querying) / [`downloading`](Self::downloading) /
627/// [`applying`](Self::applying) / [`report`](Self::report); call
628/// [`complete`](Self::complete) when done. Dropping it without `complete` reverts
629/// the reported state to `Idle`, so an aborted update never leaves the cluster
630/// stuck mid-transfer.
631pub struct OtaUpdate<'a> {
632    state: &'a OtaState,
633    notifier: &'a dyn AttrChangeNotifier,
634    done: bool,
635}
636
637impl OtaUpdate<'_> {
638    /// Report `Querying`.
639    pub fn querying(&self) {
640        self.state
641            .report(self.notifier, UpdateStateEnum::Querying, None);
642    }
643
644    /// Report `Downloading` at the given percent (`None` if unknown).
645    pub fn downloading(&self, percent: Option<u8>) {
646        self.state
647            .report(self.notifier, UpdateStateEnum::Downloading, percent);
648    }
649
650    /// Report `Applying`.
651    pub fn applying(&self) {
652        self.state
653            .report(self.notifier, UpdateStateEnum::Applying, None);
654    }
655
656    /// Report an arbitrary `state` and `progress`.
657    pub fn report(&self, state: UpdateStateEnum, progress: Option<u8>) {
658        self.state.report(self.notifier, state, progress);
659    }
660
661    /// Finish the session, returning the reported state to `Idle`.
662    pub fn complete(mut self) {
663        self.state
664            .report(self.notifier, UpdateStateEnum::Idle, None);
665        self.done = true;
666    }
667}
668
669impl Drop for OtaUpdate<'_> {
670    fn drop(&mut self) {
671        if !self.done {
672            // Abandoned mid-update: revert to Idle.
673            self.state
674                .report(self.notifier, UpdateStateEnum::Idle, None);
675        }
676    }
677}
678
679/// The server-side handler for the OTA Software Update Requestor cluster.
680pub struct OtaRequestorHandler<'a> {
681    dataver: Dataver,
682    providers: &'a Providers,
683    state: &'a OtaState,
684}
685
686impl<'a> OtaRequestorHandler<'a> {
687    /// Create a handler backed by the shared [`Providers`] registry and
688    /// [`OtaState`].
689    pub const fn new(dataver: Dataver, providers: &'a Providers, state: &'a OtaState) -> Self {
690        Self {
691            dataver,
692            providers,
693            state,
694        }
695    }
696
697    /// Adapt this handler to the generic `rs-matter` `Handler` trait.
698    pub const fn adapt(self) -> HandlerAdaptor<Self> {
699        HandlerAdaptor(self)
700    }
701}
702
703impl ClusterHandler for OtaRequestorHandler<'_> {
704    const CLUSTER: Cluster<'static> = FULL_CLUSTER.with_attrs(with!(required));
705
706    fn dataver(&self) -> u32 {
707        self.dataver.get()
708    }
709
710    fn dataver_changed(&self) {
711        self.dataver.changed();
712    }
713
714    fn lifecycle(&self, ctx: impl HandlerContext, op: LifecycleOp) -> Result<(), Error> {
715        match op {
716            LifecycleOp::Startup => ctx
717                .kv()
718                .access(|store, buf| self.providers.load_persist(store, buf)),
719            LifecycleOp::FactoryReset => ctx
720                .kv()
721                .access(|store, buf| self.providers.reset_persist(store, buf)),
722            LifecycleOp::FabricRemoval { fab_idx } => {
723                if self.providers.remove_for_fabric(fab_idx) {
724                    self.providers.store_persist(&ctx)?;
725                }
726
727                Ok(())
728            }
729        }
730    }
731
732    fn default_ota_providers<P: TLVBuilderParent>(
733        &self,
734        ctx: impl ReadContext,
735        builder: ArrayAttributeRead<ProviderLocationArrayBuilder<P>, ProviderLocationBuilder<P>>,
736    ) -> Result<P, Error> {
737        let attr = ctx.attr();
738        let fab_filter = if attr.fab_filter {
739            Some(NonZeroU8::new(attr.fab_idx).ok_or(ErrorCode::UnsupportedAccess)?)
740        } else {
741            None
742        };
743
744        self.providers.render(fab_filter, builder)
745    }
746
747    fn update_possible(&self, _ctx: impl ReadContext) -> Result<bool, Error> {
748        Ok(self.state.update_possible())
749    }
750
751    fn update_state(&self, _ctx: impl ReadContext) -> Result<UpdateStateEnum, Error> {
752        Ok(self.state.update_state())
753    }
754
755    fn update_state_progress(&self, _ctx: impl ReadContext) -> Result<Nullable<u8>, Error> {
756        Ok(self
757            .state
758            .progress()
759            .map(Nullable::some)
760            .unwrap_or_else(Nullable::none))
761    }
762
763    fn set_default_ota_providers(
764        &self,
765        ctx: impl WriteContext,
766        value: ArrayAttributeWrite<TLVArray<'_, ProviderLocation<'_>>, ProviderLocation<'_>>,
767    ) -> Result<(), Error> {
768        // Fabric-scoped writes require a valid accessing fabric.
769        let fab_idx = NonZeroU8::new(ctx.attr().fab_idx).ok_or(ErrorCode::UnsupportedAccess)?;
770
771        let to_provider = |loc: &ProviderLocation<'_>| -> Result<Provider, Error> {
772            Ok(Provider {
773                fab_idx,
774                node_id: loc.provider_node_id()?,
775                endpoint: loc.endpoint()?,
776            })
777        };
778
779        match value {
780            // At most one entry per fabric: a replacement list may carry zero or
781            // one entry; more than one is a `CONSTRAINT_ERROR`.
782            ArrayAttributeWrite::Replace(list) => {
783                let mut iter = list.iter();
784                let first = iter.next().transpose()?;
785                if iter.next().is_some() {
786                    return Err(ErrorCode::ConstraintError.into());
787                }
788
789                // Parse before mutating, so a malformed entry leaves the existing
790                // default intact.
791                let parsed = first.map(|loc| to_provider(&loc)).transpose()?;
792                self.providers.replace_default(&ctx, fab_idx, parsed)?;
793            }
794            // Adding a second entry for a fabric would exceed the one-per-fabric
795            // limit; reject it.
796            ArrayAttributeWrite::Add(loc) => {
797                self.providers
798                    .add_default(&ctx, fab_idx, to_provider(&loc)?)?;
799            }
800            // Per-index update/remove on a fabric-scoped list are converted to
801            // `InvalidAction` by the framework; reject defensively.
802            ArrayAttributeWrite::Update(_, _) | ArrayAttributeWrite::Remove(_) => {
803                return Err(ErrorCode::InvalidAction.into());
804            }
805        }
806
807        // Notify subscribers of the changed attribute (also bumps the dataver).
808        ctx.notify_changed();
809
810        Ok(())
811    }
812
813    fn handle_announce_ota_provider(
814        &self,
815        ctx: impl InvokeContext,
816        request: AnnounceOTAProviderRequest<'_>,
817    ) -> Result<(), Error> {
818        let fab_idx = NonZeroU8::new(ctx.cmd().fab_idx).ok_or(ErrorCode::UnsupportedAccess)?;
819
820        let provider = Provider {
821            fab_idx,
822            node_id: request.provider_node_id()?,
823            endpoint: request.endpoint()?,
824        };
825
826        // Per spec, an announced provider is a transient hint and SHALL NOT be
827        // added to `DefaultOTAProviders`; cache it separately and wake the app.
828        self.providers.add_announced(provider);
829
830        Ok(())
831    }
832}
833
834/// Parse a `bdx://<node-id>/<file-designator>` image URI into its `(node id, file
835/// designator)` pair. The node id is hex-encoded, as minted by an OTA Provider's
836/// `QueryImage` response.
837pub fn parse_bdx_url(url: &str) -> Result<(NodeId, &str), Error> {
838    let rest = url.strip_prefix("bdx://").ok_or(ErrorCode::InvalidData)?;
839    let (node, fd) = rest.split_once('/').ok_or(ErrorCode::InvalidData)?;
840    if fd.is_empty() {
841        // A BDX transfer needs a non-empty file designator to identify the file.
842        return Err(ErrorCode::InvalidData.into());
843    }
844    let node_id = u64::from_str_radix(node, 16).map_err(|_| ErrorCode::InvalidData)?;
845
846    Ok((node_id, fd))
847}
848
849#[cfg(test)]
850mod tests {
851    use super::*;
852
853    #[test]
854    fn parse_bdx_url_extracts_node_and_fd() {
855        let (node, fd) = parse_bdx_url("bdx://00112233AABBCCDD/my-firmware.ota").unwrap();
856        assert_eq!(node, 0x0011_2233_AABB_CCDD);
857        assert_eq!(fd, "my-firmware.ota");
858
859        assert!(parse_bdx_url("https://example.com/x").is_err());
860        assert!(parse_bdx_url("bdx://nodeid-no-slash").is_err());
861        assert!(parse_bdx_url("bdx://zzzz/fd").is_err());
862    }
863
864    #[test]
865    fn announced_dedup_evict_and_clear() {
866        let providers = Providers::new();
867        let provider = |node| Provider {
868            fab_idx: NonZeroU8::new(1).unwrap(),
869            node_id: node,
870            endpoint: 0,
871        };
872
873        // Deduplicated by (fabric, node).
874        providers.add_announced(provider(0xaa));
875        providers.add_announced(provider(0xaa));
876        assert_eq!(providers.announced_len(), 1);
877
878        // Capacity-bounded: the oldest is evicted.
879        for n in 0..(ANNOUNCED_PROVIDERS as u64 + 2) {
880            providers.add_announced(provider(0x100 + n));
881        }
882        assert_eq!(providers.announced_len(), ANNOUNCED_PROVIDERS);
883
884        providers.clear_announced();
885        assert_eq!(providers.announced_len(), 0);
886    }
887}