pub const MBAP_HEADER_LEN: usize = 7;
pub const MODBUS_PROTOCOL_ID: u16 = 0;
pub const MAX_PDU_LEN: usize = 253;
pub const MAX_ADU_LEN: usize = MBAP_HEADER_LEN + MAX_PDU_LEN;
pub const FC_READ_COILS: u8 = 0x01;
pub const FC_READ_DISCRETE_INPUTS: u8 = 0x02;
pub const FC_READ_HOLDING_REGISTERS: u8 = 0x03;
pub const FC_READ_INPUT_REGISTERS: u8 = 0x04;
pub const FC_WRITE_SINGLE_COIL: u8 = 0x05;
pub const FC_WRITE_SINGLE_REGISTER: u8 = 0x06;
pub const FC_WRITE_MULTIPLE_COILS: u8 = 0x0F;
pub const FC_WRITE_MULTIPLE_REGISTERS: u8 = 0x10;
pub const EXCEPTION_FLAG: u8 = 0x80;
pub const EXC_ILLEGAL_FUNCTION: u8 = 0x01;
pub const EXC_ILLEGAL_DATA_ADDRESS: u8 = 0x02;
pub const EXC_ILLEGAL_DATA_VALUE: u8 = 0x03;
pub const EXC_SERVER_DEVICE_FAILURE: u8 = 0x04;
pub const EXC_ACKNOWLEDGE: u8 = 0x05;
pub const EXC_SERVER_DEVICE_BUSY: u8 = 0x06;
pub const EXC_MEMORY_PARITY_ERROR: u8 = 0x08;
pub const EXC_GATEWAY_PATH_UNAVAILABLE: u8 = 0x0A;
pub const EXC_GATEWAY_TARGET_FAILED: u8 = 0x0B;
pub const UNKNOWN_EXCEPTION: &str = "unknown_exception";
pub fn exception_name(code: u8) -> &'static str {
match code {
EXC_ILLEGAL_FUNCTION => "illegal_function",
EXC_ILLEGAL_DATA_ADDRESS => "illegal_data_address",
EXC_ILLEGAL_DATA_VALUE => "illegal_data_value",
EXC_SERVER_DEVICE_FAILURE => "server_device_failure",
EXC_ACKNOWLEDGE => "acknowledge",
EXC_SERVER_DEVICE_BUSY => "server_device_busy",
EXC_MEMORY_PARITY_ERROR => "memory_parity_error",
EXC_GATEWAY_PATH_UNAVAILABLE => "gateway_path_unavailable",
EXC_GATEWAY_TARGET_FAILED => "gateway_target_device_failed_to_respond",
_ => UNKNOWN_EXCEPTION,
}
}
pub fn exception_code_from_name(name: &str) -> Option<u8> {
match name.trim().to_ascii_lowercase().as_str() {
"illegal_function" | "illegal function" => Some(EXC_ILLEGAL_FUNCTION),
"illegal_data_address" | "illegal data address" => Some(EXC_ILLEGAL_DATA_ADDRESS),
"illegal_data_value" | "illegal data value" => Some(EXC_ILLEGAL_DATA_VALUE),
"server_device_failure" | "server device failure" | "device_failure" => {
Some(EXC_SERVER_DEVICE_FAILURE)
}
"acknowledge" => Some(EXC_ACKNOWLEDGE),
"server_device_busy" | "server device busy" | "device_busy" => Some(EXC_SERVER_DEVICE_BUSY),
"memory_parity_error" | "memory parity error" => Some(EXC_MEMORY_PARITY_ERROR),
"gateway_path_unavailable" | "gateway path unavailable" => {
Some(EXC_GATEWAY_PATH_UNAVAILABLE)
}
"gateway_target_device_failed_to_respond" | "gateway_target_failed" => {
Some(EXC_GATEWAY_TARGET_FAILED)
}
_ => None,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FrameError {
NotModbus { protocol_id: u16 },
BadLength { length: u16 },
}
impl std::fmt::Display for FrameError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FrameError::NotModbus { protocol_id } => write!(
f,
"MBAP protocol identifier {protocol_id} is not 0; this is not a Modbus/TCP stream"
),
FrameError::BadLength { length } => write!(
f,
"MBAP length field {length} is outside the legal range 2..=254"
),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EncodeError {
PduLength { len: usize },
ByteCount { values: usize, bytes: usize },
}
impl std::fmt::Display for EncodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EncodeError::PduLength { len } => write!(
f,
"a Modbus PDU is 1..={MAX_PDU_LEN} octets; {len} given, which the MBAP length \
field cannot describe"
),
EncodeError::ByteCount { values, bytes } => write!(
f,
"{values} value(s) need {bytes} octets of data, past the {} a read response's \
one-octet byte count and the {MAX_PDU_LEN}-octet PDU can carry",
MAX_PDU_LEN - 2
),
}
}
}
impl std::error::Error for EncodeError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Adu {
pub transaction_id: u16,
pub unit_id: u8,
pub pdu: Vec<u8>,
}
pub fn try_parse_adu(buf: &[u8]) -> Result<Option<(Adu, usize)>, FrameError> {
if buf.len() < MBAP_HEADER_LEN {
return Ok(None);
}
let transaction_id = u16::from_be_bytes([buf[0], buf[1]]);
let protocol_id = u16::from_be_bytes([buf[2], buf[3]]);
let length = u16::from_be_bytes([buf[4], buf[5]]);
if protocol_id != MODBUS_PROTOCOL_ID {
return Err(FrameError::NotModbus { protocol_id });
}
if length < 2 || length as usize > MAX_PDU_LEN + 1 {
return Err(FrameError::BadLength { length });
}
let total = 6 + length as usize;
if buf.len() < total {
return Ok(None);
}
let unit_id = buf[6];
let pdu = buf[MBAP_HEADER_LEN..total].to_vec();
Ok(Some((
Adu {
transaction_id,
unit_id,
pdu,
},
total,
)))
}
pub fn encode_adu(transaction_id: u16, unit_id: u8, pdu: &[u8]) -> Result<Vec<u8>, EncodeError> {
if pdu.is_empty() || pdu.len() > MAX_PDU_LEN {
return Err(EncodeError::PduLength { len: pdu.len() });
}
let mut out = Vec::with_capacity(MBAP_HEADER_LEN + pdu.len());
out.extend_from_slice(&transaction_id.to_be_bytes());
out.extend_from_slice(&MODBUS_PROTOCOL_ID.to_be_bytes());
out.extend_from_slice(&((pdu.len() as u16) + 1).to_be_bytes());
out.push(unit_id);
out.extend_from_slice(pdu);
Ok(out)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModbusRequest {
ReadCoils { start: u16, quantity: u16 },
ReadDiscreteInputs { start: u16, quantity: u16 },
ReadHoldingRegisters { start: u16, quantity: u16 },
ReadInputRegisters { start: u16, quantity: u16 },
WriteSingleCoil { address: u16, value: bool },
WriteSingleRegister { address: u16, value: u16 },
WriteMultipleCoils { start: u16, values: Vec<bool> },
WriteMultipleRegisters { start: u16, values: Vec<u16> },
}
impl ModbusRequest {
pub fn function_code(&self) -> u8 {
match self {
ModbusRequest::ReadCoils { .. } => FC_READ_COILS,
ModbusRequest::ReadDiscreteInputs { .. } => FC_READ_DISCRETE_INPUTS,
ModbusRequest::ReadHoldingRegisters { .. } => FC_READ_HOLDING_REGISTERS,
ModbusRequest::ReadInputRegisters { .. } => FC_READ_INPUT_REGISTERS,
ModbusRequest::WriteSingleCoil { .. } => FC_WRITE_SINGLE_COIL,
ModbusRequest::WriteSingleRegister { .. } => FC_WRITE_SINGLE_REGISTER,
ModbusRequest::WriteMultipleCoils { .. } => FC_WRITE_MULTIPLE_COILS,
ModbusRequest::WriteMultipleRegisters { .. } => FC_WRITE_MULTIPLE_REGISTERS,
}
}
pub fn function_name(&self) -> &'static str {
match self {
ModbusRequest::ReadCoils { .. } => "read_coils",
ModbusRequest::ReadDiscreteInputs { .. } => "read_discrete_inputs",
ModbusRequest::ReadHoldingRegisters { .. } => "read_holding_registers",
ModbusRequest::ReadInputRegisters { .. } => "read_input_registers",
ModbusRequest::WriteSingleCoil { .. } => "write_single_coil",
ModbusRequest::WriteSingleRegister { .. } => "write_single_register",
ModbusRequest::WriteMultipleCoils { .. } => "write_multiple_coils",
ModbusRequest::WriteMultipleRegisters { .. } => "write_multiple_registers",
}
}
pub fn start_address(&self) -> u16 {
match self {
ModbusRequest::ReadCoils { start, .. }
| ModbusRequest::ReadDiscreteInputs { start, .. }
| ModbusRequest::ReadHoldingRegisters { start, .. }
| ModbusRequest::ReadInputRegisters { start, .. }
| ModbusRequest::WriteMultipleCoils { start, .. }
| ModbusRequest::WriteMultipleRegisters { start, .. } => *start,
ModbusRequest::WriteSingleCoil { address, .. }
| ModbusRequest::WriteSingleRegister { address, .. } => *address,
}
}
pub fn quantity(&self) -> u16 {
match self {
ModbusRequest::ReadCoils { quantity, .. }
| ModbusRequest::ReadDiscreteInputs { quantity, .. }
| ModbusRequest::ReadHoldingRegisters { quantity, .. }
| ModbusRequest::ReadInputRegisters { quantity, .. } => *quantity,
ModbusRequest::WriteSingleCoil { .. } | ModbusRequest::WriteSingleRegister { .. } => 1,
ModbusRequest::WriteMultipleCoils { values, .. } => values.len() as u16,
ModbusRequest::WriteMultipleRegisters { values, .. } => values.len() as u16,
}
}
pub fn is_bit_read(&self) -> bool {
matches!(
self,
ModbusRequest::ReadCoils { .. } | ModbusRequest::ReadDiscreteInputs { .. }
)
}
pub fn is_register_read(&self) -> bool {
matches!(
self,
ModbusRequest::ReadHoldingRegisters { .. } | ModbusRequest::ReadInputRegisters { .. }
)
}
pub fn is_write(&self) -> bool {
!self.is_bit_read() && !self.is_register_read()
}
}
pub fn parse_request(pdu: &[u8]) -> Result<ModbusRequest, u8> {
let Some(&fc) = pdu.first() else {
return Err(EXC_ILLEGAL_FUNCTION);
};
let body = &pdu[1..];
let pair = |b: &[u8]| -> Option<(u16, u16)> {
if b.len() < 4 {
None
} else {
Some((
u16::from_be_bytes([b[0], b[1]]),
u16::from_be_bytes([b[2], b[3]]),
))
}
};
match fc {
FC_READ_COILS | FC_READ_DISCRETE_INPUTS => {
let (start, quantity) = pair(body).ok_or(EXC_ILLEGAL_DATA_VALUE)?;
if !(1..=2000).contains(&quantity) {
return Err(EXC_ILLEGAL_DATA_VALUE);
}
check_range(start, quantity)?;
Ok(if fc == FC_READ_COILS {
ModbusRequest::ReadCoils { start, quantity }
} else {
ModbusRequest::ReadDiscreteInputs { start, quantity }
})
}
FC_READ_HOLDING_REGISTERS | FC_READ_INPUT_REGISTERS => {
let (start, quantity) = pair(body).ok_or(EXC_ILLEGAL_DATA_VALUE)?;
if !(1..=125).contains(&quantity) {
return Err(EXC_ILLEGAL_DATA_VALUE);
}
check_range(start, quantity)?;
Ok(if fc == FC_READ_HOLDING_REGISTERS {
ModbusRequest::ReadHoldingRegisters { start, quantity }
} else {
ModbusRequest::ReadInputRegisters { start, quantity }
})
}
FC_WRITE_SINGLE_COIL => {
let (address, raw) = pair(body).ok_or(EXC_ILLEGAL_DATA_VALUE)?;
let value = match raw {
0x0000 => false,
0xFF00 => true,
_ => return Err(EXC_ILLEGAL_DATA_VALUE),
};
Ok(ModbusRequest::WriteSingleCoil { address, value })
}
FC_WRITE_SINGLE_REGISTER => {
let (address, value) = pair(body).ok_or(EXC_ILLEGAL_DATA_VALUE)?;
Ok(ModbusRequest::WriteSingleRegister { address, value })
}
FC_WRITE_MULTIPLE_COILS => {
let (start, quantity) = pair(body).ok_or(EXC_ILLEGAL_DATA_VALUE)?;
if !(1..=1968).contains(&quantity) {
return Err(EXC_ILLEGAL_DATA_VALUE);
}
check_range(start, quantity)?;
let byte_count = *body.get(4).ok_or(EXC_ILLEGAL_DATA_VALUE)? as usize;
let expected = quantity.div_ceil(8) as usize;
if byte_count != expected || body.len() < 5 + byte_count {
return Err(EXC_ILLEGAL_DATA_VALUE);
}
let data = &body[5..5 + byte_count];
let values = (0..quantity as usize)
.map(|i| data[i / 8] & (1 << (i % 8)) != 0)
.collect();
Ok(ModbusRequest::WriteMultipleCoils { start, values })
}
FC_WRITE_MULTIPLE_REGISTERS => {
let (start, quantity) = pair(body).ok_or(EXC_ILLEGAL_DATA_VALUE)?;
if !(1..=123).contains(&quantity) {
return Err(EXC_ILLEGAL_DATA_VALUE);
}
check_range(start, quantity)?;
let byte_count = *body.get(4).ok_or(EXC_ILLEGAL_DATA_VALUE)? as usize;
if byte_count != quantity as usize * 2 || body.len() < 5 + byte_count {
return Err(EXC_ILLEGAL_DATA_VALUE);
}
let data = &body[5..5 + byte_count];
let values = data
.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
Ok(ModbusRequest::WriteMultipleRegisters { start, values })
}
_ => Err(EXC_ILLEGAL_FUNCTION),
}
}
fn check_range(start: u16, quantity: u16) -> Result<(), u8> {
if start as u32 + quantity as u32 > 0x1_0000 {
Err(EXC_ILLEGAL_DATA_ADDRESS)
} else {
Ok(())
}
}
pub const MAX_READ_RESPONSE_DATA: usize = MAX_PDU_LEN - 2;
pub fn encode_bits_response(function_code: u8, values: &[bool]) -> Result<Vec<u8>, EncodeError> {
let byte_count = values.len().div_ceil(8);
if byte_count > MAX_READ_RESPONSE_DATA {
return Err(EncodeError::ByteCount {
values: values.len(),
bytes: byte_count,
});
}
let mut pdu = Vec::with_capacity(2 + byte_count);
pdu.push(function_code);
pdu.push(byte_count as u8);
let mut packed = vec![0u8; byte_count];
for (i, &on) in values.iter().enumerate() {
if on {
packed[i / 8] |= 1 << (i % 8);
}
}
pdu.extend_from_slice(&packed);
Ok(pdu)
}
pub fn encode_registers_response(
function_code: u8,
values: &[u16],
) -> Result<Vec<u8>, EncodeError> {
let byte_count = values.len() * 2;
if byte_count > MAX_READ_RESPONSE_DATA {
return Err(EncodeError::ByteCount {
values: values.len(),
bytes: byte_count,
});
}
let mut pdu = Vec::with_capacity(2 + byte_count);
pdu.push(function_code);
pdu.push(byte_count as u8);
for v in values {
pdu.extend_from_slice(&v.to_be_bytes());
}
Ok(pdu)
}
pub fn encode_write_ack(request: &ModbusRequest) -> Vec<u8> {
let mut pdu = Vec::with_capacity(5);
pdu.push(request.function_code());
match request {
ModbusRequest::WriteSingleCoil { address, value } => {
pdu.extend_from_slice(&address.to_be_bytes());
pdu.extend_from_slice(&if *value { 0xFF00u16 } else { 0x0000u16 }.to_be_bytes());
}
ModbusRequest::WriteSingleRegister { address, value } => {
pdu.extend_from_slice(&address.to_be_bytes());
pdu.extend_from_slice(&value.to_be_bytes());
}
ModbusRequest::WriteMultipleCoils { start, values } => {
pdu.extend_from_slice(&start.to_be_bytes());
pdu.extend_from_slice(&(values.len() as u16).to_be_bytes());
}
ModbusRequest::WriteMultipleRegisters { start, values } => {
pdu.extend_from_slice(&start.to_be_bytes());
pdu.extend_from_slice(&(values.len() as u16).to_be_bytes());
}
_ => {
pdu.clear();
pdu.push(request.function_code() | EXCEPTION_FLAG);
pdu.push(EXC_SERVER_DEVICE_FAILURE);
}
}
pdu
}
pub fn encode_exception(function_code: u8, exception_code: u8) -> Vec<u8> {
vec![function_code | EXCEPTION_FLAG, exception_code]
}
pub fn encode_request(request: &ModbusRequest) -> Result<Vec<u8>, String> {
let mut pdu = Vec::with_capacity(8);
pdu.push(request.function_code());
let quantity_of = |n: usize, what: &str| -> Result<u16, String> {
u16::try_from(n).map_err(|_| format!("{n} {what} do not fit a Modbus quantity"))
};
let byte_count_of = |octets: usize, n: usize, what: &str| -> Result<u8, String> {
u8::try_from(octets)
.map_err(|_| format!("{n} {what} need {octets} octets, past a one-octet byte count"))
};
match request {
ModbusRequest::ReadCoils { start, quantity }
| ModbusRequest::ReadDiscreteInputs { start, quantity }
| ModbusRequest::ReadHoldingRegisters { start, quantity }
| ModbusRequest::ReadInputRegisters { start, quantity } => {
pdu.extend_from_slice(&start.to_be_bytes());
pdu.extend_from_slice(&quantity.to_be_bytes());
}
ModbusRequest::WriteSingleCoil { address, value } => {
pdu.extend_from_slice(&address.to_be_bytes());
pdu.extend_from_slice(&if *value { 0xFF00u16 } else { 0x0000 }.to_be_bytes());
}
ModbusRequest::WriteSingleRegister { address, value } => {
pdu.extend_from_slice(&address.to_be_bytes());
pdu.extend_from_slice(&value.to_be_bytes());
}
ModbusRequest::WriteMultipleCoils { start, values } => {
let quantity = quantity_of(values.len(), "coils")?;
let octets = values.len().div_ceil(8);
pdu.extend_from_slice(&start.to_be_bytes());
pdu.extend_from_slice(&quantity.to_be_bytes());
pdu.push(byte_count_of(octets, values.len(), "coils")?);
let mut packed = vec![0u8; octets];
for (i, &on) in values.iter().enumerate() {
if on {
packed[i / 8] |= 1 << (i % 8);
}
}
pdu.extend_from_slice(&packed);
}
ModbusRequest::WriteMultipleRegisters { start, values } => {
let quantity = quantity_of(values.len(), "registers")?;
let octets = values.len() * 2;
pdu.extend_from_slice(&start.to_be_bytes());
pdu.extend_from_slice(&quantity.to_be_bytes());
pdu.push(byte_count_of(octets, values.len(), "registers")?);
for v in values {
pdu.extend_from_slice(&v.to_be_bytes());
}
}
}
match parse_request(&pdu) {
Ok(decoded) if &decoded == request => Ok(pdu),
Ok(_) => Err("the request does not survive its own encoding".to_string()),
Err(code) => Err(format!(
"{} at address {} for {} item(s) is not a legal Modbus request ({})",
request.function_name(),
request.start_address(),
request.quantity(),
exception_name(code)
)),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModbusResponse {
Bits(Vec<bool>),
Registers(Vec<u16>),
WriteAck,
Exception { code: u8 },
}
pub fn parse_response(pdu: &[u8], request: &ModbusRequest) -> Result<ModbusResponse, String> {
let fc = request.function_code();
let Some(&got) = pdu.first() else {
return Err("empty response PDU".to_string());
};
if got == fc | EXCEPTION_FLAG {
return match pdu {
[_, code] => Ok(ModbusResponse::Exception { code: *code }),
_ => Err(format!(
"exception response is {} octets; it is exactly 2",
pdu.len()
)),
};
}
if got != fc {
return Err(format!(
"response carries function code {got:#04x}; the request was {fc:#04x}"
));
}
let quantity = usize::from(request.quantity());
if request.is_bit_read() || request.is_register_read() {
let Some((&byte_count, data)) = pdu[1..].split_first() else {
return Err("read response has no byte count".to_string());
};
let expected = if request.is_bit_read() {
quantity.div_ceil(8)
} else {
quantity * 2
};
if usize::from(byte_count) != expected || data.len() != expected {
return Err(format!(
"read response declares {byte_count} octets and carries {}; {quantity} \
item(s) need {expected}",
data.len()
));
}
return Ok(if request.is_bit_read() {
ModbusResponse::Bits(
(0..quantity)
.map(|i| data[i / 8] & (1 << (i % 8)) != 0)
.collect(),
)
} else {
ModbusResponse::Registers(
data.chunks_exact(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect(),
)
});
}
let echo = encode_write_ack(request);
if pdu == echo.as_slice() {
Ok(ModbusResponse::WriteAck)
} else {
Err(format!(
"write acknowledgement {} does not echo the request {}",
hex::encode(pdu),
hex::encode(&echo)
))
}
}