mx_remote/lib.rs
1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4#![forbid(unsafe_code)]
5#![deny(missing_docs)]
6
7//! Client library for Pulse-Eight MatrixOS devices (neo matrices, OneIP/V2IP
8//! units, ProAmp8 amplifiers) over UDP multicast/broadcast.
9//!
10//! Devices announce themselves and their bays, report signal, audio, streaming
11//! and power state as it changes, and accept routing and configuration
12//! commands. A [`Remote`] discovers them, keeps a snapshot of what they have
13//! reported, and sends those commands.
14//!
15//! # Getting started
16//!
17//! ```no_run
18//! use std::sync::{Arc, OnceLock};
19//!
20//! use mx_remote::{Config, DeviceUid, EventHandler, Remote};
21//!
22//! // A handler is handed to the client that will call it, so it cannot hold
23//! // one at the time it is built. It is filled in before the client starts,
24//! // which is before anything can call back.
25//! static CLIENT: OnceLock<Arc<Remote>> = OnceLock::new();
26//!
27//! struct Printer;
28//!
29//! impl EventHandler for Printer {
30//! fn on_device_update(&self, device: DeviceUid) {
31//! let Some(info) = CLIENT.get().and_then(|c| c.device(device)) else {
32//! return;
33//! };
34//! println!("{device} {} {}", info.model, info.name);
35//! }
36//! }
37//!
38//! let remote = Arc::new(Remote::new(Config::default(), Arc::new(Printer))?);
39//! let _ = CLIENT.set(Arc::clone(&remote));
40//! remote.start()?;
41//! # Ok::<(), std::io::Error>(())
42//! ```
43//!
44//! [`Config::default`] discovers over multicast on the interface the host
45//! picks, which is the right answer on a single-homed machine and arbitrary on
46//! any other. `cargo run --example discover` is the program above, complete.
47//!
48//! # Events say what moved
49//!
50//! Every method on [`EventHandler`] has a default that does nothing, so a
51//! caller implements only what it acts on. Most carry just the identifier of
52//! the device or bay that changed: the snapshot read back from [`Remote`] is
53//! the same state the event was derived from, and can only be fresher.
54//! [`EventHandler::on_device_update`] and [`EventHandler::on_bay_update`] fire
55//! after every event at their level, which is enough for a caller that only
56//! wants to know that something moved.
57//!
58//! Handler methods run on the receive thread, so they should return quickly.
59//!
60//! # Values arrive as they were sent
61//!
62//! Every enumeration here is a newtype over its wire integer with named
63//! constants rather than a closed set, so a value from firmware newer than
64//! this library reaches the caller unchanged instead of being folded onto a
65//! neighbour.
66//!
67//! An [`Option`] on a snapshot is a field the device has not reported yet,
68//! which is a different answer from one reported as zero or false. Zero is a
69//! valid reading for most of them, so a confidently wrong value would be worse
70//! than an absent one.
71//!
72//! # Threads
73//!
74//! [`Remote::start`] takes a receive thread and a timer thread, and
75//! [`Remote::close`] - or dropping the `Remote` - stops and joins them. There
76//! is no async runtime.
77
78mod event;
79mod runtime;
80
81mod rx;
82mod state;
83#[cfg(test)]
84mod testing;
85mod types;
86mod wire;
87
88pub use event::{Event, EventHandler};
89pub use runtime::{BayInfo, Config, ControlError, DeviceInfo, Remote};
90pub use rx::{lookup_svd, Svd};
91pub use types::*;
92pub use wire::*;