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_offset: u16,
189        mantissa_words: u8,
190        exponent_offset: u16,
191        exponent_words: u8,
192    },
193}
194
195/// §11.4 Write constraints (engineering units).
196#[derive(Clone, Copy, Debug, Default)]
197pub struct WriteDesc<'a> {
198    pub min: Option<Rational>,
199    pub max: Option<Rational>,
200    pub step: Option<Rational>,
201    pub allowed: &'a [i64],
202}
203
204/// The codec core's complete view of one point.
205#[derive(Clone, Copy, Debug)]
206pub struct PointDesc<'a> {
207    pub id: &'a str,
208    pub space: AddressSpace,
209    pub offset: u16,
210    /// §7.3 SunSpec model-relative offset (ID register = 0). Used when the
211    /// owning block declares discovery.
212    pub model_relative_offset: u16,
213    pub length_words: u8,
214    pub storage: StorageType,
215    pub value: ValueKind<'a>,
216    /// Byte order within a word: big-endian unless false (§9.1).
217    pub byte_big: bool,
218    /// Word order across words: big-endian unless false (§9.2).
219    pub word_big: bool,
220    pub scale: Option<Rational>,
221    pub offset_add: Option<Rational>,
222    pub scale_ref: Option<ScaleRefDesc<'a>>,
223    pub selector: Option<SelectorDesc<'a>>,
224    pub na: &'a [NaDesc<'a>],
225    pub access: Access,
226    pub write: Option<WriteDesc<'a>>,
227}
228
229impl<'a> PointDesc<'a> {
230    /// Register count to read/write for this point.
231    pub const fn words(&self) -> usize {
232        if self.length_words > 0 {
233            self.length_words as usize
234        } else {
235            self.storage.default_words()
236        }
237    }
238
239    pub const fn readable(&self) -> bool {
240        matches!(self.access, Access::ReadOnly | Access::ReadWrite)
241    }
242
243    pub const fn writable(&self) -> bool {
244        matches!(
245            self.access,
246            Access::ReadWrite | Access::WriteOnly | Access::Command
247        )
248    }
249}
250
251/// A minimal defaulted descriptor for building tables/tests concisely.
252pub const fn point(
253    id: &str,
254    space: AddressSpace,
255    offset: u16,
256    storage: StorageType,
257) -> PointDesc<'_> {
258    PointDesc {
259        id,
260        space,
261        offset,
262        model_relative_offset: 0,
263        length_words: 0,
264        storage,
265        value: ValueKind::Decimal,
266        byte_big: true,
267        word_big: true,
268        scale: None,
269        offset_add: None,
270        scale_ref: None,
271        selector: None,
272        na: &[],
273        access: Access::ReadOnly,
274        write: None,
275    }
276}