Skip to main content

iot_core/
client.rs

1//! The unified [`IotClient`] async trait — the entry point of the
2//! "thick-unification" layer over MQTT, Modbus, CoAP, and OPC UA.
3//!
4//! Each protocol crate continues to expose its native API (e.g.
5//! `MqttClient::publish`, `ModbusClient::read_holding_registers`) — that is
6//! the recommended API for users who care about per-protocol semantics.
7//! `IotClient` sits *on top* of the native API and is the surface the
8//! `iot-gateway` crate uses to bridge protocols.
9//!
10//! ## AFIT
11//!
12//! This trait uses async-fn-in-trait directly; that requires Rust 1.75+,
13//! which the workspace pins. There is no `async-trait` macro and no boxed
14//! futures in the hot path.
15//!
16//! Trait objects (`dyn IotClient`) are *not* supported by the bare trait —
17//! AFIT methods are not object-safe. The gateway crate provides a thin
18//! `BoxedClient` adapter for that use case.
19
20use crate::error::IotResult;
21use crate::path::PropertyPath;
22use crate::value::Value;
23
24/// One piece of telemetry — the value of a property, plus optional
25/// timestamp / quality metadata captured at the protocol layer.
26#[derive(Debug, Clone, PartialEq)]
27pub struct PropertySample {
28    /// The path the value was read from.
29    pub path: PropertyPath,
30    /// The value itself.
31    pub value: Value,
32    /// Optional protocol-supplied source timestamp, in milliseconds since
33    /// the Unix epoch. `None` for protocols (Modbus, MQTT) that do not
34    /// carry one.
35    pub source_ts_ms: Option<i64>,
36    /// Optional protocol-specific quality code (OPC UA `StatusCode`,
37    /// Modbus exception code, ...). 0 = good.
38    pub quality: u32,
39}
40
41impl PropertySample {
42    /// Construct a fresh sample with no timestamp and quality = 0.
43    pub fn now(path: PropertyPath, value: Value) -> Self {
44        Self {
45            path,
46            value,
47            source_ts_ms: None,
48            quality: 0,
49        }
50    }
51}
52
53/// The unified interface every protocol client implements.
54///
55/// Methods are intentionally narrow: read one path, write one path,
56/// observe one path. Protocols that natively support batch reads (Modbus,
57/// OPC UA `ReadRequest`) expose batching via their *native* API; the
58/// gateway only ever needs one-by-one access.
59pub trait IotClient {
60    /// Connect — must be called before any read / write / observe call.
61    fn connect(&mut self) -> impl core::future::Future<Output = IotResult<()>> + Send;
62
63    /// Cleanly disconnect.
64    fn disconnect(&mut self) -> impl core::future::Future<Output = IotResult<()>> + Send;
65
66    /// Read the current value of a bound property.
67    fn read_property(
68        &mut self,
69        path: &PropertyPath,
70    ) -> impl core::future::Future<Output = IotResult<PropertySample>> + Send;
71
72    /// Write a value to a bound property.
73    fn write_property(
74        &mut self,
75        path: &PropertyPath,
76        value: Value,
77    ) -> impl core::future::Future<Output = IotResult<()>> + Send;
78}
79
80#[cfg(test)]
81mod tests {
82    //! AFIT shape compile-checks: this module exists purely to assert that
83    //! the trait keeps its current shape across edits.
84
85    use super::*;
86    use crate::error::IotError;
87    use crate::path::PropertyPath;
88    use crate::thing::{FeatureId, PropertyId, ThingId};
89
90    struct Dummy;
91    impl IotClient for Dummy {
92        async fn connect(&mut self) -> IotResult<()> {
93            Ok(())
94        }
95        async fn disconnect(&mut self) -> IotResult<()> {
96            Ok(())
97        }
98        async fn read_property(&mut self, _p: &PropertyPath) -> IotResult<PropertySample> {
99            Err(IotError::NotConnected)
100        }
101        async fn write_property(&mut self, _p: &PropertyPath, _v: Value) -> IotResult<()> {
102            Err(IotError::NotConnected)
103        }
104    }
105
106    #[test]
107    fn afit_shape_compiles() {
108        // We do not actually drive futures here — just assert the trait
109        // can be impl'd as written. No tokio dependency needed.
110        let _ = Dummy;
111        let _path = PropertyPath::new(
112            ThingId::new("acme:x"),
113            FeatureId::new("f"),
114            PropertyId::new("p"),
115        );
116    }
117}