rusty_modbus_sim/
simulator.rs1use std::net::SocketAddr;
4use std::sync::Arc;
5
6use rusty_modbus_server::ModbusServer;
7use rusty_modbus_server::config::{DeviceIdentification, ServerConfig};
8use rusty_modbus_server::store::memory::{InMemoryStore, StoreConfig};
9use rusty_modbus_types::UnitId;
10
11use crate::config::{CoilBlock, RegisterBlock, RegisterConfig, SimConfig};
12use crate::error::SimError;
13
14const MODBUS_ADDRESS_SPACE: usize = 65_536;
15
16pub struct ModbusSimulator {
18 config: SimConfig,
19 store: Arc<InMemoryStore>,
20 server: Option<ModbusServer<InMemoryStore>>,
21}
22
23impl ModbusSimulator {
24 pub fn from_yaml(yaml: &str) -> Result<Self, SimError> {
31 let config: SimConfig = serde_yaml_ng::from_str(yaml).map_err(SimError::ConfigParse)?;
32 Self::from_config(config)
33 }
34
35 pub fn from_config(config: SimConfig) -> Result<Self, SimError> {
42 validate_register_config(&config.registers)?;
43
44 let store = Arc::new(InMemoryStore::try_new(StoreConfig::default())?);
45 apply_register_config(&store, &config.registers);
46
47 Ok(Self {
48 config,
49 store,
50 server: None,
51 })
52 }
53
54 pub async fn start(&mut self) -> Result<SocketAddr, SimError> {
60 let listen_addr: SocketAddr = self
61 .config
62 .device
63 .listen_addr
64 .parse()
65 .map_err(|e| SimError::Config(format!("invalid listen address: {e}")))?;
66
67 let server_config = ServerConfig {
68 listen_addr,
69 unit_id: UnitId(self.config.device.unit_id),
70 device_id: DeviceIdentification {
71 vendor_name: self.config.device.vendor_name.clone(),
72 product_code: self.config.device.product_code.clone(),
73 major_minor_revision: self.config.device.revision.clone(),
74 ..DeviceIdentification::default()
75 },
76 ..ServerConfig::default()
77 };
78
79 let server = ModbusServer::start(server_config, Arc::clone(&self.store))
80 .await
81 .map_err(SimError::Server)?;
82
83 let addr = server.local_addr();
84 self.server = Some(server);
85 Ok(addr)
86 }
87
88 pub async fn stop(&mut self) {
90 if let Some(server) = &self.server {
91 server.stop().await;
92 }
93 self.server = None;
94 }
95
96 pub fn set_holding_register(&self, address: u16, value: u16) -> Result<(), SimError> {
98 self.store
99 .set_holding_register(address, value)
100 .map_err(SimError::Store)
101 }
102
103 pub fn set_input_register(&self, address: u16, value: u16) -> Result<(), SimError> {
105 self.store
106 .set_input_register(address, value)
107 .map_err(SimError::Store)
108 }
109
110 pub fn set_coil(&self, address: u16, value: bool) -> Result<(), SimError> {
112 self.store.set_coil(address, value).map_err(SimError::Store)
113 }
114
115 #[must_use]
121 pub fn local_addr(&self) -> SocketAddr {
122 self.server
123 .as_ref()
124 .expect("simulator not started")
125 .local_addr()
126 }
127}
128
129impl std::fmt::Debug for ModbusSimulator {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 f.debug_struct("ModbusSimulator")
132 .field("unit_id", &self.config.device.unit_id)
133 .field("running", &self.server.is_some())
134 .finish_non_exhaustive()
135 }
136}
137
138fn validate_register_config(config: &RegisterConfig) -> Result<(), SimError> {
139 for block in &config.holding {
140 check_block_range("holding", block.address, block.count)?;
141 }
142 for block in &config.input {
143 check_block_range("input", block.address, block.count)?;
144 }
145 for block in &config.coils {
146 check_block_range("coils", block.address, block.count)?;
147 }
148 for block in &config.discrete_inputs {
149 check_block_range("discrete_inputs", block.address, block.count)?;
150 }
151 Ok(())
152}
153
154fn check_block_range(kind: &str, address: u16, count: u16) -> Result<(), SimError> {
155 let end = usize::from(address) + usize::from(count);
156 if end <= MODBUS_ADDRESS_SPACE {
157 Ok(())
158 } else {
159 Err(SimError::Config(format!(
160 "{kind} block at address {address} with count {count} exceeds Modbus address space"
161 )))
162 }
163}
164
165fn apply_register_config(store: &InMemoryStore, config: &RegisterConfig) {
167 apply_register_blocks(&config.holding, |address, value| {
168 store
169 .set_holding_register(address, value)
170 .expect("validated holding register config should fit store");
171 });
172 apply_register_blocks(&config.input, |address, value| {
173 store
174 .set_input_register(address, value)
175 .expect("validated input register config should fit store");
176 });
177 apply_coil_blocks(&config.coils, |address, value| {
178 store
179 .set_coil(address, value)
180 .expect("validated coil config should fit store");
181 });
182 apply_coil_blocks(&config.discrete_inputs, |address, value| {
183 store
184 .set_discrete_input(address, value)
185 .expect("validated discrete input config should fit store");
186 });
187}
188
189fn apply_register_blocks(blocks: &[RegisterBlock], mut set: impl FnMut(u16, u16)) {
190 for block in blocks {
191 for (i, &val) in block.initial.iter().enumerate() {
192 if i < usize::from(block.count)
193 && let Some(address) = offset_address(block.address, i)
194 {
195 set(address, val);
196 }
197 }
198 }
199}
200
201fn apply_coil_blocks(blocks: &[CoilBlock], mut set: impl FnMut(u16, bool)) {
202 for block in blocks {
203 for (i, &val) in block.initial.iter().enumerate() {
204 if i < usize::from(block.count)
205 && let Some(address) = offset_address(block.address, i)
206 {
207 set(address, val);
208 }
209 }
210 }
211}
212
213fn offset_address(address: u16, offset: usize) -> Option<u16> {
214 let offset = u16::try_from(offset).ok()?;
215 address.checked_add(offset)
216}