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}