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