Skip to main content

moddef_core/
convert.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! prost [`schema::Point`] → [`PointDesc`] conversion (`alloc`). The codec
4//! core is heap-free, so descriptors borrow: strings and `allowed_values`
5//! borrow the prost message directly; flag/field/case/na tables need owned
6//! side-buffers ([`DescBufs`]) because their layout differs from the wire
7//! form. Two-step use:
8//!
9//! ```ignore
10//! let bufs = desc_bufs(point);
11//! let desc = point_desc(point, block.space(), &bufs);
12//! ```
13//!
14//! Kind/precedence decisions mirror go/codec and moddef-ts `decodePoint`:
15//! composed > flags > fields > string/bytes > primitive (default: raw enum
16//! integer, signed if the storage is).
17
18use alloc::vec::Vec;
19
20use crate::desc::{
21    Access, AddressSpace, DateTimeEncoding, FieldDesc, NaDesc, PointDesc, Rational, ScaleMode,
22    ScaleRefDesc, SelectorCaseDesc, SelectorDesc, StorageType, StringPadding, StringTermination,
23    ValueKind, WriteDesc,
24};
25use crate::schema;
26
27/// Owned side-tables backing one [`PointDesc`] view. Strings inside borrow
28/// the prost message, so this lives exactly as long as the borrow chain.
29#[derive(Debug, Default)]
30pub struct DescBufs<'a> {
31    flags: Vec<(u8, &'a str)>,
32    fields: Vec<FieldDesc<'a>>,
33    cases: Vec<SelectorCaseDesc>,
34    na: Vec<NaDesc<'a>>,
35}
36
37/// Build the side-buffers for `p` (step 1 of 2).
38pub fn desc_bufs(p: &schema::Point) -> DescBufs<'_> {
39    let mut b = DescBufs::default();
40
41    if let Some(schema::value_type::Kind::Flags(fl)) =
42        p.value_type.as_ref().and_then(|v| v.kind.as_ref())
43    {
44        // BTreeMap iterates ascending — same order the TS decoder sorts into.
45        for (bit, name) in &fl.bits {
46            b.flags.push((*bit as u8, name.as_str()));
47        }
48    }
49
50    // Legacy bit_fields first, then §13.1 fields (TS decodeFields order).
51    for f in &p.bit_fields {
52        b.fields.push(FieldDesc {
53            id: &f.field_id,
54            bit_offset: f.bit_offset,
55            bit_length: f.bit_length,
56        });
57    }
58    for f in &p.fields {
59        b.fields.push(FieldDesc {
60            id: &f.field_id,
61            bit_offset: f.bit_offset,
62            bit_length: f.bit_length,
63        });
64    }
65
66    if let Some(sel) = &p.selector_ref {
67        for (key, c) in &sel.cases {
68            b.cases.push(SelectorCaseDesc {
69                key: *key,
70                scale: c.scale.as_ref().map(rational),
71                offset: c.offset.as_ref().map(rational),
72            });
73        }
74    }
75
76    for na in &p.na_values {
77        b.na.push(NaDesc {
78            raw: na.raw,
79            meaning: &na.meaning,
80        });
81    }
82
83    b
84}
85
86/// Build the descriptor view for `p` (step 2 of 2). `block_space` is the
87/// owning block's address space, used when the mapping leaves it unspecified.
88pub fn point_desc<'a>(
89    p: &'a schema::Point,
90    block_space: schema::AddressSpace,
91    bufs: &'a DescBufs<'a>,
92) -> PointDesc<'a> {
93    let m = p.mapping.as_ref();
94    let t = p.transform.as_ref();
95
96    let space = m
97        .map(|m| m.space())
98        .filter(|s| *s != schema::AddressSpace::Unspecified)
99        .unwrap_or(block_space);
100
101    let storage = storage_type(p.storage_type());
102
103    PointDesc {
104        id: &p.point_id,
105        space: address_space(space),
106        offset: m.map(|m| m.offset as u16).unwrap_or(0),
107        model_relative_offset: m.map(|m| m.model_relative_offset as u16).unwrap_or(0),
108        length_words: m.map(|m| m.length_words as u8).unwrap_or(0),
109        storage,
110        value: value_kind(p, storage, bufs),
111        byte_big: m
112            .map(|m| m.byte_order() != schema::ByteOrder::LittleEndian)
113            .unwrap_or(true),
114        word_big: m
115            .map(|m| m.word_order() != schema::WordOrder::WordLittleEndian)
116            .unwrap_or(true),
117        scale: t.and_then(|t| t.scale.as_ref()).map(rational),
118        offset_add: t.and_then(|t| t.offset.as_ref()).map(rational),
119        scale_ref: t.and_then(|t| t.scale_ref.as_ref()).map(|sr| ScaleRefDesc {
120            point_id: &sr.point_id,
121            mode: if sr.mode() == schema::ScaleMode::Multiply {
122                ScaleMode::Multiply
123            } else {
124                ScaleMode::Pow10
125            },
126            denominator: sr.denominator,
127        }),
128        selector: p.selector_ref.as_ref().map(|sel| SelectorDesc {
129            point_id: &sel.point_id,
130            cases: &bufs.cases,
131        }),
132        na: &bufs.na,
133        access: match p.access() {
134            schema::AccessMode::WriteOnly => Access::WriteOnly,
135            schema::AccessMode::ReadWrite => Access::ReadWrite,
136            schema::AccessMode::Command => Access::Command,
137            _ => Access::ReadOnly,
138        },
139        write: p
140            .write
141            .as_ref()
142            .and_then(|w| w.constraints.as_ref())
143            .map(|c| WriteDesc {
144                min: c.min_value.as_ref().map(rational),
145                max: c.max_value.as_ref().map(rational),
146                step: c.step.as_ref().map(rational),
147                allowed: &c.allowed_values,
148            }),
149    }
150}
151
152/// Register count to read/write for `p` (mapping length or storage default).
153pub fn point_words(p: &schema::Point) -> usize {
154    let lw = p.mapping.as_ref().map(|m| m.length_words).unwrap_or(0);
155    if lw > 0 {
156        lw as usize
157    } else {
158        storage_type(p.storage_type()).default_words()
159    }
160}
161
162fn rational(r: &schema::Rational) -> Rational {
163    Rational {
164        num: r.numerator,
165        den: r.denominator,
166    }
167}
168
169fn address_space(s: schema::AddressSpace) -> AddressSpace {
170    match s {
171        schema::AddressSpace::Coil => AddressSpace::Coil,
172        schema::AddressSpace::DiscreteInput => AddressSpace::DiscreteInput,
173        schema::AddressSpace::InputRegister => AddressSpace::InputRegister,
174        _ => AddressSpace::HoldingRegister,
175    }
176}
177
178fn storage_type(s: schema::StorageType) -> StorageType {
179    match s {
180        schema::StorageType::Bit => StorageType::Bit,
181        schema::StorageType::U16 => StorageType::U16,
182        schema::StorageType::S16 => StorageType::S16,
183        schema::StorageType::U24 => StorageType::U24,
184        schema::StorageType::U32 => StorageType::U32,
185        schema::StorageType::S32 => StorageType::S32,
186        schema::StorageType::U48 => StorageType::U48,
187        schema::StorageType::S48 => StorageType::S48,
188        schema::StorageType::U64 => StorageType::U64,
189        schema::StorageType::S64 => StorageType::S64,
190        schema::StorageType::Ieee754F32 => StorageType::F32,
191        schema::StorageType::Ieee754F64 => StorageType::F64,
192        schema::StorageType::StringAscii => StorageType::StringAscii,
193        schema::StorageType::StringUtf8 => StorageType::StringUtf8,
194        schema::StorageType::BytesRaw => StorageType::BytesRaw,
195        schema::StorageType::Bcd => StorageType::Bcd,
196        schema::StorageType::Composed => StorageType::Composed,
197        // FIXED_POINT and unspecified: width from the mapping, unsigned —
198        // the Go/TS default branch.
199        _ => StorageType::Unspecified,
200    }
201}
202
203fn value_kind<'a>(
204    p: &'a schema::Point,
205    storage: StorageType,
206    bufs: &'a DescBufs<'a>,
207) -> ValueKind<'a> {
208    // §14 composed first: from the composed sub-mappings. A missing/zero base
209    // is caught at decode time (ComposedBaseZero), matching Go/TS.
210    if storage == StorageType::Composed || p.mapping.as_ref().is_some_and(|m| m.composed.is_some())
211    {
212        let c = p.mapping.as_ref().and_then(|m| m.composed.as_deref());
213        let sub = |m: Option<&schema::Mapping>| -> (u16, u8) {
214            m.map(|m| {
215                (
216                    m.offset as u16,
217                    if m.length_words == 0 {
218                        1
219                    } else {
220                        m.length_words as u8
221                    },
222                )
223            })
224            .unwrap_or((0, 1))
225        };
226        let (mo, mw) = sub(c.and_then(|c| c.mantissa.as_deref()));
227        let (eo, ew) = sub(c.and_then(|c| c.exponent.as_deref()));
228        return ValueKind::Composed {
229            base: c.map(|c| c.base).unwrap_or(0),
230            mantissa_offset: mo,
231            mantissa_words: mw,
232            exponent_offset: eo,
233            exponent_words: ew,
234        };
235    }
236
237    if !bufs.flags.is_empty()
238        || matches!(
239            p.value_type.as_ref().and_then(|v| v.kind.as_ref()),
240            Some(schema::value_type::Kind::Flags(_))
241        )
242    {
243        return ValueKind::Flags(&bufs.flags);
244    }
245
246    if !bufs.fields.is_empty() {
247        return ValueKind::Fields(&bufs.fields);
248    }
249
250    if storage == StorageType::StringAscii || storage == StorageType::StringUtf8 {
251        let enc = p.mapping.as_ref().and_then(|m| m.string_encoding.as_ref());
252        return ValueKind::Str {
253            padding: match enc.map(|e| e.padding()) {
254                Some(schema::Padding::Null) => StringPadding::Null,
255                Some(schema::Padding::Space) => StringPadding::Space,
256                _ => StringPadding::None,
257            },
258            termination: match enc.map(|e| e.termination()) {
259                Some(schema::Termination::NullTerminated) => StringTermination::NullTerminated,
260                _ => StringTermination::FixedLength,
261            },
262        };
263    }
264    if storage == StorageType::BytesRaw {
265        return ValueKind::Bytes;
266    }
267
268    let prim = match p.value_type.as_ref().and_then(|v| v.kind.as_ref()) {
269        Some(schema::value_type::Kind::Primitive(v)) => {
270            schema::PrimitiveType::try_from(*v).unwrap_or(schema::PrimitiveType::Unspecified)
271        }
272        _ => schema::PrimitiveType::Unspecified,
273    };
274
275    match prim {
276        schema::PrimitiveType::Bool => ValueKind::Bool,
277        schema::PrimitiveType::Datetime => ValueKind::DateTime(
278            if p.datetime.as_ref().map(|d| d.encoding()) == Some(schema::DateTimeEncoding::EpochMs)
279            {
280                DateTimeEncoding::EpochMillis
281            } else {
282                // EPOCH_S and unspecified — the Go/TS default.
283                DateTimeEncoding::EpochSeconds
284            },
285        ),
286        schema::PrimitiveType::Decimal
287        | schema::PrimitiveType::Float32
288        | schema::PrimitiveType::Float64 => ValueKind::Decimal,
289        schema::PrimitiveType::Uint32 | schema::PrimitiveType::Uint64 => ValueKind::Uint,
290        schema::PrimitiveType::Int32 | schema::PrimitiveType::Int64 => ValueKind::Int,
291        // enum_ref / struct_ref / no primitive: raw integer via the enum path.
292        _ => ValueKind::Enum,
293    }
294}