1use core::cell::{Cell, RefCell};
50use core::future::Future;
51
52use heapless::String as HString;
53
54use crate::dm::{
55 ArrayAttributeRead, Cluster, Dataver, EndptId, InvokeContext, ReadContext, WriteContext,
56};
57use crate::error::{Error, ErrorCode};
58use crate::tlv::{TLVBuilderParent, Utf8Str};
59use crate::utils::storage::Vec;
60use crate::utils::sync::blocking::Mutex;
61use crate::with;
62
63#[allow(unused_imports)]
64pub use crate::dm::clusters::decl::zone_management::*;
65
66use super::super::decl::zone_management as decl;
67
68pub const MAX_ZONE_NAME_LEN: usize = 16;
70
71pub const MAX_ZONE_COLOR_LEN: usize = 7;
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76#[cfg_attr(feature = "defmt", derive(defmt::Format))]
77pub enum ZoneError {
78 ResourceExhausted,
79 DynamicConstraint,
80 NotFound,
81 Failure,
82}
83
84impl From<ZoneError> for Error {
85 fn from(e: ZoneError) -> Self {
86 match e {
87 ZoneError::ResourceExhausted => ErrorCode::ResourceExhausted.into(),
88 ZoneError::DynamicConstraint => ErrorCode::ConstraintError.into(),
89 ZoneError::NotFound => ErrorCode::NotFound.into(),
90 ZoneError::Failure => ErrorCode::Failure.into(),
91 }
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct Zone<const NV: usize> {
98 pub zone_id: u16,
99 pub zone_type: ZoneTypeEnum,
100 pub zone_source: ZoneSourceEnum,
101 pub name: HString<MAX_ZONE_NAME_LEN>,
102 pub zone_use: ZoneUseEnum,
103 pub vertices: Vec<(u16, u16), NV>,
104 pub color: Option<HString<MAX_ZONE_COLOR_LEN>>,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109#[cfg_attr(feature = "defmt", derive(defmt::Format))]
110pub struct Trigger {
111 pub zone_id: u16,
112 pub initial_duration: u32,
113 pub augmentation_duration: u32,
114 pub max_duration: u32,
115 pub blind_duration: u32,
116 pub sensitivity: Option<u8>,
117}
118
119#[allow(unused_variables)]
123pub trait ZoneMgmtHooks<const NV: usize> {
124 fn zone_created(&self, zone: &Zone<NV>) -> impl Future<Output = Result<(), ZoneError>> {
125 async { Ok(()) }
126 }
127
128 fn zone_updated(&self, zone: &Zone<NV>) -> impl Future<Output = Result<(), ZoneError>> {
129 async { Ok(()) }
130 }
131
132 fn zone_removed(&self, zone_id: u16) -> impl Future<Output = Result<(), ZoneError>> {
133 async { Ok(()) }
134 }
135
136 fn trigger_set(&self, trigger: &Trigger) -> impl Future<Output = Result<(), ZoneError>> {
137 async { Ok(()) }
138 }
139
140 fn trigger_removed(&self, zone_id: u16) -> impl Future<Output = Result<(), ZoneError>> {
141 async { Ok(()) }
142 }
143
144 fn set_sensitivity(&self, value: u8) -> impl Future<Output = Result<(), ZoneError>> {
145 async { Ok(()) }
146 }
147}
148
149impl<const NV: usize, T> ZoneMgmtHooks<NV> for &T
150where
151 T: ZoneMgmtHooks<NV>,
152{
153 fn zone_created(&self, zone: &Zone<NV>) -> impl Future<Output = Result<(), ZoneError>> {
154 (*self).zone_created(zone)
155 }
156
157 fn zone_updated(&self, zone: &Zone<NV>) -> impl Future<Output = Result<(), ZoneError>> {
158 (*self).zone_updated(zone)
159 }
160
161 fn zone_removed(&self, zone_id: u16) -> impl Future<Output = Result<(), ZoneError>> {
162 (*self).zone_removed(zone_id)
163 }
164
165 fn trigger_set(&self, trigger: &Trigger) -> impl Future<Output = Result<(), ZoneError>> {
166 (*self).trigger_set(trigger)
167 }
168
169 fn trigger_removed(&self, zone_id: u16) -> impl Future<Output = Result<(), ZoneError>> {
170 (*self).trigger_removed(zone_id)
171 }
172
173 fn set_sensitivity(&self, value: u8) -> impl Future<Output = Result<(), ZoneError>> {
174 (*self).set_sensitivity(value)
175 }
176}
177
178#[derive(Debug, Clone, Copy)]
180pub struct ZoneMgmtConfig {
181 pub max_zones: u8,
182 pub max_user_defined_zones: u8,
183 pub sensitivity_max: u8,
185 pub default_sensitivity: u8,
186 pub two_d_cartesian_max: (u16, u16),
189}
190
191struct State<const NZ: usize, const NV: usize, const NT: usize> {
192 zones: Vec<Zone<NV>, NZ>,
193 triggers: Vec<Trigger, NT>,
194 sensitivity: u8,
195 seeded: bool,
196}
197
198impl<const NZ: usize, const NV: usize, const NT: usize> State<NZ, NV, NT> {
199 const fn new() -> Self {
200 Self {
201 zones: Vec::new(),
202 triggers: Vec::new(),
203 sensitivity: 0,
204 seeded: false,
205 }
206 }
207}
208
209pub struct ZoneMgmtHandler<H, const NZ: usize, const NV: usize, const NT: usize>
211where
212 H: ZoneMgmtHooks<NV>,
213{
214 dataver: Dataver,
215 endpoint_id: EndptId,
216 config: ZoneMgmtConfig,
217 features: u32,
218 hooks: H,
219 state: Mutex<RefCell<State<NZ, NV, NT>>>,
220 next_id: Mutex<Cell<u16>>,
221}
222
223impl<H, const NZ: usize, const NV: usize, const NT: usize> ZoneMgmtHandler<H, NZ, NV, NT>
224where
225 H: ZoneMgmtHooks<NV>,
226{
227 pub const CLUSTER: Cluster<'static> = decl::FULL_CLUSTER
230 .with_revision(1)
231 .with_features(
232 decl::Feature::TWO_DIMENSIONAL_CARTESIAN_ZONE.bits()
233 | decl::Feature::USER_DEFINED.bits(),
234 )
235 .with_attrs(with!(
236 required;
237 AttributeId::MaxUserDefinedZones
238 | AttributeId::MaxZones
239 | AttributeId::Zones
240 | AttributeId::Triggers
241 | AttributeId::SensitivityMax
242 | AttributeId::Sensitivity
243 | AttributeId::TwoDCartesianMax
244 ))
245 .with_cmds(with!(
246 decl::CommandId::CreateTwoDCartesianZone
247 | decl::CommandId::UpdateTwoDCartesianZone
248 | decl::CommandId::RemoveZone
249 | decl::CommandId::CreateOrUpdateTrigger
250 | decl::CommandId::RemoveTrigger
251 ));
252
253 pub const fn new(
254 dataver: Dataver,
255 endpoint_id: EndptId,
256 config: ZoneMgmtConfig,
257 features: u32,
258 hooks: H,
259 ) -> Self {
260 Self {
261 dataver,
262 endpoint_id,
263 config,
264 features,
265 hooks,
266 state: Mutex::new(RefCell::new(State::new())),
267 next_id: Mutex::new(Cell::new(1)),
268 }
269 }
270
271 pub const fn adapt(self) -> decl::HandlerAsyncAdaptor<Self> {
272 decl::HandlerAsyncAdaptor(self)
273 }
274
275 pub const fn endpoint_id(&self) -> EndptId {
276 self.endpoint_id
277 }
278
279 pub fn add_mfg_zone(&self, mut zone: Zone<NV>) -> Result<u16, Error> {
283 zone.zone_source = ZoneSourceEnum::Mfg;
284 zone.zone_id = self.alloc_zone_id();
285 let id = zone.zone_id;
286 let pushed = self.state.lock(|cell| {
287 let mut s = cell.borrow_mut();
288 s.zones.push(zone).is_ok()
289 });
290 if !pushed {
291 return Err(ErrorCode::ResourceExhausted.into());
292 }
293 self.dataver.changed();
294 Ok(id)
295 }
296
297 fn alloc_zone_id(&self) -> u16 {
298 self.next_id.lock(|cell| {
299 let mut id = cell.get();
300 if id == 0 {
301 id = 1;
302 }
303 cell.set(id.wrapping_add(1).max(1));
304 id
305 })
306 }
307
308 fn ensure_seeded(&self) {
309 self.state.lock(|cell| {
310 let mut s = cell.borrow_mut();
311 if !s.seeded {
312 s.sensitivity = self.config.default_sensitivity;
313 s.seeded = true;
314 }
315 });
316 }
317
318 fn has_feature(&self, bit: u32) -> bool {
319 self.features & bit != 0
320 }
321
322 fn user_defined_zone_count(&self) -> usize {
323 self.state.lock(|cell| {
324 cell.borrow()
325 .zones
326 .iter()
327 .filter(|z| z.zone_source == ZoneSourceEnum::User)
328 .count()
329 })
330 }
331
332 #[allow(clippy::type_complexity)]
334 fn parse_zone_payload(
335 &self,
336 s: &TwoDCartesianZoneStruct<'_>,
337 ) -> Result<
338 (
339 HString<MAX_ZONE_NAME_LEN>,
340 ZoneUseEnum,
341 Vec<(u16, u16), NV>,
342 Option<HString<MAX_ZONE_COLOR_LEN>>,
343 ),
344 Error,
345 > {
346 let name_str: Utf8Str<'_> = s.name()?;
347 let mut name: HString<MAX_ZONE_NAME_LEN> = HString::new();
348 if name_str.len() > MAX_ZONE_NAME_LEN || name.push_str(name_str).is_err() {
349 return Err(ErrorCode::ConstraintError.into());
350 }
351
352 let zone_use = s.r#use()?;
353
354 let verts_arr = s.vertices()?;
355 let mut vertices: Vec<(u16, u16), NV> = Vec::new();
356 for v in verts_arr.iter() {
357 let v = v?;
358 let x = v.x()?;
359 let y = v.y()?;
360 if x > self.config.two_d_cartesian_max.0 || y > self.config.two_d_cartesian_max.1 {
361 return Err(ErrorCode::ConstraintError.into());
362 }
363 vertices
364 .push((x, y))
365 .map_err(|_| Error::from(ErrorCode::ResourceExhausted))?;
366 }
367 if vertices.len() < 3 {
369 return Err(ErrorCode::ConstraintError.into());
370 }
371
372 let color = match s.color()? {
373 Some(c) => {
374 let mut h: HString<MAX_ZONE_COLOR_LEN> = HString::new();
375 if c.len() > MAX_ZONE_COLOR_LEN || h.push_str(c).is_err() {
376 return Err(ErrorCode::ConstraintError.into());
377 }
378 Some(h)
379 }
380 None => None,
381 };
382
383 Ok((name, zone_use, vertices, color))
384 }
385}
386
387impl<H, const NZ: usize, const NV: usize, const NT: usize> ClusterAsyncHandler
388 for ZoneMgmtHandler<H, NZ, NV, NT>
389where
390 H: ZoneMgmtHooks<NV>,
391{
392 const CLUSTER: Cluster<'static> = Self::CLUSTER;
393
394 fn dataver(&self) -> u32 {
395 self.dataver.get()
396 }
397
398 fn dataver_changed(&self) {
399 self.dataver.changed();
400 }
401
402 async fn max_user_defined_zones(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
403 Ok(self.config.max_user_defined_zones)
404 }
405
406 async fn max_zones(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
407 Ok(self.config.max_zones)
408 }
409
410 async fn sensitivity_max(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
411 Ok(self.config.sensitivity_max)
412 }
413
414 async fn sensitivity(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
415 if self.config.sensitivity_max == 0 {
416 return Err(ErrorCode::AttributeNotFound.into());
417 }
418 self.ensure_seeded();
419 Ok(self.state.lock(|cell| cell.borrow().sensitivity))
420 }
421
422 async fn set_sensitivity(&self, _ctx: impl WriteContext, value: u8) -> Result<(), Error> {
423 if self.config.sensitivity_max == 0 {
424 return Err(ErrorCode::AttributeNotFound.into());
425 }
426 if value < 1 || value > self.config.sensitivity_max {
427 return Err(ErrorCode::ConstraintError.into());
428 }
429 self.hooks.set_sensitivity(value).await?;
430 self.state.lock(|cell| {
431 let mut s = cell.borrow_mut();
432 s.sensitivity = value;
433 s.seeded = true;
434 });
435 Ok(())
436 }
437
438 async fn two_d_cartesian_max<P: TLVBuilderParent>(
439 &self,
440 _ctx: impl ReadContext,
441 builder: TwoDCartesianVertexStructBuilder<P>,
442 ) -> Result<P, Error> {
443 builder
444 .x(self.config.two_d_cartesian_max.0)?
445 .y(self.config.two_d_cartesian_max.1)?
446 .end()
447 }
448
449 async fn zones<P: TLVBuilderParent>(
450 &self,
451 _ctx: impl ReadContext,
452 builder: ArrayAttributeRead<
453 ZoneInformationStructArrayBuilder<P>,
454 ZoneInformationStructBuilder<P>,
455 >,
456 ) -> Result<P, Error> {
457 let snapshot = self.state.lock(|cell| cell.borrow().zones.clone());
458 match builder {
459 ArrayAttributeRead::ReadAll(mut b) => {
460 for z in snapshot.iter() {
461 b = write_zone_info(b.push()?, z)?;
462 }
463 b.end()
464 }
465 ArrayAttributeRead::ReadOne(idx, b) => {
466 let Some(z) = snapshot.get(idx as usize) else {
467 return Err(ErrorCode::ConstraintError.into());
468 };
469 write_zone_info(b, z)
470 }
471 ArrayAttributeRead::ReadNone(b) => b.end(),
472 }
473 }
474
475 async fn triggers<P: TLVBuilderParent>(
476 &self,
477 _ctx: impl ReadContext,
478 builder: ArrayAttributeRead<
479 ZoneTriggerControlStructArrayBuilder<P>,
480 ZoneTriggerControlStructBuilder<P>,
481 >,
482 ) -> Result<P, Error> {
483 let snapshot = self.state.lock(|cell| cell.borrow().triggers.clone());
484 match builder {
485 ArrayAttributeRead::ReadAll(mut b) => {
486 for t in snapshot.iter() {
487 b = write_trigger(b.push()?, t)?;
488 }
489 b.end()
490 }
491 ArrayAttributeRead::ReadOne(idx, b) => {
492 let Some(t) = snapshot.get(idx as usize) else {
493 return Err(ErrorCode::ConstraintError.into());
494 };
495 write_trigger(b, t)
496 }
497 ArrayAttributeRead::ReadNone(b) => b.end(),
498 }
499 }
500
501 async fn handle_create_two_d_cartesian_zone<P: TLVBuilderParent>(
502 &self,
503 ctx: impl InvokeContext,
504 request: CreateTwoDCartesianZoneRequest<'_>,
505 response: CreateTwoDCartesianZoneResponseBuilder<P>,
506 ) -> Result<P, Error> {
507 if !self.has_feature(decl::Feature::USER_DEFINED.bits()) {
508 return Err(ErrorCode::InvalidAction.into());
509 }
510
511 let payload = request.zone()?;
512 let (name, zone_use, vertices, color) = self.parse_zone_payload(&payload)?;
513
514 if self.user_defined_zone_count() >= self.config.max_user_defined_zones as usize {
515 return Err(ErrorCode::ResourceExhausted.into());
516 }
517
518 let zone = Zone {
519 zone_id: self.alloc_zone_id(),
520 zone_type: ZoneTypeEnum::TwoDCARTZone,
521 zone_source: ZoneSourceEnum::User,
522 name,
523 zone_use,
524 vertices,
525 color,
526 };
527 let id = zone.zone_id;
528
529 let pushed = self.state.lock(|cell| {
530 let mut s = cell.borrow_mut();
531 s.zones.push(zone.clone()).is_ok()
532 });
533 if !pushed {
534 return Err(ErrorCode::ResourceExhausted.into());
535 }
536
537 if let Err(e) = self.hooks.zone_created(&zone).await {
538 self.state.lock(|cell| {
539 let mut s = cell.borrow_mut();
540 s.zones.retain(|z| z.zone_id != id);
541 });
542 return Err(e.into());
543 }
544 ctx.notify_own_attr_changed(AttributeId::Zones as _);
545
546 response.zone_id(id)?.end()
547 }
548
549 async fn handle_update_two_d_cartesian_zone(
550 &self,
551 ctx: impl InvokeContext,
552 request: UpdateTwoDCartesianZoneRequest<'_>,
553 ) -> Result<(), Error> {
554 if !self.has_feature(decl::Feature::USER_DEFINED.bits()) {
555 return Err(ErrorCode::InvalidAction.into());
556 }
557
558 let id = request.zone_id()?;
559 let payload = request.zone()?;
560 let (name, zone_use, vertices, color) = self.parse_zone_payload(&payload)?;
561
562 let prior = self.state.lock(|cell| {
563 cell.borrow()
564 .zones
565 .iter()
566 .find(|z| z.zone_id == id)
567 .cloned()
568 });
569 let Some(prior) = prior else {
570 return Err(ErrorCode::NotFound.into());
571 };
572 if prior.zone_source != ZoneSourceEnum::User {
573 return Err(ErrorCode::InvalidAction.into());
574 }
575
576 let updated = Zone {
577 zone_id: id,
578 zone_type: prior.zone_type,
579 zone_source: ZoneSourceEnum::User,
580 name,
581 zone_use,
582 vertices,
583 color,
584 };
585
586 let prev = self.state.lock(|cell| {
587 let mut s = cell.borrow_mut();
588 s.zones.iter().position(|z| z.zone_id == id).map(|i| {
589 let prev = s.zones[i].clone();
590 s.zones[i] = updated.clone();
591 prev
592 })
593 });
594 let Some(prev) = prev else {
595 return Err(ErrorCode::NotFound.into());
596 };
597
598 if let Err(e) = self.hooks.zone_updated(&updated).await {
599 self.state.lock(|cell| {
600 let mut s = cell.borrow_mut();
601 if let Some(i) = s.zones.iter().position(|z| z.zone_id == id) {
602 s.zones[i] = prev;
603 }
604 });
605 return Err(e.into());
606 }
607 ctx.notify_own_attr_changed(AttributeId::Zones as _);
608 Ok(())
609 }
610
611 async fn handle_remove_zone(
612 &self,
613 ctx: impl InvokeContext,
614 request: RemoveZoneRequest<'_>,
615 ) -> Result<(), Error> {
616 let id = request.zone_id()?;
617
618 let outcome = self.state.lock(|cell| {
619 let s = cell.borrow();
620 match s.zones.iter().find(|z| z.zone_id == id) {
621 None => Err(ErrorCode::NotFound),
622 Some(z) if z.zone_source != ZoneSourceEnum::User => Err(ErrorCode::InvalidAction),
623 _ => Ok(()),
624 }
625 });
626 outcome.map_err(Error::from)?;
627
628 self.hooks.zone_removed(id).await?;
629
630 let triggers_changed = self.state.lock(|cell| {
631 let mut s = cell.borrow_mut();
632 s.zones.retain(|z| z.zone_id != id);
633 let before = s.triggers.len();
634 s.triggers.retain(|t| t.zone_id != id);
635 s.triggers.len() != before
636 });
637 ctx.notify_own_attr_changed(AttributeId::Zones as _);
638 if triggers_changed {
639 ctx.notify_own_attr_changed(AttributeId::Triggers as _);
640 }
641 Ok(())
642 }
643
644 async fn handle_create_or_update_trigger(
645 &self,
646 ctx: impl InvokeContext,
647 request: CreateOrUpdateTriggerRequest<'_>,
648 ) -> Result<(), Error> {
649 let payload = request.trigger()?;
650 let zone_id = payload.zone_id()?;
651 let initial_duration = payload.initial_duration()?;
652 let augmentation_duration = payload.augmentation_duration()?;
653 let max_duration = payload.max_duration()?;
654 let blind_duration = payload.blind_duration()?;
655 let sensitivity = payload.sensitivity()?;
656
657 let zone_exists = self
658 .state
659 .lock(|cell| cell.borrow().zones.iter().any(|z| z.zone_id == zone_id));
660 if !zone_exists {
661 return Err(ErrorCode::NotFound.into());
662 }
663
664 if max_duration < initial_duration
665 || (augmentation_duration > 0 && initial_duration >= max_duration)
666 {
667 return Err(ErrorCode::ConstraintError.into());
668 }
669 if let Some(s) = sensitivity {
670 if s < 1 || s > self.config.sensitivity_max {
671 return Err(ErrorCode::ConstraintError.into());
672 }
673 }
674
675 let trigger = Trigger {
676 zone_id,
677 initial_duration,
678 augmentation_duration,
679 max_duration,
680 blind_duration,
681 sensitivity,
682 };
683
684 let pushed = self.state.lock(|cell| {
685 let mut s = cell.borrow_mut();
686 if let Some(t) = s.triggers.iter_mut().find(|t| t.zone_id == zone_id) {
687 *t = trigger;
688 true
689 } else {
690 s.triggers.push(trigger).is_ok()
691 }
692 });
693 if !pushed {
694 return Err(ErrorCode::ResourceExhausted.into());
695 }
696
697 if let Err(e) = self.hooks.trigger_set(&trigger).await {
698 self.state.lock(|cell| {
699 let mut s = cell.borrow_mut();
700 s.triggers.retain(|t| t.zone_id != zone_id);
701 });
702 return Err(e.into());
703 }
704 ctx.notify_own_attr_changed(AttributeId::Triggers as _);
705 Ok(())
706 }
707
708 async fn handle_remove_trigger(
709 &self,
710 ctx: impl InvokeContext,
711 request: RemoveTriggerRequest<'_>,
712 ) -> Result<(), Error> {
713 let zone_id = request.zone_id()?;
714 let existed = self
715 .state
716 .lock(|cell| cell.borrow().triggers.iter().any(|t| t.zone_id == zone_id));
717 if !existed {
718 return Err(ErrorCode::NotFound.into());
719 }
720 self.hooks.trigger_removed(zone_id).await?;
721 self.state.lock(|cell| {
722 let mut s = cell.borrow_mut();
723 s.triggers.retain(|t| t.zone_id != zone_id);
724 });
725 ctx.notify_own_attr_changed(AttributeId::Triggers as _);
726 Ok(())
727 }
728}
729
730fn write_zone_info<P: TLVBuilderParent, const NV: usize>(
735 builder: ZoneInformationStructBuilder<P>,
736 z: &Zone<NV>,
737) -> Result<P, Error> {
738 let b = builder
739 .zone_id(z.zone_id)?
740 .zone_type(z.zone_type)?
741 .zone_source(z.zone_source)?;
742 b.two_d_cartesian_zone()?
745 .with_some_if(z.zone_type == ZoneTypeEnum::TwoDCARTZone, |zone_b| {
746 let zone_b = zone_b.name(z.name.as_str())?.r#use(z.zone_use)?;
747 let mut va = zone_b.vertices()?;
748 for (x, y) in z.vertices.iter().copied() {
749 va = va.push()?.x(x)?.y(y)?.end()?;
750 }
751 let zone_b = va.end()?;
752 zone_b.color(z.color.as_ref().map(|c| c.as_str()))?.end()
753 })?
754 .end()
755}
756
757fn write_trigger<P: TLVBuilderParent>(
758 builder: ZoneTriggerControlStructBuilder<P>,
759 t: &Trigger,
760) -> Result<P, Error> {
761 builder
762 .zone_id(t.zone_id)?
763 .initial_duration(t.initial_duration)?
764 .augmentation_duration(t.augmentation_duration)?
765 .max_duration(t.max_duration)?
766 .blind_duration(t.blind_duration)?
767 .sensitivity(t.sensitivity)?
768 .end()
769}