Skip to main content

moddef_core/
rt.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Runtime support for generated clients (spec §31). `no_std`, allocation
4//! free: generated code (moddef-codegen) drives the codec core over its
5//! `static POINTS` table and calls into these helpers so the emitted method
6//! bodies stay small. Not a stable public API surface — pinned by the
7//! generator, may change with it.
8
9use crate::codec::{decode_raw, mask_for};
10use crate::desc::{AddressSpace, PointDesc};
11use crate::error::Error;
12use crate::transport::Transport;
13use crate::value::Value;
14
15/// Read a point window into a fixed-size register array. Coil/discrete
16/// spaces read a single bit into word 0 (parity with the dynamic facade).
17pub async fn read_regs<T: Transport, const N: usize>(
18    t: &mut T,
19    space: AddressSpace,
20    off: u16,
21) -> Result<[u16; N], Error<T::Error>> {
22    let mut regs = [0u16; N];
23    match space {
24        AddressSpace::HoldingRegister => t
25            .read_holding(off, &mut regs)
26            .await
27            .map_err(Error::Transport)?,
28        AddressSpace::InputRegister => t
29            .read_input(off, &mut regs)
30            .await
31            .map_err(Error::Transport)?,
32        AddressSpace::Coil => {
33            let mut bits = [false];
34            t.read_coils(off, &mut bits)
35                .await
36                .map_err(Error::Transport)?;
37            regs[0] = bits[0] as u16;
38        }
39        AddressSpace::DiscreteInput => {
40            let mut bits = [false];
41            t.read_discrete(off, &mut bits)
42                .await
43                .map_err(Error::Transport)?;
44            regs[0] = bits[0] as u16;
45        }
46    }
47    Ok(regs)
48}
49
50/// Write encoded registers (holding) or a single coil.
51pub async fn write_regs<T: Transport>(
52    t: &mut T,
53    space: AddressSpace,
54    off: u16,
55    regs: &[u16],
56) -> Result<(), Error<T::Error>> {
57    match space {
58        AddressSpace::HoldingRegister => t.write_holding(off, regs).await.map_err(Error::Transport),
59        AddressSpace::Coil => t
60            .write_coil(off, regs.first().is_some_and(|r| *r != 0))
61            .await
62            .map_err(Error::Transport),
63        _ => Err(Error::UnsupportedMapping("cannot write this address space")),
64    }
65}
66
67/// The sentinel meaning for registers that decoded to [`Value::Unavailable`].
68pub fn na_meaning(p: &PointDesc<'static>, regs: &[u16]) -> &'static str {
69    let Ok((raw, bits)) = decode_raw(p, regs) else {
70        return "";
71    };
72    p.na.iter()
73        .find(|na| (na.raw as u64) & mask_for(bits) == raw)
74        .map(|na| na.meaning)
75        .unwrap_or("")
76}
77
78const WRONG_KIND: &str = "decoded value kind does not match the generated signature";
79
80pub fn value_f64<E>(v: Value) -> Result<f64, Error<E>> {
81    v.as_f64().ok_or(Error::UnsupportedMapping(WRONG_KIND))
82}
83
84pub fn value_bool<E>(v: Value) -> Result<bool, Error<E>> {
85    match v {
86        Value::Bool(b) => Ok(b),
87        _ => Err(Error::UnsupportedMapping(WRONG_KIND)),
88    }
89}
90
91pub fn value_u64<E>(v: Value) -> Result<u64, Error<E>> {
92    match v {
93        Value::U64(x) => Ok(x),
94        _ => Err(Error::UnsupportedMapping(WRONG_KIND)),
95    }
96}
97
98pub fn value_i64<E>(v: Value) -> Result<i64, Error<E>> {
99    match v {
100        Value::I64(x) => Ok(x),
101        Value::U64(x) => i64::try_from(x).map_err(|_| Error::UnsupportedMapping(WRONG_KIND)),
102        _ => Err(Error::UnsupportedMapping(WRONG_KIND)),
103    }
104}
105
106/// Raw window of a FLAGS point (feed a generated flags struct).
107pub fn value_flags<E>(v: Value) -> Result<u64, Error<E>> {
108    match v {
109        Value::Flags(m) => Ok(m),
110        _ => Err(Error::UnsupportedMapping(WRONG_KIND)),
111    }
112}
113
114/// Raw window of a packed-fields point (feed a generated field struct).
115pub fn value_fields<E>(v: Value) -> Result<u64, Error<E>> {
116    match v {
117        Value::Fields(w) => Ok(w),
118        _ => Err(Error::UnsupportedMapping(WRONG_KIND)),
119    }
120}
121
122/// Epoch value per the point's datetime encoding.
123pub fn value_datetime<E>(v: Value) -> Result<i64, Error<E>> {
124    match v {
125        Value::DateTime(t) => Ok(t),
126        _ => Err(Error::UnsupportedMapping(WRONG_KIND)),
127    }
128}
129
130/// Integer value of a scale_ref / selector_ref companion point.
131pub fn ref_int<E>(v: Value) -> Result<i64, Error<E>> {
132    v.as_i64().ok_or(Error::UnsupportedMapping(WRONG_KIND))
133}
134
135/// "SunS" marker as two big-endian 16-bit words.
136const SUNS_MARKER: [u16; 2] = [0x5375, 0x6e53];
137
138/// Probe SunSpec anchors, walk the (model_id, length) chain, and return the
139/// offset of the target model's ID register (spec §7.3; generated discovery
140/// blocks cache the result).
141pub async fn resolve_sunspec<T: Transport>(
142    t: &mut T,
143    space: AddressSpace,
144    anchors: &[u16],
145    model_id: u16,
146) -> Result<u16, Error<T::Error>> {
147    async fn header<T: Transport>(
148        t: &mut T,
149        space: AddressSpace,
150        off: u16,
151    ) -> Result<[u16; 2], T::Error> {
152        let mut hdr = [0u16; 2];
153        match space {
154            AddressSpace::InputRegister => t.read_input(off, &mut hdr).await?,
155            _ => t.read_holding(off, &mut hdr).await?,
156        }
157        Ok(hdr)
158    }
159
160    let mut anchor = None;
161    for &a in anchors {
162        // Devices answer exceptions off-anchor; try the next candidate.
163        if let Ok(hdr) = header(t, space, a).await {
164            if hdr == SUNS_MARKER {
165                anchor = Some(a);
166                break;
167            }
168        }
169    }
170    let Some(a) = anchor else {
171        return Err(Error::UnsupportedMapping("SunS marker not found"));
172    };
173
174    let mut off = a + 2;
175    for _ in 0..256 {
176        let hdr = header(t, space, off).await.map_err(Error::Transport)?;
177        if hdr[0] == 0xffff {
178            break;
179        }
180        if hdr[0] == model_id {
181            return Ok(off);
182        }
183        off = off
184            .checked_add(2 + hdr[1])
185            .ok_or(Error::UnsupportedMapping("SunSpec model chain overflows"))?;
186    }
187    Err(Error::UnsupportedMapping("SunSpec model not found"))
188}