roco_z21_driver/station/loco.rs
1//! Module for controlling DCC locomotives via the Z21 station.
2//!
3//! This module provides a high-level API for controlling model train locomotives
4//! using the Digital Command Control (DCC) protocol via a Z21 station. It supports
5//! operations such as controlling locomotive speed, direction, functions (lights,
6//! sounds, etc.), and emergency stops.
7//!
8//! # Features
9//!
10//! - Control locomotive speed and direction
11//! - Normal and emergency stops
12//! - Function control (F0-F31) including lights, sounds, and other locomotive features
13//! - Support for different DCC throttle steps (14, 28, 128)
14//! - State monitoring and subscription
15//!
16//! # Examples
17//!
18//! ```rust
19//! # use tokio;
20//! # use std::sync::Arc;
21//! # async fn example() -> std::io::Result<()> {
22//! let station = Arc::new(Z21Station::new("192.168.0.111:21105").await?);
23//!
24//! // Control a locomotive with address 3
25//! let loco = Loco::control(station.clone(), 3).await?;
26//!
27//! // Set speed to 50% forward
28//! loco.drive(50.0).await?;
29//!
30//! // Turn on the headlights (F0)
31//! loco.set_headlights(true).await?;
32//!
33//! // Activate the horn (assuming it's on F2)
34//! loco.function_on(2).await?;
35//!
36//! // Emergency stop
37//! loco.halt().await?;
38//! # Ok(())
39//! # }
40//! ```
41
42use std::time::Duration;
43use std::{ops::Deref, sync::Arc, vec};
44
45use tokio::{io, time};
46
47use crate::messages::{DccThrottleSteps, LocoState};
48use crate::{messages::XBusMessage, Z21Station};
49
50const XBUS_LOCO_GET_INFO: u8 = 0xE3;
51const XBUS_LOCO_DRIVE: u8 = 0xE4;
52const XBUS_LOCO_INFO: u8 = 0xEF;
53const XBUS_LOCO_FUNCTION: u8 = 0xE4;
54const FUNC_OFF: u8 = 0x00;
55const FUNC_ON: u8 = 0x01;
56const FUNC_TOGGLE: u8 = 0x02;
57
58impl Default for DccThrottleSteps {
59 fn default() -> Self {
60 Self::Steps128
61 }
62}
63
64/// Represents a DCC Locomotive that can be controlled via a Z21 station.
65///
66/// This struct provides methods to control various aspects of a model train locomotive,
67/// including speed, direction, functions (lights, sounds, etc.), and emergency stops.
68/// It communicates with the locomotive through a Z21 station using the XBus protocol.
69pub struct Loco {
70 /// Reference to the Z21 station connection
71 station: Arc<Z21Station>,
72 /// DCC address of the locomotive
73 addr: u16,
74 /// DCC throttle steps configuration (14, 28, or 128 steps)
75 steps: DccThrottleSteps,
76}
77
78impl Loco {
79 /// Initializes control over a locomotive with the specified address.
80 ///
81 /// This method establishes communication with a locomotive using its DCC address
82 /// and subscribes to information about its state. It uses the default throttle
83 /// steps configuration (128 steps).
84 ///
85 /// # Arguments
86 ///
87 /// * `station` - Arc reference to a connected Z21Station
88 /// * `address` - DCC address of the locomotive (1-9999)
89 ///
90 /// # Returns
91 ///
92 /// A new `Loco` instance if successful.
93 ///
94 /// # Errors
95 ///
96 /// Returns an `io::Error` if:
97 /// - Communication with the Z21 station fails
98 /// - The locomotive does not respond
99 ///
100 /// # Example
101 ///
102 /// ```rust
103 /// # async fn example(station: Arc<Z21Station>) -> std::io::Result<()> {
104 /// let loco = Loco::control(station.clone(), 3).await?;
105 /// # Ok(())
106 /// # }
107 /// ```
108 pub async fn control(station: Arc<Z21Station>, address: u16) -> io::Result<Loco> {
109 Self::control_with_steps(station, address, DccThrottleSteps::default()).await
110 }
111
112 /// Initializes control over a locomotive with specified address and DCC stepping.
113 ///
114 /// Similar to `control()` but allows specifying the throttle stepping mode
115 /// (14, 28, or 128 steps) for more precise control or compatibility with
116 /// different locomotive decoders.
117 ///
118 /// # Arguments
119 ///
120 /// * `station` - Arc reference to a connected Z21Station
121 /// * `address` - DCC address of the locomotive (1-9999)
122 /// * `steps` - DCC throttle steps configuration
123 ///
124 /// # Returns
125 ///
126 /// A new `Loco` instance if successful.
127 ///
128 /// # Errors
129 ///
130 /// Returns an `io::Error` if:
131 /// - Communication with the Z21 station fails
132 /// - The locomotive does not respond
133 ///
134 /// # Example
135 ///
136 /// ```rust
137 /// # async fn example(station: Arc<Z21Station>) -> std::io::Result<()> {
138 /// let loco = Loco::control_with_steps(
139 /// station.clone(),
140 /// 3,
141 /// DccThrottleSteps::Steps28
142 /// ).await?;
143 /// # Ok(())
144 /// # }
145 /// ```
146 pub async fn control_with_steps(
147 station: Arc<Z21Station>,
148 address: u16,
149 steps: DccThrottleSteps,
150 ) -> io::Result<Loco> {
151 let loco = Loco {
152 station: station.clone(),
153 steps,
154 addr: address,
155 };
156
157 Self::poll_state_info(address, &loco.station).await?;
158 Ok(loco)
159 }
160
161 /// Sends a drive command to the locomotive.
162 ///
163 /// Internal helper method used by `drive()`, `stop()`, and `halt()` methods.
164 ///
165 /// # Arguments
166 ///
167 /// * `drive_byte` - Control byte for the drive command
168 ///
169 /// # Errors
170 ///
171 /// Returns an `io::Error` if the packet fails to send, or Z21 does not respond.
172 async fn send_drive(&self, drive_byte: u8) -> io::Result<()> {
173 let addr_bytes = self.addr.to_be_bytes();
174 let dbs = vec![self.steps as u8, addr_bytes[0], addr_bytes[1], drive_byte];
175 let drive_msg = XBusMessage::new_dbs_vec(XBUS_LOCO_DRIVE, dbs);
176 self.station
177 .send_xbus_command(drive_msg, Some(XBUS_LOCO_INFO))
178 .await?;
179 Ok(())
180 }
181
182 /// Performs a normal locomotive stop, equivalent to setting speed to 0.
183 ///
184 /// This stop applies braking with a braking curve, providing a gradual
185 /// and realistic deceleration.
186 ///
187 /// # Errors
188 ///
189 /// Returns an `io::Error` if the packet fails to send, or Z21 does not respond.
190 ///
191 /// # Example
192 ///
193 /// ```rust
194 /// # async fn example(loco: &Loco) -> std::io::Result<()> {
195 /// // Gradually stop the locomotive
196 /// loco.stop().await?;
197 /// # Ok(())
198 /// # }
199 /// ```
200 pub async fn stop(&self) -> io::Result<()> {
201 self.send_drive(0x0).await
202 }
203
204 /// Stops the train immediately (emergency stop).
205 ///
206 /// Unlike the normal `stop()` method, this immediately cuts power
207 /// to the locomotive, causing an abrupt stop. This should be used
208 /// only in emergency situations.
209 ///
210 /// # Errors
211 ///
212 /// Returns an `io::Error` if the packet fails to send, or Z21 does not respond.
213 ///
214 /// # Example
215 ///
216 /// ```rust
217 /// # async fn example(loco: &Loco) -> std::io::Result<()> {
218 /// // Emergency stop the locomotive
219 /// loco.halt().await?;
220 /// # Ok(())
221 /// # }
222 /// ```
223 pub async fn halt(&self) -> io::Result<()> {
224 self.send_drive(0x1).await
225 }
226
227 /// Calculates the speed byte for a locomotive based on throttle steps and speed percentage.
228 ///
229 /// This function maps a percentage speed value (-100% to 100%) to the appropriate
230 /// DCC speed step value based on the configured throttle steps. Negative values
231 /// indicate reverse direction, positive values indicate forward direction.
232 ///
233 /// # Arguments
234 ///
235 /// * `steps` - DCC throttle steps configuration (14, 28, or 128 steps)
236 /// * `speed_percent` - Speed percentage (-100.0 to 100.0)
237 ///
238 /// # Returns
239 ///
240 /// A formatted drive byte for the DCC command
241 fn calc_speed(steps: DccThrottleSteps, speed_percent: f64) -> u8 {
242 let speed = speed_percent / 100.;
243 let mapped_speed = match steps {
244 DccThrottleSteps::Steps128 => speed * 128.,
245 DccThrottleSteps::Steps28 => speed * 28.,
246 DccThrottleSteps::Steps14 => speed * 14.,
247 };
248 //let mapped_speed = (mapped_speed * 100.).round() / 100.;
249 let flag = mapped_speed > 0.;
250
251 (mapped_speed.abs() as u8) | (0x80 * flag as u8)
252 }
253
254 /// Polls the current state information of a locomotive.
255 ///
256 /// This method sends a request to the Z21 station to get the current state
257 /// of a locomotive with the specified address.
258 ///
259 /// # Arguments
260 ///
261 /// * `addr` - DCC address of the locomotive
262 /// * `station` - Reference to the Z21 station
263 ///
264 /// # Returns
265 ///
266 /// The current state of the locomotive if successful.
267 ///
268 /// # Errors
269 ///
270 /// Returns an `io::Error` if the request fails or the response is invalid.
271 async fn poll_state_info(addr: u16, station: &Arc<Z21Station>) -> io::Result<LocoState> {
272 let addr_bytes = addr.to_be_bytes();
273 let init_xbus =
274 XBusMessage::new_dbs_vec(XBUS_LOCO_GET_INFO, vec![0xf0, addr_bytes[0], addr_bytes[1]]);
275 let info = station
276 .send_xbus_command(init_xbus, Some(XBUS_LOCO_INFO))
277 .await?;
278
279 Ok(LocoState::try_from(&info)?)
280 }
281
282 /// Sets the speed of the locomotive in percent.
283 ///
284 /// This method controls both the speed and direction of the locomotive:
285 /// - Positive values move the locomotive forward
286 /// - Negative values move the locomotive backward
287 /// - Zero value gradually stops the locomotive using a braking curve
288 ///
289 /// The speed is automatically scaled based on the configured DCC throttle steps.
290 ///
291 /// # Arguments
292 ///
293 /// * `speed_percent` - Speed percentage (-100.0 to 100.0)
294 ///
295 /// # Errors
296 ///
297 /// Returns an `io::Error` if the packet fails to send, or Z21 does not respond.
298 ///
299 /// # Example
300 ///
301 /// ```rust
302 /// # async fn example(loco: &Loco) -> std::io::Result<()> {
303 /// // Drive forward at 50% speed
304 /// loco.drive(50.0).await?;
305 ///
306 /// // Drive backward at 25% speed
307 /// loco.drive(-25.0).await?;
308 ///
309 /// // Stop gradually
310 /// loco.drive(0.0).await?;
311 /// # Ok(())
312 /// # }
313 /// ```
314 pub async fn drive(&self, speed_percent: f64) -> io::Result<()> {
315 let calced = Self::calc_speed(self.steps, speed_percent);
316 self.send_drive(calced).await?;
317 Ok(())
318 }
319
320 /// Subscribes to locomotive state changes.
321 ///
322 /// This method sets up a background task that listens for locomotive state
323 /// events from the Z21 station and calls the provided callback function
324 /// whenever the state changes.
325 ///
326 /// # Arguments
327 ///
328 /// * `subscriber` - Callback function that receives locomotive state updates
329 ///
330 /// # Example
331 ///
332 /// ```rust
333 /// # fn example(loco: &Loco) {
334 /// loco.subscribe_loco_state(Box::new(|state| {
335 /// println!("Locomotive speed: {}, direction: {}",
336 /// state.speed,
337 /// if state.direction { "forward" } else { "backward" });
338 /// }));
339 /// # }
340 /// ```
341 pub fn subscribe_loco_state(&self, subscriber: Box<dyn Fn(LocoState) + Send + Sync>) {
342 let station = Arc::clone(&self.station);
343 tokio::spawn(async move {
344 loop {
345 let msg = station.receive_xbus_packet(XBUS_LOCO_INFO).await;
346 if let Ok(msg) = msg {
347 if let Ok(loco_state) = LocoState::try_from(&msg) {
348 subscriber(loco_state);
349 }
350 }
351 }
352 });
353 }
354
355 /// Controls a locomotive function (F0-F31).
356 ///
357 /// This method allows controlling the various functions of a DCC locomotive,
358 /// such as lights, sounds, couplers, smoke generators, and other features.
359 /// The specific functions available depend on the locomotive decoder.
360 ///
361 /// # Arguments
362 ///
363 /// * `function_index` - The function number (0-31) where 0 represents F0 (typically lights)
364 /// * `action` - The action to perform:
365 /// - 0: Turn function OFF
366 /// - 1: Turn function ON
367 /// - 2: Toggle function state
368 ///
369 /// # Errors
370 ///
371 /// Returns an `io::Error` if:
372 /// - The function index is invalid (must be 0-31)
373 /// - The action is invalid (must be 0-2)
374 /// - The packet fails to send
375 /// - The Z21 station does not respond
376 ///
377 /// # Example
378 ///
379 /// ```rust
380 /// # async fn example(loco: &Loco) -> std::io::Result<()> {
381 /// // Turn on the locomotive lights (F0)
382 /// loco.set_function(0, 1).await?;
383 ///
384 /// // Toggle the horn (assuming it's on F2)
385 /// loco.set_function(2, 2).await?;
386 /// # Ok(())
387 /// # }
388 /// ```
389 pub async fn set_function(&self, function_index: u8, action: u8) -> io::Result<()> {
390 if function_index > 31 {
391 return Err(io::Error::new(
392 io::ErrorKind::InvalidInput,
393 "Function index must be between 0 and 31",
394 ));
395 }
396
397 if action > 2 {
398 return Err(io::Error::new(
399 io::ErrorKind::InvalidInput,
400 "Action must be 0 (off), 1 (on), or 2 (toggle)",
401 ));
402 }
403
404 let addr_bytes = self.addr.to_be_bytes();
405 let addr_msb = if self.addr >= 128 {
406 0xC0 | addr_bytes[0]
407 } else {
408 addr_bytes[0]
409 };
410
411 // Create the function byte (TTNNNNNN): TT is action type, NNNNNN is function index
412 let function_byte = (action << 6) | (function_index & 0x3F);
413
414 let dbs = vec![0xF8, addr_msb, addr_bytes[1], function_byte];
415 let function_msg = XBusMessage::new_dbs_vec(XBUS_LOCO_FUNCTION, dbs);
416
417 self.station
418 .send_xbus_command(function_msg, Some(XBUS_LOCO_INFO))
419 .await?;
420
421 Ok(())
422 }
423
424 /// Turns on a specific locomotive function.
425 ///
426 /// This is a convenience method that calls `set_function()` with the ON action.
427 ///
428 /// # Arguments
429 ///
430 /// * `function_index` - The function number (0-31) where 0 represents F0 (typically lights)
431 ///
432 /// # Errors
433 ///
434 /// Returns an `io::Error` if the packet fails to send, or Z21 does not respond.
435 ///
436 /// # Example
437 ///
438 /// ```rust
439 /// # async fn example(loco: &Loco) -> std::io::Result<()> {
440 /// // Turn on the locomotive lights (F0)
441 /// loco.function_on(0).await?;
442 ///
443 /// // Activate the horn (assuming it's on F2)
444 /// loco.function_on(2).await?;
445 /// # Ok(())
446 /// # }
447 /// ```
448 pub async fn function_on(&self, function_index: u8) -> io::Result<()> {
449 self.set_function(function_index, FUNC_ON).await
450 }
451
452 /// Turns off a specific locomotive function.
453 ///
454 /// This is a convenience method that calls `set_function()` with the OFF action.
455 ///
456 /// # Arguments
457 ///
458 /// * `function_index` - The function number (0-31) where 0 represents F0 (typically lights)
459 ///
460 /// # Errors
461 ///
462 /// Returns an `io::Error` if the packet fails to send, or Z21 does not respond.
463 ///
464 /// # Example
465 ///
466 /// ```rust
467 /// # async fn example(loco: &Loco) -> std::io::Result<()> {
468 /// // Turn off the locomotive lights (F0)
469 /// loco.function_off(0).await?;
470 ///
471 /// // Deactivate the horn (assuming it's on F2)
472 /// loco.function_off(2).await?;
473 /// # Ok(())
474 /// # }
475 /// ```
476 pub async fn function_off(&self, function_index: u8) -> io::Result<()> {
477 self.set_function(function_index, FUNC_OFF).await
478 }
479
480 /// Toggles a specific locomotive function (if on, turns off; if off, turns on).
481 ///
482 /// This is a convenience method that calls `set_function()` with the TOGGLE action.
483 ///
484 /// # Arguments
485 ///
486 /// * `function_index` - The function number (0-31) where 0 represents F0 (typically lights)
487 ///
488 /// # Errors
489 ///
490 /// Returns an `io::Error` if the packet fails to send, or Z21 does not respond.
491 ///
492 /// # Example
493 ///
494 /// ```rust
495 /// # async fn example(loco: &Loco) -> std::io::Result<()> {
496 /// // Toggle the locomotive lights (F0)
497 /// loco.function_toggle(0).await?;
498 ///
499 /// // Toggle the horn (assuming it's on F2)
500 /// loco.function_toggle(2).await?;
501 /// # Ok(())
502 /// # }
503 /// ```
504 pub async fn function_toggle(&self, function_index: u8) -> io::Result<()> {
505 self.set_function(function_index, FUNC_TOGGLE).await
506 }
507
508 /// Convenience method to control the locomotive's headlights (F0).
509 ///
510 /// This method simplifies controlling the locomotive's headlights,
511 /// which are typically mapped to function F0 in DCC decoders.
512 ///
513 /// # Arguments
514 ///
515 /// * `on` - Whether to turn the lights on (true) or off (false)
516 ///
517 /// # Errors
518 ///
519 /// Returns an `io::Error` if the packet fails to send, or Z21 does not respond.
520 ///
521 /// # Example
522 ///
523 /// ```rust
524 /// # async fn example(loco: &Loco) -> std::io::Result<()> {
525 /// // Turn on the locomotive headlights
526 /// loco.set_headlights(true).await?;
527 ///
528 /// // Turn off the locomotive headlights
529 /// loco.set_headlights(false).await?;
530 /// # Ok(())
531 /// # }
532 /// ```
533 pub async fn set_headlights(&self, on: bool) -> io::Result<()> {
534 if on {
535 self.function_on(0).await
536 } else {
537 self.function_off(0).await
538 }
539 }
540}