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