Skip to main content

mabi_modbus/unit/
config.rs

1//! Configuration types for multi-unit management.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6use crate::registers::RegisterStoreConfig;
7use crate::types::WordOrder;
8
9/// Configuration for the MultiUnitManager.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct UnitManagerConfig {
12    /// Maximum number of units that can be managed.
13    ///
14    /// Default: 247 (Modbus max addressable units: 1-247)
15    pub max_units: usize,
16
17    /// Default word order for new units.
18    ///
19    /// Individual units can override this.
20    pub default_word_order: WordOrder,
21
22    /// Broadcast handling mode for Unit ID 0.
23    pub broadcast_mode: BroadcastMode,
24
25    /// Whether to allow creating units on-demand.
26    ///
27    /// If true, accessing a non-existent unit will create it with default config.
28    /// If false, accessing a non-existent unit returns None.
29    pub auto_create_units: bool,
30
31    /// Default register store configuration for new units.
32    #[serde(default)]
33    pub default_register_config: RegisterStoreConfig,
34
35    /// Enable metrics collection per unit.
36    pub enable_unit_metrics: bool,
37}
38
39impl Default for UnitManagerConfig {
40    fn default() -> Self {
41        Self {
42            max_units: 247,
43            default_word_order: WordOrder::BigEndian,
44            broadcast_mode: BroadcastMode::WriteAll,
45            auto_create_units: false,
46            default_register_config: RegisterStoreConfig::default(),
47            enable_unit_metrics: true,
48        }
49    }
50}
51
52impl UnitManagerConfig {
53    /// Create a new config with specified max units.
54    pub fn with_max_units(mut self, max_units: usize) -> Self {
55        self.max_units = max_units;
56        self
57    }
58
59    /// Set the default word order.
60    pub fn with_word_order(mut self, word_order: WordOrder) -> Self {
61        self.default_word_order = word_order;
62        self
63    }
64
65    /// Set the broadcast mode.
66    pub fn with_broadcast_mode(mut self, mode: BroadcastMode) -> Self {
67        self.broadcast_mode = mode;
68        self
69    }
70
71    /// Enable auto-creation of units.
72    pub fn with_auto_create(mut self, auto_create: bool) -> Self {
73        self.auto_create_units = auto_create;
74        self
75    }
76
77    /// Set the default register configuration.
78    pub fn with_register_config(mut self, config: RegisterStoreConfig) -> Self {
79        self.default_register_config = config;
80        self
81    }
82
83    /// Create a config for testing with minimal resources.
84    pub fn for_testing() -> Self {
85        Self {
86            max_units: 10,
87            default_word_order: WordOrder::BigEndian,
88            broadcast_mode: BroadcastMode::WriteAll,
89            auto_create_units: true,
90            default_register_config: RegisterStoreConfig::minimal(),
91            enable_unit_metrics: false,
92        }
93    }
94
95    /// Create a config for large-scale simulation.
96    pub fn for_large_scale() -> Self {
97        Self {
98            max_units: 247,
99            default_word_order: WordOrder::BigEndian,
100            broadcast_mode: BroadcastMode::WriteAll,
101            auto_create_units: false,
102            default_register_config: RegisterStoreConfig::large_scale(),
103            enable_unit_metrics: true,
104        }
105    }
106}
107
108/// Broadcast handling mode for Unit ID 0.
109///
110/// In Modbus, Unit ID 0 is the broadcast address. Different systems
111/// handle broadcasts differently.
112#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
113#[serde(rename_all = "snake_case")]
114pub enum BroadcastMode {
115    /// Broadcast writes are sent to all units.
116    ///
117    /// No response is generated (per Modbus spec).
118    /// This is the most common behavior.
119    WriteAll,
120
121    /// Broadcast writes are ignored.
122    ///
123    /// Some systems disable broadcast for security.
124    Disabled,
125
126    /// Broadcast writes are sent only to units in a specific list.
127    ///
128    /// Useful for selective broadcasting.
129    #[serde(skip)]
130    SelectiveList(Vec<u8>),
131
132    /// Broadcast writes are echoed to a single designated unit.
133    ///
134    /// Some systems route broadcast to a primary controller.
135    EchoToUnit(u8),
136}
137
138impl Default for BroadcastMode {
139    fn default() -> Self {
140        Self::WriteAll
141    }
142}
143
144impl fmt::Display for BroadcastMode {
145    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146        match self {
147            Self::WriteAll => write!(f, "Write to all units"),
148            Self::Disabled => write!(f, "Disabled"),
149            Self::SelectiveList(units) => write!(f, "Selective ({} units)", units.len()),
150            Self::EchoToUnit(id) => write!(f, "Echo to unit {}", id),
151        }
152    }
153}
154
155/// Configuration for a single Modbus unit.
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct UnitConfig {
158    /// Human-readable name for this unit.
159    pub name: String,
160
161    /// Optional description.
162    #[serde(default)]
163    pub description: String,
164
165    /// Word order for this unit (overrides manager default).
166    pub word_order: Option<WordOrder>,
167
168    /// Register store configuration for this unit.
169    #[serde(default)]
170    pub register_config: Option<RegisterStoreConfig>,
171
172    /// Whether this unit responds to broadcasts.
173    #[serde(default = "default_broadcast_enabled")]
174    pub broadcast_enabled: bool,
175
176    /// Response delay in microseconds (for simulation).
177    #[serde(default)]
178    pub response_delay_us: u64,
179
180    /// Whether this unit is enabled.
181    #[serde(default = "default_enabled")]
182    pub enabled: bool,
183
184    /// Custom metadata for this unit.
185    #[serde(default)]
186    pub metadata: std::collections::HashMap<String, String>,
187}
188
189fn default_broadcast_enabled() -> bool {
190    true
191}
192
193fn default_enabled() -> bool {
194    true
195}
196
197impl Default for UnitConfig {
198    fn default() -> Self {
199        Self {
200            name: "Unnamed Unit".to_string(),
201            description: String::new(),
202            word_order: None,
203            register_config: None,
204            broadcast_enabled: true,
205            response_delay_us: 0,
206            enabled: true,
207            metadata: std::collections::HashMap::new(),
208        }
209    }
210}
211
212impl UnitConfig {
213    /// Create a new unit config with a name.
214    pub fn new(name: impl Into<String>) -> Self {
215        Self {
216            name: name.into(),
217            ..Default::default()
218        }
219    }
220
221    /// Create a unit config with a specific word order.
222    pub fn with_word_order(name: impl Into<String>, word_order: WordOrder) -> Self {
223        Self {
224            name: name.into(),
225            word_order: Some(word_order),
226            ..Default::default()
227        }
228    }
229
230    /// Set the description.
231    pub fn with_description(mut self, description: impl Into<String>) -> Self {
232        self.description = description.into();
233        self
234    }
235
236    /// Set the register configuration.
237    pub fn with_register_config(mut self, config: RegisterStoreConfig) -> Self {
238        self.register_config = Some(config);
239        self
240    }
241
242    /// Set the response delay.
243    pub fn with_response_delay_us(mut self, delay: u64) -> Self {
244        self.response_delay_us = delay;
245        self
246    }
247
248    /// Set broadcast enabled state.
249    pub fn with_broadcast(mut self, enabled: bool) -> Self {
250        self.broadcast_enabled = enabled;
251        self
252    }
253
254    /// Set enabled state.
255    pub fn with_enabled(mut self, enabled: bool) -> Self {
256        self.enabled = enabled;
257        self
258    }
259
260    /// Add metadata.
261    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
262        self.metadata.insert(key.into(), value.into());
263        self
264    }
265
266    /// Get the effective word order (unit-specific or None for manager default).
267    pub fn effective_word_order(&self, default: WordOrder) -> WordOrder {
268        self.word_order.unwrap_or(default)
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[test]
277    fn test_unit_manager_config_defaults() {
278        let config = UnitManagerConfig::default();
279        assert_eq!(config.max_units, 247);
280        assert_eq!(config.default_word_order, WordOrder::BigEndian);
281        assert!(!config.auto_create_units);
282    }
283
284    #[test]
285    fn test_unit_manager_config_builder() {
286        let config = UnitManagerConfig::default()
287            .with_max_units(100)
288            .with_word_order(WordOrder::LittleEndian)
289            .with_auto_create(true);
290
291        assert_eq!(config.max_units, 100);
292        assert_eq!(config.default_word_order, WordOrder::LittleEndian);
293        assert!(config.auto_create_units);
294    }
295
296    #[test]
297    fn test_broadcast_mode_display() {
298        assert_eq!(BroadcastMode::WriteAll.to_string(), "Write to all units");
299        assert_eq!(BroadcastMode::Disabled.to_string(), "Disabled");
300        assert_eq!(BroadcastMode::EchoToUnit(5).to_string(), "Echo to unit 5");
301    }
302
303    #[test]
304    fn test_unit_config_builder() {
305        let config = UnitConfig::new("Pump #1")
306            .with_description("Main circulation pump")
307            .with_response_delay_us(1000)
308            .with_metadata("location", "Building A");
309
310        assert_eq!(config.name, "Pump #1");
311        assert_eq!(config.description, "Main circulation pump");
312        assert_eq!(config.response_delay_us, 1000);
313        assert_eq!(config.metadata.get("location"), Some(&"Building A".to_string()));
314    }
315
316    #[test]
317    fn test_unit_config_effective_word_order() {
318        let config1 = UnitConfig::new("Test1");
319        let config2 = UnitConfig::with_word_order("Test2", WordOrder::LittleEndian);
320
321        assert_eq!(config1.effective_word_order(WordOrder::BigEndian), WordOrder::BigEndian);
322        assert_eq!(config2.effective_word_order(WordOrder::BigEndian), WordOrder::LittleEndian);
323    }
324
325    #[test]
326    fn test_serde_roundtrip() {
327        let config = UnitManagerConfig::default()
328            .with_max_units(50)
329            .with_word_order(WordOrder::BigEndianWordSwap);
330
331        let json = serde_json::to_string(&config).unwrap();
332        let parsed: UnitManagerConfig = serde_json::from_str(&json).unwrap();
333
334        assert_eq!(parsed.max_units, 50);
335        assert_eq!(parsed.default_word_order, WordOrder::BigEndianWordSwap);
336    }
337}