Skip to main content

rolcb/
traction.rs

1//! Traction Control protocol payloads (MTI 0x05EB command / 0x01E9 reply)
2//! and the Train Search protocol.
3//!
4//! In OpenLCB every locomotive (or consist) is a **train node**. For DCC
5//! locos the train node is usually virtual and lives in the command
6//! station, which turns traction commands into DCC packets. A throttle
7//! drives a train by sending addressed *Traction Control Command* messages
8//! to it and reading *Traction Control Reply* messages back, as defined by
9//! the *Traction Control Standard*.
10//!
11//! This module only encodes and decodes the payload bytes. Wrap them in
12//! [`Message::TractionCommand`](crate::Message::TractionCommand) /
13//! [`Message::TractionReply`](crate::Message::TractionReply) and send them
14//! through a [`Node`](crate::Node). The [throttle guide](crate::guide::throttle)
15//! walks through a whole session.
16//!
17//! # A throttle session
18//!
19//! 1. **Find the train.** Send Identify Producer for a
20//!    [search event](search) built from the DCC address. The train node
21//!    (allocated by the command station if needed) answers with Producer
22//!    Identified for the same event; its source alias is the train.
23//! 2. **Assign controller.** Send [`Command::AssignController`] with our
24//!    Node ID. The train answers [`Reply::AssignController`] with result 0
25//!    if accepted. Only the assigned controller should send speed commands.
26//! 3. **Drive.** [`Command::SetSpeed`], [`Command::SetFunction`],
27//!    [`Command::EmergencyStop`]. Query state with [`Command::QuerySpeed`] /
28//!    [`Command::QueryFunction`].
29//! 4. **Stay alive.** A train may send [`Reply::Heartbeat`] asking for any
30//!    command within `timeout_s` seconds; answer with [`Command::Noop`] or
31//!    the train will stop itself.
32//! 5. **Release** with [`Command::ReleaseController`].
33//!
34//! # Controller handover
35//!
36//! When a second throttle sends Assign Controller to a train that already
37//! has a controller, the train asks the current controller first. It
38//! sends the *command* [`Command::ControllerChangingNotify`] (`20 04 flags
39//! NodeID`, carrying the new controller's Node ID) **to the current
40//! controller**, which answers with the *reply* [`Reply::ControllerChanged`]
41//! (`20 04 result`): result 0 accepts the handover, nonzero refuses it.
42//! So a throttle receives a Traction Control *Command* in this one case,
43//! even though it is not a train; [`Node`](crate::Node) delivers it
44//! instead of rejecting it. On accepting, the throttle should stop sending
45//! commands to that train and tell the user it lost control. The
46//! [throttle guide](crate::guide::throttle#losing-control-to-another-throttle)
47//! shows the exchange.
48//!
49//! # Command bytes
50//!
51//! | Byte 0 | Byte 1 | Meaning | Rest |
52//! |---|---|---|---|
53//! | `0x00` | | Set Speed | half-float speed (2 bytes) |
54//! | `0x01` | | Set Function | 24-bit address, 16-bit value |
55//! | `0x02` | | Emergency Stop | |
56//! | `0x10` | | Query Speed | |
57//! | `0x11` | | Query Function | 24-bit address |
58//! | `0x20` | `0x01` | Assign Controller | flags, Node ID (6) |
59//! | `0x20` | `0x02` | Release Controller | flags, Node ID (6) |
60//! | `0x20` | `0x03` | Query Controller | |
61//! | `0x20` | `0x04` | Controller Changing Notify (train → current controller) | flags, new controller Node ID (6) |
62//! | `0x30` | | Consist (listener) config | |
63//! | `0x40` | `0x01` | Management: Reserve | |
64//! | `0x40` | `0x02` | Management: Release | |
65//! | `0x40` | `0x03` | Management: Noop / heartbeat | |
66//!
67//! Replies reuse the same command bytes; see [`Reply`]. Constants are in
68//! [`cmd`]. Bit 7 of byte 0 ([`cmd::LISTENER_FLAG`]) marks a command
69//! forwarded to a consist member and is ignored by [`Command::decode`].
70//!
71//! ```
72//! use rolcb::traction::{Command, Reply, Speed};
73//!
74//! // 1.0 m/s forward is half-float 0x3C00.
75//! assert_eq!(Command::SetSpeed(Speed::from_mps(1.0, false)).encode(), [0x00, 0x3C, 0x00]);
76//! // Same speed in reverse: the sign bit is set.
77//! assert_eq!(Command::SetSpeed(Speed::from_mps(1.0, true)).encode(), [0x00, 0xBC, 0x00]);
78//! // F3 on.
79//! assert_eq!(Command::SetFunction { addr: 3, value: 1 }.encode(), [0x01, 0, 0, 3, 0, 1]);
80//!
81//! assert_eq!(Reply::decode(&[0x40, 0x03, 10]), Reply::Heartbeat { timeout_s: 10 });
82//! ```
83
84use alloc::vec::Vec;
85
86use crate::event::EventId;
87use crate::float16;
88use crate::node_id::NodeId;
89
90const MPS_PER_MPH: f32 = 0.44704;
91
92/// A traction speed: IEEE half-float in scale m/s, sign bit = reverse.
93///
94/// The inner `u16` is the raw half-float as sent on the wire (see
95/// [`crate::float16`]). The *Traction Control Standard* expresses speed in
96/// scale meters per second, with the sign bit as the direction: set means
97/// reverse. Direction is therefore kept even at zero speed, which is why
98/// there are two stop values. The magnitude accessors ([`Speed::mps`],
99/// [`Speed::mph`]) always return a non-negative number; use
100/// [`Speed::is_reverse`] for the direction.
101///
102/// ```
103/// use rolcb::traction::Speed;
104///
105/// let s = Speed::from_mph(30.0, true);
106/// assert!(s.is_reverse());
107/// assert!((s.mph() - 30.0).abs() < 0.05);
108/// assert!(s.mps() > 0.0);
109///
110/// assert_eq!(Speed::from_mps(0.0, true), Speed::STOP_REVERSE);
111/// assert_eq!(Speed::default(), Speed::STOP_FORWARD);
112/// assert!(Speed::NAN.is_nan());
113/// ```
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
115pub struct Speed(pub u16);
116
117impl Speed {
118    /// "Not available", used in Query Speed replies.
119    pub const NAN: Speed = Speed(0xFFFF);
120    /// Zero speed, facing forward (`+0.0`, `0x0000`).
121    pub const STOP_FORWARD: Speed = Speed(0x0000);
122    /// Zero speed, facing reverse (`-0.0`, `0x8000`).
123    pub const STOP_REVERSE: Speed = Speed(0x8000);
124
125    /// Build from a magnitude in scale m/s and a direction. The sign of `mps`
126    /// is ignored; only `reverse` sets the direction.
127    #[must_use]
128    pub fn from_mps(mps: f32, reverse: bool) -> Speed {
129        let mut v = float16::from_f32(mps.abs());
130        if reverse {
131            v |= 0x8000;
132        }
133        Speed(v)
134    }
135
136    /// Build from a magnitude in scale miles per hour (1 mph = 0.44704 m/s)
137    /// and a direction.
138    #[must_use]
139    pub fn from_mph(mph: f32, reverse: bool) -> Speed {
140        Self::from_mps(mph * MPS_PER_MPH, reverse)
141    }
142
143    /// True if the direction (sign) bit is set.
144    #[must_use]
145    pub fn is_reverse(self) -> bool {
146        self.0 & 0x8000 != 0
147    }
148
149    /// True for any half-float NaN, such as [`Speed::NAN`].
150    #[must_use]
151    pub fn is_nan(self) -> bool {
152        float16::to_f32(self.0).is_nan()
153    }
154
155    /// Magnitude in m/s (never negative; see [`Speed::is_reverse`]).
156    ///
157    /// For a NaN speed such as [`Speed::NAN`] ("not available") this
158    /// returns `f32::NAN`, so check [`Speed::is_nan`] before doing
159    /// arithmetic or comparisons with the result.
160    ///
161    /// ```
162    /// use rolcb::traction::Speed;
163    /// assert!(Speed::NAN.mps().is_nan());
164    /// assert_eq!(Speed::from_mps(2.0, true).mps(), 2.0);
165    /// ```
166    #[must_use]
167    pub fn mps(self) -> f32 {
168        float16::to_f32(self.0 & 0x7FFF)
169    }
170
171    /// Magnitude in mph. DCC command stations conventionally map mph 1:1
172    /// onto 128-step speed steps (126 = full). NaN for a NaN speed, like
173    /// [`Speed::mps`].
174    #[must_use]
175    pub fn mph(self) -> f32 {
176        self.mps() / MPS_PER_MPH
177    }
178}
179
180/// Traction Control command and sub-command byte values.
181///
182/// Byte 0 of every command and reply is one of the top-level values; for
183/// [`CONTROLLER_CONFIG`](crate::traction::cmd::CONTROLLER_CONFIG) and
184/// [`MANAGEMENT`](crate::traction::cmd::MANAGEMENT), byte 1 is a sub-command.
185pub mod cmd {
186    /// Set Speed (followed by a 2-byte half-float speed).
187    pub const SET_SPEED: u8 = 0x00;
188    /// Set Function (24-bit function address, 16-bit value).
189    pub const SET_FUNCTION: u8 = 0x01;
190    /// Emergency Stop: stop now and stay stopped until a new speed is set.
191    pub const EMERGENCY_STOP: u8 = 0x02;
192    /// Query Speed; the reply carries set, commanded and actual speeds.
193    pub const QUERY_SPEED: u8 = 0x10;
194    /// Query Function (24-bit function address).
195    pub const QUERY_FUNCTION: u8 = 0x11;
196    /// Controller Configuration group (assign/release/query controller).
197    pub const CONTROLLER_CONFIG: u8 = 0x20;
198    /// Consist (listener) Configuration group. Not decoded by this crate.
199    pub const CONSIST_CONFIG: u8 = 0x30;
200    /// Traction Management group (reserve/release/noop).
201    pub const MANAGEMENT: u8 = 0x40;
202    /// Set on commands forwarded to consist listeners.
203    pub const LISTENER_FLAG: u8 = 0x80;
204
205    /// Controller Configuration sub-command: Assign Controller.
206    pub const CONTROLLER_ASSIGN: u8 = 0x01;
207    /// Controller Configuration sub-command: Release Controller.
208    pub const CONTROLLER_RELEASE: u8 = 0x02;
209    /// Controller Configuration sub-command: Query Controller.
210    pub const CONTROLLER_QUERY: u8 = 0x03;
211    /// Controller Configuration sub-command: Controller Changing Notify. As
212    /// a command the train sends it to its current controller when another
213    /// node asks to take over; as a reply the controller accepts (0) or
214    /// refuses the handover.
215    pub const CONTROLLER_CHANGED: u8 = 0x04;
216
217    /// Management sub-command: Reserve (lock the train for a multi-step operation).
218    pub const MGMT_RESERVE: u8 = 0x01;
219    /// Management sub-command: Release a reservation.
220    pub const MGMT_RELEASE: u8 = 0x02;
221    /// Management sub-command: No-op / heartbeat. As a command it is a
222    /// keep-alive; as a reply it is a heartbeat request with a timeout.
223    pub const MGMT_NOOP: u8 = 0x03;
224}
225
226/// A Traction Control Command (throttle → train).
227///
228/// Encode with [`Command::encode`] and send as the payload of
229/// [`Message::TractionCommand`](crate::Message::TractionCommand). A train
230/// implementation decodes incoming payloads with [`Command::decode`].
231///
232/// ```
233/// use rolcb::traction::{Command, Speed};
234/// use rolcb::NodeId;
235///
236/// let me = NodeId::new(0x0501_0101_2299);
237/// let bytes = Command::AssignController { flags: 0, controller: me }.encode();
238/// assert_eq!(bytes, [0x20, 0x01, 0x00, 0x05, 0x01, 0x01, 0x01, 0x22, 0x99]);
239/// assert_eq!(Command::decode(&bytes), Command::AssignController { flags: 0, controller: me });
240///
241/// assert_eq!(Command::Noop.encode(), [0x40, 0x03]);
242/// assert_eq!(Command::decode(&[0x7F]), Command::Unknown(vec![0x7F]));
243/// ```
244///
245/// The enum is `#[non_exhaustive]`: match it with a `_` arm.
246#[derive(Debug, Clone, PartialEq, Eq, Hash)]
247#[non_exhaustive]
248pub enum Command {
249    /// Set speed and direction (`0x00`). Also clears an emergency stop.
250    SetSpeed(Speed),
251    /// Set a function (`0x01`). For DCC trains, addresses 0..=28 are F0..F28.
252    SetFunction {
253        /// 24-bit function address.
254        addr: u32,
255        /// New value; for on/off functions 0 = off, 1 = on (see [`function_value`]).
256        value: u16,
257    },
258    /// Emergency stop (`0x02`): stop immediately, ignoring momentum.
259    EmergencyStop,
260    /// Ask for the current speed (`0x10`); answered by [`Reply::QuerySpeed`].
261    QuerySpeed,
262    /// Ask for a function value (`0x11`); answered by [`Reply::QueryFunction`].
263    QueryFunction {
264        /// 24-bit function address.
265        addr: u32,
266    },
267    /// Become this train's controller (`0x20 0x01`). Answered by
268    /// [`Reply::AssignController`]; if another throttle was in control, the
269    /// train first asks it with [`Command::ControllerChangingNotify`].
270    AssignController {
271        /// Flags byte (0 for a normal throttle).
272        flags: u8,
273        /// The controlling throttle's Node ID.
274        controller: NodeId,
275    },
276    /// Stop being this train's controller (`0x20 0x02`).
277    ReleaseController {
278        /// Flags byte (normally 0).
279        flags: u8,
280        /// The Node ID that was assigned.
281        controller: NodeId,
282    },
283    /// Ask which node controls the train (`0x20 0x03`).
284    QueryController,
285    /// Controller Changing Notify (`0x20 0x04 flags NodeID`), **train →
286    /// current controller**: another node (`new_controller`) asked to take
287    /// over. The controller answers with [`Reply::ControllerChanged`]
288    /// (result 0 = accept). This is the one command a throttle receives;
289    /// [`Node`](crate::Node) never auto-rejects it.
290    ControllerChangingNotify {
291        /// Flags byte (normally 0).
292        flags: u8,
293        /// The node that wants to become the controller.
294        new_controller: NodeId,
295    },
296    /// Reserve the train for a multi-step operation (`0x40 0x01`).
297    Reserve,
298    /// Release a reservation (`0x40 0x02`).
299    Release,
300    /// Keep-alive, also the answer to a heartbeat request.
301    ///
302    /// Encoded as `0x40 0x03`.
303    Noop,
304    /// Anything not recognised (or too short to decode), raw bytes kept.
305    Unknown(Vec<u8>),
306}
307
308fn addr24(addr: u32) -> [u8; 3] {
309    let b = addr.to_be_bytes();
310    [b[1], b[2], b[3]]
311}
312
313fn read24(p: &[u8], i: usize) -> Option<u32> {
314    match p.get(i..i + 3) {
315        Some(&[a, b, c]) => Some(u32::from_be_bytes([0, a, b, c])),
316        _ => None,
317    }
318}
319
320fn read16(p: &[u8], i: usize) -> Option<u16> {
321    match p.get(i..i + 2) {
322        Some(&[a, b]) => Some(u16::from_be_bytes([a, b])),
323        _ => None,
324    }
325}
326
327impl Command {
328    /// Serialize to the command payload bytes.
329    #[must_use]
330    pub fn encode(&self) -> Vec<u8> {
331        use cmd::*;
332        let mut v = Vec::with_capacity(9);
333        match self {
334            Command::SetSpeed(s) => {
335                v.push(SET_SPEED);
336                v.extend_from_slice(&s.0.to_be_bytes());
337            }
338            Command::SetFunction { addr, value } => {
339                v.push(SET_FUNCTION);
340                v.extend_from_slice(&addr24(*addr));
341                v.extend_from_slice(&value.to_be_bytes());
342            }
343            Command::EmergencyStop => v.push(EMERGENCY_STOP),
344            Command::QuerySpeed => v.push(QUERY_SPEED),
345            Command::QueryFunction { addr } => {
346                v.push(QUERY_FUNCTION);
347                v.extend_from_slice(&addr24(*addr));
348            }
349            Command::AssignController { flags, controller } => {
350                v.extend_from_slice(&[CONTROLLER_CONFIG, CONTROLLER_ASSIGN, *flags]);
351                v.extend_from_slice(&controller.to_bytes());
352            }
353            Command::ReleaseController { flags, controller } => {
354                v.extend_from_slice(&[CONTROLLER_CONFIG, CONTROLLER_RELEASE, *flags]);
355                v.extend_from_slice(&controller.to_bytes());
356            }
357            Command::QueryController => v.extend_from_slice(&[CONTROLLER_CONFIG, CONTROLLER_QUERY]),
358            Command::ControllerChangingNotify {
359                flags,
360                new_controller,
361            } => {
362                v.extend_from_slice(&[CONTROLLER_CONFIG, CONTROLLER_CHANGED, *flags]);
363                v.extend_from_slice(&new_controller.to_bytes());
364            }
365            Command::Reserve => v.extend_from_slice(&[MANAGEMENT, MGMT_RESERVE]),
366            Command::Release => v.extend_from_slice(&[MANAGEMENT, MGMT_RELEASE]),
367            Command::Noop => v.extend_from_slice(&[MANAGEMENT, MGMT_NOOP]),
368            Command::Unknown(p) => v.extend_from_slice(p),
369        }
370        v
371    }
372
373    /// Parse command payload bytes. Never fails; unknown or truncated
374    /// commands become [`Command::Unknown`]. The consist
375    /// [`LISTENER_FLAG`](cmd::LISTENER_FLAG) bit in byte 0 is ignored.
376    #[must_use]
377    pub fn decode(p: &[u8]) -> Command {
378        use cmd::*;
379        let unknown = || Command::Unknown(p.to_vec());
380        let Some(&first) = p.first() else {
381            return Command::Unknown(Vec::new());
382        };
383        let sub = p.get(1).copied().unwrap_or(0);
384        // flags byte + Node ID, for the controller sub-commands.
385        let flags_node = || match p.get(2..) {
386            Some([flags, rest @ ..]) => NodeId::from_bytes(rest).ok().map(|n| (*flags, n)),
387            _ => None,
388        };
389        match first & !LISTENER_FLAG {
390            SET_SPEED => read16(p, 1).map_or_else(unknown, |s| Command::SetSpeed(Speed(s))),
391            SET_FUNCTION => match (read24(p, 1), read16(p, 4)) {
392                (Some(addr), Some(value)) => Command::SetFunction { addr, value },
393                _ => unknown(),
394            },
395            EMERGENCY_STOP => Command::EmergencyStop,
396            QUERY_SPEED => Command::QuerySpeed,
397            QUERY_FUNCTION => {
398                read24(p, 1).map_or_else(unknown, |addr| Command::QueryFunction { addr })
399            }
400            CONTROLLER_CONFIG => match (sub, flags_node()) {
401                (CONTROLLER_ASSIGN, Some((flags, controller))) => {
402                    Command::AssignController { flags, controller }
403                }
404                (CONTROLLER_RELEASE, Some((flags, controller))) => {
405                    Command::ReleaseController { flags, controller }
406                }
407                (CONTROLLER_QUERY, _) => Command::QueryController,
408                (CONTROLLER_CHANGED, Some((flags, new_controller))) => {
409                    Command::ControllerChangingNotify {
410                        flags,
411                        new_controller,
412                    }
413                }
414                _ => unknown(),
415            },
416            MANAGEMENT => match sub {
417                MGMT_RESERVE => Command::Reserve,
418                MGMT_RELEASE => Command::Release,
419                MGMT_NOOP => Command::Noop,
420                _ => unknown(),
421            },
422            _ => unknown(),
423        }
424    }
425
426    /// True if `payload` is a Controller Changing Notify command
427    /// (`20 04 …`), the one Traction Control Command a throttle receives.
428    /// Cheaper than a full [`Command::decode`].
429    ///
430    /// ```
431    /// use rolcb::traction::Command;
432    /// assert!(Command::is_controller_changing_notify(&[0x20, 0x04, 0, 5, 1, 1, 1, 0x22, 0x77]));
433    /// assert!(!Command::is_controller_changing_notify(&[0x20, 0x01]));
434    /// ```
435    #[must_use]
436    pub fn is_controller_changing_notify(payload: &[u8]) -> bool {
437        payload.starts_with(&[cmd::CONTROLLER_CONFIG, cmd::CONTROLLER_CHANGED])
438    }
439}
440
441/// A Traction Control Reply (train → throttle).
442///
443/// Decode the payload of a received
444/// [`Message::TractionReply`](crate::Message::TractionReply) with
445/// [`Reply::decode`]; a train implementation builds replies with
446/// [`Reply::encode`].
447///
448/// ```
449/// use rolcb::traction::{Reply, Speed};
450///
451/// let r = Reply::QuerySpeed {
452///     set_speed: Speed::from_mph(10.0, true),
453///     estop: false,
454///     commanded: Speed::NAN,
455///     actual: Speed::NAN,
456/// };
457/// assert_eq!(Reply::decode(&r.encode()), r);
458/// assert_eq!(Reply::decode(&[0x20, 0x01, 0x00]), Reply::AssignController { result: 0 });
459/// ```
460///
461/// The enum is `#[non_exhaustive]`: match it with a `_` arm.
462#[derive(Debug, Clone, PartialEq, Eq, Hash)]
463#[non_exhaustive]
464pub enum Reply {
465    /// Answer to [`Command::QuerySpeed`] (`0x10`).
466    QuerySpeed {
467        /// The last speed set by a controller.
468        set_speed: Speed,
469        /// True if the train is in emergency stop (status byte bit 0).
470        estop: bool,
471        /// The speed currently being commanded (may lag `set_speed` with
472        /// momentum); [`Speed::NAN`] if not available or not sent.
473        commanded: Speed,
474        /// Measured actual speed; [`Speed::NAN`] if not available or not sent.
475        actual: Speed,
476    },
477    /// Answer to [`Command::QueryFunction`] (`0x11`).
478    QueryFunction {
479        /// 24-bit function address.
480        addr: u32,
481        /// Current value.
482        value: u16,
483    },
484    /// Result 0 = accepted.
485    ///
486    /// Answer to [`Command::AssignController`] (`0x20 0x01`). A nonzero
487    /// result means the assignment was refused (for example, the current
488    /// controller did not agree to hand over).
489    AssignController {
490        /// 0 on success; nonzero error flags otherwise.
491        result: u8,
492    },
493    /// Answer to [`Command::QueryController`] (`0x20 0x03`).
494    QueryController {
495        /// Flags byte.
496        flags: u8,
497        /// The current controller; all-zero if none.
498        controller: NodeId,
499    },
500    /// Answer to [`Command::ControllerChangingNotify`] (`0x20 0x04 result`),
501    /// sent **by the current controller back to the train**.
502    ///
503    /// Despite the name, a throttle does not receive this; it *sends* it
504    /// after the train asked whether another node may take over. Result 0
505    /// accepts the handover (the throttle should then consider the train
506    /// lost); nonzero refuses it, and the train refuses the new controller's
507    /// Assign Controller.
508    ControllerChanged {
509        /// 0 = handover accepted; nonzero = refused.
510        result: u8,
511    },
512    /// Answer to [`Command::Reserve`] (`0x40 0x01`).
513    Reserve {
514        /// 0 on success.
515        result: u8,
516    },
517    /// The train wants a [`Command::Noop`] within `timeout_s` seconds.
518    ///
519    /// Encoded as `0x40 0x03 timeout`. If the controller stays silent past
520    /// the timeout, the train stops itself as a safety measure.
521    Heartbeat {
522        /// Seconds the controller has to answer.
523        timeout_s: u8,
524    },
525    /// Anything not recognised, raw bytes kept.
526    Unknown(Vec<u8>),
527}
528
529impl Reply {
530    /// Serialize to the reply payload bytes.
531    #[must_use]
532    pub fn encode(&self) -> Vec<u8> {
533        use cmd::*;
534        let mut v = Vec::with_capacity(9);
535        match self {
536            Reply::QuerySpeed {
537                set_speed,
538                estop,
539                commanded,
540                actual,
541            } => {
542                v.push(QUERY_SPEED);
543                v.extend_from_slice(&set_speed.0.to_be_bytes());
544                v.push(*estop as u8);
545                v.extend_from_slice(&commanded.0.to_be_bytes());
546                v.extend_from_slice(&actual.0.to_be_bytes());
547            }
548            Reply::QueryFunction { addr, value } => {
549                v.push(QUERY_FUNCTION);
550                v.extend_from_slice(&addr24(*addr));
551                v.extend_from_slice(&value.to_be_bytes());
552            }
553            Reply::AssignController { result } => {
554                v.extend_from_slice(&[CONTROLLER_CONFIG, CONTROLLER_ASSIGN, *result])
555            }
556            Reply::QueryController { flags, controller } => {
557                v.extend_from_slice(&[CONTROLLER_CONFIG, CONTROLLER_QUERY, *flags]);
558                v.extend_from_slice(&controller.to_bytes());
559            }
560            Reply::ControllerChanged { result } => {
561                v.extend_from_slice(&[CONTROLLER_CONFIG, CONTROLLER_CHANGED, *result])
562            }
563            Reply::Reserve { result } => v.extend_from_slice(&[MANAGEMENT, MGMT_RESERVE, *result]),
564            Reply::Heartbeat { timeout_s } => {
565                v.extend_from_slice(&[MANAGEMENT, MGMT_NOOP, *timeout_s])
566            }
567            Reply::Unknown(p) => v.extend_from_slice(p),
568        }
569        v
570    }
571
572    /// Parse reply payload bytes. Never fails; unknown or truncated replies
573    /// become [`Reply::Unknown`]. Missing optional fields of a Query Speed
574    /// reply decode as [`Speed::NAN`]; a missing result byte decodes as 0.
575    #[must_use]
576    pub fn decode(p: &[u8]) -> Reply {
577        use cmd::*;
578        let unknown = || Reply::Unknown(p.to_vec());
579        let Some(&first) = p.first() else {
580            return Reply::Unknown(Vec::new());
581        };
582        let sub = p.get(1).copied().unwrap_or(0);
583        let byte = |i: usize| p.get(i).copied().unwrap_or(0);
584        match first {
585            QUERY_SPEED => match read16(p, 1) {
586                Some(set) => Reply::QuerySpeed {
587                    set_speed: Speed(set),
588                    estop: byte(3) & 1 != 0,
589                    commanded: read16(p, 4).map_or(Speed::NAN, Speed),
590                    actual: read16(p, 6).map_or(Speed::NAN, Speed),
591                },
592                None => unknown(),
593            },
594            QUERY_FUNCTION => match (read24(p, 1), read16(p, 4)) {
595                (Some(addr), Some(value)) => Reply::QueryFunction { addr, value },
596                _ => unknown(),
597            },
598            CONTROLLER_CONFIG => match sub {
599                CONTROLLER_ASSIGN => Reply::AssignController { result: byte(2) },
600                CONTROLLER_QUERY => Reply::QueryController {
601                    flags: byte(2),
602                    controller: NodeId::from_bytes(p.get(3..).unwrap_or(&[])).unwrap_or_default(),
603                },
604                CONTROLLER_CHANGED => Reply::ControllerChanged { result: byte(2) },
605                _ => unknown(),
606            },
607            MANAGEMENT => match sub {
608                MGMT_RESERVE => Reply::Reserve { result: byte(2) },
609                MGMT_NOOP => Reply::Heartbeat { timeout_s: byte(2) },
610                _ => unknown(),
611            },
612            _ => unknown(),
613        }
614    }
615}
616
617/// Train Search protocol: find (or allocate) a train node by DCC address
618/// by emitting Identify Producer for a specially formed event ID. Matching
619/// train nodes answer with Producer Identified for the same event.
620///
621/// The *Train Search Standard* reserves the event range `09.00.99.FF.xx.xx.xx.xx`.
622/// The low 32 bits encode the query:
623///
624/// ```text
625///  63            32 31                         8 7        0
626/// +----------------+----------------------------+----------+
627/// |  0x090099FF    | 6 nibbles: query digits,   |  flags   |
628/// |  (PREFIX)      | right-aligned, unused = F  |          |
629/// +----------------+----------------------------+----------+
630///
631///  e.g. DCC 1234:  09.00.99.FF . FF.12.34 . 6C
632///
633///  flags: 0x80 ALLOCATE      create the train if none matches
634///         0x40 EXACT         exact match only
635///         0x20 ADDRESS_ONLY  match the address, not the name
636///         0x08 DCC           DCC protocol
637///         0x04 DCC_LONG_ADDRESS
638///         0x03 speed steps   0 default, 1 = 14, 2 = 28, 3 = 128
639/// ```
640///
641/// A throttle sends [`Message::IdentifyProducer`](crate::Message::IdentifyProducer)
642/// with the search event. Every train node that matches (the command
643/// station allocates a new virtual train node first if `ALLOCATE` is set)
644/// answers with Producer Identified for the same event ID; the source alias
645/// of that reply is the train to control.
646///
647/// ```
648/// use rolcb::traction::search;
649///
650/// let e = search::dcc_event(3, false, 0);
651/// assert_eq!(e.0, 0x0900_99FF_FFFF_F368); // digits "3", EXACT|ADDRESS_ONLY|DCC
652/// let e = search::dcc_event(1234, false, search::ALLOCATE);
653/// assert_eq!(e.0, 0x0900_99FF_FF12_34EC); // >127 forces the long-address flag
654/// assert!(search::is_search_event(e));
655/// assert_eq!(search::decode(e), Some((1234, 0xEC)));
656/// ```
657pub mod search {
658    use super::*;
659
660    /// Upper 32 bits of every train search event.
661    pub const PREFIX: u64 = 0x0900_99FF;
662
663    /// Create the train if no match exists (command station allocates it).
664    pub const ALLOCATE: u8 = 0x80;
665    /// Only exact matches.
666    pub const EXACT: u8 = 0x40;
667    /// Match only against the address, not the train name.
668    pub const ADDRESS_ONLY: u8 = 0x20;
669    /// DCC protocol.
670    pub const DCC: u8 = 0x08;
671    /// Force a DCC long (extended) address.
672    pub const DCC_LONG_ADDRESS: u8 = 0x04;
673    /// Speed step bits (with `DCC`): 0 = default, 1 = 14, 2 = 28, 3 = 128.
674    pub const SPEED_STEPS_MASK: u8 = 0x03;
675
676    /// Build the search event for a DCC address. Addresses above 127 are
677    /// always long; `long` forces a long address for low numbers.
678    ///
679    /// Sets [`EXACT`] | [`ADDRESS_ONLY`] | [`DCC`], plus
680    /// [`DCC_LONG_ADDRESS`] when needed, plus `extra_flags` (typically
681    /// [`ALLOCATE`] and/or a speed-step value).
682    #[must_use]
683    pub fn dcc_event(address: u16, long: bool, extra_flags: u8) -> EventId {
684        let mut flags = EXACT | ADDRESS_ONLY | DCC | extra_flags;
685        if long || address > 127 {
686            flags |= DCC_LONG_ADDRESS;
687        }
688        query_event(address as u32, flags)
689    }
690
691    /// Build a search event from a numeric query (up to 6 decimal digits).
692    ///
693    /// Each decimal digit occupies one nibble, right-aligned; unused leading
694    /// nibbles are `0xF`. Numbers above 999 999 are clamped. `flags` is used
695    /// as-is.
696    ///
697    /// ```
698    /// use rolcb::traction::search;
699    ///
700    /// let e = search::query_event(42, 0);
701    /// assert_eq!(e.0, 0x0900_99FF_FFFF_4200);
702    /// ```
703    #[must_use]
704    pub fn query_event(number: u32, flags: u8) -> EventId {
705        let mut nibbles: u32 = 0xFF_FFFF;
706        let mut n = number.min(999_999);
707        let mut shift = 0;
708        loop {
709            nibbles &= !(0xF << shift);
710            nibbles |= (n % 10) << shift;
711            n /= 10;
712            shift += 4;
713            if n == 0 || shift >= 24 {
714                break;
715            }
716        }
717        EventId((PREFIX << 32) | ((nibbles as u64) << 8) | flags as u64)
718    }
719
720    /// True if `e` lies in the train search event range ([`PREFIX`]).
721    #[must_use]
722    pub fn is_search_event(e: EventId) -> bool {
723        e.0 >> 32 == PREFIX
724    }
725
726    /// Decode a search event into (number, flags). Non-digit nibbles are skipped.
727    ///
728    /// Returns `None` if `e` is not a search event.
729    #[must_use]
730    pub fn decode(e: EventId) -> Option<(u32, u8)> {
731        if !is_search_event(e) {
732            return None;
733        }
734        let nibbles = ((e.0 >> 8) & 0xFF_FFFF) as u32;
735        let mut n = 0u32;
736        for i in (0..6).rev() {
737            let d = (nibbles >> (i * 4)) & 0xF;
738            if d <= 9 {
739                n = n * 10 + d;
740            }
741        }
742        Some((n, e.0 as u8))
743    }
744}
745
746/// Function number helpers: F0..F28 map directly to function addresses 0..28.
747///
748/// Returns the [`Command::SetFunction`] value for an on/off function:
749/// 1 for on, 0 for off.
750///
751/// ```
752/// use rolcb::traction::{function_value, Command};
753///
754/// let headlight_on = Command::SetFunction { addr: 0, value: function_value(true) };
755/// assert_eq!(headlight_on.encode(), [0x01, 0, 0, 0, 0, 1]);
756/// ```
757#[must_use]
758pub fn function_value(on: bool) -> u16 {
759    on as u16
760}
761
762#[cfg(test)]
763mod tests {
764    use super::*;
765    use alloc::vec;
766
767    #[test]
768    fn set_speed_bytes() {
769        // 1.0 m/s forward = 0x3C00, reverse = 0xBC00.
770        assert_eq!(
771            Command::SetSpeed(Speed::from_mps(1.0, false)).encode(),
772            vec![0x00, 0x3C, 0x00]
773        );
774        assert_eq!(
775            Command::SetSpeed(Speed::from_mps(1.0, true)).encode(),
776            vec![0x00, 0xBC, 0x00]
777        );
778        assert_eq!(
779            Command::SetSpeed(Speed::STOP_REVERSE).encode(),
780            vec![0x00, 0x80, 0x00]
781        );
782        let s = Speed::from_mph(126.0, true);
783        assert!(s.is_reverse());
784        assert!((s.mph() - 126.0).abs() < 0.1);
785    }
786
787    #[test]
788    fn command_round_trips() {
789        let me = NodeId::new(0x0501_0101_2299);
790        for c in [
791            Command::SetSpeed(Speed::from_mph(40.0, false)),
792            Command::SetFunction { addr: 28, value: 1 },
793            Command::EmergencyStop,
794            Command::QuerySpeed,
795            Command::QueryFunction { addr: 5 },
796            Command::AssignController {
797                flags: 0,
798                controller: me,
799            },
800            Command::ReleaseController {
801                flags: 0,
802                controller: me,
803            },
804            Command::QueryController,
805            Command::ControllerChangingNotify {
806                flags: 0,
807                new_controller: me,
808            },
809            Command::Reserve,
810            Command::Release,
811            Command::Noop,
812        ] {
813            assert_eq!(Command::decode(&c.encode()), c);
814        }
815        assert_eq!(
816            Command::AssignController {
817                flags: 0,
818                controller: me
819            }
820            .encode(),
821            vec![0x20, 0x01, 0x00, 0x05, 0x01, 0x01, 0x01, 0x22, 0x99]
822        );
823        assert_eq!(
824            Command::SetFunction { addr: 3, value: 1 }.encode(),
825            vec![0x01, 0, 0, 3, 0, 1]
826        );
827    }
828
829    #[test]
830    fn reply_round_trips() {
831        for r in [
832            Reply::QuerySpeed {
833                set_speed: Speed::from_mph(10.0, true),
834                estop: false,
835                commanded: Speed::NAN,
836                actual: Speed::NAN,
837            },
838            Reply::QueryFunction { addr: 0, value: 1 },
839            Reply::AssignController { result: 0 },
840            Reply::QueryController {
841                flags: 0,
842                controller: NodeId::new(0x0501_0101_2201),
843            },
844            Reply::ControllerChanged { result: 0 },
845            Reply::Reserve { result: 0 },
846            Reply::Heartbeat { timeout_s: 10 },
847        ] {
848            assert_eq!(Reply::decode(&r.encode()), r);
849        }
850    }
851
852    #[test]
853    fn search_events() {
854        let e = search::dcc_event(3, false, 0);
855        assert_eq!(e.0, 0x0900_99FF_FFFF_F368);
856        let e = search::dcc_event(1234, false, 0);
857        assert_eq!(e.0, 0x0900_99FF_FF12_346C);
858        assert_eq!(search::decode(e), Some((1234, 0x6C)));
859        let e = search::dcc_event(3, true, search::ALLOCATE);
860        assert_eq!(e.0 & 0xFF, 0xEC);
861        assert!(search::is_search_event(e));
862        assert!(!search::is_search_event(EventId::IS_TRAIN));
863    }
864
865    #[test]
866    fn controller_changing_notify_round_trip() {
867        let new = NodeId::new(0x0501_0101_2277);
868        let c = Command::ControllerChangingNotify {
869            flags: 0,
870            new_controller: new,
871        };
872        let bytes = c.encode();
873        assert_eq!(
874            bytes,
875            vec![0x20, 0x04, 0x00, 0x05, 0x01, 0x01, 0x01, 0x22, 0x77]
876        );
877        assert!(Command::is_controller_changing_notify(&bytes));
878        assert_eq!(Command::decode(&bytes), c);
879        // The controller's answer is a reply with the same sub-command.
880        let r = Reply::ControllerChanged { result: 0 };
881        assert_eq!(r.encode(), vec![0x20, 0x04, 0x00]);
882        assert_eq!(Reply::decode(&r.encode()), r);
883        assert_eq!(
884            Reply::decode(&[0x20, 0x04, 0x01]),
885            Reply::ControllerChanged { result: 1 }
886        );
887        // Through the message layer: 9 bytes, two frames.
888        let m = crate::Message::TractionCommand {
889            dest: 0x285,
890            payload: bytes,
891        };
892        let mut asm = crate::message::Assembler::new();
893        let raw = m
894            .to_frames(0x6A1)
895            .iter()
896            .find_map(|f| asm.accept(f))
897            .unwrap();
898        assert_eq!(crate::Message::from(raw), m);
899    }
900
901    #[test]
902    fn truncated_payloads_are_unknown() {
903        for p in [
904            &[0x00][..],
905            &[0x00, 0x3C],
906            &[0x01, 0, 0, 3, 0],
907            &[0x11, 0, 0],
908            &[0x20, 0x01, 0x00, 5, 1, 1, 1, 0x22],
909            &[0x20, 0x02],
910            &[0x20, 0x04, 0x00],
911            &[0x20, 0x09],
912            &[0x40, 0x07],
913            &[0x30, 0x01],
914        ] {
915            assert_eq!(Command::decode(p), Command::Unknown(p.to_vec()), "{p:02X?}");
916            // Unknown commands re-encode to the same bytes.
917            assert_eq!(Command::decode(p).encode(), p);
918        }
919        assert_eq!(Command::decode(&[]), Command::Unknown(vec![]));
920        for p in [
921            &[0x10, 0x3C][..],
922            &[0x11, 0, 0, 1, 0],
923            &[0x20, 0x09],
924            &[0x77],
925        ] {
926            assert_eq!(Reply::decode(p), Reply::Unknown(p.to_vec()), "{p:02X?}");
927        }
928        assert_eq!(Reply::decode(&[]), Reply::Unknown(vec![]));
929        // Optional Query Speed fields default to NaN; result bytes to 0.
930        assert_eq!(
931            Reply::decode(&[0x10, 0x3C, 0x00]),
932            Reply::QuerySpeed {
933                set_speed: Speed(0x3C00),
934                estop: false,
935                commanded: Speed::NAN,
936                actual: Speed::NAN
937            }
938        );
939        assert_eq!(
940            Reply::decode(&[0x20, 0x01]),
941            Reply::AssignController { result: 0 }
942        );
943        assert_eq!(
944            Reply::decode(&[0x20, 0x03, 0x00, 1, 2]),
945            Reply::QueryController {
946                flags: 0,
947                controller: NodeId::default()
948            }
949        );
950    }
951
952    #[test]
953    fn listener_flag_is_ignored() {
954        assert_eq!(
955            Command::decode(&[0x80, 0x3C, 0x00]),
956            Command::SetSpeed(Speed(0x3C00))
957        );
958        assert_eq!(
959            Command::decode(&[0x81, 0, 0, 2, 0, 1]),
960            Command::SetFunction { addr: 2, value: 1 }
961        );
962        assert_eq!(Command::decode(&[0x82]), Command::EmergencyStop);
963        assert_eq!(Command::decode(&[0xC0, 0x03]), Command::Noop);
964    }
965
966    #[test]
967    fn query_event_clamps_and_decodes() {
968        assert_eq!(search::query_event(0, 0).0, 0x0900_99FF_FFFF_F000);
969        assert_eq!(search::query_event(999_999, 0).0, 0x0900_99FF_9999_9900);
970        assert_eq!(
971            search::query_event(1_000_000, 0x12),
972            search::query_event(999_999, 0x12)
973        );
974        assert_eq!(
975            search::query_event(u32::MAX, 0),
976            search::query_event(999_999, 0)
977        );
978        assert_eq!(
979            search::decode(search::query_event(4_294, 0x08)),
980            Some((4_294, 0x08))
981        );
982        assert_eq!(search::decode(EventId::IS_TRAIN), None);
983        assert_eq!(
984            search::dcc_event(9_999, false, 0).0 & 0xFF,
985            (search::EXACT | search::ADDRESS_ONLY | search::DCC | search::DCC_LONG_ADDRESS) as u64
986        );
987    }
988
989    #[test]
990    fn speed_nan_and_hash() {
991        assert!(Speed::NAN.is_nan());
992        assert!(Speed::NAN.mps().is_nan());
993        assert!(Speed::NAN.mph().is_nan());
994        assert!(!Speed::STOP_REVERSE.is_nan());
995        assert_eq!(Speed::from_mps(-3.0, false), Speed::from_mps(3.0, false));
996        let mut set = alloc::collections::BTreeSet::new();
997        set.insert(alloc::format!("{:?}", Command::Noop));
998        assert_eq!(set.len(), 1);
999        fn hashable<T: core::hash::Hash>(_: &T) {}
1000        hashable(&Command::Noop);
1001        hashable(&Reply::Heartbeat { timeout_s: 1 });
1002    }
1003}