sdm72_lib/
protocol.rs

1//! This module defines the core data structures and traits for the SDM72 Modbus protocol.
2//!
3//! It includes definitions for all supported Modbus registers, data types for meter
4//! settings and measurements, and helper traits for encoding and decoding Modbus data.
5//!
6//! The documentation for this module is based on the "Eastron SDM72D-M-v2 Modbus Protocol"
7//! document.
8
9/// Represents errors that can occur within the SDM72 protocol logic.
10#[derive(Debug, thiserror::Error)]
11pub enum Error {
12    /// The provided Modbus address is outside the valid range.
13    #[error(
14        "The address value {0} is outside the permissible range of {min} to {max}",
15        min = Address::MIN,
16        max = Address::MAX
17    )]
18    AddressOutOfRange(u8),
19
20    /// The provided password is outside the valid range.
21    #[error(
22        "The password value {0} is outside the permissible range of {min} to {max}",
23        min = Password::MIN,
24        max = Password::MAX
25    )]
26    PasswordOutOfRange(u16),
27
28    /// The provided auto scroll time is outside the valid range.
29    #[error(
30        "The auto scroll time value {0} is outside the permissible range of {min} to {max}",
31        min = AutoScrollTime::MIN,
32        max = AutoScrollTime::MAX
33    )]
34    AutoScrollTimeOutOfRange(u8),
35
36    /// The provided backlight time is outside the valid range.
37    #[error(
38        "The backlit time value {0} is outside the permissible range of {min} to {max}",
39        min = BacklightTime::MIN,
40        max = BacklightTime::MAX
41    )]
42    BacklitTimeOutOfRange(u8),
43
44    /// The provided baud rate is not supported by the device.
45    #[error("Baud rate must by any value of 1200, 2400, 4800, 9600, 19200.")]
46    InvalidBaudRate,
47
48    /// A generic error for a value that is outside its permissible range.
49    #[error("Value is outside the permissible range")]
50    OutOfRange,
51
52    /// An unexpected or invalid value was received from the device.
53    #[error("Unexpected value")]
54    InvalidValue,
55
56    /// The number of words received from the device is incorrect for the requested operation.
57    #[error("Words count error")]
58    WordsCountError,
59}
60
61/// 16-bit value stored in Modbus register.
62pub type Word = u16;
63
64/// A trait for defining Modbus parameters.
65///
66/// This trait provides a common interface for defining the properties of a Modbus
67/// register, such as its address, the number of words it occupies, and the data
68/// type it represents.
69pub trait ModbusParam: Sized {
70    /// The Modbus holding register address.
71    const ADDRESS: u16;
72    /// The quantity of Modbus words (16-bit). The length in bytes is `QUANTITY * 2`.
73    const QUANTITY: u16;
74    /// The data type that the Modbus words represent (e.g., `f32`, `u16`).
75    type ProtocolType;
76}
77
78/// A macro to convert a slice of `u16` words into a protocol value (e.g., `f32`).
79macro_rules! words_to_protocol_value {
80    ($words:expr) => {{
81        let bytes = $words
82            .iter()
83            .copied()
84            .flat_map(u16::to_be_bytes)
85            .collect::<Vec<u8>>();
86        let array = bytes.try_into().or(Err(Error::WordsCountError))?;
87        Ok(<Self as ModbusParam>::ProtocolType::from_be_bytes(array))
88    }};
89}
90
91/// A macro to convert a protocol value (e.g., `f32`) into a `Vec<u16>` of Modbus words.
92macro_rules! protocol_value_to_words {
93    ($val:expr) => {
94        $val.to_be_bytes()
95            .chunks(2)
96            .map(|chunk| {
97                let array = chunk.try_into().expect("unexpected encoding error");
98                u16::from_be_bytes(array)
99            })
100            .collect()
101    };
102}
103
104/// The system (wiring) type.
105///
106/// Note: To set the value you need ['KPPA'](enum@KPPA).
107#[derive(Debug, Default, Clone, Copy, PartialEq)]
108#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
109pub enum SystemType {
110    /// 1 phase with 2 wire
111    Type1P2W,
112
113    #[default]
114    /// 3 phase with 4 wire
115    Type3P4W,
116}
117impl ModbusParam for SystemType {
118    type ProtocolType = f32;
119    const ADDRESS: u16 = 0x000A;
120    const QUANTITY: u16 = 2;
121}
122impl SystemType {
123    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
124        let val = words_to_protocol_value!(words)?;
125        match val {
126            1.0 => Ok(SystemType::Type1P2W),
127            3.0 => Ok(SystemType::Type3P4W),
128            _ => Err(Error::InvalidValue),
129        }
130    }
131
132    pub fn encode_for_write_registers(&self) -> Vec<Word> {
133        let val = match self {
134            SystemType::Type1P2W => 1,
135            SystemType::Type3P4W => 3,
136        } as <Self as ModbusParam>::ProtocolType;
137        protocol_value_to_words!(val)
138    }
139}
140impl std::fmt::Display for SystemType {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        match self {
143            SystemType::Type1P2W => write!(f, "1 phase 2 wire"),
144            SystemType::Type3P4W => write!(f, "3 phase 4 wire"),
145        }
146    }
147}
148
149/// Pulse width for the pulse output in milliseconds.
150///
151/// Note: If pulse constant is 1000 imp/kWh, then the pulse width is fixed to 35ms and cannot be adjusted!
152#[derive(Debug, Clone, Copy, PartialEq)]
153#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
154pub struct PulseWidth(u16);
155impl ModbusParam for PulseWidth {
156    type ProtocolType = f32;
157    const ADDRESS: u16 = 0x000C;
158    const QUANTITY: u16 = 2;
159}
160impl std::ops::Deref for PulseWidth {
161    type Target = u16;
162    fn deref(&self) -> &Self::Target {
163        &self.0
164    }
165}
166impl Default for PulseWidth {
167    fn default() -> Self {
168        Self(100)
169    }
170}
171impl PulseWidth {
172    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
173        let val = words_to_protocol_value!(words)?;
174        Ok(Self(val as u16))
175    }
176
177    pub fn encode_for_write_registers(&self) -> Vec<Word> {
178        let val = self.0 as <Self as ModbusParam>::ProtocolType;
179        protocol_value_to_words!(val)
180    }
181}
182impl TryFrom<u16> for PulseWidth {
183    type Error = Error;
184
185    fn try_from(value: u16) -> Result<Self, Self::Error> {
186        Ok(Self(value))
187    }
188}
189impl std::fmt::Display for PulseWidth {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        write!(f, "{}", self.0)
192    }
193}
194
195/// KPPA (Key Parameter Programming Authorization) write the correct password to get KPPA.
196/// This will be required to change the settings.
197#[derive(Debug, Clone, Copy, PartialEq)]
198#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
199pub enum KPPA {
200    NotAuthorized,
201    Authorized,
202}
203impl ModbusParam for KPPA {
204    type ProtocolType = f32;
205    const ADDRESS: u16 = 0x000E;
206    const QUANTITY: u16 = 2;
207}
208impl KPPA {
209    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
210        let val = words_to_protocol_value!(words)?;
211        match val {
212            0.0 => Ok(Self::NotAuthorized),
213            1.0 => Ok(Self::Authorized),
214            _ => Err(Error::InvalidValue),
215        }
216    }
217
218    pub fn encode_for_write_registers(password: Password) -> Vec<Word> {
219        password.encode_for_write_registers()
220    }
221}
222impl std::fmt::Display for KPPA {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        match self {
225            KPPA::NotAuthorized => write!(f, "not authorized"),
226            KPPA::Authorized => write!(f, "authorized"),
227        }
228    }
229}
230
231/// Parity and stop bits of the Modbus RTU protocol for the RS485 serial port.
232///
233/// Note: To set the value you need ['KPPA'](enum@KPPA).
234#[derive(Debug, Default, Clone, Copy, PartialEq)]
235#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
236pub enum ParityAndStopBit {
237    #[default]
238    /// no parity, one stop bit
239    NoParityOneStopBit,
240
241    /// even parity, one stop bit
242    EvenParityOneStopBit,
243
244    /// odd parity, one stop bit
245    OddParityOneStopBit,
246
247    /// no parity, two stop bits
248    NoParityTwoStopBits,
249}
250impl ModbusParam for ParityAndStopBit {
251    type ProtocolType = f32;
252    const ADDRESS: u16 = 0x0012;
253    const QUANTITY: u16 = 2;
254}
255impl ParityAndStopBit {
256    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
257        let val = words_to_protocol_value!(words)?;
258        match val {
259            0.0 => Ok(Self::NoParityOneStopBit),
260            1.0 => Ok(Self::EvenParityOneStopBit),
261            2.0 => Ok(Self::OddParityOneStopBit),
262            3.0 => Ok(Self::NoParityTwoStopBits),
263            _ => Err(Error::InvalidValue),
264        }
265    }
266
267    pub fn encode_for_write_registers(&self) -> Vec<Word> {
268        let val = match self {
269            Self::NoParityOneStopBit => 0,
270            Self::EvenParityOneStopBit => 1,
271            Self::OddParityOneStopBit => 2,
272            Self::NoParityTwoStopBits => 3,
273        } as <Self as ModbusParam>::ProtocolType;
274        protocol_value_to_words!(val)
275    }
276}
277impl std::fmt::Display for ParityAndStopBit {
278    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279        match self {
280            Self::NoParityOneStopBit => write!(f, "no parity one stop bit"),
281            Self::EvenParityOneStopBit => write!(f, "even parity one stop bit"),
282            Self::OddParityOneStopBit => write!(f, "odd parity one stop bit"),
283            Self::NoParityTwoStopBits => write!(f, "no parity two stop bit"),
284        }
285    }
286}
287
288/// Address of the Modbus RTU protocol for the RS485 serial port.
289/// The address must be in the range from 1 to 247.
290///
291/// Note: To set the value you need ['KPPA'](enum@KPPA).
292#[derive(Debug, Clone, Copy, PartialEq)]
293#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
294pub struct Address(u8);
295impl ModbusParam for Address {
296    type ProtocolType = f32;
297    const ADDRESS: u16 = 0x0014;
298    const QUANTITY: u16 = 2;
299}
300impl std::ops::Deref for Address {
301    type Target = u8;
302    fn deref(&self) -> &Self::Target {
303        &self.0
304    }
305}
306impl Default for Address {
307    fn default() -> Self {
308        Self(0x01)
309    }
310}
311impl Address {
312    pub const MIN: u8 = 1;
313    pub const MAX: u8 = 247;
314
315    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
316        let val = words_to_protocol_value!(words)?;
317        Ok(Self(val as u8))
318    }
319
320    pub fn encode_for_write_registers(&self) -> Vec<Word> {
321        let val = self.0 as <Self as ModbusParam>::ProtocolType;
322        protocol_value_to_words!(val)
323    }
324}
325impl TryFrom<u8> for Address {
326    type Error = Error;
327
328    fn try_from(value: u8) -> Result<Self, Self::Error> {
329        if (Self::MIN..=Self::MAX).contains(&value) {
330            Ok(Self(value))
331        } else {
332            Err(Error::AddressOutOfRange(value))
333        }
334    }
335}
336impl std::fmt::Display for Address {
337    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338        write!(f, "{:#04x}", self.0)
339    }
340}
341
342/// Pulse constant for the pulse output in impulses per kilo watt hour.
343///
344/// Note: If pulse constant is 1000 imp/kWh, then the pulse width is fixed to 35ms and cannot be adjusted!
345#[derive(Debug, Default, Clone, Copy, PartialEq)]
346#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
347pub enum PulseConstant {
348    #[default]
349    /// 1000 imp/kWh
350    PC1000,
351
352    /// 100 imp/kWh
353    PC100,
354
355    /// 10 imp/kWh
356    PC10,
357
358    /// 1 imp/kWh
359    PC1,
360}
361impl ModbusParam for PulseConstant {
362    type ProtocolType = f32;
363    const ADDRESS: u16 = 0x0016;
364    const QUANTITY: u16 = 2;
365}
366impl PulseConstant {
367    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
368        let val = words_to_protocol_value!(words)?;
369        match val {
370            0.0 => Ok(Self::PC1000),
371            1.0 => Ok(Self::PC100),
372            2.0 => Ok(Self::PC10),
373            3.0 => Ok(Self::PC1),
374            _ => Err(Error::InvalidValue),
375        }
376    }
377
378    pub fn encode_for_write_registers(&self) -> Vec<Word> {
379        let val = match self {
380            Self::PC1000 => 0,
381            Self::PC100 => 1,
382            Self::PC10 => 2,
383            Self::PC1 => 3,
384        } as <Self as ModbusParam>::ProtocolType;
385        protocol_value_to_words!(val)
386    }
387}
388impl std::fmt::Display for PulseConstant {
389    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
390        match self {
391            Self::PC1000 => write!(f, "1000 imp/kWh"),
392            Self::PC100 => write!(f, "100 imp/kWh"),
393            Self::PC10 => write!(f, "10 imp/kWh"),
394            Self::PC1 => write!(f, "1 imp/kWh"),
395        }
396    }
397}
398
399/// Password must be in the range from 0 to 9999.
400///
401/// Note: To set the value you need ['KPPA'](enum@KPPA).
402#[derive(Debug, Clone, Copy, PartialEq)]
403#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
404pub struct Password(u16);
405impl ModbusParam for Password {
406    type ProtocolType = f32;
407    const ADDRESS: u16 = 0x0018;
408    const QUANTITY: u16 = 2;
409}
410impl std::ops::Deref for Password {
411    type Target = u16;
412    fn deref(&self) -> &Self::Target {
413        &self.0
414    }
415}
416impl Default for Password {
417    fn default() -> Self {
418        Self(1000)
419    }
420}
421impl Password {
422    pub const MIN: u16 = 0;
423    pub const MAX: u16 = 9999;
424
425    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
426        let val = words_to_protocol_value!(words)?;
427        Ok(Self(val as u16))
428    }
429
430    pub fn encode_for_write_registers(&self) -> Vec<Word> {
431        let val = self.0 as <Self as ModbusParam>::ProtocolType;
432        protocol_value_to_words!(val)
433    }
434}
435impl TryFrom<u16> for Password {
436    type Error = Error;
437
438    fn try_from(value: u16) -> Result<Self, Self::Error> {
439        if (Self::MIN..=Self::MAX).contains(&value) {
440            Ok(Self(value))
441        } else {
442            Err(Error::PasswordOutOfRange(value))
443        }
444    }
445}
446impl std::fmt::Display for Password {
447    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448        write!(f, "{:04}", self.0)
449    }
450}
451
452/// Baud rate of the Modbus RTU protocol for the RS485 serial port.
453/// Supported rates are: 1200, 2400, 4800, 9600, 19200
454///
455/// Note: To set the value you need ['KPPA'](enum@KPPA).
456#[derive(Debug, Default, Clone, Copy, PartialEq)]
457#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
458pub enum BaudRate {
459    B1200,
460    B2400,
461    B4800,
462    #[default]
463    B9600,
464    B19200,
465}
466impl ModbusParam for BaudRate {
467    type ProtocolType = f32;
468    const ADDRESS: u16 = 0x001C;
469    const QUANTITY: u16 = 2;
470}
471impl BaudRate {
472    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
473        let val = words_to_protocol_value!(words)?;
474        match val {
475            5.0 => Ok(Self::B1200),
476            0.0 => Ok(Self::B2400),
477            1.0 => Ok(Self::B4800),
478            2.0 => Ok(Self::B9600),
479            3.0 => Ok(Self::B19200),
480            _ => Err(Error::InvalidValue),
481        }
482    }
483
484    pub fn encode_for_write_registers(&self) -> Vec<Word> {
485        let val = match self {
486            Self::B1200 => 5,
487            Self::B2400 => 0,
488            Self::B4800 => 1,
489            Self::B9600 => 2,
490            Self::B19200 => 3,
491        } as <Self as ModbusParam>::ProtocolType;
492        protocol_value_to_words!(val)
493    }
494
495    pub fn decode(words: &[Word]) -> Result<u16, Error> {
496        let val = words_to_protocol_value!(words)?;
497        Ok(val as u16)
498    }
499}
500impl TryFrom<u16> for BaudRate {
501    type Error = Error;
502
503    fn try_from(value: u16) -> Result<Self, Self::Error> {
504        match value {
505            1200 => Ok(BaudRate::B1200),
506            2400 => Ok(BaudRate::B2400),
507            4800 => Ok(BaudRate::B4800),
508            9600 => Ok(BaudRate::B9600),
509            19200 => Ok(BaudRate::B19200),
510            _ => Err(Error::InvalidBaudRate),
511        }
512    }
513}
514impl From<&BaudRate> for u16 {
515    fn from(baud_rate: &BaudRate) -> u16 {
516        match baud_rate {
517            BaudRate::B1200 => 1200,
518            BaudRate::B2400 => 2400,
519            BaudRate::B4800 => 4800,
520            BaudRate::B9600 => 9600,
521            BaudRate::B19200 => 19200,
522        }
523    }
524}
525impl std::fmt::Display for BaudRate {
526    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
527        write!(f, "{}", u16::from(self))
528    }
529}
530
531/// Automatic display scroll time in seconds.
532/// The time must be in the range from 0 to 60.
533///
534/// Note: To set the value you need ['KPPA'](enum@KPPA).
535#[derive(Debug, Clone, Copy, PartialEq)]
536#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
537pub struct AutoScrollTime(u8);
538impl ModbusParam for AutoScrollTime {
539    type ProtocolType = f32;
540    const ADDRESS: u16 = 0x003A;
541    const QUANTITY: u16 = 2;
542}
543impl Default for AutoScrollTime {
544    fn default() -> Self {
545        Self(5)
546    }
547}
548impl std::ops::Deref for AutoScrollTime {
549    type Target = u8;
550    fn deref(&self) -> &Self::Target {
551        &self.0
552    }
553}
554impl AutoScrollTime {
555    pub const MIN: u8 = 0;
556    pub const MAX: u8 = 60;
557
558    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
559        let val = words_to_protocol_value!(words)?;
560        Ok(Self(val as u8))
561    }
562
563    pub fn encode_for_write_registers(&self) -> Vec<Word> {
564        let val = self.0 as <Self as ModbusParam>::ProtocolType;
565        protocol_value_to_words!(val)
566    }
567
568    pub fn decode(words: &[Word]) -> Result<u8, Error> {
569        let val = words_to_protocol_value!(words)?;
570        Ok(val as u8)
571    }
572}
573impl TryFrom<u8> for AutoScrollTime {
574    type Error = Error;
575
576    fn try_from(value: u8) -> Result<Self, Self::Error> {
577        if (Self::MIN..=Self::MAX).contains(&value) {
578            Ok(Self(value))
579        } else {
580            Err(Error::AutoScrollTimeOutOfRange(value))
581        }
582    }
583}
584impl std::fmt::Display for AutoScrollTime {
585    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
586        write!(f, "{} sec", self.0)
587    }
588}
589
590/// Back light time of the display in minutes.
591/// The time must be in the range from 1 to 120.
592///
593/// Note: To set the value you need ['KPPA'](enum@KPPA).
594#[derive(Debug, Clone, Copy, PartialEq)]
595#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
596pub enum BacklightTime {
597    AlwaysOn,
598    AlwaysOff,
599    Delayed(u8),
600}
601impl ModbusParam for BacklightTime {
602    type ProtocolType = f32;
603    const ADDRESS: u16 = 0x003C;
604    const QUANTITY: u16 = 2;
605}
606impl Default for BacklightTime {
607    fn default() -> Self {
608        Self::Delayed(60)
609    }
610}
611impl BacklightTime {
612    pub const MIN: u8 = 1;
613    pub const MAX: u8 = 120;
614
615    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
616        let val = words_to_protocol_value!(words)?;
617        match val {
618            0.0 => Ok(Self::AlwaysOn),
619            121.0 => Ok(Self::AlwaysOff),
620            _ => Ok(Self::Delayed(val as u8)),
621        }
622    }
623
624    pub fn encode_for_write_registers(&self) -> Vec<Word> {
625        let val = match self {
626            Self::AlwaysOn => 0,
627            Self::AlwaysOff => 121,
628            Self::Delayed(val) => *val,
629        } as <Self as ModbusParam>::ProtocolType;
630        protocol_value_to_words!(val)
631    }
632}
633impl TryFrom<u8> for BacklightTime {
634    type Error = Error;
635
636    fn try_from(value: u8) -> Result<Self, Self::Error> {
637        if (Self::MIN..=Self::MAX).contains(&value) {
638            Ok(Self::Delayed(value))
639        } else if value == 0 {
640            Ok(Self::AlwaysOn)
641        } else if value == 121 {
642            Ok(Self::AlwaysOff)
643        } else {
644            Err(Error::BacklitTimeOutOfRange(value))
645        }
646    }
647}
648impl std::fmt::Display for BacklightTime {
649    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
650        match self {
651            Self::AlwaysOn => write!(f, "always on"),
652            Self::AlwaysOff => write!(f, "always off"),
653            Self::Delayed(val) => write!(f, "{val} min"),
654        }
655    }
656}
657
658/// Pulse energy type for the pulse output. This is the value that the pulse output returns.
659#[derive(Debug, Default, Clone, Copy, PartialEq)]
660#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
661pub enum PulseEnergyType {
662    ImportActiveEnergy,
663
664    #[default]
665    TotalActiveEnergy,
666
667    ExportActiveEnergy,
668}
669impl ModbusParam for PulseEnergyType {
670    type ProtocolType = f32;
671    const ADDRESS: u16 = 0x0056;
672    const QUANTITY: u16 = 2;
673}
674impl PulseEnergyType {
675    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
676        let val = words_to_protocol_value!(words)?;
677        match val {
678            1.0 => Ok(Self::ImportActiveEnergy),
679            2.0 => Ok(Self::TotalActiveEnergy),
680            4.0 => Ok(Self::ExportActiveEnergy),
681            _ => Err(Error::InvalidValue),
682        }
683    }
684
685    pub fn encode_for_write_registers(&self) -> Vec<Word> {
686        let val = match self {
687            Self::ImportActiveEnergy => 1,
688            Self::TotalActiveEnergy => 2,
689            Self::ExportActiveEnergy => 4,
690        } as <Self as ModbusParam>::ProtocolType;
691        protocol_value_to_words!(val)
692    }
693}
694impl std::fmt::Display for PulseEnergyType {
695    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
696        match self {
697            Self::ImportActiveEnergy => write!(f, "import active energy"),
698            Self::TotalActiveEnergy => write!(f, "total active energy"),
699            Self::ExportActiveEnergy => write!(f, "export active energy"),
700        }
701    }
702}
703
704/// Reset the historical saved data.
705///
706/// Note: To reset the data you need ['KPPA'](enum@KPPA).
707pub struct ResetHistoricalData;
708impl ModbusParam for ResetHistoricalData {
709    type ProtocolType = u16;
710    const ADDRESS: u16 = 0xF010;
711    const QUANTITY: u16 = 1;
712}
713impl ResetHistoricalData {
714    pub fn encode_for_write_registers() -> Vec<Word> {
715        let val = 0x0003 as <Self as ModbusParam>::ProtocolType;
716        protocol_value_to_words!(val)
717    }
718}
719#[derive(Debug, Clone, Copy, PartialEq)]
720#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
721pub struct SerialNumber(u32);
722impl ModbusParam for SerialNumber {
723    type ProtocolType = u32;
724    const ADDRESS: u16 = 0xFC00;
725    const QUANTITY: u16 = 2;
726}
727impl SerialNumber {
728    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
729        let val = words_to_protocol_value!(words)?;
730        Ok(Self(val))
731    }
732}
733impl std::ops::Deref for SerialNumber {
734    type Target = u32;
735    fn deref(&self) -> &Self::Target {
736        &self.0
737    }
738}
739impl std::fmt::Display for SerialNumber {
740    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
741        write!(f, "{}", self.0)
742    }
743}
744
745/// Meter code SDM72D-M-2 = 0089
746#[derive(Debug, Clone, Copy, PartialEq)]
747#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
748pub struct MeterCode(u16);
749impl ModbusParam for MeterCode {
750    type ProtocolType = u16;
751    const ADDRESS: u16 = 0xFC02;
752    const QUANTITY: u16 = 1;
753}
754impl MeterCode {
755    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
756        let val = words_to_protocol_value!(words)?;
757        Ok(Self(val))
758    }
759}
760impl std::ops::Deref for MeterCode {
761    type Target = u16;
762    fn deref(&self) -> &Self::Target {
763        &self.0
764    }
765}
766impl std::fmt::Display for MeterCode {
767    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
768        write!(f, "{:0>4x}", self.0)
769    }
770}
771
772/// The software version showed on display
773#[derive(Debug, Clone, Copy, PartialEq)]
774#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
775pub struct SoftwareVersion(u16);
776impl ModbusParam for SoftwareVersion {
777    type ProtocolType = u16;
778    const ADDRESS: u16 = 0xFC84;
779    const QUANTITY: u16 = 1;
780}
781impl SoftwareVersion {
782    pub fn decode_from_holding_registers(words: &[Word]) -> Result<Self, Error> {
783        let val = words_to_protocol_value!(words)?;
784        Ok(Self(val))
785    }
786}
787impl std::ops::Deref for SoftwareVersion {
788    type Target = u16;
789    fn deref(&self) -> &Self::Target {
790        &self.0
791    }
792}
793impl std::fmt::Display for SoftwareVersion {
794    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
795        write!(f, "{:0>2x}.{:0>2x}", (self.0 >> 8) as u8, self.0 as u8)
796    }
797}
798
799/// A trait for Modbus input registers.
800///
801/// Input registers are used to indicate the present values of the measured and
802/// calculated electrical quantities. Modbus Protocol function code 04 is used to
803/// access all parameters.
804///
805/// Note: Each request for data must be restricted to 30 parameters or less.
806/// Exceeding the 30 parameter limit will cause a Modbus Protocol exception code
807/// to be returned.
808pub trait ModbusInputRegister: ModbusParam {
809    /// Decodes a value from a slice of Modbus input register words.
810    fn decode_from_input_register(words: &[Word]) -> Result<Self, Error>;
811}
812
813fn f32round(val: f32) -> f32 {
814    ((val as f64 * 100.).round() / 100.) as f32
815}
816
817#[cfg(feature = "serde")]
818fn f32ser2<S>(fv: &f32, se: S) -> Result<S::Ok, S::Error>
819where
820    S: serde::Serializer,
821{
822    se.serialize_f32(f32round(*fv))
823}
824
825/// A macro to define a newtype struct for a Modbus input register.
826///
827/// This macro generates a newtype struct that wraps a protocol type (e.g., `f32`)
828/// and implements the `ModbusParam` and `ModbusInputRegister` traits for it.
829/// It also implements `Display` and `Deref`.
830macro_rules! modbus_input_register {
831    ($vis:vis $ty:ident, $address:expr, $quantity:expr, $protocol_type:ty) => {
832        #[derive(Debug, Clone, Copy, PartialEq)]
833        #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
834        $vis struct $ty(
835            #[cfg_attr(feature = "serde", serde(serialize_with = "f32ser2"))]
836            $protocol_type,
837        );
838        impl std::fmt::Display for $ty {
839            fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
840                write!(fmt, "{}", f32round(self.0))
841            }
842        }
843
844        impl ModbusParam for $ty {
845            type ProtocolType = $protocol_type;
846            const ADDRESS: u16 = $address;
847            const QUANTITY: u16 = $quantity;
848        }
849
850        impl std::ops::Deref for $ty {
851            type Target = $protocol_type;
852            fn deref(&self) -> &Self::Target {
853                &self.0
854            }
855        }
856
857        impl $ty {
858            pub fn decode_from_input_register(words: &[Word]) -> Result<Self, Error> {
859                let val = words_to_protocol_value!(words)?;
860                Ok(Self(val as $protocol_type))
861            }
862        }
863    };
864}
865/// A macro to get the range of a register within a larger response slice.
866///
867/// This is used when reading a batch of registers at once.
868#[macro_export]
869macro_rules! get_subset_register_range {
870    ($offset:expr, $register_name:ty) => {{
871        (<$register_name>::ADDRESS - $offset) as usize
872            ..(<$register_name>::ADDRESS - $offset + <$register_name>::QUANTITY) as usize
873    }};
874}
875
876/// A macro to decode a holding register value from a response slice.
877///
878/// This is used when reading a batch of registers at once.
879#[macro_export]
880macro_rules! decode_subset_item_from_holding_register {
881    ($offset:expr, $register_name:ty, $rsp:expr) => {{
882        <$register_name>::decode_from_holding_registers(
883            &$rsp[$crate::get_subset_register_range!($offset, $register_name)],
884        )
885    }};
886}
887
888/// A macro to decode an input register value from a response slice.
889///
890/// This is used when reading a batch of registers at once.
891#[macro_export]
892macro_rules! decode_subset_item_from_input_register {
893    ($offset:expr, $register_name:ty, $rsp:expr) => {{
894        <$register_name>::decode_from_input_register(
895            &$rsp[$crate::get_subset_register_range!($offset, $register_name)],
896        )
897    }};
898}
899
900// 1 Batch
901modbus_input_register!(pub L1Voltage, 0x0000, 2, f32);
902modbus_input_register!(pub L2Voltage, 0x0002, 2, f32);
903modbus_input_register!(pub L3Voltage, 0x0004, 2, f32);
904modbus_input_register!(pub L1Current, 0x0006, 2, f32);
905modbus_input_register!(pub L2Current, 0x0008, 2, f32);
906modbus_input_register!(pub L3Current, 0x000A, 2, f32);
907modbus_input_register!(pub L1PowerActive, 0x000C, 2, f32);
908modbus_input_register!(pub L2PowerActive, 0x000E, 2, f32);
909modbus_input_register!(pub L3PowerActive, 0x0010, 2, f32);
910modbus_input_register!(pub L1PowerApparent, 0x0012, 2, f32);
911modbus_input_register!(pub L2PowerApparent, 0x0014, 2, f32);
912modbus_input_register!(pub L3PowerApparent, 0x0016, 2, f32);
913modbus_input_register!(pub L1PowerReactive, 0x0018, 2, f32);
914modbus_input_register!(pub L2PowerReactive, 0x001A, 2, f32);
915modbus_input_register!(pub L3PowerReactive, 0x001C, 2, f32);
916modbus_input_register!(pub L1PowerFactor, 0x0001E, 2, f32);
917modbus_input_register!(pub L2PowerFactor, 0x0020, 2, f32);
918modbus_input_register!(pub L3PowerFactor, 0x0022, 2, f32);
919modbus_input_register!(pub LtoNAverageVoltage, 0x002A, 2, f32);
920modbus_input_register!(pub LtoNAverageCurrent, 0x002E, 2, f32);
921modbus_input_register!(pub TotalLineCurrent, 0x0030, 2, f32);
922modbus_input_register!(pub TotalPower, 0x0034, 2, f32);
923modbus_input_register!(pub TotalPowerApparent, 0x0038, 2, f32);
924modbus_input_register!(pub TotalPowerReactive, 0x003C, 2, f32);
925modbus_input_register!(pub TotalPowerFactor, 0x003E, 2, f32);
926modbus_input_register!(pub Frequency, 0x0046, 2, f32);
927modbus_input_register!(pub ImportEnergyActive, 0x0048, 2, f32);
928modbus_input_register!(pub ExportEnergyActive, 0x004A, 2, f32);
929// 2 Batch
930modbus_input_register!(pub L1ToL2Voltage, 0x00C8, 2, f32);
931modbus_input_register!(pub L2ToL3Voltage, 0x00CA, 2, f32);
932modbus_input_register!(pub L3ToL1Voltage, 0x00CC, 2, f32);
933modbus_input_register!(pub LtoLAverageVoltage, 0x00CE, 2, f32);
934modbus_input_register!(pub NeutralCurrent, 0x00E0, 2, f32);
935// 3 Batch
936modbus_input_register!(pub TotalEnergyActive, 0x0156, 2, f32);
937modbus_input_register!(pub TotalEnergyReactive, 0x0158, 2, f32);
938modbus_input_register!(pub ResettableTotalEnergyActive, 0x0180, 2, f32);
939modbus_input_register!(pub ResettableTotalEnergyReactive, 0x0182, 2, f32);
940modbus_input_register!(pub ResettableImportEnergyActive, 0x0184, 2, f32);
941modbus_input_register!(pub ResettableExportEnergyActive, 0x0186, 2, f32);
942modbus_input_register!(pub NetKwh, 0x018C, 2, f32);
943modbus_input_register!(pub ImportTotalPowerActive, 0x0500, 2, f32);
944modbus_input_register!(pub ExportTotalPowerActive, 0x0502, 2, f32);