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}