mx_remote/types/v2ip.rs
1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! V2IP stream configuration, statistics and the sink-side route.
5
6use core::fmt;
7use std::net::Ipv4Addr;
8
9use crate::wire::{DeviceUid, MxrSignalType, V2IP_DSCP_MAX, V2IP_DSCP_SET};
10
11/// Which of a V2IP device's streams an address describes.
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub enum StreamKind {
14 /// The video stream.
15 #[default]
16 Video,
17 /// The audio stream.
18 Audio,
19 /// The ancillary-data stream.
20 Anc,
21 /// The audio-return stream.
22 Arc,
23}
24
25impl fmt::Display for StreamKind {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 f.write_str(match self {
28 Self::Video => "video",
29 Self::Audio => "audio",
30 Self::Anc => "anc",
31 Self::Arc => "arc",
32 })
33 }
34}
35
36/// A single multicast stream address.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct V2ipStreamSource {
39 /// Which stream this address is for.
40 pub kind: StreamKind,
41 /// The multicast group.
42 pub ip: Ipv4Addr,
43 /// The destination UDP port.
44 pub port: u16,
45}
46
47impl Default for V2ipStreamSource {
48 fn default() -> Self {
49 Self {
50 kind: StreamKind::default(),
51 ip: Ipv4Addr::UNSPECIFIED,
52 port: 0,
53 }
54 }
55}
56
57impl V2ipStreamSource {
58 /// Reports whether this carries a usable address: a multicast group and a
59 /// non-zero port, both, matching firmware `mxr_v2ip_stream_valid`.
60 pub const fn is_valid(&self) -> bool {
61 self.ip.is_multicast() && self.port != 0
62 }
63}
64
65impl fmt::Display for V2ipStreamSource {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 write!(f, "{}={}:{}", self.kind, self.ip, self.port)
68 }
69}
70
71/// The streams advertised by a single V2IP source.
72#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
73pub struct V2ipStreamSources {
74 /// The originating device, or the zero UID when it is not known.
75 pub uid: DeviceUid,
76 /// The video stream.
77 pub video: V2ipStreamSource,
78 /// The audio stream.
79 pub audio: V2ipStreamSource,
80 /// The ancillary-data stream.
81 pub anc: V2ipStreamSource,
82 /// The audio-return stream, when one is advertised.
83 pub arc: Option<V2ipStreamSource>,
84}
85
86impl fmt::Display for V2ipStreamSources {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 write!(
89 f,
90 "video:{} audio:{} anc:{}",
91 self.video, self.audio, self.anc
92 )
93 }
94}
95
96/// Overrides the sample rate and channel count of a V2IP audio stream.
97///
98/// Zero values mean "use the firmware default".
99#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
100pub struct V2ipAudioFormat {
101 /// Sample rate in Hz.
102 pub sample_rate: u32,
103 /// Channel count.
104 pub channels: u8,
105}
106
107impl V2ipAudioFormat {
108 /// Encodes `v2ip_audio_format`: a `u32` rate, a channel byte and three
109 /// reserved bytes, padded to the struct's 8-byte alignment.
110 pub(crate) fn wire(&self) -> [u8; 8] {
111 let r = self.sample_rate.to_le_bytes();
112 [r[0], r[1], r[2], r[3], self.channels, 0, 0, 0]
113 }
114}
115
116impl fmt::Display for V2ipAudioFormat {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 write!(f, "{}Hz/{}ch", self.sample_rate, self.channels)
119 }
120}
121
122/// A V2IP output's scaling mode, refresh rate and flags.
123#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
124pub struct V2ipScalingSettings {
125 /// The signal type the output scales to.
126 pub mode: MxrSignalType,
127 /// Refresh rate in Hz.
128 pub refresh: u16,
129 /// The flag bits below.
130 pub flags: u8,
131}
132
133/// Set when the frame carries a scaling mode and refresh rate.
134pub const SCALING_FLAG_MODE_VALID: u8 = 1 << 0;
135
136/// Set when the frame carries the scaling options.
137pub const SCALING_FLAG_OPTIONS_VALID: u8 = 1 << 1;
138
139/// Set when the output scales automatically.
140pub const SCALING_FLAG_AUTO_SCALING: u8 = 1 << 7;
141
142/// The flag bits that carry meaning.
143///
144/// Bits 2..6 are undefined and are not reliably zero on the wire: firmware
145/// predating the fix builds this frame from an uninitialised stack local and
146/// ORs its flags onto whatever was there.
147pub const SCALING_FLAGS_DEFINED: u8 =
148 SCALING_FLAG_MODE_VALID | SCALING_FLAG_OPTIONS_VALID | SCALING_FLAG_AUTO_SCALING;
149
150impl V2ipScalingSettings {
151 /// Folds a received scaling config onto the cached one, field by field.
152 ///
153 /// A write carries the mode or the options alone, so taking the block
154 /// wholesale would drop whichever half was not being written. The options
155 /// branch replaces the option bit rather than adding to it, which is what
156 /// lets an options-only write clear [`SCALING_FLAG_AUTO_SCALING`].
157 #[must_use]
158 pub fn merge(self, previous: Self) -> Self {
159 let mut out = previous;
160 if self.flags & SCALING_FLAG_MODE_VALID != 0 {
161 out.mode = self.mode;
162 out.refresh = self.refresh;
163 out.flags |= SCALING_FLAG_MODE_VALID;
164 }
165 if self.flags & SCALING_FLAG_OPTIONS_VALID != 0 {
166 out.flags &= !SCALING_FLAG_AUTO_SCALING;
167 out.flags |= SCALING_FLAG_OPTIONS_VALID;
168 out.flags |= self.flags & SCALING_FLAG_AUTO_SCALING;
169 }
170 out
171 }
172}
173
174/// The per-stream DSCP marking in a V2IP device configuration.
175///
176/// A stream whose wire byte carries no [`V2IP_DSCP_SET`] bit reads back as
177/// `None`. Firmware treats the marking as all-or-nothing: it applies one only
178/// when all three streams carry a value and otherwise falls back to the
179/// default, so [`V2ipDscpConfig::is_complete`] reports which case a frame is in.
180#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
181pub struct V2ipDscpConfig {
182 /// Marking for the video stream.
183 pub video: Option<u8>,
184 /// Marking for the audio stream.
185 pub audio: Option<u8>,
186 /// Marking for the ancillary-data stream.
187 pub anc: Option<u8>,
188}
189
190impl V2ipDscpConfig {
191 /// Reports whether all three streams carry a marking, which is what
192 /// firmware requires before it applies one.
193 pub const fn is_complete(&self) -> bool {
194 self.video.is_some() && self.audio.is_some() && self.anc.is_some()
195 }
196}
197
198impl fmt::Display for V2ipDscpConfig {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 match (self.video, self.audio, self.anc) {
201 (Some(v), Some(a), Some(n)) => write!(f, "video:{v} audio:{a} anc:{n}"),
202 _ => f.write_str("no marking"),
203 }
204 }
205}
206
207/// Decodes one `dscp` byte, or `None` when the byte carries no marking.
208pub(crate) fn parse_dscp(raw: u8) -> Option<u8> {
209 (raw & V2IP_DSCP_SET != 0).then_some(raw & V2IP_DSCP_MAX)
210}
211
212/// The local encoder/decoder configuration of a V2IP device.
213#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
214pub struct DeviceV2ipDetails {
215 /// The video stream this device sources.
216 pub video: V2ipStreamSource,
217 /// The audio stream this device sources.
218 pub audio: V2ipStreamSource,
219 /// The ancillary-data stream this device sources.
220 pub anc: V2ipStreamSource,
221 /// The audio-return stream this device sources.
222 pub arc: V2ipStreamSource,
223
224 /// Encoder rate in units of 10Mb/s, or `None` when the sender offered no
225 /// rate.
226 ///
227 /// A rate-only write carries the rate on its own; every other controller
228 /// write puts a value outside the valid range here, which firmware drops as
229 /// invalid so that address-only and scaling writes leave the peer's rate
230 /// alone.
231 pub tx_rate: Option<u8>,
232
233 /// Per-stream DSCP marking.
234 pub dscp: V2ipDscpConfig,
235 /// Scaling mode, refresh rate and flags.
236 pub scaling: V2ipScalingSettings,
237}
238
239impl DeviceV2ipDetails {
240 /// Reports whether the source block carries usable addresses.
241 ///
242 /// Firmware requires video and anc; audio is optional and is carried with
243 /// them.
244 pub const fn source_is_valid(&self) -> bool {
245 self.video.is_valid() && self.anc.is_valid()
246 }
247
248 /// Folds a received device configuration onto the cached one.
249 ///
250 /// Every field is optional behind its own validity marker: the payload is
251 /// zeroed before a sender fills in the one field it is writing, so a
252 /// controller writing a TX rate sends zeroed addresses and a controller
253 /// writing addresses sends an out-of-range rate. Firmware applies each
254 /// field only behind its own test, so replacing the whole cached config on
255 /// every frame would make the peer read back with its addresses, rate or
256 /// marking gone.
257 #[must_use]
258 pub fn merge(mut self, previous: Option<Self>) -> Self {
259 let Some(previous) = previous else {
260 return self;
261 };
262 if !self.source_is_valid() {
263 self.video = previous.video;
264 self.audio = previous.audio;
265 self.anc = previous.anc;
266 }
267 if !self.arc.is_valid() {
268 self.arc = previous.arc;
269 }
270 if self.tx_rate.is_none() {
271 self.tx_rate = previous.tx_rate;
272 }
273 // Firmware gates all three dscp bytes on the video byte's set bit
274 // alone, and stores whatever the other two carry.
275 if self.dscp.video.is_none() {
276 self.dscp = previous.dscp;
277 }
278 self.scaling = self.scaling.merge(previous.scaling);
279 self
280 }
281}
282
283/// The sink-side route a V2IP device is currently subscribed to.
284#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
285pub struct DeviceV2ipSink {
286 /// The streams the sink subscribes to.
287 pub addresses: V2ipStreamSources,
288 /// The resolved audio format, when the sender reported one.
289 pub audio_fmt: Option<V2ipAudioFormat>,
290}
291
292/// Transmitter stream statistics.
293#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
294pub struct V2ipTxStats {
295 /// Video packets sent.
296 pub video: u32,
297 /// Audio packets sent.
298 pub audio: u32,
299 /// Ancillary-data packets sent.
300 pub anc: u32,
301 /// Times the stream went down.
302 pub stream_down: u32,
303 /// Transmit overflows.
304 pub overflow: u32,
305}
306
307/// The health state of a V2IP decoder.
308#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
309pub struct V2ipDecoderState(u8);
310
311impl V2ipDecoderState {
312 /// The sink has not reported a state.
313 pub const UNKNOWN: Self = Self(0);
314 /// Decoding normally.
315 pub const HEALTHY: Self = Self(1);
316 /// Failed to decode.
317 pub const BAD: Self = Self(2);
318 /// Still coming up, which any sink subscribed to during a route change
319 /// reports.
320 pub const STARTING: Self = Self(3);
321
322 /// Wraps a raw wire value, including one this library has no name for.
323 pub const fn from_wire(value: u8) -> Self {
324 Self(value)
325 }
326
327 /// Returns the raw wire value.
328 pub const fn to_wire(self) -> u8 {
329 self.0
330 }
331
332 /// Reports whether the decoder has reached a verdict.
333 ///
334 /// Only healthy and bad are verdicts. Testing for failure as "not healthy"
335 /// reads a receiver that is merely coming up as one that failed to decode,
336 /// which is what a sink reports for a moment after every route change.
337 pub const fn is_settled(self) -> bool {
338 matches!(self, Self::HEALTHY | Self::BAD)
339 }
340}
341
342impl fmt::Display for V2ipDecoderState {
343 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344 match *self {
345 Self::UNKNOWN => f.write_str("Unknown"),
346 Self::HEALTHY => f.write_str("Healthy"),
347 Self::BAD => f.write_str("Bad"),
348 Self::STARTING => f.write_str("Starting"),
349 Self(v) => write!(f, "state {v}"),
350 }
351 }
352}
353
354/// Receiver stream statistics.
355#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
356pub struct V2ipRxStats {
357 /// Video packets received.
358 pub video_total: u32,
359 /// Video packets dropped.
360 pub video_dropped: u32,
361 /// Video sequence errors.
362 pub video_seq_errors: u32,
363 /// Watchdog timeouts.
364 pub wdt_timeout: u32,
365 /// Audio packets received.
366 pub audio_total: u32,
367 /// Audio packets dropped.
368 pub audio_dropped: u32,
369 /// Audio sequence errors.
370 pub audio_seq_errors: u32,
371 /// Ancillary-data packets received.
372 pub anc_total: u32,
373 /// Ancillary-data packets dropped.
374 pub anc_dropped: u32,
375 /// Ancillary-data sequence errors.
376 pub anc_seq_errors: u32,
377 /// The decoder's health state.
378 pub decoder_state: V2ipDecoderState,
379}
380
381/// The cumulative and per-minute transmit and receive statistics.
382#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
383pub struct V2ipDeviceStats {
384 /// Transmit totals since boot.
385 pub tx: V2ipTxStats,
386 /// Transmit counts over the last minute.
387 pub tx_per_minute: V2ipTxStats,
388 /// Receive totals since boot.
389 pub rx: V2ipRxStats,
390 /// Receive counts over the last minute.
391 pub rx_per_minute: V2ipRxStats,
392}