#[cfg(not(feature = "std"))]
use alloc::{format, string::ToString, vec::Vec};
use core::fmt;
use crate::error::{ModbusError, ModbusResult};
pub type ModbusAddress = u16;
pub type ModbusValue = u16;
pub type SlaveId = u8;
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum ModbusFunction {
ReadCoils = 0x01,
ReadDiscreteInputs = 0x02,
ReadHoldingRegisters = 0x03,
ReadInputRegisters = 0x04,
WriteSingleCoil = 0x05,
WriteSingleRegister = 0x06,
WriteMultipleCoils = 0x0F,
WriteMultipleRegisters = 0x10,
}
impl ModbusFunction {
pub fn from_u8(value: u8) -> ModbusResult<Self> {
match value {
0x01 => Ok(ModbusFunction::ReadCoils),
0x02 => Ok(ModbusFunction::ReadDiscreteInputs),
0x03 => Ok(ModbusFunction::ReadHoldingRegisters),
0x04 => Ok(ModbusFunction::ReadInputRegisters),
0x05 => Ok(ModbusFunction::WriteSingleCoil),
0x06 => Ok(ModbusFunction::WriteSingleRegister),
0x0F => Ok(ModbusFunction::WriteMultipleCoils),
0x10 => Ok(ModbusFunction::WriteMultipleRegisters),
_ => Err(ModbusError::invalid_function(value)),
}
}
pub fn to_u8(self) -> u8 {
self as u8
}
pub fn is_read_function(self) -> bool {
matches!(
self,
ModbusFunction::ReadCoils
| ModbusFunction::ReadDiscreteInputs
| ModbusFunction::ReadHoldingRegisters
| ModbusFunction::ReadInputRegisters
)
}
pub fn is_write_function(self) -> bool {
matches!(
self,
ModbusFunction::WriteSingleCoil
| ModbusFunction::WriteSingleRegister
| ModbusFunction::WriteMultipleCoils
| ModbusFunction::WriteMultipleRegisters
)
}
}
impl fmt::Display for ModbusFunction {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match self {
ModbusFunction::ReadCoils => "Read Coils",
ModbusFunction::ReadDiscreteInputs => "Read Discrete Inputs",
ModbusFunction::ReadHoldingRegisters => "Read Holding Registers",
ModbusFunction::ReadInputRegisters => "Read Input Registers",
ModbusFunction::WriteSingleCoil => "Write Single Coil",
ModbusFunction::WriteSingleRegister => "Write Single Register",
ModbusFunction::WriteMultipleCoils => "Write Multiple Coils",
ModbusFunction::WriteMultipleRegisters => "Write Multiple Registers",
};
write!(f, "{} (0x{:02X})", name, *self as u8)
}
}
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ModbusException {
IllegalFunction = 0x01,
IllegalDataAddress = 0x02,
IllegalDataValue = 0x03,
ServerDeviceFailure = 0x04,
Acknowledge = 0x05,
ServerDeviceBusy = 0x06,
MemoryParityError = 0x08,
GatewayPathUnavailable = 0x0A,
GatewayTargetDeviceFailedToRespond = 0x0B,
}
impl ModbusException {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0x01 => Some(ModbusException::IllegalFunction),
0x02 => Some(ModbusException::IllegalDataAddress),
0x03 => Some(ModbusException::IllegalDataValue),
0x04 => Some(ModbusException::ServerDeviceFailure),
0x05 => Some(ModbusException::Acknowledge),
0x06 => Some(ModbusException::ServerDeviceBusy),
0x08 => Some(ModbusException::MemoryParityError),
0x0A => Some(ModbusException::GatewayPathUnavailable),
0x0B => Some(ModbusException::GatewayTargetDeviceFailedToRespond),
_ => None,
}
}
pub fn to_u8(self) -> u8 {
self as u8
}
pub fn description(self) -> &'static str {
match self {
ModbusException::IllegalFunction => "The function code received in the query is not an allowable action for the server",
ModbusException::IllegalDataAddress => "The data address received in the query is not an allowable address for the server",
ModbusException::IllegalDataValue => "A value contained in the query data field is not an allowable value for server",
ModbusException::ServerDeviceFailure => "An unrecoverable error occurred while the server was attempting to perform the requested action",
ModbusException::Acknowledge => "The server has accepted the request and is processing it, but a long duration of time will be required to do so",
ModbusException::ServerDeviceBusy => "The server is engaged in processing a long-duration program command",
ModbusException::MemoryParityError => "The server attempted to read record file, but detected a parity error in the memory",
ModbusException::GatewayPathUnavailable => "Gateway was unable to allocate an internal communication path",
ModbusException::GatewayTargetDeviceFailedToRespond => "No response was obtained from the target device",
}
}
}
impl fmt::Display for ModbusException {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Modbus Exception 0x{:02X}: {}",
self.to_u8(),
self.description()
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ModbusRequest {
pub slave_id: SlaveId,
pub function: ModbusFunction,
pub address: ModbusAddress,
pub quantity: u16,
pub data: Vec<u8>,
}
impl ModbusRequest {
pub fn new_read(
slave_id: SlaveId,
function: ModbusFunction,
address: ModbusAddress,
quantity: u16,
) -> Self {
Self {
slave_id,
function,
address,
quantity,
data: Vec::new(),
}
}
pub fn new_write(
slave_id: SlaveId,
function: ModbusFunction,
address: ModbusAddress,
data: Vec<u8>,
) -> Self {
let quantity = match function {
ModbusFunction::WriteSingleCoil | ModbusFunction::WriteSingleRegister => 1,
ModbusFunction::WriteMultipleCoils => data.len() as u16 * 8,
ModbusFunction::WriteMultipleRegisters => data.len() as u16 / 2,
_ => 0,
};
Self {
slave_id,
function,
address,
quantity,
data,
}
}
pub fn validate(&self) -> ModbusResult<()> {
if self.slave_id > 247 {
return Err(ModbusError::invalid_data(format!(
"Invalid slave ID: {} (must be 0-247)",
self.slave_id
)));
}
if self.slave_id == 0 && self.function.is_read_function() {
return Err(ModbusError::invalid_data(
"Broadcast (slave_id=0) is only valid for write operations",
));
}
if self.function.is_read_function() {
validate_address_range(self.address, self.quantity)?;
match self.function {
ModbusFunction::ReadCoils | ModbusFunction::ReadDiscreteInputs => {
if self.quantity > crate::MAX_READ_COILS as u16 {
return Err(ModbusError::invalid_data(format!(
"Too many coils requested: {}",
self.quantity
)));
}
}
ModbusFunction::ReadHoldingRegisters | ModbusFunction::ReadInputRegisters => {
if self.quantity > crate::MAX_READ_REGISTERS as u16 {
return Err(ModbusError::invalid_data(format!(
"Too many registers requested: {}",
self.quantity
)));
}
}
_ => {}
}
}
match self.function {
ModbusFunction::WriteSingleCoil => {
validate_address_range(self.address, 1)?;
match self.data.as_slice() {
[_] => {}
[hi, lo] if u16::from_be_bytes([*hi, *lo]) == 0x0000 => {}
[hi, lo] if u16::from_be_bytes([*hi, *lo]) == 0xFF00 => {}
_ => {
return Err(ModbusError::invalid_data(
"Invalid single coil payload; expected one boolean byte or 0x0000/0xFF00",
));
}
}
}
ModbusFunction::WriteSingleRegister => {
validate_address_range(self.address, 1)?;
if self.data.len() != 2 {
return Err(ModbusError::invalid_data(format!(
"Invalid single register payload length: expected 2, got {}",
self.data.len()
)));
}
}
ModbusFunction::WriteMultipleCoils => {
validate_address_range(self.address, self.quantity)?;
if self.quantity > crate::MAX_WRITE_COILS as u16 {
return Err(ModbusError::invalid_data(format!(
"Too many coils to write: {}",
self.quantity
)));
}
let expected_bytes = usize::from(self.quantity.div_ceil(8));
if self.data.len() != expected_bytes {
return Err(ModbusError::invalid_data(format!(
"Invalid coil payload length: expected {}, got {}",
expected_bytes,
self.data.len()
)));
}
}
ModbusFunction::WriteMultipleRegisters => {
validate_address_range(self.address, self.quantity)?;
if self.quantity > crate::MAX_WRITE_REGISTERS as u16 {
return Err(ModbusError::invalid_data(format!(
"Too many registers to write: {}",
self.quantity
)));
}
let expected_bytes = usize::from(self.quantity) * 2;
if self.data.len() != expected_bytes {
return Err(ModbusError::invalid_data(format!(
"Invalid register payload length: expected {}, got {}",
expected_bytes,
self.data.len()
)));
}
}
_ => {}
}
Ok(())
}
}
#[inline]
fn validate_address_range(address: ModbusAddress, quantity: u16) -> ModbusResult<()> {
if quantity == 0 {
return Err(ModbusError::invalid_address(address, quantity));
}
if address.checked_add(quantity - 1).is_none() {
return Err(ModbusError::invalid_address(address, quantity));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq)]
pub struct ModbusResponse {
pub slave_id: SlaveId,
pub function: ModbusFunction,
buffer: Vec<u8>,
data_offset: usize,
data_len: usize,
pub exception: Option<ModbusException>,
}
impl ModbusResponse {
pub fn new_success(slave_id: SlaveId, function: ModbusFunction, data: Vec<u8>) -> Self {
let data_len = data.len();
Self {
slave_id,
function,
buffer: data,
data_offset: 0,
data_len,
exception: None,
}
}
#[inline]
pub fn new_from_frame(
frame: Vec<u8>,
slave_id: SlaveId,
function: ModbusFunction,
data_start: usize,
data_len: usize,
) -> Self {
Self {
slave_id,
function,
buffer: frame,
data_offset: data_start,
data_len,
exception: None,
}
}
pub fn new_broadcast_ack(function: ModbusFunction) -> Self {
Self {
slave_id: 0,
function,
buffer: Vec::new(),
data_offset: 0,
data_len: 0,
exception: None,
}
}
pub fn new_exception(slave_id: SlaveId, function: ModbusFunction, exception_code: u8) -> Self {
let exception = ModbusException::from_u8(exception_code);
Self {
slave_id,
function,
buffer: Vec::new(),
data_offset: 0,
data_len: 0,
exception,
}
}
#[inline]
pub fn data(&self) -> &[u8] {
&self.buffer[self.data_offset..self.data_offset + self.data_len]
}
#[inline]
pub fn data_len(&self) -> usize {
self.data_len
}
#[inline]
pub fn is_exception(&self) -> bool {
self.exception.is_some()
}
pub fn get_exception(&self) -> Option<ModbusError> {
self.exception
.map(|exc| ModbusError::protocol(format!("Modbus exception: {}", exc)))
}
pub fn parse_registers(&self) -> ModbusResult<Vec<u16>> {
if self.is_exception() {
return Err(self.get_exception().unwrap());
}
let data = self.data();
if data.is_empty() {
return Err(ModbusError::frame("Empty response data"));
}
let byte_count = data[0] as usize;
if data.len() < 1 + byte_count {
return Err(ModbusError::frame("Incomplete register data"));
}
if byte_count % 2 != 0 {
return Err(ModbusError::frame("Invalid register data length"));
}
let mut registers = Vec::with_capacity(byte_count / 2);
for i in (1..1 + byte_count).step_by(2) {
let value = u16::from_be_bytes([data[i], data[i + 1]]);
registers.push(value);
}
Ok(registers)
}
pub fn parse_bits(&self) -> ModbusResult<Vec<bool>> {
if self.is_exception() {
return Err(self.get_exception().unwrap());
}
let data = self.data();
if data.is_empty() {
return Err(ModbusError::frame("Empty response data"));
}
let byte_count = data[0] as usize;
if data.len() < 1 + byte_count {
return Err(ModbusError::frame("Incomplete bit data"));
}
let mut bits = Vec::with_capacity(byte_count * 8);
for &byte_value in data.iter().skip(1).take(byte_count) {
for bit_pos in 0..8 {
bits.push((byte_value & (1 << bit_pos)) != 0);
}
}
Ok(bits)
}
}
pub mod data_utils {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::vec;
pub fn registers_to_bytes(registers: &[u16]) -> Vec<u8> {
let mut bytes = Vec::with_capacity(registers.len() * 2);
for ®ister in registers {
bytes.extend_from_slice(®ister.to_be_bytes());
}
bytes
}
pub fn bytes_to_registers(bytes: &[u8]) -> ModbusResult<Vec<u16>> {
if bytes.len() % 2 != 0 {
return Err(ModbusError::invalid_data(
"Byte array length must be even".to_string(),
));
}
let mut registers = Vec::new();
for chunk in bytes.chunks(2) {
let value = u16::from_be_bytes([chunk[0], chunk[1]]);
registers.push(value);
}
Ok(registers)
}
pub fn pack_bits(bits: &[bool]) -> Vec<u8> {
let byte_count = bits.len().div_ceil(8);
let mut bytes = vec![0u8; byte_count];
for (i, &bit) in bits.iter().enumerate() {
if bit {
let byte_index = i / 8;
let bit_index = i % 8;
bytes[byte_index] |= 1 << bit_index;
}
}
bytes
}
pub fn unpack_bits(bytes: &[u8], bit_count: usize) -> Vec<bool> {
let mut bits = Vec::with_capacity(bit_count);
for i in 0..bit_count {
let byte_index = i / 8;
let bit_index = i % 8;
if byte_index < bytes.len() {
let bit_value = (bytes[byte_index] & (1 << bit_index)) != 0;
bits.push(bit_value);
} else {
bits.push(false);
}
}
bits
}
pub fn u32_to_registers(value: u32) -> [u16; 2] {
[(value >> 16) as u16, value as u16]
}
pub fn registers_to_u32(registers: &[u16]) -> ModbusResult<u32> {
if registers.len() < 2 {
return Err(ModbusError::invalid_data(
"Need at least 2 registers for u32".to_string(),
));
}
Ok(((registers[0] as u32) << 16) | (registers[1] as u32))
}
pub fn f32_to_registers(value: f32) -> [u16; 2] {
u32_to_registers(value.to_bits())
}
pub fn registers_to_f32(registers: &[u16]) -> ModbusResult<f32> {
let u32_value = registers_to_u32(registers)?;
Ok(f32::from_bits(u32_value))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_function_conversion() {
assert_eq!(
ModbusFunction::from_u8(0x03).unwrap(),
ModbusFunction::ReadHoldingRegisters
);
assert_eq!(ModbusFunction::ReadHoldingRegisters.to_u8(), 0x03);
assert!(ModbusFunction::from_u8(0xFF).is_err());
}
#[test]
fn test_exception_conversion() {
assert_eq!(
ModbusException::from_u8(0x02).unwrap(),
ModbusException::IllegalDataAddress
);
assert_eq!(ModbusException::IllegalDataAddress.to_u8(), 0x02);
}
#[test]
fn test_request_validation() {
let valid_request =
ModbusRequest::new_read(1, ModbusFunction::ReadHoldingRegisters, 100, 10);
assert!(valid_request.validate().is_ok());
let invalid_slave =
ModbusRequest::new_read(0, ModbusFunction::ReadHoldingRegisters, 100, 10);
assert!(invalid_slave.validate().is_err());
let too_many_registers =
ModbusRequest::new_read(1, ModbusFunction::ReadHoldingRegisters, 100, 200);
assert!(too_many_registers.validate().is_err());
let address_overflow =
ModbusRequest::new_read(1, ModbusFunction::ReadHoldingRegisters, u16::MAX, 2);
assert!(address_overflow.validate().is_err());
let valid_write_multiple = ModbusRequest {
slave_id: 1,
function: ModbusFunction::WriteMultipleRegisters,
address: 10,
quantity: 2,
data: vec![0x12, 0x34, 0x56, 0x78],
};
assert!(valid_write_multiple.validate().is_ok());
let invalid_write_payload = ModbusRequest {
slave_id: 1,
function: ModbusFunction::WriteMultipleRegisters,
address: 10,
quantity: 2,
data: vec![0x12, 0x34],
};
assert!(invalid_write_payload.validate().is_err());
let empty_single_register =
ModbusRequest::new_write(1, ModbusFunction::WriteSingleRegister, 10, vec![]);
assert!(empty_single_register.validate().is_err());
let invalid_single_coil =
ModbusRequest::new_write(1, ModbusFunction::WriteSingleCoil, 10, vec![0x00, 0x01]);
assert!(invalid_single_coil.validate().is_err());
}
#[test]
fn test_data_utils() {
let registers = vec![0x1234, 0x5678];
let bytes = data_utils::registers_to_bytes(®isters);
assert_eq!(bytes, vec![0x12, 0x34, 0x56, 0x78]);
let back_to_registers = data_utils::bytes_to_registers(&bytes).unwrap();
assert_eq!(back_to_registers, registers);
let bits = vec![true, false, true, true, false, false, false, false];
let packed = data_utils::pack_bits(&bits);
let unpacked = data_utils::unpack_bits(&packed, bits.len());
assert_eq!(unpacked, bits);
}
#[test]
fn test_response_parsing() {
let register_data = vec![4, 0x12, 0x34, 0x56, 0x78]; let response =
ModbusResponse::new_success(1, ModbusFunction::ReadHoldingRegisters, register_data);
let registers = response.parse_registers().unwrap();
assert_eq!(registers, vec![0x1234, 0x5678]);
let bit_data = vec![1, 0b10101010]; let response = ModbusResponse::new_success(1, ModbusFunction::ReadCoils, bit_data);
let bits = response.parse_bits().unwrap();
assert!(!bits[0]); assert!(bits[1]);
assert!(!bits[2]);
assert!(bits[3]);
}
#[test]
fn test_broadcast_read_rejected() {
for fc in [
ModbusFunction::ReadCoils,
ModbusFunction::ReadDiscreteInputs,
ModbusFunction::ReadHoldingRegisters,
ModbusFunction::ReadInputRegisters,
] {
let req = ModbusRequest::new_read(0, fc, 0, 1);
let err = req.validate().unwrap_err();
assert!(
err.to_string().contains("Broadcast"),
"expected broadcast error for {fc:?}, got: {err}"
);
}
}
#[test]
fn test_broadcast_write_validates_ok() {
for fc in [
ModbusFunction::WriteSingleCoil,
ModbusFunction::WriteSingleRegister,
ModbusFunction::WriteMultipleCoils,
ModbusFunction::WriteMultipleRegisters,
] {
let req = ModbusRequest::new_write(0, fc, 0, vec![0xFF, 0x00]);
assert!(
req.validate().is_ok(),
"broadcast write should be valid for {fc:?}"
);
}
}
#[test]
fn test_broadcast_ack_response() {
let ack = ModbusResponse::new_broadcast_ack(ModbusFunction::WriteSingleRegister);
assert_eq!(ack.slave_id, 0);
assert_eq!(ack.function, ModbusFunction::WriteSingleRegister);
assert!(!ack.is_exception());
assert_eq!(ack.data_len(), 0);
assert!(ack.data().is_empty());
}
#[test]
fn test_invalid_slave_id_above_247() {
let req = ModbusRequest::new_read(248, ModbusFunction::ReadHoldingRegisters, 0, 1);
assert!(req.validate().is_err());
}
}