fennec_modbus/contrib/mini_qube/
schedule.rs1use core::{
2 fmt::{Display, Formatter},
3 ops::RangeInclusive,
4};
5
6use bytes::{Buf, BufMut};
7
8use crate::{
9 Error,
10 contrib::types::{Percentage, Watts},
11 protocol::{
12 Address,
13 address,
14 codec::{BitSize, Decode, Encode},
15 function::{ReadHoldingRegisters, ReadWriteRegisters, WriteMultipleRegisters},
16 },
17};
18
19pub const N_SLOTS_PER_BLOCK: u16 = 12;
23
24pub const N_BLOCKS: u16 = 8;
26
27pub type Full = [Slot; Slot::N_TOTAL as usize];
32
33pub type Block = [Slot; N_SLOTS_PER_BLOCK as usize];
35
36pub const START_ADDRESS: u16 = 48010;
38
39pub type BlockStride = address::Stride<START_ADDRESS, N_BLOCKS, Block>;
43
44#[must_use]
48#[derive(Copy, Clone)]
49pub struct BlockIndex(pub u16);
50
51impl BlockIndex {
52 pub const LAST: u16 = (N_BLOCKS - 1);
54}
55
56impl Address for BlockIndex {}
57
58impl Encode for BlockIndex {
59 fn encode_to(&self, buf: &mut impl BufMut) {
60 BlockStride::new(self.0).encode_to(buf);
61 }
62}
63
64#[derive(Copy, Clone, Debug, Eq, PartialEq)]
65#[repr(u16)]
66#[must_use]
67pub enum WorkingMode {
68 SelfUse = 1_u16,
72
73 FeedInPriority = 2_u16,
75
76 BackUp = 3_u16,
78
79 PeakShaving = 4_u16,
80
81 ForceCharge = 6_u16,
83
84 ForceDischarge = 7_u16,
86
87 Unknown(u16),
88}
89
90impl BitSize for WorkingMode {
91 const N_BITS: u16 = u16::N_BITS;
92 const N_BYTES: u8 = u16::N_BYTES;
93 const N_WORDS: u16 = u16::N_WORDS;
94}
95
96impl Encode for WorkingMode {
97 fn encode_to(&self, buf: &mut impl BufMut) {
98 buf.put_u16(match self {
99 Self::SelfUse => 1,
100 Self::FeedInPriority => 2,
101 Self::BackUp => 3,
102 Self::PeakShaving => 4,
103 Self::ForceCharge => 6,
104 Self::ForceDischarge => 7,
105 Self::Unknown(working_mode) => *working_mode,
106 });
107 }
108}
109
110impl Decode for WorkingMode {
111 fn decode_from(buf: &mut impl Buf) -> Result<Self, Error> {
112 Ok(match buf.try_get_u16()? {
113 1 => Self::SelfUse,
114 2 => Self::FeedInPriority,
115 3 => Self::BackUp,
116 4 => Self::PeakShaving,
117 6 => Self::ForceCharge,
118 7 => Self::ForceDischarge,
119 working_mode => Self::Unknown(working_mode),
120 })
121 }
122}
123
124#[derive(Copy, Clone, Debug, Eq, PartialEq)]
126#[must_use]
127pub struct NaiveTime {
128 pub hour: u8,
129 pub minute: u8,
130}
131
132impl Display for NaiveTime {
133 fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
134 write!(f, "{:02}:{:02}", self.hour, self.minute)
135 }
136}
137
138impl NaiveTime {
139 pub const MIN: Self = Self { hour: 0, minute: 0 };
141
142 pub const MAX: Self = Self { hour: 23, minute: 59 };
146}
147
148impl BitSize for NaiveTime {
149 const N_BITS: u16 = u8::N_BITS * 2;
150 const N_BYTES: u8 = u8::N_BYTES * 2;
151 const N_WORDS: u16 = 1;
152}
153
154impl Encode for NaiveTime {
155 fn encode_to(&self, buf: &mut impl BufMut) {
156 buf.put_u8(self.hour);
157 buf.put_u8(self.minute);
158 }
159}
160
161impl Decode for NaiveTime {
162 fn decode_from(buf: &mut impl Buf) -> Result<Self, Error> {
163 Ok(Self { hour: buf.try_get_u8()?, minute: buf.try_get_u8()? })
164 }
165}
166
167#[must_use]
171#[derive(Copy, Clone, Eq, PartialEq, Debug)]
172pub struct StateOfChargeRange {
173 pub min: Percentage<u8>,
174 pub max: Percentage<u8>,
175}
176
177impl From<RangeInclusive<Percentage<u8>>> for StateOfChargeRange {
178 fn from(value: RangeInclusive<Percentage<u8>>) -> Self {
179 Self { min: *value.start(), max: *value.end() }
180 }
181}
182
183impl BitSize for StateOfChargeRange {
184 const N_BITS: u16 = u8::N_BITS * 2;
185 const N_BYTES: u8 = u8::N_BYTES * 2;
186 const N_WORDS: u16 = 1;
187}
188
189impl Encode for StateOfChargeRange {
190 fn encode_to(&self, buf: &mut impl BufMut) {
191 buf.put_u8(self.max.0);
192 buf.put_u8(self.min.0);
193 }
194}
195
196impl Decode for StateOfChargeRange {
197 fn decode_from(buf: &mut impl Buf) -> Result<Self, Error> {
198 Ok(Self { max: Percentage(buf.try_get_u8()?), min: Percentage(buf.try_get_u8()?) })
199 }
200}
201
202#[derive(Copy, Clone, Debug, Eq, PartialEq)]
204#[must_use]
205pub struct Slot {
206 pub is_enabled: bool,
207
208 pub start_time: NaiveTime,
210
211 pub end_time: NaiveTime,
216
217 pub working_mode: WorkingMode,
218
219 pub state_of_charge_range: StateOfChargeRange,
220
221 #[allow(clippy::doc_markdown)]
224 pub target_state_of_charge: Percentage<u16>,
225
226 pub power: Watts<u16>,
227
228 pub reserved_1: u16,
230
231 pub reserved_2: u16,
233
234 pub reserved_3: u16,
236}
237
238impl Slot {
239 pub const N_TOTAL: u16 = N_BLOCKS * N_SLOTS_PER_BLOCK;
241}
242
243impl BitSize for Slot {
244 const N_BITS: u16 = 20 * 8;
245}
246
247impl Encode for Slot {
248 fn encode_to(&self, buf: &mut impl BufMut) {
249 buf.put_u16(u16::from(self.is_enabled));
250 self.start_time.encode_to(buf);
251 self.end_time.encode_to(buf);
252 self.working_mode.encode_to(buf);
253 self.state_of_charge_range.encode_to(buf);
254 self.target_state_of_charge.encode_to(buf);
255 self.power.encode_to(buf);
256 self.reserved_1.encode_to(buf);
257 self.reserved_2.encode_to(buf);
258 self.reserved_3.encode_to(buf);
259 }
260}
261
262impl Decode for Slot {
263 fn decode_from(buf: &mut impl Buf) -> Result<Self, Error> {
264 Ok(Self {
265 is_enabled: buf.try_get_u16()? != 0,
266 start_time: NaiveTime::decode_from(buf)?,
267 end_time: NaiveTime::decode_from(buf)?,
268 working_mode: WorkingMode::decode_from(buf)?,
269 state_of_charge_range: StateOfChargeRange::decode_from(buf)?,
270 target_state_of_charge: Percentage::decode_from(buf)?,
271 power: Watts::decode_from(buf)?,
272 reserved_1: u16::decode_from(buf)?,
273 reserved_2: u16::decode_from(buf)?,
274 reserved_3: u16::decode_from(buf)?,
275 })
276 }
277}
278
279pub type ReadSlot =
285 ReadHoldingRegisters<address::Stride<START_ADDRESS, { Slot::N_TOTAL }, Slot>, Slot>;
286
287pub type ReadBlock = ReadHoldingRegisters<BlockIndex, Block>;
289
290pub type WriteSlot =
296 WriteMultipleRegisters<address::Stride<START_ADDRESS, { Slot::N_TOTAL }, Slot>, Slot>;
297
298pub type WriteBlock = WriteMultipleRegisters<BlockIndex, Block>;
300
301pub type ReadWriteBlock = ReadWriteRegisters<BlockIndex, Block, BlockIndex, Block>;