Skip to main content

iot_core/
binding.rs

1//! Bridge from the protocol-agnostic [`crate::path::PropertyPath`] to the
2//! concrete address used on the wire.
3//!
4//! Each protocol crate consumes the matching [`ProtocolBinding`] variant
5//! through its `IotClient` impl. The gateway crate (`iot-gateway`) walks a
6//! [`ThingMapping`] to bridge protocols.
7//!
8//! These types are deliberately *value*-shaped: no methods that touch the
9//! network, no async. They serialise cleanly via serde so a deployment can
10//! ship the mapping as TOML/YAML/JSON.
11
12#[cfg(feature = "alloc")]
13use alloc::collections::BTreeMap;
14
15use smol_str::SmolStr;
16
17use crate::path::PropertyPath;
18use crate::thing::ThingId;
19
20/// MQTT QoS level.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23#[repr(u8)]
24pub enum Qos {
25    /// At most once — fire and forget.
26    #[default]
27    AtMostOnce = 0,
28    /// At least once — acknowledged delivery.
29    AtLeastOnce = 1,
30    /// Exactly once — full QoS2 handshake.
31    ExactlyOnce = 2,
32}
33
34/// How an MQTT payload encodes a [`crate::Value`].
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub enum PayloadCodec {
38    /// JSON object `{"value": ...}` — default for cloud bridges.
39    #[default]
40    Json,
41    /// CBOR-encoded value — compact for cellular links.
42    Cbor,
43    /// Raw bytes / utf-8 string — payload IS the value.
44    Raw,
45}
46
47/// Modbus register area.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
50pub enum RegisterArea {
51    /// Read/write single bit (function 0x01 read, 0x05/0x0F write).
52    Coil,
53    /// Read-only bit (function 0x02).
54    DiscreteInput,
55    /// Read/write 16-bit register (function 0x03 read, 0x06/0x10 write).
56    HoldingRegister,
57    /// Read-only 16-bit register (function 0x04).
58    InputRegister,
59}
60
61/// How a multi-register Modbus value is laid out in memory.
62///
63/// Variants describe both endianness *and* word order — Modbus does not
64/// fix this, every vendor does it differently.
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
66#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
67pub enum ByteOrder {
68    /// Big-endian, big-word-first (`AB CD`). Most common.
69    #[default]
70    BigEndian,
71    /// Little-endian, little-word-first (`DC BA`).
72    LittleEndian,
73    /// Mid-big — bytes big-endian, words swapped (`CD AB`).
74    MidBigEndian,
75    /// Mid-little — bytes little-endian, words swapped (`BA DC`).
76    MidLittleEndian,
77}
78
79/// How to interpret one or more registers as a typed value.
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
81#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
82pub enum DataType {
83    /// 16-bit unsigned (1 register).
84    U16,
85    /// 16-bit signed (1 register).
86    I16,
87    /// 32-bit unsigned (2 registers).
88    U32,
89    /// 32-bit signed (2 registers).
90    I32,
91    /// IEEE-754 single (2 registers).
92    F32,
93    /// IEEE-754 double (4 registers).
94    F64,
95    /// `len` bytes, packed two per register, ASCII / UTF-8 — the integer
96    /// is the byte length of the string.
97    String(u16),
98    /// Raw byte blob, `len` bytes (`len/2` registers).
99    Bytes(u16),
100}
101
102/// Content-Format option used by CoAP — RFC 7252 §12.3.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
104#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
105#[repr(u16)]
106pub enum ContentFormat {
107    /// `text/plain;charset=utf-8`.
108    #[default]
109    TextPlain = 0,
110    /// `application/octet-stream`.
111    OctetStream = 42,
112    /// `application/json`.
113    Json = 50,
114    /// `application/cbor`.
115    Cbor = 60,
116}
117
118/// Concrete protocol address backing one [`PropertyPath`].
119///
120/// Each protocol crate handles exactly one variant — the others are
121/// transparent to it. The variants are gated by the corresponding feature
122/// flag in the umbrella crate but are always *defined* here (unconditionally)
123/// so that the gateway can walk a heterogeneous mapping.
124#[derive(Debug, Clone, PartialEq)]
125#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
126#[cfg_attr(feature = "serde", serde(tag = "kind", rename_all = "snake_case"))]
127pub enum ProtocolBinding {
128    /// MQTT topic + QoS + payload encoding.
129    Mqtt {
130        /// Topic to publish reported values on.
131        publish_topic: SmolStr,
132        /// Topic to subscribe to incoming desired values.
133        subscribe_topic: SmolStr,
134        /// QoS level.
135        qos: Qos,
136        /// Payload codec.
137        codec: PayloadCodec,
138    },
139    /// Modbus register address.
140    Modbus {
141        /// Slave / unit identifier.
142        unit_id: u8,
143        /// Coil / discrete-input / holding / input register area.
144        area: RegisterArea,
145        /// Starting register number (zero-based on the wire).
146        address: u16,
147        /// Typed view over the register(s).
148        data_type: DataType,
149        /// Byte / word order.
150        byte_order: ByteOrder,
151    },
152    /// CoAP resource URI.
153    Coap {
154        /// Full `coap://host[:port]/path` URI.
155        uri: SmolStr,
156        /// Content-Format option to send.
157        content_format: ContentFormat,
158        /// Whether the resource supports `Observe` (RFC 7641).
159        observable: bool,
160    },
161    /// OPC UA node — `node_id` is held as the canonical text form
162    /// (`ns=2;s=PumpTemp`). The OPC UA crate parses it lazily.
163    OpcUa {
164        /// Canonical text form NodeId.
165        node_id: SmolStr,
166        /// Attribute id (Value = 13, ...). Default = Value.
167        attribute: u32,
168    },
169}
170
171/// Mapping of every [`PropertyPath`] in one Thing onto its protocol
172/// address.
173///
174/// In `no_std + alloc`, `BTreeMap` is the canonical container; it is
175/// `O(log n)` and deterministic, both of which matter for industrial
176/// devices.
177#[cfg(feature = "alloc")]
178#[derive(Debug, Clone, PartialEq)]
179#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
180pub struct ThingMapping {
181    /// Owning Thing.
182    #[cfg_attr(feature = "serde", serde(rename = "thingId"))]
183    pub thing_id: ThingId,
184    /// Address of every bound property.
185    pub bindings: BTreeMap<PropertyPath, ProtocolBinding>,
186}
187
188#[cfg(feature = "alloc")]
189impl ThingMapping {
190    /// Construct an empty mapping.
191    pub fn new(thing_id: ThingId) -> Self {
192        Self {
193            thing_id,
194            bindings: BTreeMap::new(),
195        }
196    }
197
198    /// Bind one path. Returns the previous binding if any.
199    pub fn bind(
200        &mut self,
201        path: PropertyPath,
202        binding: ProtocolBinding,
203    ) -> Option<ProtocolBinding> {
204        self.bindings.insert(path, binding)
205    }
206
207    /// Look up a binding.
208    pub fn get(&self, path: &PropertyPath) -> Option<&ProtocolBinding> {
209        self.bindings.get(path)
210    }
211}
212
213#[cfg(test)]
214#[cfg(feature = "alloc")]
215mod tests {
216    use super::*;
217    use crate::thing::{FeatureId, PropertyId};
218
219    #[test]
220    fn bind_and_lookup() {
221        let mut m = ThingMapping::new(ThingId::new("acme:pump-7"));
222        let p = PropertyPath::new(
223            ThingId::new("acme:pump-7"),
224            FeatureId::new("temperature"),
225            PropertyId::new("value"),
226        );
227        let b = ProtocolBinding::Modbus {
228            unit_id: 1,
229            area: RegisterArea::HoldingRegister,
230            address: 40001 - 40001, // zero-based on wire
231            data_type: DataType::F32,
232            byte_order: ByteOrder::BigEndian,
233        };
234        assert!(m.bind(p.clone(), b.clone()).is_none());
235        assert_eq!(m.get(&p), Some(&b));
236    }
237}