1use crate::{ModbusService, ServiceError};
2use rustmod_core::encoding::Writer;
3use rustmod_core::pdu::{DecodedRequest, FunctionCode};
4use rustmod_core::{EncodeError, UnitId};
5use std::sync::RwLock;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct CoilBank {
10 values: Vec<bool>,
11}
12
13impl CoilBank {
14 pub fn new(size: usize) -> Self {
16 Self {
17 values: vec![false; size],
18 }
19 }
20
21 pub fn len(&self) -> usize {
22 self.values.len()
23 }
24
25 pub fn is_empty(&self) -> bool {
26 self.values.is_empty()
27 }
28
29 pub fn get(&self, index: usize) -> Option<bool> {
30 self.values.get(index).copied()
31 }
32
33 pub fn set(&mut self, index: usize, value: bool) -> Result<(), ServiceError> {
34 let slot = self
35 .values
36 .get_mut(index)
37 .ok_or(ServiceError::InvalidRequest("coil address out of range"))?;
38 *slot = value;
39 Ok(())
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct RegisterBank {
46 values: Vec<u16>,
47}
48
49impl RegisterBank {
50 pub fn new(size: usize) -> Self {
52 Self {
53 values: vec![0u16; size],
54 }
55 }
56
57 pub fn len(&self) -> usize {
58 self.values.len()
59 }
60
61 pub fn is_empty(&self) -> bool {
62 self.values.is_empty()
63 }
64
65 pub fn get(&self, index: usize) -> Option<u16> {
66 self.values.get(index).copied()
67 }
68
69 pub fn set(&mut self, index: usize, value: u16) -> Result<(), ServiceError> {
70 let slot = self
71 .values
72 .get_mut(index)
73 .ok_or(ServiceError::InvalidRequest("register address out of range"))?;
74 *slot = value;
75 Ok(())
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct InMemoryPointModel {
82 pub coils: CoilBank,
83 pub discrete_inputs: CoilBank,
84 pub holding_registers: RegisterBank,
85 pub input_registers: RegisterBank,
86}
87
88impl InMemoryPointModel {
89 pub fn new(
91 coil_count: usize,
92 discrete_input_count: usize,
93 holding_register_count: usize,
94 input_register_count: usize,
95 ) -> Self {
96 Self {
97 coils: CoilBank::new(coil_count),
98 discrete_inputs: CoilBank::new(discrete_input_count),
99 holding_registers: RegisterBank::new(holding_register_count),
100 input_registers: RegisterBank::new(input_register_count),
101 }
102 }
103}
104
105#[derive(Debug)]
113pub struct InMemoryModbusService {
114 model: RwLock<InMemoryPointModel>,
115}
116
117impl InMemoryModbusService {
118 pub fn new(
120 coil_count: usize,
121 discrete_input_count: usize,
122 holding_register_count: usize,
123 input_register_count: usize,
124 ) -> Self {
125 Self::with_model(InMemoryPointModel::new(
126 coil_count,
127 discrete_input_count,
128 holding_register_count,
129 input_register_count,
130 ))
131 }
132
133 pub fn with_model(model: InMemoryPointModel) -> Self {
135 Self {
136 model: RwLock::new(model),
137 }
138 }
139
140 fn read_model(&self) -> Result<std::sync::RwLockReadGuard<'_, InMemoryPointModel>, ServiceError> {
141 self.model
142 .read()
143 .map_err(|_| ServiceError::Internal("in-memory point model lock poisoned"))
144 }
145
146 fn write_model(&self) -> Result<std::sync::RwLockWriteGuard<'_, InMemoryPointModel>, ServiceError> {
147 self.model
148 .write()
149 .map_err(|_| ServiceError::Internal("in-memory point model lock poisoned"))
150 }
151
152 pub fn snapshot(&self) -> Result<InMemoryPointModel, ServiceError> {
154 Ok(self.read_model()?.clone())
155 }
156
157 pub fn set_coil(&self, address: u16, value: bool) -> Result<(), ServiceError> {
159 self.write_model()?.coils.set(usize::from(address), value)
160 }
161
162 pub fn set_discrete_input(&self, address: u16, value: bool) -> Result<(), ServiceError> {
164 self.write_model()?
165 .discrete_inputs
166 .set(usize::from(address), value)
167 }
168
169 pub fn set_holding_register(&self, address: u16, value: u16) -> Result<(), ServiceError> {
171 self.write_model()?
172 .holding_registers
173 .set(usize::from(address), value)
174 }
175
176 pub fn set_input_register(&self, address: u16, value: u16) -> Result<(), ServiceError> {
178 self.write_model()?
179 .input_registers
180 .set(usize::from(address), value)
181 }
182
183 pub fn coil(&self, address: u16) -> Result<Option<bool>, ServiceError> {
185 Ok(self.read_model()?.coils.get(usize::from(address)))
186 }
187
188 pub fn holding_register(&self, address: u16) -> Result<Option<u16>, ServiceError> {
190 Ok(self
191 .read_model()?
192 .holding_registers
193 .get(usize::from(address)))
194 }
195}
196
197impl ModbusService for InMemoryModbusService {
198 fn handle(
199 &self,
200 unit_id: UnitId,
201 request: DecodedRequest<'_>,
202 response_pdu: &mut [u8],
203 ) -> Result<usize, ServiceError> {
204 let mut model = self.write_model()?;
205
206 let mut w = Writer::new(response_pdu);
207
208 match request {
209 DecodedRequest::ReadCoils(req) => {
210 let range = checked_range(req.start_address, req.quantity, model.coils.len())
211 .ok_or(ServiceError::Exception(
212 rustmod_core::pdu::ExceptionCode::IllegalDataAddress,
213 ))?;
214 let byte_count = range.len().div_ceil(8);
215 let byte_count_u8 = u8::try_from(byte_count)
216 .map_err(|_| ServiceError::Internal("coil response too large"))?;
217
218 w.write_u8(FunctionCode::ReadCoils.as_u8())
219 .map_err(map_encode)?;
220 w.write_u8(byte_count_u8).map_err(map_encode)?;
221
222 let mut packed = [0u8; 250];
223 for (i, address) in range.enumerate() {
224 if model.coils.get(address).unwrap_or(false) {
225 packed[i / 8] |= 1u8 << (i % 8);
226 }
227 }
228 w.write_all(&packed[..byte_count]).map_err(map_encode)?;
229 }
230 DecodedRequest::ReadDiscreteInputs(req) => {
231 let range = checked_range(
232 req.start_address,
233 req.quantity,
234 model.discrete_inputs.len(),
235 )
236 .ok_or(ServiceError::Exception(
237 rustmod_core::pdu::ExceptionCode::IllegalDataAddress,
238 ))?;
239 let byte_count = range.len().div_ceil(8);
240 let byte_count_u8 = u8::try_from(byte_count)
241 .map_err(|_| ServiceError::Internal("discrete input response too large"))?;
242
243 w.write_u8(FunctionCode::ReadDiscreteInputs.as_u8())
244 .map_err(map_encode)?;
245 w.write_u8(byte_count_u8).map_err(map_encode)?;
246
247 let mut packed = [0u8; 250];
248 for (i, address) in range.enumerate() {
249 if model.discrete_inputs.get(address).unwrap_or(false) {
250 packed[i / 8] |= 1u8 << (i % 8);
251 }
252 }
253 w.write_all(&packed[..byte_count]).map_err(map_encode)?;
254 }
255 DecodedRequest::ReadHoldingRegisters(req) => {
256 let range = checked_range(
257 req.start_address,
258 req.quantity,
259 model.holding_registers.len(),
260 )
261 .ok_or(ServiceError::Exception(
262 rustmod_core::pdu::ExceptionCode::IllegalDataAddress,
263 ))?;
264
265 let byte_count = range.len() * 2;
266 let byte_count_u8 = u8::try_from(byte_count)
267 .map_err(|_| ServiceError::Internal("register response too large"))?;
268
269 w.write_u8(FunctionCode::ReadHoldingRegisters.as_u8())
270 .map_err(map_encode)?;
271 w.write_u8(byte_count_u8).map_err(map_encode)?;
272 for address in range {
273 w.write_be_u16(model.holding_registers.get(address).unwrap_or(0))
274 .map_err(map_encode)?;
275 }
276 }
277 DecodedRequest::ReadInputRegisters(req) => {
278 let range = checked_range(req.start_address, req.quantity, model.input_registers.len())
279 .ok_or(ServiceError::Exception(
280 rustmod_core::pdu::ExceptionCode::IllegalDataAddress,
281 ))?;
282
283 let byte_count = range.len() * 2;
284 let byte_count_u8 = u8::try_from(byte_count)
285 .map_err(|_| ServiceError::Internal("input register response too large"))?;
286
287 w.write_u8(FunctionCode::ReadInputRegisters.as_u8())
288 .map_err(map_encode)?;
289 w.write_u8(byte_count_u8).map_err(map_encode)?;
290 for address in range {
291 w.write_be_u16(model.input_registers.get(address).unwrap_or(0))
292 .map_err(map_encode)?;
293 }
294 }
295 DecodedRequest::WriteSingleCoil(req) => {
296 model
297 .coils
298 .set(usize::from(req.address), req.value)
299 .map_err(|_| {
300 ServiceError::Exception(rustmod_core::pdu::ExceptionCode::IllegalDataAddress)
301 })?;
302 w.write_u8(FunctionCode::WriteSingleCoil.as_u8())
303 .map_err(map_encode)?;
304 w.write_be_u16(req.address).map_err(map_encode)?;
305 w.write_be_u16(if req.value { 0xFF00 } else { 0x0000 })
306 .map_err(map_encode)?;
307 }
308 DecodedRequest::WriteSingleRegister(req) => {
309 model
310 .holding_registers
311 .set(usize::from(req.address), req.value)
312 .map_err(|_| {
313 ServiceError::Exception(rustmod_core::pdu::ExceptionCode::IllegalDataAddress)
314 })?;
315 w.write_u8(FunctionCode::WriteSingleRegister.as_u8())
316 .map_err(map_encode)?;
317 w.write_be_u16(req.address).map_err(map_encode)?;
318 w.write_be_u16(req.value).map_err(map_encode)?;
319 }
320 DecodedRequest::WriteMultipleCoils(req) => {
321 let range = checked_range(req.start_address, req.quantity, model.coils.len()).ok_or(
322 ServiceError::Exception(rustmod_core::pdu::ExceptionCode::IllegalDataAddress),
323 )?;
324
325 for (i, address) in range.enumerate() {
326 let value = req.coil(i).ok_or(ServiceError::InvalidRequest(
327 "invalid packed coil write payload",
328 ))?;
329 model.coils.set(address, value)?;
330 }
331
332 w.write_u8(FunctionCode::WriteMultipleCoils.as_u8())
333 .map_err(map_encode)?;
334 w.write_be_u16(req.start_address).map_err(map_encode)?;
335 w.write_be_u16(req.quantity).map_err(map_encode)?;
336 }
337 DecodedRequest::WriteMultipleRegisters(req) => {
338 let quantity = req.quantity();
339 let quantity_u16 = u16::try_from(quantity)
340 .map_err(|_| ServiceError::InvalidRequest("register quantity too large"))?;
341 let range = checked_range(
342 req.start_address,
343 quantity_u16,
344 model.holding_registers.len(),
345 )
346 .ok_or(ServiceError::Exception(
347 rustmod_core::pdu::ExceptionCode::IllegalDataAddress,
348 ))?;
349
350 for (i, address) in range.enumerate() {
351 let value = req
352 .register(i)
353 .ok_or(ServiceError::InvalidRequest("invalid register payload"))?;
354 model.holding_registers.set(address, value)?;
355 }
356
357 w.write_u8(FunctionCode::WriteMultipleRegisters.as_u8())
358 .map_err(map_encode)?;
359 w.write_be_u16(req.start_address).map_err(map_encode)?;
360 w.write_be_u16(quantity_u16).map_err(map_encode)?;
361 }
362 DecodedRequest::MaskWriteRegister(req) => {
363 let address = usize::from(req.address);
364 let current = model.holding_registers.get(address).ok_or(ServiceError::Exception(
365 rustmod_core::pdu::ExceptionCode::IllegalDataAddress,
366 ))?;
367 let next = (current & req.and_mask) | (req.or_mask & !req.and_mask);
368 model.holding_registers.set(address, next).map_err(|_| {
369 ServiceError::Exception(rustmod_core::pdu::ExceptionCode::IllegalDataAddress)
370 })?;
371
372 w.write_u8(FunctionCode::MaskWriteRegister.as_u8())
373 .map_err(map_encode)?;
374 w.write_be_u16(req.address).map_err(map_encode)?;
375 w.write_be_u16(req.and_mask).map_err(map_encode)?;
376 w.write_be_u16(req.or_mask).map_err(map_encode)?;
377 }
378 DecodedRequest::ReadWriteMultipleRegisters(req) => {
379 let write_quantity = req.write_quantity();
380 let write_quantity_u16 = u16::try_from(write_quantity)
381 .map_err(|_| ServiceError::InvalidRequest("write quantity too large"))?;
382
383 let write_range = checked_range(
384 req.write_start_address,
385 write_quantity_u16,
386 model.holding_registers.len(),
387 )
388 .ok_or(ServiceError::Exception(
389 rustmod_core::pdu::ExceptionCode::IllegalDataAddress,
390 ))?;
391
392 for (i, address) in write_range.enumerate() {
393 let value = req
394 .register(i)
395 .ok_or(ServiceError::InvalidRequest("invalid register payload"))?;
396 model.holding_registers.set(address, value)?;
397 }
398
399 let read_range = checked_range(
400 req.read_start_address,
401 req.read_quantity,
402 model.holding_registers.len(),
403 )
404 .ok_or(ServiceError::Exception(
405 rustmod_core::pdu::ExceptionCode::IllegalDataAddress,
406 ))?;
407
408 let byte_count = read_range.len() * 2;
409 let byte_count_u8 = u8::try_from(byte_count)
410 .map_err(|_| ServiceError::Internal("register response too large"))?;
411 w.write_u8(FunctionCode::ReadWriteMultipleRegisters.as_u8())
412 .map_err(map_encode)?;
413 w.write_u8(byte_count_u8).map_err(map_encode)?;
414 for address in read_range {
415 w.write_be_u16(model.holding_registers.get(address).unwrap_or(0))
416 .map_err(map_encode)?;
417 }
418 }
419 DecodedRequest::ReadExceptionStatus(_) => {
420 w.write_u8(FunctionCode::ReadExceptionStatus.as_u8())
421 .map_err(map_encode)?;
422 w.write_u8(0x00).map_err(map_encode)?;
423 }
424 DecodedRequest::Diagnostics(req) => {
425 match req.sub_function {
426 0x0000 => {
427 w.write_u8(FunctionCode::Diagnostics.as_u8())
429 .map_err(map_encode)?;
430 w.write_be_u16(req.sub_function).map_err(map_encode)?;
431 w.write_be_u16(req.data).map_err(map_encode)?;
432 }
433 0x000A => {
434 w.write_u8(FunctionCode::Diagnostics.as_u8())
436 .map_err(map_encode)?;
437 w.write_be_u16(req.sub_function).map_err(map_encode)?;
438 w.write_be_u16(0x0000).map_err(map_encode)?;
439 }
440 _ => {
441 return Err(ServiceError::Exception(
442 rustmod_core::pdu::ExceptionCode::IllegalFunction,
443 ));
444 }
445 }
446 }
447 DecodedRequest::ReadFifoQueue(req) => {
448 let addr = usize::from(req.fifo_pointer_address);
449 let fifo_count_val = model.holding_registers.get(addr).ok_or(
450 ServiceError::Exception(rustmod_core::pdu::ExceptionCode::IllegalDataAddress),
451 )?;
452 let fifo_count = usize::from(fifo_count_val);
453 if fifo_count > 31 {
454 return Err(ServiceError::Exception(
455 rustmod_core::pdu::ExceptionCode::IllegalDataValue,
456 ));
457 }
458 let byte_count = fifo_count * 2 + 2;
459 w.write_u8(FunctionCode::ReadFifoQueue.as_u8())
460 .map_err(map_encode)?;
461 w.write_be_u16(u16::try_from(byte_count).map_err(|_| {
462 ServiceError::Internal("fifo byte count overflow")
463 })?)
464 .map_err(map_encode)?;
465 w.write_be_u16(fifo_count_val).map_err(map_encode)?;
466 for i in 0..fifo_count {
467 let reg_addr = addr.checked_add(1 + i).ok_or(ServiceError::Exception(
468 rustmod_core::pdu::ExceptionCode::IllegalDataAddress,
469 ))?;
470 let val = model.holding_registers.get(reg_addr).ok_or(
471 ServiceError::Exception(
472 rustmod_core::pdu::ExceptionCode::IllegalDataAddress,
473 ),
474 )?;
475 w.write_be_u16(val).map_err(map_encode)?;
476 }
477 }
478 DecodedRequest::Custom(req) => {
479 if req.function_code == 0x11 {
480 w.write_u8(0x11).map_err(map_encode)?;
482 w.write_u8(0x02).map_err(map_encode)?;
483 w.write_u8(unit_id.as_u8()).map_err(map_encode)?;
484 w.write_u8(0xFF).map_err(map_encode)?;
485 } else if req.function_code == 0x2B {
486 if req.data.len() != 3 || req.data[0] != 0x0E {
488 return Err(ServiceError::Exception(
489 rustmod_core::pdu::ExceptionCode::IllegalDataValue,
490 ));
491 }
492 let read_code = req.data[1];
493 w.write_u8(0x2B).map_err(map_encode)?;
494 w.write_u8(0x0E).map_err(map_encode)?;
495 w.write_u8(read_code).map_err(map_encode)?;
496 w.write_u8(0x01).map_err(map_encode)?; w.write_u8(0x00).map_err(map_encode)?; w.write_u8(0x00).map_err(map_encode)?; let objects = [
501 (0x00u8, b"rust-mod-sim".as_slice()),
502 (0x01u8, b"in-memory".as_slice()),
503 (0x02u8, b"0.1".as_slice()),
504 ];
505 w.write_u8(objects.len() as u8).map_err(map_encode)?;
506 for (id, value) in objects {
507 let value_len = u8::try_from(value.len()).map_err(|_| {
508 ServiceError::Internal("device identification object too large")
509 })?;
510 w.write_u8(id).map_err(map_encode)?;
511 w.write_u8(value_len).map_err(map_encode)?;
512 w.write_all(value).map_err(map_encode)?;
513 }
514 } else {
515 return Err(ServiceError::Exception(
516 rustmod_core::pdu::ExceptionCode::IllegalFunction,
517 ));
518 }
519 }
520 _ => {
521 return Err(ServiceError::Exception(
522 rustmod_core::pdu::ExceptionCode::IllegalFunction,
523 ));
524 }
525 }
526
527 Ok(w.position())
528 }
529}
530
531fn checked_range(start: u16, quantity: u16, len: usize) -> Option<std::ops::Range<usize>> {
532 let start = usize::from(start);
533 let quantity = usize::from(quantity);
534 let end = start.checked_add(quantity)?;
535 if quantity == 0 || end > len {
536 return None;
537 }
538 Some(start..end)
539}
540
541fn map_encode(err: EncodeError) -> ServiceError {
542 let msg = match err {
543 EncodeError::BufferTooSmall => "response buffer too small",
544 EncodeError::ValueOutOfRange => "response value out of range",
545 EncodeError::InvalidLength => "response length invalid",
546 EncodeError::Unsupported => "response operation unsupported",
547 EncodeError::Message(_) => "response encode message",
548 _ => "response encode error",
549 };
550 ServiceError::Internal(msg)
551}
552
553#[cfg(test)]
554mod tests {
555 use super::{InMemoryModbusService, InMemoryPointModel};
556 use crate::ModbusService;
557 use rustmod_core::encoding::Reader;
558 use rustmod_core::pdu::{DecodedRequest, Response};
559 use rustmod_core::UnitId;
560
561 #[test]
562 fn in_memory_service_reads_and_writes() {
563 let service = InMemoryModbusService::with_model(InMemoryPointModel::new(16, 16, 16, 16));
564 service.set_holding_register(0, 42).unwrap();
565
566 let mut pdu = [0u8; 260];
567 let request = {
568 let mut r = Reader::new(&[0x03, 0x00, 0x00, 0x00, 0x01]);
569 DecodedRequest::decode(&mut r).unwrap()
570 };
571 let len = service.handle(UnitId::new(1), request, &mut pdu).unwrap();
572
573 let mut rr = Reader::new(&pdu[..len]);
574 match Response::decode(&mut rr).unwrap() {
575 Response::ReadHoldingRegisters(resp) => assert_eq!(resp.register(0), Some(42)),
576 other => panic!("unexpected response: {other:?}"),
577 }
578
579 let write_req = {
580 let mut r = Reader::new(&[0x06, 0x00, 0x01, 0x12, 0x34]);
581 DecodedRequest::decode(&mut r).unwrap()
582 };
583 let _ = service.handle(UnitId::new(1), write_req, &mut pdu).unwrap();
584 assert_eq!(service.holding_register(1).unwrap(), Some(0x1234));
585
586 let mask_req = {
587 let mut r = Reader::new(&[0x16, 0x00, 0x01, 0xFF, 0x00, 0x00, 0x12]);
588 DecodedRequest::decode(&mut r).unwrap()
589 };
590 let _ = service.handle(UnitId::new(1), mask_req, &mut pdu).unwrap();
591 assert_eq!(service.holding_register(1).unwrap(), Some(0x1212));
592
593 let rw_req = {
594 let mut r = Reader::new(&[
595 0x17, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x04, 0xBE, 0xEF, 0xCA,
596 0xFE,
597 ]);
598 DecodedRequest::decode(&mut r).unwrap()
599 };
600 let len = service.handle(UnitId::new(1), rw_req, &mut pdu).unwrap();
601 let mut rr = Reader::new(&pdu[..len]);
602 match Response::decode(&mut rr).unwrap() {
603 Response::ReadWriteMultipleRegisters(resp) => {
604 assert_eq!(resp.register(0), Some(0xBEEF));
605 assert_eq!(resp.register(1), Some(0xCAFE));
606 }
607 other => panic!("unexpected response: {other:?}"),
608 }
609 }
610
611 #[test]
612 fn in_memory_service_supports_report_server_id() {
613 let service = InMemoryModbusService::with_model(InMemoryPointModel::new(4, 4, 4, 4));
614 let mut pdu = [0u8; 260];
615
616 let request = {
617 let mut r = Reader::new(&[0x11]);
618 DecodedRequest::decode(&mut r).unwrap()
619 };
620 let len = service.handle(UnitId::new(0x2A), request, &mut pdu).unwrap();
621
622 let mut rr = Reader::new(&pdu[..len]);
623 match Response::decode(&mut rr).unwrap() {
624 Response::Custom(resp) => {
625 assert_eq!(resp.function_code, 0x11);
626 assert_eq!(resp.data, &[0x02, 0x2A, 0xFF]);
627 }
628 other => panic!("unexpected response: {other:?}"),
629 }
630 }
631
632 #[test]
633 fn in_memory_service_supports_read_device_identification() {
634 let service = InMemoryModbusService::with_model(InMemoryPointModel::new(4, 4, 4, 4));
635 let mut pdu = [0u8; 260];
636
637 let request = {
638 let mut r = Reader::new(&[0x2B, 0x0E, 0x01, 0x00]);
639 DecodedRequest::decode(&mut r).unwrap()
640 };
641 let len = service.handle(UnitId::new(0x2A), request, &mut pdu).unwrap();
642
643 let mut rr = Reader::new(&pdu[..len]);
644 match Response::decode(&mut rr).unwrap() {
645 Response::Custom(resp) => {
646 assert_eq!(resp.function_code, 0x2B);
647 assert_eq!(resp.data[0], 0x0E);
648 assert_eq!(resp.data[1], 0x01);
649 assert_eq!(resp.data[5], 0x03);
650 }
651 other => panic!("unexpected response: {other:?}"),
652 }
653 }
654}