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}