Skip to main content

mabi_core/
capabilities.rs

1//! Protocol capabilities and feature detection.
2//!
3//! This module provides a way to define and query protocol-specific
4//! capabilities, enabling extensible feature detection and runtime
5//! capability negotiation.
6//!
7//! # Example
8//!
9//! ```rust,ignore
10//! use mabi_core::capabilities::{ProtocolCapabilities, Capability};
11//!
12//! // Check if a device supports subscriptions
13//! if device.capabilities().supports(Capability::Subscription) {
14//!     device.subscribe(point_id).await?;
15//! }
16//!
17//! // Get all supported capabilities
18//! for cap in device.capabilities().list() {
19//!     println!("Supports: {:?}", cap);
20//! }
21//! ```
22
23use std::collections::HashSet;
24
25use serde::{Deserialize, Serialize};
26
27use crate::protocol::Protocol;
28
29/// Standard capabilities that protocols may support.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum Capability {
33    // Data Operations
34    /// Single point read operations.
35    Read,
36    /// Single point write operations.
37    Write,
38    /// Batch read operations (multiple points at once).
39    BatchRead,
40    /// Batch write operations (multiple points at once).
41    BatchWrite,
42
43    // Subscription/Notification
44    /// Push-based subscriptions for value changes.
45    Subscription,
46    /// Change of Value (COV) notifications.
47    ChangeOfValue,
48    /// Deadband filtering for subscriptions.
49    Deadband,
50
51    // Historical Data
52    /// Historical data read support.
53    HistoryRead,
54    /// Historical data write support.
55    HistoryWrite,
56    /// Trend logging.
57    TrendLog,
58
59    // Discovery
60    /// Device discovery (broadcast/multicast).
61    Discovery,
62    /// Browse/enumerate data points.
63    Browse,
64
65    // Security
66    /// Authentication support.
67    Authentication,
68    /// Encryption support.
69    Encryption,
70    /// Certificate-based security.
71    Certificates,
72
73    // Advanced Features
74    /// Alarms and events.
75    Alarms,
76    /// Scheduling support.
77    Scheduling,
78    /// Time synchronization.
79    TimeSync,
80    /// Diagnostics and self-test.
81    Diagnostics,
82
83    // Data Types
84    /// Array/sequence data types.
85    ArrayTypes,
86    /// Structure/complex data types.
87    StructTypes,
88    /// Bit string operations.
89    BitStrings,
90
91    // Protocol-Specific
92    /// Modbus function codes 1-4 (basic read).
93    ModbusBasicRead,
94    /// Modbus function codes 5-6, 15-16 (basic write).
95    ModbusBasicWrite,
96    /// OPC UA method calls.
97    OpcUaMethods,
98    /// BACnet services.
99    BacnetServices,
100    /// KNX group communication.
101    KnxGroupComm,
102}
103
104impl Capability {
105    /// Get a human-readable description of this capability.
106    pub fn description(&self) -> &'static str {
107        match self {
108            Self::Read => "Read single data points",
109            Self::Write => "Write single data points",
110            Self::BatchRead => "Read multiple data points at once",
111            Self::BatchWrite => "Write multiple data points at once",
112            Self::Subscription => "Subscribe to value changes",
113            Self::ChangeOfValue => "Change of Value notifications",
114            Self::Deadband => "Deadband filtering for subscriptions",
115            Self::HistoryRead => "Read historical data",
116            Self::HistoryWrite => "Write historical data",
117            Self::TrendLog => "Trend logging support",
118            Self::Discovery => "Device discovery",
119            Self::Browse => "Browse/enumerate data points",
120            Self::Authentication => "Authentication support",
121            Self::Encryption => "Encryption support",
122            Self::Certificates => "Certificate-based security",
123            Self::Alarms => "Alarms and events",
124            Self::Scheduling => "Scheduling support",
125            Self::TimeSync => "Time synchronization",
126            Self::Diagnostics => "Diagnostics and self-test",
127            Self::ArrayTypes => "Array/sequence data types",
128            Self::StructTypes => "Structure/complex data types",
129            Self::BitStrings => "Bit string operations",
130            Self::ModbusBasicRead => "Modbus basic read functions",
131            Self::ModbusBasicWrite => "Modbus basic write functions",
132            Self::OpcUaMethods => "OPC UA method calls",
133            Self::BacnetServices => "BACnet services",
134            Self::KnxGroupComm => "KNX group communication",
135        }
136    }
137
138    /// Check if this is a core capability (expected in most protocols).
139    pub fn is_core(&self) -> bool {
140        matches!(self, Self::Read | Self::Write | Self::Browse)
141    }
142}
143
144/// A set of capabilities supported by a device or protocol.
145#[derive(Debug, Clone, Default, Serialize, Deserialize)]
146pub struct CapabilitySet {
147    capabilities: HashSet<Capability>,
148}
149
150impl CapabilitySet {
151    /// Create an empty capability set.
152    pub fn new() -> Self {
153        Self::default()
154    }
155
156    /// Create a capability set with the given capabilities.
157    pub fn with_capabilities(caps: impl IntoIterator<Item = Capability>) -> Self {
158        Self {
159            capabilities: caps.into_iter().collect(),
160        }
161    }
162
163    /// Add a capability.
164    pub fn add(&mut self, cap: Capability) {
165        self.capabilities.insert(cap);
166    }
167
168    /// Remove a capability.
169    pub fn remove(&mut self, cap: Capability) {
170        self.capabilities.remove(&cap);
171    }
172
173    /// Check if a capability is supported.
174    pub fn supports(&self, cap: Capability) -> bool {
175        self.capabilities.contains(&cap)
176    }
177
178    /// Check if all given capabilities are supported.
179    pub fn supports_all(&self, caps: &[Capability]) -> bool {
180        caps.iter().all(|c| self.supports(*c))
181    }
182
183    /// Check if any of the given capabilities are supported.
184    pub fn supports_any(&self, caps: &[Capability]) -> bool {
185        caps.iter().any(|c| self.supports(*c))
186    }
187
188    /// List all supported capabilities.
189    pub fn list(&self) -> impl Iterator<Item = Capability> + '_ {
190        self.capabilities.iter().copied()
191    }
192
193    /// Get the number of supported capabilities.
194    pub fn len(&self) -> usize {
195        self.capabilities.len()
196    }
197
198    /// Check if empty.
199    pub fn is_empty(&self) -> bool {
200        self.capabilities.is_empty()
201    }
202
203    /// Merge with another capability set.
204    pub fn merge(&mut self, other: &CapabilitySet) {
205        self.capabilities.extend(&other.capabilities);
206    }
207
208    /// Create the intersection of two capability sets.
209    pub fn intersection(&self, other: &CapabilitySet) -> CapabilitySet {
210        CapabilitySet {
211            capabilities: self
212                .capabilities
213                .intersection(&other.capabilities)
214                .copied()
215                .collect(),
216        }
217    }
218}
219
220impl From<Vec<Capability>> for CapabilitySet {
221    fn from(caps: Vec<Capability>) -> Self {
222        Self::with_capabilities(caps)
223    }
224}
225
226impl FromIterator<Capability> for CapabilitySet {
227    fn from_iter<T: IntoIterator<Item = Capability>>(iter: T) -> Self {
228        Self::with_capabilities(iter)
229    }
230}
231
232/// Trait for types that have protocol capabilities.
233pub trait ProtocolCapabilities {
234    /// Get the capability set for this protocol/device.
235    fn capabilities(&self) -> &CapabilitySet;
236
237    /// Check if a specific capability is supported.
238    fn supports(&self, cap: Capability) -> bool {
239        self.capabilities().supports(cap)
240    }
241
242    /// Check if all the given capabilities are supported.
243    fn supports_all(&self, caps: &[Capability]) -> bool {
244        self.capabilities().supports_all(caps)
245    }
246}
247
248/// Get the default capabilities for a protocol.
249pub fn default_capabilities(protocol: Protocol) -> CapabilitySet {
250    match protocol {
251        Protocol::ModbusTcp | Protocol::ModbusRtu => CapabilitySet::with_capabilities([
252            Capability::Read,
253            Capability::Write,
254            Capability::BatchRead,
255            Capability::BatchWrite,
256            Capability::Browse,
257            Capability::ModbusBasicRead,
258            Capability::ModbusBasicWrite,
259            Capability::BitStrings,
260        ]),
261
262        Protocol::OpcUa => CapabilitySet::with_capabilities([
263            Capability::Read,
264            Capability::Write,
265            Capability::BatchRead,
266            Capability::BatchWrite,
267            Capability::Browse,
268            Capability::Subscription,
269            Capability::Deadband,
270            Capability::HistoryRead,
271            Capability::HistoryWrite,
272            Capability::Discovery,
273            Capability::Authentication,
274            Capability::Encryption,
275            Capability::Certificates,
276            Capability::Alarms,
277            Capability::OpcUaMethods,
278            Capability::ArrayTypes,
279            Capability::StructTypes,
280        ]),
281
282        Protocol::BacnetIp => CapabilitySet::with_capabilities([
283            Capability::Read,
284            Capability::Write,
285            Capability::BatchRead,
286            Capability::BatchWrite,
287            Capability::Browse,
288            Capability::ChangeOfValue,
289            Capability::Discovery,
290            Capability::TrendLog,
291            Capability::Alarms,
292            Capability::Scheduling,
293            Capability::TimeSync,
294            Capability::BacnetServices,
295        ]),
296
297        Protocol::KnxIp => CapabilitySet::with_capabilities([
298            Capability::Read,
299            Capability::Write,
300            Capability::Browse,
301            Capability::Discovery,
302            Capability::KnxGroupComm,
303        ]),
304    }
305}
306
307/// Builder for creating custom capability sets.
308#[derive(Debug, Default)]
309pub struct CapabilitySetBuilder {
310    set: CapabilitySet,
311}
312
313impl CapabilitySetBuilder {
314    /// Create a new builder.
315    pub fn new() -> Self {
316        Self::default()
317    }
318
319    /// Start with default capabilities for a protocol.
320    pub fn from_protocol(protocol: Protocol) -> Self {
321        Self {
322            set: default_capabilities(protocol),
323        }
324    }
325
326    /// Add a capability.
327    #[allow(clippy::should_implement_trait)]
328    pub fn add(mut self, cap: Capability) -> Self {
329        self.set.add(cap);
330        self
331    }
332
333    /// Add multiple capabilities.
334    pub fn add_all(mut self, caps: impl IntoIterator<Item = Capability>) -> Self {
335        for cap in caps {
336            self.set.add(cap);
337        }
338        self
339    }
340
341    /// Remove a capability.
342    pub fn remove(mut self, cap: Capability) -> Self {
343        self.set.remove(cap);
344        self
345    }
346
347    /// Add core capabilities (Read, Write, Browse).
348    pub fn with_core(self) -> Self {
349        self.add(Capability::Read)
350            .add(Capability::Write)
351            .add(Capability::Browse)
352    }
353
354    /// Add subscription capabilities.
355    pub fn with_subscriptions(self) -> Self {
356        self.add(Capability::Subscription)
357            .add(Capability::ChangeOfValue)
358            .add(Capability::Deadband)
359    }
360
361    /// Add history capabilities.
362    pub fn with_history(self) -> Self {
363        self.add(Capability::HistoryRead)
364            .add(Capability::HistoryWrite)
365            .add(Capability::TrendLog)
366    }
367
368    /// Add security capabilities.
369    pub fn with_security(self) -> Self {
370        self.add(Capability::Authentication)
371            .add(Capability::Encryption)
372            .add(Capability::Certificates)
373    }
374
375    /// Build the capability set.
376    pub fn build(self) -> CapabilitySet {
377        self.set
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384
385    #[test]
386    fn test_capability_set() {
387        let mut set = CapabilitySet::new();
388        set.add(Capability::Read);
389        set.add(Capability::Write);
390
391        assert!(set.supports(Capability::Read));
392        assert!(set.supports(Capability::Write));
393        assert!(!set.supports(Capability::Subscription));
394        assert_eq!(set.len(), 2);
395    }
396
397    #[test]
398    fn test_supports_all_any() {
399        let set = CapabilitySet::with_capabilities([
400            Capability::Read,
401            Capability::Write,
402            Capability::Browse,
403        ]);
404
405        assert!(set.supports_all(&[Capability::Read, Capability::Write]));
406        assert!(!set.supports_all(&[Capability::Read, Capability::Subscription]));
407
408        assert!(set.supports_any(&[Capability::Read, Capability::Subscription]));
409        assert!(!set.supports_any(&[Capability::Subscription, Capability::HistoryRead]));
410    }
411
412    #[test]
413    fn test_default_capabilities() {
414        let modbus_caps = default_capabilities(Protocol::ModbusTcp);
415        assert!(modbus_caps.supports(Capability::Read));
416        assert!(modbus_caps.supports(Capability::ModbusBasicRead));
417        assert!(!modbus_caps.supports(Capability::Subscription));
418
419        let opcua_caps = default_capabilities(Protocol::OpcUa);
420        assert!(opcua_caps.supports(Capability::Subscription));
421        assert!(opcua_caps.supports(Capability::HistoryRead));
422        assert!(opcua_caps.supports(Capability::OpcUaMethods));
423    }
424
425    #[test]
426    fn test_capability_builder() {
427        let set = CapabilitySetBuilder::new()
428            .with_core()
429            .with_subscriptions()
430            .add(Capability::Discovery)
431            .build();
432
433        assert!(set.supports(Capability::Read));
434        assert!(set.supports(Capability::Write));
435        assert!(set.supports(Capability::Browse));
436        assert!(set.supports(Capability::Subscription));
437        assert!(set.supports(Capability::Discovery));
438    }
439
440    #[test]
441    fn test_capability_builder_from_protocol() {
442        let set = CapabilitySetBuilder::from_protocol(Protocol::ModbusTcp)
443            .remove(Capability::Write) // Remove write capability
444            .add(Capability::Subscription) // Add custom capability
445            .build();
446
447        assert!(set.supports(Capability::Read));
448        assert!(!set.supports(Capability::Write));
449        assert!(set.supports(Capability::Subscription));
450    }
451
452    #[test]
453    fn test_intersection() {
454        let set1 = CapabilitySet::with_capabilities([
455            Capability::Read,
456            Capability::Write,
457            Capability::Browse,
458        ]);
459
460        let set2 = CapabilitySet::with_capabilities([
461            Capability::Read,
462            Capability::Browse,
463            Capability::Subscription,
464        ]);
465
466        let intersection = set1.intersection(&set2);
467        assert!(intersection.supports(Capability::Read));
468        assert!(intersection.supports(Capability::Browse));
469        assert!(!intersection.supports(Capability::Write));
470        assert!(!intersection.supports(Capability::Subscription));
471    }
472
473    #[test]
474    fn test_capability_description() {
475        assert_eq!(Capability::Read.description(), "Read single data points");
476        assert!(Capability::Read.is_core());
477        assert!(!Capability::Subscription.is_core());
478    }
479}