iot-core 0.0.1

Core types for the iot-protocols SDK: Thing model, IotClient trait, errors, paths, protocol bindings.
Documentation
//! The unified [`IotClient`] async trait — the entry point of the
//! "thick-unification" layer over MQTT, Modbus, CoAP, and OPC UA.
//!
//! Each protocol crate continues to expose its native API (e.g.
//! `MqttClient::publish`, `ModbusClient::read_holding_registers`) — that is
//! the recommended API for users who care about per-protocol semantics.
//! `IotClient` sits *on top* of the native API and is the surface the
//! `iot-gateway` crate uses to bridge protocols.
//!
//! ## AFIT
//!
//! This trait uses async-fn-in-trait directly; that requires Rust 1.75+,
//! which the workspace pins. There is no `async-trait` macro and no boxed
//! futures in the hot path.
//!
//! Trait objects (`dyn IotClient`) are *not* supported by the bare trait —
//! AFIT methods are not object-safe. The gateway crate provides a thin
//! `BoxedClient` adapter for that use case.

use crate::error::IotResult;
use crate::path::PropertyPath;
use crate::value::Value;

/// One piece of telemetry — the value of a property, plus optional
/// timestamp / quality metadata captured at the protocol layer.
#[derive(Debug, Clone, PartialEq)]
pub struct PropertySample {
    /// The path the value was read from.
    pub path: PropertyPath,
    /// The value itself.
    pub value: Value,
    /// Optional protocol-supplied source timestamp, in milliseconds since
    /// the Unix epoch. `None` for protocols (Modbus, MQTT) that do not
    /// carry one.
    pub source_ts_ms: Option<i64>,
    /// Optional protocol-specific quality code (OPC UA `StatusCode`,
    /// Modbus exception code, ...). 0 = good.
    pub quality: u32,
}

impl PropertySample {
    /// Construct a fresh sample with no timestamp and quality = 0.
    pub fn now(path: PropertyPath, value: Value) -> Self {
        Self {
            path,
            value,
            source_ts_ms: None,
            quality: 0,
        }
    }
}

/// The unified interface every protocol client implements.
///
/// Methods are intentionally narrow: read one path, write one path,
/// observe one path. Protocols that natively support batch reads (Modbus,
/// OPC UA `ReadRequest`) expose batching via their *native* API; the
/// gateway only ever needs one-by-one access.
pub trait IotClient {
    /// Connect — must be called before any read / write / observe call.
    fn connect(&mut self) -> impl core::future::Future<Output = IotResult<()>> + Send;

    /// Cleanly disconnect.
    fn disconnect(&mut self) -> impl core::future::Future<Output = IotResult<()>> + Send;

    /// Read the current value of a bound property.
    fn read_property(
        &mut self,
        path: &PropertyPath,
    ) -> impl core::future::Future<Output = IotResult<PropertySample>> + Send;

    /// Write a value to a bound property.
    fn write_property(
        &mut self,
        path: &PropertyPath,
        value: Value,
    ) -> impl core::future::Future<Output = IotResult<()>> + Send;
}

#[cfg(test)]
mod tests {
    //! AFIT shape compile-checks: this module exists purely to assert that
    //! the trait keeps its current shape across edits.

    use super::*;
    use crate::error::IotError;
    use crate::path::PropertyPath;
    use crate::thing::{FeatureId, PropertyId, ThingId};

    struct Dummy;
    impl IotClient for Dummy {
        async fn connect(&mut self) -> IotResult<()> {
            Ok(())
        }
        async fn disconnect(&mut self) -> IotResult<()> {
            Ok(())
        }
        async fn read_property(&mut self, _p: &PropertyPath) -> IotResult<PropertySample> {
            Err(IotError::NotConnected)
        }
        async fn write_property(&mut self, _p: &PropertyPath, _v: Value) -> IotResult<()> {
            Err(IotError::NotConnected)
        }
    }

    #[test]
    fn afit_shape_compiles() {
        // We do not actually drive futures here — just assert the trait
        // can be impl'd as written. No tokio dependency needed.
        let _ = Dummy;
        let _path = PropertyPath::new(
            ThingId::new("acme:x"),
            FeatureId::new("f"),
            PropertyId::new("p"),
        );
    }
}