mx_remote/runtime/control.rs
1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! The control surface: what a caller can ask a device to do.
5//!
6//! Every method here has the same shape. It reads the registry to decide what
7//! to send, releases that lock, transmits, and only then writes back what the
8//! device will have done. The order is what makes a handler woken by the
9//! write-back free to call in again, and it keeps the receive thread from
10//! waiting on a socket write for the lock it needs to decode.
11//!
12//! Nothing here reaches the wire on its own: a payload is bytes until the
13//! single transmit path stamps and writes it, which is where the addressee's
14//! protocol version is checked.
15//!
16//! The multiviewer and audio-endpoint methods are served by loadable modules
17//! rather than by the device firmware, and a model may not load modules at
18//! all, may not ship that one, or may not support it. Those modules answer
19//! nothing either way, so an `Ok` from one of those methods says a frame left
20//! the socket and no more: "the device did it" and "nothing on the device
21//! handles this" are the same observation from here. Read the state back to
22//! tell them apart. A multiviewer broadcasts its whole status shortly after a
23//! setting it accepted, which serves as that read for every one of its methods
24//! but [`Remote::set_multiviewer_remote_control`] and
25//! [`Remote::set_multiviewer_input_source`], which broadcast nothing.
26
27use std::fmt;
28use std::net::Ipv4Addr;
29use std::time::{SystemTime, UNIX_EPOCH};
30
31use crate::event::Event;
32use crate::state::{Bay, Device, State};
33use crate::types::{
34 AmpZoneSettings, AudioFeatures, HiddenStatus, MultiviewerStatus, PowerStatus, ThirdPartyVendor,
35 UnitNetConfig, V2ipAudioFormat, V2ipDeviceSettings, V2ipDownmix, V2ipOutputMode,
36 V2ipPowerSaveSchedule, V2ipPtpMessages, V2ipRoute, V2ipRouteTarget, V2ipScalingSettings,
37 V2ipStreamSources, V2ipTestSync, V2ipTestTone, V2ipTestcard, V2ipVlan, VideoWallOp,
38 VideoWallWindow, VolumeMuteStatus, MULTIVIEWER_INPUTS, OSD_MESSAGE_TEXT_MAX,
39 SCALING_FLAG_AUTO_SCALING, SCALING_FLAG_MODE_VALID, SCALING_FLAG_OPTIONS_VALID,
40 THIRD_PARTY_PAYLOAD_MAX, V2IP_PTP_DOMAIN_MAX, V2IP_SDP_MAX, V2IP_SINK_ROTATION_MAX,
41 V2IP_VLAN_PORT_SFP, VIDEO_WALL_CLEARED,
42};
43use crate::wire::{
44 audio_cmd_header, audio_param, audio_sub, build_amp_zone_settings, build_audio_select_input,
45 build_bay_hide, build_edid_profile, build_edid_request, build_mesh_operation,
46 build_osd_message, build_privacy_request, build_rc_action, build_rc_key, build_set_bay_name,
47 build_set_volume, build_sink_sdp_command, build_sink_sdp_pages, build_stats_request,
48 build_target_only, build_third_party, build_time_zone, build_unit_command,
49 build_v2ip_device_settings, build_v2ip_downmix, build_v2ip_manual_source_switch,
50 build_v2ip_scaling, build_v2ip_settings_all, build_v2ip_source_switch, build_v2ip_testcard,
51 build_v2ip_vlan, build_video_wall, mesh_op, mv_cmd_payload, mv_sub, op, osd_kind, sdp_type,
52 testcard_part, testcard_type, Addressee, BayUid, DeviceFeature, DeviceUid, EdidProfile,
53 MultiviewerAspectRatio, MultiviewerEdidTemplate, MultiviewerHdcpMode, MultiviewerItcMode,
54 MultiviewerOutputMode, MultiviewerPipPosition, MultiviewerPipSize, MultiviewerSource,
55 MultiviewerViewMode, MxrSignalType, Opcode, OsdMessageFlag, OsdMessagePriority, RcAction,
56 RcKey, SendError, StreamAddr, UnitCommandFlag, UnitCommandKind, V2ipDeviceSetting,
57 V2ipFpgaFeature, V2ipPowerSaveLevel, V2ipPtpMode, V2ipStreams, V2ipTestPattern, V2ipToneMode,
58 V2ipVlanFlag, DEVICE_NAME_LEN, TIME_ZONE_NAME_LEN, TIME_ZONE_RULE_LEN, V2IP_DSCP_MAX,
59 V2IP_IR_PROFILE_MAX, V2IP_IR_PROFILE_NOT_SET, V2IP_PORT_ANC, V2IP_PORT_AUDIO, V2IP_PORT_VIDEO,
60};
61
62use super::{Remote, Shared};
63
64/// Why a control method did nothing.
65#[derive(Debug)]
66#[non_exhaustive]
67pub enum ControlError {
68 /// No device with this identifier has been heard from.
69 UnknownDevice(DeviceUid),
70 /// The device has reported no bay on this port.
71 UnknownBay(BayUid),
72 /// No input bay on the device carries this user-assigned name.
73 UnknownSource(String),
74 /// The addressee does not do what was asked of it.
75 Unsupported(&'static str),
76 /// The request breaks a rule the device is not guaranteed to check.
77 ///
78 /// Nothing was sent. This is the caller's to fix, and it is separate from
79 /// [`ControlError::Unsupported`] because the device would have taken the
80 /// frame: refusing here is this library declining to let a bad value
81 /// reach hardware that may store it rather than reject it.
82 InvalidRequest(&'static str),
83 /// The device has not reported something the request is assembled from.
84 ///
85 /// Unlike [`ControlError::Unsupported`], the same call may succeed once it
86 /// has: this says the value is missing, not that it cannot exist.
87 NotReported(&'static str),
88 /// The frame could not be sent.
89 Send(SendError),
90}
91
92impl fmt::Display for ControlError {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 match self {
95 Self::UnknownDevice(uid) => write!(f, "no device {uid}"),
96 Self::UnknownBay(uid) => write!(f, "no bay {uid}"),
97 Self::UnknownSource(name) => write!(f, "no source named {name:?}"),
98 Self::Unsupported(what) => f.write_str(what),
99 Self::InvalidRequest(what) => f.write_str(what),
100 Self::NotReported(what) => write!(f, "{what} has not been reported"),
101 Self::Send(e) => write!(f, "{e}"),
102 }
103 }
104}
105
106impl std::error::Error for ControlError {
107 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
108 match self {
109 Self::Send(e) => Some(e),
110 _ => None,
111 }
112 }
113}
114
115impl From<SendError> for ControlError {
116 fn from(e: SendError) -> Self {
117 Self::Send(e)
118 }
119}
120
121/// What a command does to this client's copy of the registry once its frame is
122/// away.
123///
124/// A device does not acknowledge a command, so without this a caller that read
125/// back what it just wrote would see the old value until some unrelated report
126/// happened to carry the new one.
127type WriteBack = Box<dyn FnOnce(&mut State, &mut Vec<Event>) + Send>;
128
129/// One command: the frame to send, and what the addressee will do with it.
130struct Command {
131 to: Addressee,
132 opcode: Opcode,
133 payload: Vec<u8>,
134 write_back: Option<WriteBack>,
135}
136
137impl Command {
138 fn new(to: Addressee, opcode: Opcode, payload: Vec<u8>) -> Self {
139 Self {
140 to,
141 opcode,
142 payload,
143 write_back: None,
144 }
145 }
146
147 /// Records what to apply locally once the frame is away.
148 fn then(mut self, f: impl FnOnce(&mut State, &mut Vec<Event>) + Send + 'static) -> Self {
149 self.write_back = Some(Box::new(f));
150 self
151 }
152}
153
154impl Shared {
155 /// Runs one command: prepare under the registry lock, send without it,
156 /// then write back.
157 fn command(
158 &self,
159 prepare: impl FnOnce(&State) -> Result<Command, ControlError>,
160 ) -> Result<(), ControlError> {
161 let command = self.read(prepare)?;
162 self.send(&command.to, command.opcode, &command.payload)?;
163 if let Some(write_back) = command.write_back {
164 self.mutate(|state, ev| write_back(state, ev));
165 }
166 Ok(())
167 }
168}
169
170/// The first protocol version whose devices can act on the automatic address
171/// operation.
172///
173/// Not the stamp: that is the mesh operation opcode's, and a device between
174/// the two takes the frame and ignores an operation it does not know. Not
175/// every device on this version has it either, but none below it does.
176const AUTO_ADDRESSES_PROTOCOL: u16 = 0x2B;
177
178fn device_of(state: &State, uid: DeviceUid) -> Result<&Device, ControlError> {
179 state.device(uid).ok_or(ControlError::UnknownDevice(uid))
180}
181
182/// The device behind `uid`, once it is known to be a OneIP output.
183fn sdp_sink_of(state: &State, uid: DeviceUid) -> Result<&Device, ControlError> {
184 let device = device_of(state, uid)?;
185 if !device.is_v2ip_sink() {
186 return Err(ControlError::Unsupported(
187 "the device is not a OneIP output",
188 ));
189 }
190 Ok(device)
191}
192
193/// The device behind `uid`, once it is known to be a multiviewer.
194fn multiviewer_of(state: &State, uid: DeviceUid) -> Result<&Device, ControlError> {
195 let device = device_of(state, uid)?;
196 if !device.is_multiviewer() {
197 return Err(ControlError::Unsupported("the device is not a multiviewer"));
198 }
199 Ok(device)
200}
201
202/// Wraps one multiviewer sub-command in the envelope every one of them shares.
203fn mv_command(device: &Device, sub: u8, args: &[u8]) -> Command {
204 Command::new(
205 Addressee::device(device),
206 op::V2IP_MULTIVIEWER,
207 mv_cmd_payload(device.uid, sub, args),
208 )
209}
210
211/// The zero-based input a source names, refused when it names none.
212///
213/// A multiviewer reads zero as its first input, so there is no value that says
214/// "no input": a source that names none would arrive as a request to switch to
215/// input 1.
216fn source_index(source: MultiviewerSource, what: &'static str) -> Result<u8, ControlError> {
217 source
218 .to_zero_based()
219 .ok_or(ControlError::InvalidRequest(what))
220}
221
222/// A multiviewer setting within the range its firmware accepts.
223///
224/// Every one of these settings is numbered from one, with zero reserved for
225/// "the device has reported nothing". The device drops a value it does not
226/// know without answering, so a caller sending one would see a send succeed
227/// and the setting stay as it was; this is what turns that into an error.
228fn mv_setting(value: u8, highest: u8, what: &'static str) -> Result<u8, ControlError> {
229 if (1..=highest).contains(&value) {
230 Ok(value)
231 } else {
232 Err(ControlError::InvalidRequest(what))
233 }
234}
235
236fn bay_of(state: &State, uid: BayUid) -> Result<(&Device, &Bay), ControlError> {
237 let device = device_of(state, uid.device)?;
238 let bay = device.bay(uid.port).ok_or(ControlError::UnknownBay(uid))?;
239 Ok((device, bay))
240}
241
242/// The bay a command about `uid` acts on, and its device.
243///
244/// A mesh bay stands for bay 0 of another unit, which holds everything the
245/// mesh bay reports, so a command goes to that unit about that bay. Until
246/// the unit has reported the bay there is nowhere to send it, and it is
247/// refused.
248fn acted_on(state: &State, uid: BayUid) -> Result<(&Device, &Bay), ControlError> {
249 let (device, bay) = bay_of(state, uid)?;
250 if !bay.is_v2ip_remote() {
251 return Ok((device, bay));
252 }
253 let owner = state
254 .mesh_owner(bay)
255 .ok_or(ControlError::NotReported("the bay the mesh bay stands for"))?;
256 Ok((device_of(state, owner.uid().device)?, owner))
257}
258
259/// The streams the source bay on `port` advertises.
260fn source_streams(device: &Device, port: u16) -> Result<&V2ipStreamSources, ControlError> {
261 let source = device
262 .bay(port)
263 .ok_or(ControlError::UnknownBay(BayUid::new(device.uid, port)))?;
264 device
265 .v2ip_source_for(source)
266 .ok_or(ControlError::NotReported("the source's stream addresses"))
267}
268
269/// A sink bay, or the reason it cannot be routed.
270///
271/// Returns the device listing the sink, whose numbering the source ports of
272/// a route are in, then the unit and bay that play the route.
273fn v2ip_sink(state: &State, uid: BayUid) -> Result<(&Device, &Device, &Bay), ControlError> {
274 let (_, bay) = bay_of(state, uid)?;
275 if !bay.is_v2ip_sink() {
276 return Err(ControlError::Unsupported("routing needs a OneIP sink"));
277 }
278 let (unit, sink) = acted_on(state, uid)?;
279 Ok((device_of(state, uid.device)?, unit, sink))
280}
281
282/// One route slot as the wire carries it, substituting the stream's standard
283/// port for an unset one.
284///
285/// An unset address sends the slot zeroed, port included: the firmware reads
286/// the pair together, and a port beside 0.0.0.0 describes nothing.
287fn stream_addr(target: V2ipRouteTarget, standard_port: u16) -> StreamAddr {
288 if target.ip.is_unspecified() {
289 return StreamAddr::default();
290 }
291 StreamAddr {
292 ip: target.ip,
293 port: target.port_or(standard_port),
294 }
295}
296
297/// The protocol from which every unit's audio follows its video for a source
298/// switch naming no audio. Some units below it route no audio instead.
299const AUDIO_FOLLOWS_SINCE: u16 = 0x27;
300
301/// The protocol of the earliest firmware known to carry the current audio
302/// module layout; a unit below it that has not reported its endpoints is
303/// taken to carry the older one.
304const AUDIO_LAYOUT_SINCE: u16 = 0x27;
305
306/// The protocol from which a unit applies a scaling write from management.
307const SCALING_WRITE_PROTOCOL: u16 = 0x2F;
308
309/// The name as the device will store it. A unit keeps 15 bytes of the
310/// [`DEVICE_NAME_LEN`]-byte field, the last being its terminator, so a longer
311/// name is cut there, before any character that would not fit whole.
312fn stored_name(name: &str) -> String {
313 let mut end = name.len().min(DEVICE_NAME_LEN - 1);
314 while !name.is_char_boundary(end) {
315 end -= 1;
316 }
317 name[..end].to_owned()
318}
319
320impl Remote {
321 // ---- routing ----
322
323 /// Routes this OneIP sink's video to the stream a source port advertises.
324 ///
325 /// The sink's audio then follows the video: a separate audio route is
326 /// dropped, so route audio after video to keep them apart. A unit below
327 /// protocol 0x27 is routed the source's audio stream instead, as some of
328 /// those route no audio for "follow the video".
329 pub fn select_video_source(&self, sink: BayUid, source_port: u16) -> Result<(), ControlError> {
330 self.shared.command(|state| {
331 let (listing, device, bay) = v2ip_sink(state, sink)?;
332 if !bay.is_output() {
333 return Err(ControlError::Unsupported("not an output bay"));
334 }
335 let streams = source_streams(listing, source_port)?;
336 // Older firmware routes no audio for an empty audio address rather
337 // than following the video, so it is told the source's audio
338 // stream instead, which every version plays.
339 let audio = if device.hello.supported_protocol < AUDIO_FOLLOWS_SINCE {
340 streams.audio.ip
341 } else {
342 Ipv4Addr::UNSPECIFIED
343 };
344 Ok(Command::new(
345 Addressee::device(device),
346 op::V2IP_SOURCE_SWITCH,
347 build_v2ip_source_switch(device.uid, streams.video.ip, audio),
348 ))
349 })
350 }
351
352 /// Routes this OneIP sink's audio to the stream a source port advertises.
353 pub fn select_audio_source(&self, sink: BayUid, source_port: u16) -> Result<(), ControlError> {
354 self.shared.command(|state| {
355 let (listing, device, _) = v2ip_sink(state, sink)?;
356 let streams = source_streams(listing, source_port)?;
357 Ok(Command::new(
358 Addressee::device(device),
359 op::V2IP_SOURCE_SWITCH,
360 build_v2ip_source_switch(device.uid, Ipv4Addr::UNSPECIFIED, streams.audio.ip),
361 ))
362 })
363 }
364
365 /// Routes this OneIP sink's video to the input bay with the given
366 /// user-assigned name.
367 pub fn select_video_source_by_name(
368 &self,
369 sink: BayUid,
370 name: &str,
371 ) -> Result<(), ControlError> {
372 self.select_video_source(sink, self.source_port(sink, name)?)
373 }
374
375 /// Routes this OneIP sink's audio to a multicast address directly, leaving
376 /// its video and ancillary streams alone.
377 ///
378 /// An unset port is the standard OneIP audio port. A format overrides the
379 /// sample rate and channel count the receiver would otherwise assume.
380 pub fn select_audio_source_addr(
381 &self,
382 sink: BayUid,
383 audio_ip: Ipv4Addr,
384 audio_port: Option<u16>,
385 format: Option<V2ipAudioFormat>,
386 ) -> Result<(), ControlError> {
387 self.shared.command(move |state| {
388 let (_, device, _) = v2ip_sink(state, sink)?;
389 let streams = V2ipStreams {
390 audio: StreamAddr {
391 ip: audio_ip,
392 port: audio_port.unwrap_or(V2IP_PORT_AUDIO),
393 },
394 ..V2ipStreams::default()
395 };
396 Ok(Command::new(
397 Addressee::device(device),
398 op::V2IP_MANUAL_SRC_SWITCH,
399 build_v2ip_manual_source_switch(device.uid, streams, format),
400 ))
401 })
402 }
403
404 /// Routes this OneIP sink's video, audio and ancillary streams to
405 /// multicast groups the caller names.
406 ///
407 /// This is the only way to reach a stream no device on the mesh
408 /// advertises, such as one the host is transmitting itself; a route by
409 /// source port can only name a stream some bay has announced.
410 ///
411 /// Set all three groups. The firmware decides whether a sink has a manual
412 /// route by looking at the video and ancillary groups, so a route that
413 /// leaves either unset does not register as one and the sink falls back to
414 /// the audio source its mesh picks.
415 ///
416 /// An unset `format` sends [`V2ipAudioFormat::STANDARD`] rather than
417 /// omitting the trailer. The firmware stores whatever this frame carries
418 /// and hands it to the FPGA unexamined, so a frame without one leaves a
419 /// zero rate and zero channel count there, which the FPGA rejects and
420 /// which takes the switch down with it.
421 pub fn select_source_addr(
422 &self,
423 sink: BayUid,
424 route: V2ipRoute,
425 format: Option<V2ipAudioFormat>,
426 ) -> Result<(), ControlError> {
427 let streams = V2ipStreams {
428 video: stream_addr(route.video, V2IP_PORT_VIDEO),
429 audio: stream_addr(route.audio, V2IP_PORT_AUDIO),
430 anc: stream_addr(route.anc, V2IP_PORT_ANC),
431 };
432 let format = format.unwrap_or(V2ipAudioFormat::STANDARD);
433 self.shared.command(move |state| {
434 let (_, device, _) = v2ip_sink(state, sink)?;
435 Ok(Command::new(
436 Addressee::device(device),
437 op::V2IP_MANUAL_SRC_SWITCH,
438 build_v2ip_manual_source_switch(device.uid, streams, Some(format)),
439 ))
440 })
441 }
442
443 /// Routes this OneIP sink's audio from the input bay with the given
444 /// user-assigned name.
445 ///
446 /// A format is carried on the manual switch frame, which is the only form
447 /// that can override the receiver's sample rate and channel count.
448 pub fn select_audio_source_by_name(
449 &self,
450 sink: BayUid,
451 name: &str,
452 format: Option<V2ipAudioFormat>,
453 ) -> Result<(), ControlError> {
454 let port = self.source_port(sink, name)?;
455 let Some(format) = format else {
456 return self.select_audio_source(sink, port);
457 };
458 self.shared.command(move |state| {
459 let (listing, device, _) = v2ip_sink(state, sink)?;
460 let audio = source_streams(listing, port)?.audio;
461 let streams = V2ipStreams {
462 audio: StreamAddr {
463 ip: audio.ip,
464 port: audio.port,
465 },
466 ..V2ipStreams::default()
467 };
468 Ok(Command::new(
469 Addressee::device(device),
470 op::V2IP_MANUAL_SRC_SWITCH,
471 build_v2ip_manual_source_switch(device.uid, streams, Some(format)),
472 ))
473 })
474 }
475
476 /// The port of the input bay on `sink`'s device carrying `name`.
477 fn source_port(&self, sink: BayUid, name: &str) -> Result<u16, ControlError> {
478 self.shared.read(|state| {
479 let (device, _) = bay_of(state, sink)?;
480 state
481 .input_by_user_name(device, name)
482 .map(|b| b.port)
483 .ok_or_else(|| ControlError::UnknownSource(name.to_owned()))
484 })
485 }
486
487 // ---- bay settings ----
488
489 /// Renames a bay.
490 pub fn set_bay_name(&self, bay: BayUid, name: &str) -> Result<(), ControlError> {
491 let name = stored_name(name);
492 self.shared.command(move |state| {
493 let (device, b) = acted_on(state, bay)?;
494 let bay = b.uid();
495 let payload = build_set_bay_name(device.uid, bay.port, &name);
496 Ok(
497 Command::new(Addressee::device(device), op::CHANGE_BAY_NAME, payload).then(
498 move |state, ev| {
499 if let Some(b) = state.bay_mut(bay) {
500 b.set_user_name(name, ev);
501 }
502 },
503 ),
504 )
505 })
506 }
507
508 /// Hides a bay from the pickers that list it, or shows it again.
509 pub fn set_bay_hidden(&self, bay: BayUid, hidden: bool) -> Result<(), ControlError> {
510 self.shared.command(move |state| {
511 let (device, b) = acted_on(state, bay)?;
512 let bay = b.uid();
513 Ok(Command::new(
514 Addressee::device(device),
515 op::BAY_HIDE,
516 build_bay_hide(device.uid, bay.port, hidden),
517 )
518 .then(move |state, ev| {
519 if let Some(b) = state.bay_mut(bay) {
520 let status = if hidden {
521 HiddenStatus::Hidden
522 } else {
523 HiddenStatus::Visible
524 };
525 b.apply_hidden(status, ev);
526 }
527 }))
528 })
529 }
530
531 /// Sets the EDID profile an input presents to the source attached to it.
532 pub fn select_edid_profile(
533 &self,
534 bay: BayUid,
535 profile: EdidProfile,
536 ) -> Result<(), ControlError> {
537 self.shared.command(move |state| {
538 let (device, b) = acted_on(state, bay)?;
539 let bay = b.uid();
540 Ok(Command::new(
541 Addressee::device(device),
542 op::BAY_EDID_PROFILE,
543 build_edid_profile(device.uid, profile),
544 )
545 .then(move |state, ev| {
546 if let Some(b) = state.bay_mut(bay) {
547 b.set_edid_profile(profile, ev);
548 }
549 }))
550 })
551 }
552
553 /// Sends a remote-control action to whatever is attached to a bay.
554 pub fn send_action(&self, bay: BayUid, action: RcAction) -> Result<(), ControlError> {
555 self.shared.command(move |state| {
556 let (device, b) = acted_on(state, bay)?;
557 Ok(Command::new(
558 Addressee::device(device),
559 op::RC_TX_ACTION,
560 build_rc_action(device.uid, b.port, action),
561 ))
562 })
563 }
564
565 /// Sends a remote-control key press to whatever is attached to a bay.
566 ///
567 /// The device forwards it over CEC, infrared or IP, whichever that bay is
568 /// configured for; the caller does not choose. An action from
569 /// [`Remote::send_action`] names an outcome instead, and the device
570 /// decides which keys reach it.
571 pub fn send_key(&self, bay: BayUid, key: RcKey) -> Result<(), ControlError> {
572 self.shared.command(move |state| {
573 let (device, b) = acted_on(state, bay)?;
574 Ok(Command::new(
575 Addressee::device(device),
576 op::RC_TX_KEY,
577 build_rc_key(device.uid, b.port, key),
578 ))
579 })
580 }
581
582 /// Powers on the device attached to a bay.
583 pub fn power_on(&self, bay: BayUid) -> Result<(), ControlError> {
584 self.set_power(bay, RcAction::POWER_ON, PowerStatus::On)
585 }
586
587 /// Powers off the device attached to a bay.
588 pub fn power_off(&self, bay: BayUid) -> Result<(), ControlError> {
589 self.set_power(bay, RcAction::POWER_OFF, PowerStatus::Off)
590 }
591
592 fn set_power(
593 &self,
594 bay: BayUid,
595 action: RcAction,
596 power: PowerStatus,
597 ) -> Result<(), ControlError> {
598 self.shared.command(move |state| {
599 let (device, b) = acted_on(state, bay)?;
600 let bay = b.uid();
601 Ok(Command::new(
602 Addressee::device(device),
603 op::RC_TX_ACTION,
604 build_rc_action(device.uid, bay.port, action),
605 )
606 .then(move |state, ev| {
607 if let Some(b) = state.bay_mut(bay) {
608 b.set_power_status(power, ev);
609 }
610 }))
611 })
612 }
613
614 /// Sets a bay's volume, as a percentage, and optionally its mute state.
615 ///
616 /// Both channels are set together: the wire carries them separately, but
617 /// nothing on this surface splits them.
618 ///
619 /// A bay with no volume control of its own is set through its
620 /// [`linked_bay`](crate::BayInfo::linked_bay), so an output wired to an
621 /// amplifier zone reaches that zone. [`volume_up`](Self::volume_up),
622 /// [`volume_down`](Self::volume_down) and [`set_muted`](Self::set_muted)
623 /// follow the same link, and read the volume they step from through it.
624 pub fn set_volume(
625 &self,
626 bay: BayUid,
627 volume: u8,
628 muted: Option<bool>,
629 ) -> Result<(), ControlError> {
630 let volume = volume.min(100);
631 let wanted = VolumeMuteStatus {
632 volume_left: Some(volume),
633 volume_right: Some(volume),
634 muted_left: muted,
635 muted_right: muted,
636 };
637 self.shared.command(move |state| {
638 // The mesh may put this bay's volume control on another device, and
639 // the command belongs where the volume lives, not where it was
640 // addressed.
641 let target = state.volume_bay(acted_on(state, bay)?.1.uid());
642 let (device, b) = bay_of(state, target)?;
643 if !b.has_volume_control() {
644 return Err(ControlError::Unsupported("the bay has no volume control"));
645 }
646 Ok(Command::new(
647 Addressee::device(device),
648 op::AUDIO_SET_VOLUME,
649 build_set_volume(device.uid, target.port, wanted),
650 )
651 .then(move |state, ev| {
652 if let Some(device) = state.device_mut(target.device) {
653 device.apply_bay_volume(target.port, wanted, ev);
654 }
655 }))
656 })
657 }
658
659 /// Raises a bay's volume by one percent.
660 ///
661 /// The step carries the mute state the bay last reported rather than
662 /// "unchanged", which an older matrix reads as mute.
663 pub fn volume_up(&self, bay: BayUid) -> Result<(), ControlError> {
664 let (volume, muted) = self.current_volume(bay)?;
665 self.set_volume(bay, volume.saturating_add(1), muted)
666 }
667
668 /// Lowers a bay's volume by one percent, as [`Remote::volume_up`] does.
669 pub fn volume_down(&self, bay: BayUid) -> Result<(), ControlError> {
670 let (volume, muted) = self.current_volume(bay)?;
671 self.set_volume(bay, volume.saturating_sub(1), muted)
672 }
673
674 /// Mutes or unmutes a bay, keeping the volume it is set to.
675 pub fn set_muted(&self, bay: BayUid, muted: bool) -> Result<(), ControlError> {
676 self.set_volume(bay, self.current_volume(bay)?.0, Some(muted))
677 }
678
679 /// The volume a step or a mute is relative to, and the mute state it
680 /// keeps.
681 fn current_volume(&self, bay: BayUid) -> Result<(u8, Option<bool>), ControlError> {
682 self.shared.read(|state| {
683 let (_, b) = bay_of(state, state.volume_bay(acted_on(state, bay)?.1.uid()))?;
684 b.audio_volume
685 .map(|v| (v.volume(), v.muted()))
686 .ok_or(ControlError::NotReported("the bay's volume"))
687 })
688 }
689
690 /// Applies amplifier settings to a zone.
691 pub fn set_amp_zone_settings(
692 &self,
693 bay: BayUid,
694 settings: AmpZoneSettings,
695 ) -> Result<(), ControlError> {
696 self.shared.command(move |state| {
697 let (device, b) = acted_on(state, bay)?;
698 let bay = b.uid();
699 Ok(Command::new(
700 Addressee::device(device),
701 op::AMP_ZONE_SETTINGS,
702 build_amp_zone_settings(device.uid, bay.port, &settings),
703 )
704 .then(move |state, ev| {
705 if let Some(b) = state.bay_mut(bay) {
706 b.set_amp_settings(settings, ev);
707 }
708 }))
709 })
710 }
711
712 // ---- audio endpoints ----
713
714 /// Mutes or unmutes an audio endpoint.
715 ///
716 /// Refused unless the endpoint reports [`AudioFeatures::MUTE`]: a unit
717 /// changes nothing for one that does not.
718 pub fn set_audio_endpoint_muted(
719 &self,
720 device: DeviceUid,
721 endpoint: u16,
722 muted: bool,
723 ) -> Result<(), ControlError> {
724 self.featured_audio_endpoint(
725 device,
726 audio_sub::MUTE,
727 endpoint,
728 muted,
729 AudioFeatures::MUTE,
730 )
731 }
732
733 /// Sets an audio endpoint's trigger output.
734 ///
735 /// Refused unless the endpoint reports [`AudioFeatures::TRIGGER`]: a unit
736 /// changes nothing for one that does not.
737 pub fn set_audio_endpoint_trigger(
738 &self,
739 device: DeviceUid,
740 endpoint: u16,
741 active: bool,
742 ) -> Result<(), ControlError> {
743 self.featured_audio_endpoint(
744 device,
745 audio_sub::TRIGGER,
746 endpoint,
747 active,
748 AudioFeatures::TRIGGER,
749 )
750 }
751
752 /// Locks or unlocks the audio source of an audio endpoint: while it is
753 /// locked, a video route change leaves the endpoint's audio source alone.
754 ///
755 /// Refused unless the endpoint reports
756 /// [`AudioFeatures::AUDIO_LOCK`], which
757 /// is the only one the device acts on. The device reports its endpoints
758 /// again once the lock has changed;
759 /// [`AudioEndpoints::status`](crate::AudioEndpoints::status) reads it.
760 pub fn set_audio_endpoint_locked(
761 &self,
762 device: DeviceUid,
763 endpoint: u8,
764 locked: bool,
765 ) -> Result<(), ControlError> {
766 self.featured_audio_endpoint(
767 device,
768 audio_sub::LOCK,
769 u16::from(endpoint),
770 locked,
771 AudioFeatures::AUDIO_LOCK,
772 )
773 }
774
775 /// Sends an on/off endpoint command, refused unless the endpoint reports
776 /// `feature`, the only case a unit acts on.
777 fn featured_audio_endpoint(
778 &self,
779 device: DeviceUid,
780 sub: u16,
781 endpoint: u16,
782 on: bool,
783 feature: AudioFeatures,
784 ) -> Result<(), ControlError> {
785 self.shared.command(move |state| {
786 let d = device_of(state, device)?;
787 let Some(endpoints) = d.audio.as_ref() else {
788 return Err(ControlError::NotReported("the device's audio endpoints"));
789 };
790 let id = u8::try_from(endpoint).ok();
791 if !id
792 .and_then(|id| endpoints.get(id))
793 .is_some_and(|ep| ep.features.has(feature))
794 {
795 return Err(ControlError::Unsupported(
796 "the endpoint does not have that feature",
797 ));
798 }
799 let mut payload = audio_cmd_header(sub, d.uid);
800 payload.extend_from_slice(&audio_param(endpoint, u32::from(on)));
801 Ok(Command::new(Addressee::device(d), op::V2IP_AUDIO, payload))
802 })
803 }
804
805 /// Sets an audio endpoint's volume. Always refused.
806 ///
807 /// No unit acts on the command: the audio module defines it but neither
808 /// sends nor handles it, and a unit on released firmware reads the same
809 /// sub-opcode as a request to select an output.
810 pub fn set_audio_endpoint_volume(
811 &self,
812 _device: DeviceUid,
813 _endpoint: u16,
814 _volume: u32,
815 ) -> Result<(), ControlError> {
816 Err(ControlError::Unsupported(
817 "no unit acts on an endpoint volume",
818 ))
819 }
820
821 /// Routes a source endpoint on one device to a sink endpoint on another.
822 ///
823 /// A unit on released firmware up to 4.4.2 routes only between its own
824 /// endpoints, so a route from another unit to one is refused.
825 pub fn select_audio_endpoint_input(
826 &self,
827 sink: DeviceUid,
828 sink_endpoint: u16,
829 source: DeviceUid,
830 source_endpoint: u16,
831 ) -> Result<(), ControlError> {
832 self.shared.command(move |state| {
833 let device = device_of(state, sink)?;
834 // An audio module on released firmware up to 4.4.2 reads an
835 // 8-byte body naming two of its own endpoints, and the current
836 // one reads 36 bytes naming both units; neither checks the
837 // length, so each is sent only its own.
838 let payload = if device
839 .audio_legacy
840 .unwrap_or(device.hello.supported_protocol < AUDIO_LAYOUT_SINCE)
841 {
842 if source != sink {
843 return Err(ControlError::Unsupported(
844 "the unit routes only its own audio endpoints",
845 ));
846 }
847 let mut payload = audio_cmd_header(audio_sub::SELECT_INPUT, sink);
848 payload.extend_from_slice(&audio_param(sink_endpoint, u32::from(source_endpoint)));
849 payload
850 } else {
851 build_audio_select_input(sink, sink_endpoint, source, source_endpoint)
852 };
853 Ok(Command::new(
854 Addressee::device(device),
855 op::V2IP_AUDIO,
856 payload,
857 ))
858 })
859 }
860
861 // ---- the whole device ----
862
863 /// Starts or stops a OneIP device reporting its transport statistics.
864 ///
865 /// There is no free-running mode: a device reports only while a
866 /// subscription is live, at 1Hz, and the subscription lapses after a
867 /// minute. A caller that wants a continuous feed re-sends inside the
868 /// minute; nothing here re-arms it.
869 ///
870 /// Reports reach [`crate::EventHandler::on_v2ip_stats_changed`] and read
871 /// back through [`Remote::v2ip_stats`]. A device new enough to send it also
872 /// carries what the sink's decoder recovered, in
873 /// [`crate::V2ipDeviceStats::decoder`].
874 pub fn subscribe_v2ip_stats(
875 &self,
876 device: DeviceUid,
877 subscribe: bool,
878 ) -> Result<(), ControlError> {
879 self.shared.command(move |state| {
880 let device = device_of(state, device)?;
881 Ok(Command::new(
882 Addressee::device(device),
883 op::V2IP_STATS,
884 build_stats_request(device.uid, subscribe),
885 ))
886 })
887 }
888
889 /// Asks a device for an EDID: the one the display on its output
890 /// publishes, or the one it presents to the source on its input.
891 ///
892 /// The device answers with a frame the receive path decodes, so the bytes
893 /// arrive at [`crate::EventHandler::on_edid_received`] and stay readable
894 /// through [`Remote::edid`].
895 ///
896 /// Only OneIP hardware handles this opcode. A matrix or an amplifier
897 /// accepts the frame and answers nothing, at any protocol version, so the
898 /// silence that follows is permanent rather than a reply still to come.
899 /// This call cannot tell the two apart and does not try: it reports what
900 /// was sent, and a caller polling for an EDID should ask a device that can
901 /// answer rather than wait on one that cannot.
902 pub fn request_edid(&self, device: DeviceUid, output: bool) -> Result<(), ControlError> {
903 self.shared.command(move |state| {
904 let device = device_of(state, device)?;
905 Ok(Command::new(
906 Addressee::device(device),
907 op::DEV_EDID,
908 build_edid_request(device.uid, output),
909 ))
910 })
911 }
912
913 /// Asks for a detailed signal report from every bay of one device, or -
914 /// with no device named - from every bay on the network.
915 ///
916 /// Devices report on their own when a signal changes, so this is what a
917 /// client that has just started needs: without it, a bay that has been
918 /// showing the same picture for an hour says nothing until it changes.
919 pub fn request_signal_status(&self, device: Option<DeviceUid>) -> Result<(), ControlError> {
920 let Some(device) = device else {
921 self.shared
922 .send(&Addressee::Broadcast, op::BAY_SIGNAL_STATUS, &[])?;
923 return Ok(());
924 };
925 self.shared.command(move |state| {
926 let device = device_of(state, device)?;
927 Ok(Command::new(
928 Addressee::device(device),
929 op::BAY_SIGNAL_STATUS,
930 build_target_only(device.uid),
931 ))
932 })
933 }
934
935 /// Reboots a device.
936 ///
937 /// The device is marked as rebooting once the frame is away, so the
938 /// silence that follows does not read as one that went offline. A device
939 /// on protocol 0x2C or later takes it only from its mesh controller or a
940 /// management application, which this client announces itself as.
941 pub fn reboot(&self, device: DeviceUid) -> Result<(), ControlError> {
942 self.shared.command(move |state| {
943 let d = device_of(state, device)?;
944 Ok(Command::new(
945 Addressee::device(d),
946 op::SYS_REBOOT,
947 build_target_only(d.uid),
948 )
949 .then(move |state, _| {
950 if let Some(d) = state.device_mut(device) {
951 d.rebooting = true;
952 }
953 }))
954 })
955 }
956
957 /// Asks a device to announce itself now.
958 ///
959 /// Its hello marks it online again, so a device suspected to be gone is
960 /// confirmed or ruled out within a second or two rather than at the end of
961 /// its silence window. A device that stays silent is not taken offline
962 /// here: that remains the silence window's call.
963 pub fn ping(&self, device: DeviceUid) -> Result<(), ControlError> {
964 self.shared.command(move |state| {
965 let d = device_of(state, device)?;
966 Ok(Command::new(
967 Addressee::device(d),
968 op::SYS_PING,
969 build_target_only(d.uid),
970 ))
971 })
972 }
973
974 /// Hands a OneIP source's stream addresses back to automatic assignment,
975 /// undoing addresses that were set on it by hand.
976 ///
977 /// The device takes this only from management, which this client
978 /// announces itself as. Nothing acknowledges it: the device's next
979 /// configuration report carries the addresses it ends up with, and
980 /// [`Remote::v2ip_details`] reads them.
981 ///
982 /// Refused for a device that is not a OneIP source, and for one below
983 /// protocol 0x2B, which predates the operation and ignores it.
984 pub fn auto_assign_v2ip_source_addresses(&self, device: DeviceUid) -> Result<(), ControlError> {
985 self.shared.command(move |state| {
986 let d = device_of(state, device)?;
987 if !d.hello.features.has(DeviceFeature::V2IP_SOURCE) {
988 return Err(ControlError::Unsupported(
989 "the device is not a OneIP source",
990 ));
991 }
992 let have = d.hello.supported_protocol;
993 if have != 0 && have < AUTO_ADDRESSES_PROTOCOL {
994 return Err(ControlError::Send(SendError::ProtocolTooOld {
995 serial: d.serial().to_owned(),
996 opcode: op::MESH_OPERATION.0,
997 have,
998 need: AUTO_ADDRESSES_PROTOCOL,
999 }));
1000 }
1001 Ok(Command::new(
1002 Addressee::device(d),
1003 op::MESH_OPERATION,
1004 build_mesh_operation(mesh_op::AUTO_ADDRESSES, d.uid),
1005 ))
1006 })
1007 }
1008
1009 // ---- OneIP session descriptions ----
1010
1011 /// Has a OneIP output play the stream a session description describes.
1012 ///
1013 /// The description goes out in pages of up to 1 KB, and the output plays
1014 /// it once the last page is in. It takes them only from its mesh
1015 /// controller or a management application, which this client announces
1016 /// itself as. Nothing acknowledges the pages: [`Remote::v2ip_sink_sdp`]
1017 /// reads what the output reports next. Refused for an empty description
1018 /// or one longer than [`V2IP_SDP_MAX`](crate::V2IP_SDP_MAX) bytes, for a
1019 /// device that is not a OneIP output, and for one below protocol 0x2E.
1020 pub fn play_v2ip_sdp(&self, device: DeviceUid, sdp: &str) -> Result<(), ControlError> {
1021 if sdp.is_empty() || sdp.len() > V2IP_SDP_MAX {
1022 return Err(ControlError::InvalidRequest(
1023 "a session description of 1 to 65535 bytes",
1024 ));
1025 }
1026 let (to, pages) = self.shared.read(|state| {
1027 let d = sdp_sink_of(state, device)?;
1028 Ok::<_, ControlError>((
1029 Addressee::device(d),
1030 build_sink_sdp_pages(d.uid, sdp.as_bytes()),
1031 ))
1032 })?;
1033 for page in pages {
1034 self.shared.send(&to, op::V2IP_SINK_SDP, &page)?;
1035 }
1036 Ok(())
1037 }
1038
1039 /// Stops the stream a OneIP output plays from a session description. As
1040 /// [`Remote::play_v2ip_sdp`].
1041 pub fn stop_v2ip_sdp(&self, device: DeviceUid) -> Result<(), ControlError> {
1042 self.sink_sdp_command(device, sdp_type::STOP, 0)
1043 }
1044
1045 /// Sets what a OneIP output shows while it plays audio alone, as the wire
1046 /// value. As [`Remote::play_v2ip_sdp`].
1047 pub fn set_v2ip_sdp_screen(&self, device: DeviceUid, screen: u8) -> Result<(), ControlError> {
1048 self.sink_sdp_command(device, sdp_type::SCREEN, screen)
1049 }
1050
1051 /// Asks a OneIP output what it plays from a session description now,
1052 /// rather than at its next report, which comes at least once a minute.
1053 /// As [`Remote::play_v2ip_sdp`].
1054 pub fn request_v2ip_sink_sdp(&self, device: DeviceUid) -> Result<(), ControlError> {
1055 self.sink_sdp_command(device, sdp_type::REQUEST, 0)
1056 }
1057
1058 /// Asks a OneIP source unit how its streams are encrypted now, rather than
1059 /// at its next report, which comes at least once a minute and whenever
1060 /// they change. [`Remote::v2ip_privacy`] reads the answer.
1061 ///
1062 /// Refused for a device that is not a OneIP source, and for one below
1063 /// protocol 0x30, which predates the report.
1064 pub fn request_v2ip_privacy(&self, device: DeviceUid) -> Result<(), ControlError> {
1065 self.shared.command(move |state| {
1066 let d = device_of(state, device)?;
1067 if !d.hello.features.has(DeviceFeature::V2IP_SOURCE) {
1068 return Err(ControlError::Unsupported(
1069 "the device is not a OneIP source",
1070 ));
1071 }
1072 Ok(Command::new(
1073 Addressee::device(d),
1074 op::V2IP_PRIVACY,
1075 build_privacy_request(d.uid),
1076 ))
1077 })
1078 }
1079
1080 /// Asks a unit for the message on its output, which
1081 /// [`Remote::osd_message`] reads once the unit answers.
1082 ///
1083 /// Refused for a device that is not a OneIP output.
1084 pub fn request_osd_message(&self, device: DeviceUid) -> Result<(), ControlError> {
1085 let none = OsdMessagePriority::LOW;
1086 self.send_osd_message(
1087 device,
1088 osd_kind::MESSAGE_REQUEST,
1089 OsdMessageFlag::NONE,
1090 0,
1091 none,
1092 "",
1093 )
1094 }
1095
1096 /// Shows a message on a unit's output, in place of any it has.
1097 ///
1098 /// `logo` is shown with it: 0 the built-in logo, -1 none, 1 to 5 a logo
1099 /// slot. `text` is markup of at most
1100 /// [`OSD_MESSAGE_TEXT_MAX`](crate::OSD_MESSAGE_TEXT_MAX) bytes, with no
1101 /// control character; empty, the logo shows alone.
1102 ///
1103 /// The unit takes it from management, which this client announces itself
1104 /// as, and holds it until it restarts. It answers with the message it
1105 /// then has, which [`Remote::osd_message`] reads, flagged
1106 /// [`REFUSED`](OsdMessageFlag::REFUSED) when its video processor has no
1107 /// message layer. Refused here for a device that is not a OneIP output,
1108 /// and for a logo, priority or text the unit would refuse.
1109 pub fn show_osd_message(
1110 &self,
1111 device: DeviceUid,
1112 logo: i8,
1113 priority: OsdMessagePriority,
1114 text: &str,
1115 ) -> Result<(), ControlError> {
1116 if !(-1..=5).contains(&logo) {
1117 return Err(ControlError::InvalidRequest("a logo of -1 to 5"));
1118 }
1119 if priority > OsdMessagePriority::TOP {
1120 return Err(ControlError::InvalidRequest("no such message priority"));
1121 }
1122 if text.len() > OSD_MESSAGE_TEXT_MAX {
1123 return Err(ControlError::InvalidRequest(
1124 "a message of at most 255 bytes",
1125 ));
1126 }
1127 if text.bytes().any(|b| b < 0x20) {
1128 return Err(ControlError::InvalidRequest(
1129 "a control character in a message",
1130 ));
1131 }
1132 let set = OsdMessageFlag::SET;
1133 self.send_osd_message(device, osd_kind::MESSAGE_SET, set, logo, priority, text)
1134 }
1135
1136 /// Takes the message down from a unit's output. As
1137 /// [`Remote::show_osd_message`].
1138 pub fn clear_osd_message(&self, device: DeviceUid) -> Result<(), ControlError> {
1139 let none = OsdMessagePriority::LOW;
1140 self.send_osd_message(
1141 device,
1142 osd_kind::MESSAGE_SET,
1143 OsdMessageFlag::NONE,
1144 0,
1145 none,
1146 "",
1147 )
1148 }
1149
1150 /// The one send behind the message methods, to a OneIP output.
1151 fn send_osd_message(
1152 &self,
1153 device: DeviceUid,
1154 kind: u8,
1155 flags: OsdMessageFlag,
1156 logo: i8,
1157 priority: OsdMessagePriority,
1158 text: &str,
1159 ) -> Result<(), ControlError> {
1160 self.shared.command(move |state| {
1161 let d = device_of(state, device)?;
1162 if !d.is_v2ip_sink() {
1163 return Err(ControlError::Unsupported(
1164 "the device is not a OneIP output",
1165 ));
1166 }
1167 Ok(Command::new(
1168 Addressee::device(d),
1169 op::V2IP_OSD_SYNC,
1170 build_osd_message(kind, d.uid, flags, logo, priority, text),
1171 ))
1172 })
1173 }
1174
1175 /// Asks every OneIP source unit of the mesh how its streams are encrypted,
1176 /// as [`Remote::request_v2ip_privacy`] asks one. A unit below protocol
1177 /// 0x30 ignores it.
1178 pub fn request_all_v2ip_privacy(&self) -> Result<(), ControlError> {
1179 self.shared.send(
1180 &Addressee::Broadcast,
1181 op::V2IP_PRIVACY,
1182 &build_privacy_request(DeviceUid::ZERO),
1183 )?;
1184 Ok(())
1185 }
1186
1187 /// The one send behind the session description commands.
1188 fn sink_sdp_command(&self, device: DeviceUid, kind: u8, value: u8) -> Result<(), ControlError> {
1189 self.shared.command(move |state| {
1190 let d = sdp_sink_of(state, device)?;
1191 Ok(Command::new(
1192 Addressee::device(d),
1193 op::V2IP_SINK_SDP,
1194 build_sink_sdp_command(d.uid, kind, value),
1195 ))
1196 })
1197 }
1198
1199 // ---- units ----
1200
1201 /// Clears a unit's current system status.
1202 ///
1203 /// The terms of [`Remote::upgrade_unit_fpga`] apply.
1204 pub fn clear_unit_status(&self, device: DeviceUid) -> Result<(), ControlError> {
1205 self.unit_command(
1206 device,
1207 UnitCommandKind::CLEAR_STATUS,
1208 UnitCommandFlag::NONE,
1209 None,
1210 )
1211 }
1212
1213 /// Upgrades a unit's video processor to the latest image it holds, or
1214 /// flashes that image again when `force` is set and it already runs it.
1215 ///
1216 /// The unit takes a command only from its mesh controller or a management
1217 /// application, which this client announces itself as. Nothing
1218 /// acknowledges it: [`Remote::unit_status`] reads what the unit reports
1219 /// next. Refused for a device that is not a OneIP unit, and for one below
1220 /// protocol 0x2C, which predates the command.
1221 pub fn upgrade_unit_fpga(&self, device: DeviceUid, force: bool) -> Result<(), ControlError> {
1222 let flags = if force {
1223 UnitCommandFlag::FORCE
1224 } else {
1225 UnitCommandFlag::NONE
1226 };
1227 self.unit_command(device, UnitCommandKind::FPGA_UPGRADE, flags, None)
1228 }
1229
1230 /// Switches a unit to an IP configuration from DHCP.
1231 ///
1232 /// The terms of [`Remote::set_unit_network`] apply.
1233 pub fn set_unit_dhcp(&self, device: DeviceUid) -> Result<(), ControlError> {
1234 self.unit_command(
1235 device,
1236 UnitCommandKind::NET_CONFIG,
1237 UnitCommandFlag::DHCP,
1238 Some(UnitNetConfig::default()),
1239 )
1240 }
1241
1242 /// Gives a unit a static IP configuration.
1243 ///
1244 /// The address must be one a unit can hold and the netmask a run of ones
1245 /// from the top; anything else is refused before it is sent.
1246 ///
1247 /// The unit applies the change at once and reverts it unless its mesh
1248 /// controller, hearing the unit report it, confirms it in time.
1249 /// [`UnitStatus::network_pending`](crate::UnitStatus::network_pending) says
1250 /// whether it still waits, and
1251 /// [`Remote::confirm_unit_network`] confirms it. Otherwise as
1252 /// [`Remote::upgrade_unit_fpga`].
1253 pub fn set_unit_network(
1254 &self,
1255 device: DeviceUid,
1256 network: UnitNetConfig,
1257 ) -> Result<(), ControlError> {
1258 if !network.is_valid() {
1259 return Err(ControlError::InvalidRequest(
1260 "not an address and netmask a unit can hold",
1261 ));
1262 }
1263 self.unit_command(
1264 device,
1265 UnitCommandKind::NET_CONFIG,
1266 UnitCommandFlag::NONE,
1267 Some(network),
1268 )
1269 }
1270
1271 /// Keeps a unit's pending IP configuration, as its mesh controller does
1272 /// when it hears the unit at its new address.
1273 ///
1274 /// The terms of [`Remote::upgrade_unit_fpga`] apply.
1275 pub fn confirm_unit_network(&self, device: DeviceUid) -> Result<(), ControlError> {
1276 self.unit_command(
1277 device,
1278 UnitCommandKind::NET_CONFIRM,
1279 UnitCommandFlag::NONE,
1280 None,
1281 )
1282 }
1283
1284 /// The one send behind the unit command methods.
1285 fn unit_command(
1286 &self,
1287 device: DeviceUid,
1288 command: UnitCommandKind,
1289 flags: UnitCommandFlag,
1290 network: Option<UnitNetConfig>,
1291 ) -> Result<(), ControlError> {
1292 self.shared.command(move |state| {
1293 let d = device_of(state, device)?;
1294 if !d.is_v2ip() {
1295 return Err(ControlError::Unsupported("the device is not a OneIP unit"));
1296 }
1297 Ok(Command::new(
1298 Addressee::device(d),
1299 op::SYS_UNIT_COMMAND,
1300 build_unit_command(d.uid, command, flags, network.as_ref()),
1301 ))
1302 })
1303 }
1304
1305 /// Sets the time zone of every device that hears it.
1306 ///
1307 /// `zone` is an IANA name such as `Europe/Amsterdam` and `rule` the POSIX
1308 /// TZ rule the devices keep time by, such as `CET-1CEST,M3.5.0,M10.5.0/3`.
1309 /// A device applies the rule and shows the name.
1310 /// Neither may be empty, hold a NUL, or be longer than its field on the
1311 /// wire leaves room for; [`Remote::clear_mesh_time_zone`] sends both empty.
1312 ///
1313 /// The mesh controller takes it too, and announces it from then on with
1314 /// every periodic broadcast. A device takes it only from a management
1315 /// application or the controller, and this client announces itself as a
1316 /// management application.
1317 pub fn set_mesh_time_zone(&self, zone: &str, rule: &str) -> Result<(), ControlError> {
1318 let fits = |s: &str, len: usize| !s.is_empty() && s.len() < len && !s.contains('\0');
1319 if !fits(zone, TIME_ZONE_NAME_LEN) || !fits(rule, TIME_ZONE_RULE_LEN) {
1320 return Err(ControlError::InvalidRequest(
1321 "a time zone name or rule is empty, holds a NUL, or is too long",
1322 ));
1323 }
1324 self.shared.send(
1325 &Addressee::Broadcast,
1326 op::TIME_ZONE,
1327 &build_time_zone(zone, rule),
1328 )?;
1329 Ok(())
1330 }
1331
1332 /// Clears the time zone of every device that hears it, which then keeps
1333 /// UTC. Sent, received and announced as [`Remote::set_mesh_time_zone`]
1334 /// with an empty name and rule.
1335 pub fn clear_mesh_time_zone(&self) -> Result<(), ControlError> {
1336 self.shared.send(
1337 &Addressee::Broadcast,
1338 op::TIME_ZONE,
1339 &build_time_zone("", ""),
1340 )?;
1341 Ok(())
1342 }
1343
1344 /// Sets the clock of every device that hears it to `time`.
1345 ///
1346 /// A device keeps its own clock where that is within 2s of `time`. It takes
1347 /// the time only from a management application or the controller; see
1348 /// [`Remote::set_mesh_time_zone`]. A time before 1970 or past what 32 bits
1349 /// of seconds hold, in 2106, is refused.
1350 pub fn set_mesh_time(&self, time: SystemTime) -> Result<(), ControlError> {
1351 let utc = time
1352 .duration_since(UNIX_EPOCH)
1353 .ok()
1354 .and_then(|d| u32::try_from(d.as_secs()).ok())
1355 .ok_or(ControlError::InvalidRequest(
1356 "the time does not fit the frame",
1357 ))?;
1358 self.shared
1359 .send(&Addressee::Broadcast, op::TIME, &utc.to_le_bytes())?;
1360 Ok(())
1361 }
1362
1363 /// Tells every unit to check its update server for new firmware now.
1364 pub fn send_monitoring_pulse(&self) -> Result<(), ControlError> {
1365 self.shared
1366 .send(&Addressee::Broadcast, op::SYS_MONITORING_PULSE, &[])?;
1367 Ok(())
1368 }
1369
1370 /// Sends an application's own message to every device, under `vendor`.
1371 ///
1372 /// Every other client of this library that has heard this one announce
1373 /// itself receives it at [`crate::EventHandler::on_third_party_message`];
1374 /// this client does not, and neither does one sharing its identifier. No
1375 /// unit is meant to read it. Nothing acknowledges it, so an application
1376 /// that needs to know it arrived has to have its peers answer.
1377 ///
1378 /// A payload over [`crate::THIRD_PARTY_PAYLOAD_MAX`] bytes is refused.
1379 pub fn send_third_party(
1380 &self,
1381 vendor: ThirdPartyVendor,
1382 payload: &[u8],
1383 ) -> Result<(), ControlError> {
1384 if payload.len() > THIRD_PARTY_PAYLOAD_MAX {
1385 return Err(ControlError::InvalidRequest(
1386 "a third-party payload is at most 1444 bytes",
1387 ));
1388 }
1389 self.shared.send(
1390 &Addressee::Broadcast,
1391 op::THIRD_PARTY,
1392 &build_third_party(vendor, payload),
1393 )?;
1394 Ok(())
1395 }
1396
1397 // ---- OneIP scaling ----
1398
1399 /// Turns a OneIP sink's automatic scaling on or off.
1400 ///
1401 /// Refused for a unit below protocol 0x2F: released firmware drops a
1402 /// scaling write from management.
1403 ///
1404 /// Automatic scaling and a configured output mode are separate reasons for
1405 /// a sink to scale, and this moves only the first: a sink with a mode
1406 /// configured goes on scaling to it with automatic scaling off. Turning
1407 /// both off is this call plus [`Remote::clear_v2ip_output_mode`].
1408 ///
1409 /// Nothing acknowledges the frame. Read the sink back through
1410 /// [`Remote::v2ip_details`] to learn what it did, and treat the block as
1411 /// meaningful only where [`crate::DeviceInfo::config_initialised`] is set.
1412 /// **Read any route you still need before writing.** The sink rebuilds
1413 /// and rebroadcasts its subscription in response, and that report can
1414 /// arrive empty for up to a minute; [`crate::DeviceV2ipSink`] says when
1415 /// and why.
1416 pub fn set_v2ip_auto_scaling(
1417 &self,
1418 device: DeviceUid,
1419 enabled: bool,
1420 ) -> Result<(), ControlError> {
1421 let written = if enabled {
1422 SCALING_FLAG_OPTIONS_VALID | SCALING_FLAG_AUTO_SCALING
1423 } else {
1424 SCALING_FLAG_OPTIONS_VALID
1425 };
1426 self.set_v2ip_scaling(device, MxrSignalType::NONE, 0, written, move |cached| {
1427 V2ipScalingSettings {
1428 flags: (cached.flags & !SCALING_FLAG_AUTO_SCALING)
1429 | SCALING_FLAG_OPTIONS_VALID
1430 | (written & SCALING_FLAG_AUTO_SCALING),
1431 ..cached
1432 }
1433 })
1434 }
1435
1436 /// Sets the output format a OneIP sink scales to.
1437 ///
1438 /// Refused for a unit below protocol 0x2F: released firmware drops a
1439 /// scaling write from management.
1440 ///
1441 /// The mode is checked here and nothing is sent if it fails, because every
1442 /// value a sink refuses it refuses in silence. Passing that check is not a
1443 /// guarantee: the sink also weighs the format against the display's EDID
1444 /// and against what its own output stage can produce.
1445 ///
1446 /// **Turn automatic scaling off first if it is on.** A sink refuses a mode
1447 /// whose format the attached display does not list while it is scaling
1448 /// automatically, and refuses it silently. Setting a mode and then turning
1449 /// automatic scaling back on is the order that survives, because the mode
1450 /// is checked while automatic scaling is still off.
1451 ///
1452 /// Configuring a mode is itself a reason to scale, so a sink with one
1453 /// scales whether or not automatic scaling is on.
1454 ///
1455 /// **Pass a descriptor and a refresh rate that agree.** A sink stores both
1456 /// halves and, with its match-source setting on as it ships, reports back
1457 /// the descriptor matching the refresh it holds: a 60Hz descriptor written
1458 /// with a refresh of 50 reads back as that descriptor's 50Hz sibling, once,
1459 /// and stays there. A sink with match-source off reports the descriptor it
1460 /// was given. Either way a pair that agrees reads back unchanged, and the
1461 /// format driven is the same - so this costs a caller nothing except a
1462 /// descriptor it did not write. That substitution shows up on the sink's
1463 /// next report rather than immediately, because
1464 /// [`crate::V2ipScalingSettings`] holds what was written until then.
1465 ///
1466 /// A mode read from the sink's own web interface is not interchangeable
1467 /// with this pair. That interface reports the descriptor's 60Hz sibling and
1468 /// carries the refresh in a field of its own, so writing back what it shows
1469 /// as the mode, on its own, changes the setting rather than restoring it.
1470 /// **Read any route you still need before writing.** The sink rebuilds
1471 /// and rebroadcasts its subscription in response, and that report can
1472 /// arrive empty for up to a minute; [`crate::DeviceV2ipSink`] says when
1473 /// and why.
1474 pub fn set_v2ip_output_mode(
1475 &self,
1476 device: DeviceUid,
1477 mode: V2ipOutputMode,
1478 ) -> Result<(), ControlError> {
1479 mode.validate().map_err(ControlError::InvalidRequest)?;
1480 let signal = mode.to_signal_type();
1481 let refresh = mode.refresh;
1482 self.set_v2ip_scaling(
1483 device,
1484 signal,
1485 refresh,
1486 SCALING_FLAG_MODE_VALID,
1487 move |cached| V2ipScalingSettings {
1488 mode: signal,
1489 refresh,
1490 flags: cached.flags | SCALING_FLAG_MODE_VALID,
1491 },
1492 )
1493 }
1494
1495 /// Clears the output format a OneIP sink is configured to scale to.
1496 ///
1497 /// Refused for a unit below protocol 0x2F: released firmware drops a
1498 /// scaling write from management.
1499 ///
1500 /// The sink stops scaling for that reason and keeps its automatic scaling
1501 /// setting, so a sink scaling for both reasons goes on scaling until
1502 /// [`Remote::set_v2ip_auto_scaling`] turns the other one off.
1503 ///
1504 /// This is the only way to express "no mode configured", and it is what a
1505 /// caller restoring a sink that had none has to send: a sink reports no
1506 /// mode by leaving the mode's valid bit clear, which is not something a
1507 /// write can say.
1508 /// **Read any route you still need before writing.** The sink rebuilds
1509 /// and rebroadcasts its subscription in response, and that report can
1510 /// arrive empty for up to a minute; [`crate::DeviceV2ipSink`] says when
1511 /// and why.
1512 pub fn clear_v2ip_output_mode(&self, device: DeviceUid) -> Result<(), ControlError> {
1513 // The valid bit with a zero descriptor is the clear. The receiver takes
1514 // that branch ahead of validating anything, and ignores the depth,
1515 // colour space and refresh rate beside it.
1516 self.set_v2ip_scaling(
1517 device,
1518 MxrSignalType::NONE,
1519 0,
1520 SCALING_FLAG_MODE_VALID,
1521 |cached| V2ipScalingSettings {
1522 mode: MxrSignalType::NONE,
1523 refresh: 0,
1524 flags: cached.flags & !SCALING_FLAG_MODE_VALID,
1525 },
1526 )
1527 }
1528
1529 /// The one send behind the scaling methods.
1530 ///
1531 /// `written` is the flag byte that goes out, and `applied` says what the
1532 /// sink will report afterwards. The two differ where the wire spells a
1533 /// write differently from the state it produces - clearing a mode is sent
1534 /// as the valid bit over a zero mode and read back as the valid bit clear -
1535 /// so predicting the cached value from the frame alone would leave a
1536 /// caller reading a state no device ever broadcasts.
1537 fn set_v2ip_scaling(
1538 &self,
1539 device: DeviceUid,
1540 mode: MxrSignalType,
1541 refresh: u16,
1542 written: u8,
1543 applied: impl FnOnce(V2ipScalingSettings) -> V2ipScalingSettings + Send + 'static,
1544 ) -> Result<(), ControlError> {
1545 self.shared.command(move |state| {
1546 let d = device_of(state, device)?;
1547 if !d.is_v2ip_sink() {
1548 return Err(ControlError::Unsupported(
1549 "scaling settings need a OneIP sink",
1550 ));
1551 }
1552 // A unit below this protocol drops a scaling write from
1553 // management.
1554 if d.hello.supported_protocol < SCALING_WRITE_PROTOCOL {
1555 return Err(ControlError::Unsupported(
1556 "the unit's firmware ignores scaling writes",
1557 ));
1558 }
1559 Ok(Command::new(
1560 Addressee::device(d),
1561 op::V2IP_DEVICE_CFG,
1562 build_v2ip_scaling(d.uid, mode, refresh, written),
1563 )
1564 .then(move |state, ev| {
1565 if let Some(d) = state.device_mut(device) {
1566 let cached = d.v2ip_scaling();
1567 d.set_v2ip_scaling(applied(cached), ev);
1568 }
1569 }))
1570 })
1571 }
1572
1573 // ---- OneIP device settings ----
1574
1575 /// Switches on/off settings of a OneIP device, all to the same value.
1576 ///
1577 /// `setting` names one or more of [`V2ipDeviceSetting::SWITCHES`], and
1578 /// each must be one the device has reported: a device ignores a setting it
1579 /// does not have, so a write for one would read back as applied here and
1580 /// change nothing there.
1581 ///
1582 /// Nothing acknowledges the frame. The device answers by reporting its
1583 /// settings, and until then [`Remote::v2ip_device_settings`] reads back
1584 /// what was written.
1585 pub fn set_v2ip_device_setting(
1586 &self,
1587 device: DeviceUid,
1588 setting: V2ipDeviceSetting,
1589 enabled: bool,
1590 ) -> Result<(), ControlError> {
1591 if setting.is_empty() || !setting.without(V2ipDeviceSetting::SWITCHES).is_empty() {
1592 return Err(ControlError::InvalidRequest(
1593 "only on/off device settings are switched",
1594 ));
1595 }
1596 self.set_v2ip_device_settings(
1597 device,
1598 V2ipDeviceSettings {
1599 valid: setting,
1600 flags: if enabled {
1601 setting
1602 } else {
1603 V2ipDeviceSetting::NONE
1604 },
1605 ..V2ipDeviceSettings::default()
1606 },
1607 )
1608 }
1609
1610 /// Sets the infrared profile of a OneIP device's global infrared port.
1611 ///
1612 /// `profile` is below [`V2IP_IR_PROFILE_MAX`], and is checked here because
1613 /// a device ignores one out of range. The terms of
1614 /// [`Remote::set_v2ip_device_setting`] apply.
1615 pub fn set_v2ip_ir_profile(&self, device: DeviceUid, profile: i8) -> Result<(), ControlError> {
1616 if !(0..V2IP_IR_PROFILE_MAX).contains(&profile) {
1617 return Err(ControlError::InvalidRequest("no such infrared profile"));
1618 }
1619 self.set_v2ip_device_settings(
1620 device,
1621 V2ipDeviceSettings {
1622 valid: V2ipDeviceSetting::IR_PROFILE,
1623 ir_profile: profile,
1624 ..V2ipDeviceSettings::default()
1625 },
1626 )
1627 }
1628
1629 /// Sets the infrared profile of a OneIP device's output infrared port.
1630 ///
1631 /// [`V2IP_IR_PROFILE_NOT_SET`] makes the port follow the global one.
1632 /// Otherwise as [`Remote::set_v2ip_ir_profile`].
1633 pub fn set_v2ip_sink_ir_profile(
1634 &self,
1635 device: DeviceUid,
1636 profile: i8,
1637 ) -> Result<(), ControlError> {
1638 if !(V2IP_IR_PROFILE_NOT_SET..V2IP_IR_PROFILE_MAX).contains(&profile) {
1639 return Err(ControlError::InvalidRequest("no such infrared profile"));
1640 }
1641 self.set_v2ip_device_settings(
1642 device,
1643 V2ipDeviceSettings {
1644 valid: V2ipDeviceSetting::IR_PROFILE_SINK,
1645 ir_profile_sink: profile,
1646 ..V2ipDeviceSettings::default()
1647 },
1648 )
1649 }
1650
1651 /// Sets how many idle minutes a OneIP device waits before it powers down by
1652 /// itself, 0 for never. The terms of [`Remote::set_v2ip_device_setting`]
1653 /// apply.
1654 pub fn set_v2ip_auto_power_save(
1655 &self,
1656 device: DeviceUid,
1657 minutes: u16,
1658 ) -> Result<(), ControlError> {
1659 self.set_v2ip_device_settings(
1660 device,
1661 V2ipDeviceSettings {
1662 valid: V2ipDeviceSetting::AUTO_POWER_SAVE,
1663 auto_power_save: minutes,
1664 ..V2ipDeviceSettings::default()
1665 },
1666 )
1667 }
1668
1669 /// Sets a OneIP device's daily power save windows.
1670 ///
1671 /// Every time is below [`V2IP_MINUTES_PER_DAY`](crate::V2IP_MINUTES_PER_DAY),
1672 /// and is checked here.
1673 /// The windows are kept in the device's own time zone. The terms of
1674 /// [`Remote::set_v2ip_device_setting`] apply.
1675 pub fn set_v2ip_power_save_schedule(
1676 &self,
1677 device: DeviceUid,
1678 schedule: V2ipPowerSaveSchedule,
1679 ) -> Result<(), ControlError> {
1680 if !schedule.is_valid() {
1681 return Err(ControlError::InvalidRequest(
1682 "a power save time is not a time of day",
1683 ));
1684 }
1685 self.set_v2ip_device_settings(
1686 device,
1687 V2ipDeviceSettings {
1688 valid: V2ipDeviceSetting::POWER_SAVE_SCHEDULE,
1689 power_save: schedule,
1690 ..V2ipDeviceSettings::default()
1691 },
1692 )
1693 }
1694
1695 /// Switches a OneIP device's power save on, so it powers down by itself
1696 /// to the level it picks, or off, so it powers down only when asked.
1697 ///
1698 /// On is written as a mesh controller writes it: both
1699 /// [`POWER_SAVE_DISABLED`](V2ipDeviceSetting::POWER_SAVE_DISABLED), clear,
1700 /// and [`POWER_SAVE_LEVEL`](V2ipDeviceSetting::POWER_SAVE_LEVEL), at 0, so
1701 /// a device whose firmware takes the level as its setting returns to its
1702 /// own default. The terms of [`Remote::set_v2ip_device_setting`] apply.
1703 pub fn set_v2ip_power_save(
1704 &self,
1705 device: DeviceUid,
1706 enabled: bool,
1707 ) -> Result<(), ControlError> {
1708 self.set_v2ip_device_settings(device, power_save_switch(enabled))
1709 }
1710
1711 /// Switches power save on or off on every OneIP device of the mesh with
1712 /// one frame, written as [`Remote::set_v2ip_power_save`] writes it. As
1713 /// [`Remote::set_all_v2ip_device_settings`], which carries the other
1714 /// settings.
1715 pub fn set_all_v2ip_power_save(&self, enabled: bool) -> Result<(), ControlError> {
1716 self.set_all_v2ip_device_settings(power_save_switch(enabled))
1717 }
1718
1719 /// Writes the level a OneIP device powers down to in power save, which
1720 /// switches power save on.
1721 ///
1722 /// A device picks its level itself and ignores the one written, except
1723 /// one whose firmware predates picking it, which takes the level as its
1724 /// setting. A level that unit does not offer is one its
1725 /// [`UnitStatus`](crate::UnitStatus) reports without
1726 /// [`SUPPORTED`](crate::UnitPowerSave::SUPPORTED). The level is not
1727 /// cached: [`V2ipDeviceSettings::power_save_level`] reads what the device
1728 /// reports. [`Remote::set_v2ip_power_save`] switches power save on or off
1729 /// without naming a level. The terms of
1730 /// [`Remote::set_v2ip_device_setting`] apply.
1731 pub fn set_v2ip_power_save_level(
1732 &self,
1733 device: DeviceUid,
1734 level: V2ipPowerSaveLevel,
1735 ) -> Result<(), ControlError> {
1736 if level > V2ipPowerSaveLevel::OFF {
1737 return Err(ControlError::InvalidRequest("no such power save level"));
1738 }
1739 self.set_v2ip_device_settings(
1740 device,
1741 V2ipDeviceSettings {
1742 valid: V2ipDeviceSetting::POWER_SAVE_LEVEL,
1743 power_save_level: level,
1744 ..V2ipDeviceSettings::default()
1745 },
1746 )
1747 }
1748
1749 /// Sets how a OneIP device paces its PTP messages, its SAP timings, its
1750 /// streams' TTL and its output's rotation: the values of `messages` behind
1751 /// the bits of `which`, each one of
1752 /// [`SETTINGS_BLOCK`](V2ipDeviceSetting::SETTINGS_BLOCK). A DSCP above
1753 /// [`V2IP_DSCP_MAX`](crate::V2IP_DSCP_MAX), a stream TTL of 0 and a
1754 /// rotation that is not 0 or a multiple of the step the device reports
1755 /// are refused. The terms of [`Remote::set_v2ip_device_setting`] apply.
1756 pub fn set_v2ip_ptp_messages(
1757 &self,
1758 device: DeviceUid,
1759 which: V2ipDeviceSetting,
1760 messages: V2ipPtpMessages,
1761 ) -> Result<(), ControlError> {
1762 if which.is_empty() || !V2ipDeviceSetting::SETTINGS_BLOCK.has(which) {
1763 return Err(ControlError::InvalidRequest(
1764 "only PTP message, SAP timing, stream TTL and rotation settings are carried here",
1765 ));
1766 }
1767 if which.has(V2ipDeviceSetting::PTP_DSCP) && messages.dscp > V2IP_DSCP_MAX {
1768 return Err(ControlError::InvalidRequest("a DSCP above 63"));
1769 }
1770 if which.has(V2ipDeviceSetting::STREAM_TTL) && messages.stream_ttl == 0 {
1771 return Err(ControlError::InvalidRequest("a stream TTL of 0"));
1772 }
1773 self.set_v2ip_device_settings(
1774 device,
1775 V2ipDeviceSettings {
1776 valid: which,
1777 ptp_messages: messages,
1778 ..V2ipDeviceSettings::default()
1779 },
1780 )
1781 }
1782
1783 /// Sets how a OneIP device's output mixes 5.1 and 7.1 PCM down to stereo:
1784 /// the mode, level and LFE percent of `downmix`, which the device applies
1785 /// together. Its state and channels are the device's to report, and are
1786 /// not sent.
1787 ///
1788 /// Refused before anything is sent for a mode, level or LFE percent out of
1789 /// range, and unless the device reports a downmix, which one whose output
1790 /// cannot mix down never does. The new downmix is cached once the frame
1791 /// is sent, and the device's next report replaces it.
1792 pub fn set_v2ip_downmix(
1793 &self,
1794 device: DeviceUid,
1795 downmix: V2ipDownmix,
1796 ) -> Result<(), ControlError> {
1797 if !downmix.is_valid() {
1798 return Err(ControlError::InvalidRequest(
1799 "no such downmix mode or level, or an LFE above 100 percent",
1800 ));
1801 }
1802 self.shared.command(move |state| {
1803 let d = device_of(state, device)?;
1804 if d.v2ip_settings.is_none() {
1805 return Err(ControlError::NotReported("the device's settings"));
1806 }
1807 let Some(reported) = d.v2ip_downmix else {
1808 return Err(ControlError::Unsupported(
1809 "the device does not mix its output down",
1810 ));
1811 };
1812 Ok(Command::new(
1813 Addressee::device(d),
1814 op::V2IP_DEVICE_CFG,
1815 build_v2ip_downmix(d.uid, &downmix),
1816 )
1817 .then(move |state, ev| {
1818 if let Some(d) = state.device_mut(device) {
1819 d.set_v2ip_downmix(Some(reported.written(downmix)), ev);
1820 }
1821 }))
1822 })
1823 }
1824
1825 /// Sets the PTP mode a OneIP device runs. The terms of
1826 /// [`Remote::set_v2ip_device_setting`] apply.
1827 pub fn set_v2ip_ptp_mode(
1828 &self,
1829 device: DeviceUid,
1830 mode: V2ipPtpMode,
1831 ) -> Result<(), ControlError> {
1832 if mode > V2ipPtpMode::AUTO {
1833 return Err(ControlError::InvalidRequest("no such PTP mode"));
1834 }
1835 self.set_v2ip_device_settings(
1836 device,
1837 V2ipDeviceSettings {
1838 valid: V2ipDeviceSetting::PTP_MODE,
1839 ptp_mode: mode,
1840 ..V2ipDeviceSettings::default()
1841 },
1842 )
1843 }
1844
1845 /// Sets the PTP domain a OneIP device runs in, at most
1846 /// [`V2IP_PTP_DOMAIN_MAX`](crate::V2IP_PTP_DOMAIN_MAX). The terms of
1847 /// [`Remote::set_v2ip_device_setting`] apply.
1848 pub fn set_v2ip_ptp_domain(&self, device: DeviceUid, domain: u8) -> Result<(), ControlError> {
1849 if domain > V2IP_PTP_DOMAIN_MAX {
1850 return Err(ControlError::InvalidRequest("no such PTP domain"));
1851 }
1852 self.set_v2ip_device_settings(
1853 device,
1854 V2ipDeviceSettings {
1855 valid: V2ipDeviceSetting::PTP_DOMAIN,
1856 ptp_domain: domain,
1857 ..V2ipDeviceSettings::default()
1858 },
1859 )
1860 }
1861
1862 /// Sets the priority1 a OneIP device announces as a PTP grandmaster, lower
1863 /// winning. The terms of [`Remote::set_v2ip_device_setting`] apply.
1864 pub fn set_v2ip_ptp_priority1(
1865 &self,
1866 device: DeviceUid,
1867 priority1: u8,
1868 ) -> Result<(), ControlError> {
1869 self.set_v2ip_device_settings(
1870 device,
1871 V2ipDeviceSettings {
1872 valid: V2ipDeviceSetting::PTP_PRIORITY1,
1873 ptp_priority1: priority1,
1874 ..V2ipDeviceSettings::default()
1875 },
1876 )
1877 }
1878
1879 /// Changes settings on every OneIP device of the mesh with one frame.
1880 ///
1881 /// `settings` carries the settings behind their bits in
1882 /// [`valid`](V2ipDeviceSettings::valid), as a device reports them. Each
1883 /// device applies those it has and ignores the rest, and none that
1884 /// predates the frame applies any. A setting only a device reports about
1885 /// itself, a profile, PTP mode, PTP domain, power save level or PTP DSCP
1886 /// out of range, a stream TTL of 0, a rotation above
1887 /// [`V2IP_SINK_ROTATION_MAX`](crate::V2IP_SINK_ROTATION_MAX) and a
1888 /// schedule time that is not a time of day are refused here, since every
1889 /// device would ignore them. Each device takes a rotation only by its own
1890 /// step.
1891 ///
1892 /// Nothing is cached: each device that applies a change reports its
1893 /// settings, and [`Remote::v2ip_device_settings`] reads that.
1894 pub fn set_all_v2ip_device_settings(
1895 &self,
1896 settings: V2ipDeviceSettings,
1897 ) -> Result<(), ControlError> {
1898 let valid = settings.valid;
1899 if valid.is_empty() {
1900 return Err(ControlError::InvalidRequest("no setting is carried"));
1901 }
1902 if !(valid & V2ipDeviceSetting::REPORTED_ONLY).is_empty() {
1903 return Err(ControlError::InvalidRequest(
1904 "a setting only a device reports is carried",
1905 ));
1906 }
1907 if valid.has(V2ipDeviceSetting::IR_PROFILE)
1908 && !(0..V2IP_IR_PROFILE_MAX).contains(&settings.ir_profile)
1909 {
1910 return Err(ControlError::InvalidRequest("no such infrared profile"));
1911 }
1912 if valid.has(V2ipDeviceSetting::IR_PROFILE_SINK)
1913 && !(V2IP_IR_PROFILE_NOT_SET..V2IP_IR_PROFILE_MAX).contains(&settings.ir_profile_sink)
1914 {
1915 return Err(ControlError::InvalidRequest("no such infrared profile"));
1916 }
1917 if valid.has(V2ipDeviceSetting::POWER_SAVE_SCHEDULE) && !settings.power_save.is_valid() {
1918 return Err(ControlError::InvalidRequest(
1919 "a power save time is not a time of day",
1920 ));
1921 }
1922 if valid.has(V2ipDeviceSetting::PTP_MODE) && settings.ptp_mode > V2ipPtpMode::AUTO {
1923 return Err(ControlError::InvalidRequest("no such PTP mode"));
1924 }
1925 if valid.has(V2ipDeviceSetting::PTP_DOMAIN) && settings.ptp_domain > V2IP_PTP_DOMAIN_MAX {
1926 return Err(ControlError::InvalidRequest("no such PTP domain"));
1927 }
1928 if valid.has(V2ipDeviceSetting::POWER_SAVE_LEVEL)
1929 && settings.power_save_level > V2ipPowerSaveLevel::OFF
1930 {
1931 return Err(ControlError::InvalidRequest("no such power save level"));
1932 }
1933 if valid.has(V2ipDeviceSetting::PTP_DSCP) && settings.ptp_messages.dscp > V2IP_DSCP_MAX {
1934 return Err(ControlError::InvalidRequest("a DSCP above 63"));
1935 }
1936 if valid.has(V2ipDeviceSetting::STREAM_TTL) && settings.ptp_messages.stream_ttl == 0 {
1937 return Err(ControlError::InvalidRequest("a stream TTL of 0"));
1938 }
1939 if valid.has(V2ipDeviceSetting::SINK_ROTATION)
1940 && settings.ptp_messages.sink_rotation > V2IP_SINK_ROTATION_MAX
1941 {
1942 return Err(ControlError::InvalidRequest("a rotation above 359 degrees"));
1943 }
1944 self.shared.send(
1945 &Addressee::Broadcast,
1946 op::V2IP_SETTINGS_ALL,
1947 &build_v2ip_settings_all(&settings),
1948 )?;
1949 Ok(())
1950 }
1951
1952 /// The one send behind the device settings methods.
1953 fn set_v2ip_device_settings(
1954 &self,
1955 device: DeviceUid,
1956 settings: V2ipDeviceSettings,
1957 ) -> Result<(), ControlError> {
1958 self.shared.command(move |state| {
1959 let d = device_of(state, device)?;
1960 let Some(reported) = d.v2ip_settings else {
1961 return Err(ControlError::NotReported("the device's settings"));
1962 };
1963 if !reported.valid.has(settings.valid) {
1964 return Err(ControlError::Unsupported(
1965 "the device does not have this setting",
1966 ));
1967 }
1968 if settings.valid.has(V2ipDeviceSetting::SINK_ROTATION)
1969 && !reported
1970 .ptp_messages
1971 .takes_sink_rotation(settings.ptp_messages.sink_rotation)
1972 {
1973 return Err(ControlError::InvalidRequest(
1974 "the device does not turn its output by this angle",
1975 ));
1976 }
1977 let cached = settings.as_applied_to(&reported);
1978 Ok(Command::new(
1979 Addressee::device(d),
1980 op::V2IP_DEVICE_CFG,
1981 build_v2ip_device_settings(d.uid, &settings),
1982 )
1983 .then(move |state, ev| {
1984 if let Some(d) = state.device_mut(device) {
1985 d.merge_v2ip_settings(cached, ev);
1986 }
1987 }))
1988 })
1989 }
1990
1991 // ---- OneIP VLAN ----
1992
1993 /// Changes a OneIP device's VLAN configuration.
1994 ///
1995 /// Writes the VLAN ids, the pinned uplink and the
1996 /// [`TRUNK`](V2ipVlanFlag::TRUNK) flag of `vlan`; its other flags and the
1997 /// fields only the device reports are not sent. Refused before anything is
1998 /// sent unless the device announces [`DeviceFeature::VLAN`] and has
1999 /// reported its configuration, every id is at most
2000 /// [`V2IP_VLAN_ID_MAX`](crate::V2IP_VLAN_ID_MAX), and the uplink is
2001 /// detected or names a port the device has.
2002 ///
2003 /// The device applies the change at once and reverts it unless the mesh
2004 /// controller, hearing the device report it, confirms it. Nothing is
2005 /// cached here: [`Remote::v2ip_vlan`] reads what the device reports, and
2006 /// [`V2ipVlan::is_pending`] whether it is still to be confirmed.
2007 pub fn set_v2ip_vlan(&self, device: DeviceUid, vlan: V2ipVlan) -> Result<(), ControlError> {
2008 if !vlan.is_valid() {
2009 return Err(ControlError::InvalidRequest(
2010 "a VLAN id is out of range or the uplink names no port",
2011 ));
2012 }
2013 let written = V2ipVlan {
2014 flags: V2ipVlanFlag::VALID | (vlan.flags & V2ipVlanFlag::TRUNK),
2015 device: vlan.device,
2016 port: vlan.port,
2017 uplink: vlan.uplink,
2018 active_uplink: 0,
2019 revert_s: 0,
2020 };
2021 self.shared.command(move |state| {
2022 let d = device_of(state, device)?;
2023 if !d.hello.features.has(DeviceFeature::VLAN) {
2024 return Err(ControlError::Unsupported("the device does not take VLANs"));
2025 }
2026 let Some(reported) = d.v2ip_vlan else {
2027 return Err(ControlError::NotReported("the device's VLAN configuration"));
2028 };
2029 if written.pinned_uplink() == Some(V2IP_VLAN_PORT_SFP) && !reported.has_sfp() {
2030 return Err(ControlError::Unsupported("the device has no SFP port"));
2031 }
2032 Ok(Command::new(
2033 Addressee::device(d),
2034 op::V2IP_DEVICE_CFG,
2035 build_v2ip_vlan(d.uid, &written),
2036 ))
2037 })
2038 }
2039
2040 // ---- OneIP test card ----
2041
2042 /// Asks a OneIP sink for its test card, which it reports straight back;
2043 /// [`Remote::v2ip_testcard`] reads it.
2044 ///
2045 /// Refused unless the sink's video processor has reported
2046 /// [`SINK_TEST_PATTERN`](V2ipFpgaFeature::SINK_TEST_PATTERN). A sink with
2047 /// that feature but without the module that draws the test card does not
2048 /// answer.
2049 pub fn request_v2ip_testcard(&self, device: DeviceUid) -> Result<(), ControlError> {
2050 self.send_v2ip_testcard(device, testcard_type::REQUEST, 0, V2ipTestcard::default())
2051 }
2052
2053 /// Shows a test pattern on a OneIP sink's output, or none for
2054 /// [`V2ipTestPattern::OFF`]. `colour` is `0xRRGGBB`, used by
2055 /// [`V2ipTestPattern::FLAT`].
2056 ///
2057 /// A pattern runs until it is turned off, and holds the output on while it
2058 /// does. The sink reports its test card in answer. Refused as
2059 /// [`Remote::request_v2ip_testcard`] is, and for a pattern this library
2060 /// does not name or a colour above `0xFFFFFF`.
2061 pub fn set_v2ip_test_pattern(
2062 &self,
2063 device: DeviceUid,
2064 pattern: V2ipTestPattern,
2065 colour: u32,
2066 ) -> Result<(), ControlError> {
2067 if pattern.to_wire() > V2ipTestPattern::CARD.to_wire() || colour > 0x00FF_FFFF {
2068 return Err(ControlError::InvalidRequest(
2069 "no such test pattern, or a colour wider than 24 bits",
2070 ));
2071 }
2072 self.send_v2ip_testcard(
2073 device,
2074 testcard_type::SET,
2075 testcard_part::PATTERN,
2076 V2ipTestcard {
2077 pattern,
2078 colour,
2079 ..V2ipTestcard::default()
2080 },
2081 )
2082 }
2083
2084 /// Plays a test tone on a OneIP sink's output.
2085 ///
2086 /// Every value is checked here, as the sink ignores a tone that is not
2087 /// [`V2ipTestTone::is_valid`]; [`V2ipToneMode::OFF`] stops it whatever the
2088 /// rest holds, and the sink keeps those values as its last ones. Otherwise
2089 /// as [`Remote::set_v2ip_test_pattern`].
2090 pub fn set_v2ip_test_tone(
2091 &self,
2092 device: DeviceUid,
2093 tone: V2ipTestTone,
2094 ) -> Result<(), ControlError> {
2095 if tone.mode != V2ipToneMode::OFF && !tone.is_valid() {
2096 return Err(ControlError::InvalidRequest(
2097 "a test tone value is out of range",
2098 ));
2099 }
2100 self.send_v2ip_testcard(
2101 device,
2102 testcard_type::SET,
2103 testcard_part::TONE,
2104 V2ipTestcard {
2105 tone,
2106 ..V2ipTestcard::default()
2107 },
2108 )
2109 }
2110
2111 /// Sets a OneIP sink's lip-sync flash.
2112 ///
2113 /// Checked here as [`V2ipTestSync::is_valid`], since the sink ignores
2114 /// settings that are not. Otherwise as [`Remote::set_v2ip_test_pattern`].
2115 pub fn set_v2ip_test_sync(
2116 &self,
2117 device: DeviceUid,
2118 sync: V2ipTestSync,
2119 ) -> Result<(), ControlError> {
2120 if !sync.is_valid() {
2121 return Err(ControlError::InvalidRequest(
2122 "a lip-sync value is out of range",
2123 ));
2124 }
2125 self.send_v2ip_testcard(
2126 device,
2127 testcard_type::SET,
2128 testcard_part::SYNC,
2129 V2ipTestcard {
2130 sync,
2131 ..V2ipTestcard::default()
2132 },
2133 )
2134 }
2135
2136 /// The one send behind the test card methods. Nothing is cached: the sink
2137 /// answers every frame with its test card.
2138 fn send_v2ip_testcard(
2139 &self,
2140 device: DeviceUid,
2141 kind: u8,
2142 parts: u8,
2143 testcard: V2ipTestcard,
2144 ) -> Result<(), ControlError> {
2145 self.shared.command(move |state| {
2146 let d = device_of(state, device)?;
2147 let Some(features) = d.v2ip_features else {
2148 return Err(ControlError::NotReported(
2149 "the sink's video processor features",
2150 ));
2151 };
2152 if !features.has(V2ipFpgaFeature::SINK_TEST_PATTERN) {
2153 return Err(ControlError::Unsupported(
2154 "the sink cannot draw a test card",
2155 ));
2156 }
2157 Ok(Command::new(
2158 Addressee::device(d),
2159 op::V2IP_TESTCARD,
2160 build_v2ip_testcard(d.uid, kind, parts, &testcard),
2161 ))
2162 })
2163 }
2164
2165 // ---- video wall ----
2166
2167 /// Shows a window on a sink's video wall without persisting it.
2168 ///
2169 /// The window survives until the sink is told otherwise or restarts.
2170 /// [`Remote::revert_video_wall`] puts back whatever was stored.
2171 ///
2172 /// Pass [`crate::VIDEO_WALL_CLEARED`] to show the whole frame again.
2173 pub fn preview_video_wall(
2174 &self,
2175 sink: DeviceUid,
2176 window: VideoWallWindow,
2177 ) -> Result<(), ControlError> {
2178 self.set_video_wall(sink, window, VideoWallOp::PREVIEW)
2179 }
2180
2181 /// Persists a window as a sink's video wall.
2182 ///
2183 /// The geometry is checked here, before anything is sent, because the sink
2184 /// is not guaranteed to check it. A sink running a video-wall module older
2185 /// than 2026083100 writes the window to its configuration *before* asking
2186 /// its video processor to apply it, and the processor's refusal does not
2187 /// undo that write - so an out-of-spec window survives a reboot and is
2188 /// re-offered on every stream restart until something else replaces it. A
2189 /// power cycle does not clear it.
2190 ///
2191 /// Nothing acknowledges this frame either way, so an `Ok` says only that
2192 /// it was sent. Read the sink's state back to learn what it did.
2193 ///
2194 /// Pass [`crate::VIDEO_WALL_CLEARED`] to store "show the whole frame".
2195 pub fn store_video_wall(
2196 &self,
2197 sink: DeviceUid,
2198 window: VideoWallWindow,
2199 ) -> Result<(), ControlError> {
2200 self.set_video_wall(sink, window, VideoWallOp::STORE)
2201 }
2202
2203 /// Restores the window a sink has stored, discarding a preview.
2204 ///
2205 /// Carries no window of its own: the sink already holds the one this puts
2206 /// back.
2207 pub fn revert_video_wall(&self, sink: DeviceUid) -> Result<(), ControlError> {
2208 self.set_video_wall(sink, VIDEO_WALL_CLEARED, VideoWallOp::REVERT)
2209 }
2210
2211 /// The one send behind the three video-wall methods.
2212 ///
2213 /// Validation sits here rather than in each of them, so an operation added
2214 /// later cannot reach the wire without it, and is skipped for a revert
2215 /// because the sink ignores the window on that operation rather than
2216 /// checking it.
2217 ///
2218 /// Refused for a unit that does not report
2219 /// [`VIDEO_WALL`](DeviceFeature::VIDEO_WALL), which drops the frame
2220 /// unanswered.
2221 ///
2222 /// Passing it is not proof a wall appeared. Two things the sink refuses
2223 /// afterwards are equally silent: a window it will not draw, which it logs
2224 /// and drops, and a sink whose image has no tiling support at all, which
2225 /// takes the window into its own state and then fails to push it to the
2226 /// hardware. Neither reaches the wire, so read the sink back over HTTP to
2227 /// learn a window landed.
2228 fn set_video_wall(
2229 &self,
2230 sink: DeviceUid,
2231 window: VideoWallWindow,
2232 op: VideoWallOp,
2233 ) -> Result<(), ControlError> {
2234 if op != VideoWallOp::REVERT {
2235 window.validate().map_err(ControlError::InvalidRequest)?;
2236 }
2237 self.shared.command(move |state| {
2238 let device = device_of(state, sink)?;
2239 // A unit without the module drops the frame unanswered.
2240 if !device.hello.features.has(DeviceFeature::VIDEO_WALL) {
2241 return Err(ControlError::Unsupported("the unit has no video wall"));
2242 }
2243 Ok(Command::new(
2244 Addressee::device(device),
2245 op::V2IP_VIDEO_WALL,
2246 build_video_wall(device.uid, window, op),
2247 ))
2248 })
2249 }
2250
2251 // ---- multiviewer ----
2252
2253 /// Sets the window layout.
2254 pub fn set_multiviewer_view_mode(
2255 &self,
2256 device: DeviceUid,
2257 mode: MultiviewerViewMode,
2258 ) -> Result<(), ControlError> {
2259 let mode = mv_setting(mode.to_wire(), 8, "the multiviewer has no such view mode")?;
2260 self.multiviewer(device, mv_sub::VIEW_MODE, &[mode])
2261 }
2262
2263 /// Assigns a source to one window, counting windows from zero.
2264 ///
2265 /// A window index the multiviewer is not currently showing is refused
2266 /// rather than sent: firmware accepts an index one past the last window
2267 /// and writes through the end of the array it indexes, so the frame that
2268 /// would carry it is the one frame this library must never put on the
2269 /// wire. The bound comes from the layout in the multiviewer's last status
2270 /// report, so a multiviewer that has reported none can only be given
2271 /// window zero, which every layout has.
2272 pub fn set_multiviewer_video_source(
2273 &self,
2274 device: DeviceUid,
2275 screen: u8,
2276 source: MultiviewerSource,
2277 ) -> Result<(), ControlError> {
2278 let source = source_index(source, "the source names no multiviewer input")?;
2279 self.shared.command(|state| {
2280 let target = multiviewer_of(state, device)?;
2281 let windows = target
2282 .multiviewer
2283 .as_ref()
2284 .and_then(MultiviewerStatus::window_count)
2285 .unwrap_or(1);
2286 if screen >= windows {
2287 return Err(ControlError::InvalidRequest(
2288 "the window is not one the multiviewer is showing",
2289 ));
2290 }
2291 Ok(mv_command(target, mv_sub::VIDEO_SOURCE, &[screen, source]))
2292 })
2293 }
2294
2295 /// Selects which window's audio is output.
2296 pub fn set_multiviewer_audio_source(
2297 &self,
2298 device: DeviceUid,
2299 source: MultiviewerSource,
2300 ) -> Result<(), ControlError> {
2301 let source = source_index(source, "the audio source names no multiviewer input")?;
2302 self.multiviewer(device, mv_sub::AUDIO_SOURCE, &[source])
2303 }
2304
2305 /// Sets the output volume, as a percentage, and the mute state.
2306 ///
2307 /// A volume above 100 is refused rather than sent. What a multiviewer does
2308 /// with one depends on its module version: from 2026083100 it drops the
2309 /// whole frame, and before that it dropped the volume alone and still
2310 /// acted on the mute beside it. Neither is what the caller asked for, and
2311 /// neither is reported back.
2312 pub fn set_multiviewer_audio_volume(
2313 &self,
2314 device: DeviceUid,
2315 volume: u8,
2316 muted: bool,
2317 ) -> Result<(), ControlError> {
2318 if volume > 100 {
2319 return Err(ControlError::InvalidRequest(
2320 "a multiviewer volume is a percentage",
2321 ));
2322 }
2323 self.multiviewer(device, mv_sub::AUDIO_VOLUME, &[volume, u8::from(muted)])
2324 }
2325
2326 /// Sets the EDID template presented to the sources.
2327 pub fn set_multiviewer_edid_template(
2328 &self,
2329 device: DeviceUid,
2330 template: MultiviewerEdidTemplate,
2331 ) -> Result<(), ControlError> {
2332 let template = mv_setting(
2333 template.to_wire(),
2334 19,
2335 "the multiviewer has no such EDID template",
2336 )?;
2337 self.multiviewer(device, mv_sub::EDID_TEMPLATE, &[template])
2338 }
2339
2340 /// Selects which window receives remote-control passthrough.
2341 pub fn set_multiviewer_remote_control(
2342 &self,
2343 device: DeviceUid,
2344 source: MultiviewerSource,
2345 ) -> Result<(), ControlError> {
2346 let source = source_index(
2347 source,
2348 "the remote-control source names no multiviewer input",
2349 )?;
2350 self.multiviewer(device, mv_sub::ROUTE_RC, &[source])
2351 }
2352
2353 /// Sets how large the picture-in-picture window is.
2354 pub fn set_multiviewer_pip_size(
2355 &self,
2356 device: DeviceUid,
2357 size: MultiviewerPipSize,
2358 ) -> Result<(), ControlError> {
2359 let size = mv_setting(
2360 size.to_wire(),
2361 3,
2362 "the multiviewer has no such picture-in-picture size",
2363 )?;
2364 self.multiviewer(device, mv_sub::PIP_SIZE, &[size])
2365 }
2366
2367 /// Sets which corner the picture-in-picture window sits in.
2368 pub fn set_multiviewer_pip_position(
2369 &self,
2370 device: DeviceUid,
2371 position: MultiviewerPipPosition,
2372 ) -> Result<(), ControlError> {
2373 let position = mv_setting(
2374 position.to_wire(),
2375 4,
2376 "the multiviewer has no such picture-in-picture position",
2377 )?;
2378 self.multiviewer(device, mv_sub::PIP_POSITION, &[position])
2379 }
2380
2381 /// Sets the aspect ratio the windows are scaled to.
2382 pub fn set_multiviewer_aspect_ratio(
2383 &self,
2384 device: DeviceUid,
2385 aspect: MultiviewerAspectRatio,
2386 ) -> Result<(), ControlError> {
2387 let aspect = mv_setting(
2388 aspect.to_wire(),
2389 2,
2390 "the multiviewer has no such aspect ratio",
2391 )?;
2392 self.multiviewer(device, mv_sub::ASPECT, &[aspect])
2393 }
2394
2395 /// Enables or disables switching windows on its own.
2396 pub fn set_multiviewer_auto_switch(
2397 &self,
2398 device: DeviceUid,
2399 enable: bool,
2400 ) -> Result<(), ControlError> {
2401 self.multiviewer(device, mv_sub::AUTO_SWITCH, &[u8::from(enable)])
2402 }
2403
2404 /// Sets the output resolution and refresh rate.
2405 pub fn set_multiviewer_output_mode(
2406 &self,
2407 device: DeviceUid,
2408 mode: MultiviewerOutputMode,
2409 ) -> Result<(), ControlError> {
2410 let mode = mv_setting(
2411 mode.to_wire(),
2412 14,
2413 "the multiviewer has no such output mode",
2414 )?;
2415 self.multiviewer(device, mv_sub::OUTPUT_MODE, &[mode])
2416 }
2417
2418 /// Sets the IT-content flag on the output.
2419 pub fn set_multiviewer_output_itc(
2420 &self,
2421 device: DeviceUid,
2422 mode: MultiviewerItcMode,
2423 ) -> Result<(), ControlError> {
2424 let mode = mv_setting(
2425 mode.to_wire(),
2426 2,
2427 "the multiviewer has no such IT-content mode",
2428 )?;
2429 self.multiviewer(device, mv_sub::OUTPUT_ITC_MODE, &[mode])
2430 }
2431
2432 /// Sets the HDCP version negotiated on the output.
2433 pub fn set_multiviewer_hdcp_mode(
2434 &self,
2435 device: DeviceUid,
2436 mode: MultiviewerHdcpMode,
2437 ) -> Result<(), ControlError> {
2438 let mode = mv_setting(mode.to_wire(), 3, "the multiviewer has no such HDCP mode")?;
2439 self.multiviewer(device, mv_sub::HDCP_MODE, &[mode])
2440 }
2441
2442 /// Maps a source device onto one of the multiviewer's inputs, counting
2443 /// inputs from zero.
2444 ///
2445 /// [`DeviceUid::ZERO`] clears the mapping on a multiviewer running module
2446 /// version 2026083100 or newer, and is stored as a mapping like any other
2447 /// on anything older. No version checks that a mapping names a device on
2448 /// the mesh.
2449 ///
2450 /// Which of the two happened shows in `mappings` on a later status report,
2451 /// where a cleared input reads as [`DeviceUid::ZERO`] only from that same
2452 /// version. It will not be the next frame this multiviewer sends: this is
2453 /// one of the two settings that schedule no status broadcast of their own,
2454 /// so the answer arrives whenever something else prompts one.
2455 pub fn set_multiviewer_input_source(
2456 &self,
2457 device: DeviceUid,
2458 input: u8,
2459 source: DeviceUid,
2460 ) -> Result<(), ControlError> {
2461 if usize::from(input) >= MULTIVIEWER_INPUTS {
2462 return Err(ControlError::InvalidRequest(
2463 "the multiviewer has no such input",
2464 ));
2465 }
2466 let mut args = Vec::with_capacity(24);
2467 args.extend_from_slice(source.as_bytes());
2468 args.push(input);
2469 // The body is 8-aligned, so seven bytes of padding round the uid and
2470 // the input index up to 24.
2471 args.extend_from_slice(&[0; 7]);
2472 self.multiviewer(device, mv_sub::CONFIG_SOURCE, &args)
2473 }
2474
2475 /// Asks the multiviewer to route its sources itself.
2476 pub fn multiviewer_auto_route(&self, device: DeviceUid) -> Result<(), ControlError> {
2477 self.multiviewer(device, mv_sub::AUTO_ROUTE, &[])
2478 }
2479
2480 fn multiviewer(&self, device: DeviceUid, sub: u8, args: &[u8]) -> Result<(), ControlError> {
2481 self.shared
2482 .command(|state| Ok(mv_command(multiviewer_of(state, device)?, sub, args)))
2483 }
2484}
2485
2486/// The settings that switch a OneIP device's power save on or off. On is both
2487/// [`POWER_SAVE_DISABLED`](V2ipDeviceSetting::POWER_SAVE_DISABLED), clear, and
2488/// [`POWER_SAVE_LEVEL`](V2ipDeviceSetting::POWER_SAVE_LEVEL), at 0.
2489fn power_save_switch(enabled: bool) -> V2ipDeviceSettings {
2490 let disabled = V2ipDeviceSetting::POWER_SAVE_DISABLED;
2491 if enabled {
2492 V2ipDeviceSettings {
2493 valid: disabled | V2ipDeviceSetting::POWER_SAVE_LEVEL,
2494 power_save_level: V2ipPowerSaveLevel::NONE,
2495 ..V2ipDeviceSettings::default()
2496 }
2497 } else {
2498 V2ipDeviceSettings {
2499 valid: disabled,
2500 flags: disabled,
2501 ..V2ipDeviceSettings::default()
2502 }
2503 }
2504}