sdm72_lib/
tokio_sync.rs

1//! This module provides a synchronous client for the SDM72 energy meter.
2//!
3//! The [`SDM72`] struct is the main entry point for interacting with the meter. It
4//! wraps a synchronous `tokio-modbus` context and provides high-level methods
5//! for reading and writing meter data.
6//!
7//! This client is suitable for applications that do not require asynchronous
8//! operations and can be used in environments without the `tokio` runtime. For
9//! applications that use `tokio`, the asynchronous client in the
10//! [`crate::tokio_async`] module may be more suitable.
11//!
12//! # Example
13//!
14//! ```no_run
15//! use sdm72_lib::{
16//!     protocol::Address,
17//!     tokio_sync::SDM72,
18//! };
19//! use tokio_modbus::client::sync::tcp;
20//! use tokio_modbus::Slave;
21//! use std::time::Duration;
22//!
23//! fn main() -> Result<(), Box<dyn std::error::Error>> {
24//!     let socket_addr = "192.168.1.100:502".parse()?;
25//!     let mut ctx = tcp::connect_slave(socket_addr, Slave(*Address::default()))?;
26//!
27//!     let values = SDM72::read_all(&mut ctx, &Duration::from_millis(100))?;
28//!
29//!     println!("Successfully read values: {:#?}", values);
30//!
31//!     Ok(())
32//! }
33//! ```
34
35use crate::{
36    protocol::{self as proto, ModbusParam},
37    tokio_common::{AllSettings, AllValues, Result},
38};
39use tokio_modbus::prelude::{SyncReader, SyncWriter};
40
41/// A synchronous client for the SDM72 energy meter.
42///
43/// This struct provides a high-level interface for interacting with the SDM72
44/// energy meter. It uses a synchronous `tokio-modbus` context for communication.
45/// An instance of this client can be created using the [`new`](#method.new) method.
46pub struct SDM72;
47
48/// A macro to generate a function for reading a holding register.
49macro_rules! read_holding {
50    ($func_name:expr, $ty:ident) => {
51        paste::item! {
52            #[doc = "Reads the [`proto::" $ty "`] value from the Modbus holding register."]
53            pub fn $func_name(ctx: &mut tokio_modbus::client::sync::Context) -> Result<proto::$ty> {
54                let rsp = ctx
55                    .read_holding_registers(<proto::$ty>::ADDRESS, <proto::$ty>::QUANTITY)??;
56                Ok(<proto::$ty>::decode_from_holding_registers(&rsp)?)
57            }
58        }
59    };
60}
61
62/// A macro to generate a function for writing a holding register.
63macro_rules! write_holding {
64    ($func_name:expr, $ty:ident) => {
65        paste::item! {
66            #[doc = "Writes the [`proto::" $ty "`] value to the Modbus holding register."]
67            pub fn [< set_ $func_name >](ctx: &mut tokio_modbus::client::sync::Context, value: proto::$ty) -> Result<()> {
68                Ok(ctx.write_multiple_registers(
69                    <proto::$ty>::ADDRESS,
70                    &value.encode_for_write_registers(),
71                )??)
72            }
73        }
74    };
75}
76
77impl SDM72 {
78    read_holding!(system_type, SystemType);
79    write_holding!(system_type, SystemType);
80    read_holding!(pulse_width, PulseWidth);
81    write_holding!(pulse_width, PulseWidth);
82    read_holding!(kppa, KPPA);
83    /// Sets the Key Parameter Programming Authorization (KPPA).
84    ///
85    /// This is required to change settings on the meter.
86    pub fn set_kppa(
87        ctx: &mut tokio_modbus::client::sync::Context,
88        password: proto::Password,
89    ) -> Result<()> {
90        Ok(ctx.write_multiple_registers(
91            proto::KPPA::ADDRESS,
92            &proto::KPPA::encode_for_write_registers(password),
93        )??)
94    }
95    read_holding!(parity_and_stop_bit, ParityAndStopBit);
96    write_holding!(parity_and_stop_bit, ParityAndStopBit);
97    read_holding!(address, Address);
98    write_holding!(address, Address);
99    read_holding!(pulse_constant, PulseConstant);
100    write_holding!(pulse_constant, PulseConstant);
101    read_holding!(password, Password);
102    write_holding!(password, Password);
103    read_holding!(baud_rate, BaudRate);
104    write_holding!(baud_rate, BaudRate);
105    read_holding!(auto_scroll_time, AutoScrollTime);
106    write_holding!(auto_scroll_time, AutoScrollTime);
107    read_holding!(backlight_time, BacklightTime);
108    write_holding!(backlight_time, BacklightTime);
109    read_holding!(pulse_energy_type, PulseEnergyType);
110    write_holding!(pulse_energy_type, PulseEnergyType);
111    /// Resets the historical data on the meter.
112    ///
113    /// This requires KPPA authorization.
114    pub fn reset_historical_data(ctx: &mut tokio_modbus::client::sync::Context) -> Result<()> {
115        Ok(ctx.write_multiple_registers(
116            proto::ResetHistoricalData::ADDRESS,
117            &proto::ResetHistoricalData::encode_for_write_registers(),
118        )??)
119    }
120    read_holding!(serial_number, SerialNumber);
121    read_holding!(meter_code, MeterCode);
122    read_holding!(software_version, SoftwareVersion);
123
124    /// Reads all settings from the meter in a single batch operation.
125    ///
126    /// This method is more efficient than reading each setting individually because
127    /// it minimizes the number of Modbus requests by batching them. The SDM72
128    /// meter has a limit of 30 parameters per request, so this function splits
129    /// the reads into multiple batches.
130    ///
131    /// # Arguments
132    ///
133    /// * `delay` - The delay to be inserted between Modbus requests. This is
134    ///   necessary for some Modbus devices, which may need a short pause to
135    ///   process a request before they are ready to accept the next one. A
136    ///   typical value is 100 milliseconds, but this may vary depending on the
137    ///   device and network conditions.
138    pub fn read_all_settings(
139        ctx: &mut tokio_modbus::client::sync::Context,
140        delay: &std::time::Duration,
141    ) -> Result<AllSettings> {
142        let offset1 = proto::SystemType::ADDRESS;
143        let quantity =
144            { proto::PulseEnergyType::ADDRESS - offset1 + proto::PulseEnergyType::QUANTITY };
145        let rsp1 = ctx.read_holding_registers(offset1, quantity)??;
146
147        std::thread::sleep(*delay);
148        let serial_number = Self::serial_number(ctx)?;
149        std::thread::sleep(*delay);
150        let meter_code = Self::meter_code(ctx)?;
151        std::thread::sleep(*delay);
152        let software_version = Self::software_version(ctx)?;
153
154        Ok(AllSettings {
155            system_type: crate::decode_subset_item_from_holding_register!(
156                offset1,
157                proto::SystemType,
158                &rsp1
159            )?,
160            pulse_width: crate::decode_subset_item_from_holding_register!(
161                offset1,
162                proto::PulseWidth,
163                &rsp1
164            )?,
165            kppa: crate::decode_subset_item_from_holding_register!(offset1, proto::KPPA, &rsp1)?,
166            parity_and_stop_bit: crate::decode_subset_item_from_holding_register!(
167                offset1,
168                proto::ParityAndStopBit,
169                &rsp1
170            )?,
171            address: crate::decode_subset_item_from_holding_register!(
172                offset1,
173                proto::Address,
174                &rsp1
175            )?,
176            pulse_constant: crate::decode_subset_item_from_holding_register!(
177                offset1,
178                proto::PulseConstant,
179                &rsp1
180            )?,
181            password: crate::decode_subset_item_from_holding_register!(
182                offset1,
183                proto::Password,
184                &rsp1
185            )?,
186            baud_rate: crate::decode_subset_item_from_holding_register!(
187                offset1,
188                proto::BaudRate,
189                &rsp1
190            )?,
191            auto_scroll_time: crate::decode_subset_item_from_holding_register!(
192                offset1,
193                proto::AutoScrollTime,
194                &rsp1
195            )?,
196            backlight_time: crate::decode_subset_item_from_holding_register!(
197                offset1,
198                proto::BacklightTime,
199                &rsp1
200            )?,
201            pulse_energy_type: crate::decode_subset_item_from_holding_register!(
202                offset1,
203                proto::PulseEnergyType,
204                &rsp1
205            )?,
206            serial_number,
207            meter_code,
208            software_version,
209        })
210    }
211
212    /// Reads all measurement values from the meter in a single batch operation.
213    ///
214    /// This method is more efficient than reading each value individually because
215    /// it minimizes the number of Modbus requests by batching them. The SDM72
216    /// meter has a limit of 30 parameters per request, so this function splits
217    /// the reads into multiple batches.
218    ///
219    /// # Arguments
220    ///
221    /// * `delay` - The delay to be inserted between Modbus requests. This is
222    ///   necessary for some Modbus devices, which may need a short pause to
223    ///   process a request before they are ready to accept the next one. A
224    ///   typical value is 100 milliseconds, but this may vary depending on the
225    ///   device and network conditions.
226    pub fn read_all(
227        ctx: &mut tokio_modbus::client::sync::Context,
228        delay: &std::time::Duration,
229    ) -> Result<AllValues> {
230        let offset1 = proto::L1Voltage::ADDRESS;
231        let quantity =
232            { proto::ExportEnergyActive::ADDRESS - offset1 + proto::ExportEnergyActive::QUANTITY };
233        let rsp1 = ctx.read_input_registers(offset1, quantity)??;
234
235        std::thread::sleep(*delay);
236
237        let offset2 = proto::L1ToL2Voltage::ADDRESS;
238        let quantity =
239            { proto::NeutralCurrent::ADDRESS - offset2 + proto::NeutralCurrent::QUANTITY };
240        let rsp2 = ctx.read_input_registers(offset2, quantity)??;
241
242        std::thread::sleep(*delay);
243
244        let offset3 = proto::TotalEnergyActive::ADDRESS;
245        let quantity = { proto::NetKwh::ADDRESS - offset3 + proto::NetKwh::QUANTITY };
246        let rsp3 = ctx.read_input_registers(offset3, quantity)??;
247
248        std::thread::sleep(*delay);
249
250        let offset4 = proto::ImportTotalPowerActive::ADDRESS;
251        let quantity = {
252            proto::ExportTotalPowerActive::ADDRESS - offset4
253                + proto::ExportTotalPowerActive::QUANTITY
254        };
255        let rsp4 = ctx.read_input_registers(offset4, quantity)??;
256
257        Ok(AllValues {
258            l1_voltage: crate::decode_subset_item_from_input_register!(
259                offset1,
260                proto::L1Voltage,
261                &rsp1
262            )?,
263            l2_voltage: crate::decode_subset_item_from_input_register!(
264                offset1,
265                proto::L2Voltage,
266                &rsp1
267            )?,
268            l3_voltage: crate::decode_subset_item_from_input_register!(
269                offset1,
270                proto::L3Voltage,
271                &rsp1
272            )?,
273            l1_current: crate::decode_subset_item_from_input_register!(
274                offset1,
275                proto::L1Current,
276                &rsp1
277            )?,
278            l2_current: crate::decode_subset_item_from_input_register!(
279                offset1,
280                proto::L2Current,
281                &rsp1
282            )?,
283            l3_current: crate::decode_subset_item_from_input_register!(
284                offset1,
285                proto::L3Current,
286                &rsp1
287            )?,
288            l1_power_active: crate::decode_subset_item_from_input_register!(
289                offset1,
290                proto::L1PowerActive,
291                &rsp1
292            )?,
293            l2_power_active: crate::decode_subset_item_from_input_register!(
294                offset1,
295                proto::L2PowerActive,
296                &rsp1
297            )?,
298            l3_power_active: crate::decode_subset_item_from_input_register!(
299                offset1,
300                proto::L3PowerActive,
301                &rsp1
302            )?,
303            l1_power_apparent: crate::decode_subset_item_from_input_register!(
304                offset1,
305                proto::L1PowerApparent,
306                &rsp1
307            )?,
308            l2_power_apparent: crate::decode_subset_item_from_input_register!(
309                offset1,
310                proto::L2PowerApparent,
311                &rsp1
312            )?,
313            l3_power_apparent: crate::decode_subset_item_from_input_register!(
314                offset1,
315                proto::L3PowerApparent,
316                &rsp1
317            )?,
318            l1_power_reactive: crate::decode_subset_item_from_input_register!(
319                offset1,
320                proto::L1PowerReactive,
321                &rsp1
322            )?,
323            l2_power_reactive: crate::decode_subset_item_from_input_register!(
324                offset1,
325                proto::L2PowerReactive,
326                &rsp1
327            )?,
328            l3_power_reactive: crate::decode_subset_item_from_input_register!(
329                offset1,
330                proto::L3PowerReactive,
331                &rsp1
332            )?,
333            l1_power_factor: crate::decode_subset_item_from_input_register!(
334                offset1,
335                proto::L1PowerFactor,
336                &rsp1
337            )?,
338            l2_power_factor: crate::decode_subset_item_from_input_register!(
339                offset1,
340                proto::L2PowerFactor,
341                &rsp1
342            )?,
343            l3_power_factor: crate::decode_subset_item_from_input_register!(
344                offset1,
345                proto::L3PowerFactor,
346                &rsp1
347            )?,
348            ln_average_voltage: crate::decode_subset_item_from_input_register!(
349                offset1,
350                proto::LtoNAverageVoltage,
351                &rsp1
352            )?,
353            ln_average_current: crate::decode_subset_item_from_input_register!(
354                offset1,
355                proto::LtoNAverageCurrent,
356                &rsp1
357            )?,
358            total_line_current: crate::decode_subset_item_from_input_register!(
359                offset1,
360                proto::TotalLineCurrent,
361                &rsp1
362            )?,
363            total_power: crate::decode_subset_item_from_input_register!(
364                offset1,
365                proto::TotalPower,
366                &rsp1
367            )?,
368            total_power_apparent: crate::decode_subset_item_from_input_register!(
369                offset1,
370                proto::TotalPowerApparent,
371                &rsp1
372            )?,
373            total_power_reactive: crate::decode_subset_item_from_input_register!(
374                offset1,
375                proto::TotalPowerReactive,
376                &rsp1
377            )?,
378            total_power_factor: crate::decode_subset_item_from_input_register!(
379                offset1,
380                proto::TotalPowerFactor,
381                &rsp1
382            )?,
383            frequency: crate::decode_subset_item_from_input_register!(
384                offset1,
385                proto::Frequency,
386                &rsp1
387            )?,
388            import_energy_active: crate::decode_subset_item_from_input_register!(
389                offset1,
390                proto::ImportEnergyActive,
391                &rsp1
392            )?,
393            export_energy_active: crate::decode_subset_item_from_input_register!(
394                offset1,
395                proto::ExportEnergyActive,
396                &rsp1
397            )?,
398
399            l1l2_voltage: crate::decode_subset_item_from_input_register!(
400                offset2,
401                proto::L1ToL2Voltage,
402                &rsp2
403            )?,
404            l2l3_voltage: crate::decode_subset_item_from_input_register!(
405                offset2,
406                proto::L2ToL3Voltage,
407                &rsp2
408            )?,
409            l3l1_voltage: crate::decode_subset_item_from_input_register!(
410                offset2,
411                proto::L3ToL1Voltage,
412                &rsp2
413            )?,
414            ll_average_voltage: crate::decode_subset_item_from_input_register!(
415                offset2,
416                proto::LtoLAverageVoltage,
417                &rsp2
418            )?,
419            neutral_current: crate::decode_subset_item_from_input_register!(
420                offset2,
421                proto::NeutralCurrent,
422                &rsp2
423            )?,
424
425            total_energy_active: crate::decode_subset_item_from_input_register!(
426                offset3,
427                proto::TotalEnergyActive,
428                &rsp3
429            )?,
430            total_energy_reactive: crate::decode_subset_item_from_input_register!(
431                offset3,
432                proto::TotalEnergyReactive,
433                &rsp3
434            )?,
435            resettable_total_energy_active: crate::decode_subset_item_from_input_register!(
436                offset3,
437                proto::ResettableTotalEnergyActive,
438                &rsp3
439            )?,
440            resettable_total_energy_reactive: crate::decode_subset_item_from_input_register!(
441                offset3,
442                proto::ResettableTotalEnergyReactive,
443                &rsp3
444            )?,
445            resettable_import_energy_active: crate::decode_subset_item_from_input_register!(
446                offset3,
447                proto::ResettableImportEnergyActive,
448                &rsp3
449            )?,
450            resettable_export_energy_active: crate::decode_subset_item_from_input_register!(
451                offset3,
452                proto::ResettableExportEnergyActive,
453                &rsp3
454            )?,
455            net_kwh: crate::decode_subset_item_from_input_register!(offset3, proto::NetKwh, &rsp3)?,
456
457            import_total_energy_active: crate::decode_subset_item_from_input_register!(
458                offset4,
459                proto::ImportTotalPowerActive,
460                &rsp4
461            )?,
462            export_total_energy_active: crate::decode_subset_item_from_input_register!(
463                offset4,
464                proto::ExportTotalPowerActive,
465                &rsp4
466            )?,
467        })
468    }
469}