Skip to main content

moddef_core/
desc.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Lightweight, heap-free point descriptors — the codec core's view of a
4//! point (spec §7–§15). Generated code emits `static` tables of these;
5//! the `alloc` feature converts prost `Point`s into them (see `convert`).
6//!
7//! Lifetimes: `'a` is the backing storage — `'static` for generated tables,
8//! the document's lifetime for runtime-parsed profiles.
9
10/// §7 Modbus address space.
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum AddressSpace {
13    Coil,
14    DiscreteInput,
15    InputRegister,
16    HoldingRegister,
17}
18
19/// §8.2 Physical storage type.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub enum StorageType {
22    /// No storage type declared: width comes from the mapping's length_words,
23    /// unsigned (matches the Go/TS default branch).
24    Unspecified,
25    Bit,
26    U16,
27    S16,
28    U24,
29    U32,
30    S32,
31    U48,
32    S48,
33    U64,
34    S64,
35    F32,
36    F64,
37    StringAscii,
38    StringUtf8,
39    BytesRaw,
40    Bcd,
41    Composed,
42}
43
44impl StorageType {
45    pub const fn bits(self, words: usize) -> u32 {
46        match self {
47            StorageType::Bit => 1,
48            StorageType::U16 | StorageType::S16 => 16,
49            StorageType::U24 => 24,
50            StorageType::U32 | StorageType::S32 => 32,
51            StorageType::U48 | StorageType::S48 => 48,
52            StorageType::U64 | StorageType::S64 => 64,
53            _ => {
54                let w = if words == 0 { 1 } else { words };
55                if w * 16 > 64 {
56                    64
57                } else {
58                    (w * 16) as u32
59                }
60            }
61        }
62    }
63
64    pub const fn signed(self) -> bool {
65        matches!(
66            self,
67            StorageType::S16 | StorageType::S32 | StorageType::S48 | StorageType::S64
68        )
69    }
70
71    /// Default register width when the mapping omits length_words.
72    pub const fn default_words(self) -> usize {
73        match self {
74            StorageType::U32 | StorageType::S32 | StorageType::F32 | StorageType::U24 => 2,
75            StorageType::U48 | StorageType::S48 => 3,
76            StorageType::U64 | StorageType::S64 | StorageType::F64 => 4,
77            _ => 1,
78        }
79    }
80}
81
82/// §11 Access mode.
83#[derive(Clone, Copy, Debug, PartialEq, Eq)]
84pub enum Access {
85    ReadOnly,
86    WriteOnly,
87    ReadWrite,
88    Command,
89}
90
91/// §10.1 Rational scale/offset.
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub struct Rational {
94    pub num: i64,
95    pub den: i64,
96}
97
98/// §10.4 Register-referenced scaling mode.
99#[derive(Clone, Copy, Debug, PartialEq, Eq)]
100pub enum ScaleMode {
101    Pow10,
102    Multiply,
103}
104
105#[derive(Clone, Copy, Debug)]
106pub struct ScaleRefDesc<'a> {
107    pub point_id: &'a str,
108    pub mode: ScaleMode,
109    /// MULTIPLY denominator (0 treated as 1).
110    pub denominator: i64,
111}
112
113/// §10.5 One selector case: scale/offset chosen by the selector value.
114#[derive(Clone, Copy, Debug)]
115pub struct SelectorCaseDesc {
116    pub key: i64,
117    pub scale: Option<Rational>,
118    pub offset: Option<Rational>,
119}
120
121#[derive(Clone, Copy, Debug)]
122pub struct SelectorDesc<'a> {
123    pub point_id: &'a str,
124    pub cases: &'a [SelectorCaseDesc],
125}
126
127/// §8.4 Unavailable/sentinel raw value.
128#[derive(Clone, Copy, Debug)]
129pub struct NaDesc<'a> {
130    pub raw: i64,
131    pub meaning: &'a str,
132}
133
134/// §13 Bit/register sub-field.
135#[derive(Clone, Copy, Debug)]
136pub struct FieldDesc<'a> {
137    pub id: &'a str,
138    pub bit_offset: u32,
139    pub bit_length: u32,
140}
141
142/// §8.5 Date/time encoding.
143#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub enum DateTimeEncoding {
145    EpochSeconds,
146    EpochMillis,
147}
148
149/// §15 String padding / termination.
150#[derive(Clone, Copy, Debug, PartialEq, Eq)]
151pub enum StringPadding {
152    None,
153    Null,
154    Space,
155}
156
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
158pub enum StringTermination {
159    FixedLength,
160    NullTerminated,
161}
162
163/// §8/§13 Logical value interpretation.
164#[derive(Clone, Copy, Debug)]
165pub enum ValueKind<'a> {
166    /// Scaled numeric (§10) — DECIMAL / FLOAT primitives.
167    Decimal,
168    Bool,
169    /// UINT32/UINT64 raw.
170    Uint,
171    /// INT32/INT64 raw.
172    Int,
173    /// Enum-backed: raw integer, mapped by the caller / generated enum.
174    Enum,
175    /// §13.2 flag set: (bit, name).
176    Flags(&'a [(u8, &'a str)]),
177    /// §13/§13.1 packed sub-fields.
178    Fields(&'a [FieldDesc<'a>]),
179    Str {
180        padding: StringPadding,
181        termination: StringTermination,
182    },
183    Bytes,
184    DateTime(DateTimeEncoding),
185    /// §14 mantissa/exponent over the read window.
186    Composed {
187        base: i64,
188        mantissa: ComposedSub,
189        exponent: ComposedSub,
190    },
191}
192
193/// §14 composed sub-mapping (word offset relative to the read window),
194/// optionally carrying a §14.2 bit window — the embedded decade exponent,
195/// where mantissa and exponent share a word (Iskra T5/T6, Eaton PXM).
196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub struct ComposedSub {
198    pub offset: u16,
199    pub words: u8,
200    /// Bit window over the assembled sub-window (LSB = 0).
201    pub bit_offset: u8,
202    /// Bit window length; 0 = no bit window (whole sub-window).
203    pub bit_length: u8,
204    /// Width in bits when no bit window applies (storage or window width).
205    pub width_bits: u8,
206    /// Sign-extend — from bit_length when windowed, else width_bits. True
207    /// when the sub-mapping declares no storage_type (pre-v0.5 behavior).
208    pub signed: bool,
209}
210
211/// §11.4 Write constraints (engineering units).
212#[derive(Clone, Copy, Debug, Default)]
213pub struct WriteDesc<'a> {
214    pub min: Option<Rational>,
215    pub max: Option<Rational>,
216    pub step: Option<Rational>,
217    pub allowed: &'a [i64],
218}
219
220/// The codec core's complete view of one point.
221#[derive(Clone, Copy, Debug)]
222pub struct PointDesc<'a> {
223    pub id: &'a str,
224    pub space: AddressSpace,
225    pub offset: u16,
226    /// §7.3 SunSpec model-relative offset (ID register = 0). Used when the
227    /// owning block declares discovery.
228    pub model_relative_offset: u16,
229    pub length_words: u16,
230    pub storage: StorageType,
231    pub value: ValueKind<'a>,
232    /// Byte order within a word: big-endian unless false (§9.1).
233    pub byte_big: bool,
234    /// Word order across words: big-endian unless false (§9.2).
235    pub word_big: bool,
236    pub scale: Option<Rational>,
237    pub offset_add: Option<Rational>,
238    pub scale_ref: Option<ScaleRefDesc<'a>>,
239    pub selector: Option<SelectorDesc<'a>>,
240    pub na: &'a [NaDesc<'a>],
241    pub access: Access,
242    pub write: Option<WriteDesc<'a>>,
243}
244
245impl<'a> PointDesc<'a> {
246    /// Register count to read/write for this point.
247    pub const fn words(&self) -> usize {
248        if self.length_words > 0 {
249            self.length_words as usize
250        } else {
251            self.storage.default_words()
252        }
253    }
254
255    pub const fn readable(&self) -> bool {
256        matches!(self.access, Access::ReadOnly | Access::ReadWrite)
257    }
258
259    pub const fn writable(&self) -> bool {
260        matches!(
261            self.access,
262            Access::ReadWrite | Access::WriteOnly | Access::Command
263        )
264    }
265}
266
267/// A minimal defaulted descriptor for building tables/tests concisely.
268pub const fn point(
269    id: &str,
270    space: AddressSpace,
271    offset: u16,
272    storage: StorageType,
273) -> PointDesc<'_> {
274    PointDesc {
275        id,
276        space,
277        offset,
278        model_relative_offset: 0,
279        length_words: 0,
280        storage,
281        value: ValueKind::Decimal,
282        byte_big: true,
283        word_big: true,
284        scale: None,
285        offset_add: None,
286        scale_ref: None,
287        selector: None,
288        na: &[],
289        access: Access::ReadOnly,
290        write: None,
291    }
292}