1pub const MBAP_HEADER_LEN: usize = 7;
16
17pub const MODBUS_PROTOCOL_ID: u16 = 0;
19
20pub const MAX_PDU_LEN: usize = 253;
22
23pub const MAX_ADU_LEN: usize = MBAP_HEADER_LEN + MAX_PDU_LEN;
25
26pub const FC_READ_COILS: u8 = 0x01;
31pub const FC_READ_DISCRETE_INPUTS: u8 = 0x02;
32pub const FC_READ_HOLDING_REGISTERS: u8 = 0x03;
33pub const FC_READ_INPUT_REGISTERS: u8 = 0x04;
34pub const FC_WRITE_SINGLE_COIL: u8 = 0x05;
35pub const FC_WRITE_SINGLE_REGISTER: u8 = 0x06;
36pub const FC_WRITE_MULTIPLE_COILS: u8 = 0x0F;
37pub const FC_WRITE_MULTIPLE_REGISTERS: u8 = 0x10;
38
39pub const EXCEPTION_FLAG: u8 = 0x80;
41
42pub const EXC_ILLEGAL_FUNCTION: u8 = 0x01;
47pub const EXC_ILLEGAL_DATA_ADDRESS: u8 = 0x02;
48pub const EXC_ILLEGAL_DATA_VALUE: u8 = 0x03;
49pub const EXC_SERVER_DEVICE_FAILURE: u8 = 0x04;
50pub const EXC_ACKNOWLEDGE: u8 = 0x05;
51pub const EXC_SERVER_DEVICE_BUSY: u8 = 0x06;
52pub const EXC_MEMORY_PARITY_ERROR: u8 = 0x08;
53pub const EXC_GATEWAY_PATH_UNAVAILABLE: u8 = 0x0A;
54pub const EXC_GATEWAY_TARGET_FAILED: u8 = 0x0B;
55
56pub const UNKNOWN_EXCEPTION: &str = "unknown_exception";
61
62pub fn exception_name(code: u8) -> &'static str {
64 match code {
65 EXC_ILLEGAL_FUNCTION => "illegal_function",
66 EXC_ILLEGAL_DATA_ADDRESS => "illegal_data_address",
67 EXC_ILLEGAL_DATA_VALUE => "illegal_data_value",
68 EXC_SERVER_DEVICE_FAILURE => "server_device_failure",
69 EXC_ACKNOWLEDGE => "acknowledge",
70 EXC_SERVER_DEVICE_BUSY => "server_device_busy",
71 EXC_MEMORY_PARITY_ERROR => "memory_parity_error",
72 EXC_GATEWAY_PATH_UNAVAILABLE => "gateway_path_unavailable",
73 EXC_GATEWAY_TARGET_FAILED => "gateway_target_device_failed_to_respond",
74 _ => UNKNOWN_EXCEPTION,
75 }
76}
77
78pub fn exception_code_from_name(name: &str) -> Option<u8> {
80 match name.trim().to_ascii_lowercase().as_str() {
81 "illegal_function" | "illegal function" => Some(EXC_ILLEGAL_FUNCTION),
82 "illegal_data_address" | "illegal data address" => Some(EXC_ILLEGAL_DATA_ADDRESS),
83 "illegal_data_value" | "illegal data value" => Some(EXC_ILLEGAL_DATA_VALUE),
84 "server_device_failure" | "server device failure" | "device_failure" => {
85 Some(EXC_SERVER_DEVICE_FAILURE)
86 }
87 "acknowledge" => Some(EXC_ACKNOWLEDGE),
88 "server_device_busy" | "server device busy" | "device_busy" => Some(EXC_SERVER_DEVICE_BUSY),
89 "memory_parity_error" | "memory parity error" => Some(EXC_MEMORY_PARITY_ERROR),
90 "gateway_path_unavailable" | "gateway path unavailable" => {
91 Some(EXC_GATEWAY_PATH_UNAVAILABLE)
92 }
93 "gateway_target_device_failed_to_respond" | "gateway_target_failed" => {
94 Some(EXC_GATEWAY_TARGET_FAILED)
95 }
96 _ => None,
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
109pub enum FrameError {
110 NotModbus { protocol_id: u16 },
112 BadLength { length: u16 },
115}
116
117impl std::fmt::Display for FrameError {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 match self {
120 FrameError::NotModbus { protocol_id } => write!(
121 f,
122 "MBAP protocol identifier {protocol_id} is not 0; this is not a Modbus/TCP stream"
123 ),
124 FrameError::BadLength { length } => write!(
125 f,
126 "MBAP length field {length} is outside the legal range 2..=254"
127 ),
128 }
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
141pub enum EncodeError {
142 PduLength { len: usize },
145 ByteCount { values: usize, bytes: usize },
148}
149
150impl std::fmt::Display for EncodeError {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 match self {
153 EncodeError::PduLength { len } => write!(
154 f,
155 "a Modbus PDU is 1..={MAX_PDU_LEN} octets; {len} given, which the MBAP length \
156 field cannot describe"
157 ),
158 EncodeError::ByteCount { values, bytes } => write!(
159 f,
160 "{values} value(s) need {bytes} octets of data, past the {} a read response's \
161 one-octet byte count and the {MAX_PDU_LEN}-octet PDU can carry",
162 MAX_PDU_LEN - 2
163 ),
164 }
165 }
166}
167
168impl std::error::Error for EncodeError {}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct Adu {
173 pub transaction_id: u16,
174 pub unit_id: u8,
175 pub pdu: Vec<u8>,
176}
177
178pub fn try_parse_adu(buf: &[u8]) -> Result<Option<(Adu, usize)>, FrameError> {
183 if buf.len() < MBAP_HEADER_LEN {
184 return Ok(None);
185 }
186
187 let transaction_id = u16::from_be_bytes([buf[0], buf[1]]);
188 let protocol_id = u16::from_be_bytes([buf[2], buf[3]]);
189 let length = u16::from_be_bytes([buf[4], buf[5]]);
190
191 if protocol_id != MODBUS_PROTOCOL_ID {
192 return Err(FrameError::NotModbus { protocol_id });
193 }
194
195 if length < 2 || length as usize > MAX_PDU_LEN + 1 {
197 return Err(FrameError::BadLength { length });
198 }
199
200 let total = 6 + length as usize;
201 if buf.len() < total {
202 return Ok(None);
203 }
204
205 let unit_id = buf[6];
206 let pdu = buf[MBAP_HEADER_LEN..total].to_vec();
207
208 Ok(Some((
209 Adu {
210 transaction_id,
211 unit_id,
212 pdu,
213 },
214 total,
215 )))
216}
217
218pub fn encode_adu(transaction_id: u16, unit_id: u8, pdu: &[u8]) -> Result<Vec<u8>, EncodeError> {
225 if pdu.is_empty() || pdu.len() > MAX_PDU_LEN {
226 return Err(EncodeError::PduLength { len: pdu.len() });
227 }
228 let mut out = Vec::with_capacity(MBAP_HEADER_LEN + pdu.len());
229 out.extend_from_slice(&transaction_id.to_be_bytes());
230 out.extend_from_slice(&MODBUS_PROTOCOL_ID.to_be_bytes());
231 out.extend_from_slice(&((pdu.len() as u16) + 1).to_be_bytes());
233 out.push(unit_id);
234 out.extend_from_slice(pdu);
235 Ok(out)
236}
237
238#[derive(Debug, Clone, PartialEq, Eq)]
244pub enum ModbusRequest {
245 ReadCoils { start: u16, quantity: u16 },
246 ReadDiscreteInputs { start: u16, quantity: u16 },
247 ReadHoldingRegisters { start: u16, quantity: u16 },
248 ReadInputRegisters { start: u16, quantity: u16 },
249 WriteSingleCoil { address: u16, value: bool },
250 WriteSingleRegister { address: u16, value: u16 },
251 WriteMultipleCoils { start: u16, values: Vec<bool> },
252 WriteMultipleRegisters { start: u16, values: Vec<u16> },
253}
254
255impl ModbusRequest {
256 pub fn function_code(&self) -> u8 {
257 match self {
258 ModbusRequest::ReadCoils { .. } => FC_READ_COILS,
259 ModbusRequest::ReadDiscreteInputs { .. } => FC_READ_DISCRETE_INPUTS,
260 ModbusRequest::ReadHoldingRegisters { .. } => FC_READ_HOLDING_REGISTERS,
261 ModbusRequest::ReadInputRegisters { .. } => FC_READ_INPUT_REGISTERS,
262 ModbusRequest::WriteSingleCoil { .. } => FC_WRITE_SINGLE_COIL,
263 ModbusRequest::WriteSingleRegister { .. } => FC_WRITE_SINGLE_REGISTER,
264 ModbusRequest::WriteMultipleCoils { .. } => FC_WRITE_MULTIPLE_COILS,
265 ModbusRequest::WriteMultipleRegisters { .. } => FC_WRITE_MULTIPLE_REGISTERS,
266 }
267 }
268
269 pub fn function_name(&self) -> &'static str {
271 match self {
272 ModbusRequest::ReadCoils { .. } => "read_coils",
273 ModbusRequest::ReadDiscreteInputs { .. } => "read_discrete_inputs",
274 ModbusRequest::ReadHoldingRegisters { .. } => "read_holding_registers",
275 ModbusRequest::ReadInputRegisters { .. } => "read_input_registers",
276 ModbusRequest::WriteSingleCoil { .. } => "write_single_coil",
277 ModbusRequest::WriteSingleRegister { .. } => "write_single_register",
278 ModbusRequest::WriteMultipleCoils { .. } => "write_multiple_coils",
279 ModbusRequest::WriteMultipleRegisters { .. } => "write_multiple_registers",
280 }
281 }
282
283 pub fn start_address(&self) -> u16 {
285 match self {
286 ModbusRequest::ReadCoils { start, .. }
287 | ModbusRequest::ReadDiscreteInputs { start, .. }
288 | ModbusRequest::ReadHoldingRegisters { start, .. }
289 | ModbusRequest::ReadInputRegisters { start, .. }
290 | ModbusRequest::WriteMultipleCoils { start, .. }
291 | ModbusRequest::WriteMultipleRegisters { start, .. } => *start,
292 ModbusRequest::WriteSingleCoil { address, .. }
293 | ModbusRequest::WriteSingleRegister { address, .. } => *address,
294 }
295 }
296
297 pub fn quantity(&self) -> u16 {
299 match self {
300 ModbusRequest::ReadCoils { quantity, .. }
301 | ModbusRequest::ReadDiscreteInputs { quantity, .. }
302 | ModbusRequest::ReadHoldingRegisters { quantity, .. }
303 | ModbusRequest::ReadInputRegisters { quantity, .. } => *quantity,
304 ModbusRequest::WriteSingleCoil { .. } | ModbusRequest::WriteSingleRegister { .. } => 1,
305 ModbusRequest::WriteMultipleCoils { values, .. } => values.len() as u16,
306 ModbusRequest::WriteMultipleRegisters { values, .. } => values.len() as u16,
307 }
308 }
309
310 pub fn is_bit_read(&self) -> bool {
312 matches!(
313 self,
314 ModbusRequest::ReadCoils { .. } | ModbusRequest::ReadDiscreteInputs { .. }
315 )
316 }
317
318 pub fn is_register_read(&self) -> bool {
320 matches!(
321 self,
322 ModbusRequest::ReadHoldingRegisters { .. } | ModbusRequest::ReadInputRegisters { .. }
323 )
324 }
325
326 pub fn is_write(&self) -> bool {
328 !self.is_bit_read() && !self.is_register_read()
329 }
330}
331
332pub fn parse_request(pdu: &[u8]) -> Result<ModbusRequest, u8> {
338 let Some(&fc) = pdu.first() else {
339 return Err(EXC_ILLEGAL_FUNCTION);
340 };
341 let body = &pdu[1..];
342
343 let pair = |b: &[u8]| -> Option<(u16, u16)> {
345 if b.len() < 4 {
346 None
347 } else {
348 Some((
349 u16::from_be_bytes([b[0], b[1]]),
350 u16::from_be_bytes([b[2], b[3]]),
351 ))
352 }
353 };
354
355 match fc {
356 FC_READ_COILS | FC_READ_DISCRETE_INPUTS => {
357 let (start, quantity) = pair(body).ok_or(EXC_ILLEGAL_DATA_VALUE)?;
358 if !(1..=2000).contains(&quantity) {
359 return Err(EXC_ILLEGAL_DATA_VALUE);
360 }
361 check_range(start, quantity)?;
362 Ok(if fc == FC_READ_COILS {
363 ModbusRequest::ReadCoils { start, quantity }
364 } else {
365 ModbusRequest::ReadDiscreteInputs { start, quantity }
366 })
367 }
368 FC_READ_HOLDING_REGISTERS | FC_READ_INPUT_REGISTERS => {
369 let (start, quantity) = pair(body).ok_or(EXC_ILLEGAL_DATA_VALUE)?;
370 if !(1..=125).contains(&quantity) {
371 return Err(EXC_ILLEGAL_DATA_VALUE);
372 }
373 check_range(start, quantity)?;
374 Ok(if fc == FC_READ_HOLDING_REGISTERS {
375 ModbusRequest::ReadHoldingRegisters { start, quantity }
376 } else {
377 ModbusRequest::ReadInputRegisters { start, quantity }
378 })
379 }
380 FC_WRITE_SINGLE_COIL => {
381 let (address, raw) = pair(body).ok_or(EXC_ILLEGAL_DATA_VALUE)?;
382 let value = match raw {
383 0x0000 => false,
384 0xFF00 => true,
385 _ => return Err(EXC_ILLEGAL_DATA_VALUE),
388 };
389 Ok(ModbusRequest::WriteSingleCoil { address, value })
390 }
391 FC_WRITE_SINGLE_REGISTER => {
392 let (address, value) = pair(body).ok_or(EXC_ILLEGAL_DATA_VALUE)?;
393 Ok(ModbusRequest::WriteSingleRegister { address, value })
394 }
395 FC_WRITE_MULTIPLE_COILS => {
396 let (start, quantity) = pair(body).ok_or(EXC_ILLEGAL_DATA_VALUE)?;
397 if !(1..=1968).contains(&quantity) {
398 return Err(EXC_ILLEGAL_DATA_VALUE);
399 }
400 check_range(start, quantity)?;
401 let byte_count = *body.get(4).ok_or(EXC_ILLEGAL_DATA_VALUE)? as usize;
402 let expected = quantity.div_ceil(8) as usize;
403 if byte_count != expected || body.len() < 5 + byte_count {
404 return Err(EXC_ILLEGAL_DATA_VALUE);
405 }
406 let data = &body[5..5 + byte_count];
407 let values = (0..quantity as usize)
408 .map(|i| data[i / 8] & (1 << (i % 8)) != 0)
409 .collect();
410 Ok(ModbusRequest::WriteMultipleCoils { start, values })
411 }
412 FC_WRITE_MULTIPLE_REGISTERS => {
413 let (start, quantity) = pair(body).ok_or(EXC_ILLEGAL_DATA_VALUE)?;
414 if !(1..=123).contains(&quantity) {
415 return Err(EXC_ILLEGAL_DATA_VALUE);
416 }
417 check_range(start, quantity)?;
418 let byte_count = *body.get(4).ok_or(EXC_ILLEGAL_DATA_VALUE)? as usize;
419 if byte_count != quantity as usize * 2 || body.len() < 5 + byte_count {
420 return Err(EXC_ILLEGAL_DATA_VALUE);
421 }
422 let data = &body[5..5 + byte_count];
423 let values = data
424 .chunks_exact(2)
425 .map(|c| u16::from_be_bytes([c[0], c[1]]))
426 .collect();
427 Ok(ModbusRequest::WriteMultipleRegisters { start, values })
428 }
429 _ => Err(EXC_ILLEGAL_FUNCTION),
430 }
431}
432
433fn check_range(start: u16, quantity: u16) -> Result<(), u8> {
435 if start as u32 + quantity as u32 > 0x1_0000 {
436 Err(EXC_ILLEGAL_DATA_ADDRESS)
437 } else {
438 Ok(())
439 }
440}
441
442pub const MAX_READ_RESPONSE_DATA: usize = MAX_PDU_LEN - 2;
450
451pub fn encode_bits_response(function_code: u8, values: &[bool]) -> Result<Vec<u8>, EncodeError> {
458 let byte_count = values.len().div_ceil(8);
459 if byte_count > MAX_READ_RESPONSE_DATA {
460 return Err(EncodeError::ByteCount {
461 values: values.len(),
462 bytes: byte_count,
463 });
464 }
465 let mut pdu = Vec::with_capacity(2 + byte_count);
466 pdu.push(function_code);
467 pdu.push(byte_count as u8);
469 let mut packed = vec![0u8; byte_count];
470 for (i, &on) in values.iter().enumerate() {
471 if on {
472 packed[i / 8] |= 1 << (i % 8);
473 }
474 }
475 pdu.extend_from_slice(&packed);
476 Ok(pdu)
477}
478
479pub fn encode_registers_response(
484 function_code: u8,
485 values: &[u16],
486) -> Result<Vec<u8>, EncodeError> {
487 let byte_count = values.len() * 2;
488 if byte_count > MAX_READ_RESPONSE_DATA {
489 return Err(EncodeError::ByteCount {
490 values: values.len(),
491 bytes: byte_count,
492 });
493 }
494 let mut pdu = Vec::with_capacity(2 + byte_count);
495 pdu.push(function_code);
496 pdu.push(byte_count as u8);
498 for v in values {
499 pdu.extend_from_slice(&v.to_be_bytes());
500 }
501 Ok(pdu)
502}
503
504pub fn encode_write_ack(request: &ModbusRequest) -> Vec<u8> {
510 let mut pdu = Vec::with_capacity(5);
511 pdu.push(request.function_code());
512 match request {
513 ModbusRequest::WriteSingleCoil { address, value } => {
514 pdu.extend_from_slice(&address.to_be_bytes());
515 pdu.extend_from_slice(&if *value { 0xFF00u16 } else { 0x0000u16 }.to_be_bytes());
516 }
517 ModbusRequest::WriteSingleRegister { address, value } => {
518 pdu.extend_from_slice(&address.to_be_bytes());
519 pdu.extend_from_slice(&value.to_be_bytes());
520 }
521 ModbusRequest::WriteMultipleCoils { start, values } => {
522 pdu.extend_from_slice(&start.to_be_bytes());
523 pdu.extend_from_slice(&(values.len() as u16).to_be_bytes());
524 }
525 ModbusRequest::WriteMultipleRegisters { start, values } => {
526 pdu.extend_from_slice(&start.to_be_bytes());
527 pdu.extend_from_slice(&(values.len() as u16).to_be_bytes());
528 }
529 _ => {
532 pdu.clear();
533 pdu.push(request.function_code() | EXCEPTION_FLAG);
534 pdu.push(EXC_SERVER_DEVICE_FAILURE);
535 }
536 }
537 pdu
538}
539
540pub fn encode_exception(function_code: u8, exception_code: u8) -> Vec<u8> {
542 vec![function_code | EXCEPTION_FLAG, exception_code]
543}
544
545pub fn encode_request(request: &ModbusRequest) -> Result<Vec<u8>, String> {
559 let mut pdu = Vec::with_capacity(8);
560 pdu.push(request.function_code());
561 let quantity_of = |n: usize, what: &str| -> Result<u16, String> {
562 u16::try_from(n).map_err(|_| format!("{n} {what} do not fit a Modbus quantity"))
563 };
564 let byte_count_of = |octets: usize, n: usize, what: &str| -> Result<u8, String> {
565 u8::try_from(octets)
566 .map_err(|_| format!("{n} {what} need {octets} octets, past a one-octet byte count"))
567 };
568 match request {
569 ModbusRequest::ReadCoils { start, quantity }
570 | ModbusRequest::ReadDiscreteInputs { start, quantity }
571 | ModbusRequest::ReadHoldingRegisters { start, quantity }
572 | ModbusRequest::ReadInputRegisters { start, quantity } => {
573 pdu.extend_from_slice(&start.to_be_bytes());
574 pdu.extend_from_slice(&quantity.to_be_bytes());
575 }
576 ModbusRequest::WriteSingleCoil { address, value } => {
577 pdu.extend_from_slice(&address.to_be_bytes());
578 pdu.extend_from_slice(&if *value { 0xFF00u16 } else { 0x0000 }.to_be_bytes());
579 }
580 ModbusRequest::WriteSingleRegister { address, value } => {
581 pdu.extend_from_slice(&address.to_be_bytes());
582 pdu.extend_from_slice(&value.to_be_bytes());
583 }
584 ModbusRequest::WriteMultipleCoils { start, values } => {
585 let quantity = quantity_of(values.len(), "coils")?;
586 let octets = values.len().div_ceil(8);
587 pdu.extend_from_slice(&start.to_be_bytes());
588 pdu.extend_from_slice(&quantity.to_be_bytes());
589 pdu.push(byte_count_of(octets, values.len(), "coils")?);
590 let mut packed = vec![0u8; octets];
591 for (i, &on) in values.iter().enumerate() {
592 if on {
593 packed[i / 8] |= 1 << (i % 8);
594 }
595 }
596 pdu.extend_from_slice(&packed);
597 }
598 ModbusRequest::WriteMultipleRegisters { start, values } => {
599 let quantity = quantity_of(values.len(), "registers")?;
600 let octets = values.len() * 2;
601 pdu.extend_from_slice(&start.to_be_bytes());
602 pdu.extend_from_slice(&quantity.to_be_bytes());
603 pdu.push(byte_count_of(octets, values.len(), "registers")?);
604 for v in values {
605 pdu.extend_from_slice(&v.to_be_bytes());
606 }
607 }
608 }
609 match parse_request(&pdu) {
610 Ok(decoded) if &decoded == request => Ok(pdu),
611 Ok(_) => Err("the request does not survive its own encoding".to_string()),
612 Err(code) => Err(format!(
613 "{} at address {} for {} item(s) is not a legal Modbus request ({})",
614 request.function_name(),
615 request.start_address(),
616 request.quantity(),
617 exception_name(code)
618 )),
619 }
620}
621
622#[derive(Debug, Clone, PartialEq, Eq)]
624pub enum ModbusResponse {
625 Bits(Vec<bool>),
627 Registers(Vec<u16>),
629 WriteAck,
631 Exception { code: u8 },
633}
634
635pub fn parse_response(pdu: &[u8], request: &ModbusRequest) -> Result<ModbusResponse, String> {
642 let fc = request.function_code();
643 let Some(&got) = pdu.first() else {
644 return Err("empty response PDU".to_string());
645 };
646 if got == fc | EXCEPTION_FLAG {
647 return match pdu {
648 [_, code] => Ok(ModbusResponse::Exception { code: *code }),
649 _ => Err(format!(
650 "exception response is {} octets; it is exactly 2",
651 pdu.len()
652 )),
653 };
654 }
655 if got != fc {
656 return Err(format!(
657 "response carries function code {got:#04x}; the request was {fc:#04x}"
658 ));
659 }
660 let quantity = usize::from(request.quantity());
661 if request.is_bit_read() || request.is_register_read() {
662 let Some((&byte_count, data)) = pdu[1..].split_first() else {
663 return Err("read response has no byte count".to_string());
664 };
665 let expected = if request.is_bit_read() {
666 quantity.div_ceil(8)
667 } else {
668 quantity * 2
669 };
670 if usize::from(byte_count) != expected || data.len() != expected {
671 return Err(format!(
672 "read response declares {byte_count} octets and carries {}; {quantity} \
673 item(s) need {expected}",
674 data.len()
675 ));
676 }
677 return Ok(if request.is_bit_read() {
678 ModbusResponse::Bits(
679 (0..quantity)
680 .map(|i| data[i / 8] & (1 << (i % 8)) != 0)
681 .collect(),
682 )
683 } else {
684 ModbusResponse::Registers(
685 data.chunks_exact(2)
686 .map(|c| u16::from_be_bytes([c[0], c[1]]))
687 .collect(),
688 )
689 });
690 }
691 let echo = encode_write_ack(request);
693 if pdu == echo.as_slice() {
694 Ok(ModbusResponse::WriteAck)
695 } else {
696 Err(format!(
697 "write acknowledgement {} does not echo the request {}",
698 hex::encode(pdu),
699 hex::encode(&echo)
700 ))
701 }
702}