mx_remote/types/commands.rs
1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! Payloads of the command and notification opcodes.
5//!
6//! These frames are addressed to a device rather than reporting its state, so
7//! most surface as events rather than cached state. A frame addressed to
8//! another unit still reaches every client on the group: the target field says
9//! who it was for, and that is neither necessarily this client nor the sender.
10
11use core::fmt;
12use std::net::Ipv4Addr;
13
14use crate::wire::{DeviceUid, EdidProfile, RcAction, RcKey};
15
16/// Asks a device, addressed by serial, to switch a sink.
17#[derive(Clone, Debug, Default, PartialEq, Eq)]
18pub struct SetRouteRequest {
19 /// Serial of the device to act on.
20 pub serial: String,
21 /// Output bay to switch.
22 pub sink_bay: u16,
23 /// Source bay to switch it to.
24 pub source_bay: u16,
25 /// Whether to skip the power-on commands that normally accompany a switch.
26 pub no_power_on: bool,
27 /// True when this arrived on `AUDIO_SET_ROUTE` rather than `MX_SET_ROUTE`.
28 pub audio_only: bool,
29}
30
31impl fmt::Display for SetRouteRequest {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 write!(
34 f,
35 "set route on {}: sink={} source={}",
36 self.serial, self.sink_bay, self.source_bay
37 )
38 }
39}
40
41/// One EDID block from a `DEV_EDID` reply.
42///
43/// A reply carries one record per bay mode, so a combined reply produces two.
44#[derive(Clone, Debug, Default, PartialEq, Eq)]
45pub struct EdidRecord {
46 /// True for a sink's EDID, false for a source's.
47 pub output: bool,
48 /// A 256-byte EDID: a base block plus exactly one extension block. A
49 /// display publishing further extension blocks yields only the first.
50 pub data: Vec<u8>,
51}
52
53/// Asks one device for its EDID.
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
55pub struct EdidRequest {
56 /// The device being asked.
57 pub target: DeviceUid,
58 /// Whether the sink's EDID is wanted rather than the source's.
59 pub output: bool,
60}
61
62/// Asks a device to rename one of its bays.
63#[derive(Clone, Debug, Default, PartialEq, Eq)]
64pub struct BayNameChange {
65 /// The device to act on.
66 pub target: DeviceUid,
67 /// The bay to rename.
68 pub port: u16,
69 /// The new name.
70 pub name: String,
71}
72
73/// Asks a device to switch its input EDID profile.
74#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
75pub struct EdidProfileChange {
76 /// The device to act on.
77 pub target: DeviceUid,
78 /// The profile to switch to.
79 pub profile: EdidProfile,
80}
81
82/// Asks peers to factory-reset.
83#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
84pub struct FactoryResetRequest {
85 /// Set by the broadcast form, which targets every peer.
86 pub all: bool,
87 /// Set by the single-uid form. With neither this nor `all`, the request
88 /// addresses only the sender.
89 pub target: Option<DeviceUid>,
90}
91
92/// Asks one device to reboot.
93#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
94pub struct RebootRequest {
95 /// The device to reboot.
96 pub target: DeviceUid,
97}
98
99/// The window a sink is currently told to show.
100///
101/// This is the readable, pollable view of a sink's window. It is not the
102/// persisted video wall setting: on a sink running the v2ipwall module a write
103/// here is transient, because that module's reconciler pushes its own target
104/// window back within about a second. [`VideoWallCommand`] carries intent.
105#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
106pub struct V2ipTilingConfig {
107 /// The sink this window belongs to.
108 pub target: DeviceUid,
109 /// Window origin, horizontal.
110 pub pos_x: u16,
111 /// Window origin, vertical.
112 pub pos_y: u16,
113 /// Window width.
114 pub width: u16,
115 /// Window height.
116 pub height: u16,
117}
118
119impl fmt::Display for V2ipTilingConfig {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 write!(
122 f,
123 "tiling x={} y={} {}x{}",
124 self.pos_x, self.pos_y, self.width, self.height
125 )
126 }
127}
128
129/// Asks a sink to enter or leave power save.
130#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
131pub struct V2ipPowerSaveRequest {
132 /// The sink to act on, or `None` on the broadcast form.
133 pub target: Option<DeviceUid>,
134 /// Whether power save is being entered.
135 pub enabled: bool,
136}
137
138/// The remote-control configuration of a source bay.
139#[derive(Clone, Debug, Default, PartialEq, Eq)]
140pub struct RcSettings {
141 /// The device this configuration belongs to.
142 pub target: DeviceUid,
143 /// The control method (`rc_target_t`).
144 ///
145 /// A single byte: the enum is plain and Cortex-M builds with
146 /// `-fshort-enums`, so three bytes of padding follow it before the address.
147 /// That padding is not zero - firmware copies an uncleared stack local over
148 /// the payload - so widening this to a `u32` makes one unchanged setting
149 /// decode differently on every frame.
150 pub rc_target: u8,
151 /// The control target's address, `None` when unset.
152 pub ip: Option<Ipv4Addr>,
153 /// Whether CEC is enabled.
154 pub cec_enabled: bool,
155 /// Whether CEC powers the sink on automatically.
156 pub cec_auto_on: bool,
157 /// Whether remote-control commands are forwarded.
158 pub forward_rc: bool,
159 /// Whether infrared is forwarded.
160 pub forward_ir: bool,
161 /// The driver state on the source (`mxr_rc_status_t`).
162 ///
163 /// A value above the last one this library knows is passed through as it
164 /// arrived rather than clamped, so a firmware update cannot make it read as
165 /// a known state.
166 pub rc_status: u8,
167 /// The driver-reported status string, empty when unknown.
168 pub status_name: String,
169}
170
171/// The raw-IR metadata shared by the IR capture and transmit frames.
172#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
173pub struct IrMeta {
174 /// Tick length of the timing values.
175 pub timer_resolution: u16,
176 /// Carrier frequency in Hz.
177 pub frequency: u16,
178 /// Number of timing values that follow.
179 pub nb_timings: u16,
180 /// Index at which the repeat section starts.
181 pub repeat_offset: u16,
182 /// Capture status.
183 pub status: u8,
184}
185
186/// Raw IR captured on a bay of the sending device.
187#[derive(Clone, Debug, Default, PartialEq, Eq)]
188pub struct IrCapture {
189 /// The bay that captured it.
190 pub port: u16,
191 /// Sender clock at capture time.
192 pub timestamp: u32,
193 /// Sender clock at the last signal change.
194 pub last_change: u32,
195 /// Metadata for the timings.
196 pub meta: IrMeta,
197 /// The raw on/off timing blob following the header.
198 pub timings: Vec<u8>,
199}
200
201/// Asks one device to blast raw IR on one of its local bays.
202#[derive(Clone, Debug, Default, PartialEq, Eq)]
203pub struct IrTransmitRequest {
204 /// The device to act on.
205 pub target: DeviceUid,
206 /// Bay mode in the target's own numbering, not a port.
207 pub local_mode: u8,
208 /// Bay number in the target's own numbering, not a port.
209 pub local_bay: u8,
210 /// Sender clock at send time.
211 pub timestamp: u32,
212 /// Metadata for the timings.
213 pub meta: IrMeta,
214 /// The raw on/off timing blob following the header.
215 pub timings: Vec<u8>,
216}
217
218/// Asks one device to send a remote-control key on a bay.
219#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
220pub struct KeyTransmitRequest {
221 /// The device to act on.
222 pub target: DeviceUid,
223 /// Bay in the target's own numbering.
224 pub local_bay: u16,
225 /// The key to send.
226 pub key: RcKey,
227}
228
229/// Asks one device to perform a remote-control action.
230#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
231pub struct ActionTransmitRequest {
232 /// The device to act on.
233 pub target: DeviceUid,
234 /// Bay in the target's own numbering.
235 pub local_bay: u16,
236 /// The action to perform.
237 pub action: RcAction,
238}
239
240/// Reports that a bay detected audio clipping.
241#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
242pub struct AudioClip {
243 /// The bay that clipped.
244 pub port: u16,
245 /// The clip level reported.
246 pub clip: u8,
247}
248
249/// The electrical state a PDU reports.
250#[derive(Clone, Copy, Debug, Default, PartialEq)]
251pub struct PduState {
252 /// Current in amperes.
253 pub current: f64,
254 /// Voltage in volts.
255 pub voltage: f64,
256 /// Real power in watts.
257 pub power: f64,
258 /// Dissipation in watts.
259 pub dissipation: f64,
260 /// Mains frequency in Hz.
261 pub frequency: f64,
262 /// Per-outlet state.
263 pub outlets: [u8; 8],
264}
265
266impl fmt::Display for PduState {
267 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268 write!(
269 f,
270 "{:.2}A {:.2}V {:.2}W",
271 self.current, self.voltage, self.power
272 )
273 }
274}
275
276/// Registers or unregisters a device on the source blacklist.
277///
278/// The firmware guards this opcode behind `V2IP_SUPPORT_BLACKLIST`, which is 0
279/// in shipping builds, so nothing in current firmware emits it.
280#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
281pub struct V2ipBlacklistChange {
282 /// The device being listed.
283 pub target: DeviceUid,
284 /// Whether it is being registered rather than removed.
285 pub registered: bool,
286}
287
288/// What a [`VideoWallCommand`] asks the sink to do with the window.
289#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
290pub struct VideoWallOp(u8);
291
292impl VideoWallOp {
293 /// Applies the window without persisting it.
294 pub const PREVIEW: Self = Self(0);
295 /// Persists the window as the sink's wall setting.
296 pub const STORE: Self = Self(1);
297 /// Restores the persisted setting; carries no window.
298 pub const REVERT: Self = Self(2);
299
300 /// Wraps a raw wire value, including one this library has no name for.
301 pub const fn from_wire(value: u8) -> Self {
302 Self(value)
303 }
304
305 /// Returns the raw wire value.
306 pub const fn to_wire(self) -> u8 {
307 self.0
308 }
309}
310
311impl fmt::Display for VideoWallOp {
312 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
313 f.write_str(match *self {
314 Self::PREVIEW => "preview",
315 Self::STORE => "store",
316 Self::REVERT => "revert",
317 _ => "unknown",
318 })
319 }
320}
321
322/// Asks one sink to crop its source to a wall window.
323///
324/// This replaces the sink's window outright: unlike a V2IP device config, no
325/// field carries a validity marker, and a zero width or height is the wire
326/// spelling of "clear the wall and show the full frame" rather than "unset".
327///
328/// The opcode belongs to the loadable v2ipwall module rather than MatrixOS, and
329/// a wall has no object of its own on the wire: it is a set of sinks each
330/// holding one rectangle, one frame each. It is a command with no reply, so
331/// nothing here is ever a status readback.
332#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
333pub struct VideoWallCommand {
334 /// The sink to act on.
335 pub target: DeviceUid,
336 /// Window origin, horizontal.
337 pub pos_x: u16,
338 /// Window origin, vertical.
339 pub pos_y: u16,
340 /// Window width.
341 pub width: u16,
342 /// Window height.
343 pub height: u16,
344 /// Active picture width the window was authored against.
345 ///
346 /// The raster travels with the window because only the sender knows what
347 /// the installer drew against; a sink deriving it from what it happens to
348 /// be showing would store the window against the wrong picture.
349 pub raster_w: u16,
350 /// Active picture height the window was authored against.
351 pub raster_h: u16,
352 /// What to do with the window.
353 pub op: VideoWallOp,
354}
355
356impl VideoWallCommand {
357 /// Reports whether the geometry in this command is meaningful.
358 ///
359 /// A revert zeroes the window and raster and the receiver ignores those
360 /// bytes, so its zeros are not a clear.
361 pub fn has_window(&self) -> bool {
362 self.op != VideoWallOp::REVERT
363 }
364
365 /// Reports a command that clears the wall and shows the full frame.
366 pub fn is_cleared(&self) -> bool {
367 self.has_window() && (self.width == 0 || self.height == 0)
368 }
369}
370
371impl fmt::Display for VideoWallCommand {
372 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373 if !self.has_window() {
374 return f.write_str("video wall revert");
375 }
376 if self.is_cleared() {
377 return write!(f, "video wall {}: clear", self.op);
378 }
379 write!(
380 f,
381 "video wall {}: {}x{}+{}+{} of {}x{}",
382 self.op, self.width, self.height, self.pos_x, self.pos_y, self.raster_w, self.raster_h
383 )
384 }
385}
386
387/// A command addressed to a multiviewer.
388///
389/// The parameters past the envelope are exposed as raw bytes: the opcode
390/// belongs to the multiviewer module rather than MatrixOS, so beyond the
391/// envelope there is no firmware source here to pin per-sub-command field
392/// semantics against.
393#[derive(Clone, Debug, Default, PartialEq, Eq)]
394pub struct MultiviewerCommand {
395 /// The multiviewer being addressed.
396 pub target: DeviceUid,
397 /// The sub-opcode. A value this library has no name for still arrives.
398 pub op: u8,
399 /// Everything after the envelope, empty when the frame carries none.
400 pub params: Vec<u8>,
401}
402
403impl fmt::Display for MultiviewerCommand {
404 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405 write!(
406 f,
407 "multiviewer command {} for {} ({} param bytes)",
408 self.op,
409 self.target,
410 self.params.len()
411 )
412 }
413}
414
415/// An audio input-selection change: which source endpoint a sink endpoint was
416/// switched to.
417///
418/// The sink is named twice on the wire, once as the command header's target and
419/// again at the head of the body; the body's second uid is the source.
420#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
421pub struct AudioChangeSource {
422 /// The device whose endpoint is being listened to.
423 pub source_uid: DeviceUid,
424 /// The endpoint being listened to.
425 pub source_id: u16,
426 /// The device doing the listening.
427 pub target_uid: DeviceUid,
428 /// The endpoint doing the listening.
429 pub target_id: u16,
430}
431
432impl fmt::Display for AudioChangeSource {
433 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
434 write!(
435 f,
436 "audio source change {}:{} -> {}:{}",
437 self.source_uid, self.source_id, self.target_uid, self.target_id
438 )
439 }
440}