Skip to main content

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