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}