Skip to main content

mx_remote/runtime/
mod.rs

1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! The running client: the socket, the two threads that drive it, and the
5//! read surface over what they discover.
6
7mod control;
8mod info;
9mod logo_server;
10mod osd;
11mod schedule;
12
13#[cfg(test)]
14mod control_tests;
15#[cfg(test)]
16mod tests;
17
18// The reuseport check reads a Linux socket option, and pinning an addressless
19// interface is Linux-only, so the whole file is.
20#[cfg(all(test, target_os = "linux"))]
21mod socket;
22
23use std::io;
24use std::net::Ipv4Addr;
25use std::path::PathBuf;
26use std::sync::atomic::{AtomicBool, Ordering};
27use std::sync::{Arc, Condvar, Mutex, MutexGuard};
28use std::thread::JoinHandle;
29use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
30
31use crate::event::{Event, EventHandler};
32use crate::rx::process_frame;
33use crate::state::{Device, State, CONFIG_GRACE};
34use crate::types::*;
35use crate::wire::{
36    build_hello, op, Addressee, BayUid, Conn, DeviceFeature, DeviceUid, FirmwareType, Opcode,
37    SendError, Tx, V2ipFpgaFeature, MULTICAST_IP, MULTICAST_PORT, PROTOCOL_VERSION, VERSION,
38};
39
40pub use control::ControlError;
41pub use info::{BayInfo, DeviceInfo};
42use osd::OsdSender;
43use schedule::Schedule;
44
45/// The name this client advertises when the caller sets none.
46const DEFAULT_NAME: &str = "MXR Rust";
47
48/// The serial this client advertises.
49///
50/// A client is not a unit with a serial number, but the field is fixed-width
51/// and every device fills it, so it carries a constant rather than a blank.
52const CLIENT_SERIAL: &str = "P9SN00000000";
53
54/// The file the client's identifier is kept in, under the user's home
55/// directory. The identifier has to survive a restart, or every peer would see
56/// each run as a new client.
57const UID_FILE: &str = ".mxr-uid";
58
59/// Largest datagram the receive buffer accepts.
60const RECV_BUFFER: usize = 65535;
61
62/// How often the background thread re-examines the network.
63const PROBE_TICK: Duration = Duration::from_secs(1);
64
65/// How often that thread looks up from waiting to see whether it should stop.
66const SHUTDOWN_POLL: Duration = Duration::from_millis(50);
67
68/// Shortest gap between two discovery requests.
69const DISCOVER_INTERVAL: Duration = Duration::from_secs(5);
70
71/// How long a discover to every device stands in for asking any one of them.
72///
73/// Each device answers it with everything it holds, after a jitter of up to a
74/// few seconds, so asking one again inside this window repeats an answer
75/// already on its way.
76const DISCOVER_COVERS: Duration = Duration::from_secs(60);
77
78/// How a [`Remote`] finds the network.
79///
80/// [`Config::default`] discovers over multicast on the interface the host
81/// picks, which is the right answer on a single-homed machine and arbitrary on
82/// any other.
83#[derive(Clone, Debug, Default)]
84#[non_exhaustive]
85pub struct Config {
86    /// Where to send. Unset means the multicast group, or the interface's
87    /// broadcast address when [`Config::broadcast`] is set.
88    pub target_ip: Option<Ipv4Addr>,
89    /// UDP port. Unset means the default for the selected mode.
90    pub port: Option<u16>,
91    /// Selects the interface by address.
92    ///
93    /// It becomes both the multicast egress interface and the membership
94    /// interface, so it decides which NIC frames leave by and which one they
95    /// are accepted on. Getting it wrong on a multi-homed host is one-sided:
96    /// periodic broadcasts still arrive, so discovery looks healthy while
97    /// every request this client sends leaves by the wrong NIC and is never
98    /// answered.
99    pub local_ip: Option<Ipv4Addr>,
100    /// Selects the interface by name, taking precedence over
101    /// [`Config::local_ip`].
102    ///
103    /// An interface with no address of its own - a tagged VLAN - can only be
104    /// named this way, and only on Linux.
105    pub interface: Option<String>,
106    /// Use broadcast rather than multicast.
107    pub broadcast: bool,
108    /// The name this client advertises. Unset means `MXR Rust`.
109    pub name: Option<String>,
110    /// This client's identifier. Unset loads it from
111    /// [`Config::uid_path`], generating and storing one on first run.
112    pub uid: Option<DeviceUid>,
113    /// Where the identifier is kept. Unset means `.mxr-uid` in the user's home
114    /// directory.
115    pub uid_path: Option<PathBuf>,
116    /// The TCP port units fetch an overlay sync's logo images from. Unset
117    /// picks a free one when the first sync with images is sent.
118    pub logo_port: Option<u16>,
119}
120
121/// Everything the threads share.
122struct Shared {
123    uid: DeviceUid,
124    name: String,
125    handler: Arc<dyn EventHandler>,
126    state: Mutex<State>,
127    tx: Mutex<Tx>,
128    schedule: Mutex<Schedule>,
129    network: Mutex<Network>,
130    osd: Mutex<OsdSender>,
131    /// Woken when units ask for overlay settings.
132    osd_queued: Condvar,
133    logo_port: Option<u16>,
134    closing: AtomicBool,
135}
136
137/// The network parameters the socket was opened with, so it can be reopened.
138#[derive(Clone, Debug)]
139struct Network {
140    target_ip: Option<Ipv4Addr>,
141    port: Option<u16>,
142    local_ip: Option<Ipv4Addr>,
143    interface: Option<String>,
144    broadcast: bool,
145}
146
147impl Network {
148    /// The address to send to: what the caller asked for, else the multicast
149    /// group, else - in broadcast mode - the chosen interface's own broadcast
150    /// address.
151    fn target(&self) -> io::Result<Ipv4Addr> {
152        if let Some(ip) = self.target_ip {
153            return Ok(ip);
154        }
155        if !self.broadcast {
156            return Ok(MULTICAST_IP);
157        }
158        Ok(crate::wire::broadcast_address(self.local_ip).unwrap_or(MULTICAST_IP))
159    }
160
161    fn port(&self) -> u16 {
162        self.port.unwrap_or(if self.broadcast {
163            crate::wire::BROADCAST_PORT
164        } else {
165            MULTICAST_PORT
166        })
167    }
168
169    fn open(&self) -> io::Result<Conn> {
170        Conn::open(
171            self.target()?,
172            self.port(),
173            self.local_ip,
174            self.interface.as_deref(),
175        )
176    }
177}
178
179/// A client on the MX Remote network.
180///
181/// Create one with [`Remote::new`], then [`Remote::start`] it. Discovery runs
182/// on threads this client owns until [`Remote::close`], or until the `Remote`
183/// is dropped.
184pub struct Remote {
185    shared: Arc<Shared>,
186    workers: Mutex<Vec<JoinHandle<()>>>,
187}
188
189impl Remote {
190    /// Builds a client, loading or generating its identifier.
191    ///
192    /// Nothing is sent and no socket is opened until [`Remote::start`].
193    pub fn new(config: Config, handler: Arc<dyn EventHandler>) -> io::Result<Self> {
194        let uid = match config.uid {
195            Some(uid) => uid,
196            None => load_uid(config.uid_path.clone())?,
197        };
198        let name = config.name.unwrap_or_else(|| DEFAULT_NAME.to_owned());
199        Ok(Self {
200            shared: Arc::new(Shared {
201                uid,
202                name,
203                handler,
204                state: Mutex::new(State::new(uid)),
205                tx: Mutex::new(Tx::default()),
206                schedule: Mutex::new(Schedule::new()),
207                network: Mutex::new(Network {
208                    target_ip: config.target_ip,
209                    port: config.port,
210                    local_ip: config.local_ip,
211                    interface: config.interface,
212                    broadcast: config.broadcast,
213                }),
214                osd: Mutex::new(OsdSender::new()),
215                osd_queued: Condvar::new(),
216                logo_port: config.logo_port,
217                closing: AtomicBool::new(false),
218            }),
219            workers: Mutex::new(Vec::new()),
220        })
221    }
222
223    /// Opens the socket, announces this client and begins discovery.
224    ///
225    /// Returns once the receive thread is running; discovery continues in the
226    /// background.
227    pub fn start(&self) -> io::Result<()> {
228        let conn = lock(&self.shared.network).open()?;
229        lock(&self.shared.tx).set_conn(Some(conn));
230        self.shared.closing.store(false, Ordering::SeqCst);
231        self.spawn_workers()?;
232        self.shared.announce();
233        let _ = self.shared.discover();
234        Ok(())
235    }
236
237    /// Stops discovery, closes the socket and waits for the threads to finish.
238    ///
239    /// Calling it more than once, or before [`Remote::start`], does nothing.
240    pub fn close(&self) {
241        self.shared.closing.store(true, Ordering::SeqCst);
242        for worker in std::mem::take(&mut *lock(&self.workers)) {
243            let _ = worker.join();
244        }
245        // A closed client answers no unit, so it holds no sync either.
246        self.shared.drop_osd_sync();
247        // Only once no thread can still be reading from it: a descriptor
248        // released while another thread is parked on it can be reissued to
249        // something else before that thread returns.
250        lock(&self.shared.tx).set_conn(None);
251    }
252
253    fn spawn_workers(&self) -> io::Result<()> {
254        let mut workers = lock(&self.workers);
255        if !workers.is_empty() {
256            return Ok(());
257        }
258        for (name, body) in [
259            ("mxr-rx", Shared::receive_loop as fn(&Shared)),
260            ("mxr-probe", Shared::probe_loop as fn(&Shared)),
261            ("mxr-osd", Shared::osd_loop as fn(&Shared)),
262        ] {
263            let shared = Arc::clone(&self.shared);
264            workers.push(
265                std::thread::Builder::new()
266                    .name(name.to_owned())
267                    .spawn(move || body(&shared))?,
268            );
269        }
270        Ok(())
271    }
272
273    // ---- identity ----
274
275    /// This client's identifier, as peers see it.
276    pub fn uid(&self) -> DeviceUid {
277        self.shared.uid
278    }
279
280    /// The name this client advertises.
281    pub fn name(&self) -> &str {
282        &self.shared.name
283    }
284
285    /// The address frames are being sent to, once started.
286    pub fn target(&self) -> Option<std::net::SocketAddrV4> {
287        lock(&self.shared.tx).conn().map(|conn| conn.target())
288    }
289
290    // ---- reading the registry ----
291
292    /// Every device heard from, in no particular order.
293    pub fn devices(&self) -> Vec<DeviceUid> {
294        self.shared
295            .read(|state| state.devices.keys().copied().collect())
296    }
297
298    /// A snapshot of one device.
299    pub fn device(&self, uid: DeviceUid) -> Option<DeviceInfo> {
300        let now = Instant::now();
301        self.shared
302            .read(|state| state.device(uid).map(|d| DeviceInfo::of(d, now)))
303    }
304
305    /// The device with the given serial number.
306    pub fn device_by_serial(&self, serial: &str) -> Option<DeviceUid> {
307        self.shared
308            .read(|state| state.device_by_serial(serial).map(|d| d.uid))
309    }
310
311    /// Resolves a device from its dotted-hex identifier, falling back to a
312    /// serial-number match.
313    pub fn resolve_device(&self, name: &str) -> Option<DeviceUid> {
314        if let Ok(uid) = name.parse::<DeviceUid>() {
315            if self.shared.read(|state| state.device(uid).is_some()) {
316                return Some(uid);
317            }
318        }
319        self.device_by_serial(name)
320    }
321
322    /// A snapshot of one bay.
323    pub fn bay(&self, uid: BayUid) -> Option<BayInfo> {
324        self.shared
325            .read(|state| state.bay(uid).map(|bay| BayInfo::of(state, bay)))
326    }
327
328    /// The bay on `device` with the given port name, such as `Output 1`.
329    pub fn bay_by_name(&self, device: DeviceUid, port_name: &str) -> Option<BayUid> {
330        self.shared.read(|state| {
331            state
332                .device(device)?
333                .bay_by_name(port_name)
334                .map(crate::state::Bay::uid)
335        })
336    }
337
338    /// The source bay advertising the given multicast group, for the video or
339    /// the audio stream.
340    pub fn bay_by_stream_ip(&self, ip: Ipv4Addr, audio: bool) -> Option<BayUid> {
341        self.shared.read(|state| state.bay_by_stream_ip(ip, audio))
342    }
343
344    /// The OneIP streams a device advertises.
345    pub fn v2ip_sources(&self, uid: DeviceUid) -> Option<Vec<V2ipStreamSources>> {
346        self.shared
347            .read(|state| state.device(uid)?.v2ip_sources.clone())
348    }
349
350    /// A OneIP device's own encoder configuration.
351    pub fn v2ip_details(&self, uid: DeviceUid) -> Option<DeviceV2ipDetails> {
352        self.shared.read(|state| state.device(uid)?.v2ip_details)
353    }
354
355    /// The streams a OneIP sink is subscribed to.
356    pub fn v2ip_sink(&self, uid: DeviceUid) -> Option<DeviceV2ipSink> {
357        self.shared.read(|state| state.device(uid)?.v2ip_sink)
358    }
359
360    /// Transport statistics a OneIP device reports.
361    pub fn v2ip_stats(&self, uid: DeviceUid) -> Option<V2ipDeviceStats> {
362        self.shared.read(|state| state.device(uid)?.v2ip_stats)
363    }
364
365    /// What a OneIP device's video processor supports.
366    ///
367    /// `None` until the device has reported a non-empty mask about itself.
368    /// A device reports nothing at all while its processor has yet to answer,
369    /// and an older processor answers with none of the optional commands, so
370    /// "no features" and "not yet known" are the same bytes on the wire and
371    /// both read as `None` here. A mask that has arrived only gains bits.
372    pub fn v2ip_features(&self, uid: DeviceUid) -> Option<V2ipFpgaFeature> {
373        self.shared.read(|state| state.device(uid)?.v2ip_features)
374    }
375
376    /// A OneIP device's settings, `None` until it has reported any.
377    ///
378    /// A device reports only the settings it has, so one missing from
379    /// [`V2ipDeviceSettings::valid`] once the rest have arrived is one the
380    /// device does not have.
381    pub fn v2ip_device_settings(&self, uid: DeviceUid) -> Option<V2ipDeviceSettings> {
382        self.shared.read(|state| state.device(uid)?.v2ip_settings)
383    }
384
385    /// The VLAN configuration a OneIP device last reported about itself,
386    /// `None` until it has reported one.
387    pub fn v2ip_vlan(&self, uid: DeviceUid) -> Option<V2ipVlan> {
388        self.shared.read(|state| state.device(uid)?.v2ip_vlan)
389    }
390
391    /// What a OneIP output last reported playing from a session description,
392    /// `None` until it has reported.
393    pub fn v2ip_sink_sdp(&self, uid: DeviceUid) -> Option<V2ipSinkSdp> {
394        self.shared
395            .read(|state| state.device(uid)?.v2ip_sink_sdp.clone())
396    }
397
398    /// How a OneIP source unit's streams are encrypted, as it last reported
399    /// it, `None` until it has reported.
400    pub fn v2ip_privacy(&self, uid: DeviceUid) -> Option<V2ipPrivacy> {
401        self.shared.read(|state| state.device(uid)?.v2ip_privacy)
402    }
403
404    /// The overlay sync a management unit last sent, `None` until it has
405    /// sent one.
406    pub fn osd_sync(&self, uid: DeviceUid) -> Option<OsdSync> {
407        self.shared
408            .read(|state| state.device(uid)?.osd_sync.clone())
409    }
410
411    /// How a unit last reported getting on with an overlay sync, `None` until
412    /// it has reported. Match it to the sync by `seq` and the sync's source.
413    pub fn osd_sync_report(&self, uid: DeviceUid) -> Option<OsdSyncReport> {
414        self.shared
415            .read(|state| state.device(uid)?.osd_sync_report.clone())
416    }
417
418    /// The message on a unit's output, as the unit last reported it to this
419    /// client, `None` until it has. A unit reports it only when asked, or
420    /// when this client writes it.
421    pub fn osd_message(&self, uid: DeviceUid) -> Option<OsdMessage> {
422        self.shared
423            .read(|state| state.device(uid)?.osd_message.clone())
424    }
425
426    /// The health and IP configuration a unit last reported, `None` until it
427    /// has reported them.
428    pub fn unit_status(&self, uid: DeviceUid) -> Option<UnitStatus> {
429        self.shared
430            .read(|state| state.device(uid)?.unit_status.clone())
431    }
432
433    /// The PTP state a OneIP device last reported about itself, `None` until
434    /// it has reported one, and while it has no PTP to report: none on its
435    /// video processor, or in power save.
436    pub fn v2ip_ptp(&self, uid: DeviceUid) -> Option<V2ipPtpState> {
437        self.shared.read(|state| state.device(uid)?.v2ip_ptp)
438    }
439
440    /// The test card a OneIP sink last reported, `None` until it has answered
441    /// [`Remote::request_v2ip_testcard`] or a change.
442    pub fn v2ip_testcard(&self, uid: DeviceUid) -> Option<V2ipTestcard> {
443        self.shared.read(|state| state.device(uid)?.v2ip_testcard)
444    }
445
446    /// The time zone a device announced for its mesh, `None` until it has.
447    ///
448    /// The mesh controller announces it with every periodic broadcast, empty
449    /// when it has none.
450    pub fn time_zone(&self, uid: DeviceUid) -> Option<TimeZone> {
451        self.shared
452            .read(|state| state.device(uid)?.time_zone.clone())
453    }
454
455    /// A device's clock as of now: the time it last announced, advanced by
456    /// how long ago that arrived. `None` until it has announced one.
457    ///
458    /// The mesh controller announces its clock with every periodic broadcast,
459    /// and only once it has been set.
460    pub fn device_clock(&self, uid: DeviceUid) -> Option<SystemTime> {
461        let (utc, at) = self.shared.read(|state| state.device(uid)?.clock)?;
462        let announced = UNIX_EPOCH.checked_add(Duration::from_secs(u64::from(utc)))?;
463        announced.checked_add(at.elapsed())
464    }
465
466    /// The video-wall tiling a OneIP device is configured for.
467    pub fn v2ip_tiling(&self, uid: DeviceUid) -> Option<V2ipTilingConfig> {
468        self.shared.read(|state| state.device(uid)?.tiling)
469    }
470
471    /// The audio endpoint tree a device exposes.
472    pub fn audio_endpoints(&self, uid: DeviceUid) -> Option<AudioEndpoints> {
473        self.shared.read(|state| state.device(uid)?.audio.clone())
474    }
475
476    /// The multiviewer layout a device is showing.
477    pub fn multiviewer_status(&self, uid: DeviceUid) -> Option<MultiviewerStatus> {
478        self.shared
479            .read(|state| state.device(uid)?.multiviewer.clone())
480    }
481
482    /// An amplifier's Dolby decoder settings.
483    pub fn dolby_settings(&self, uid: DeviceUid) -> Option<AmpDolbySettings> {
484        self.shared.read(|state| state.device(uid)?.dolby_settings)
485    }
486
487    /// A device's remote-control settings.
488    pub fn rc_settings(&self, uid: DeviceUid) -> Option<RcSettings> {
489        self.shared
490            .read(|state| state.device(uid)?.rc_settings.clone())
491    }
492
493    /// Every network port a device reports, in port order.
494    pub fn network_status(&self, uid: DeviceUid) -> Vec<NetworkPortStatus> {
495        self.shared.read(|state| {
496            state
497                .device(uid)
498                .map(|d| d.network.values().cloned().collect())
499                .unwrap_or_default()
500        })
501    }
502
503    /// The mesh topology a device reports.
504    pub fn topology(&self, uid: DeviceUid) -> Vec<TopologyEntry> {
505        self.shared.read(|state| {
506            state
507                .device(uid)
508                .map(|d| d.topology.clone())
509                .unwrap_or_default()
510        })
511    }
512
513    /// The EDID a device last reported: the display on its output, or the one
514    /// it presents to the source on its input.
515    ///
516    /// Filled in by a device's answer to [`Remote::request_edid`], and by any
517    /// answer to a peer's request that this client happened to hear.
518    pub fn edid(&self, uid: DeviceUid, output: bool) -> Option<Vec<u8>> {
519        self.shared
520            .read(|state| state.device(uid)?.edid(output).map(<[u8]>::to_vec))
521    }
522
523    /// How many frames from other senders have parsed since this client
524    /// started.
525    ///
526    /// It separates a mesh with nothing on it from an interface nothing is on:
527    /// a client that has discovered no device but is counting frames is
528    /// hearing traffic it cannot get answers from, which on a multi-homed host
529    /// is what a wrong [`Config::local_ip`] looks like. Frames this client
530    /// sent are not counted, because the host loops its own multicast back
531    /// whichever interface was selected.
532    pub fn frames_received(&self) -> u64 {
533        self.shared.read(|state| state.frames_received)
534    }
535
536    /// Every firmware image a device reports a version for.
537    pub fn firmware(&self, uid: DeviceUid) -> Vec<(FirmwareType, FirmwareVersion)> {
538        self.shared.read(|state| {
539            state
540                .device(uid)
541                .map(|d| d.firmware.iter().map(|(k, v)| (*k, v.clone())).collect())
542                .unwrap_or_default()
543        })
544    }
545
546    // ---- reconfiguring ----
547
548    /// Changes the interface and the multicast/broadcast mode while running,
549    /// reopening the socket when either differs from what is in use.
550    pub fn update_config(&self, local_ip: Option<Ipv4Addr>, broadcast: bool) -> io::Result<()> {
551        let network = {
552            let mut network = lock(&self.shared.network);
553            if network.local_ip == local_ip && network.broadcast == broadcast {
554                return Ok(());
555            }
556            network.local_ip = local_ip;
557            network.broadcast = broadcast;
558            network.clone()
559        };
560        // Opened before the old one is dropped, so a bind that fails leaves the
561        // client on the socket it had rather than on none.
562        let conn = network.open()?;
563        lock(&self.shared.tx).set_conn(Some(conn));
564        self.shared.announce();
565        let _ = self.shared.discover();
566        Ok(())
567    }
568
569    /// Asks every device on the network to announce itself.
570    pub fn discover(&self) -> Result<(), SendError> {
571        self.shared.discover()
572    }
573}
574
575impl Drop for Remote {
576    fn drop(&mut self) {
577        self.close();
578    }
579}
580
581impl Shared {
582    /// Reads the registry.
583    fn read<R>(&self, f: impl FnOnce(&State) -> R) -> R {
584        f(&lock(&self.state))
585    }
586
587    /// Mutates the registry, then delivers what changed.
588    ///
589    /// The queue is drained after the lock is released, so an event handler
590    /// may call back into the library.
591    fn mutate<R>(&self, f: impl FnOnce(&mut State, &mut Vec<Event>) -> R) -> R {
592        let mut events = Vec::new();
593        let result = {
594            let mut state = lock(&self.state);
595            let result = f(&mut state, &mut events);
596            state.mirror_mesh_events(&mut events);
597            result
598        };
599        self.dispatch(events);
600        result
601    }
602
603    fn dispatch(&self, events: Vec<Event>) {
604        for event in events {
605            event.dispatch(&*self.handler);
606        }
607    }
608
609    /// Decodes one datagram and delivers what it changed.
610    ///
611    /// This is the receive entry point. Keeping it distinct from the decode
612    /// below matters even though the wrapper is thin: announcing is driven by a
613    /// clock, and the one frame that may add a hello here is a ping addressed
614    /// to this client, which asks for exactly that.
615    fn process_datagram(&self, data: &[u8], from: Ipv4Addr) {
616        let (events, hello_requested, state_requests, osd_requests) = {
617            let mut state = lock(&self.state);
618            let events = process_frame(&mut state, data, Some(from), Instant::now());
619            (
620                events,
621                std::mem::take(&mut state.hello_requested),
622                std::mem::take(&mut state.state_requests),
623                std::mem::take(&mut state.osd_requests),
624            )
625        };
626        self.dispatch(events);
627        if hello_requested {
628            self.announce();
629        }
630        for device in state_requests {
631            self.request_state(device);
632        }
633        if !osd_requests.is_empty() {
634            self.answer_osd_requests(osd_requests);
635        }
636    }
637
638    /// Sends a frame, refusing one the addressee cannot decode.
639    fn send(&self, to: &Addressee, opcode: Opcode, payload: &[u8]) -> Result<usize, SendError> {
640        lock(&self.tx).send(to, self.uid, opcode, payload)
641    }
642
643    fn discover(&self) -> Result<(), SendError> {
644        lock(&self.schedule).discovered(Instant::now());
645        self.send(&Addressee::Broadcast, op::SYS_DISCOVER, &[])?;
646        Ok(())
647    }
648
649    /// Asks one device for everything it holds, with a discover naming it.
650    ///
651    /// Skipped while a discover to every device is recent enough to have
652    /// covered it. A receiver that predates the named form answers it as a
653    /// discover to every device, so each one asked costs the whole mesh's
654    /// state on such a network.
655    fn request_state(&self, device: DeviceUid) {
656        if lock(&self.schedule).discover_covers(Instant::now()) {
657            return;
658        }
659        let _ = self.send(&Addressee::Broadcast, op::SYS_DISCOVER, device.as_bytes());
660    }
661
662    /// Announces this client, and re-arms the announcement timer only once the
663    /// frame is away.
664    ///
665    /// The firmware resets its own hello timeout inside the branch where the
666    /// transmit succeeded. A send that fails is then retried on the next tick
667    /// rather than costing a whole interval of silence, which matters most at
668    /// startup and after a network blip: exactly when being heard is worth the
669    /// most.
670    ///
671    /// It announces `CONFIG_INITIALISED` because a unit uses the scaling
672    /// fields of a 0x3C write only from a sender that does, and this client
673    /// zeroes every field it does not set.
674    ///
675    /// While it holds an overlay sync it also announces itself as the mesh's
676    /// overlay sync manager, which is what has the controller leave units'
677    /// requests for a new sync to it.
678    fn announce(&self) {
679        let mut features = DeviceFeature::MANAGER | DeviceFeature::CONFIG_INITIALISED;
680        if lock(&self.osd).holds() {
681            features |= DeviceFeature::OSD_SYNC;
682        }
683        let payload = build_hello(
684            PROTOCOL_VERSION,
685            &self.name,
686            CLIENT_SERIAL,
687            VERSION,
688            features.bits(),
689        );
690        match self.send(&Addressee::Broadcast, op::SYS_HELLO, &payload) {
691            Ok(n) if n > 0 => lock(&self.schedule).announced(Instant::now()),
692            _ => {}
693        }
694    }
695
696    /// Waits out one tick, reporting whether the client is still running.
697    ///
698    /// Slept in short steps rather than one, so closing does not have to wait
699    /// out a whole tick before the thread can be joined.
700    fn sleep_until_next_tick(&self) -> bool {
701        let deadline = Instant::now() + PROBE_TICK;
702        while Instant::now() < deadline {
703            if self.closing.load(Ordering::SeqCst) {
704                return false;
705            }
706            std::thread::sleep(SHUTDOWN_POLL);
707        }
708        !self.closing.load(Ordering::SeqCst)
709    }
710
711    /// Whether it is time to announce again.
712    ///
713    /// This is a timer, not a response to traffic: a device announces itself on
714    /// a schedule whether or not anything is talking to it, and a client that
715    /// only re-announced when a datagram arrived would go silent on a quiet
716    /// network and stay unknown to every peer that started after it.
717    fn announce_due(&self, now: Instant) -> bool {
718        !self.closing.load(Ordering::SeqCst) && lock(&self.schedule).announce_due(now)
719    }
720
721    /// Reads datagrams until the client is closing.
722    fn receive_loop(&self) {
723        let mut buf = vec![0u8; RECV_BUFFER];
724        while !self.closing.load(Ordering::SeqCst) {
725            let Some(conn) = lock(&self.tx).conn() else {
726                break;
727            };
728            match conn.recv(&mut buf) {
729                Ok(Some((data, from))) => self.process_datagram(data, from),
730                Ok(None) => {}
731                Err(_) => break,
732            }
733        }
734    }
735
736    /// Runs [`Self::probe_once`] once a second until the client is closing.
737    fn probe_loop(&self) {
738        while self.sleep_until_next_tick() {
739            self.probe_once(Instant::now());
740        }
741    }
742
743    /// One pass: liveness, completion, the announcement timer and discovery.
744    pub(super) fn probe_once(&self, now: Instant) {
745        let want_discover = self.mutate(|state, ev| {
746            let mut incomplete = false;
747            let mut any_complete = false;
748            for device in state.devices.values_mut() {
749                device.check_online(now, ev);
750                // Re-tested here rather than only where a frame lands: a device
751                // stops waiting for its links on the clock alone, and no frame
752                // arrives to announce that.
753                device.check_config_complete(now, ev);
754                if device.configuration_complete(now) {
755                    any_complete = true;
756                } else if now.saturating_duration_since(device.first_seen) > CONFIG_GRACE {
757                    // Past the grace period a device has said nothing more, so
758                    // ask the network again rather than wait forever.
759                    incomplete = true;
760                }
761            }
762            // Nothing has finished describing itself, so nothing has been
763            // discovered yet at all.
764            incomplete || !any_complete
765        });
766
767        let discover_due = lock(&self.schedule).discover_due(now);
768        if self.announce_due(now) {
769            self.announce();
770        }
771        if want_discover && discover_due {
772            let _ = self.discover();
773        }
774    }
775}
776
777/// Takes a lock, continuing through a poisoning.
778///
779/// A poisoned lock here means a panic somewhere that held it. The state behind
780/// it is a cache of what devices have reported and is rebuilt by the next
781/// frame from each, so refusing to touch it again would retire a working
782/// client over one bad datagram.
783fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
784    m.lock().unwrap_or_else(|e| e.into_inner())
785}
786
787/// Loads the client's identifier, generating and storing one on first run.
788///
789/// A generated identifier that cannot be stored is still used: a client with a
790/// new identity each run is worse than one that works today, and the failure is
791/// the caller's home directory, not the network.
792fn load_uid(path: Option<PathBuf>) -> io::Result<DeviceUid> {
793    let path = path.or_else(|| {
794        std::env::var_os("HOME")
795            .or_else(|| std::env::var_os("USERPROFILE"))
796            .map(|home| PathBuf::from(home).join(UID_FILE))
797    });
798    if let Some(path) = &path {
799        if let Ok(bytes) = std::fs::read(path) {
800            if let Ok(array) = <[u8; 16]>::try_from(bytes.get(..16).unwrap_or_default()) {
801                return Ok(DeviceUid::from_array(array));
802            }
803        }
804    }
805    let mut bytes = [0u8; 16];
806    getrandom::getrandom(&mut bytes).map_err(|e| io::Error::other(e.to_string()))?;
807    if let Some(path) = &path {
808        let _ = std::fs::write(path, bytes);
809    }
810    Ok(DeviceUid::from_array(bytes))
811}
812
813/// The protocol floor is checked against what the device says it can decode.
814impl crate::wire::ProtocolTarget for Device {
815    fn serial(&self) -> &str {
816        Device::serial(self)
817    }
818
819    fn supported_protocol(&self) -> u16 {
820        self.hello.supported_protocol
821    }
822}