Skip to main content

rustmod_datalink/
sim.rs

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/// A fixed-size array of boolean values representing Modbus coils or discrete inputs.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct CoilBank {
10    values: Vec<bool>,
11}
12
13impl CoilBank {
14    /// Create a bank of `size` coils, all initially `false`.
15    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/// A fixed-size array of 16-bit values representing Modbus holding or input registers.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct RegisterBank {
46    values: Vec<u16>,
47}
48
49impl RegisterBank {
50    /// Create a bank of `size` registers, all initially `0`.
51    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/// The four Modbus address spaces: coils, discrete inputs, holding registers, and input registers.
80#[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    /// Create a new point model with the given address space sizes.
90    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/// Thread-safe in-memory Modbus device simulator.
106///
107/// Implements [`ModbusService`] with support for all standard function codes
108/// (FC01–FC08, FC15–FC16, FC22–FC24) plus FC17 (Report Server ID) and
109/// FC43/0x0E (Read Device Identification).
110///
111/// Useful for testing client code without physical hardware.
112#[derive(Debug)]
113pub struct InMemoryModbusService {
114    model: RwLock<InMemoryPointModel>,
115}
116
117impl InMemoryModbusService {
118    /// Create a new simulator with the given address space sizes.
119    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    /// Create a simulator from an existing [`InMemoryPointModel`].
134    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    /// Clone the current state of all address spaces.
153    pub fn snapshot(&self) -> Result<InMemoryPointModel, ServiceError> {
154        Ok(self.read_model()?.clone())
155    }
156
157    /// Set a coil value at the given address.
158    pub fn set_coil(&self, address: u16, value: bool) -> Result<(), ServiceError> {
159        self.write_model()?.coils.set(usize::from(address), value)
160    }
161
162    /// Set a discrete input value at the given address.
163    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    /// Set a holding register value at the given address.
170    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    /// Set an input register value at the given address.
177    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    /// Read a coil value at the given address.
184    pub fn coil(&self, address: u16) -> Result<Option<bool>, ServiceError> {
185        Ok(self.read_model()?.coils.get(usize::from(address)))
186    }
187
188    /// Read a holding register value at the given address.
189    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                        // Return Query Data: echo request
428                        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                        // Clear Counters: echo request
435                        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                    // FC17 Report Server ID: byte-count + server-id + run-indicator.
481                    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                    // FC43/MEI 0x0E Read Device Identification.
487                    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)?; // basic conformity level
497                    w.write_u8(0x00).map_err(map_encode)?; // no more follows
498                    w.write_u8(0x00).map_err(map_encode)?; // next object id
499
500                    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}