1use std::fmt;
17use std::net::Ipv4Addr;
18
19use crate::event::Event;
20use crate::state::{Bay, Device, State};
21use crate::types::{
22 AmpZoneSettings, HiddenStatus, PowerStatus, V2ipAudioFormat, V2ipStreamSources,
23 VolumeMuteStatus,
24};
25use crate::wire::{
26 audio_cmd_header, audio_param, audio_sub, build_amp_zone_settings, build_audio_select_input,
27 build_bay_hide, build_edid_profile, build_rc_action, build_set_bay_name, build_set_volume,
28 build_stats_request, build_target_only, build_v2ip_manual_source_switch,
29 build_v2ip_source_switch, mv_cmd_payload, mv_sub, op, Addressee, BayUid, DeviceUid,
30 EdidProfile, MultiviewerAspectRatio, MultiviewerEdidTemplate, MultiviewerHdcpMode,
31 MultiviewerItcMode, MultiviewerOutputMode, MultiviewerPipPosition, MultiviewerPipSize,
32 MultiviewerSource, MultiviewerViewMode, Opcode, RcAction, SendError, StreamAddr, V2ipStreams,
33 DEVICE_NAME_LEN, V2IP_PORT_AUDIO,
34};
35
36use super::{Remote, Shared};
37
38#[derive(Debug)]
40#[non_exhaustive]
41pub enum ControlError {
42 UnknownDevice(DeviceUid),
44 UnknownBay(BayUid),
46 UnknownSource(String),
48 Unsupported(&'static str),
50 NotReported(&'static str),
55 Send(SendError),
57}
58
59impl fmt::Display for ControlError {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 Self::UnknownDevice(uid) => write!(f, "no device {uid}"),
63 Self::UnknownBay(uid) => write!(f, "no bay {uid}"),
64 Self::UnknownSource(name) => write!(f, "no source named {name:?}"),
65 Self::Unsupported(what) => f.write_str(what),
66 Self::NotReported(what) => write!(f, "{what} has not been reported"),
67 Self::Send(e) => write!(f, "{e}"),
68 }
69 }
70}
71
72impl std::error::Error for ControlError {
73 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
74 match self {
75 Self::Send(e) => Some(e),
76 _ => None,
77 }
78 }
79}
80
81impl From<SendError> for ControlError {
82 fn from(e: SendError) -> Self {
83 Self::Send(e)
84 }
85}
86
87type WriteBack = Box<dyn FnOnce(&mut State, &mut Vec<Event>) + Send>;
94
95struct Command {
97 to: Addressee,
98 opcode: Opcode,
99 payload: Vec<u8>,
100 write_back: Option<WriteBack>,
101}
102
103impl Command {
104 fn new(to: Addressee, opcode: Opcode, payload: Vec<u8>) -> Self {
105 Self {
106 to,
107 opcode,
108 payload,
109 write_back: None,
110 }
111 }
112
113 fn then(mut self, f: impl FnOnce(&mut State, &mut Vec<Event>) + Send + 'static) -> Self {
115 self.write_back = Some(Box::new(f));
116 self
117 }
118}
119
120impl Shared {
121 fn command(
124 &self,
125 prepare: impl FnOnce(&State) -> Result<Command, ControlError>,
126 ) -> Result<(), ControlError> {
127 let command = self.read(prepare)?;
128 self.send(&command.to, command.opcode, &command.payload)?;
129 if let Some(write_back) = command.write_back {
130 self.mutate(|state, ev| write_back(state, ev));
131 }
132 Ok(())
133 }
134}
135
136fn device_of(state: &State, uid: DeviceUid) -> Result<&Device, ControlError> {
137 state.device(uid).ok_or(ControlError::UnknownDevice(uid))
138}
139
140fn bay_of(state: &State, uid: BayUid) -> Result<(&Device, &Bay), ControlError> {
141 let device = device_of(state, uid.device)?;
142 let bay = device.bay(uid.port).ok_or(ControlError::UnknownBay(uid))?;
143 Ok((device, bay))
144}
145
146fn source_streams(device: &Device, port: u16) -> Result<&V2ipStreamSources, ControlError> {
148 let source = device
149 .bay(port)
150 .ok_or(ControlError::UnknownBay(BayUid::new(device.uid, port)))?;
151 device
152 .v2ip_source_for(source)
153 .ok_or(ControlError::NotReported("the source's stream addresses"))
154}
155
156fn v2ip_sink(state: &State, uid: BayUid) -> Result<(&Device, &Bay), ControlError> {
158 let (device, bay) = bay_of(state, uid)?;
159 if !bay.is_v2ip_sink() {
160 return Err(ControlError::Unsupported("routing needs a V2IP sink"));
161 }
162 Ok((device, bay))
163}
164
165fn stored_name(name: &str) -> String {
168 let bytes = name.as_bytes();
169 String::from_utf8_lossy(bytes.get(..DEVICE_NAME_LEN).unwrap_or(bytes)).into_owned()
170}
171
172impl Remote {
173 pub fn select_video_source(&self, sink: BayUid, source_port: u16) -> Result<(), ControlError> {
177 self.shared.command(|state| {
178 let (device, bay) = v2ip_sink(state, sink)?;
179 if !bay.is_output() {
180 return Err(ControlError::Unsupported("not an output bay"));
181 }
182 let streams = source_streams(device, source_port)?;
183 Ok(Command::new(
184 Addressee::device(device),
185 op::V2IP_SOURCE_SWITCH,
186 build_v2ip_source_switch(device.uid, streams.video.ip, Ipv4Addr::UNSPECIFIED),
187 ))
188 })
189 }
190
191 pub fn select_audio_source(&self, sink: BayUid, source_port: u16) -> Result<(), ControlError> {
193 self.shared.command(|state| {
194 let (device, _) = v2ip_sink(state, sink)?;
195 let streams = source_streams(device, source_port)?;
196 Ok(Command::new(
197 Addressee::device(device),
198 op::V2IP_SOURCE_SWITCH,
199 build_v2ip_source_switch(device.uid, Ipv4Addr::UNSPECIFIED, streams.audio.ip),
200 ))
201 })
202 }
203
204 pub fn select_video_source_by_name(
207 &self,
208 sink: BayUid,
209 name: &str,
210 ) -> Result<(), ControlError> {
211 self.select_video_source(sink, self.source_port(sink, name)?)
212 }
213
214 pub fn select_audio_source_addr(
220 &self,
221 sink: BayUid,
222 audio_ip: Ipv4Addr,
223 audio_port: Option<u16>,
224 format: Option<V2ipAudioFormat>,
225 ) -> Result<(), ControlError> {
226 self.shared.command(move |state| {
227 let (device, _) = v2ip_sink(state, sink)?;
228 let streams = V2ipStreams {
229 audio: StreamAddr {
230 ip: audio_ip,
231 port: audio_port.unwrap_or(V2IP_PORT_AUDIO),
232 },
233 ..V2ipStreams::default()
234 };
235 Ok(Command::new(
236 Addressee::device(device),
237 op::V2IP_MANUAL_SRC_SWITCH,
238 build_v2ip_manual_source_switch(device.uid, streams, format),
239 ))
240 })
241 }
242
243 pub fn select_audio_source_by_name(
249 &self,
250 sink: BayUid,
251 name: &str,
252 format: Option<V2ipAudioFormat>,
253 ) -> Result<(), ControlError> {
254 let port = self.source_port(sink, name)?;
255 let Some(format) = format else {
256 return self.select_audio_source(sink, port);
257 };
258 self.shared.command(move |state| {
259 let (device, _) = v2ip_sink(state, sink)?;
260 let audio = source_streams(device, port)?.audio;
261 let streams = V2ipStreams {
262 audio: StreamAddr {
263 ip: audio.ip,
264 port: audio.port,
265 },
266 ..V2ipStreams::default()
267 };
268 Ok(Command::new(
269 Addressee::device(device),
270 op::V2IP_MANUAL_SRC_SWITCH,
271 build_v2ip_manual_source_switch(device.uid, streams, Some(format)),
272 ))
273 })
274 }
275
276 fn source_port(&self, sink: BayUid, name: &str) -> Result<u16, ControlError> {
278 self.shared.read(|state| {
279 let (device, _) = bay_of(state, sink)?;
280 device
281 .bay_by_user_name(name)
282 .map(|b| b.port)
283 .ok_or_else(|| ControlError::UnknownSource(name.to_owned()))
284 })
285 }
286
287 pub fn set_bay_name(&self, bay: BayUid, name: &str) -> Result<(), ControlError> {
291 let name = stored_name(name);
292 self.shared.command(move |state| {
293 let (device, _) = bay_of(state, bay)?;
294 let payload = build_set_bay_name(device.uid, bay.port, &name);
295 Ok(
296 Command::new(Addressee::device(device), op::CHANGE_BAY_NAME, payload).then(
297 move |state, ev| {
298 if let Some(b) = state.bay_mut(bay) {
299 b.set_user_name(name, ev);
300 }
301 },
302 ),
303 )
304 })
305 }
306
307 pub fn set_bay_hidden(&self, bay: BayUid, hidden: bool) -> Result<(), ControlError> {
309 self.shared.command(move |state| {
310 let (device, _) = bay_of(state, bay)?;
311 Ok(Command::new(
312 Addressee::device(device),
313 op::BAY_HIDE,
314 build_bay_hide(device.uid, bay.port, hidden),
315 )
316 .then(move |state, ev| {
317 if let Some(b) = state.bay_mut(bay) {
318 let status = if hidden {
319 HiddenStatus::Hidden
320 } else {
321 HiddenStatus::Visible
322 };
323 b.apply_hidden(status, ev);
324 }
325 }))
326 })
327 }
328
329 pub fn select_edid_profile(
331 &self,
332 bay: BayUid,
333 profile: EdidProfile,
334 ) -> Result<(), ControlError> {
335 self.shared.command(move |state| {
336 let (device, _) = bay_of(state, bay)?;
337 Ok(Command::new(
338 Addressee::device(device),
339 op::BAY_EDID_PROFILE,
340 build_edid_profile(device.uid, profile),
341 )
342 .then(move |state, ev| {
343 if let Some(b) = state.bay_mut(bay) {
344 b.set_edid_profile(profile, ev);
345 }
346 }))
347 })
348 }
349
350 pub fn send_action(&self, bay: BayUid, action: RcAction) -> Result<(), ControlError> {
352 self.shared.command(move |state| {
353 let (device, _) = bay_of(state, bay)?;
354 Ok(Command::new(
355 Addressee::device(device),
356 op::RC_TX_ACTION,
357 build_rc_action(device.uid, bay.port, action),
358 ))
359 })
360 }
361
362 pub fn power_on(&self, bay: BayUid) -> Result<(), ControlError> {
364 self.set_power(bay, RcAction::POWER_ON, PowerStatus::On)
365 }
366
367 pub fn power_off(&self, bay: BayUid) -> Result<(), ControlError> {
369 self.set_power(bay, RcAction::POWER_OFF, PowerStatus::Off)
370 }
371
372 fn set_power(
373 &self,
374 bay: BayUid,
375 action: RcAction,
376 power: PowerStatus,
377 ) -> Result<(), ControlError> {
378 self.shared.command(move |state| {
379 let (device, _) = bay_of(state, bay)?;
380 Ok(Command::new(
381 Addressee::device(device),
382 op::RC_TX_ACTION,
383 build_rc_action(device.uid, bay.port, action),
384 )
385 .then(move |state, ev| {
386 if let Some(b) = state.bay_mut(bay) {
387 b.set_power_status(power, ev);
388 }
389 }))
390 })
391 }
392
393 pub fn set_volume(
404 &self,
405 bay: BayUid,
406 volume: u8,
407 muted: Option<bool>,
408 ) -> Result<(), ControlError> {
409 let volume = volume.min(100);
410 let wanted = VolumeMuteStatus {
411 volume_left: Some(volume),
412 volume_right: Some(volume),
413 muted_left: muted,
414 muted_right: muted,
415 };
416 self.shared.command(move |state| {
417 let target = state.volume_bay(bay);
421 let (device, b) = bay_of(state, target)?;
422 if !b.has_volume_control() {
423 return Err(ControlError::Unsupported("the bay has no volume control"));
424 }
425 Ok(Command::new(
426 Addressee::device(device),
427 op::AUDIO_SET_VOLUME,
428 build_set_volume(device.uid, target.port, wanted),
429 )
430 .then(move |state, ev| {
431 if let Some(device) = state.device_mut(target.device) {
432 device.apply_bay_volume(target.port, wanted, ev);
433 }
434 }))
435 })
436 }
437
438 pub fn volume_up(&self, bay: BayUid) -> Result<(), ControlError> {
440 self.set_volume(bay, self.current_volume(bay)?.saturating_add(1), None)
441 }
442
443 pub fn volume_down(&self, bay: BayUid) -> Result<(), ControlError> {
445 self.set_volume(bay, self.current_volume(bay)?.saturating_sub(1), None)
446 }
447
448 pub fn set_muted(&self, bay: BayUid, muted: bool) -> Result<(), ControlError> {
450 self.set_volume(bay, self.current_volume(bay)?, Some(muted))
451 }
452
453 fn current_volume(&self, bay: BayUid) -> Result<u8, ControlError> {
455 self.shared.read(|state| {
456 let (_, b) = bay_of(state, state.volume_bay(bay))?;
457 b.audio_volume
458 .map(|v| v.volume())
459 .ok_or(ControlError::NotReported("the bay's volume"))
460 })
461 }
462
463 pub fn set_amp_zone_settings(
465 &self,
466 bay: BayUid,
467 settings: AmpZoneSettings,
468 ) -> Result<(), ControlError> {
469 self.shared.command(move |state| {
470 let (device, _) = bay_of(state, bay)?;
471 Ok(Command::new(
472 Addressee::device(device),
473 op::AMP_ZONE_SETTINGS,
474 build_amp_zone_settings(device.uid, bay.port, &settings),
475 )
476 .then(move |state, ev| {
477 if let Some(b) = state.bay_mut(bay) {
478 b.set_amp_settings(settings, ev);
479 }
480 }))
481 })
482 }
483
484 pub fn set_audio_endpoint_muted(
488 &self,
489 device: DeviceUid,
490 endpoint: u16,
491 muted: bool,
492 ) -> Result<(), ControlError> {
493 self.audio_endpoint(device, audio_sub::MUTE, endpoint, u32::from(muted))
494 }
495
496 pub fn set_audio_endpoint_trigger(
498 &self,
499 device: DeviceUid,
500 endpoint: u16,
501 active: bool,
502 ) -> Result<(), ControlError> {
503 self.audio_endpoint(device, audio_sub::TRIGGER, endpoint, u32::from(active))
504 }
505
506 pub fn set_audio_endpoint_volume(
508 &self,
509 device: DeviceUid,
510 endpoint: u16,
511 volume: u32,
512 ) -> Result<(), ControlError> {
513 self.audio_endpoint(device, audio_sub::VOLUME, endpoint, volume)
514 }
515
516 fn audio_endpoint(
517 &self,
518 device: DeviceUid,
519 sub: u16,
520 endpoint: u16,
521 value: u32,
522 ) -> Result<(), ControlError> {
523 self.shared.command(move |state| {
524 let device = device_of(state, device)?;
525 let mut payload = audio_cmd_header(sub, device.uid);
526 payload.extend_from_slice(&audio_param(endpoint, value));
527 Ok(Command::new(
528 Addressee::device(device),
529 op::V2IP_AUDIO,
530 payload,
531 ))
532 })
533 }
534
535 pub fn select_audio_endpoint_input(
537 &self,
538 sink: DeviceUid,
539 sink_endpoint: u16,
540 source: DeviceUid,
541 source_endpoint: u16,
542 ) -> Result<(), ControlError> {
543 self.shared.command(move |state| {
544 let device = device_of(state, sink)?;
545 Ok(Command::new(
546 Addressee::device(device),
547 op::V2IP_AUDIO,
548 build_audio_select_input(sink, sink_endpoint, source, source_endpoint),
549 ))
550 })
551 }
552
553 pub fn subscribe_v2ip_stats(
557 &self,
558 device: DeviceUid,
559 subscribe: bool,
560 ) -> Result<(), ControlError> {
561 self.shared.command(move |state| {
562 let device = device_of(state, device)?;
563 Ok(Command::new(
564 Addressee::device(device),
565 op::V2IP_STATS,
566 build_stats_request(device.uid, subscribe),
567 ))
568 })
569 }
570
571 pub fn reboot(&self, device: DeviceUid) -> Result<(), ControlError> {
576 self.shared.command(move |state| {
577 let d = device_of(state, device)?;
578 Ok(Command::new(
579 Addressee::device(d),
580 op::SYS_REBOOT,
581 build_target_only(d.uid),
582 )
583 .then(move |state, _| {
584 if let Some(d) = state.device_mut(device) {
585 d.rebooting = true;
586 }
587 }))
588 })
589 }
590
591 pub fn send_monitoring_pulse(&self) -> Result<(), ControlError> {
594 self.shared
595 .send(&Addressee::Broadcast, op::SYS_MONITORING_PULSE, &[])?;
596 Ok(())
597 }
598
599 pub fn set_multiviewer_view_mode(
603 &self,
604 device: DeviceUid,
605 mode: MultiviewerViewMode,
606 ) -> Result<(), ControlError> {
607 self.multiviewer(device, mv_sub::VIEW_MODE, &[mode.to_wire()])
608 }
609
610 pub fn set_multiviewer_video_source(
612 &self,
613 device: DeviceUid,
614 screen: u8,
615 source: MultiviewerSource,
616 ) -> Result<(), ControlError> {
617 self.multiviewer(device, mv_sub::VIDEO_SOURCE, &[screen, source.to_wire()])
618 }
619
620 pub fn set_multiviewer_audio_source(
622 &self,
623 device: DeviceUid,
624 source: MultiviewerSource,
625 ) -> Result<(), ControlError> {
626 self.multiviewer(
629 device,
630 mv_sub::AUDIO_SOURCE,
631 &[source.to_wire().saturating_sub(1)],
632 )
633 }
634
635 pub fn set_multiviewer_audio_volume(
637 &self,
638 device: DeviceUid,
639 volume: u8,
640 muted: bool,
641 ) -> Result<(), ControlError> {
642 self.multiviewer(device, mv_sub::AUDIO_VOLUME, &[volume, u8::from(muted)])
643 }
644
645 pub fn set_multiviewer_edid_template(
647 &self,
648 device: DeviceUid,
649 template: MultiviewerEdidTemplate,
650 ) -> Result<(), ControlError> {
651 self.multiviewer(device, mv_sub::EDID_TEMPLATE, &[template.to_wire()])
652 }
653
654 pub fn set_multiviewer_remote_control(
656 &self,
657 device: DeviceUid,
658 source: MultiviewerSource,
659 ) -> Result<(), ControlError> {
660 self.multiviewer(
663 device,
664 mv_sub::ROUTE_RC,
665 &[source.to_wire().saturating_sub(1)],
666 )
667 }
668
669 pub fn set_multiviewer_pip_size(
671 &self,
672 device: DeviceUid,
673 size: MultiviewerPipSize,
674 ) -> Result<(), ControlError> {
675 self.multiviewer(device, mv_sub::PIP_SIZE, &[size.to_wire()])
676 }
677
678 pub fn set_multiviewer_pip_position(
680 &self,
681 device: DeviceUid,
682 position: MultiviewerPipPosition,
683 ) -> Result<(), ControlError> {
684 self.multiviewer(device, mv_sub::PIP_POSITION, &[position.to_wire()])
685 }
686
687 pub fn set_multiviewer_aspect_ratio(
689 &self,
690 device: DeviceUid,
691 aspect: MultiviewerAspectRatio,
692 ) -> Result<(), ControlError> {
693 self.multiviewer(device, mv_sub::ASPECT, &[aspect.to_wire()])
694 }
695
696 pub fn set_multiviewer_auto_switch(
698 &self,
699 device: DeviceUid,
700 enable: bool,
701 ) -> Result<(), ControlError> {
702 self.multiviewer(device, mv_sub::AUTO_SWITCH, &[u8::from(enable)])
703 }
704
705 pub fn set_multiviewer_output_mode(
707 &self,
708 device: DeviceUid,
709 mode: MultiviewerOutputMode,
710 ) -> Result<(), ControlError> {
711 self.multiviewer(device, mv_sub::OUTPUT_MODE, &[mode.to_wire()])
712 }
713
714 pub fn set_multiviewer_output_itc(
716 &self,
717 device: DeviceUid,
718 mode: MultiviewerItcMode,
719 ) -> Result<(), ControlError> {
720 self.multiviewer(device, mv_sub::OUTPUT_ITC_MODE, &[mode.to_wire()])
721 }
722
723 pub fn set_multiviewer_hdcp_mode(
725 &self,
726 device: DeviceUid,
727 mode: MultiviewerHdcpMode,
728 ) -> Result<(), ControlError> {
729 self.multiviewer(device, mv_sub::HDCP_MODE, &[mode.to_wire()])
730 }
731
732 pub fn set_multiviewer_input_source(
736 &self,
737 device: DeviceUid,
738 input: u8,
739 source: DeviceUid,
740 ) -> Result<(), ControlError> {
741 let mut args = Vec::with_capacity(24);
742 args.extend_from_slice(source.as_bytes());
743 args.push(input);
744 args.extend_from_slice(&[0; 7]);
747 self.multiviewer(device, mv_sub::CONFIG_SOURCE, &args)
748 }
749
750 pub fn multiviewer_auto_route(&self, device: DeviceUid) -> Result<(), ControlError> {
752 self.multiviewer(device, mv_sub::AUTO_ROUTE, &[])
753 }
754
755 fn multiviewer(&self, device: DeviceUid, sub: u8, args: &[u8]) -> Result<(), ControlError> {
756 self.shared.command(|state| {
757 let device = device_of(state, device)?;
758 if !device.is_multiviewer() {
759 return Err(ControlError::Unsupported("the device is not a multiviewer"));
760 }
761 Ok(Command::new(
762 Addressee::device(device),
763 op::V2IP_MULTIVIEWER,
764 mv_cmd_payload(device.uid, sub, args),
765 ))
766 })
767 }
768}