Skip to main content

mabi_core/
device.rs

1//! Device trait and related types.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9
10use crate::error::Result;
11use crate::protocol::Protocol;
12use crate::types::{DataPoint, DataPointDef};
13use crate::value::Value;
14
15/// Device state.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
17#[serde(rename_all = "lowercase")]
18pub enum DeviceState {
19    /// Device is not initialized.
20    #[default]
21    Uninitialized,
22    /// Device is initializing.
23    Initializing,
24    /// Device is online and ready.
25    Online,
26    /// Device is offline.
27    Offline,
28    /// Device has an error.
29    Error,
30    /// Device is shutting down.
31    ShuttingDown,
32}
33
34impl DeviceState {
35    /// Check if device is operational.
36    pub fn is_operational(&self) -> bool {
37        matches!(self, Self::Online)
38    }
39
40    /// Check if device can accept requests.
41    pub fn can_accept_requests(&self) -> bool {
42        matches!(self, Self::Online | Self::Initializing)
43    }
44}
45
46/// Device information.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct DeviceInfo {
49    /// Unique device ID.
50    pub id: String,
51    /// Human-readable name.
52    pub name: String,
53    /// Description.
54    pub description: String,
55    /// Protocol.
56    pub protocol: Protocol,
57    /// Device state.
58    pub state: DeviceState,
59    /// Number of data points.
60    pub point_count: usize,
61    /// Creation time.
62    pub created_at: DateTime<Utc>,
63    /// Last update time.
64    pub updated_at: DateTime<Utc>,
65    /// Custom metadata.
66    #[serde(default)]
67    pub metadata: HashMap<String, String>,
68}
69
70impl DeviceInfo {
71    /// Create new device info.
72    pub fn new(id: impl Into<String>, name: impl Into<String>, protocol: Protocol) -> Self {
73        let now = Utc::now();
74        Self {
75            id: id.into(),
76            name: name.into(),
77            description: String::new(),
78            protocol,
79            state: DeviceState::Uninitialized,
80            point_count: 0,
81            created_at: now,
82            updated_at: now,
83            metadata: HashMap::new(),
84        }
85    }
86
87    /// Set description.
88    pub fn with_description(mut self, description: impl Into<String>) -> Self {
89        self.description = description.into();
90        self
91    }
92
93    /// Add metadata.
94    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
95        self.metadata.insert(key.into(), value.into());
96        self
97    }
98}
99
100/// Core device trait that all protocol devices must implement.
101#[async_trait]
102pub trait Device: Send + Sync {
103    /// Get device information.
104    fn info(&self) -> &DeviceInfo;
105
106    /// Get device ID.
107    fn id(&self) -> &str {
108        &self.info().id
109    }
110
111    /// Get device name.
112    fn name(&self) -> &str {
113        &self.info().name
114    }
115
116    /// Get protocol.
117    fn protocol(&self) -> Protocol {
118        self.info().protocol
119    }
120
121    /// Get device state.
122    fn state(&self) -> DeviceState {
123        self.info().state
124    }
125
126    /// Initialize the device.
127    async fn initialize(&mut self) -> Result<()>;
128
129    /// Start the device.
130    async fn start(&mut self) -> Result<()>;
131
132    /// Stop the device.
133    async fn stop(&mut self) -> Result<()>;
134
135    /// Process one tick (for simulation updates).
136    async fn tick(&mut self) -> Result<()>;
137
138    /// Get all data point definitions.
139    fn point_definitions(&self) -> Vec<&DataPointDef>;
140
141    /// Get a data point definition by ID.
142    fn point_definition(&self, point_id: &str) -> Option<&DataPointDef>;
143
144    /// Read a data point value.
145    async fn read(&self, point_id: &str) -> Result<DataPoint>;
146
147    /// Read multiple data point values.
148    async fn read_multiple(&self, point_ids: &[&str]) -> Result<Vec<DataPoint>> {
149        let mut results = Vec::with_capacity(point_ids.len());
150        for point_id in point_ids {
151            results.push(self.read(point_id).await?);
152        }
153        Ok(results)
154    }
155
156    /// Read all data points.
157    async fn read_all(&self) -> Result<Vec<DataPoint>> {
158        let point_ids: Vec<&str> = self
159            .point_definitions()
160            .iter()
161            .map(|d| d.id.as_str())
162            .collect();
163        self.read_multiple(&point_ids).await
164    }
165
166    /// Write a data point value.
167    async fn write(&mut self, point_id: &str, value: Value) -> Result<()>;
168
169    /// Write multiple data point values.
170    async fn write_multiple(&mut self, values: &[(&str, Value)]) -> Result<()> {
171        for (point_id, value) in values {
172            self.write(point_id, value.clone()).await?;
173        }
174        Ok(())
175    }
176
177    /// Subscribe to value changes (returns a receiver).
178    fn subscribe(&self) -> Option<tokio::sync::broadcast::Receiver<DataPoint>> {
179        None
180    }
181
182    /// Get device statistics.
183    fn statistics(&self) -> DeviceStatistics {
184        DeviceStatistics::default()
185    }
186}
187
188/// Device statistics.
189#[derive(Debug, Clone, Default, Serialize, Deserialize)]
190pub struct DeviceStatistics {
191    /// Total read operations.
192    pub reads_total: u64,
193    /// Total write operations.
194    pub writes_total: u64,
195    /// Read errors.
196    pub read_errors: u64,
197    /// Write errors.
198    pub write_errors: u64,
199    /// Total ticks processed.
200    pub ticks_total: u64,
201    /// Average tick duration in microseconds.
202    pub avg_tick_duration_us: u64,
203    /// Last error message.
204    pub last_error: Option<String>,
205    /// Uptime in seconds.
206    pub uptime_secs: u64,
207}
208
209impl DeviceStatistics {
210    /// Record a successful read.
211    pub fn record_read(&mut self) {
212        self.reads_total += 1;
213    }
214
215    /// Record a read error.
216    pub fn record_read_error(&mut self, error: &str) {
217        self.read_errors += 1;
218        self.last_error = Some(error.to_string());
219    }
220
221    /// Record a successful write.
222    pub fn record_write(&mut self) {
223        self.writes_total += 1;
224    }
225
226    /// Record a write error.
227    pub fn record_write_error(&mut self, error: &str) {
228        self.write_errors += 1;
229        self.last_error = Some(error.to_string());
230    }
231
232    /// Record a tick.
233    pub fn record_tick(&mut self, duration_us: u64) {
234        self.ticks_total += 1;
235        // Moving average
236        self.avg_tick_duration_us =
237            (self.avg_tick_duration_us * (self.ticks_total - 1) + duration_us) / self.ticks_total;
238    }
239}
240
241/// Type alias for a boxed device.
242pub type BoxedDevice = Box<dyn Device>;
243
244/// Type alias for an Arc'd device.
245pub type ArcDevice = Arc<dyn Device>;
246
247/// Device handle for thread-safe access.
248/// Uses tokio::sync::RwLock for async-friendly locking.
249#[derive(Clone)]
250pub struct DeviceHandle {
251    inner: Arc<tokio::sync::RwLock<BoxedDevice>>,
252    /// Cached device info for synchronous access.
253    cached_info: Arc<parking_lot::RwLock<DeviceInfo>>,
254}
255
256impl DeviceHandle {
257    /// Create a new device handle.
258    pub fn new(device: BoxedDevice) -> Self {
259        let info = device.info().clone();
260        Self {
261            inner: Arc::new(tokio::sync::RwLock::new(device)),
262            cached_info: Arc::new(parking_lot::RwLock::new(info)),
263        }
264    }
265
266    /// Get device info (synchronous, returns cached info).
267    pub fn info(&self) -> DeviceInfo {
268        self.cached_info.read().clone()
269    }
270
271    /// Get device ID (synchronous).
272    pub fn id(&self) -> String {
273        self.cached_info.read().id.clone()
274    }
275
276    /// Get device state (synchronous, returns cached state).
277    pub fn state(&self) -> DeviceState {
278        self.cached_info.read().state
279    }
280
281    /// Refresh cached info from the device.
282    pub async fn refresh_info(&self) {
283        let info = self.inner.read().await.info().clone();
284        *self.cached_info.write() = info;
285    }
286
287    /// Initialize the device.
288    pub async fn initialize(&self) -> Result<()> {
289        let result = self.inner.write().await.initialize().await;
290        self.refresh_info().await;
291        result
292    }
293
294    /// Start the device.
295    pub async fn start(&self) -> Result<()> {
296        let result = self.inner.write().await.start().await;
297        self.refresh_info().await;
298        result
299    }
300
301    /// Stop the device.
302    pub async fn stop(&self) -> Result<()> {
303        let result = self.inner.write().await.stop().await;
304        self.refresh_info().await;
305        result
306    }
307
308    /// Process one tick.
309    pub async fn tick(&self) -> Result<()> {
310        self.inner.write().await.tick().await
311    }
312
313    /// Read a data point.
314    pub async fn read(&self, point_id: &str) -> Result<DataPoint> {
315        self.inner.read().await.read(point_id).await
316    }
317
318    /// Write a data point.
319    pub async fn write(&self, point_id: &str, value: Value) -> Result<()> {
320        self.inner.write().await.write(point_id, value).await
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use super::*;
327
328    #[test]
329    fn test_device_state() {
330        assert!(!DeviceState::Uninitialized.is_operational());
331        assert!(DeviceState::Online.is_operational());
332        assert!(DeviceState::Online.can_accept_requests());
333    }
334
335    #[test]
336    fn test_device_info() {
337        let info = DeviceInfo::new("dev-001", "Test Device", Protocol::ModbusTcp)
338            .with_description("A test device")
339            .with_metadata("location", "Building A");
340
341        assert_eq!(info.id, "dev-001");
342        assert_eq!(info.protocol, Protocol::ModbusTcp);
343        assert_eq!(
344            info.metadata.get("location"),
345            Some(&"Building A".to_string())
346        );
347    }
348
349    #[test]
350    fn test_device_statistics() {
351        let mut stats = DeviceStatistics::default();
352        stats.record_read();
353        stats.record_tick(100);
354        stats.record_tick(200);
355
356        assert_eq!(stats.reads_total, 1);
357        assert_eq!(stats.ticks_total, 2);
358        assert_eq!(stats.avg_tick_duration_us, 150);
359    }
360}