1use core::future::{ready, Future};
38use core::num::NonZeroU8;
39
40use crate::dm::{
41 ArrayAttributeRead, AttrId, Cluster, ClusterId, Dataver, EndptId, HandlerContext,
42 InvokeContext, LifecycleOp, ReadContext, SceneId,
43};
44use crate::error::{Error, ErrorCode};
45use crate::persist::{KvBlobStore, KvBlobStoreAccess, Persist};
46use crate::tlv::{
47 FromTLV, Nullable, OptionalBuilder, TLVArray, TLVBuilder, TLVBuilderParent, TLVElement,
48 TLVSequence, TLVTag, TLVWrite, TLVWriteParent, ToTLV, TLV,
49};
50use crate::utils::cell::RefCell;
51use crate::utils::init::{init, Init};
52use crate::utils::storage::{Vec, WriteBuf};
53use crate::utils::sync::blocking::Mutex;
54
55pub use crate::dm::clusters::decl::scenes_management::*;
56pub use crate::persist::SCENES_KEY;
57
58const SC_NOT_FOUND: u8 = 0x8B;
61const SC_INSUFFICIENT_SPACE: u8 = 0x89;
62const SC_INVALID_COMMAND: u8 = 0x85;
63const SC_CONSTRAINT_ERROR: u8 = 0x87;
64
65const RESERVED_SCENE_ID: SceneId = 0xFF;
67
68const GLOBAL_SCENE_ID: SceneId = 0;
71
72const MAX_TRANSITION_TIME_MS: u32 = 60_000_000;
75
76pub const MAX_EXT_FIELDS_LEN: usize = 128;
82
83pub trait SceneClusterHandler {
93 const CLUSTER_ID: ClusterId;
95
96 fn endpoint_id(&self) -> EndptId;
99
100 fn is_scenable_attribute(_attribute_id: AttrId) -> bool {
104 false
105 }
106
107 fn capture<P: TLVBuilderParent>(
112 &self,
113 avp_array: AttributeValuePairStructArrayBuilder<P>,
114 ) -> Result<AttributeValuePairStructArrayBuilder<P>, Error>;
115
116 fn apply<C: HandlerContext>(
132 &self,
133 ctx: &C,
134 avp_list: &TLVArray<'_, AttributeValuePairStruct<'_>>,
135 transition_time_ms: u32,
136 ) -> impl Future<Output = Result<(), Error>>;
137}
138
139impl<T: SceneClusterHandler + ?Sized> SceneClusterHandler for &T {
143 const CLUSTER_ID: ClusterId = T::CLUSTER_ID;
144
145 fn endpoint_id(&self) -> EndptId {
146 T::endpoint_id(*self)
147 }
148
149 fn is_scenable_attribute(attribute_id: AttrId) -> bool {
150 T::is_scenable_attribute(attribute_id)
151 }
152
153 fn capture<P: TLVBuilderParent>(
154 &self,
155 avp_array: AttributeValuePairStructArrayBuilder<P>,
156 ) -> Result<AttributeValuePairStructArrayBuilder<P>, Error> {
157 T::capture(*self, avp_array)
158 }
159
160 async fn apply<C: HandlerContext>(
161 &self,
162 ctx: &C,
163 avp_list: &TLVArray<'_, AttributeValuePairStruct<'_>>,
164 transition_time_ms: u32,
165 ) -> Result<(), Error> {
166 T::apply(*self, ctx, avp_list, transition_time_ms).await
167 }
168}
169
170pub trait SceneClusters {
174 fn capture<P: TLVBuilderParent>(&self, endpoint_id: EndptId, parent: P) -> Result<P, Error>;
179
180 fn check_scenable(&self, cluster_id: ClusterId, attribute_id: AttrId) -> Option<bool>;
187
188 fn apply<C: HandlerContext>(
192 &self,
193 ctx: &C,
194 endpoint_id: EndptId,
195 cluster_id: ClusterId,
196 avp_list: &TLVArray<'_, AttributeValuePairStruct<'_>>,
197 transition_time_ms: u32,
198 ) -> impl Future<Output = Result<bool, Error>>;
199}
200
201impl SceneClusters for () {
202 fn capture<P: TLVBuilderParent>(&self, _endpoint_id: EndptId, parent: P) -> Result<P, Error> {
203 Ok(parent)
204 }
205
206 fn check_scenable(&self, _cluster_id: ClusterId, _attribute_id: AttrId) -> Option<bool> {
207 None
208 }
209
210 fn apply<C: HandlerContext>(
211 &self,
212 _ctx: &C,
213 _endpoint_id: EndptId,
214 _cluster_id: ClusterId,
215 _avp_list: &TLVArray<'_, AttributeValuePairStruct<'_>>,
216 _transition_time_ms: u32,
217 ) -> impl Future<Output = Result<bool, Error>> {
218 ready(Ok(false))
219 }
220}
221
222impl<H, T> SceneClusters for (H, T)
223where
224 H: SceneClusterHandler,
225 T: SceneClusters,
226{
227 fn check_scenable(&self, cluster_id: ClusterId, attribute_id: AttrId) -> Option<bool> {
228 if cluster_id == H::CLUSTER_ID {
229 Some(H::is_scenable_attribute(attribute_id))
230 } else {
231 self.1.check_scenable(cluster_id, attribute_id)
232 }
233 }
234
235 fn capture<P: TLVBuilderParent>(&self, endpoint_id: EndptId, parent: P) -> Result<P, Error> {
236 let parent = if self.0.endpoint_id() == endpoint_id {
237 let efs = ExtensionFieldSetStructBuilder::new(parent, &TLVTag::Anonymous)?;
238 let efs = efs.cluster_id(H::CLUSTER_ID)?;
239 let avp_array = efs.attribute_value_list()?;
240 let avp_array = self.0.capture(avp_array)?;
241 let efs = avp_array.end()?;
242 efs.end()?
243 } else {
244 parent
245 };
246 self.1.capture(endpoint_id, parent)
247 }
248
249 async fn apply<C: HandlerContext>(
250 &self,
251 ctx: &C,
252 endpoint_id: EndptId,
253 cluster_id: ClusterId,
254 avp_list: &TLVArray<'_, AttributeValuePairStruct<'_>>,
255 transition_time_ms: u32,
256 ) -> Result<bool, Error> {
257 if H::CLUSTER_ID == cluster_id && self.0.endpoint_id() == endpoint_id {
258 self.0.apply(ctx, avp_list, transition_time_ms).await?;
259 Ok(true)
260 } else {
261 self.1
262 .apply(ctx, endpoint_id, cluster_id, avp_list, transition_time_ms)
263 .await
264 }
265 }
266}
267
268impl<P> AttributeValuePairStructArrayBuilder<P>
272where
273 P: TLVBuilderParent,
274{
275 pub fn push_u8(self, attr_id: AttrId, value: u8) -> Result<Self, Error> {
277 self.push()?
278 .attribute_id(attr_id)?
279 .value_unsigned_8(Some(value))?
280 .value_signed_8(None)?
281 .value_unsigned_16(None)?
282 .value_signed_16(None)?
283 .value_unsigned_32(None)?
284 .value_signed_32(None)?
285 .value_unsigned_64(None)?
286 .value_signed_64(None)?
287 .end()
288 }
289
290 pub fn push_u16(self, attr_id: AttrId, value: u16) -> Result<Self, Error> {
292 self.push()?
293 .attribute_id(attr_id)?
294 .value_unsigned_8(None)?
295 .value_signed_8(None)?
296 .value_unsigned_16(Some(value))?
297 .value_signed_16(None)?
298 .value_unsigned_32(None)?
299 .value_signed_32(None)?
300 .value_unsigned_64(None)?
301 .value_signed_64(None)?
302 .end()
303 }
304}
305
306#[derive(Debug)]
312#[cfg_attr(feature = "defmt", derive(defmt::Format))]
313pub struct SceneEntry<const M: usize = MAX_EXT_FIELDS_LEN> {
314 fab_idx: NonZeroU8,
315 endpoint_id: EndptId,
316 group_id: u16,
317 scene_id: SceneId,
318 transition_time: u32,
320 extension_fields: Vec<u8, M>,
325}
326
327impl<const M: usize> SceneEntry<M> {
328 fn matches(
329 &self,
330 fab_idx: NonZeroU8,
331 endpoint_id: EndptId,
332 group_id: u16,
333 scene_id: SceneId,
334 ) -> bool {
335 self.fab_idx == fab_idx
336 && self.endpoint_id == endpoint_id
337 && self.group_id == group_id
338 && self.scene_id == scene_id
339 }
340
341 fn init(
346 fab_idx: NonZeroU8,
347 endpoint_id: EndptId,
348 group_id: u16,
349 scene_id: SceneId,
350 transition_time: u32,
351 ) -> impl Init<Self> {
352 init!(Self {
353 fab_idx,
354 endpoint_id,
355 group_id,
356 scene_id,
357 transition_time,
358 extension_fields <- Vec::init(),
359 })
360 }
361}
362
363#[derive(Debug, Clone, Copy, FromTLV, ToTLV)]
372#[cfg_attr(feature = "defmt", derive(defmt::Format))]
373struct CurrentScene {
374 fab_idx: NonZeroU8,
375 endpoint_id: EndptId,
376 group_id: u16,
377 scene_id: SceneId,
378 valid: bool,
379}
380
381struct ScenesStateInner<const N: usize, const M: usize = MAX_EXT_FIELDS_LEN> {
384 table: Vec<SceneEntry<M>, N>,
386 current_per_fabric: Vec<CurrentScene, N>,
388 info_dataver: u32,
390}
391
392impl<const N: usize, const M: usize> ScenesStateInner<N, M> {
393 const fn new() -> Self {
394 Self {
395 table: Vec::new(),
396 current_per_fabric: Vec::new(),
397 info_dataver: 0,
398 }
399 }
400
401 fn init() -> impl Init<Self> {
402 init!(Self {
403 table <- Vec::init(),
404 current_per_fabric <- Vec::init(),
405 info_dataver: 0,
406 })
407 }
408
409 fn bump_info_dataver(&mut self) {
410 self.info_dataver = self.info_dataver.wrapping_add(1);
411 }
412}
413
414pub struct ScenesState<const N: usize, const M: usize = MAX_EXT_FIELDS_LEN> {
426 inner: Mutex<RefCell<ScenesStateInner<N, M>>>,
427}
428
429impl<const N: usize, const M: usize> ScenesState<N, M> {
430 pub const fn new() -> Self {
431 Self {
432 inner: Mutex::new(RefCell::new(ScenesStateInner::new())),
433 }
434 }
435
436 pub fn init() -> impl Init<Self> {
438 init!(Self {
439 inner <- Mutex::init(RefCell::init(ScenesStateInner::init())),
440 })
441 }
442
443 fn with<F, R>(&self, f: F) -> R
445 where
446 F: FnOnce(&mut ScenesStateInner<N, M>) -> R,
447 {
448 self.inner.lock(|cell| {
449 let mut inner = cell.borrow_mut();
450 f(&mut inner)
451 })
452 }
453}
454
455impl<const N: usize, const M: usize> Default for ScenesState<N, M> {
456 fn default() -> Self {
457 Self::new()
458 }
459}
460
461pub trait SceneInvalidator {
476 fn scenable_attribute_changed(&self, endpoint_id: EndptId);
480}
481
482impl<T: SceneInvalidator + ?Sized> SceneInvalidator for &T {
483 fn scenable_attribute_changed(&self, endpoint_id: EndptId) {
484 (**self).scenable_attribute_changed(endpoint_id);
485 }
486}
487
488impl<const N: usize, const M: usize> SceneInvalidator for ScenesState<N, M> {
489 fn scenable_attribute_changed(&self, endpoint_id: EndptId) {
490 self.with(|inner| {
491 let mut bumped = false;
492 for c in inner.current_per_fabric.iter_mut() {
493 if c.valid && c.endpoint_id == endpoint_id {
494 c.valid = false;
495 bumped = true;
496 }
497 }
498 if bumped {
499 inner.bump_info_dataver();
500 }
501 });
502 }
503}
504
505impl<const M: usize> ToTLV for SceneEntry<M> {
516 fn to_tlv<W: TLVWrite>(&self, tag: &TLVTag, mut tw: W) -> Result<(), Error> {
517 tw.start_struct(tag)?;
518 self.fab_idx.to_tlv(&TLVTag::Context(0), &mut tw)?;
519 self.endpoint_id.to_tlv(&TLVTag::Context(1), &mut tw)?;
520 self.group_id.to_tlv(&TLVTag::Context(2), &mut tw)?;
521 self.scene_id.to_tlv(&TLVTag::Context(3), &mut tw)?;
522 self.transition_time.to_tlv(&TLVTag::Context(4), &mut tw)?;
523 tw.str(&TLVTag::Context(5), &self.extension_fields)?;
527 tw.end_container()
528 }
529
530 fn tlv_iter(&self, _tag: TLVTag) -> impl Iterator<Item = Result<TLV<'_>, Error>> {
531 core::iter::empty()
534 }
535}
536
537impl<'a, const M: usize> FromTLV<'a> for SceneEntry<M> {
538 fn from_tlv(element: &TLVElement<'a>) -> Result<Self, Error> {
539 let s = element.structure()?;
540 let mut extension_fields = Vec::<u8, M>::new();
541 extension_fields
542 .extend_from_slice(s.ctx(5)?.str()?)
543 .map_err(|_| ErrorCode::NoSpace)?;
544 Ok(Self {
545 fab_idx: NonZeroU8::from_tlv(&s.ctx(0)?)?,
546 endpoint_id: EndptId::from_tlv(&s.ctx(1)?)?,
547 group_id: u16::from_tlv(&s.ctx(2)?)?,
548 scene_id: SceneId::from_tlv(&s.ctx(3)?)?,
549 transition_time: u32::from_tlv(&s.ctx(4)?)?,
550 extension_fields,
551 })
552 }
553}
554
555impl<const N: usize, const M: usize> ToTLV for ScenesStateInner<N, M> {
556 fn to_tlv<W: TLVWrite>(&self, tag: &TLVTag, mut tw: W) -> Result<(), Error> {
557 tw.start_struct(tag)?;
558 self.table.to_tlv(&TLVTag::Context(0), &mut tw)?;
559 self.current_per_fabric
560 .to_tlv(&TLVTag::Context(1), &mut tw)?;
561 tw.end_container()
562 }
563
564 fn tlv_iter(&self, _tag: TLVTag) -> impl Iterator<Item = Result<TLV<'_>, Error>> {
565 core::iter::empty()
566 }
567}
568
569impl<'a, const N: usize, const M: usize> FromTLV<'a> for ScenesStateInner<N, M> {
570 fn from_tlv(element: &TLVElement<'a>) -> Result<Self, Error> {
571 let s = element.structure()?;
572 Ok(Self {
573 table: Vec::<SceneEntry<M>, N>::from_tlv(&s.ctx(0)?)?,
574 current_per_fabric: Vec::<CurrentScene, N>::from_tlv(&s.ctx(1)?)?,
575 info_dataver: 0,
576 })
577 }
578}
579
580impl<const N: usize, const M: usize> ScenesState<N, M> {
581 pub fn load_persist<S: KvBlobStore>(&self, mut store: S, buf: &mut [u8]) -> Result<(), Error> {
588 let Some(data) = store.load(SCENES_KEY, buf)? else {
589 self.with(|inner| {
592 inner.table.clear();
593 inner.current_per_fabric.clear();
594 });
595 return Ok(());
596 };
597
598 let loaded = ScenesStateInner::<N, M>::from_tlv(&TLVElement::new(data))?;
599 let entries = loaded.table.len();
600
601 self.with(|inner| {
602 inner.table = loaded.table;
603 inner.current_per_fabric = loaded.current_per_fabric;
604 inner.bump_info_dataver();
605 });
606
607 info!("Loaded Scenes state from storage ({} entries)", entries);
608
609 Ok(())
610 }
611
612 pub fn reset_persist<S: KvBlobStore>(&self, mut store: S, buf: &mut [u8]) -> Result<(), Error> {
618 self.with(|inner| {
619 inner.table.clear();
620 inner.current_per_fabric.clear();
621 inner.bump_info_dataver();
622 });
623
624 store.remove(SCENES_KEY, buf)
625 }
626
627 pub fn remove_for_fabric(&self, fab_idx: NonZeroU8) -> bool {
635 self.with(|inner| {
636 let before = inner.table.len() + inner.current_per_fabric.len();
637
638 inner.table.retain(|entry| entry.fab_idx != fab_idx);
639 inner.current_per_fabric.retain(|c| c.fab_idx != fab_idx);
640
641 let removed = inner.table.len() + inner.current_per_fabric.len() < before;
642 if removed {
643 inner.bump_info_dataver();
644 }
645
646 removed
647 })
648 }
649
650 fn store_persist<C: HandlerContext>(&self, ctx: &C) -> Result<(), Error> {
654 let mut persist = Persist::new(ctx.kv());
655
656 self.inner.lock(|cell| {
657 let inner = cell.borrow();
658 persist.store_tlv(SCENES_KEY, &*inner)
659 })?;
660
661 persist.run()
662 }
663}
664
665pub struct ScenesHandler<'a, const N: usize, R = (), const M: usize = MAX_EXT_FIELDS_LEN>
683where
684 R: SceneClusters,
685{
686 dataver: Dataver,
687 state: &'a ScenesState<N, M>,
688 clusters: R,
689}
690
691impl<'a, const N: usize, R, const M: usize> ScenesHandler<'a, N, R, M>
692where
693 R: SceneClusters,
694{
695 pub const fn new(dataver: Dataver, state: &'a ScenesState<N, M>, clusters: R) -> Self {
696 Self {
697 dataver,
698 state,
699 clusters,
700 }
701 }
702
703 pub const fn adapt(self) -> HandlerAsyncAdaptor<Self> {
704 HandlerAsyncAdaptor(self)
705 }
706
707 fn fab_idx<C: InvokeContext>(ctx: &C) -> Result<NonZeroU8, Error> {
708 ctx.accessor()?.fab_idx()
709 }
710
711 fn remaining_capacity_for_fab(inner: &ScenesStateInner<N, M>, fab_idx: NonZeroU8) -> u8 {
716 let per_fab_budget = N.saturating_sub(1) / 2;
717 let used = inner.table.iter().filter(|e| e.fab_idx == fab_idx).count();
718 let per_fab_remaining = per_fab_budget.saturating_sub(used);
719 let global_remaining = N.saturating_sub(inner.table.len());
720 per_fab_remaining.min(global_remaining).min(0xFF) as u8
721 }
722
723 fn group_in_table<C: InvokeContext>(
728 ctx: &C,
729 fab_idx: NonZeroU8,
730 endpoint_id: EndptId,
731 group_id: u16,
732 ) -> Result<bool, Error> {
733 if group_id == 0 {
734 return Ok(true);
735 }
736
737 #[cfg(feature = "groups")]
738 {
739 ctx.exchange().with_state(|state| {
740 let fabric = state.fabrics.fabric(fab_idx)?;
741 Ok(fabric
742 .groups()
743 .get(group_id)
744 .map(|g| g.endpoints.contains(&endpoint_id))
745 .unwrap_or(false))
746 })
747 }
748
749 #[cfg(not(feature = "groups"))]
752 {
753 let _ = (ctx, fab_idx, endpoint_id);
754 Ok(false)
755 }
756 }
757
758 fn remember_current(
762 inner: &mut ScenesStateInner<N, M>,
763 fab_idx: NonZeroU8,
764 endpoint_id: EndptId,
765 group_id: u16,
766 scene_id: SceneId,
767 ) {
768 if let Some(slot) = inner
769 .current_per_fabric
770 .iter_mut()
771 .find(|c| c.fab_idx == fab_idx)
772 {
773 slot.endpoint_id = endpoint_id;
774 slot.group_id = group_id;
775 slot.scene_id = scene_id;
776 slot.valid = true;
777 } else {
778 let _ = inner.current_per_fabric.push(CurrentScene {
782 fab_idx,
783 endpoint_id,
784 group_id,
785 scene_id,
786 valid: true,
787 });
788 }
789 inner.bump_info_dataver();
790 }
791
792 fn invalidate_current_if_match_scene(
799 inner: &mut ScenesStateInner<N, M>,
800 fab_idx: NonZeroU8,
801 group_id: u16,
802 scene_id: SceneId,
803 ) {
804 let mut bumped = false;
805 for c in inner.current_per_fabric.iter_mut() {
806 if c.valid && c.fab_idx == fab_idx && c.group_id == group_id && c.scene_id == scene_id {
807 c.valid = false;
808 bumped = true;
809 }
810 }
811 if bumped {
812 inner.bump_info_dataver();
813 }
814 }
815
816 fn invalidate_current_if_match_group(
822 inner: &mut ScenesStateInner<N, M>,
823 fab_idx: NonZeroU8,
824 group_id: u16,
825 ) {
826 let mut bumped = false;
827 for c in inner.current_per_fabric.iter_mut() {
828 if c.valid && c.fab_idx == fab_idx && c.group_id == group_id {
829 c.valid = false;
830 bumped = true;
831 }
832 }
833 if bumped {
834 inner.bump_info_dataver();
835 }
836 }
837
838 #[allow(clippy::too_many_arguments)]
845 fn copy_scenes_inner(
846 inner: &mut ScenesStateInner<N, M>,
847 fab_idx: NonZeroU8,
848 endpoint_id: EndptId,
849 group_from: u16,
850 scene_from: SceneId,
851 group_to: u16,
852 scene_to: SceneId,
853 copy_all: bool,
854 ) -> u8 {
855 if Self::remaining_capacity_for_fab(inner, fab_idx) == 0 {
860 return SC_INSUFFICIENT_SPACE;
861 }
862
863 let mut found_source = false;
864 let mut idx = 0;
865 while idx < inner.table.len() {
866 let src = &inner.table[idx];
867 let src_matches = src.fab_idx == fab_idx
868 && src.endpoint_id == endpoint_id
869 && src.group_id == group_from
870 && (copy_all || src.scene_id == scene_from);
871 if src_matches {
872 found_source = true;
873 let src_scene_id = src.scene_id;
876 let src_transition_time = src.transition_time;
877 let src_extension_fields = src.extension_fields.clone();
878 let target_scene_id = if copy_all { src_scene_id } else { scene_to };
879
880 if let Some(pos) = inner
882 .table
883 .iter()
884 .position(|e| e.matches(fab_idx, endpoint_id, group_to, target_scene_id))
885 {
886 inner.table[pos].transition_time = src_transition_time;
887 inner.table[pos].extension_fields = src_extension_fields;
888 } else {
889 if Self::remaining_capacity_for_fab(inner, fab_idx) == 0 {
893 return SC_INSUFFICIENT_SPACE;
894 }
895
896 if inner
897 .table
898 .push(SceneEntry {
899 fab_idx,
900 endpoint_id,
901 group_id: group_to,
902 scene_id: target_scene_id,
903 transition_time: src_transition_time,
904 extension_fields: src_extension_fields,
905 })
906 .is_err()
907 {
908 return SC_INSUFFICIENT_SPACE;
909 }
910 }
911
912 if !copy_all {
914 break;
915 }
916 }
917 idx += 1;
918 }
919
920 if !found_source {
921 return SC_NOT_FOUND;
922 }
923
924 if copy_all {
927 Self::invalidate_current_if_match_group(inner, fab_idx, group_to);
928 } else {
929 Self::invalidate_current_if_match_scene(inner, fab_idx, group_to, scene_to);
930 }
931 0
932 }
933
934 fn read_fabric_scene_info<P: TLVBuilderParent>(
941 &self,
942 ctx: &impl ReadContext,
943 builder: ArrayAttributeRead<SceneInfoStructArrayBuilder<P>, SceneInfoStructBuilder<P>>,
944 ) -> Result<P, Error> {
945 let endpoint_id = ctx.attr().endpoint_id;
946 let accessor_fab_idx = ctx.accessor()?.fab_idx()?;
947
948 let (has_state, scene_count, cur_group, cur_scene, valid, remaining) =
954 self.state.with(|inner| {
955 let count = inner
956 .table
957 .iter()
958 .filter(|e| e.fab_idx == accessor_fab_idx && e.endpoint_id == endpoint_id)
959 .count();
960 let current = inner
961 .current_per_fabric
962 .iter()
963 .find(|c| c.fab_idx == accessor_fab_idx)
964 .copied();
965 let has_state = count > 0 || current.is_some();
966 let (g, s, v) = match current {
970 Some(c) => (Some(c.group_id), Some(c.scene_id), c.valid),
971 None => (Some(0u16), Some(0u8), false),
972 };
973 let rem = Self::remaining_capacity_for_fab(inner, accessor_fab_idx);
974 (has_state, count.min(0xFF) as u8, g, s, v, rem)
975 });
976
977 match builder {
978 ArrayAttributeRead::ReadAll(arr) => {
979 if !has_state {
980 return arr.end();
981 }
982
983 let arr = arr
984 .push()?
985 .scene_count(scene_count)?
986 .current_scene(cur_scene)?
987 .current_group(cur_group)?
988 .scene_valid(Some(valid))?
989 .remaining_capacity(remaining)?
990 .fabric_index(Some(accessor_fab_idx.get()))?
991 .end()?;
992
993 arr.end()
994 }
995 ArrayAttributeRead::ReadNone(arr) => arr.end(),
996 ArrayAttributeRead::ReadOne(_idx, _entry) => {
997 Err(ErrorCode::AttributeNotFound.into())
1000 }
1001 }
1002 }
1003
1004 fn add_scene<P: TLVBuilderParent>(
1005 &self,
1006 ctx: &impl InvokeContext,
1007 request: &AddSceneRequest<'_>,
1008 response: AddSceneResponseBuilder<P>,
1009 ) -> Result<P, Error> {
1010 let fab_idx = Self::fab_idx(ctx)?;
1011 let endpoint_id = ctx.cmd().endpoint_id;
1012 let group_id = request.group_id()?;
1013 let scene_id = request.scene_id()?;
1014 let transition_time = request.transition_time()?;
1015
1016 if scene_id == GLOBAL_SCENE_ID
1019 || scene_id == RESERVED_SCENE_ID
1020 || transition_time > MAX_TRANSITION_TIME_MS
1021 {
1022 return response
1023 .status(SC_CONSTRAINT_ERROR)?
1024 .group_id(group_id)?
1025 .scene_id(scene_id)?
1026 .end();
1027 }
1028
1029 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_id)? {
1030 return response
1031 .status(SC_INVALID_COMMAND)?
1032 .group_id(group_id)?
1033 .scene_id(scene_id)?
1034 .end();
1035 }
1036
1037 let efs_array_opt = request.extension_field_set_structs().ok();
1043 let raw = match efs_array_opt {
1044 Some(ref array) => array.element().raw_value()?,
1045 None => &[],
1046 };
1047
1048 if let Some(ref efs_array) = efs_array_opt {
1054 for efs in efs_array.iter() {
1055 let efs = efs?;
1056 let cid = efs.cluster_id()?;
1057 for avp in efs.attribute_value_list()?.iter() {
1058 let avp = avp?;
1059 let aid = avp.attribute_id()?;
1060 if let Some(false) = self.clusters.check_scenable(cid, aid) {
1061 return response
1062 .status(SC_INVALID_COMMAND)?
1063 .group_id(group_id)?
1064 .scene_id(scene_id)?
1065 .end();
1066 }
1067 }
1068 }
1069 }
1070
1071 if raw.len() > M {
1074 return response
1075 .status(SC_INSUFFICIENT_SPACE)?
1076 .group_id(group_id)?
1077 .scene_id(scene_id)?
1078 .end();
1079 }
1080
1081 let status_code = self.state.with(|inner| {
1082 Self::upsert_scene(
1083 inner,
1084 fab_idx,
1085 endpoint_id,
1086 group_id,
1087 scene_id,
1088 transition_time,
1089 |ext_fields| {
1090 if !raw.is_empty() {
1091 ext_fields
1092 .extend_from_slice(raw)
1093 .map_err(|_| ErrorCode::NoSpace)?;
1094 }
1095 Ok(())
1096 },
1097 )
1098 })?;
1099
1100 if status_code == 0 {
1101 self.state.store_persist(ctx)?;
1102 ctx.notify_own_attr_changed(AttributeId::FabricSceneInfo as _);
1103 }
1104
1105 response
1106 .status(status_code)?
1107 .group_id(group_id)?
1108 .scene_id(scene_id)?
1109 .end()
1110 }
1111
1112 fn upsert_scene<F>(
1123 inner: &mut ScenesStateInner<N, M>,
1124 fab_idx: NonZeroU8,
1125 endpoint_id: EndptId,
1126 group_id: u16,
1127 scene_id: SceneId,
1128 transition_time: u32,
1129 fill: F,
1130 ) -> Result<u8, Error>
1131 where
1132 F: FnOnce(&mut Vec<u8, M>) -> Result<(), Error>,
1133 {
1134 if let Some(pos) = inner
1135 .table
1136 .iter()
1137 .position(|e| e.matches(fab_idx, endpoint_id, group_id, scene_id))
1138 {
1139 inner.table[pos].transition_time = transition_time;
1141 inner.table[pos].extension_fields.clear();
1142 fill(&mut inner.table[pos].extension_fields)?;
1143 Self::invalidate_current_if_match_scene(inner, fab_idx, group_id, scene_id);
1144 Ok(0)
1145 } else if inner.table.len() >= N {
1146 Ok(SC_INSUFFICIENT_SPACE)
1147 } else {
1148 inner
1151 .table
1152 .push_init_unchecked(SceneEntry::init(
1153 fab_idx,
1154 endpoint_id,
1155 group_id,
1156 scene_id,
1157 transition_time,
1158 ))
1159 .unwrap();
1160 let pos = inner.table.len() - 1;
1161 if let Err(e) = fill(&mut inner.table[pos].extension_fields) {
1162 let _ = inner.table.pop();
1163 return Err(e);
1164 }
1165 Self::invalidate_current_if_match_scene(inner, fab_idx, group_id, scene_id);
1166 Ok(0)
1167 }
1168 }
1169
1170 fn view_scene<P: TLVBuilderParent>(
1171 &self,
1172 ctx: &impl InvokeContext,
1173 request: &ViewSceneRequest<'_>,
1174 response: ViewSceneResponseBuilder<P>,
1175 ) -> Result<P, Error> {
1176 let fab_idx = Self::fab_idx(ctx)?;
1177 let endpoint_id = ctx.cmd().endpoint_id;
1178 let group_id = request.group_id()?;
1179 let scene_id = request.scene_id()?;
1180
1181 if scene_id == GLOBAL_SCENE_ID || scene_id == RESERVED_SCENE_ID {
1183 return response
1184 .status(SC_CONSTRAINT_ERROR)?
1185 .group_id(group_id)?
1186 .scene_id(scene_id)?
1187 .transition_time(None)?
1188 .scene_name(None)?
1189 .extension_field_set_structs()?
1190 .none()
1191 .end();
1192 }
1193
1194 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_id)? {
1195 return response
1196 .status(SC_INVALID_COMMAND)?
1197 .group_id(group_id)?
1198 .scene_id(scene_id)?
1199 .transition_time(None)?
1200 .scene_name(None)?
1201 .extension_field_set_structs()?
1202 .none()
1203 .end();
1204 }
1205
1206 self.state.with(|inner| -> Result<P, Error> {
1210 let entry = inner
1211 .table
1212 .iter()
1213 .find(|e| e.matches(fab_idx, endpoint_id, group_id, scene_id));
1214
1215 let Some(e) = entry else {
1216 return response
1217 .status(SC_NOT_FOUND)?
1218 .group_id(group_id)?
1219 .scene_id(scene_id)?
1220 .transition_time(None)?
1221 .scene_name(None)?
1222 .extension_field_set_structs()?
1223 .none()
1224 .end();
1225 };
1226
1227 let opt = response
1228 .status(0)?
1229 .group_id(group_id)?
1230 .scene_id(scene_id)?
1231 .transition_time(Some(e.transition_time))?
1232 .scene_name(Some(""))?
1233 .extension_field_set_structs()?;
1234
1235 Self::write_blob_or_none(opt, &e.extension_fields)?.end()
1236 })
1237 }
1238
1239 fn write_blob_or_none<P, Q>(mut opt: OptionalBuilder<P, Q>, blob: &[u8]) -> Result<P, Error>
1244 where
1245 P: TLVBuilderParent,
1246 Q: TLVBuilder<P>,
1247 {
1248 if !blob.is_empty() {
1249 let writer = opt.writer();
1250 writer.start_array(&TLVTag::Context(5))?;
1251 writer.write_raw_data(blob.iter().copied())?;
1252 }
1253 Ok(opt.none())
1254 }
1255
1256 fn remove_scene<P: TLVBuilderParent>(
1257 &self,
1258 ctx: &impl InvokeContext,
1259 request: &RemoveSceneRequest<'_>,
1260 response: RemoveSceneResponseBuilder<P>,
1261 ) -> Result<P, Error> {
1262 let fab_idx = Self::fab_idx(ctx)?;
1263 let endpoint_id = ctx.cmd().endpoint_id;
1264 let group_id = request.group_id()?;
1265 let scene_id = request.scene_id()?;
1266
1267 if scene_id == GLOBAL_SCENE_ID || scene_id == RESERVED_SCENE_ID {
1269 return response
1270 .status(SC_CONSTRAINT_ERROR)?
1271 .group_id(group_id)?
1272 .scene_id(scene_id)?
1273 .end();
1274 }
1275
1276 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_id)? {
1277 return response
1278 .status(SC_INVALID_COMMAND)?
1279 .group_id(group_id)?
1280 .scene_id(scene_id)?
1281 .end();
1282 }
1283
1284 let status: u8 = self.state.with(|inner| {
1285 if let Some(pos) = inner
1286 .table
1287 .iter()
1288 .position(|e| e.matches(fab_idx, endpoint_id, group_id, scene_id))
1289 {
1290 inner.table.swap_remove(pos);
1291 Self::invalidate_current_if_match_scene(inner, fab_idx, group_id, scene_id);
1292 0
1293 } else {
1294 SC_NOT_FOUND
1295 }
1296 });
1297
1298 if status == 0 {
1299 self.state.store_persist(ctx)?;
1300 ctx.notify_own_attr_changed(AttributeId::FabricSceneInfo as _);
1301 }
1302
1303 response
1304 .status(status)?
1305 .group_id(group_id)?
1306 .scene_id(scene_id)?
1307 .end()
1308 }
1309
1310 fn remove_all_scenes<P: TLVBuilderParent>(
1311 &self,
1312 ctx: &impl InvokeContext,
1313 request: &RemoveAllScenesRequest<'_>,
1314 response: RemoveAllScenesResponseBuilder<P>,
1315 ) -> Result<P, Error> {
1316 let fab_idx = Self::fab_idx(ctx)?;
1317 let endpoint_id = ctx.cmd().endpoint_id;
1318 let group_id = request.group_id()?;
1319
1320 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_id)? {
1321 return response
1322 .status(SC_INVALID_COMMAND)?
1323 .group_id(group_id)?
1324 .end();
1325 }
1326
1327 let removed = self.state.with(|inner| {
1328 let before = inner.table.len();
1329 inner.table.retain(|e| {
1330 !(e.fab_idx == fab_idx && e.endpoint_id == endpoint_id && e.group_id == group_id)
1331 });
1332 let changed = before != inner.table.len();
1333 if changed {
1334 Self::invalidate_current_if_match_group(inner, fab_idx, group_id);
1335 }
1336 changed
1337 });
1338
1339 if removed {
1340 self.state.store_persist(ctx)?;
1341 ctx.notify_own_attr_changed(AttributeId::FabricSceneInfo as _);
1342 }
1343
1344 response.status(0)?.group_id(group_id)?.end()
1345 }
1346
1347 async fn store_scene<P: TLVBuilderParent>(
1351 &self,
1352 ctx: &impl InvokeContext,
1353 request: &StoreSceneRequest<'_>,
1354 response: StoreSceneResponseBuilder<P>,
1355 ) -> Result<P, Error> {
1356 let fab_idx = Self::fab_idx(ctx)?;
1357 let endpoint_id = ctx.cmd().endpoint_id;
1358 let group_id = request.group_id()?;
1359 let scene_id = request.scene_id()?;
1360
1361 if scene_id == GLOBAL_SCENE_ID || scene_id == RESERVED_SCENE_ID {
1363 return response
1364 .status(SC_CONSTRAINT_ERROR)?
1365 .group_id(group_id)?
1366 .scene_id(scene_id)?
1367 .end();
1368 }
1369
1370 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_id)? {
1371 return response
1372 .status(SC_INVALID_COMMAND)?
1373 .group_id(group_id)?
1374 .scene_id(scene_id)?
1375 .end();
1376 }
1377
1378 let mut scratch = [0u8; M];
1384 let total_len = {
1385 let mut wb = WriteBuf::new(&mut scratch);
1386 let parent = TLVWriteParent::new("StoreScene EFS", &mut wb);
1387 let _ = self.clusters.capture(endpoint_id, parent)?;
1388 wb.end_container()?;
1389 wb.get_tail()
1390 };
1391 let stored_bytes = &scratch[..total_len];
1392
1393 let prior_tt = self.state.with(|inner| {
1396 inner
1397 .table
1398 .iter()
1399 .find(|e| e.matches(fab_idx, endpoint_id, group_id, scene_id))
1400 .map(|e| e.transition_time)
1401 });
1402 let transition_time = prior_tt.unwrap_or(0);
1403
1404 let status_code = self.state.with(|inner| {
1405 let status = Self::upsert_scene(
1406 inner,
1407 fab_idx,
1408 endpoint_id,
1409 group_id,
1410 scene_id,
1411 transition_time,
1412 |ext_fields| {
1413 if !stored_bytes.is_empty() {
1414 ext_fields
1415 .extend_from_slice(stored_bytes)
1416 .map_err(|_| ErrorCode::NoSpace)?;
1417 }
1418 Ok(())
1419 },
1420 )?;
1421 if status == 0 {
1427 Self::remember_current(inner, fab_idx, endpoint_id, group_id, scene_id);
1428 }
1429 Ok::<_, Error>(status)
1430 })?;
1431
1432 if status_code == 0 {
1433 self.state.store_persist(ctx)?;
1434 ctx.notify_own_attr_changed(AttributeId::FabricSceneInfo as _);
1435 }
1436
1437 response
1438 .status(status_code)?
1439 .group_id(group_id)?
1440 .scene_id(scene_id)?
1441 .end()
1442 }
1443
1444 async fn recall_scene(
1449 &self,
1450 ctx: &impl InvokeContext,
1451 request: &RecallSceneRequest<'_>,
1452 ) -> Result<(), Error> {
1453 let fab_idx = Self::fab_idx(ctx)?;
1454 let endpoint_id = ctx.cmd().endpoint_id;
1455 let group_id = request.group_id()?;
1456 let scene_id = request.scene_id()?;
1457
1458 if scene_id == GLOBAL_SCENE_ID || scene_id == RESERVED_SCENE_ID {
1468 return Err(ErrorCode::ConstraintError.into());
1469 }
1470
1471 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_id)? {
1472 return Err(ErrorCode::InvalidCommand.into());
1473 }
1474
1475 let override_tt_ms: Option<u32> = request.transition_time()?.and_then(|n| n.into_option());
1478
1479 let mut blob = [0u8; M];
1485 let (blob_len, stored_tt_ms) = self.state.with(|inner| -> Result<_, Error> {
1486 let Some(e) = inner
1487 .table
1488 .iter()
1489 .find(|e| e.matches(fab_idx, endpoint_id, group_id, scene_id))
1490 else {
1491 return Ok((None, None));
1492 };
1493 let len = e.extension_fields.len();
1494 blob[..len].copy_from_slice(&e.extension_fields);
1495 Ok((Some(len), Some(e.transition_time)))
1496 })?;
1497 let (Some(blob_len), Some(stored_tt_ms)) = (blob_len, stored_tt_ms) else {
1498 return Err(ErrorCode::NotFound.into());
1499 };
1500
1501 let effective_tt_ms = override_tt_ms.unwrap_or(stored_tt_ms);
1502
1503 for efs_element in TLVSequence(&blob[..blob_len]).iter() {
1504 let efs = ExtensionFieldSetStruct::new(efs_element?);
1505 let cluster_id = efs.cluster_id()?;
1506 let avp_list = efs.attribute_value_list()?;
1507 let _ = self
1510 .clusters
1511 .apply(ctx, endpoint_id, cluster_id, &avp_list, effective_tt_ms)
1512 .await?;
1513 }
1514
1515 self.state
1516 .with(|inner| Self::remember_current(inner, fab_idx, endpoint_id, group_id, scene_id));
1517
1518 self.state.store_persist(ctx)?;
1519 ctx.notify_own_attr_changed(AttributeId::FabricSceneInfo as _);
1520 Ok(())
1521 }
1522
1523 fn get_scene_membership<P: TLVBuilderParent>(
1524 &self,
1525 ctx: &impl InvokeContext,
1526 request: &GetSceneMembershipRequest<'_>,
1527 response: GetSceneMembershipResponseBuilder<P>,
1528 ) -> Result<P, Error> {
1529 let fab_idx = Self::fab_idx(ctx)?;
1530 let endpoint_id = ctx.cmd().endpoint_id;
1531 let group_id = request.group_id()?;
1532
1533 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_id)? {
1536 return response
1537 .status(SC_INVALID_COMMAND)?
1538 .capacity(Nullable::none())?
1539 .group_id(group_id)?
1540 .scene_list()?
1541 .none()
1542 .end();
1543 }
1544
1545 self.state.with(|inner| -> Result<P, Error> {
1550 let remaining = Self::remaining_capacity_for_fab(inner, fab_idx);
1551
1552 let resp = response
1553 .status(0)?
1554 .capacity(Nullable::some(remaining))?
1555 .group_id(group_id)?;
1556
1557 let list = resp.scene_list()?.some()?;
1558 let list = inner
1559 .table
1560 .iter()
1561 .filter(|e| {
1562 e.fab_idx == fab_idx && e.endpoint_id == endpoint_id && e.group_id == group_id
1563 })
1564 .try_fold(list, |list, e| list.push(&e.scene_id))?;
1565 list.end()?.end()
1566 })
1567 }
1568
1569 fn copy_scene<P: TLVBuilderParent>(
1570 &self,
1571 ctx: &impl InvokeContext,
1572 request: &CopySceneRequest<'_>,
1573 response: CopySceneResponseBuilder<P>,
1574 ) -> Result<P, Error> {
1575 let fab_idx = Self::fab_idx(ctx)?;
1576 let endpoint_id = ctx.cmd().endpoint_id;
1577 let mode = request.mode()?;
1578 let group_from = request.group_identifier_from()?;
1579 let scene_from = request.scene_identifier_from()?;
1580 let group_to = request.group_identifier_to()?;
1581 let scene_to = request.scene_identifier_to()?;
1582
1583 let copy_all = (mode.bits() & 0x01) != 0;
1586
1587 if !copy_all
1590 && (scene_from == GLOBAL_SCENE_ID
1591 || scene_from == RESERVED_SCENE_ID
1592 || scene_to == GLOBAL_SCENE_ID
1593 || scene_to == RESERVED_SCENE_ID)
1594 {
1595 return response
1596 .status(SC_CONSTRAINT_ERROR)?
1597 .group_identifier_from(group_from)?
1598 .scene_identifier_from(scene_from)?
1599 .end();
1600 }
1601
1602 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_from)?
1603 || !Self::group_in_table(ctx, fab_idx, endpoint_id, group_to)?
1604 {
1605 return response
1606 .status(SC_INVALID_COMMAND)?
1607 .group_identifier_from(group_from)?
1608 .scene_identifier_from(scene_from)?
1609 .end();
1610 }
1611
1612 let status = self.state.with(|inner| {
1613 Self::copy_scenes_inner(
1614 inner,
1615 fab_idx,
1616 endpoint_id,
1617 group_from,
1618 scene_from,
1619 group_to,
1620 scene_to,
1621 copy_all,
1622 )
1623 });
1624
1625 if status == 0 {
1626 self.state.store_persist(ctx)?;
1627 ctx.notify_own_attr_changed(AttributeId::FabricSceneInfo as _);
1628 }
1629
1630 response
1631 .status(status)?
1632 .group_identifier_from(group_from)?
1633 .scene_identifier_from(scene_from)?
1634 .end()
1635 }
1636}
1637
1638impl<const N: usize, R, const M: usize> ClusterAsyncHandler for ScenesHandler<'_, N, R, M>
1639where
1640 R: SceneClusters,
1641{
1642 const CLUSTER: Cluster<'static> = FULL_CLUSTER;
1643
1644 fn dataver(&self) -> u32 {
1645 self.dataver.get()
1646 }
1647
1648 fn dataver_changed(&self) {
1649 self.dataver.changed();
1650 }
1651
1652 fn lifecycle(&self, ctx: impl HandlerContext, op: LifecycleOp) -> Result<(), Error> {
1653 match op {
1654 LifecycleOp::Startup => ctx
1655 .kv()
1656 .access(|store, buf| self.state.load_persist(store, buf)),
1657 LifecycleOp::FactoryReset => ctx
1658 .kv()
1659 .access(|store, buf| self.state.reset_persist(store, buf)),
1660 LifecycleOp::FabricRemoval { fab_idx } => {
1661 if self.state.remove_for_fabric(fab_idx) {
1662 self.state.store_persist(&ctx)
1663 } else {
1664 Ok(())
1665 }
1666 }
1667 }
1668 }
1669
1670 fn scene_table_size(&self, _ctx: impl ReadContext) -> impl Future<Output = Result<u16, Error>> {
1671 ready(Ok(N as u16))
1672 }
1673
1674 fn fabric_scene_info<P: TLVBuilderParent>(
1675 &self,
1676 ctx: impl ReadContext,
1677 builder: ArrayAttributeRead<SceneInfoStructArrayBuilder<P>, SceneInfoStructBuilder<P>>,
1678 ) -> impl Future<Output = Result<P, Error>> {
1679 ready(self.read_fabric_scene_info(&ctx, builder))
1680 }
1681
1682 fn handle_add_scene<P: TLVBuilderParent>(
1683 &self,
1684 ctx: impl InvokeContext,
1685 request: AddSceneRequest<'_>,
1686 response: AddSceneResponseBuilder<P>,
1687 ) -> impl Future<Output = Result<P, Error>> {
1688 ready(self.add_scene(&ctx, &request, response))
1689 }
1690
1691 fn handle_view_scene<P: TLVBuilderParent>(
1692 &self,
1693 ctx: impl InvokeContext,
1694 request: ViewSceneRequest<'_>,
1695 response: ViewSceneResponseBuilder<P>,
1696 ) -> impl Future<Output = Result<P, Error>> {
1697 ready(self.view_scene(&ctx, &request, response))
1698 }
1699
1700 fn handle_remove_scene<P: TLVBuilderParent>(
1701 &self,
1702 ctx: impl InvokeContext,
1703 request: RemoveSceneRequest<'_>,
1704 response: RemoveSceneResponseBuilder<P>,
1705 ) -> impl Future<Output = Result<P, Error>> {
1706 ready(self.remove_scene(&ctx, &request, response))
1707 }
1708
1709 fn handle_remove_all_scenes<P: TLVBuilderParent>(
1710 &self,
1711 ctx: impl InvokeContext,
1712 request: RemoveAllScenesRequest<'_>,
1713 response: RemoveAllScenesResponseBuilder<P>,
1714 ) -> impl Future<Output = Result<P, Error>> {
1715 ready(self.remove_all_scenes(&ctx, &request, response))
1716 }
1717
1718 async fn handle_store_scene<P: TLVBuilderParent>(
1719 &self,
1720 ctx: impl InvokeContext,
1721 request: StoreSceneRequest<'_>,
1722 response: StoreSceneResponseBuilder<P>,
1723 ) -> Result<P, Error> {
1724 self.store_scene(&ctx, &request, response).await
1725 }
1726
1727 async fn handle_recall_scene(
1728 &self,
1729 ctx: impl InvokeContext,
1730 request: RecallSceneRequest<'_>,
1731 ) -> Result<(), Error> {
1732 self.recall_scene(&ctx, &request).await
1733 }
1734
1735 fn handle_get_scene_membership<P: TLVBuilderParent>(
1736 &self,
1737 ctx: impl InvokeContext,
1738 request: GetSceneMembershipRequest<'_>,
1739 response: GetSceneMembershipResponseBuilder<P>,
1740 ) -> impl Future<Output = Result<P, Error>> {
1741 ready(self.get_scene_membership(&ctx, &request, response))
1742 }
1743
1744 fn handle_copy_scene<P: TLVBuilderParent>(
1745 &self,
1746 ctx: impl InvokeContext,
1747 request: CopySceneRequest<'_>,
1748 response: CopySceneResponseBuilder<P>,
1749 ) -> impl Future<Output = Result<P, Error>> {
1750 ready(self.copy_scene(&ctx, &request, response))
1751 }
1752}
1753
1754#[cfg(test)]
1755mod tests {
1756 use super::*;
1763
1764 fn fab(n: u8) -> NonZeroU8 {
1765 NonZeroU8::new(n).unwrap()
1766 }
1767
1768 fn entry(
1769 fab_idx: NonZeroU8,
1770 endpoint_id: EndptId,
1771 group_id: u16,
1772 scene_id: SceneId,
1773 transition_time: u32,
1774 ) -> SceneEntry {
1775 SceneEntry {
1776 fab_idx,
1777 endpoint_id,
1778 group_id,
1779 scene_id,
1780 transition_time,
1781 extension_fields: Vec::new(),
1782 }
1783 }
1784
1785 fn entry_with_blob(
1787 fab_idx: NonZeroU8,
1788 endpoint_id: EndptId,
1789 group_id: u16,
1790 scene_id: SceneId,
1791 transition_time: u32,
1792 blob: &[u8],
1793 ) -> SceneEntry {
1794 let mut ext: Vec<u8, MAX_EXT_FIELDS_LEN> = Vec::new();
1795 ext.extend_from_slice(blob)
1796 .expect("blob too large for test");
1797 SceneEntry {
1798 fab_idx,
1799 endpoint_id,
1800 group_id,
1801 scene_id,
1802 transition_time,
1803 extension_fields: ext,
1804 }
1805 }
1806
1807 fn push<const N: usize>(inner: &mut ScenesStateInner<N>, e: SceneEntry) {
1808 inner.table.push(e).expect("test table overflow");
1809 }
1810
1811 fn count<const N: usize>(
1813 inner: &ScenesStateInner<N>,
1814 fab_idx: NonZeroU8,
1815 ep: EndptId,
1816 group: u16,
1817 ) -> usize {
1818 inner
1819 .table
1820 .iter()
1821 .filter(|e| e.fab_idx == fab_idx && e.endpoint_id == ep && e.group_id == group)
1822 .count()
1823 }
1824
1825 fn find_tt<const N: usize>(
1826 inner: &ScenesStateInner<N>,
1827 fab_idx: NonZeroU8,
1828 ep: EndptId,
1829 group: u16,
1830 scene: SceneId,
1831 ) -> Option<u32> {
1832 inner
1833 .table
1834 .iter()
1835 .find(|e| e.matches(fab_idx, ep, group, scene))
1836 .map(|e| e.transition_time)
1837 }
1838
1839 fn find_blob<const N: usize>(
1841 inner: &ScenesStateInner<N>,
1842 fab_idx: NonZeroU8,
1843 ep: EndptId,
1844 group: u16,
1845 scene: SceneId,
1846 ) -> Option<&[u8]> {
1847 inner
1848 .table
1849 .iter()
1850 .find(|e| e.matches(fab_idx, ep, group, scene))
1851 .map(|e| e.extension_fields.as_slice())
1852 }
1853
1854 #[test]
1857 fn copy_single_scene_preserves_extension_fields_blob() {
1858 let blob = &[0xDE, 0xAD, 0xBE, 0xEF, 0x18];
1861 let mut inner = ScenesStateInner::<8>::new();
1862 push(&mut inner, entry_with_blob(fab(1), 1, 10, 5, 100, blob));
1863
1864 let status =
1865 ScenesHandler::<8>::copy_scenes_inner(&mut inner, fab(1), 1, 10, 5, 20, 7, false);
1866 assert_eq!(status, 0);
1867
1868 assert_eq!(find_blob(&inner, fab(1), 1, 20, 7), Some(&blob[..]));
1869 assert_eq!(find_blob(&inner, fab(1), 1, 10, 5), Some(&blob[..]));
1871 }
1872
1873 #[test]
1874 fn copy_all_preserves_each_source_blob() {
1875 let blob_a = &[0xAA, 0xBB, 0x18];
1878 let blob_b = &[0xCC, 0x18];
1879 let mut inner = ScenesStateInner::<16>::new();
1880 push(&mut inner, entry_with_blob(fab(1), 1, 10, 1, 100, blob_a));
1881 push(&mut inner, entry_with_blob(fab(1), 1, 10, 2, 200, blob_b));
1882
1883 let status =
1884 ScenesHandler::<16>::copy_scenes_inner(&mut inner, fab(1), 1, 10, 0, 20, 0, true);
1885 assert_eq!(status, 0);
1886
1887 assert_eq!(find_blob(&inner, fab(1), 1, 20, 1), Some(&blob_a[..]));
1888 assert_eq!(find_blob(&inner, fab(1), 1, 20, 2), Some(&blob_b[..]));
1889 }
1890
1891 #[test]
1892 fn copy_overwrites_existing_dest_blob() {
1893 let old_blob = &[0x11, 0x18];
1894 let new_blob = &[0x22, 0x33, 0x18];
1895 let mut inner = ScenesStateInner::<8>::new();
1896 push(&mut inner, entry_with_blob(fab(1), 1, 10, 5, 100, new_blob));
1897 push(&mut inner, entry_with_blob(fab(1), 1, 20, 7, 999, old_blob));
1898
1899 let status =
1900 ScenesHandler::<8>::copy_scenes_inner(&mut inner, fab(1), 1, 10, 5, 20, 7, false);
1901 assert_eq!(status, 0);
1902
1903 assert_eq!(find_blob(&inner, fab(1), 1, 20, 7), Some(&new_blob[..]));
1906 }
1907
1908 #[test]
1911 fn copy_single_scene_to_new_dest() {
1912 let mut inner = ScenesStateInner::<8>::new();
1913 push(&mut inner, entry(fab(1), 1, 10, 5, 100));
1914
1915 let status = ScenesHandler::<8>::copy_scenes_inner(
1916 &mut inner,
1917 fab(1),
1918 1,
1919 10,
1920 5,
1921 20,
1922 7,
1923 false,
1924 );
1925
1926 assert_eq!(status, 0);
1927 assert_eq!(find_tt(&inner, fab(1), 1, 10, 5), Some(100));
1929 assert_eq!(find_tt(&inner, fab(1), 1, 20, 7), Some(100));
1932 assert_eq!(inner.table.len(), 2);
1933 }
1934
1935 #[test]
1936 fn copy_single_scene_replaces_existing_dest() {
1937 let mut inner = ScenesStateInner::<8>::new();
1938 push(&mut inner, entry(fab(1), 1, 10, 5, 100));
1939 push(&mut inner, entry(fab(1), 1, 20, 7, 999));
1940
1941 let status =
1942 ScenesHandler::<8>::copy_scenes_inner(&mut inner, fab(1), 1, 10, 5, 20, 7, false);
1943
1944 assert_eq!(status, 0);
1945 assert_eq!(find_tt(&inner, fab(1), 1, 20, 7), Some(100));
1947 assert_eq!(inner.table.len(), 2);
1948 }
1949
1950 #[test]
1951 fn copy_single_scene_missing_source_returns_not_found() {
1952 let mut inner = ScenesStateInner::<8>::new();
1953 push(&mut inner, entry(fab(1), 1, 10, 5, 100));
1954
1955 let status = ScenesHandler::<8>::copy_scenes_inner(
1956 &mut inner,
1957 fab(1),
1958 1,
1959 99, 5,
1961 20,
1962 7,
1963 false,
1964 );
1965
1966 assert_eq!(status, SC_NOT_FOUND);
1967 assert_eq!(inner.table.len(), 1);
1969 assert_eq!(find_tt(&inner, fab(1), 1, 20, 7), None);
1970 }
1971
1972 #[test]
1975 fn copy_all_copies_every_source_scene() {
1976 let mut inner = ScenesStateInner::<16>::new();
1979 push(&mut inner, entry(fab(1), 1, 10, 1, 100));
1980 push(&mut inner, entry(fab(1), 1, 10, 2, 200));
1981 push(&mut inner, entry(fab(1), 1, 10, 3, 300));
1982
1983 let status = ScenesHandler::<16>::copy_scenes_inner(
1984 &mut inner,
1985 fab(1),
1986 1,
1987 10,
1988 0, 20,
1990 0, true,
1992 );
1993
1994 assert_eq!(status, 0);
1995 assert_eq!(count(&inner, fab(1), 1, 20), 3);
1998 assert_eq!(find_tt(&inner, fab(1), 1, 20, 1), Some(100));
1999 assert_eq!(find_tt(&inner, fab(1), 1, 20, 2), Some(200));
2000 assert_eq!(find_tt(&inner, fab(1), 1, 20, 3), Some(300));
2001 assert_eq!(count(&inner, fab(1), 1, 10), 3);
2003 }
2004
2005 #[test]
2006 fn copy_all_to_same_group_is_noop() {
2007 let mut inner = ScenesStateInner::<8>::new();
2012 push(&mut inner, entry(fab(1), 1, 10, 1, 100));
2013 push(&mut inner, entry(fab(1), 1, 10, 2, 200));
2014
2015 let status = ScenesHandler::<8>::copy_scenes_inner(
2016 &mut inner,
2017 fab(1),
2018 1,
2019 10,
2020 0,
2021 10, 0,
2023 true,
2024 );
2025
2026 assert_eq!(status, 0);
2027 assert_eq!(inner.table.len(), 2);
2028 }
2029
2030 #[test]
2031 fn copy_all_missing_source_returns_not_found() {
2032 let mut inner = ScenesStateInner::<8>::new();
2033 push(&mut inner, entry(fab(1), 1, 99, 1, 100));
2035
2036 let status = ScenesHandler::<8>::copy_scenes_inner(
2037 &mut inner,
2038 fab(1),
2039 1,
2040 10, 0,
2042 20,
2043 0,
2044 true,
2045 );
2046
2047 assert_eq!(status, SC_NOT_FOUND);
2048 assert_eq!(inner.table.len(), 1);
2049 assert_eq!(count(&inner, fab(1), 1, 20), 0);
2050 }
2051
2052 #[test]
2053 fn copy_all_capacity_exhaustion_returns_insufficient_space() {
2054 let mut inner = ScenesStateInner::<3>::new();
2057 inner.table.push(entry(fab(1), 1, 10, 1, 100)).unwrap();
2058 inner.table.push(entry(fab(1), 1, 10, 2, 200)).unwrap();
2059 inner.table.push(entry(fab(1), 1, 10, 3, 300)).unwrap();
2060
2061 let status =
2062 ScenesHandler::<3>::copy_scenes_inner(&mut inner, fab(1), 1, 10, 0, 20, 0, true);
2063
2064 assert_eq!(status, SC_INSUFFICIENT_SPACE);
2065 assert_eq!(inner.table.len(), 3);
2069 }
2070
2071 #[test]
2074 fn copy_does_not_cross_fabric_boundary() {
2075 let mut inner = ScenesStateInner::<8>::new();
2076 push(&mut inner, entry(fab(1), 1, 10, 5, 100));
2077
2078 let status = ScenesHandler::<8>::copy_scenes_inner(
2079 &mut inner,
2080 fab(2), 1,
2082 10,
2083 5,
2084 20,
2085 7,
2086 false,
2087 );
2088
2089 assert_eq!(status, SC_NOT_FOUND);
2090 assert_eq!(count(&inner, fab(2), 1, 20), 0);
2092 assert_eq!(find_tt(&inner, fab(1), 1, 10, 5), Some(100));
2094 }
2095
2096 #[test]
2097 fn copy_does_not_cross_endpoint_boundary() {
2098 let mut inner = ScenesStateInner::<8>::new();
2099 push(&mut inner, entry(fab(1), 1, 10, 5, 100));
2100
2101 let status = ScenesHandler::<8>::copy_scenes_inner(
2102 &mut inner,
2103 fab(1),
2104 2, 10,
2106 5,
2107 20,
2108 7,
2109 false,
2110 );
2111
2112 assert_eq!(status, SC_NOT_FOUND);
2113 assert_eq!(count(&inner, fab(1), 2, 20), 0);
2114 }
2115
2116 #[test]
2119 fn successful_copy_invalidates_current_scene_on_match() {
2120 let mut inner = ScenesStateInner::<8>::new();
2121 push(&mut inner, entry(fab(1), 1, 10, 5, 100));
2122 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 20, 7);
2127 assert_eq!(inner.current_per_fabric.len(), 1);
2128
2129 let status =
2130 ScenesHandler::<8>::copy_scenes_inner(&mut inner, fab(1), 1, 10, 5, 20, 7, false);
2131
2132 assert_eq!(status, 0);
2133 let slot = inner
2136 .current_per_fabric
2137 .iter()
2138 .find(|c| c.fab_idx == fab(1))
2139 .expect("slot kept");
2140 assert!(!slot.valid);
2141 }
2142
2143 #[test]
2144 fn successful_copy_preserves_current_scene_when_target_doesnt_match() {
2145 let mut inner = ScenesStateInner::<8>::new();
2146 push(&mut inner, entry(fab(1), 1, 10, 5, 100));
2147 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 99, 99);
2152
2153 let status =
2154 ScenesHandler::<8>::copy_scenes_inner(&mut inner, fab(1), 1, 10, 5, 20, 7, false);
2155
2156 assert_eq!(status, 0);
2157 assert_eq!(inner.current_per_fabric.len(), 1);
2158 assert_eq!(inner.current_per_fabric[0].group_id, 99);
2159 assert_eq!(inner.current_per_fabric[0].scene_id, 99);
2160 assert!(inner.current_per_fabric[0].valid);
2161 }
2162
2163 #[test]
2164 fn failed_copy_does_not_invalidate_current_scene() {
2165 let mut inner = ScenesStateInner::<8>::new();
2166 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 99, 99);
2167 let dv_before = inner.info_dataver;
2168
2169 let status = ScenesHandler::<8>::copy_scenes_inner(
2170 &mut inner,
2171 fab(1),
2172 1,
2173 10, 5,
2175 20,
2176 7,
2177 false,
2178 );
2179
2180 assert_eq!(status, SC_NOT_FOUND);
2181 assert_eq!(inner.current_per_fabric.len(), 1);
2183 assert_eq!(inner.current_per_fabric[0].fab_idx, fab(1));
2184 assert_eq!(inner.current_per_fabric[0].group_id, 99);
2185 assert_eq!(inner.current_per_fabric[0].scene_id, 99);
2186 assert_eq!(inner.info_dataver, dv_before);
2188 }
2189
2190 #[test]
2193 fn remember_current_replaces_existing_slot_in_place() {
2194 let mut inner = ScenesStateInner::<8>::new();
2195 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 10, 1);
2196 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 20, 2);
2197
2198 assert_eq!(inner.current_per_fabric.len(), 1);
2200 assert_eq!(inner.current_per_fabric[0].group_id, 20);
2201 assert_eq!(inner.current_per_fabric[0].scene_id, 2);
2202 }
2203
2204 #[test]
2205 fn remember_current_keeps_fabrics_independent() {
2206 let mut inner = ScenesStateInner::<8>::new();
2207 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 10, 1);
2208 ScenesHandler::<8>::remember_current(&mut inner, fab(2), 1, 20, 2);
2209
2210 assert_eq!(inner.current_per_fabric.len(), 2);
2211 }
2212
2213 #[test]
2214 fn invalidate_match_scene_only_clears_exact_match() {
2215 let mut inner = ScenesStateInner::<8>::new();
2216 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 10, 1);
2217 ScenesHandler::<8>::remember_current(&mut inner, fab(2), 1, 20, 2);
2218
2219 ScenesHandler::<8>::invalidate_current_if_match_scene(&mut inner, fab(1), 99, 99);
2224 assert_eq!(inner.current_per_fabric.len(), 2);
2225 assert!(inner.current_per_fabric.iter().all(|c| c.valid));
2226
2227 ScenesHandler::<8>::invalidate_current_if_match_scene(&mut inner, fab(1), 10, 1);
2231 assert_eq!(inner.current_per_fabric.len(), 2);
2232 let f1 = inner
2233 .current_per_fabric
2234 .iter()
2235 .find(|c| c.fab_idx == fab(1))
2236 .unwrap();
2237 assert!(!f1.valid);
2238 let f2 = inner
2239 .current_per_fabric
2240 .iter()
2241 .find(|c| c.fab_idx == fab(2))
2242 .unwrap();
2243 assert!(f2.valid);
2244 }
2245
2246 #[test]
2247 fn invalidate_match_group_clears_any_scene_in_group() {
2248 let mut inner = ScenesStateInner::<8>::new();
2249 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 10, 7);
2250 ScenesHandler::<8>::remember_current(&mut inner, fab(2), 1, 20, 2);
2251
2252 ScenesHandler::<8>::invalidate_current_if_match_group(&mut inner, fab(1), 99);
2254 assert!(inner.current_per_fabric.iter().all(|c| c.valid));
2255
2256 ScenesHandler::<8>::invalidate_current_if_match_group(&mut inner, fab(1), 10);
2260 let f1 = inner
2261 .current_per_fabric
2262 .iter()
2263 .find(|c| c.fab_idx == fab(1))
2264 .unwrap();
2265 assert!(!f1.valid);
2266 let f2 = inner
2267 .current_per_fabric
2268 .iter()
2269 .find(|c| c.fab_idx == fab(2))
2270 .unwrap();
2271 assert!(f2.valid);
2272 }
2273
2274 fn fill_with<'a>(blob: &'a [u8]) -> impl FnOnce(&mut Vec<u8, 128>) -> Result<(), Error> + 'a {
2278 move |ext| {
2279 ext.extend_from_slice(blob)
2280 .map_err(|_| ErrorCode::NoSpace.into())
2281 }
2282 }
2283
2284 #[test]
2285 fn upsert_inserts_new_record_with_status_zero() {
2286 let mut inner = ScenesStateInner::<8>::new();
2287 let status = ScenesHandler::<8>::upsert_scene(
2288 &mut inner,
2289 fab(1),
2290 1,
2291 10,
2292 5,
2293 100,
2294 fill_with(&[0xAA, 0x18]),
2295 )
2296 .unwrap();
2297
2298 assert_eq!(status, 0);
2299 assert_eq!(inner.table.len(), 1);
2300 assert_eq!(find_tt(&inner, fab(1), 1, 10, 5), Some(100));
2301 assert_eq!(find_blob(&inner, fab(1), 1, 10, 5), Some(&[0xAA, 0x18][..]));
2302 }
2303
2304 #[test]
2305 fn upsert_replaces_existing_record_in_place_no_growth() {
2306 let mut inner = ScenesStateInner::<8>::new();
2307 push(
2308 &mut inner,
2309 entry_with_blob(fab(1), 1, 10, 5, 100, &[0xAA, 0x18]),
2310 );
2311
2312 let status = ScenesHandler::<8>::upsert_scene(
2313 &mut inner,
2314 fab(1),
2315 1,
2316 10,
2317 5,
2318 999,
2319 fill_with(&[0xBB, 0xCC, 0x18]),
2320 )
2321 .unwrap();
2322
2323 assert_eq!(status, 0);
2324 assert_eq!(inner.table.len(), 1, "replace must not grow the table");
2325 assert_eq!(find_tt(&inner, fab(1), 1, 10, 5), Some(999));
2326 assert_eq!(
2327 find_blob(&inner, fab(1), 1, 10, 5),
2328 Some(&[0xBB, 0xCC, 0x18][..])
2329 );
2330 }
2331
2332 #[test]
2333 fn upsert_returns_insufficient_space_when_table_is_full() {
2334 let mut inner = ScenesStateInner::<3>::new();
2336 inner.table.push(entry(fab(1), 1, 10, 1, 100)).unwrap();
2337 inner.table.push(entry(fab(1), 1, 10, 2, 100)).unwrap();
2338 inner.table.push(entry(fab(1), 1, 10, 3, 100)).unwrap();
2339
2340 let status = ScenesHandler::<3>::upsert_scene(
2341 &mut inner,
2342 fab(1),
2343 1,
2344 10,
2345 99, 200,
2347 fill_with(&[0x18]),
2348 )
2349 .unwrap();
2350
2351 assert_eq!(status, SC_INSUFFICIENT_SPACE);
2352 assert_eq!(inner.table.len(), 3, "table size unchanged on rejection");
2353 }
2354
2355 #[test]
2356 fn upsert_replace_at_full_capacity_still_succeeds() {
2357 let mut inner = ScenesStateInner::<3>::new();
2360 inner.table.push(entry(fab(1), 1, 10, 1, 100)).unwrap();
2361 inner.table.push(entry(fab(1), 1, 10, 2, 100)).unwrap();
2362 inner.table.push(entry(fab(1), 1, 10, 3, 100)).unwrap();
2363
2364 let status = ScenesHandler::<3>::upsert_scene(
2365 &mut inner,
2366 fab(1),
2367 1,
2368 10,
2369 2, 999,
2371 fill_with(&[0x18]),
2372 )
2373 .unwrap();
2374
2375 assert_eq!(status, 0);
2376 assert_eq!(inner.table.len(), 3);
2377 assert_eq!(find_tt(&inner, fab(1), 1, 10, 2), Some(999));
2378 }
2379
2380 #[test]
2381 fn upsert_invalidates_current_scene_when_upsert_targets_it() {
2382 let mut inner = ScenesStateInner::<8>::new();
2388 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 10, 5);
2389
2390 let _ =
2391 ScenesHandler::<8>::upsert_scene(&mut inner, fab(1), 1, 10, 5, 100, fill_with(&[0x18]))
2392 .unwrap();
2393
2394 let f1 = inner
2397 .current_per_fabric
2398 .iter()
2399 .find(|c| c.fab_idx == fab(1))
2400 .expect("slot kept");
2401 assert!(!f1.valid);
2402 }
2403
2404 #[test]
2405 fn upsert_preserves_current_scene_when_upsert_targets_a_different_scene() {
2406 let mut inner = ScenesStateInner::<8>::new();
2408 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 1, 1);
2409
2410 let _ =
2411 ScenesHandler::<8>::upsert_scene(&mut inner, fab(1), 1, 2, 1, 100, fill_with(&[0x18]))
2413 .unwrap();
2414
2415 assert_eq!(inner.current_per_fabric.len(), 1);
2416 assert_eq!(inner.current_per_fabric[0].group_id, 1);
2417 assert_eq!(inner.current_per_fabric[0].scene_id, 1);
2418 assert!(inner.current_per_fabric[0].valid);
2419 }
2420
2421 #[test]
2422 fn upsert_keeps_other_fabrics_current_scene_intact() {
2423 let mut inner = ScenesStateInner::<8>::new();
2424 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 10, 5);
2427 ScenesHandler::<8>::remember_current(&mut inner, fab(2), 1, 10, 5);
2428
2429 let _ =
2430 ScenesHandler::<8>::upsert_scene(&mut inner, fab(1), 1, 10, 5, 100, fill_with(&[0x18]))
2431 .unwrap();
2432
2433 let f1 = inner
2436 .current_per_fabric
2437 .iter()
2438 .find(|c| c.fab_idx == fab(1))
2439 .expect("fab(1) slot kept");
2440 assert!(!f1.valid);
2441 let f2 = inner
2442 .current_per_fabric
2443 .iter()
2444 .find(|c| c.fab_idx == fab(2))
2445 .expect("fab(2) slot kept");
2446 assert!(f2.valid, "fab(2)'s CurrentScene must not be touched");
2447 }
2448
2449 #[test]
2450 fn upsert_at_full_capacity_does_not_invalidate_current() {
2451 let mut inner = ScenesStateInner::<3>::new();
2454 inner.table.push(entry(fab(1), 1, 10, 1, 100)).unwrap();
2455 inner.table.push(entry(fab(1), 1, 10, 2, 100)).unwrap();
2456 inner.table.push(entry(fab(1), 1, 10, 3, 100)).unwrap();
2457 ScenesHandler::<3>::remember_current(&mut inner, fab(1), 1, 99, 99);
2458
2459 let status = ScenesHandler::<3>::upsert_scene(
2460 &mut inner,
2461 fab(1),
2462 1,
2463 10,
2464 99,
2465 200,
2466 fill_with(&[0x18]),
2467 )
2468 .unwrap();
2469
2470 assert_eq!(status, SC_INSUFFICIENT_SPACE);
2471 assert!(inner.current_per_fabric.iter().any(|c| c.fab_idx == fab(1)));
2472 }
2473
2474 #[test]
2475 fn upsert_fill_failure_on_new_entry_rolls_back_the_push() {
2476 let mut inner = ScenesStateInner::<8>::new();
2480 push(&mut inner, entry(fab(1), 1, 10, 1, 100));
2481
2482 let result = ScenesHandler::<8>::upsert_scene(
2483 &mut inner,
2484 fab(1),
2485 1,
2486 10,
2487 42, 200,
2489 |_| Err(ErrorCode::NoSpace.into()),
2490 );
2491
2492 assert!(result.is_err());
2493 assert_eq!(
2494 inner.table.len(),
2495 1,
2496 "rolled-back push leaves count untouched"
2497 );
2498 assert!(find_tt(&inner, fab(1), 1, 10, 42).is_none());
2499 }
2500}