Skip to main content

dalybms_lib/
serialport.rs

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