r4dcb08_lib/tokio_sync.rs
1//! Synchronous `tokio-modbus` client for the R4DCB08 temperature module.
2//!
3//! This module provides a high-level API (`R4DCB08` struct) to interact with
4//! the R4DCB08 8-channel temperature module using Modbus RTU or TCP. It handles
5//! the conversion between Rust types defined in the `crate::protocol` module and
6//! the raw Modbus register values.
7//!
8//! # Examples
9//!
10//! ## TCP Client Example
11//!
12//! ```no_run
13//! use r4dcb08_lib::tokio_sync::R4DCB08;
14//! use std::net::SocketAddr;
15//! use std::time::Duration;
16//!
17//! fn main() -> Result<(), Box<dyn std::error::Error>> {
18//! let socket_addr: SocketAddr = "127.0.0.1:502".parse()?;
19//!
20//! // Connect to the Modbus TCP device
21//! let mut modbus_ctx = tokio_modbus::client::sync::tcp::connect(socket_addr)?;
22//! modbus_ctx.set_timeout(Some(Duration::from_secs(1)));
23//!
24//! // Read temperatures from all 8 channels
25//! let temperatures = R4DCB08::read_temperatures(&mut modbus_ctx)?;
26//! println!("Temperatures: {}", temperatures);
27//!
28//! Ok(())
29//! }
30//! ```
31//!
32//! ## RTU Client Example
33//!
34//! ```no_run
35//! use r4dcb08_lib::tokio_sync::R4DCB08;
36//! use r4dcb08_lib::protocol::{Address, BaudRate};
37//! use std::time::Duration;
38//!
39//! fn main() -> Result<(), Box<dyn std::error::Error>> {
40//! let builder = r4dcb08_lib::tokio_common::serial_port_builder(
41//! "/dev/ttyUSB0", // Or "COM3" on Windows, etc.
42//! &BaudRate::B9600,
43//! );
44//! let slave = tokio_modbus::Slave(1);
45//! let mut modbus_ctx = tokio_modbus::client::sync::rtu::connect_slave(&builder, slave)?;
46//! modbus_ctx.set_timeout(Some(Duration::from_secs(1)));
47//!
48//! // Read the device's configured baud rate
49//! let remote_baud_rate = R4DCB08::read_baud_rate(&mut modbus_ctx)?;
50//! println!("Device baud rate: {}", remote_baud_rate);
51//!
52//! Ok(())
53//! }
54//! ```
55
56use crate::{protocol as proto, tokio_common::Result};
57use tokio_modbus::prelude::{SyncReader, SyncWriter};
58
59/// Synchronous client for interacting with the R4DCB08 temperature module over Modbus.
60///
61/// This struct provides methods to read sensor data and configure the module's
62/// operational parameters by wrapping `tokio-modbus` synchronous operations.
63///
64/// All methods that interact with the Modbus device will block the current thread.
65#[derive(Debug)]
66pub struct R4DCB08;
67
68impl R4DCB08 {
69 /// Helper function to map tokio result to our result.
70 fn map_tokio_result<T>(result: tokio_modbus::Result<T>) -> Result<T> {
71 match result {
72 Ok(Ok(result)) => Ok(result),
73 Ok(Err(err)) => Err(err.into()), // Modbus exception
74 Err(err) => Err(err.into()), // IO error
75 }
76 }
77
78 /// Helper function to read holding registers and decode them into a specific type.
79 fn read_and_decode<T, F>(
80 ctx: &mut tokio_modbus::client::sync::Context,
81 address: u16,
82 quantity: u16,
83 decoder: F,
84 ) -> Result<T>
85 where
86 F: FnOnce(&[u16]) -> std::result::Result<T, proto::Error>,
87 {
88 Ok(decoder(&Self::map_tokio_result(
89 ctx.read_holding_registers(address, quantity),
90 )?)?)
91 }
92
93 /// Reads the current temperatures from all 8 available channels in degrees Celsius (°C).
94 ///
95 /// If a channel's sensor is not connected or reports an error, the corresponding
96 /// `proto::Temperature` value will be `proto::Temperature::NAN`.
97 ///
98 /// # Returns
99 ///
100 /// A `Result<proto::Temperatures>` containing the temperatures for all channels,
101 /// or a Modbus error
102 ///
103 /// # Errors
104 ///
105 /// * `tokio_modbus::Error` if a Modbus communication error occurs (e.g., IO error, timeout, Modbus exception).
106 /// * `tokio_modbus::Error::Transport` with `std::io::ErrorKind::InvalidData` if the device returns
107 /// an unexpected number of registers.
108 ///
109 /// # Examples
110 ///
111 /// ```no_run
112 /// # use r4dcb08_lib::tokio_sync::R4DCB08;
113 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
114 /// # let mut modbus_ctx = tokio_modbus::client::sync::tcp::connect("127.0.0.1:502".parse()?)?;
115 /// let temperatures = R4DCB08::read_temperatures(&mut modbus_ctx)?;
116 /// println!("Temperatures read successfully:");
117 /// for (i, temp) in temperatures.iter().enumerate() {
118 /// println!(" Channel {}: {}", i, temp); // `temp` uses Display impl from protocol
119 /// }
120 /// # Ok(())
121 /// # }
122 /// ```
123 pub fn read_temperatures(
124 ctx: &mut tokio_modbus::client::sync::Context,
125 ) -> Result<proto::Temperatures> {
126 Self::read_and_decode(
127 ctx,
128 proto::Temperatures::ADDRESS,
129 proto::Temperatures::QUANTITY,
130 proto::Temperatures::decode_from_holding_registers,
131 )
132 }
133
134 /// Reads the configured temperature correction values (°C) for all 8 channels.
135 ///
136 /// A `proto::Temperature` value of `0.0` typically means no correction is applied,
137 /// while `proto::Temperature::NAN` might indicate an uninitialized or error state for a correction value if read.
138 ///
139 /// # Returns
140 ///
141 /// A `Result<proto::TemperatureCorrection>` containing correction values for each channel,
142 /// or a Modbus error.
143 ///
144 /// # Errors
145 ///
146 /// * `tokio_modbus::Error` for Modbus communication errors.
147 /// * `tokio_modbus::Error::Transport` with `std::io::ErrorKind::InvalidData` if the device returns
148 /// an unexpected number of registers.
149 ///
150 /// # Examples
151 ///
152 /// ```no_run
153 /// # use r4dcb08_lib::tokio_sync::R4DCB08;
154 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
155 /// # let mut modbus_ctx = tokio_modbus::client::sync::tcp::connect("127.0.0.1:502".parse()?)?;
156 /// let corrections = R4DCB08::read_temperature_correction(&mut modbus_ctx)?;
157 /// println!("Temperature correction values: {}", corrections);
158 /// # Ok(())
159 /// # }
160 /// ```
161 pub fn read_temperature_correction(
162 ctx: &mut tokio_modbus::client::sync::Context,
163 ) -> Result<proto::TemperatureCorrection> {
164 Self::read_and_decode(
165 ctx,
166 proto::TemperatureCorrection::ADDRESS,
167 proto::TemperatureCorrection::QUANTITY,
168 proto::TemperatureCorrection::decode_from_holding_registers,
169 )
170 }
171
172 /// Sets a temperature correction value for a specific channel.
173 ///
174 /// The `correction` value will be added to the raw temperature reading by the module.
175 /// Setting a correction value of `0.0` effectively disables it for that channel.
176 ///
177 /// # Arguments
178 ///
179 /// * `channel` - The `proto::Channel` to configure.
180 /// * `correction` - The `proto::Temperature` correction value to apply (in °C).
181 /// This type ensures the temperature value is within the representable range.
182 ///
183 /// # Returns
184 ///
185 /// A `Result<()>` indicating success or failure of the write operation.
186 ///
187 /// # Errors
188 ///
189 /// * `tokio_modbus::Error` for Modbus communication errors.
190 /// * `tokio_modbus::Error::Transport` with `std::io::ErrorKind::InvalidInput` if the
191 /// `correction` value is `NAN`.
192 ///
193 /// # Examples
194 ///
195 /// ```no_run
196 /// # use r4dcb08_lib::tokio_sync::R4DCB08;
197 /// use r4dcb08_lib::protocol::{Channel, Temperature};
198 ///
199 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
200 /// # let mut modbus_ctx = tokio_modbus::client::sync::tcp::connect("127.0.0.1:502".parse()?)?;
201 /// // Set the temperature correction for channel 3 to +1.3°C.
202 /// let channel = Channel::try_from(3)?; // Or handle ErrorChannelOutOfRange
203 /// let correction_value = Temperature::try_from(1.3)?; // Or handle ErrorDegreeCelsiusOutOfRange
204 ///
205 /// R4DCB08::set_temperature_correction(&mut modbus_ctx, channel, correction_value)?;
206 /// println!("Correction for channel {} set to {}.", channel, correction_value);
207 /// # Ok(())
208 /// # }
209 /// ```
210 pub fn set_temperature_correction(
211 ctx: &mut tokio_modbus::client::sync::Context,
212 channel: proto::Channel,
213 correction: proto::Temperature,
214 ) -> Result<()> {
215 Self::map_tokio_result(ctx.write_single_register(
216 proto::TemperatureCorrection::channel_address(channel),
217 proto::TemperatureCorrection::encode_for_write_register(correction)?,
218 ))
219 }
220
221 /// Reads the automatic temperature reporting interval.
222 ///
223 /// An interval of `0` seconds ([`proto::AutomaticReport::DISABLED`]) means automatic reporting is off.
224 ///
225 /// # Returns
226 ///
227 /// A `Result<proto::AutomaticReport>` indicating the configured reporting interval,
228 /// or a Modbus error.
229 ///
230 /// # Errors
231 ///
232 /// * `tokio_modbus::Error` for Modbus communication errors.
233 /// * `tokio_modbus::Error::Transport` with `std::io::ErrorKind::InvalidData` if the device returns
234 /// malformed data.
235 ///
236 /// # Examples
237 ///
238 /// ```no_run
239 /// # use r4dcb08_lib::tokio_sync::R4DCB08;
240 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
241 /// # let mut modbus_ctx = tokio_modbus::client::sync::tcp::connect("127.0.0.1:502".parse()?)?;
242 /// let report = R4DCB08::read_automatic_report(&mut modbus_ctx)?;
243 /// if report.is_disabled() {
244 /// println!("Automatic reporting is disabled.");
245 /// } else {
246 /// println!("Automatic report interval: {} seconds.", report.as_secs());
247 /// }
248 /// # Ok(())
249 /// # }
250 /// ```
251 pub fn read_automatic_report(
252 ctx: &mut tokio_modbus::client::sync::Context,
253 ) -> Result<proto::AutomaticReport> {
254 Self::read_and_decode(
255 ctx,
256 proto::AutomaticReport::ADDRESS,
257 proto::AutomaticReport::QUANTITY,
258 proto::AutomaticReport::decode_from_holding_registers,
259 )
260 }
261
262 /// Sets the automatic temperature reporting interval.
263 ///
264 /// When enabled (interval > 0), the module will periodically send temperature data
265 /// unsolicitedly over the RS485 bus (if applicable to the module's firmware).
266 ///
267 /// # Arguments
268 ///
269 /// * `report` - The `proto::AutomaticReport` interval (0 = disabled, 1-255 seconds).
270 /// The `proto::AutomaticReport` type ensures the value is within the valid hardware range.
271 ///
272 /// # Returns
273 ///
274 /// A `Result<()>` indicating success or failure of the write operation.
275 ///
276 /// # Errors
277 ///
278 /// * `tokio_modbus::Error` for Modbus communication errors.
279 ///
280 /// # Examples
281 ///
282 /// ```no_run
283 /// # use r4dcb08_lib::tokio_sync::R4DCB08;
284 /// use r4dcb08_lib::protocol::AutomaticReport;
285 /// use std::time::Duration;
286 ///
287 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
288 /// # let mut modbus_ctx = tokio_modbus::client::sync::tcp::connect("127.0.0.1:502".parse()?)?;
289 /// // Set the automatic report interval to 10 seconds.
290 /// let report_interval = AutomaticReport::try_from(Duration::from_secs(10))?;
291 /// R4DCB08::set_automatic_report(&mut modbus_ctx, report_interval)?;
292 /// println!("Automatic report interval set to 10 seconds.");
293 ///
294 /// // Disable automatic reporting
295 /// R4DCB08::set_automatic_report(&mut modbus_ctx, AutomaticReport::DISABLED)?;
296 /// println!("Automatic report disabled.");
297 /// # Ok(())
298 /// # }
299 /// ```
300 pub fn set_automatic_report(
301 ctx: &mut tokio_modbus::client::sync::Context,
302 report: proto::AutomaticReport,
303 ) -> Result<()> {
304 Self::map_tokio_result(ctx.write_single_register(
305 proto::AutomaticReport::ADDRESS,
306 report.encode_for_write_register(),
307 ))
308 }
309
310 /// Reads the current Modbus communication baud rate setting from the device.
311 ///
312 /// # Returns
313 ///
314 /// A `Result<proto::BaudRate>` containing the configured baud rate,
315 /// or a Modbus error.
316 ///
317 /// # Errors
318 ///
319 /// * `tokio_modbus::Error` for Modbus communication errors.
320 /// * `tokio_modbus::Error::Transport` with `std::io::ErrorKind::InvalidData` if the device returns
321 /// an invalid baud rate code.
322 ///
323 /// # Examples
324 ///
325 /// ```no_run
326 /// # use r4dcb08_lib::tokio_sync::R4DCB08;
327 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
328 /// # let mut modbus_ctx = tokio_modbus::client::sync::tcp::connect("127.0.0.1:502".parse()?)?;
329 /// let baud_rate = R4DCB08::read_baud_rate(&mut modbus_ctx)?;
330 /// println!("Current baud rate: {}", baud_rate);
331 /// # Ok(())
332 /// # }
333 /// ```
334 pub fn read_baud_rate(
335 ctx: &mut tokio_modbus::client::sync::Context,
336 ) -> Result<proto::BaudRate> {
337 Self::read_and_decode(
338 ctx,
339 proto::BaudRate::ADDRESS,
340 proto::BaudRate::QUANTITY,
341 proto::BaudRate::decode_from_holding_registers,
342 )
343 }
344
345 /// Sets the Modbus communication baud rate for the device.
346 ///
347 /// **Important:** The new baud rate setting will only take effect after the
348 /// R4DCB08 module is **power cycled** (turned off and then on again).
349 ///
350 /// # Arguments
351 ///
352 /// * `baud_rate` - The desired `proto::BaudRate` to set.
353 ///
354 /// # Returns
355 ///
356 /// A `Result<()>` indicating success or failure of the write operation.
357 ///
358 /// # Errors
359 ///
360 /// * `tokio_modbus::Error` for Modbus communication errors.
361 ///
362 /// # Examples
363 ///
364 /// ```no_run
365 /// # use r4dcb08_lib::tokio_sync::R4DCB08;
366 /// use r4dcb08_lib::protocol::BaudRate;
367 ///
368 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
369 /// # let mut modbus_ctx = tokio_modbus::client::sync::tcp::connect("127.0.0.1:502".parse()?)?;
370 /// // Set the baud rate to 19200.
371 /// let new_baud_rate = BaudRate::B19200; // Direct enum variant
372 /// // Or from u16:
373 /// // let new_baud_rate = BaudRate::try_from(19200)?;
374 ///
375 /// R4DCB08::set_baud_rate(&mut modbus_ctx, new_baud_rate)?;
376 /// println!("Baud rate set to {}. Power cycle the device for changes to take effect.", new_baud_rate);
377 /// # Ok(())
378 /// # }
379 /// ```
380 pub fn set_baud_rate(
381 ctx: &mut tokio_modbus::client::sync::Context,
382 baud_rate: proto::BaudRate,
383 ) -> Result<()> {
384 Self::map_tokio_result(ctx.write_single_register(
385 proto::BaudRate::ADDRESS,
386 baud_rate.encode_for_write_register(),
387 ))
388 }
389
390 /// Resets the R4DCB08 module to its factory default settings.
391 ///
392 /// This will reset all configurable parameters like Modbus Address, Baud Rate,
393 /// Temperature Corrections, etc., to their original defaults.
394 ///
395 /// **Important:**
396 /// * After this command is successfully sent, the module may become unresponsive
397 /// on the Modbus bus until it is power cycled.
398 /// * A **power cycle** (turning the device off and then on again) is **required**
399 /// to complete the factory reset process and for the default settings to be applied.
400 ///
401 /// # Returns
402 ///
403 /// A `Result<()>` indicating if the reset command was sent successfully.
404 /// It does not confirm the reset is complete, only that the Modbus write was acknowledged.
405 ///
406 /// # Errors
407 ///
408 /// * `tokio_modbus::Error` for Modbus communication errors. A timeout error after this
409 /// command might be expected as the device resets and may not send a response.
410 ///
411 /// # Examples
412 ///
413 /// ```no_run
414 /// # use r4dcb08_lib::tokio_sync::R4DCB08;
415 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
416 /// # let mut modbus_ctx = tokio_modbus::client::sync::tcp::connect("127.0.0.1:502".parse()?)?;
417 /// println!("Attempting to send factory reset command...");
418 /// match R4DCB08::factory_reset(&mut modbus_ctx) {
419 /// Ok(()) => println!("Factory reset command sent. Power cycle the device to complete."),
420 /// Err(e) => {
421 /// let ignore_error = if let r4dcb08_lib::tokio_common::Error::Modbus(tokio_modbus::Error::Transport(error)) = &e {
422 /// // After the a successful factory reset we get no response :-(
423 /// error.kind() == std::io::ErrorKind::TimedOut
424 /// } else {
425 /// false
426 /// };
427 ///
428 /// if ignore_error {
429 /// println!("Factory reset command sent. Device timed out as expected. Power cycle to complete.");
430 /// } else {
431 /// eprintln!("Modbus error during factory reset: {}", e);
432 /// }
433 /// }
434 /// }
435 /// # Ok(())
436 /// # }
437 /// ```
438 pub fn factory_reset(ctx: &mut tokio_modbus::client::sync::Context) -> Result<()> {
439 Self::map_tokio_result(ctx.write_single_register(
440 proto::FactoryReset::ADDRESS,
441 proto::FactoryReset::encode_for_write_register(),
442 ))
443 }
444
445 /// Reads the current Modbus device address from the module.
446 ///
447 /// **Important Usage Notes:**
448 /// * This command is typically used when the device's
449 /// current address is unknown. To do this, the Modbus request **must be sent to
450 /// the broadcast address ([`proto::Address::BROADCAST`])**.
451 /// * **Single Device Only:** Only **one** R4DCB08 module should be connected to the
452 /// Modbus bus when executing this command with the broadcast address. If multiple
453 /// devices are present, they might all respond, leading to data collisions and errors.
454 ///
455 /// # Returns
456 ///
457 /// A `Result<proto::Address>` containing the device's configured Modbus address,
458 /// or a Modbus error.
459 ///
460 /// # Errors
461 ///
462 /// * `tokio_modbus::Error` for Modbus communication errors.
463 /// * `tokio_modbus::Error::Transport` with `std::io::ErrorKind::InvalidData` if the device returns
464 /// a malformed or out-of-range address.
465 ///
466 /// # Examples
467 ///
468 /// ```no_run
469 /// use r4dcb08_lib::tokio_sync::R4DCB08;
470 /// use r4dcb08_lib::protocol::Address;
471 ///
472 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
473 /// // Requires serial port features enabled in tokio-modbus
474 /// let builder = tokio_serial::new("/dev/ttyUSB0", 9600) // Baud rate 9600
475 /// .parity(tokio_serial::Parity::None)
476 /// .stop_bits(tokio_serial::StopBits::One)
477 /// .data_bits(tokio_serial::DataBits::Eight)
478 /// .flow_control(tokio_serial::FlowControl::None);
479 /// // Assume only one device connected, use broadcast address for reading
480 /// let slave = tokio_modbus::Slave(*Address::BROADCAST);
481 /// let mut modbus_ctx = tokio_modbus::client::sync::rtu::connect_slave(&builder, slave).expect("Failed to connect");
482 ///
483 /// let address = R4DCB08::read_address(&mut modbus_ctx)?;
484 /// println!("Device responded with address: {}", address);
485 /// # Ok(())
486 /// # }
487 /// ```
488 pub fn read_address(ctx: &mut tokio_modbus::client::sync::Context) -> Result<proto::Address> {
489 Self::read_and_decode(
490 ctx,
491 proto::Address::ADDRESS,
492 proto::Address::QUANTITY,
493 proto::Address::decode_from_holding_registers,
494 )
495 }
496
497 /// Sets a new Modbus device address.
498 ///
499 /// **Warning:**
500 /// * This permanently changes the device's Modbus address.
501 /// * This command must be sent while addressing the device using its **current** Modbus address.
502 /// * After successfully changing the address, subsequent communication with the
503 /// device **must** use the new address.
504 ///
505 /// # Arguments
506 ///
507 /// * `new_address` - The new [`proto::Address`] to assign to the device.
508 ///
509 /// # Returns
510 ///
511 /// A `Result<()>` indicating success or failure of the write operation.
512 ///
513 /// # Errors
514 ///
515 /// * `tokio_modbus::Error` for Modbus communication errors.
516 ///
517 /// # Examples
518 ///
519 /// ```no_run
520 /// use r4dcb08_lib::tokio_sync::R4DCB08;
521 /// use r4dcb08_lib::protocol::Address;
522 ///
523 /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
524 /// // Requires serial port features enabled in tokio-modbus
525 /// let builder = tokio_serial::new("/dev/ttyUSB0", 9600) // Baud rate 9600
526 /// .parity(tokio_serial::Parity::None)
527 /// .stop_bits(tokio_serial::StopBits::One)
528 /// .data_bits(tokio_serial::DataBits::Eight)
529 /// .flow_control(tokio_serial::FlowControl::None);
530 /// // --- Assume device is currently at address 1 ---
531 /// let current_device_address = Address::try_from(1)?;
532 ///
533 /// let slave = tokio_modbus::Slave(*current_device_address);
534 /// let mut modbus_ctx = tokio_modbus::client::sync::rtu::connect_slave(&builder, slave).expect("Failed to connect");
535 ///
536 /// // --- New address we want to set ---
537 /// let new_device_address = Address::try_from(10)?;
538 ///
539 /// println!("Attempting to change device address from {} to {}...", current_device_address, new_device_address);
540 /// R4DCB08::set_address(&mut modbus_ctx, new_device_address)?;
541 /// println!("Device address successfully changed to {}.", new_device_address);
542 /// println!("You will need to reconnect using the new address for further communication.");
543 /// # Ok(())
544 /// # }
545 /// ```
546 pub fn set_address(
547 ctx: &mut tokio_modbus::client::sync::Context,
548 new_address: proto::Address,
549 ) -> Result<()> {
550 Self::map_tokio_result(ctx.write_single_register(
551 proto::Address::ADDRESS,
552 new_address.encode_for_write_register(),
553 ))
554 }
555}