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}