1use core::cell::RefCell;
72use core::future::Future;
73
74use heapless::String as HString;
75
76use crate::dm::{ArrayAttributeRead, Cluster, Dataver, EndptId, InvokeContext, ReadContext};
77use crate::error::{Error, ErrorCode};
78use crate::tlv::{TLVBuilderParent, Utf8Str};
79use crate::utils::storage::Vec;
80use crate::utils::sync::blocking::Mutex;
81use crate::with;
82
83#[allow(unused_imports)]
84pub use crate::dm::clusters::decl::camera_av_settings_user_level_management::*;
85
86use super::super::decl::camera_av_settings_user_level_management as decl;
87
88pub const MAX_PRESET_NAME_LEN: usize = 32;
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97#[cfg_attr(feature = "defmt", derive(defmt::Format))]
98pub enum CamAvSettingsError {
99 Failure,
101 NotFound,
103 ResourceExhausted,
105 DynamicConstraint,
108}
109
110impl From<CamAvSettingsError> for Error {
111 fn from(e: CamAvSettingsError) -> Self {
112 match e {
113 CamAvSettingsError::Failure => ErrorCode::Failure.into(),
114 CamAvSettingsError::NotFound => ErrorCode::NotFound.into(),
115 CamAvSettingsError::ResourceExhausted => ErrorCode::ResourceExhausted.into(),
116 CamAvSettingsError::DynamicConstraint => ErrorCode::ConstraintError.into(),
117 }
118 }
119}
120
121#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
124#[cfg_attr(feature = "defmt", derive(defmt::Format))]
125pub struct Mptz {
126 pub pan: Option<i16>,
127 pub tilt: Option<i16>,
128 pub zoom: Option<u8>,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
133pub struct MptzPreset {
134 pub preset_id: u8,
135 pub name: HString<MAX_PRESET_NAME_LEN>,
136 pub settings: Mptz,
137}
138
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142#[cfg_attr(feature = "defmt", derive(defmt::Format))]
143pub struct DptzView {
144 pub video_stream_id: u16,
145 pub x1: u16,
146 pub y1: u16,
147 pub x2: u16,
148 pub y2: u16,
149}
150
151impl DptzView {
152 fn validate(&self) -> Result<(), Error> {
153 if self.x2 <= self.x1 || self.y2 <= self.y1 {
154 return Err(ErrorCode::ConstraintError.into());
155 }
156 Ok(())
157 }
158}
159
160#[derive(Debug, Clone, Copy)]
167pub struct CamAvSettingsConfig {
168 pub pan_range: (i16, i16),
171 pub tilt_range: (i16, i16),
174 pub zoom_max: u8,
177 pub default_position: Mptz,
179 pub max_presets: u8,
182}
183
184#[allow(unused_variables)]
187pub trait CamAvSettingsHooks {
188 fn mptz_apply(&self, target: &Mptz) -> impl Future<Output = Result<(), CamAvSettingsError>> {
195 async { Ok(()) }
196 }
197
198 fn dptz_apply(&self, view: &DptzView) -> impl Future<Output = Result<(), CamAvSettingsError>> {
203 async { Ok(()) }
204 }
205
206 fn preset_saved(
211 &self,
212 preset: &MptzPreset,
213 ) -> impl Future<Output = Result<(), CamAvSettingsError>> {
214 async { Ok(()) }
215 }
216
217 fn preset_removed(
219 &self,
220 preset_id: u8,
221 ) -> impl Future<Output = Result<(), CamAvSettingsError>> {
222 async { Ok(()) }
223 }
224}
225
226impl<T> CamAvSettingsHooks for &T
227where
228 T: CamAvSettingsHooks,
229{
230 fn mptz_apply(&self, target: &Mptz) -> impl Future<Output = Result<(), CamAvSettingsError>> {
231 (**self).mptz_apply(target)
232 }
233
234 fn dptz_apply(&self, view: &DptzView) -> impl Future<Output = Result<(), CamAvSettingsError>> {
235 (**self).dptz_apply(view)
236 }
237
238 fn preset_saved(
239 &self,
240 preset: &MptzPreset,
241 ) -> impl Future<Output = Result<(), CamAvSettingsError>> {
242 (**self).preset_saved(preset)
243 }
244
245 fn preset_removed(
246 &self,
247 preset_id: u8,
248 ) -> impl Future<Output = Result<(), CamAvSettingsError>> {
249 (**self).preset_removed(preset_id)
250 }
251}
252
253struct State<const NP: usize, const NS: usize> {
258 mptz: Mptz,
259 presets: Vec<MptzPreset, NP>,
260 dptz: Vec<DptzView, NS>,
261 seeded: bool,
262}
263
264impl<const NP: usize, const NS: usize> State<NP, NS> {
265 const fn new() -> Self {
266 Self {
267 mptz: Mptz {
268 pan: None,
269 tilt: None,
270 zoom: None,
271 },
272 presets: Vec::new(),
273 dptz: Vec::new(),
274 seeded: false,
275 }
276 }
277}
278
279pub const CLUSTER_FULL: Cluster<'static> = decl::FULL_CLUSTER
285 .with_revision(1)
286 .with_features(
287 decl::Feature::DIGITAL_PTZ.bits()
288 | decl::Feature::MECHANICAL_PAN.bits()
289 | decl::Feature::MECHANICAL_TILT.bits()
290 | decl::Feature::MECHANICAL_ZOOM.bits()
291 | decl::Feature::MECHANICAL_PRESETS.bits(),
292 )
293 .with_attrs(with!(
294 required;
295 AttributeId::MPTZPosition
296 | AttributeId::MaxPresets
297 | AttributeId::MPTZPresets
298 | AttributeId::DPTZStreams
299 | AttributeId::ZoomMax
300 | AttributeId::TiltMin
301 | AttributeId::TiltMax
302 | AttributeId::PanMin
303 | AttributeId::PanMax
304 | AttributeId::MovementState
305 ))
306 .with_cmds(with!(
307 decl::CommandId::MPTZSetPosition
308 | decl::CommandId::MPTZRelativeMove
309 | decl::CommandId::MPTZMoveToPreset
310 | decl::CommandId::MPTZSavePreset
311 | decl::CommandId::MPTZRemovePreset
312 | decl::CommandId::DPTZSetViewport
313 | decl::CommandId::DPTZRelativeMove
314 ));
315
316pub const CLUSTER_DPTZ_ONLY: Cluster<'static> = decl::FULL_CLUSTER
319 .with_revision(1)
320 .with_features(decl::Feature::DIGITAL_PTZ.bits())
321 .with_attrs(with!(required; AttributeId::DPTZStreams))
322 .with_cmds(with!(
323 decl::CommandId::DPTZSetViewport | decl::CommandId::DPTZRelativeMove
324 ));
325
326pub const CLUSTER_MPTZ_ALL: Cluster<'static> = decl::FULL_CLUSTER
329 .with_revision(1)
330 .with_features(
331 decl::Feature::MECHANICAL_PAN.bits()
332 | decl::Feature::MECHANICAL_TILT.bits()
333 | decl::Feature::MECHANICAL_ZOOM.bits()
334 | decl::Feature::MECHANICAL_PRESETS.bits(),
335 )
336 .with_attrs(with!(
337 required;
338 AttributeId::MPTZPosition
339 | AttributeId::MaxPresets
340 | AttributeId::MPTZPresets
341 | AttributeId::ZoomMax
342 | AttributeId::TiltMin
343 | AttributeId::TiltMax
344 | AttributeId::PanMin
345 | AttributeId::PanMax
346 | AttributeId::MovementState
347 ))
348 .with_cmds(with!(
349 decl::CommandId::MPTZSetPosition
350 | decl::CommandId::MPTZRelativeMove
351 | decl::CommandId::MPTZMoveToPreset
352 | decl::CommandId::MPTZSavePreset
353 | decl::CommandId::MPTZRemovePreset
354 ));
355
356pub struct CamAvSettingsHandler<H, const NP: usize, const NS: usize>
363where
364 H: CamAvSettingsHooks,
365{
366 dataver: Dataver,
367 endpoint_id: EndptId,
368 cluster: Cluster<'static>,
369 config: CamAvSettingsConfig,
370 hooks: H,
371 state: Mutex<RefCell<State<NP, NS>>>,
372}
373
374impl<H, const NP: usize, const NS: usize> CamAvSettingsHandler<H, NP, NS>
375where
376 H: CamAvSettingsHooks,
377{
378 pub const fn new(
384 dataver: Dataver,
385 endpoint_id: EndptId,
386 cluster: Cluster<'static>,
387 config: CamAvSettingsConfig,
388 hooks: H,
389 ) -> Self {
390 Self {
391 dataver,
392 endpoint_id,
393 cluster,
394 config,
395 hooks,
396 state: Mutex::new(RefCell::new(State::new())),
397 }
398 }
399
400 pub const fn adapt(self) -> decl::HandlerAsyncAdaptor<Self> {
403 decl::HandlerAsyncAdaptor(self)
404 }
405
406 pub const fn endpoint_id(&self) -> EndptId {
408 self.endpoint_id
409 }
410
411 pub fn features(&self) -> u32 {
413 self.cluster.feature_map
414 }
415
416 fn has_feature(&self, bit: u32) -> bool {
417 self.features() & bit != 0
418 }
419
420 pub fn add_preallocated_dptz(&self, view: DptzView) -> Result<(), Error> {
424 view.validate()?;
425 self.state.lock(|cell| {
426 let mut s = cell.borrow_mut();
427 if s.dptz
428 .iter()
429 .any(|v| v.video_stream_id == view.video_stream_id)
430 {
431 return Err(Error::from(ErrorCode::ConstraintError));
432 }
433 s.dptz
434 .push(view)
435 .map_err(|_| Error::from(ErrorCode::ResourceExhausted))
436 })?;
437 self.dataver.changed();
438 Ok(())
439 }
440
441 pub fn remove_dptz_stream(&self, video_stream_id: u16) -> Result<(), Error> {
444 self.state.lock(|cell| -> Result<(), Error> {
445 let mut s = cell.borrow_mut();
446 let pos = s
447 .dptz
448 .iter()
449 .position(|v| v.video_stream_id == video_stream_id)
450 .ok_or(Error::from(ErrorCode::NotFound))?;
451 s.dptz.swap_remove(pos);
452 Ok(())
453 })?;
454 self.dataver.changed();
455 Ok(())
456 }
457
458 pub fn add_preallocated_preset(&self, preset: MptzPreset) -> Result<(), Error> {
461 self.validate_preset_id(preset.preset_id)?;
462 self.validate_mptz(&preset.settings)?;
463 if preset.name.is_empty() {
464 return Err(ErrorCode::ConstraintError.into());
465 }
466 self.state.lock(|cell| {
467 let mut s = cell.borrow_mut();
468 if s.presets.iter().any(|p| p.preset_id == preset.preset_id) {
469 return Err(Error::from(ErrorCode::ConstraintError));
470 }
471 s.presets
472 .push(preset)
473 .map_err(|_| Error::from(ErrorCode::ResourceExhausted))
474 })?;
475 self.dataver.changed();
476 Ok(())
477 }
478
479 pub fn current_mptz(&self) -> Mptz {
482 self.ensure_seeded();
483 self.state.lock(|cell| cell.borrow().mptz)
484 }
485
486 pub fn dptz_streams(&self) -> Vec<DptzView, NS> {
488 self.state.lock(|cell| cell.borrow().dptz.clone())
489 }
490
491 pub fn presets(&self) -> Vec<MptzPreset, NP> {
493 self.state.lock(|cell| cell.borrow().presets.clone())
494 }
495
496 fn ensure_seeded(&self) {
497 self.state.lock(|cell| {
498 let mut s = cell.borrow_mut();
499 if !s.seeded {
500 s.mptz = self.masked_default_position();
501 s.seeded = true;
502 }
503 });
504 }
505
506 fn masked_default_position(&self) -> Mptz {
509 Mptz {
510 pan: if self.has_feature(decl::Feature::MECHANICAL_PAN.bits()) {
511 self.config.default_position.pan
512 } else {
513 None
514 },
515 tilt: if self.has_feature(decl::Feature::MECHANICAL_TILT.bits()) {
516 self.config.default_position.tilt
517 } else {
518 None
519 },
520 zoom: if self.has_feature(decl::Feature::MECHANICAL_ZOOM.bits()) {
521 self.config.default_position.zoom
522 } else {
523 None
524 },
525 }
526 }
527
528 fn any_mptz_axis(&self) -> bool {
529 self.has_feature(decl::Feature::MECHANICAL_PAN.bits())
530 || self.has_feature(decl::Feature::MECHANICAL_TILT.bits())
531 || self.has_feature(decl::Feature::MECHANICAL_ZOOM.bits())
532 }
533
534 fn validate_preset_id(&self, id: u8) -> Result<(), Error> {
535 if id == 0 || id > self.config.max_presets {
536 return Err(ErrorCode::ConstraintError.into());
537 }
538 Ok(())
539 }
540
541 fn validate_mptz(&self, m: &Mptz) -> Result<(), Error> {
545 let pan_en = self.has_feature(decl::Feature::MECHANICAL_PAN.bits());
546 let tilt_en = self.has_feature(decl::Feature::MECHANICAL_TILT.bits());
547 let zoom_en = self.has_feature(decl::Feature::MECHANICAL_ZOOM.bits());
548
549 match (pan_en, m.pan) {
550 (true, Some(v)) if v < self.config.pan_range.0 || v > self.config.pan_range.1 => {
551 return Err(ErrorCode::ConstraintError.into());
552 }
553 (true, None) | (true, Some(_)) => {}
554 (false, Some(_)) => return Err(ErrorCode::ConstraintError.into()),
555 (false, None) => {}
556 }
557 match (tilt_en, m.tilt) {
558 (true, Some(v)) if v < self.config.tilt_range.0 || v > self.config.tilt_range.1 => {
559 return Err(ErrorCode::ConstraintError.into());
560 }
561 (true, None) | (true, Some(_)) => {}
562 (false, Some(_)) => return Err(ErrorCode::ConstraintError.into()),
563 (false, None) => {}
564 }
565 match (zoom_en, m.zoom) {
566 (true, Some(v)) if v < 1 || v > self.config.zoom_max => {
567 return Err(ErrorCode::ConstraintError.into());
568 }
569 (true, None) | (true, Some(_)) => {}
570 (false, Some(_)) => return Err(ErrorCode::ConstraintError.into()),
571 (false, None) => {}
572 }
573 Ok(())
574 }
575
576 fn clamp_axis_i16(&self, lo: i16, hi: i16, v: i32) -> i16 {
577 v.max(lo as i32).min(hi as i32) as i16
578 }
579
580 fn clamp_zoom(&self, v: i32) -> u8 {
581 v.max(1).min(self.config.zoom_max as i32) as u8
582 }
583}
584
585impl<H, const NP: usize, const NS: usize> ClusterAsyncHandler for CamAvSettingsHandler<H, NP, NS>
590where
591 H: CamAvSettingsHooks,
592{
593 const CLUSTER: Cluster<'static> = CLUSTER_FULL;
594
595 fn dataver(&self) -> u32 {
596 self.dataver.get()
597 }
598
599 fn dataver_changed(&self) {
600 self.dataver.changed();
601 }
602
603 async fn mptz_position<P: TLVBuilderParent>(
606 &self,
607 _ctx: impl ReadContext,
608 builder: MPTZStructBuilder<P>,
609 ) -> Result<P, Error> {
610 if !self.any_mptz_axis() {
611 return Err(ErrorCode::AttributeNotFound.into());
612 }
613 self.ensure_seeded();
614 let m = self.state.lock(|cell| cell.borrow().mptz);
615 builder.pan(m.pan)?.tilt(m.tilt)?.zoom(m.zoom)?.end()
616 }
617
618 async fn max_presets(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
619 if !self.has_feature(decl::Feature::MECHANICAL_PRESETS.bits()) {
620 return Err(ErrorCode::AttributeNotFound.into());
621 }
622 Ok(self.config.max_presets.min(NP as u8))
623 }
624
625 async fn mptz_presets<P: TLVBuilderParent>(
626 &self,
627 _ctx: impl ReadContext,
628 builder: ArrayAttributeRead<MPTZPresetStructArrayBuilder<P>, MPTZPresetStructBuilder<P>>,
629 ) -> Result<P, Error> {
630 if !self.has_feature(decl::Feature::MECHANICAL_PRESETS.bits()) {
631 return Err(ErrorCode::AttributeNotFound.into());
632 }
633 let snapshot = self.state.lock(|cell| cell.borrow().presets.clone());
634 match builder {
635 ArrayAttributeRead::ReadAll(mut b) => {
636 for p in snapshot.iter() {
637 b = write_preset(b.push()?, p)?;
638 }
639 b.end()
640 }
641 ArrayAttributeRead::ReadOne(idx, b) => {
642 let Some(p) = snapshot.get(idx as usize) else {
643 return Err(ErrorCode::ConstraintError.into());
644 };
645 write_preset(b, p)
646 }
647 ArrayAttributeRead::ReadNone(b) => b.end(),
648 }
649 }
650
651 async fn dptz_streams<P: TLVBuilderParent>(
652 &self,
653 _ctx: impl ReadContext,
654 builder: ArrayAttributeRead<DPTZStructArrayBuilder<P>, DPTZStructBuilder<P>>,
655 ) -> Result<P, Error> {
656 if !self.has_feature(decl::Feature::DIGITAL_PTZ.bits()) {
657 return Err(ErrorCode::AttributeNotFound.into());
658 }
659 let snapshot = self.state.lock(|cell| cell.borrow().dptz.clone());
660 match builder {
661 ArrayAttributeRead::ReadAll(mut b) => {
662 for v in snapshot.iter() {
663 b = write_dptz(b.push()?, v)?;
664 }
665 b.end()
666 }
667 ArrayAttributeRead::ReadOne(idx, b) => {
668 let Some(v) = snapshot.get(idx as usize) else {
669 return Err(ErrorCode::ConstraintError.into());
670 };
671 write_dptz(b, v)
672 }
673 ArrayAttributeRead::ReadNone(b) => b.end(),
674 }
675 }
676
677 async fn zoom_max(&self, _ctx: impl ReadContext) -> Result<u8, Error> {
678 if !self.has_feature(decl::Feature::MECHANICAL_ZOOM.bits()) {
679 return Err(ErrorCode::AttributeNotFound.into());
680 }
681 Ok(self.config.zoom_max)
682 }
683
684 async fn tilt_min(&self, _ctx: impl ReadContext) -> Result<i16, Error> {
685 if !self.has_feature(decl::Feature::MECHANICAL_TILT.bits()) {
686 return Err(ErrorCode::AttributeNotFound.into());
687 }
688 Ok(self.config.tilt_range.0)
689 }
690
691 async fn tilt_max(&self, _ctx: impl ReadContext) -> Result<i16, Error> {
692 if !self.has_feature(decl::Feature::MECHANICAL_TILT.bits()) {
693 return Err(ErrorCode::AttributeNotFound.into());
694 }
695 Ok(self.config.tilt_range.1)
696 }
697
698 async fn pan_min(&self, _ctx: impl ReadContext) -> Result<i16, Error> {
699 if !self.has_feature(decl::Feature::MECHANICAL_PAN.bits()) {
700 return Err(ErrorCode::AttributeNotFound.into());
701 }
702 Ok(self.config.pan_range.0)
703 }
704
705 async fn pan_max(&self, _ctx: impl ReadContext) -> Result<i16, Error> {
706 if !self.has_feature(decl::Feature::MECHANICAL_PAN.bits()) {
707 return Err(ErrorCode::AttributeNotFound.into());
708 }
709 Ok(self.config.pan_range.1)
710 }
711
712 async fn movement_state(&self, _ctx: impl ReadContext) -> Result<PhysicalMovementEnum, Error> {
713 if !self.any_mptz_axis() {
714 return Err(ErrorCode::AttributeNotFound.into());
715 }
716 Ok(PhysicalMovementEnum::Idle)
720 }
721
722 async fn handle_mptz_set_position(
725 &self,
726 ctx: impl InvokeContext,
727 request: MPTZSetPositionRequest<'_>,
728 ) -> Result<(), Error> {
729 if !self.any_mptz_axis() {
730 return Err(ErrorCode::InvalidAction.into());
731 }
732 self.ensure_seeded();
733
734 let req_pan = request.pan()?;
735 let req_tilt = request.tilt()?;
736 let req_zoom = request.zoom()?;
737
738 let prior = self.state.lock(|cell| cell.borrow().mptz);
739 let target = Mptz {
740 pan: if self.has_feature(decl::Feature::MECHANICAL_PAN.bits()) {
741 req_pan.or(prior.pan)
742 } else {
743 None
744 },
745 tilt: if self.has_feature(decl::Feature::MECHANICAL_TILT.bits()) {
746 req_tilt.or(prior.tilt)
747 } else {
748 None
749 },
750 zoom: if self.has_feature(decl::Feature::MECHANICAL_ZOOM.bits()) {
751 req_zoom.or(prior.zoom)
752 } else {
753 None
754 },
755 };
756 if (!self.has_feature(decl::Feature::MECHANICAL_PAN.bits()) && req_pan.is_some())
758 || (!self.has_feature(decl::Feature::MECHANICAL_TILT.bits()) && req_tilt.is_some())
759 || (!self.has_feature(decl::Feature::MECHANICAL_ZOOM.bits()) && req_zoom.is_some())
760 {
761 return Err(ErrorCode::ConstraintError.into());
762 }
763 self.validate_mptz(&target)?;
764
765 self.state.lock(|cell| cell.borrow_mut().mptz = target);
766 if let Err(e) = self.hooks.mptz_apply(&target).await {
767 self.state.lock(|cell| cell.borrow_mut().mptz = prior);
768 return Err(e.into());
769 }
770 ctx.notify_own_attr_changed(AttributeId::MPTZPosition as _);
771 Ok(())
772 }
773
774 async fn handle_mptz_relative_move(
775 &self,
776 ctx: impl InvokeContext,
777 request: MPTZRelativeMoveRequest<'_>,
778 ) -> Result<(), Error> {
779 if !self.any_mptz_axis() {
780 return Err(ErrorCode::InvalidAction.into());
781 }
782 self.ensure_seeded();
783
784 let pan_delta = request.pan_delta()?;
785 let tilt_delta = request.tilt_delta()?;
786 let zoom_delta = request.zoom_delta()?;
787
788 if (!self.has_feature(decl::Feature::MECHANICAL_PAN.bits()) && pan_delta.is_some())
789 || (!self.has_feature(decl::Feature::MECHANICAL_TILT.bits()) && tilt_delta.is_some())
790 || (!self.has_feature(decl::Feature::MECHANICAL_ZOOM.bits()) && zoom_delta.is_some())
791 {
792 return Err(ErrorCode::ConstraintError.into());
793 }
794
795 let prior = self.state.lock(|cell| cell.borrow().mptz);
796 let mut target = prior;
797 if let (true, Some(d)) = (
798 self.has_feature(decl::Feature::MECHANICAL_PAN.bits()),
799 pan_delta,
800 ) {
801 let cur = prior.pan.unwrap_or(0) as i32 + d as i32;
802 target.pan =
803 Some(self.clamp_axis_i16(self.config.pan_range.0, self.config.pan_range.1, cur));
804 }
805 if let (true, Some(d)) = (
806 self.has_feature(decl::Feature::MECHANICAL_TILT.bits()),
807 tilt_delta,
808 ) {
809 let cur = prior.tilt.unwrap_or(0) as i32 + d as i32;
810 target.tilt =
811 Some(self.clamp_axis_i16(self.config.tilt_range.0, self.config.tilt_range.1, cur));
812 }
813 if let (true, Some(d)) = (
814 self.has_feature(decl::Feature::MECHANICAL_ZOOM.bits()),
815 zoom_delta,
816 ) {
817 let cur = prior.zoom.unwrap_or(1) as i32 + d as i32;
818 target.zoom = Some(self.clamp_zoom(cur));
819 }
820
821 self.state.lock(|cell| cell.borrow_mut().mptz = target);
822 if let Err(e) = self.hooks.mptz_apply(&target).await {
823 self.state.lock(|cell| cell.borrow_mut().mptz = prior);
824 return Err(e.into());
825 }
826 ctx.notify_own_attr_changed(AttributeId::MPTZPosition as _);
827 Ok(())
828 }
829
830 async fn handle_mptz_move_to_preset(
831 &self,
832 ctx: impl InvokeContext,
833 request: MPTZMoveToPresetRequest<'_>,
834 ) -> Result<(), Error> {
835 if !self.has_feature(decl::Feature::MECHANICAL_PRESETS.bits()) {
836 return Err(ErrorCode::InvalidAction.into());
837 }
838 let id = request.preset_id()?;
839 self.validate_preset_id(id)?;
840 let preset = self.state.lock(|cell| {
841 cell.borrow()
842 .presets
843 .iter()
844 .find(|p| p.preset_id == id)
845 .cloned()
846 });
847 let Some(preset) = preset else {
848 return Err(ErrorCode::NotFound.into());
849 };
850 self.ensure_seeded();
852 let prior = self.state.lock(|cell| cell.borrow().mptz);
853 self.state
854 .lock(|cell| cell.borrow_mut().mptz = preset.settings);
855 if let Err(e) = self.hooks.mptz_apply(&preset.settings).await {
856 self.state.lock(|cell| cell.borrow_mut().mptz = prior);
857 return Err(e.into());
858 }
859 ctx.notify_own_attr_changed(AttributeId::MPTZPosition as _);
860 Ok(())
861 }
862
863 async fn handle_mptz_save_preset(
864 &self,
865 ctx: impl InvokeContext,
866 request: MPTZSavePresetRequest<'_>,
867 ) -> Result<(), Error> {
868 if !self.has_feature(decl::Feature::MECHANICAL_PRESETS.bits()) {
869 return Err(ErrorCode::InvalidAction.into());
870 }
871 self.ensure_seeded();
872
873 let req_id = request.preset_id()?;
874 let name_str: Utf8Str<'_> = request.name()?;
875 let mut name: HString<MAX_PRESET_NAME_LEN> = HString::new();
876 if name_str.is_empty()
877 || name_str.len() > MAX_PRESET_NAME_LEN
878 || name.push_str(name_str).is_err()
879 {
880 return Err(ErrorCode::ConstraintError.into());
881 }
882
883 let id = match req_id {
884 Some(i) => {
885 self.validate_preset_id(i)?;
886 i
887 }
888 None => {
889 let max_id = self.config.max_presets.min(NP as u8);
892 let used: heapless::Vec<u8, NP> = self.state.lock(|cell| {
893 let mut v: heapless::Vec<u8, NP> = heapless::Vec::new();
894 for p in cell.borrow().presets.iter() {
895 let _ = v.push(p.preset_id);
896 }
897 v
898 });
899 let mut chosen: Option<u8> = None;
900 for cand in 1..=max_id {
901 if !used.contains(&cand) {
902 chosen = Some(cand);
903 break;
904 }
905 }
906 chosen.ok_or_else(|| Error::from(ErrorCode::ResourceExhausted))?
907 }
908 };
909
910 let settings = self.state.lock(|cell| cell.borrow().mptz);
911 self.validate_mptz(&settings)?;
912 let preset = MptzPreset {
913 preset_id: id,
914 name,
915 settings,
916 };
917
918 let prior = self.state.lock(|cell| {
920 cell.borrow()
921 .presets
922 .iter()
923 .find(|p| p.preset_id == id)
924 .cloned()
925 });
926 let pushed = self.state.lock(|cell| {
927 let mut s = cell.borrow_mut();
928 if let Some(p) = s.presets.iter_mut().find(|p| p.preset_id == id) {
929 *p = preset.clone();
930 true
931 } else {
932 s.presets.push(preset.clone()).is_ok()
933 }
934 });
935 if !pushed {
936 return Err(ErrorCode::ResourceExhausted.into());
937 }
938
939 if let Err(e) = self.hooks.preset_saved(&preset).await {
940 self.state.lock(|cell| {
941 let mut s = cell.borrow_mut();
942 match prior {
943 Some(p) => {
944 if let Some(slot) = s.presets.iter_mut().find(|x| x.preset_id == id) {
945 *slot = p;
946 }
947 }
948 None => s.presets.retain(|x| x.preset_id != id),
949 }
950 });
951 return Err(e.into());
952 }
953 ctx.notify_own_attr_changed(AttributeId::MPTZPresets as _);
954 Ok(())
955 }
956
957 async fn handle_mptz_remove_preset(
958 &self,
959 ctx: impl InvokeContext,
960 request: MPTZRemovePresetRequest<'_>,
961 ) -> Result<(), Error> {
962 if !self.has_feature(decl::Feature::MECHANICAL_PRESETS.bits()) {
963 return Err(ErrorCode::InvalidAction.into());
964 }
965 let id = request.preset_id()?;
966 self.validate_preset_id(id)?;
967 let existed = self
968 .state
969 .lock(|cell| cell.borrow().presets.iter().any(|p| p.preset_id == id));
970 if !existed {
971 return Err(ErrorCode::NotFound.into());
972 }
973 self.hooks.preset_removed(id).await?;
974 self.state.lock(|cell| {
975 let mut s = cell.borrow_mut();
976 s.presets.retain(|p| p.preset_id != id);
977 });
978 ctx.notify_own_attr_changed(AttributeId::MPTZPresets as _);
979 Ok(())
980 }
981
982 async fn handle_dptz_set_viewport(
983 &self,
984 ctx: impl InvokeContext,
985 request: DPTZSetViewportRequest<'_>,
986 ) -> Result<(), Error> {
987 if !self.has_feature(decl::Feature::DIGITAL_PTZ.bits()) {
988 return Err(ErrorCode::InvalidAction.into());
989 }
990 let stream_id = request.video_stream_id()?;
991 let vp = request.viewport()?;
992 let view = DptzView {
993 video_stream_id: stream_id,
994 x1: vp.x_1()?,
995 y1: vp.y_1()?,
996 x2: vp.x_2()?,
997 y2: vp.y_2()?,
998 };
999 view.validate()?;
1000
1001 let prior = self.state.lock(|cell| {
1002 cell.borrow()
1003 .dptz
1004 .iter()
1005 .find(|v| v.video_stream_id == stream_id)
1006 .copied()
1007 });
1008 let pushed = self.state.lock(|cell| {
1009 let mut s = cell.borrow_mut();
1010 if let Some(slot) = s.dptz.iter_mut().find(|v| v.video_stream_id == stream_id) {
1011 *slot = view;
1012 true
1013 } else {
1014 s.dptz.push(view).is_ok()
1015 }
1016 });
1017 if !pushed {
1018 return Err(ErrorCode::ResourceExhausted.into());
1019 }
1020
1021 if let Err(e) = self.hooks.dptz_apply(&view).await {
1022 self.state.lock(|cell| {
1023 let mut s = cell.borrow_mut();
1024 match prior {
1025 Some(p) => {
1026 if let Some(slot) =
1027 s.dptz.iter_mut().find(|v| v.video_stream_id == stream_id)
1028 {
1029 *slot = p;
1030 }
1031 }
1032 None => s.dptz.retain(|v| v.video_stream_id != stream_id),
1033 }
1034 });
1035 return Err(e.into());
1036 }
1037 ctx.notify_own_attr_changed(AttributeId::DPTZStreams as _);
1038 Ok(())
1039 }
1040
1041 async fn handle_dptz_relative_move(
1042 &self,
1043 ctx: impl InvokeContext,
1044 request: DPTZRelativeMoveRequest<'_>,
1045 ) -> Result<(), Error> {
1046 if !self.has_feature(decl::Feature::DIGITAL_PTZ.bits()) {
1047 return Err(ErrorCode::InvalidAction.into());
1048 }
1049 let stream_id = request.video_stream_id()?;
1050 let dx = request.delta_x()?.unwrap_or(0) as i32;
1051 let dy = request.delta_y()?.unwrap_or(0) as i32;
1052 let dz = request.zoom_delta()?.unwrap_or(0) as i32;
1053
1054 let prior = self.state.lock(|cell| {
1055 cell.borrow()
1056 .dptz
1057 .iter()
1058 .find(|v| v.video_stream_id == stream_id)
1059 .copied()
1060 });
1061 let Some(prior) = prior else {
1062 return Err(ErrorCode::NotFound.into());
1063 };
1064
1065 let mut x1 = prior.x1 as i32 + dx;
1070 let mut x2 = prior.x2 as i32 + dx;
1071 let mut y1 = prior.y1 as i32 + dy;
1072 let mut y2 = prior.y2 as i32 + dy;
1073 x1 = x1.max(0);
1074 y1 = y1.max(0);
1075 x2 = x2.max(x1 + 1);
1076 y2 = y2.max(y1 + 1);
1077 if dz != 0 {
1078 let half = dz / 2;
1079 x1 = (x1 - half).max(0);
1080 y1 = (y1 - half).max(0);
1081 x2 = (x2 + half).max(x1 + 1);
1082 y2 = (y2 + half).max(y1 + 1);
1083 }
1084 let view = DptzView {
1085 video_stream_id: stream_id,
1086 x1: x1.min(u16::MAX as i32) as u16,
1087 y1: y1.min(u16::MAX as i32) as u16,
1088 x2: x2.min(u16::MAX as i32) as u16,
1089 y2: y2.min(u16::MAX as i32) as u16,
1090 };
1091 view.validate()?;
1092
1093 self.state.lock(|cell| {
1094 let mut s = cell.borrow_mut();
1095 if let Some(slot) = s.dptz.iter_mut().find(|v| v.video_stream_id == stream_id) {
1096 *slot = view;
1097 }
1098 });
1099 if let Err(e) = self.hooks.dptz_apply(&view).await {
1100 self.state.lock(|cell| {
1101 let mut s = cell.borrow_mut();
1102 if let Some(slot) = s.dptz.iter_mut().find(|v| v.video_stream_id == stream_id) {
1103 *slot = prior;
1104 }
1105 });
1106 return Err(e.into());
1107 }
1108 ctx.notify_own_attr_changed(AttributeId::DPTZStreams as _);
1109 Ok(())
1110 }
1111}
1112
1113fn write_preset<P: TLVBuilderParent>(
1118 builder: MPTZPresetStructBuilder<P>,
1119 p: &MptzPreset,
1120) -> Result<P, Error> {
1121 let b = builder.preset_id(p.preset_id)?.name(p.name.as_str())?;
1122 let s = b.settings()?;
1123 s.pan(p.settings.pan)?
1124 .tilt(p.settings.tilt)?
1125 .zoom(p.settings.zoom)?
1126 .end()?
1127 .end()
1128}
1129
1130fn write_dptz<P: TLVBuilderParent>(
1131 builder: DPTZStructBuilder<P>,
1132 v: &DptzView,
1133) -> Result<P, Error> {
1134 let b = builder.video_stream_id(v.video_stream_id)?;
1135 b.viewport()?
1136 .x_1(v.x1)?
1137 .y_1(v.y1)?
1138 .x_2(v.x2)?
1139 .y_2(v.y2)?
1140 .end()?
1141 .end()
1142}