Skip to main content

xy_modbus/types/
enums.rs

1//! Wire-encoded status enums (regulation mode, temperature unit,
2//! protection cause, baud-rate code).
3
4use core::fmt;
5
6// ─── RegMode ─────────────────────────────────────────────────────────────────
7
8/// Regulation mode reported by `CVCC` (register 0x0011).
9#[derive(Copy, Clone, Debug, PartialEq, Eq)]
10#[cfg_attr(feature = "defmt", derive(defmt::Format))]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub enum RegMode {
13    ConstantVoltage,
14    ConstantCurrent,
15}
16
17impl RegMode {
18    pub const fn from_reg(v: u16) -> Self {
19        match v {
20            0 => Self::ConstantVoltage,
21            _ => Self::ConstantCurrent,
22        }
23    }
24}
25
26// ─── TempUnit ────────────────────────────────────────────────────────────────
27
28/// Temperature unit selected by `F-C` (register 0x0013).
29#[derive(Copy, Clone, Debug, PartialEq, Eq)]
30#[cfg_attr(feature = "defmt", derive(defmt::Format))]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub enum TempUnit {
33    Celsius,
34    Fahrenheit,
35}
36
37impl TempUnit {
38    pub const fn from_reg(v: u16) -> Self {
39        match v {
40            0 => Self::Celsius,
41            _ => Self::Fahrenheit,
42        }
43    }
44    pub const fn to_reg(self) -> u16 {
45        match self {
46            Self::Celsius => 0,
47            Self::Fahrenheit => 1,
48        }
49    }
50}
51
52// ─── ProtectionStatus ────────────────────────────────────────────────────────
53
54/// Latched protection cause read from `PROTECT` (register 0x0010).
55///
56/// `Normal` (0) is the only non-tripped state. The register stays
57/// latched until written back to 0.
58#[derive(Copy, Clone, Debug, PartialEq, Eq)]
59#[cfg_attr(feature = "defmt", derive(defmt::Format))]
60#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
61pub enum ProtectionStatus {
62    /// Operating normally.
63    Normal,
64    /// Output overvoltage. Also fires transiently when V-SET is raised
65    /// above the current S-OVP threshold — program protection before
66    /// raising V-SET.
67    Ovp,
68    /// Output overcurrent.
69    Ocp,
70    /// Output overpower.
71    Opp,
72    /// Input under-voltage (LVP setpoint).
73    Lvp,
74    /// Cumulative charge limit reached.
75    Oah,
76    /// Output-on time limit reached.
77    Ohp,
78    /// Over-temperature.
79    Otp,
80    /// Cumulative energy (Ah) limit reached.
81    Oep,
82    /// Cumulative energy (Wh) limit reached.
83    Owh,
84    /// Input over-current / inrush.
85    Icp,
86    /// Register read back a value outside the documented 0–10 range.
87    Unknown(u16),
88}
89
90impl ProtectionStatus {
91    pub const fn from_reg(raw: u16) -> Self {
92        match raw {
93            0 => Self::Normal,
94            1 => Self::Ovp,
95            2 => Self::Ocp,
96            3 => Self::Opp,
97            4 => Self::Lvp,
98            5 => Self::Oah,
99            6 => Self::Ohp,
100            7 => Self::Otp,
101            8 => Self::Oep,
102            9 => Self::Owh,
103            10 => Self::Icp,
104            other => Self::Unknown(other),
105        }
106    }
107}
108
109impl fmt::Display for ProtectionStatus {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        f.write_str(match self {
112            Self::Normal => "normal",
113            Self::Ovp => "ovp",
114            Self::Ocp => "ocp",
115            Self::Opp => "opp",
116            Self::Lvp => "lvp",
117            Self::Oah => "oah",
118            Self::Ohp => "ohp",
119            Self::Otp => "otp",
120            Self::Oep => "oep",
121            Self::Owh => "owh",
122            Self::Icp => "icp",
123            Self::Unknown(v) => return write!(f, "unknown({v})"),
124        })
125    }
126}
127
128// ─── BaudRate ────────────────────────────────────────────────────────────────
129
130/// Baud-rate codes for `BAUDRATE_L` (register 0x0019).
131///
132/// Only `B115200` (code 6) is documented in the seller manual; codes
133/// 0–5 and 7–8 are community-derived. Verify on your unit before
134/// committing a write. Baud changes take effect after device reset.
135#[derive(Copy, Clone, Debug, PartialEq, Eq)]
136#[cfg_attr(feature = "defmt", derive(defmt::Format))]
137#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
138pub enum BaudRate {
139    B9600,
140    B14400,
141    B19200,
142    B38400,
143    B56000,
144    B57600,
145    B115200,
146    B2400,
147    B4800,
148    /// Register read back a code outside the documented 0–8 range.
149    Unknown(u16),
150}
151
152impl BaudRate {
153    /// Encoded register value. `Unknown(c)` round-trips its raw code.
154    pub const fn code(self) -> u16 {
155        match self {
156            Self::B9600 => 0,
157            Self::B14400 => 1,
158            Self::B19200 => 2,
159            Self::B38400 => 3,
160            Self::B56000 => 4,
161            Self::B57600 => 5,
162            Self::B115200 => 6,
163            Self::B2400 => 7,
164            Self::B4800 => 8,
165            Self::Unknown(c) => c,
166        }
167    }
168    pub const fn from_code(code: u16) -> Self {
169        match code {
170            0 => Self::B9600,
171            1 => Self::B14400,
172            2 => Self::B19200,
173            3 => Self::B38400,
174            4 => Self::B56000,
175            5 => Self::B57600,
176            6 => Self::B115200,
177            7 => Self::B2400,
178            8 => Self::B4800,
179            c => Self::Unknown(c),
180        }
181    }
182    /// Bits-per-second, or `None` for `Unknown`.
183    pub const fn baud(self) -> Option<u32> {
184        Some(match self {
185            Self::B2400 => 2400,
186            Self::B4800 => 4800,
187            Self::B9600 => 9600,
188            Self::B14400 => 14400,
189            Self::B19200 => 19200,
190            Self::B38400 => 38400,
191            Self::B56000 => 56000,
192            Self::B57600 => 57600,
193            Self::B115200 => 115200,
194            Self::Unknown(_) => return None,
195        })
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    extern crate std;
202    use super::*;
203    use std::format;
204
205    /// Pin every documented protection code (0..=10) plus an out-of-range
206    /// case. A reordering of the match arms in `from_reg` would surface
207    /// here.
208    #[test]
209    fn protection_status_from_reg_full_mapping() {
210        let cases = [
211            (0, ProtectionStatus::Normal),
212            (1, ProtectionStatus::Ovp),
213            (2, ProtectionStatus::Ocp),
214            (3, ProtectionStatus::Opp),
215            (4, ProtectionStatus::Lvp),
216            (5, ProtectionStatus::Oah),
217            (6, ProtectionStatus::Ohp),
218            (7, ProtectionStatus::Otp),
219            (8, ProtectionStatus::Oep),
220            (9, ProtectionStatus::Owh),
221            (10, ProtectionStatus::Icp),
222            (11, ProtectionStatus::Unknown(11)),
223            (0xFFFF, ProtectionStatus::Unknown(0xFFFF)),
224        ];
225        for (raw, expected) in cases {
226            assert_eq!(ProtectionStatus::from_reg(raw), expected);
227        }
228    }
229
230    /// Display strings are part of the public API (used in logs); pin them.
231    #[test]
232    fn protection_status_display_strings() {
233        assert_eq!(format!("{}", ProtectionStatus::Normal), "normal");
234        assert_eq!(format!("{}", ProtectionStatus::Ovp), "ovp");
235        assert_eq!(format!("{}", ProtectionStatus::Icp), "icp");
236        assert_eq!(format!("{}", ProtectionStatus::Unknown(42)), "unknown(42)");
237    }
238
239    /// `code()` and `from_code()` must invert each other across the full
240    /// 0..=8 range, and `Unknown(c)` must round-trip arbitrary codes.
241    /// `baud()` returns the documented bits-per-second.
242    #[test]
243    fn baud_rate_full_table() {
244        let cases = [
245            (0, BaudRate::B9600, 9600),
246            (1, BaudRate::B14400, 14400),
247            (2, BaudRate::B19200, 19200),
248            (3, BaudRate::B38400, 38400),
249            (4, BaudRate::B56000, 56000),
250            (5, BaudRate::B57600, 57600),
251            (6, BaudRate::B115200, 115200),
252            (7, BaudRate::B2400, 2400),
253            (8, BaudRate::B4800, 4800),
254        ];
255        for (code, variant, bps) in cases {
256            assert_eq!(BaudRate::from_code(code), variant);
257            assert_eq!(variant.code(), code);
258            assert_eq!(variant.baud(), Some(bps));
259        }
260        assert_eq!(BaudRate::from_code(99), BaudRate::Unknown(99));
261        assert_eq!(BaudRate::Unknown(99).code(), 99);
262        assert_eq!(BaudRate::Unknown(99).baud(), None);
263    }
264
265    #[test]
266    fn temp_unit_round_trip() {
267        assert_eq!(TempUnit::from_reg(0), TempUnit::Celsius);
268        assert_eq!(TempUnit::from_reg(1), TempUnit::Fahrenheit);
269        // Any nonzero value decodes to Fahrenheit.
270        assert_eq!(TempUnit::from_reg(99), TempUnit::Fahrenheit);
271        assert_eq!(TempUnit::Celsius.to_reg(), 0);
272        assert_eq!(TempUnit::Fahrenheit.to_reg(), 1);
273    }
274}