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}