dalybms_lib/
tokio_serial_async.rs

1//! Provides an asynchronous client for interacting with a Daly BMS (Battery Management System)
2//! using Tokio and the `tokio-serial` crate for serial communication.
3//!
4//! This module is suitable for applications built on the Tokio runtime.
5//!
6//! # Example
7//!
8//! ```no_run
9//! use dalybms_lib::tokio_serial_async::{DalyBMS, Error};
10//! use std::time::Duration;
11//!
12//! #[tokio::main]
13//! async fn main() -> Result<(), Error> {
14//!     let mut bms = DalyBMS::new("/dev/ttyUSB0")?;
15//!     bms.set_timeout(Duration::from_millis(500))?;
16//!
17//!     let soc = bms.get_soc().await?;
18//!     println!("SOC: {:?}", soc);
19//!
20//!     // It's recommended to call get_status() first to populate cell/sensor counts
21//!     // for other methods like get_cell_voltages() or get_cell_temperatures().
22//!     let status = bms.get_status().await?;
23//!     println!("Status: {:?}", status);
24//!
25//!     let cell_voltages = bms.get_cell_voltages().await?;
26//!     println!("Cell Voltages: {:?}", cell_voltages);
27//!
28//!     Ok(())
29//! }
30//! ```
31
32use crate::protocol::*;
33use std::time::{Duration, Instant};
34use tokio::io::{AsyncReadExt, AsyncWriteExt};
35use tokio_serial::{SerialPort, SerialPortBuilderExt};
36
37/// Errors specific to the asynchronous Tokio serial port client.
38#[derive(Debug, thiserror::Error)]
39pub enum Error {
40    /// Error indicating that `get_status()` must be called before certain other methods
41    /// that rely on information like cell count or temperature sensor count.
42    #[error("get_status() has to be called at least once before")]
43    StatusError,
44    /// An error originating from the underlying Daly BMS protocol library.
45    #[error("Daly error: {0}")]
46    DalyError(#[from] crate::Error),
47    /// An I/O error, typically from the serial port communication.
48    #[error("IO error: {0}")]
49    IOError(#[from] std::io::Error),
50    /// An error from the `tokio-serial` crate.
51    #[error("Tokio serial error: {0}")]
52    TokioSerial(#[from] tokio_serial::Error),
53    /// An error indicating that a Tokio timeout elapsed during an I/O operation.
54    #[error("Tokio timeout elapsed: {0}")]
55    TokioElapsed(#[from] tokio::time::error::Elapsed),
56}
57
58/// A specialized `Result` type for operations within the `tokio_serial_async` module.
59type Result<T> = std::result::Result<T, Error>;
60
61/// The main struct for interacting asynchronously with a Daly BMS using Tokio.
62///
63/// It handles sending commands and receiving/decoding responses from the BMS
64/// in an asynchronous manner, suitable for Tokio-based applications.
65/// Most methods are `async` and require a mutable reference to `self`.
66#[derive(Debug)]
67pub struct DalyBMS {
68    serial: tokio_serial::SerialStream,
69    last_execution: Instant,
70    io_timeout: Duration,   // Timeout for individual I/O operations
71    delay: Duration,        // Delay between commands
72    status: Option<Status>, // Stores the latest status
73    retries: u8,
74}
75
76macro_rules! request_with_retry {
77    ($self:ident, $X:ident, $request_bytes:expr, $reply_size:expr) => {{
78        'retry_block: {
79            for t in 0..$self.retries {
80                match $self.send_and_receive($request_bytes, $reply_size).await {
81                    Ok(reply_bytes) => match $X::decode(&reply_bytes) {
82                        Ok(result) => break 'retry_block Ok(result),
83                        Err(err) => {
84                            log::trace!(
85                                "Failed try {} of {}, repeating ({err})",
86                                t + 1,
87                                $self.retries
88                            );
89                        }
90                    },
91                    Err(err) => {
92                        log::trace!(
93                            "Failed try {} of {}, repeating ({err})",
94                            t + 1,
95                            $self.retries
96                        );
97                    }
98                }
99            }
100            Ok($X::decode(
101                &$self.send_and_receive($request_bytes, $reply_size).await?,
102            )?)
103        }
104    }};
105
106    ($self:ident, $X:ident, $request_bytes:expr, $reply_size:expr, $decode_arg:expr) => {{
107        'retry_block: {
108            for t in 0..$self.retries {
109                match $self.send_and_receive($request_bytes, $reply_size).await {
110                    Ok(reply_bytes) => match $X::decode(&reply_bytes, $decode_arg) {
111                        Ok(result) => break 'retry_block Ok(result),
112                        Err(err) => {
113                            log::trace!(
114                                "Failed try {} of {}, repeating ({err})",
115                                t + 1,
116                                $self.retries
117                            );
118                        }
119                    },
120                    Err(err) => {
121                        log::trace!(
122                            "Failed try {} of {}, repeating ({err})",
123                            t + 1,
124                            $self.retries
125                        );
126                    }
127                }
128            }
129            Ok($X::decode(
130                &$self.send_and_receive($request_bytes, $reply_size).await?,
131                $decode_arg,
132            )?)
133        }
134    }};
135}
136
137impl DalyBMS {
138    /// Creates a new `DalyBMS` instance for asynchronous communication.
139    ///
140    /// # Arguments
141    ///
142    /// * `port`: The path to the serial port device (e.g., `/dev/ttyUSB0` on Linux, `COM3` on Windows).
143    ///
144    /// # Returns
145    ///
146    /// A `Result` containing the `DalyBMS` instance or an `Error` if the serial port
147    /// cannot be opened or configured for asynchronous operation.
148    ///
149    /// # Example
150    ///
151    /// ```no_run
152    /// use dalybms_lib::tokio_serial_async::DalyBMS;
153    ///
154    /// #[tokio::main]
155    /// async fn main() {
156    ///     let bms_result = DalyBMS::new("/dev/ttyUSB0");
157    ///     if let Ok(mut bms_instance) = bms_result {
158    ///         // Use the BMS instance
159    ///         if let Ok(soc) = bms_instance.get_soc().await {
160    ///             println!("SOC: {}%", soc.soc_percent);
161    ///         }
162    ///     } else {
163    ///         eprintln!("Failed to connect to BMS: {:?}", bms_result.err());
164    ///     }
165    /// }
166    /// ```
167    pub fn new(port: &str) -> Result<Self> {
168        Ok(Self {
169            serial: tokio_serial::new(port, 9600)
170                .data_bits(tokio_serial::DataBits::Eight)
171                .parity(tokio_serial::Parity::None)
172                .stop_bits(tokio_serial::StopBits::One)
173                .flow_control(tokio_serial::FlowControl::None)
174                .open_native_async()?,
175            last_execution: Instant::now(),
176            delay: MINIMUM_DELAY, // Default delay from protocol module
177            io_timeout: Duration::from_secs(5), // Default I/O timeout
178            status: None,
179            retries: 3,
180        })
181    }
182
183    /// sets the number of retries for a failed send_bytes operation
184    pub fn set_retry(&mut self, n_retries: u8) {
185        self.retries = n_retries;
186    }
187
188    /// Asynchronously waits for the configured delay duration since the last command execution.
189    /// This is a private helper to ensure commands are not sent too frequently.
190    async fn serial_await_delay(&self) {
191        let last_exec_diff = Instant::now().duration_since(self.last_execution);
192        if let Some(time_until_delay_reached) = self.delay.checked_sub(last_exec_diff) {
193            tokio::time::sleep(time_until_delay_reached).await;
194        }
195    }
196
197    /// Private async helper to send bytes to the serial port.
198    /// It handles clearing pending data, awaiting delay, and writing the buffer with timeouts.
199    async fn send_bytes(&mut self, tx_buffer: &[u8]) -> Result<()> {
200        // Before sending a new command, it's crucial to clear any lingering data
201        // in the serial port's read buffer. This prevents a scenario where a previous,
202        // timed-out response could be misinterpreted as the response to the current command.
203        loop {
204            log::trace!("read to see if there is any pending data");
205            let pending = self.serial.bytes_to_read()?;
206            log::trace!("got {pending} pending bytes");
207            if pending > 0 {
208                let mut buf: Vec<u8> = vec![0; 64]; // Temporary buffer to drain
209                let received =
210                    tokio::time::timeout(self.io_timeout, self.serial.read(buf.as_mut_slice()))
211                        .await??;
212                log::trace!("{received} pending bytes consumed");
213            } else {
214                break;
215            }
216        }
217        self.serial_await_delay().await;
218
219        log::trace!("write bytes: {tx_buffer:02X?}");
220        tokio::time::timeout(self.io_timeout, self.serial.write_all(tx_buffer)).await??;
221
222        // Flushing is usually not necessary and can sometimes cause issues.
223        if false {
224            // Disabled by default
225            log::trace!("flush connection");
226            tokio::time::timeout(self.io_timeout, self.serial.flush()).await??;
227        }
228        Ok(())
229    }
230
231    /// Private async helper to receive a specified number of bytes from the serial port with timeouts.
232    async fn receive_bytes(&mut self, size: usize) -> Result<Vec<u8>> {
233        let mut rx_buffer = vec![0; size];
234
235        log::trace!("read {size} bytes");
236        tokio::time::timeout(self.io_timeout, self.serial.read_exact(&mut rx_buffer)).await??;
237
238        self.last_execution = Instant::now(); // Update last execution time
239
240        log::trace!("receive_bytes: {rx_buffer:02X?}");
241        Ok(rx_buffer)
242    }
243
244    async fn send_and_receive(&mut self, tx_buffer: &[u8], reply_size: usize) -> Result<Vec<u8>> {
245        self.send_bytes(tx_buffer).await?;
246        self.receive_bytes(reply_size).await
247    }
248
249    /// Sets the timeout for individual I/O operations (read/write) on the serial port.
250    ///
251    /// # Arguments
252    ///
253    /// * `timeout`: The duration to wait for an I/O operation before timing out.
254    ///
255    /// # Returns
256    ///
257    /// A `Result` indicating success. This operation currently always succeeds.
258    pub fn set_timeout(&mut self, timeout: Duration) -> Result<()> {
259        log::trace!("set timeout to {timeout:?}");
260        self.io_timeout = timeout;
261        Ok(())
262    }
263
264    /// Sets the minimum delay between sending commands to the BMS.
265    ///
266    /// If the provided `delay` is less than `MINIMUM_DELAY` from the `protocol` module,
267    /// `MINIMUM_DELAY` will be used.
268    ///
269    /// # Arguments
270    ///
271    /// * `delay`: The desired minimum delay between commands.
272    pub fn set_delay(&mut self, delay: Duration) {
273        if delay < MINIMUM_DELAY {
274            log::warn!("delay {delay:?} lower minimum {MINIMUM_DELAY:?}, use minimum");
275            self.delay = MINIMUM_DELAY;
276        } else {
277            self.delay = delay;
278        }
279        log::trace!("set delay to {:?}", self.delay);
280    }
281
282    /// Asynchronously retrieves the State of Charge (SOC) and other primary battery metrics.
283    ///
284    /// # Returns
285    ///
286    /// A `Result` containing the `Soc` data or an `Error` if the command fails,
287    /// decoding is unsuccessful, or a timeout occurs.
288    ///
289    /// # Example
290    ///
291    /// ```no_run
292    /// # use dalybms_lib::tokio_serial_async::{DalyBMS, Error};
293    /// # use std::time::Duration;
294    /// # #[tokio::main]
295    /// # async fn main() -> Result<(), Error> {
296    /// # let mut bms = DalyBMS::new("/dev/ttyUSB0")?;
297    /// let soc_data = bms.get_soc().await?;
298    /// println!("Voltage: {:.1}V, Current: {:.1}A, SOC: {:.1}%",
299    ///          soc_data.total_voltage, soc_data.current, soc_data.soc_percent);
300    /// # Ok(())
301    /// # }
302    /// ```
303    pub async fn get_soc(&mut self) -> Result<Soc> {
304        log::trace!("get SOC");
305        request_with_retry!(self, Soc, &Soc::request(Address::Host), Soc::reply_size())
306    }
307
308    /// Asynchronously retrieves the highest and lowest cell voltages in the battery pack.
309    ///
310    /// # Returns
311    ///
312    /// A `Result` containing the `CellVoltageRange` data or an `Error`.
313    pub async fn get_cell_voltage_range(&mut self) -> Result<CellVoltageRange> {
314        log::trace!("get cell voltage range");
315        request_with_retry!(
316            self,
317            CellVoltageRange,
318            &CellVoltageRange::request(Address::Host),
319            CellVoltageRange::reply_size()
320        )
321    }
322
323    /// Asynchronously retrieves the highest and lowest temperatures measured by the BMS.
324    ///
325    /// # Returns
326    ///
327    /// A `Result` containing the `TemperatureRange` data or an `Error`.
328    pub async fn get_temperature_range(&mut self) -> Result<TemperatureRange> {
329        log::trace!("get temperature range");
330        request_with_retry!(
331            self,
332            TemperatureRange,
333            &TemperatureRange::request(Address::Host),
334            TemperatureRange::reply_size()
335        )
336    }
337
338    /// Asynchronously retrieves the status of the charging and discharging MOSFETs, and other related data.
339    ///
340    /// # Returns
341    ///
342    /// A `Result` containing the `MosfetStatus` data or an `Error`.
343    pub async fn get_mosfet_status(&mut self) -> Result<MosfetStatus> {
344        log::trace!("get mosfet status");
345        request_with_retry!(
346            self,
347            MosfetStatus,
348            &MosfetStatus::request(Address::Host),
349            MosfetStatus::reply_size()
350        )
351    }
352
353    /// Asynchronously retrieves general status information from the BMS, including cell count and temperature sensor count.
354    ///
355    /// This method also caches the retrieved status internally, as this information is
356    /// required by other methods like `get_cell_voltages` and `get_cell_temperatures`.
357    /// It's recommended to call this method at least once before calling those methods.
358    ///
359    /// # Returns
360    ///
361    /// A `Result` containing the `Status` data or an `Error`.
362    pub async fn get_status(&mut self) -> Result<Status> {
363        log::trace!("get status");
364        match request_with_retry!(
365            self,
366            Status,
367            &Status::request(Address::Host),
368            Status::reply_size()
369        ) {
370            Ok(status) => {
371                self.status = Some(status.clone()); // Cache the status
372                Ok(status)
373            }
374            Err(err) => Err(err),
375        }
376    }
377
378    /// Asynchronously retrieves the voltage of each individual cell in the battery pack.
379    ///
380    /// **Note:** `get_status().await` must be called at least once before this method
381    /// to determine the number of cells.
382    ///
383    /// # Returns
384    ///
385    /// A `Result` containing a `CellVoltages` of cell voltages or an `Error`.
386    /// Returns `Error::StatusError` if `get_status().await` was not called previously.
387    pub async fn get_cell_voltages(&mut self) -> Result<CellVoltages> {
388        log::trace!("get cell voltages");
389        let n_cells = if let Some(status) = &self.status {
390            status.cells
391        } else {
392            return Err(Error::StatusError);
393        };
394        request_with_retry!(
395            self,
396            CellVoltages,
397            &CellVoltages::request(Address::Host),
398            CellVoltages::reply_size(n_cells),
399            n_cells
400        )
401    }
402
403    /// Asynchronously retrieves the temperature from each individual temperature sensor.
404    ///
405    /// **Note:** `get_status().await` must be called at least once before this method
406    /// to determine the number of temperature sensors.
407    ///
408    /// # Returns
409    ///
410    /// A `Result` containing a `Vec<i32>` of temperatures in Celsius or an `Error`.
411    /// Returns `Error::StatusError` if `get_status().await` was not called previously.
412    pub async fn get_cell_temperatures(&mut self) -> Result<Vec<i32>> {
413        log::trace!("get cell temperatures");
414        let n_sensors = if let Some(status) = &self.status {
415            status.temperature_sensors
416        } else {
417            return Err(Error::StatusError);
418        };
419        request_with_retry!(
420            self,
421            CellTemperatures,
422            &CellTemperatures::request(Address::Host),
423            CellTemperatures::reply_size(n_sensors),
424            n_sensors
425        )
426    }
427
428    /// Asynchronously retrieves the balancing status of each individual cell.
429    ///
430    /// **Note:** `get_status().await` must be called at least once before this method
431    /// to determine the number of cells.
432    ///
433    /// # Returns
434    ///
435    /// A `Result` containing a `Vec<bool>` where `true` indicates the cell is currently balancing,
436    /// or an `Error`. Returns `Error::StatusError` if `get_status().await` was not called previously.
437    pub async fn get_balancing_status(&mut self) -> Result<Vec<bool>> {
438        log::trace!("get balancing status");
439        let n_cells = if let Some(status) = &self.status {
440            status.cells
441        } else {
442            return Err(Error::StatusError);
443        };
444        request_with_retry!(
445            self,
446            CellBalanceState,
447            &CellBalanceState::request(Address::Host),
448            CellBalanceState::reply_size(),
449            n_cells
450        )
451    }
452
453    /// Asynchronously retrieves a list of active error codes from the BMS.
454    ///
455    /// # Returns
456    ///
457    /// A `Result` containing a `Vec<ErrorCode>` of active errors or an `Error`.
458    /// An empty vector means no errors are currently active.
459    pub async fn get_errors(&mut self) -> Result<Vec<ErrorCode>> {
460        log::trace!("get errors");
461        request_with_retry!(
462            self,
463            ErrorCode,
464            &ErrorCode::request(Address::Host),
465            ErrorCode::reply_size()
466        )
467    }
468
469    /// Asynchronously enables or disables the discharging MOSFET.
470    ///
471    /// # Arguments
472    ///
473    /// * `enable`: Set to `true` to enable the discharging MOSFET, `false` to disable it.
474    ///
475    /// # Returns
476    ///
477    /// An empty `Result` indicating success or an `Error`.
478    pub async fn set_discharge_mosfet(&mut self, enable: bool) -> Result<()> {
479        log::trace!("set discharge mosfet to {enable}");
480        request_with_retry!(
481            self,
482            SetDischargeMosfet,
483            &SetDischargeMosfet::request(Address::Host, enable),
484            SetDischargeMosfet::reply_size()
485        )
486    }
487
488    /// Asynchronously enables or disables the charging MOSFET.
489    ///
490    /// # Arguments
491    ///
492    /// * `enable`: Set to `true` to enable the charging MOSFET, `false` to disable it.
493    ///
494    /// # Returns
495    ///
496    /// An empty `Result` indicating success or an `Error`.
497    pub async fn set_charge_mosfet(&mut self, enable: bool) -> Result<()> {
498        log::trace!("set charge mosfet to {enable}");
499        request_with_retry!(
500            self,
501            SetChargeMosfet,
502            &SetChargeMosfet::request(Address::Host, enable),
503            SetChargeMosfet::reply_size()
504        )
505    }
506
507    /// Asynchronously sets the State of Charge (SOC) percentage on the BMS.
508    ///
509    /// # Arguments
510    ///
511    /// * `soc_percent`: The desired SOC percentage (0.0 to 100.0). Values outside this range will be clamped by the protocol.
512    ///
513    /// # Returns
514    ///
515    /// An empty `Result` indicating success or an `Error`.
516    pub async fn set_soc(&mut self, soc_percent: f32) -> Result<()> {
517        log::trace!("set SOC to {soc_percent}");
518        request_with_retry!(
519            self,
520            SetSoc,
521            &SetSoc::request(Address::Host, soc_percent),
522            SetSoc::reply_size()
523        )
524    }
525
526    /// Asynchronously resets the BMS to its factory default settings.
527    ///
528    /// **Use with caution!**
529    ///
530    /// # Returns
531    ///
532    /// An empty `Result` indicating success or an `Error`.
533    pub async fn reset(&mut self) -> Result<()> {
534        log::trace!("reset to factory default settings");
535        request_with_retry!(
536            self,
537            BmsReset,
538            &BmsReset::request(Address::Host),
539            BmsReset::reply_size()
540        )
541    }
542}