1use core::future::{ready, Future};
36use core::num::NonZeroU8;
37
38use crate::dm::{
39 ArrayAttributeRead, AttrId, Cluster, ClusterId, Dataver, EndptId, HandlerContext,
40 InvokeContext, ReadContext, SceneId,
41};
42use crate::error::{Error, ErrorCode};
43use crate::persist::{KvBlobStore, Persist};
44use crate::tlv::{
45 FromTLV, Nullable, OptionalBuilder, TLVArray, TLVBuilder, TLVBuilderParent, TLVElement,
46 TLVSequence, TLVTag, TLVWrite, TLVWriteParent, ToTLV, TLV,
47};
48use crate::utils::cell::RefCell;
49use crate::utils::init::{init, Init};
50use crate::utils::storage::{Vec, WriteBuf};
51use crate::utils::sync::blocking::Mutex;
52
53pub use crate::dm::clusters::decl::scenes_management::*;
54pub use crate::persist::SCENES_KEY;
55
56const SC_NOT_FOUND: u8 = 0x8B;
59const SC_INSUFFICIENT_SPACE: u8 = 0x89;
60const SC_INVALID_COMMAND: u8 = 0x85;
61const SC_CONSTRAINT_ERROR: u8 = 0x87;
62
63const RESERVED_SCENE_ID: SceneId = 0xFF;
65
66const GLOBAL_SCENE_ID: SceneId = 0;
69
70const MAX_TRANSITION_TIME_MS: u32 = 60_000_000;
73
74pub const MAX_EXT_FIELDS_LEN: usize = 128;
80
81pub trait SceneClusterHandler {
91 const CLUSTER_ID: ClusterId;
93
94 fn endpoint_id(&self) -> EndptId;
97
98 fn is_scenable_attribute(_attribute_id: AttrId) -> bool {
102 false
103 }
104
105 fn capture<P: TLVBuilderParent>(
110 &self,
111 avp_array: AttributeValuePairStructArrayBuilder<P>,
112 ) -> Result<AttributeValuePairStructArrayBuilder<P>, Error>;
113
114 fn apply<C: HandlerContext>(
130 &self,
131 ctx: &C,
132 avp_list: &TLVArray<'_, AttributeValuePairStruct<'_>>,
133 transition_time_ms: u32,
134 ) -> impl Future<Output = Result<(), Error>>;
135}
136
137impl<T: SceneClusterHandler + ?Sized> SceneClusterHandler for &T {
141 const CLUSTER_ID: ClusterId = T::CLUSTER_ID;
142
143 fn endpoint_id(&self) -> EndptId {
144 T::endpoint_id(*self)
145 }
146
147 fn is_scenable_attribute(attribute_id: AttrId) -> bool {
148 T::is_scenable_attribute(attribute_id)
149 }
150
151 fn capture<P: TLVBuilderParent>(
152 &self,
153 avp_array: AttributeValuePairStructArrayBuilder<P>,
154 ) -> Result<AttributeValuePairStructArrayBuilder<P>, Error> {
155 T::capture(*self, avp_array)
156 }
157
158 async fn apply<C: HandlerContext>(
159 &self,
160 ctx: &C,
161 avp_list: &TLVArray<'_, AttributeValuePairStruct<'_>>,
162 transition_time_ms: u32,
163 ) -> Result<(), Error> {
164 T::apply(*self, ctx, avp_list, transition_time_ms).await
165 }
166}
167
168pub trait SceneClusters {
172 fn capture<P: TLVBuilderParent>(&self, endpoint_id: EndptId, parent: P) -> Result<P, Error>;
177
178 fn check_scenable(&self, cluster_id: ClusterId, attribute_id: AttrId) -> Option<bool>;
185
186 fn apply<C: HandlerContext>(
190 &self,
191 ctx: &C,
192 endpoint_id: EndptId,
193 cluster_id: ClusterId,
194 avp_list: &TLVArray<'_, AttributeValuePairStruct<'_>>,
195 transition_time_ms: u32,
196 ) -> impl Future<Output = Result<bool, Error>>;
197}
198
199impl SceneClusters for () {
200 fn capture<P: TLVBuilderParent>(&self, _endpoint_id: EndptId, parent: P) -> Result<P, Error> {
201 Ok(parent)
202 }
203
204 fn check_scenable(&self, _cluster_id: ClusterId, _attribute_id: AttrId) -> Option<bool> {
205 None
206 }
207
208 fn apply<C: HandlerContext>(
209 &self,
210 _ctx: &C,
211 _endpoint_id: EndptId,
212 _cluster_id: ClusterId,
213 _avp_list: &TLVArray<'_, AttributeValuePairStruct<'_>>,
214 _transition_time_ms: u32,
215 ) -> impl Future<Output = Result<bool, Error>> {
216 ready(Ok(false))
217 }
218}
219
220impl<H, T> SceneClusters for (H, T)
221where
222 H: SceneClusterHandler,
223 T: SceneClusters,
224{
225 fn check_scenable(&self, cluster_id: ClusterId, attribute_id: AttrId) -> Option<bool> {
226 if cluster_id == H::CLUSTER_ID {
227 Some(H::is_scenable_attribute(attribute_id))
228 } else {
229 self.1.check_scenable(cluster_id, attribute_id)
230 }
231 }
232
233 fn capture<P: TLVBuilderParent>(&self, endpoint_id: EndptId, parent: P) -> Result<P, Error> {
234 let parent = if self.0.endpoint_id() == endpoint_id {
235 let efs = ExtensionFieldSetStructBuilder::new(parent, &TLVTag::Anonymous)?;
236 let efs = efs.cluster_id(H::CLUSTER_ID)?;
237 let avp_array = efs.attribute_value_list()?;
238 let avp_array = self.0.capture(avp_array)?;
239 let efs = avp_array.end()?;
240 efs.end()?
241 } else {
242 parent
243 };
244 self.1.capture(endpoint_id, parent)
245 }
246
247 async fn apply<C: HandlerContext>(
248 &self,
249 ctx: &C,
250 endpoint_id: EndptId,
251 cluster_id: ClusterId,
252 avp_list: &TLVArray<'_, AttributeValuePairStruct<'_>>,
253 transition_time_ms: u32,
254 ) -> Result<bool, Error> {
255 if H::CLUSTER_ID == cluster_id && self.0.endpoint_id() == endpoint_id {
256 self.0.apply(ctx, avp_list, transition_time_ms).await?;
257 Ok(true)
258 } else {
259 self.1
260 .apply(ctx, endpoint_id, cluster_id, avp_list, transition_time_ms)
261 .await
262 }
263 }
264}
265
266impl<P> AttributeValuePairStructArrayBuilder<P>
270where
271 P: TLVBuilderParent,
272{
273 pub fn push_u8(self, attr_id: AttrId, value: u8) -> Result<Self, Error> {
275 self.push()?
276 .attribute_id(attr_id)?
277 .value_unsigned_8(Some(value))?
278 .value_signed_8(None)?
279 .value_unsigned_16(None)?
280 .value_signed_16(None)?
281 .value_unsigned_32(None)?
282 .value_signed_32(None)?
283 .value_unsigned_64(None)?
284 .value_signed_64(None)?
285 .end()
286 }
287
288 pub fn push_u16(self, attr_id: AttrId, value: u16) -> Result<Self, Error> {
290 self.push()?
291 .attribute_id(attr_id)?
292 .value_unsigned_8(None)?
293 .value_signed_8(None)?
294 .value_unsigned_16(Some(value))?
295 .value_signed_16(None)?
296 .value_unsigned_32(None)?
297 .value_signed_32(None)?
298 .value_unsigned_64(None)?
299 .value_signed_64(None)?
300 .end()
301 }
302}
303
304#[derive(Debug)]
310#[cfg_attr(feature = "defmt", derive(defmt::Format))]
311pub struct SceneEntry<const M: usize = MAX_EXT_FIELDS_LEN> {
312 fab_idx: NonZeroU8,
313 endpoint_id: EndptId,
314 group_id: u16,
315 scene_id: SceneId,
316 transition_time: u32,
318 extension_fields: Vec<u8, M>,
323}
324
325impl<const M: usize> SceneEntry<M> {
326 fn matches(
327 &self,
328 fab_idx: NonZeroU8,
329 endpoint_id: EndptId,
330 group_id: u16,
331 scene_id: SceneId,
332 ) -> bool {
333 self.fab_idx == fab_idx
334 && self.endpoint_id == endpoint_id
335 && self.group_id == group_id
336 && self.scene_id == scene_id
337 }
338
339 fn init(
344 fab_idx: NonZeroU8,
345 endpoint_id: EndptId,
346 group_id: u16,
347 scene_id: SceneId,
348 transition_time: u32,
349 ) -> impl Init<Self> {
350 init!(Self {
351 fab_idx,
352 endpoint_id,
353 group_id,
354 scene_id,
355 transition_time,
356 extension_fields <- Vec::init(),
357 })
358 }
359}
360
361#[derive(Debug, Clone, Copy, FromTLV, ToTLV)]
370#[cfg_attr(feature = "defmt", derive(defmt::Format))]
371struct CurrentScene {
372 fab_idx: NonZeroU8,
373 endpoint_id: EndptId,
374 group_id: u16,
375 scene_id: SceneId,
376 valid: bool,
377}
378
379struct ScenesStateInner<const N: usize, const M: usize = MAX_EXT_FIELDS_LEN> {
382 table: Vec<SceneEntry<M>, N>,
384 current_per_fabric: Vec<CurrentScene, N>,
386 info_dataver: u32,
388}
389
390impl<const N: usize, const M: usize> ScenesStateInner<N, M> {
391 const fn new() -> Self {
392 Self {
393 table: Vec::new(),
394 current_per_fabric: Vec::new(),
395 info_dataver: 0,
396 }
397 }
398
399 fn init() -> impl Init<Self> {
400 init!(Self {
401 table <- Vec::init(),
402 current_per_fabric <- Vec::init(),
403 info_dataver: 0,
404 })
405 }
406
407 fn bump_info_dataver(&mut self) {
408 self.info_dataver = self.info_dataver.wrapping_add(1);
409 }
410}
411
412pub struct ScenesState<const N: usize, const M: usize = MAX_EXT_FIELDS_LEN> {
424 inner: Mutex<RefCell<ScenesStateInner<N, M>>>,
425}
426
427impl<const N: usize, const M: usize> ScenesState<N, M> {
428 pub const fn new() -> Self {
429 Self {
430 inner: Mutex::new(RefCell::new(ScenesStateInner::new())),
431 }
432 }
433
434 pub fn init() -> impl Init<Self> {
436 init!(Self {
437 inner <- Mutex::init(RefCell::init(ScenesStateInner::init())),
438 })
439 }
440
441 fn with<F, R>(&self, f: F) -> R
443 where
444 F: FnOnce(&mut ScenesStateInner<N, M>) -> R,
445 {
446 self.inner.lock(|cell| {
447 let mut inner = cell.borrow_mut();
448 f(&mut inner)
449 })
450 }
451}
452
453impl<const N: usize, const M: usize> Default for ScenesState<N, M> {
454 fn default() -> Self {
455 Self::new()
456 }
457}
458
459pub trait SceneInvalidator {
474 fn scenable_attribute_changed(&self, endpoint_id: EndptId);
478}
479
480impl<T: SceneInvalidator + ?Sized> SceneInvalidator for &T {
481 fn scenable_attribute_changed(&self, endpoint_id: EndptId) {
482 (**self).scenable_attribute_changed(endpoint_id);
483 }
484}
485
486impl<const N: usize, const M: usize> SceneInvalidator for ScenesState<N, M> {
487 fn scenable_attribute_changed(&self, endpoint_id: EndptId) {
488 self.with(|inner| {
489 let mut bumped = false;
490 for c in inner.current_per_fabric.iter_mut() {
491 if c.valid && c.endpoint_id == endpoint_id {
492 c.valid = false;
493 bumped = true;
494 }
495 }
496 if bumped {
497 inner.bump_info_dataver();
498 }
499 });
500 }
501}
502
503impl<const M: usize> ToTLV for SceneEntry<M> {
514 fn to_tlv<W: TLVWrite>(&self, tag: &TLVTag, mut tw: W) -> Result<(), Error> {
515 tw.start_struct(tag)?;
516 self.fab_idx.to_tlv(&TLVTag::Context(0), &mut tw)?;
517 self.endpoint_id.to_tlv(&TLVTag::Context(1), &mut tw)?;
518 self.group_id.to_tlv(&TLVTag::Context(2), &mut tw)?;
519 self.scene_id.to_tlv(&TLVTag::Context(3), &mut tw)?;
520 self.transition_time.to_tlv(&TLVTag::Context(4), &mut tw)?;
521 tw.str(&TLVTag::Context(5), &self.extension_fields)?;
525 tw.end_container()
526 }
527
528 fn tlv_iter(&self, _tag: TLVTag) -> impl Iterator<Item = Result<TLV<'_>, Error>> {
529 core::iter::empty()
532 }
533}
534
535impl<'a, const M: usize> FromTLV<'a> for SceneEntry<M> {
536 fn from_tlv(element: &TLVElement<'a>) -> Result<Self, Error> {
537 let s = element.structure()?;
538 let mut extension_fields = Vec::<u8, M>::new();
539 extension_fields
540 .extend_from_slice(s.ctx(5)?.str()?)
541 .map_err(|_| ErrorCode::NoSpace)?;
542 Ok(Self {
543 fab_idx: NonZeroU8::from_tlv(&s.ctx(0)?)?,
544 endpoint_id: EndptId::from_tlv(&s.ctx(1)?)?,
545 group_id: u16::from_tlv(&s.ctx(2)?)?,
546 scene_id: SceneId::from_tlv(&s.ctx(3)?)?,
547 transition_time: u32::from_tlv(&s.ctx(4)?)?,
548 extension_fields,
549 })
550 }
551}
552
553impl<const N: usize, const M: usize> ToTLV for ScenesStateInner<N, M> {
554 fn to_tlv<W: TLVWrite>(&self, tag: &TLVTag, mut tw: W) -> Result<(), Error> {
555 tw.start_struct(tag)?;
556 self.table.to_tlv(&TLVTag::Context(0), &mut tw)?;
557 self.current_per_fabric
558 .to_tlv(&TLVTag::Context(1), &mut tw)?;
559 tw.end_container()
560 }
561
562 fn tlv_iter(&self, _tag: TLVTag) -> impl Iterator<Item = Result<TLV<'_>, Error>> {
563 core::iter::empty()
564 }
565}
566
567impl<'a, const N: usize, const M: usize> FromTLV<'a> for ScenesStateInner<N, M> {
568 fn from_tlv(element: &TLVElement<'a>) -> Result<Self, Error> {
569 let s = element.structure()?;
570 Ok(Self {
571 table: Vec::<SceneEntry<M>, N>::from_tlv(&s.ctx(0)?)?,
572 current_per_fabric: Vec::<CurrentScene, N>::from_tlv(&s.ctx(1)?)?,
573 info_dataver: 0,
574 })
575 }
576}
577
578impl<const N: usize, const M: usize> ScenesState<N, M> {
579 pub async fn load_persist<S: KvBlobStore>(
585 &self,
586 mut store: S,
587 buf: &mut [u8],
588 ) -> Result<(), Error> {
589 let Some(data) = store.load(SCENES_KEY, buf)? else {
590 self.with(|inner| {
593 inner.table.clear();
594 inner.current_per_fabric.clear();
595 });
596 return Ok(());
597 };
598
599 let loaded = ScenesStateInner::<N, M>::from_tlv(&TLVElement::new(data))?;
600 let entries = loaded.table.len();
601
602 self.with(|inner| {
603 inner.table = loaded.table;
604 inner.current_per_fabric = loaded.current_per_fabric;
605 inner.bump_info_dataver();
606 });
607
608 info!("Loaded Scenes state from storage ({} entries)", entries);
609
610 Ok(())
611 }
612
613 fn store_persist<C: HandlerContext>(&self, ctx: &C) -> Result<(), Error> {
617 let mut persist = Persist::new(ctx.kv());
618
619 self.inner.lock(|cell| {
620 let inner = cell.borrow();
621 persist.store_tlv(SCENES_KEY, &*inner)
622 })?;
623
624 persist.run()
625 }
626}
627
628pub struct ScenesHandler<'a, const N: usize, R = (), const M: usize = MAX_EXT_FIELDS_LEN>
646where
647 R: SceneClusters,
648{
649 dataver: Dataver,
650 state: &'a ScenesState<N, M>,
651 clusters: R,
652}
653
654impl<'a, const N: usize, R, const M: usize> ScenesHandler<'a, N, R, M>
655where
656 R: SceneClusters,
657{
658 pub const fn new(dataver: Dataver, state: &'a ScenesState<N, M>, clusters: R) -> Self {
659 Self {
660 dataver,
661 state,
662 clusters,
663 }
664 }
665
666 pub const fn adapt(self) -> HandlerAsyncAdaptor<Self> {
667 HandlerAsyncAdaptor(self)
668 }
669
670 fn fab_idx<C: InvokeContext>(ctx: &C) -> Result<NonZeroU8, Error> {
671 ctx.exchange().accessor()?.fab_idx()
672 }
673
674 fn remaining_capacity_for_fab(inner: &ScenesStateInner<N, M>, fab_idx: NonZeroU8) -> u8 {
679 let per_fab_budget = N.saturating_sub(1) / 2;
680 let used = inner.table.iter().filter(|e| e.fab_idx == fab_idx).count();
681 let per_fab_remaining = per_fab_budget.saturating_sub(used);
682 let global_remaining = N.saturating_sub(inner.table.len());
683 per_fab_remaining.min(global_remaining).min(0xFF) as u8
684 }
685
686 fn group_in_table<C: InvokeContext>(
691 ctx: &C,
692 fab_idx: NonZeroU8,
693 endpoint_id: EndptId,
694 group_id: u16,
695 ) -> Result<bool, Error> {
696 if group_id == 0 {
697 return Ok(true);
698 }
699 ctx.exchange().with_state(|state| {
700 let fabric = state.fabrics.fabric(fab_idx)?;
701 Ok(fabric
702 .groups()
703 .get(group_id)
704 .map(|g| g.endpoints.contains(&endpoint_id))
705 .unwrap_or(false))
706 })
707 }
708
709 fn remember_current(
713 inner: &mut ScenesStateInner<N, M>,
714 fab_idx: NonZeroU8,
715 endpoint_id: EndptId,
716 group_id: u16,
717 scene_id: SceneId,
718 ) {
719 if let Some(slot) = inner
720 .current_per_fabric
721 .iter_mut()
722 .find(|c| c.fab_idx == fab_idx)
723 {
724 slot.endpoint_id = endpoint_id;
725 slot.group_id = group_id;
726 slot.scene_id = scene_id;
727 slot.valid = true;
728 } else {
729 let _ = inner.current_per_fabric.push(CurrentScene {
733 fab_idx,
734 endpoint_id,
735 group_id,
736 scene_id,
737 valid: true,
738 });
739 }
740 inner.bump_info_dataver();
741 }
742
743 fn invalidate_current_if_match_scene(
750 inner: &mut ScenesStateInner<N, M>,
751 fab_idx: NonZeroU8,
752 group_id: u16,
753 scene_id: SceneId,
754 ) {
755 let mut bumped = false;
756 for c in inner.current_per_fabric.iter_mut() {
757 if c.valid && c.fab_idx == fab_idx && c.group_id == group_id && c.scene_id == scene_id {
758 c.valid = false;
759 bumped = true;
760 }
761 }
762 if bumped {
763 inner.bump_info_dataver();
764 }
765 }
766
767 fn invalidate_current_if_match_group(
773 inner: &mut ScenesStateInner<N, M>,
774 fab_idx: NonZeroU8,
775 group_id: u16,
776 ) {
777 let mut bumped = false;
778 for c in inner.current_per_fabric.iter_mut() {
779 if c.valid && c.fab_idx == fab_idx && c.group_id == group_id {
780 c.valid = false;
781 bumped = true;
782 }
783 }
784 if bumped {
785 inner.bump_info_dataver();
786 }
787 }
788
789 #[allow(clippy::too_many_arguments)]
796 fn copy_scenes_inner(
797 inner: &mut ScenesStateInner<N, M>,
798 fab_idx: NonZeroU8,
799 endpoint_id: EndptId,
800 group_from: u16,
801 scene_from: SceneId,
802 group_to: u16,
803 scene_to: SceneId,
804 copy_all: bool,
805 ) -> u8 {
806 if Self::remaining_capacity_for_fab(inner, fab_idx) == 0 {
811 return SC_INSUFFICIENT_SPACE;
812 }
813
814 let mut found_source = false;
815 let mut idx = 0;
816 while idx < inner.table.len() {
817 let src = &inner.table[idx];
818 let src_matches = src.fab_idx == fab_idx
819 && src.endpoint_id == endpoint_id
820 && src.group_id == group_from
821 && (copy_all || src.scene_id == scene_from);
822 if src_matches {
823 found_source = true;
824 let src_scene_id = src.scene_id;
827 let src_transition_time = src.transition_time;
828 let src_extension_fields = src.extension_fields.clone();
829 let target_scene_id = if copy_all { src_scene_id } else { scene_to };
830
831 if let Some(pos) = inner
833 .table
834 .iter()
835 .position(|e| e.matches(fab_idx, endpoint_id, group_to, target_scene_id))
836 {
837 inner.table[pos].transition_time = src_transition_time;
838 inner.table[pos].extension_fields = src_extension_fields;
839 } else {
840 if Self::remaining_capacity_for_fab(inner, fab_idx) == 0 {
844 return SC_INSUFFICIENT_SPACE;
845 }
846
847 if inner
848 .table
849 .push(SceneEntry {
850 fab_idx,
851 endpoint_id,
852 group_id: group_to,
853 scene_id: target_scene_id,
854 transition_time: src_transition_time,
855 extension_fields: src_extension_fields,
856 })
857 .is_err()
858 {
859 return SC_INSUFFICIENT_SPACE;
860 }
861 }
862
863 if !copy_all {
865 break;
866 }
867 }
868 idx += 1;
869 }
870
871 if !found_source {
872 return SC_NOT_FOUND;
873 }
874
875 if copy_all {
878 Self::invalidate_current_if_match_group(inner, fab_idx, group_to);
879 } else {
880 Self::invalidate_current_if_match_scene(inner, fab_idx, group_to, scene_to);
881 }
882 0
883 }
884
885 fn read_fabric_scene_info<P: TLVBuilderParent>(
892 &self,
893 ctx: &impl ReadContext,
894 builder: ArrayAttributeRead<SceneInfoStructArrayBuilder<P>, SceneInfoStructBuilder<P>>,
895 ) -> Result<P, Error> {
896 let endpoint_id = ctx.attr().endpoint_id;
897 let accessor_fab_idx = ctx.exchange().accessor()?.fab_idx()?;
898
899 let (has_state, scene_count, cur_group, cur_scene, valid, remaining) =
905 self.state.with(|inner| {
906 let count = inner
907 .table
908 .iter()
909 .filter(|e| e.fab_idx == accessor_fab_idx && e.endpoint_id == endpoint_id)
910 .count();
911 let current = inner
912 .current_per_fabric
913 .iter()
914 .find(|c| c.fab_idx == accessor_fab_idx)
915 .copied();
916 let has_state = count > 0 || current.is_some();
917 let (g, s, v) = match current {
921 Some(c) => (Some(c.group_id), Some(c.scene_id), c.valid),
922 None => (Some(0u16), Some(0u8), false),
923 };
924 let rem = Self::remaining_capacity_for_fab(inner, accessor_fab_idx);
925 (has_state, count.min(0xFF) as u8, g, s, v, rem)
926 });
927
928 match builder {
929 ArrayAttributeRead::ReadAll(arr) => {
930 if !has_state {
931 return arr.end();
932 }
933
934 let arr = arr
935 .push()?
936 .scene_count(scene_count)?
937 .current_scene(cur_scene)?
938 .current_group(cur_group)?
939 .scene_valid(Some(valid))?
940 .remaining_capacity(remaining)?
941 .fabric_index(Some(accessor_fab_idx.get()))?
942 .end()?;
943
944 arr.end()
945 }
946 ArrayAttributeRead::ReadNone(arr) => arr.end(),
947 ArrayAttributeRead::ReadOne(_idx, _entry) => {
948 Err(ErrorCode::AttributeNotFound.into())
951 }
952 }
953 }
954
955 fn add_scene<P: TLVBuilderParent>(
956 &self,
957 ctx: &impl InvokeContext,
958 request: &AddSceneRequest<'_>,
959 response: AddSceneResponseBuilder<P>,
960 ) -> Result<P, Error> {
961 let fab_idx = Self::fab_idx(ctx)?;
962 let endpoint_id = ctx.cmd().endpoint_id;
963 let group_id = request.group_id()?;
964 let scene_id = request.scene_id()?;
965 let transition_time = request.transition_time()?;
966
967 if scene_id == GLOBAL_SCENE_ID
970 || scene_id == RESERVED_SCENE_ID
971 || transition_time > MAX_TRANSITION_TIME_MS
972 {
973 return response
974 .status(SC_CONSTRAINT_ERROR)?
975 .group_id(group_id)?
976 .scene_id(scene_id)?
977 .end();
978 }
979
980 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_id)? {
981 return response
982 .status(SC_INVALID_COMMAND)?
983 .group_id(group_id)?
984 .scene_id(scene_id)?
985 .end();
986 }
987
988 let efs_array_opt = request.extension_field_set_structs().ok();
994 let raw = match efs_array_opt {
995 Some(ref array) => array.element().raw_value()?,
996 None => &[],
997 };
998
999 if let Some(ref efs_array) = efs_array_opt {
1005 for efs in efs_array.iter() {
1006 let efs = efs?;
1007 let cid = efs.cluster_id()?;
1008 for avp in efs.attribute_value_list()?.iter() {
1009 let avp = avp?;
1010 let aid = avp.attribute_id()?;
1011 if let Some(false) = self.clusters.check_scenable(cid, aid) {
1012 return response
1013 .status(SC_INVALID_COMMAND)?
1014 .group_id(group_id)?
1015 .scene_id(scene_id)?
1016 .end();
1017 }
1018 }
1019 }
1020 }
1021
1022 if raw.len() > M {
1025 return response
1026 .status(SC_INSUFFICIENT_SPACE)?
1027 .group_id(group_id)?
1028 .scene_id(scene_id)?
1029 .end();
1030 }
1031
1032 let status_code = self.state.with(|inner| {
1033 Self::upsert_scene(
1034 inner,
1035 fab_idx,
1036 endpoint_id,
1037 group_id,
1038 scene_id,
1039 transition_time,
1040 |ext_fields| {
1041 if !raw.is_empty() {
1042 ext_fields
1043 .extend_from_slice(raw)
1044 .map_err(|_| ErrorCode::NoSpace)?;
1045 }
1046 Ok(())
1047 },
1048 )
1049 })?;
1050
1051 if status_code == 0 {
1052 self.state.store_persist(ctx)?;
1053 ctx.notify_own_attr_changed(AttributeId::FabricSceneInfo as _);
1054 }
1055
1056 response
1057 .status(status_code)?
1058 .group_id(group_id)?
1059 .scene_id(scene_id)?
1060 .end()
1061 }
1062
1063 fn upsert_scene<F>(
1074 inner: &mut ScenesStateInner<N, M>,
1075 fab_idx: NonZeroU8,
1076 endpoint_id: EndptId,
1077 group_id: u16,
1078 scene_id: SceneId,
1079 transition_time: u32,
1080 fill: F,
1081 ) -> Result<u8, Error>
1082 where
1083 F: FnOnce(&mut Vec<u8, M>) -> Result<(), Error>,
1084 {
1085 if let Some(pos) = inner
1086 .table
1087 .iter()
1088 .position(|e| e.matches(fab_idx, endpoint_id, group_id, scene_id))
1089 {
1090 inner.table[pos].transition_time = transition_time;
1092 inner.table[pos].extension_fields.clear();
1093 fill(&mut inner.table[pos].extension_fields)?;
1094 Self::invalidate_current_if_match_scene(inner, fab_idx, group_id, scene_id);
1095 Ok(0)
1096 } else if inner.table.len() >= N {
1097 Ok(SC_INSUFFICIENT_SPACE)
1098 } else {
1099 inner
1102 .table
1103 .push_init_unchecked(SceneEntry::init(
1104 fab_idx,
1105 endpoint_id,
1106 group_id,
1107 scene_id,
1108 transition_time,
1109 ))
1110 .unwrap();
1111 let pos = inner.table.len() - 1;
1112 if let Err(e) = fill(&mut inner.table[pos].extension_fields) {
1113 let _ = inner.table.pop();
1114 return Err(e);
1115 }
1116 Self::invalidate_current_if_match_scene(inner, fab_idx, group_id, scene_id);
1117 Ok(0)
1118 }
1119 }
1120
1121 fn view_scene<P: TLVBuilderParent>(
1122 &self,
1123 ctx: &impl InvokeContext,
1124 request: &ViewSceneRequest<'_>,
1125 response: ViewSceneResponseBuilder<P>,
1126 ) -> Result<P, Error> {
1127 let fab_idx = Self::fab_idx(ctx)?;
1128 let endpoint_id = ctx.cmd().endpoint_id;
1129 let group_id = request.group_id()?;
1130 let scene_id = request.scene_id()?;
1131
1132 if scene_id == GLOBAL_SCENE_ID || scene_id == RESERVED_SCENE_ID {
1134 return response
1135 .status(SC_CONSTRAINT_ERROR)?
1136 .group_id(group_id)?
1137 .scene_id(scene_id)?
1138 .transition_time(None)?
1139 .scene_name(None)?
1140 .extension_field_set_structs()?
1141 .none()
1142 .end();
1143 }
1144
1145 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_id)? {
1146 return response
1147 .status(SC_INVALID_COMMAND)?
1148 .group_id(group_id)?
1149 .scene_id(scene_id)?
1150 .transition_time(None)?
1151 .scene_name(None)?
1152 .extension_field_set_structs()?
1153 .none()
1154 .end();
1155 }
1156
1157 self.state.with(|inner| -> Result<P, Error> {
1161 let entry = inner
1162 .table
1163 .iter()
1164 .find(|e| e.matches(fab_idx, endpoint_id, group_id, scene_id));
1165
1166 let Some(e) = entry else {
1167 return response
1168 .status(SC_NOT_FOUND)?
1169 .group_id(group_id)?
1170 .scene_id(scene_id)?
1171 .transition_time(None)?
1172 .scene_name(None)?
1173 .extension_field_set_structs()?
1174 .none()
1175 .end();
1176 };
1177
1178 let opt = response
1179 .status(0)?
1180 .group_id(group_id)?
1181 .scene_id(scene_id)?
1182 .transition_time(Some(e.transition_time))?
1183 .scene_name(Some(""))?
1184 .extension_field_set_structs()?;
1185
1186 Self::write_blob_or_none(opt, &e.extension_fields)?.end()
1187 })
1188 }
1189
1190 fn write_blob_or_none<P, Q>(mut opt: OptionalBuilder<P, Q>, blob: &[u8]) -> Result<P, Error>
1195 where
1196 P: TLVBuilderParent,
1197 Q: TLVBuilder<P>,
1198 {
1199 if !blob.is_empty() {
1200 let writer = opt.writer();
1201 writer.start_array(&TLVTag::Context(5))?;
1202 writer.write_raw_data(blob.iter().copied())?;
1203 }
1204 Ok(opt.none())
1205 }
1206
1207 fn remove_scene<P: TLVBuilderParent>(
1208 &self,
1209 ctx: &impl InvokeContext,
1210 request: &RemoveSceneRequest<'_>,
1211 response: RemoveSceneResponseBuilder<P>,
1212 ) -> Result<P, Error> {
1213 let fab_idx = Self::fab_idx(ctx)?;
1214 let endpoint_id = ctx.cmd().endpoint_id;
1215 let group_id = request.group_id()?;
1216 let scene_id = request.scene_id()?;
1217
1218 if scene_id == GLOBAL_SCENE_ID || scene_id == RESERVED_SCENE_ID {
1220 return response
1221 .status(SC_CONSTRAINT_ERROR)?
1222 .group_id(group_id)?
1223 .scene_id(scene_id)?
1224 .end();
1225 }
1226
1227 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_id)? {
1228 return response
1229 .status(SC_INVALID_COMMAND)?
1230 .group_id(group_id)?
1231 .scene_id(scene_id)?
1232 .end();
1233 }
1234
1235 let status: u8 = self.state.with(|inner| {
1236 if let Some(pos) = inner
1237 .table
1238 .iter()
1239 .position(|e| e.matches(fab_idx, endpoint_id, group_id, scene_id))
1240 {
1241 inner.table.swap_remove(pos);
1242 Self::invalidate_current_if_match_scene(inner, fab_idx, group_id, scene_id);
1243 0
1244 } else {
1245 SC_NOT_FOUND
1246 }
1247 });
1248
1249 if status == 0 {
1250 self.state.store_persist(ctx)?;
1251 ctx.notify_own_attr_changed(AttributeId::FabricSceneInfo as _);
1252 }
1253
1254 response
1255 .status(status)?
1256 .group_id(group_id)?
1257 .scene_id(scene_id)?
1258 .end()
1259 }
1260
1261 fn remove_all_scenes<P: TLVBuilderParent>(
1262 &self,
1263 ctx: &impl InvokeContext,
1264 request: &RemoveAllScenesRequest<'_>,
1265 response: RemoveAllScenesResponseBuilder<P>,
1266 ) -> Result<P, Error> {
1267 let fab_idx = Self::fab_idx(ctx)?;
1268 let endpoint_id = ctx.cmd().endpoint_id;
1269 let group_id = request.group_id()?;
1270
1271 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_id)? {
1272 return response
1273 .status(SC_INVALID_COMMAND)?
1274 .group_id(group_id)?
1275 .end();
1276 }
1277
1278 let removed = self.state.with(|inner| {
1279 let before = inner.table.len();
1280 inner.table.retain(|e| {
1281 !(e.fab_idx == fab_idx && e.endpoint_id == endpoint_id && e.group_id == group_id)
1282 });
1283 let changed = before != inner.table.len();
1284 if changed {
1285 Self::invalidate_current_if_match_group(inner, fab_idx, group_id);
1286 }
1287 changed
1288 });
1289
1290 if removed {
1291 self.state.store_persist(ctx)?;
1292 ctx.notify_own_attr_changed(AttributeId::FabricSceneInfo as _);
1293 }
1294
1295 response.status(0)?.group_id(group_id)?.end()
1296 }
1297
1298 async fn store_scene<P: TLVBuilderParent>(
1302 &self,
1303 ctx: &impl InvokeContext,
1304 request: &StoreSceneRequest<'_>,
1305 response: StoreSceneResponseBuilder<P>,
1306 ) -> Result<P, Error> {
1307 let fab_idx = Self::fab_idx(ctx)?;
1308 let endpoint_id = ctx.cmd().endpoint_id;
1309 let group_id = request.group_id()?;
1310 let scene_id = request.scene_id()?;
1311
1312 if scene_id == GLOBAL_SCENE_ID || scene_id == RESERVED_SCENE_ID {
1314 return response
1315 .status(SC_CONSTRAINT_ERROR)?
1316 .group_id(group_id)?
1317 .scene_id(scene_id)?
1318 .end();
1319 }
1320
1321 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_id)? {
1322 return response
1323 .status(SC_INVALID_COMMAND)?
1324 .group_id(group_id)?
1325 .scene_id(scene_id)?
1326 .end();
1327 }
1328
1329 let mut scratch = [0u8; M];
1335 let total_len = {
1336 let mut wb = WriteBuf::new(&mut scratch);
1337 let parent = TLVWriteParent::new("StoreScene EFS", &mut wb);
1338 let _ = self.clusters.capture(endpoint_id, parent)?;
1339 wb.end_container()?;
1340 wb.get_tail()
1341 };
1342 let stored_bytes = &scratch[..total_len];
1343
1344 let prior_tt = self.state.with(|inner| {
1347 inner
1348 .table
1349 .iter()
1350 .find(|e| e.matches(fab_idx, endpoint_id, group_id, scene_id))
1351 .map(|e| e.transition_time)
1352 });
1353 let transition_time = prior_tt.unwrap_or(0);
1354
1355 let status_code = self.state.with(|inner| {
1356 let status = Self::upsert_scene(
1357 inner,
1358 fab_idx,
1359 endpoint_id,
1360 group_id,
1361 scene_id,
1362 transition_time,
1363 |ext_fields| {
1364 if !stored_bytes.is_empty() {
1365 ext_fields
1366 .extend_from_slice(stored_bytes)
1367 .map_err(|_| ErrorCode::NoSpace)?;
1368 }
1369 Ok(())
1370 },
1371 )?;
1372 if status == 0 {
1378 Self::remember_current(inner, fab_idx, endpoint_id, group_id, scene_id);
1379 }
1380 Ok::<_, Error>(status)
1381 })?;
1382
1383 if status_code == 0 {
1384 self.state.store_persist(ctx)?;
1385 ctx.notify_own_attr_changed(AttributeId::FabricSceneInfo as _);
1386 }
1387
1388 response
1389 .status(status_code)?
1390 .group_id(group_id)?
1391 .scene_id(scene_id)?
1392 .end()
1393 }
1394
1395 async fn recall_scene(
1400 &self,
1401 ctx: &impl InvokeContext,
1402 request: &RecallSceneRequest<'_>,
1403 ) -> Result<(), Error> {
1404 let fab_idx = Self::fab_idx(ctx)?;
1405 let endpoint_id = ctx.cmd().endpoint_id;
1406 let group_id = request.group_id()?;
1407 let scene_id = request.scene_id()?;
1408
1409 if scene_id == GLOBAL_SCENE_ID || scene_id == RESERVED_SCENE_ID {
1419 return Err(ErrorCode::ConstraintError.into());
1420 }
1421
1422 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_id)? {
1423 return Err(ErrorCode::InvalidCommand.into());
1424 }
1425
1426 let override_tt_ms: Option<u32> = request.transition_time()?.and_then(|n| n.into_option());
1429
1430 let mut blob = [0u8; M];
1436 let (blob_len, stored_tt_ms) = self.state.with(|inner| -> Result<_, Error> {
1437 let Some(e) = inner
1438 .table
1439 .iter()
1440 .find(|e| e.matches(fab_idx, endpoint_id, group_id, scene_id))
1441 else {
1442 return Ok((None, None));
1443 };
1444 let len = e.extension_fields.len();
1445 blob[..len].copy_from_slice(&e.extension_fields);
1446 Ok((Some(len), Some(e.transition_time)))
1447 })?;
1448 let (Some(blob_len), Some(stored_tt_ms)) = (blob_len, stored_tt_ms) else {
1449 return Err(ErrorCode::NotFound.into());
1450 };
1451
1452 let effective_tt_ms = override_tt_ms.unwrap_or(stored_tt_ms);
1453
1454 for efs_element in TLVSequence(&blob[..blob_len]).iter() {
1455 let efs = ExtensionFieldSetStruct::new(efs_element?);
1456 let cluster_id = efs.cluster_id()?;
1457 let avp_list = efs.attribute_value_list()?;
1458 let _ = self
1461 .clusters
1462 .apply(ctx, endpoint_id, cluster_id, &avp_list, effective_tt_ms)
1463 .await?;
1464 }
1465
1466 self.state
1467 .with(|inner| Self::remember_current(inner, fab_idx, endpoint_id, group_id, scene_id));
1468
1469 self.state.store_persist(ctx)?;
1470 ctx.notify_own_attr_changed(AttributeId::FabricSceneInfo as _);
1471 Ok(())
1472 }
1473
1474 fn get_scene_membership<P: TLVBuilderParent>(
1475 &self,
1476 ctx: &impl InvokeContext,
1477 request: &GetSceneMembershipRequest<'_>,
1478 response: GetSceneMembershipResponseBuilder<P>,
1479 ) -> Result<P, Error> {
1480 let fab_idx = Self::fab_idx(ctx)?;
1481 let endpoint_id = ctx.cmd().endpoint_id;
1482 let group_id = request.group_id()?;
1483
1484 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_id)? {
1487 return response
1488 .status(SC_INVALID_COMMAND)?
1489 .capacity(Nullable::none())?
1490 .group_id(group_id)?
1491 .scene_list()?
1492 .none()
1493 .end();
1494 }
1495
1496 self.state.with(|inner| -> Result<P, Error> {
1501 let remaining = Self::remaining_capacity_for_fab(inner, fab_idx);
1502
1503 let resp = response
1504 .status(0)?
1505 .capacity(Nullable::some(remaining))?
1506 .group_id(group_id)?;
1507
1508 let list = resp.scene_list()?.some()?;
1509 let list = inner
1510 .table
1511 .iter()
1512 .filter(|e| {
1513 e.fab_idx == fab_idx && e.endpoint_id == endpoint_id && e.group_id == group_id
1514 })
1515 .try_fold(list, |list, e| list.push(&e.scene_id))?;
1516 list.end()?.end()
1517 })
1518 }
1519
1520 fn copy_scene<P: TLVBuilderParent>(
1521 &self,
1522 ctx: &impl InvokeContext,
1523 request: &CopySceneRequest<'_>,
1524 response: CopySceneResponseBuilder<P>,
1525 ) -> Result<P, Error> {
1526 let fab_idx = Self::fab_idx(ctx)?;
1527 let endpoint_id = ctx.cmd().endpoint_id;
1528 let mode = request.mode()?;
1529 let group_from = request.group_identifier_from()?;
1530 let scene_from = request.scene_identifier_from()?;
1531 let group_to = request.group_identifier_to()?;
1532 let scene_to = request.scene_identifier_to()?;
1533
1534 let copy_all = (mode.bits() & 0x01) != 0;
1537
1538 if !copy_all
1541 && (scene_from == GLOBAL_SCENE_ID
1542 || scene_from == RESERVED_SCENE_ID
1543 || scene_to == GLOBAL_SCENE_ID
1544 || scene_to == RESERVED_SCENE_ID)
1545 {
1546 return response
1547 .status(SC_CONSTRAINT_ERROR)?
1548 .group_identifier_from(group_from)?
1549 .scene_identifier_from(scene_from)?
1550 .end();
1551 }
1552
1553 if !Self::group_in_table(ctx, fab_idx, endpoint_id, group_from)?
1554 || !Self::group_in_table(ctx, fab_idx, endpoint_id, group_to)?
1555 {
1556 return response
1557 .status(SC_INVALID_COMMAND)?
1558 .group_identifier_from(group_from)?
1559 .scene_identifier_from(scene_from)?
1560 .end();
1561 }
1562
1563 let status = self.state.with(|inner| {
1564 Self::copy_scenes_inner(
1565 inner,
1566 fab_idx,
1567 endpoint_id,
1568 group_from,
1569 scene_from,
1570 group_to,
1571 scene_to,
1572 copy_all,
1573 )
1574 });
1575
1576 if status == 0 {
1577 self.state.store_persist(ctx)?;
1578 ctx.notify_own_attr_changed(AttributeId::FabricSceneInfo as _);
1579 }
1580
1581 response
1582 .status(status)?
1583 .group_identifier_from(group_from)?
1584 .scene_identifier_from(scene_from)?
1585 .end()
1586 }
1587}
1588
1589impl<const N: usize, R, const M: usize> ClusterAsyncHandler for ScenesHandler<'_, N, R, M>
1590where
1591 R: SceneClusters,
1592{
1593 const CLUSTER: Cluster<'static> = FULL_CLUSTER;
1594
1595 fn dataver(&self) -> u32 {
1596 self.dataver.get()
1597 }
1598
1599 fn dataver_changed(&self) {
1600 self.dataver.changed();
1601 }
1602
1603 fn scene_table_size(&self, _ctx: impl ReadContext) -> impl Future<Output = Result<u16, Error>> {
1604 ready(Ok(N as u16))
1605 }
1606
1607 fn fabric_scene_info<P: TLVBuilderParent>(
1608 &self,
1609 ctx: impl ReadContext,
1610 builder: ArrayAttributeRead<SceneInfoStructArrayBuilder<P>, SceneInfoStructBuilder<P>>,
1611 ) -> impl Future<Output = Result<P, Error>> {
1612 ready(self.read_fabric_scene_info(&ctx, builder))
1613 }
1614
1615 fn handle_add_scene<P: TLVBuilderParent>(
1616 &self,
1617 ctx: impl InvokeContext,
1618 request: AddSceneRequest<'_>,
1619 response: AddSceneResponseBuilder<P>,
1620 ) -> impl Future<Output = Result<P, Error>> {
1621 ready(self.add_scene(&ctx, &request, response))
1622 }
1623
1624 fn handle_view_scene<P: TLVBuilderParent>(
1625 &self,
1626 ctx: impl InvokeContext,
1627 request: ViewSceneRequest<'_>,
1628 response: ViewSceneResponseBuilder<P>,
1629 ) -> impl Future<Output = Result<P, Error>> {
1630 ready(self.view_scene(&ctx, &request, response))
1631 }
1632
1633 fn handle_remove_scene<P: TLVBuilderParent>(
1634 &self,
1635 ctx: impl InvokeContext,
1636 request: RemoveSceneRequest<'_>,
1637 response: RemoveSceneResponseBuilder<P>,
1638 ) -> impl Future<Output = Result<P, Error>> {
1639 ready(self.remove_scene(&ctx, &request, response))
1640 }
1641
1642 fn handle_remove_all_scenes<P: TLVBuilderParent>(
1643 &self,
1644 ctx: impl InvokeContext,
1645 request: RemoveAllScenesRequest<'_>,
1646 response: RemoveAllScenesResponseBuilder<P>,
1647 ) -> impl Future<Output = Result<P, Error>> {
1648 ready(self.remove_all_scenes(&ctx, &request, response))
1649 }
1650
1651 async fn handle_store_scene<P: TLVBuilderParent>(
1652 &self,
1653 ctx: impl InvokeContext,
1654 request: StoreSceneRequest<'_>,
1655 response: StoreSceneResponseBuilder<P>,
1656 ) -> Result<P, Error> {
1657 self.store_scene(&ctx, &request, response).await
1658 }
1659
1660 async fn handle_recall_scene(
1661 &self,
1662 ctx: impl InvokeContext,
1663 request: RecallSceneRequest<'_>,
1664 ) -> Result<(), Error> {
1665 self.recall_scene(&ctx, &request).await
1666 }
1667
1668 fn handle_get_scene_membership<P: TLVBuilderParent>(
1669 &self,
1670 ctx: impl InvokeContext,
1671 request: GetSceneMembershipRequest<'_>,
1672 response: GetSceneMembershipResponseBuilder<P>,
1673 ) -> impl Future<Output = Result<P, Error>> {
1674 ready(self.get_scene_membership(&ctx, &request, response))
1675 }
1676
1677 fn handle_copy_scene<P: TLVBuilderParent>(
1678 &self,
1679 ctx: impl InvokeContext,
1680 request: CopySceneRequest<'_>,
1681 response: CopySceneResponseBuilder<P>,
1682 ) -> impl Future<Output = Result<P, Error>> {
1683 ready(self.copy_scene(&ctx, &request, response))
1684 }
1685}
1686
1687#[cfg(test)]
1688mod tests {
1689 use super::*;
1696
1697 fn fab(n: u8) -> NonZeroU8 {
1698 NonZeroU8::new(n).unwrap()
1699 }
1700
1701 fn entry(
1702 fab_idx: NonZeroU8,
1703 endpoint_id: EndptId,
1704 group_id: u16,
1705 scene_id: SceneId,
1706 transition_time: u32,
1707 ) -> SceneEntry {
1708 SceneEntry {
1709 fab_idx,
1710 endpoint_id,
1711 group_id,
1712 scene_id,
1713 transition_time,
1714 extension_fields: Vec::new(),
1715 }
1716 }
1717
1718 fn entry_with_blob(
1720 fab_idx: NonZeroU8,
1721 endpoint_id: EndptId,
1722 group_id: u16,
1723 scene_id: SceneId,
1724 transition_time: u32,
1725 blob: &[u8],
1726 ) -> SceneEntry {
1727 let mut ext: Vec<u8, MAX_EXT_FIELDS_LEN> = Vec::new();
1728 ext.extend_from_slice(blob)
1729 .expect("blob too large for test");
1730 SceneEntry {
1731 fab_idx,
1732 endpoint_id,
1733 group_id,
1734 scene_id,
1735 transition_time,
1736 extension_fields: ext,
1737 }
1738 }
1739
1740 fn push<const N: usize>(inner: &mut ScenesStateInner<N>, e: SceneEntry) {
1741 inner.table.push(e).expect("test table overflow");
1742 }
1743
1744 fn count<const N: usize>(
1746 inner: &ScenesStateInner<N>,
1747 fab_idx: NonZeroU8,
1748 ep: EndptId,
1749 group: u16,
1750 ) -> usize {
1751 inner
1752 .table
1753 .iter()
1754 .filter(|e| e.fab_idx == fab_idx && e.endpoint_id == ep && e.group_id == group)
1755 .count()
1756 }
1757
1758 fn find_tt<const N: usize>(
1759 inner: &ScenesStateInner<N>,
1760 fab_idx: NonZeroU8,
1761 ep: EndptId,
1762 group: u16,
1763 scene: SceneId,
1764 ) -> Option<u32> {
1765 inner
1766 .table
1767 .iter()
1768 .find(|e| e.matches(fab_idx, ep, group, scene))
1769 .map(|e| e.transition_time)
1770 }
1771
1772 fn find_blob<const N: usize>(
1774 inner: &ScenesStateInner<N>,
1775 fab_idx: NonZeroU8,
1776 ep: EndptId,
1777 group: u16,
1778 scene: SceneId,
1779 ) -> Option<&[u8]> {
1780 inner
1781 .table
1782 .iter()
1783 .find(|e| e.matches(fab_idx, ep, group, scene))
1784 .map(|e| e.extension_fields.as_slice())
1785 }
1786
1787 #[test]
1790 fn copy_single_scene_preserves_extension_fields_blob() {
1791 let blob = &[0xDE, 0xAD, 0xBE, 0xEF, 0x18];
1794 let mut inner = ScenesStateInner::<8>::new();
1795 push(&mut inner, entry_with_blob(fab(1), 1, 10, 5, 100, blob));
1796
1797 let status =
1798 ScenesHandler::<8>::copy_scenes_inner(&mut inner, fab(1), 1, 10, 5, 20, 7, false);
1799 assert_eq!(status, 0);
1800
1801 assert_eq!(find_blob(&inner, fab(1), 1, 20, 7), Some(&blob[..]));
1802 assert_eq!(find_blob(&inner, fab(1), 1, 10, 5), Some(&blob[..]));
1804 }
1805
1806 #[test]
1807 fn copy_all_preserves_each_source_blob() {
1808 let blob_a = &[0xAA, 0xBB, 0x18];
1811 let blob_b = &[0xCC, 0x18];
1812 let mut inner = ScenesStateInner::<16>::new();
1813 push(&mut inner, entry_with_blob(fab(1), 1, 10, 1, 100, blob_a));
1814 push(&mut inner, entry_with_blob(fab(1), 1, 10, 2, 200, blob_b));
1815
1816 let status =
1817 ScenesHandler::<16>::copy_scenes_inner(&mut inner, fab(1), 1, 10, 0, 20, 0, true);
1818 assert_eq!(status, 0);
1819
1820 assert_eq!(find_blob(&inner, fab(1), 1, 20, 1), Some(&blob_a[..]));
1821 assert_eq!(find_blob(&inner, fab(1), 1, 20, 2), Some(&blob_b[..]));
1822 }
1823
1824 #[test]
1825 fn copy_overwrites_existing_dest_blob() {
1826 let old_blob = &[0x11, 0x18];
1827 let new_blob = &[0x22, 0x33, 0x18];
1828 let mut inner = ScenesStateInner::<8>::new();
1829 push(&mut inner, entry_with_blob(fab(1), 1, 10, 5, 100, new_blob));
1830 push(&mut inner, entry_with_blob(fab(1), 1, 20, 7, 999, old_blob));
1831
1832 let status =
1833 ScenesHandler::<8>::copy_scenes_inner(&mut inner, fab(1), 1, 10, 5, 20, 7, false);
1834 assert_eq!(status, 0);
1835
1836 assert_eq!(find_blob(&inner, fab(1), 1, 20, 7), Some(&new_blob[..]));
1839 }
1840
1841 #[test]
1844 fn copy_single_scene_to_new_dest() {
1845 let mut inner = ScenesStateInner::<8>::new();
1846 push(&mut inner, entry(fab(1), 1, 10, 5, 100));
1847
1848 let status = ScenesHandler::<8>::copy_scenes_inner(
1849 &mut inner,
1850 fab(1),
1851 1,
1852 10,
1853 5,
1854 20,
1855 7,
1856 false,
1857 );
1858
1859 assert_eq!(status, 0);
1860 assert_eq!(find_tt(&inner, fab(1), 1, 10, 5), Some(100));
1862 assert_eq!(find_tt(&inner, fab(1), 1, 20, 7), Some(100));
1865 assert_eq!(inner.table.len(), 2);
1866 }
1867
1868 #[test]
1869 fn copy_single_scene_replaces_existing_dest() {
1870 let mut inner = ScenesStateInner::<8>::new();
1871 push(&mut inner, entry(fab(1), 1, 10, 5, 100));
1872 push(&mut inner, entry(fab(1), 1, 20, 7, 999));
1873
1874 let status =
1875 ScenesHandler::<8>::copy_scenes_inner(&mut inner, fab(1), 1, 10, 5, 20, 7, false);
1876
1877 assert_eq!(status, 0);
1878 assert_eq!(find_tt(&inner, fab(1), 1, 20, 7), Some(100));
1880 assert_eq!(inner.table.len(), 2);
1881 }
1882
1883 #[test]
1884 fn copy_single_scene_missing_source_returns_not_found() {
1885 let mut inner = ScenesStateInner::<8>::new();
1886 push(&mut inner, entry(fab(1), 1, 10, 5, 100));
1887
1888 let status = ScenesHandler::<8>::copy_scenes_inner(
1889 &mut inner,
1890 fab(1),
1891 1,
1892 99, 5,
1894 20,
1895 7,
1896 false,
1897 );
1898
1899 assert_eq!(status, SC_NOT_FOUND);
1900 assert_eq!(inner.table.len(), 1);
1902 assert_eq!(find_tt(&inner, fab(1), 1, 20, 7), None);
1903 }
1904
1905 #[test]
1908 fn copy_all_copies_every_source_scene() {
1909 let mut inner = ScenesStateInner::<16>::new();
1912 push(&mut inner, entry(fab(1), 1, 10, 1, 100));
1913 push(&mut inner, entry(fab(1), 1, 10, 2, 200));
1914 push(&mut inner, entry(fab(1), 1, 10, 3, 300));
1915
1916 let status = ScenesHandler::<16>::copy_scenes_inner(
1917 &mut inner,
1918 fab(1),
1919 1,
1920 10,
1921 0, 20,
1923 0, true,
1925 );
1926
1927 assert_eq!(status, 0);
1928 assert_eq!(count(&inner, fab(1), 1, 20), 3);
1931 assert_eq!(find_tt(&inner, fab(1), 1, 20, 1), Some(100));
1932 assert_eq!(find_tt(&inner, fab(1), 1, 20, 2), Some(200));
1933 assert_eq!(find_tt(&inner, fab(1), 1, 20, 3), Some(300));
1934 assert_eq!(count(&inner, fab(1), 1, 10), 3);
1936 }
1937
1938 #[test]
1939 fn copy_all_to_same_group_is_noop() {
1940 let mut inner = ScenesStateInner::<8>::new();
1945 push(&mut inner, entry(fab(1), 1, 10, 1, 100));
1946 push(&mut inner, entry(fab(1), 1, 10, 2, 200));
1947
1948 let status = ScenesHandler::<8>::copy_scenes_inner(
1949 &mut inner,
1950 fab(1),
1951 1,
1952 10,
1953 0,
1954 10, 0,
1956 true,
1957 );
1958
1959 assert_eq!(status, 0);
1960 assert_eq!(inner.table.len(), 2);
1961 }
1962
1963 #[test]
1964 fn copy_all_missing_source_returns_not_found() {
1965 let mut inner = ScenesStateInner::<8>::new();
1966 push(&mut inner, entry(fab(1), 1, 99, 1, 100));
1968
1969 let status = ScenesHandler::<8>::copy_scenes_inner(
1970 &mut inner,
1971 fab(1),
1972 1,
1973 10, 0,
1975 20,
1976 0,
1977 true,
1978 );
1979
1980 assert_eq!(status, SC_NOT_FOUND);
1981 assert_eq!(inner.table.len(), 1);
1982 assert_eq!(count(&inner, fab(1), 1, 20), 0);
1983 }
1984
1985 #[test]
1986 fn copy_all_capacity_exhaustion_returns_insufficient_space() {
1987 let mut inner = ScenesStateInner::<3>::new();
1990 inner.table.push(entry(fab(1), 1, 10, 1, 100)).unwrap();
1991 inner.table.push(entry(fab(1), 1, 10, 2, 200)).unwrap();
1992 inner.table.push(entry(fab(1), 1, 10, 3, 300)).unwrap();
1993
1994 let status =
1995 ScenesHandler::<3>::copy_scenes_inner(&mut inner, fab(1), 1, 10, 0, 20, 0, true);
1996
1997 assert_eq!(status, SC_INSUFFICIENT_SPACE);
1998 assert_eq!(inner.table.len(), 3);
2002 }
2003
2004 #[test]
2007 fn copy_does_not_cross_fabric_boundary() {
2008 let mut inner = ScenesStateInner::<8>::new();
2009 push(&mut inner, entry(fab(1), 1, 10, 5, 100));
2010
2011 let status = ScenesHandler::<8>::copy_scenes_inner(
2012 &mut inner,
2013 fab(2), 1,
2015 10,
2016 5,
2017 20,
2018 7,
2019 false,
2020 );
2021
2022 assert_eq!(status, SC_NOT_FOUND);
2023 assert_eq!(count(&inner, fab(2), 1, 20), 0);
2025 assert_eq!(find_tt(&inner, fab(1), 1, 10, 5), Some(100));
2027 }
2028
2029 #[test]
2030 fn copy_does_not_cross_endpoint_boundary() {
2031 let mut inner = ScenesStateInner::<8>::new();
2032 push(&mut inner, entry(fab(1), 1, 10, 5, 100));
2033
2034 let status = ScenesHandler::<8>::copy_scenes_inner(
2035 &mut inner,
2036 fab(1),
2037 2, 10,
2039 5,
2040 20,
2041 7,
2042 false,
2043 );
2044
2045 assert_eq!(status, SC_NOT_FOUND);
2046 assert_eq!(count(&inner, fab(1), 2, 20), 0);
2047 }
2048
2049 #[test]
2052 fn successful_copy_invalidates_current_scene_on_match() {
2053 let mut inner = ScenesStateInner::<8>::new();
2054 push(&mut inner, entry(fab(1), 1, 10, 5, 100));
2055 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 20, 7);
2060 assert_eq!(inner.current_per_fabric.len(), 1);
2061
2062 let status =
2063 ScenesHandler::<8>::copy_scenes_inner(&mut inner, fab(1), 1, 10, 5, 20, 7, false);
2064
2065 assert_eq!(status, 0);
2066 let slot = inner
2069 .current_per_fabric
2070 .iter()
2071 .find(|c| c.fab_idx == fab(1))
2072 .expect("slot kept");
2073 assert!(!slot.valid);
2074 }
2075
2076 #[test]
2077 fn successful_copy_preserves_current_scene_when_target_doesnt_match() {
2078 let mut inner = ScenesStateInner::<8>::new();
2079 push(&mut inner, entry(fab(1), 1, 10, 5, 100));
2080 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 99, 99);
2085
2086 let status =
2087 ScenesHandler::<8>::copy_scenes_inner(&mut inner, fab(1), 1, 10, 5, 20, 7, false);
2088
2089 assert_eq!(status, 0);
2090 assert_eq!(inner.current_per_fabric.len(), 1);
2091 assert_eq!(inner.current_per_fabric[0].group_id, 99);
2092 assert_eq!(inner.current_per_fabric[0].scene_id, 99);
2093 assert!(inner.current_per_fabric[0].valid);
2094 }
2095
2096 #[test]
2097 fn failed_copy_does_not_invalidate_current_scene() {
2098 let mut inner = ScenesStateInner::<8>::new();
2099 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 99, 99);
2100 let dv_before = inner.info_dataver;
2101
2102 let status = ScenesHandler::<8>::copy_scenes_inner(
2103 &mut inner,
2104 fab(1),
2105 1,
2106 10, 5,
2108 20,
2109 7,
2110 false,
2111 );
2112
2113 assert_eq!(status, SC_NOT_FOUND);
2114 assert_eq!(inner.current_per_fabric.len(), 1);
2116 assert_eq!(inner.current_per_fabric[0].fab_idx, fab(1));
2117 assert_eq!(inner.current_per_fabric[0].group_id, 99);
2118 assert_eq!(inner.current_per_fabric[0].scene_id, 99);
2119 assert_eq!(inner.info_dataver, dv_before);
2121 }
2122
2123 #[test]
2126 fn remember_current_replaces_existing_slot_in_place() {
2127 let mut inner = ScenesStateInner::<8>::new();
2128 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 10, 1);
2129 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 20, 2);
2130
2131 assert_eq!(inner.current_per_fabric.len(), 1);
2133 assert_eq!(inner.current_per_fabric[0].group_id, 20);
2134 assert_eq!(inner.current_per_fabric[0].scene_id, 2);
2135 }
2136
2137 #[test]
2138 fn remember_current_keeps_fabrics_independent() {
2139 let mut inner = ScenesStateInner::<8>::new();
2140 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 10, 1);
2141 ScenesHandler::<8>::remember_current(&mut inner, fab(2), 1, 20, 2);
2142
2143 assert_eq!(inner.current_per_fabric.len(), 2);
2144 }
2145
2146 #[test]
2147 fn invalidate_match_scene_only_clears_exact_match() {
2148 let mut inner = ScenesStateInner::<8>::new();
2149 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 10, 1);
2150 ScenesHandler::<8>::remember_current(&mut inner, fab(2), 1, 20, 2);
2151
2152 ScenesHandler::<8>::invalidate_current_if_match_scene(&mut inner, fab(1), 99, 99);
2157 assert_eq!(inner.current_per_fabric.len(), 2);
2158 assert!(inner.current_per_fabric.iter().all(|c| c.valid));
2159
2160 ScenesHandler::<8>::invalidate_current_if_match_scene(&mut inner, fab(1), 10, 1);
2164 assert_eq!(inner.current_per_fabric.len(), 2);
2165 let f1 = inner
2166 .current_per_fabric
2167 .iter()
2168 .find(|c| c.fab_idx == fab(1))
2169 .unwrap();
2170 assert!(!f1.valid);
2171 let f2 = inner
2172 .current_per_fabric
2173 .iter()
2174 .find(|c| c.fab_idx == fab(2))
2175 .unwrap();
2176 assert!(f2.valid);
2177 }
2178
2179 #[test]
2180 fn invalidate_match_group_clears_any_scene_in_group() {
2181 let mut inner = ScenesStateInner::<8>::new();
2182 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 10, 7);
2183 ScenesHandler::<8>::remember_current(&mut inner, fab(2), 1, 20, 2);
2184
2185 ScenesHandler::<8>::invalidate_current_if_match_group(&mut inner, fab(1), 99);
2187 assert!(inner.current_per_fabric.iter().all(|c| c.valid));
2188
2189 ScenesHandler::<8>::invalidate_current_if_match_group(&mut inner, fab(1), 10);
2193 let f1 = inner
2194 .current_per_fabric
2195 .iter()
2196 .find(|c| c.fab_idx == fab(1))
2197 .unwrap();
2198 assert!(!f1.valid);
2199 let f2 = inner
2200 .current_per_fabric
2201 .iter()
2202 .find(|c| c.fab_idx == fab(2))
2203 .unwrap();
2204 assert!(f2.valid);
2205 }
2206
2207 fn fill_with<'a>(blob: &'a [u8]) -> impl FnOnce(&mut Vec<u8, 128>) -> Result<(), Error> + 'a {
2211 move |ext| {
2212 ext.extend_from_slice(blob)
2213 .map_err(|_| ErrorCode::NoSpace.into())
2214 }
2215 }
2216
2217 #[test]
2218 fn upsert_inserts_new_record_with_status_zero() {
2219 let mut inner = ScenesStateInner::<8>::new();
2220 let status = ScenesHandler::<8>::upsert_scene(
2221 &mut inner,
2222 fab(1),
2223 1,
2224 10,
2225 5,
2226 100,
2227 fill_with(&[0xAA, 0x18]),
2228 )
2229 .unwrap();
2230
2231 assert_eq!(status, 0);
2232 assert_eq!(inner.table.len(), 1);
2233 assert_eq!(find_tt(&inner, fab(1), 1, 10, 5), Some(100));
2234 assert_eq!(find_blob(&inner, fab(1), 1, 10, 5), Some(&[0xAA, 0x18][..]));
2235 }
2236
2237 #[test]
2238 fn upsert_replaces_existing_record_in_place_no_growth() {
2239 let mut inner = ScenesStateInner::<8>::new();
2240 push(
2241 &mut inner,
2242 entry_with_blob(fab(1), 1, 10, 5, 100, &[0xAA, 0x18]),
2243 );
2244
2245 let status = ScenesHandler::<8>::upsert_scene(
2246 &mut inner,
2247 fab(1),
2248 1,
2249 10,
2250 5,
2251 999,
2252 fill_with(&[0xBB, 0xCC, 0x18]),
2253 )
2254 .unwrap();
2255
2256 assert_eq!(status, 0);
2257 assert_eq!(inner.table.len(), 1, "replace must not grow the table");
2258 assert_eq!(find_tt(&inner, fab(1), 1, 10, 5), Some(999));
2259 assert_eq!(
2260 find_blob(&inner, fab(1), 1, 10, 5),
2261 Some(&[0xBB, 0xCC, 0x18][..])
2262 );
2263 }
2264
2265 #[test]
2266 fn upsert_returns_insufficient_space_when_table_is_full() {
2267 let mut inner = ScenesStateInner::<3>::new();
2269 inner.table.push(entry(fab(1), 1, 10, 1, 100)).unwrap();
2270 inner.table.push(entry(fab(1), 1, 10, 2, 100)).unwrap();
2271 inner.table.push(entry(fab(1), 1, 10, 3, 100)).unwrap();
2272
2273 let status = ScenesHandler::<3>::upsert_scene(
2274 &mut inner,
2275 fab(1),
2276 1,
2277 10,
2278 99, 200,
2280 fill_with(&[0x18]),
2281 )
2282 .unwrap();
2283
2284 assert_eq!(status, SC_INSUFFICIENT_SPACE);
2285 assert_eq!(inner.table.len(), 3, "table size unchanged on rejection");
2286 }
2287
2288 #[test]
2289 fn upsert_replace_at_full_capacity_still_succeeds() {
2290 let mut inner = ScenesStateInner::<3>::new();
2293 inner.table.push(entry(fab(1), 1, 10, 1, 100)).unwrap();
2294 inner.table.push(entry(fab(1), 1, 10, 2, 100)).unwrap();
2295 inner.table.push(entry(fab(1), 1, 10, 3, 100)).unwrap();
2296
2297 let status = ScenesHandler::<3>::upsert_scene(
2298 &mut inner,
2299 fab(1),
2300 1,
2301 10,
2302 2, 999,
2304 fill_with(&[0x18]),
2305 )
2306 .unwrap();
2307
2308 assert_eq!(status, 0);
2309 assert_eq!(inner.table.len(), 3);
2310 assert_eq!(find_tt(&inner, fab(1), 1, 10, 2), Some(999));
2311 }
2312
2313 #[test]
2314 fn upsert_invalidates_current_scene_when_upsert_targets_it() {
2315 let mut inner = ScenesStateInner::<8>::new();
2321 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 10, 5);
2322
2323 let _ =
2324 ScenesHandler::<8>::upsert_scene(&mut inner, fab(1), 1, 10, 5, 100, fill_with(&[0x18]))
2325 .unwrap();
2326
2327 let f1 = inner
2330 .current_per_fabric
2331 .iter()
2332 .find(|c| c.fab_idx == fab(1))
2333 .expect("slot kept");
2334 assert!(!f1.valid);
2335 }
2336
2337 #[test]
2338 fn upsert_preserves_current_scene_when_upsert_targets_a_different_scene() {
2339 let mut inner = ScenesStateInner::<8>::new();
2341 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 1, 1);
2342
2343 let _ =
2344 ScenesHandler::<8>::upsert_scene(&mut inner, fab(1), 1, 2, 1, 100, fill_with(&[0x18]))
2346 .unwrap();
2347
2348 assert_eq!(inner.current_per_fabric.len(), 1);
2349 assert_eq!(inner.current_per_fabric[0].group_id, 1);
2350 assert_eq!(inner.current_per_fabric[0].scene_id, 1);
2351 assert!(inner.current_per_fabric[0].valid);
2352 }
2353
2354 #[test]
2355 fn upsert_keeps_other_fabrics_current_scene_intact() {
2356 let mut inner = ScenesStateInner::<8>::new();
2357 ScenesHandler::<8>::remember_current(&mut inner, fab(1), 1, 10, 5);
2360 ScenesHandler::<8>::remember_current(&mut inner, fab(2), 1, 10, 5);
2361
2362 let _ =
2363 ScenesHandler::<8>::upsert_scene(&mut inner, fab(1), 1, 10, 5, 100, fill_with(&[0x18]))
2364 .unwrap();
2365
2366 let f1 = inner
2369 .current_per_fabric
2370 .iter()
2371 .find(|c| c.fab_idx == fab(1))
2372 .expect("fab(1) slot kept");
2373 assert!(!f1.valid);
2374 let f2 = inner
2375 .current_per_fabric
2376 .iter()
2377 .find(|c| c.fab_idx == fab(2))
2378 .expect("fab(2) slot kept");
2379 assert!(f2.valid, "fab(2)'s CurrentScene must not be touched");
2380 }
2381
2382 #[test]
2383 fn upsert_at_full_capacity_does_not_invalidate_current() {
2384 let mut inner = ScenesStateInner::<3>::new();
2387 inner.table.push(entry(fab(1), 1, 10, 1, 100)).unwrap();
2388 inner.table.push(entry(fab(1), 1, 10, 2, 100)).unwrap();
2389 inner.table.push(entry(fab(1), 1, 10, 3, 100)).unwrap();
2390 ScenesHandler::<3>::remember_current(&mut inner, fab(1), 1, 99, 99);
2391
2392 let status = ScenesHandler::<3>::upsert_scene(
2393 &mut inner,
2394 fab(1),
2395 1,
2396 10,
2397 99,
2398 200,
2399 fill_with(&[0x18]),
2400 )
2401 .unwrap();
2402
2403 assert_eq!(status, SC_INSUFFICIENT_SPACE);
2404 assert!(inner.current_per_fabric.iter().any(|c| c.fab_idx == fab(1)));
2405 }
2406
2407 #[test]
2408 fn upsert_fill_failure_on_new_entry_rolls_back_the_push() {
2409 let mut inner = ScenesStateInner::<8>::new();
2413 push(&mut inner, entry(fab(1), 1, 10, 1, 100));
2414
2415 let result = ScenesHandler::<8>::upsert_scene(
2416 &mut inner,
2417 fab(1),
2418 1,
2419 10,
2420 42, 200,
2422 |_| Err(ErrorCode::NoSpace.into()),
2423 );
2424
2425 assert!(result.is_err());
2426 assert_eq!(
2427 inner.table.len(),
2428 1,
2429 "rolled-back push leaves count untouched"
2430 );
2431 assert!(find_tt(&inner, fab(1), 1, 10, 42).is_none());
2432 }
2433}