Skip to main content

mx_remote/wire/
tx.rs

1// Author: Lars Op den Kamp (lars@opdenkamp-it.nl)
2// Copyright (c) 2026 Op den Kamp IT Solutions
3
4//! The transmit path: the protocol-floor gate, and the only way through it.
5
6use std::fmt;
7use std::io;
8
9use super::conn::Conn;
10use super::frame::build_frame;
11use super::opcode::{stamp_for, Opcode};
12use super::uid::DeviceUid;
13
14/// Why a frame was not sent.
15#[derive(Debug)]
16#[non_exhaustive]
17pub enum SendError {
18    /// The addressed device speaks a protocol older than the opcode requires.
19    ///
20    /// A receiver silently drops any frame stamped above its own version, with
21    /// no NAK, so sending anyway would report success and change nothing.
22    ProtocolTooOld {
23        /// Serial number of the addressed device.
24        serial: String,
25        /// The opcode that was refused.
26        opcode: u16,
27        /// The protocol version the device reports.
28        have: u16,
29        /// The version the opcode requires.
30        need: u16,
31    },
32    /// The client is not connected, because it was never started or has been
33    /// closed.
34    NotConnected,
35    /// The socket write failed.
36    Io(io::Error),
37}
38
39impl fmt::Display for SendError {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            Self::ProtocolTooOld {
43                serial,
44                opcode,
45                have,
46                need,
47            } => write!(
48                f,
49                "{serial} reports protocol {have:#04x}, opcode {opcode:#04x} needs {need:#04x}"
50            ),
51            Self::NotConnected => write!(f, "not connected"),
52            Self::Io(e) => write!(f, "{e}"),
53        }
54    }
55}
56
57impl std::error::Error for SendError {
58    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
59        match self {
60            Self::Io(e) => Some(e),
61            _ => None,
62        }
63    }
64}
65
66impl From<io::Error> for SendError {
67    fn from(e: io::Error) -> Self {
68        Self::Io(e)
69    }
70}
71
72/// What the protocol floor is checked against.
73///
74/// A device reports the highest protocol version it can decode in its hello.
75pub(crate) trait ProtocolTarget {
76    /// Serial number, for the error message.
77    fn serial(&self) -> &str;
78    /// The version the device reports, or zero when it has not said.
79    fn supported_protocol(&self) -> u16;
80}
81
82/// Who a frame is addressed to.
83///
84/// Naming the recipient is a separate decision from building the payload,
85/// because the addressed device is a payload uid at an offset that differs per
86/// opcode while the opcode itself sits at a fixed place in the header. This is
87/// an enumeration rather than an `Option` so that a frame with no single
88/// recipient says so, instead of looking like one whose sender did not bother.
89#[derive(Clone, Debug)]
90pub(crate) enum Addressee {
91    /// Addressed to one device, whose protocol floor is checked.
92    Device {
93        /// Serial number, for the error message.
94        serial: String,
95        /// The version the device reports.
96        protocol: u16,
97    },
98    /// No single recipient. Only discovery, hello and the monitoring pulse.
99    Broadcast,
100}
101
102impl Addressee {
103    /// Addresses a frame to a device.
104    pub(crate) fn device(target: &dyn ProtocolTarget) -> Self {
105        Self::Device {
106            serial: target.serial().to_owned(),
107            protocol: target.supported_protocol(),
108        }
109    }
110}
111
112/// The transmit side of a client: the socket, and this client's own identifier.
113///
114/// [`Tx::send`] is the only path from an opcode to the wire. It is the gate
115/// that refuses a frame the target cannot decode, and it can be the gate
116/// because the two things it sits between - the frame constructor and the
117/// socket write - are both private to this module and unreachable from
118/// anywhere else in the crate.
119/// A tap on the transmit path, for a test reading back what would go on the
120/// wire.
121#[cfg(test)]
122pub(crate) type TxTap = std::sync::Arc<dyn Fn(&[u8]) + Send + Sync>;
123
124#[derive(Default)]
125pub(crate) struct Tx {
126    conn: Option<std::sync::Arc<Conn>>,
127    /// Captures each frame that passes the gate. Frames are assembled inside
128    /// the method that sends them and cannot be reached any other way, so this
129    /// is how a test reads back what would go on the wire.
130    #[cfg(test)]
131    tap: Option<TxTap>,
132}
133
134impl Tx {
135    /// Replaces the socket.
136    pub(crate) fn set_conn(&mut self, conn: Option<Conn>) {
137        self.conn = conn.map(std::sync::Arc::new);
138    }
139
140    /// A handle on the socket that outlives this lock.
141    ///
142    /// The receive thread parks in the kernel for as long as its read timeout,
143    /// and must not hold the transmit lock while it does or every send would
144    /// wait behind it. Holding a share of the socket instead also settles what
145    /// a reconfiguration does to a thread already reading: the old socket stays
146    /// open until that read returns, so its descriptor cannot be reissued to
147    /// something else underneath it.
148    pub(crate) fn conn(&self) -> Option<std::sync::Arc<Conn>> {
149        self.conn.clone()
150    }
151
152    #[cfg(test)]
153    pub(crate) fn set_tap(&mut self, tap: TxTap) {
154        self.tap = Some(tap);
155    }
156
157    /// Builds a frame and writes it to the wire, unless `to` cannot decode it.
158    ///
159    /// The frame is stamped with the version the opcode itself needs rather
160    /// than the version this library speaks, so a device that caps lower still
161    /// accepts every opcode it does understand. The gate compares against that
162    /// same stamp: a receiver drops what is stamped above its own version, so
163    /// checking anything else would leave the hole the gate exists to close.
164    pub(crate) fn send(
165        &self,
166        to: &Addressee,
167        uid: DeviceUid,
168        opcode: Opcode,
169        payload: &[u8],
170    ) -> Result<usize, SendError> {
171        let need = stamp_for(opcode);
172        if let Addressee::Device { serial, protocol } = to {
173            // A device that has not reported a version is let through: not
174            // knowing is not the same as knowing it is too old.
175            if *protocol != 0 && *protocol < need {
176                return Err(SendError::ProtocolTooOld {
177                    serial: serial.clone(),
178                    opcode: opcode.0,
179                    have: *protocol,
180                    need,
181                });
182            }
183        }
184
185        let frame = build_frame(uid, opcode, need, payload);
186        #[cfg(test)]
187        if let Some(tap) = &self.tap {
188            tap(&frame);
189        }
190        let conn = self.conn.as_ref().ok_or(SendError::NotConnected)?;
191        Ok(conn.send(&frame)?)
192    }
193}