use crate::bytes::{bytes_4_to_regs, bytes_8_to_regs, regs_to_bytes_4, regs_to_bytes_8, ByteOrder};
use crate::constants;
use crate::error::{ModbusError, ModbusResult};
use crate::pdu::{ModbusPdu, PduBuilder};
use crate::value::ModbusValue;
pub struct ModbusCodec;
pub fn decode_register_value(
registers: &[u16],
data_type: &str,
bit_position: u8,
byte_order: ByteOrder,
) -> ModbusResult<ModbusValue> {
let dt = data_type;
if dt.eq_ignore_ascii_case("bool")
|| dt.eq_ignore_ascii_case("boolean")
|| dt.eq_ignore_ascii_case("coil")
{
if registers.is_empty() {
return Err(ModbusError::InvalidData {
message: "No registers for bool".to_string(),
});
}
if bit_position > 15 {
return Err(ModbusError::InvalidData {
message: format!("Invalid bit position: {} (must be 0-15)", bit_position),
});
}
let value = registers[0];
let bit_value = (value >> bit_position) & 0x01;
return Ok(ModbusValue::Bool(bit_value != 0));
}
if dt.eq_ignore_ascii_case("uint16")
|| dt.eq_ignore_ascii_case("u16")
|| dt.eq_ignore_ascii_case("word")
{
if registers.is_empty() {
return Err(ModbusError::InvalidData {
message: "No registers for uint16".to_string(),
});
}
return Ok(ModbusValue::U16(registers[0]));
}
if dt.eq_ignore_ascii_case("int16")
|| dt.eq_ignore_ascii_case("i16")
|| dt.eq_ignore_ascii_case("short")
{
if registers.is_empty() {
return Err(ModbusError::InvalidData {
message: "No registers for int16".to_string(),
});
}
return Ok(ModbusValue::I16(registers[0] as i16));
}
if dt.eq_ignore_ascii_case("uint32")
|| dt.eq_ignore_ascii_case("u32")
|| dt.eq_ignore_ascii_case("dword")
{
if registers.len() < 2 {
return Err(ModbusError::InvalidData {
message: "Not enough registers for uint32".to_string(),
});
}
let regs: [u16; 2] = [registers[0], registers[1]];
let bytes = regs_to_bytes_4(®s, byte_order);
return Ok(ModbusValue::U32(u32::from_be_bytes(bytes)));
}
if dt.eq_ignore_ascii_case("int32")
|| dt.eq_ignore_ascii_case("i32")
|| dt.eq_ignore_ascii_case("long")
{
if registers.len() < 2 {
return Err(ModbusError::InvalidData {
message: "Not enough registers for int32".to_string(),
});
}
let regs: [u16; 2] = [registers[0], registers[1]];
let bytes = regs_to_bytes_4(®s, byte_order);
return Ok(ModbusValue::I32(i32::from_be_bytes(bytes)));
}
if dt.eq_ignore_ascii_case("float32")
|| dt.eq_ignore_ascii_case("f32")
|| dt.eq_ignore_ascii_case("float")
|| dt.eq_ignore_ascii_case("real")
{
if registers.len() < 2 {
return Err(ModbusError::InvalidData {
message: "Not enough registers for float32".to_string(),
});
}
let regs: [u16; 2] = [registers[0], registers[1]];
let bytes = regs_to_bytes_4(®s, byte_order);
return Ok(ModbusValue::F32(f32::from_be_bytes(bytes)));
}
if dt.eq_ignore_ascii_case("uint64")
|| dt.eq_ignore_ascii_case("u64")
|| dt.eq_ignore_ascii_case("qword")
{
if registers.len() < 4 {
return Err(ModbusError::InvalidData {
message: "Not enough registers for uint64".to_string(),
});
}
let regs: [u16; 4] = [registers[0], registers[1], registers[2], registers[3]];
let bytes = regs_to_bytes_8(®s, byte_order);
return Ok(ModbusValue::U64(u64::from_be_bytes(bytes)));
}
if dt.eq_ignore_ascii_case("int64")
|| dt.eq_ignore_ascii_case("i64")
|| dt.eq_ignore_ascii_case("longlong")
{
if registers.len() < 4 {
return Err(ModbusError::InvalidData {
message: "Not enough registers for int64".to_string(),
});
}
let regs: [u16; 4] = [registers[0], registers[1], registers[2], registers[3]];
let bytes = regs_to_bytes_8(®s, byte_order);
return Ok(ModbusValue::I64(i64::from_be_bytes(bytes)));
}
if dt.eq_ignore_ascii_case("float64")
|| dt.eq_ignore_ascii_case("f64")
|| dt.eq_ignore_ascii_case("double")
|| dt.eq_ignore_ascii_case("lreal")
{
if registers.len() < 4 {
return Err(ModbusError::InvalidData {
message: "Not enough registers for float64".to_string(),
});
}
let regs: [u16; 4] = [registers[0], registers[1], registers[2], registers[3]];
let bytes = regs_to_bytes_8(®s, byte_order);
return Ok(ModbusValue::F64(f64::from_be_bytes(bytes)));
}
Err(ModbusError::InvalidData {
message: format!("Unsupported data type: {}", data_type),
})
}
pub fn clamp_to_data_type(value: f64, data_type: &str) -> f64 {
let dt = data_type;
let (min, max): (f64, f64) =
if dt.eq_ignore_ascii_case("uint16") || dt.eq_ignore_ascii_case("u16") {
(0.0, 65535.0)
} else if dt.eq_ignore_ascii_case("int16") || dt.eq_ignore_ascii_case("i16") {
(-32768.0, 32767.0)
} else if dt.eq_ignore_ascii_case("uint32") || dt.eq_ignore_ascii_case("u32") {
(0.0, 4294967295.0)
} else if dt.eq_ignore_ascii_case("int32") || dt.eq_ignore_ascii_case("i32") {
(-2147483648.0, 2147483647.0)
} else if dt.eq_ignore_ascii_case("uint64") || dt.eq_ignore_ascii_case("u64") {
(0.0, u64::MAX as f64)
} else if dt.eq_ignore_ascii_case("int64") || dt.eq_ignore_ascii_case("i64") {
(i64::MIN as f64, i64::MAX as f64)
} else if dt.eq_ignore_ascii_case("float32") || dt.eq_ignore_ascii_case("f32") {
(f32::MIN as f64, f32::MAX as f64)
} else if dt.eq_ignore_ascii_case("float64") || dt.eq_ignore_ascii_case("f64") {
(f64::MIN, f64::MAX)
} else {
return value;
};
value.clamp(min, max)
}
pub fn parse_read_response(
pdu: &ModbusPdu,
function_code: u8,
_expected_count: u16,
) -> ModbusResult<Vec<u16>> {
let pdu_data = pdu.as_slice();
if pdu_data.len() < 2 {
return Ok(Vec::new()); }
let actual_fc = pdu.function_code().unwrap_or(0);
if actual_fc != function_code {
return Err(ModbusError::Protocol {
message: format!(
"Function code mismatch: expected {}, got {}",
function_code, actual_fc
),
});
}
let byte_count = pdu_data[1] as usize;
let available_bytes = pdu_data.len().saturating_sub(2);
let actual_byte_count = byte_count.min(available_bytes);
match function_code {
1 | 2 => {
let mut registers = Vec::with_capacity(actual_byte_count);
for &byte in &pdu_data[2..2 + actual_byte_count] {
registers.push(u16::from(byte));
}
Ok(registers)
}
3 | 4 => {
let complete_pairs = actual_byte_count / 2;
let mut registers = Vec::with_capacity(complete_pairs);
for i in 0..complete_pairs {
let offset = 2 + i * 2;
if offset + 1 < pdu_data.len() {
let value =
(u16::from(pdu_data[offset]) << 8) | u16::from(pdu_data[offset + 1]);
registers.push(value);
}
}
Ok(registers)
}
_ => Err(ModbusError::Protocol {
message: format!("Unsupported function code: {}", function_code),
}),
}
}
pub fn encode_value(value: &ModbusValue, byte_order: ByteOrder) -> ModbusResult<Vec<u16>> {
match value {
ModbusValue::Bool(b) => Ok(vec![if *b { 1 } else { 0 }]),
ModbusValue::U16(v) => Ok(vec![*v]),
ModbusValue::I16(v) => Ok(vec![*v as u16]),
ModbusValue::U32(v) => {
let bytes = v.to_be_bytes();
Ok(bytes_4_to_regs(&bytes, byte_order).to_vec())
}
ModbusValue::I32(v) => {
let bytes = v.to_be_bytes();
Ok(bytes_4_to_regs(&bytes, byte_order).to_vec())
}
ModbusValue::F32(v) => {
let bytes = v.to_be_bytes();
Ok(bytes_4_to_regs(&bytes, byte_order).to_vec())
}
ModbusValue::U64(v) => {
let bytes = v.to_be_bytes();
Ok(bytes_8_to_regs(&bytes, byte_order).to_vec())
}
ModbusValue::I64(v) => {
let bytes = v.to_be_bytes();
Ok(bytes_8_to_regs(&bytes, byte_order).to_vec())
}
ModbusValue::F64(v) => {
let bytes = v.to_be_bytes();
Ok(bytes_8_to_regs(&bytes, byte_order).to_vec())
}
}
}
pub fn encode_f64_as_type(
value: f64,
data_type: &str,
byte_order: ByteOrder,
) -> ModbusResult<Vec<u16>> {
let clamped = clamp_to_data_type(value, data_type);
let dt = data_type;
if dt.eq_ignore_ascii_case("bool")
|| dt.eq_ignore_ascii_case("boolean")
|| dt.eq_ignore_ascii_case("coil")
{
return Ok(vec![if clamped != 0.0 { 1 } else { 0 }]);
}
if dt.eq_ignore_ascii_case("uint16")
|| dt.eq_ignore_ascii_case("u16")
|| dt.eq_ignore_ascii_case("word")
{
return Ok(vec![clamped as u16]);
}
if dt.eq_ignore_ascii_case("int16")
|| dt.eq_ignore_ascii_case("i16")
|| dt.eq_ignore_ascii_case("short")
{
return Ok(vec![(clamped as i16) as u16]);
}
if dt.eq_ignore_ascii_case("uint32")
|| dt.eq_ignore_ascii_case("u32")
|| dt.eq_ignore_ascii_case("dword")
{
let bytes = (clamped as u32).to_be_bytes();
return Ok(bytes_4_to_regs(&bytes, byte_order).to_vec());
}
if dt.eq_ignore_ascii_case("int32")
|| dt.eq_ignore_ascii_case("i32")
|| dt.eq_ignore_ascii_case("long")
{
let bytes = (clamped as i32).to_be_bytes();
return Ok(bytes_4_to_regs(&bytes, byte_order).to_vec());
}
if dt.eq_ignore_ascii_case("float32")
|| dt.eq_ignore_ascii_case("f32")
|| dt.eq_ignore_ascii_case("float")
|| dt.eq_ignore_ascii_case("real")
{
let bytes = (clamped as f32).to_be_bytes();
return Ok(bytes_4_to_regs(&bytes, byte_order).to_vec());
}
if dt.eq_ignore_ascii_case("uint64")
|| dt.eq_ignore_ascii_case("u64")
|| dt.eq_ignore_ascii_case("qword")
{
let bytes = (clamped as u64).to_be_bytes();
return Ok(bytes_8_to_regs(&bytes, byte_order).to_vec());
}
if dt.eq_ignore_ascii_case("int64")
|| dt.eq_ignore_ascii_case("i64")
|| dt.eq_ignore_ascii_case("longlong")
{
let bytes = (clamped as i64).to_be_bytes();
return Ok(bytes_8_to_regs(&bytes, byte_order).to_vec());
}
if dt.eq_ignore_ascii_case("float64")
|| dt.eq_ignore_ascii_case("f64")
|| dt.eq_ignore_ascii_case("double")
|| dt.eq_ignore_ascii_case("lreal")
{
let bytes = clamped.to_be_bytes();
return Ok(bytes_8_to_regs(&bytes, byte_order).to_vec());
}
Err(ModbusError::InvalidData {
message: format!("Unsupported data type: {}", data_type),
})
}
impl ModbusCodec {
pub fn build_fc05_pdu(address: u16, value: bool) -> ModbusResult<ModbusPdu> {
Ok(PduBuilder::new()
.function_code(0x05)?
.address(address)?
.byte(if value { 0xFF } else { 0x00 })?
.byte(0x00)?
.build())
}
pub fn build_fc06_pdu(address: u16, value: u16) -> ModbusResult<ModbusPdu> {
Ok(PduBuilder::new()
.function_code(0x06)?
.address(address)?
.quantity(value)?
.build())
}
pub fn build_fc15_pdu(start_address: u16, values: &[bool]) -> ModbusResult<ModbusPdu> {
if values.is_empty() || values.len() > constants::MAX_WRITE_COILS {
return Err(ModbusError::InvalidData {
message: "Invalid coil count for FC15".to_string(),
});
}
let mut pdu = ModbusPdu::new();
pdu.push(0x0F)?;
pdu.push_u16(start_address)?;
let quantity = values.len() as u16;
pdu.push_u16(quantity)?;
let byte_count = values.len().div_ceil(8) as u8;
pdu.push(byte_count)?;
let mut current_byte = 0u8;
let mut bit_index = 0;
for &value in values {
if value {
current_byte |= 1 << bit_index;
}
bit_index += 1;
if bit_index == 8 {
pdu.push(current_byte)?;
current_byte = 0;
bit_index = 0;
}
}
if bit_index > 0 {
pdu.push(current_byte)?;
}
Ok(pdu)
}
pub fn build_fc16_pdu(start_address: u16, values: &[u16]) -> ModbusResult<ModbusPdu> {
if values.is_empty() || values.len() > constants::MAX_WRITE_REGISTERS {
return Err(ModbusError::InvalidData {
message: "Invalid register count for FC16".to_string(),
});
}
let mut pdu = ModbusPdu::new();
pdu.push(0x10)?;
pdu.push_u16(start_address)?;
let quantity = values.len() as u16;
pdu.push_u16(quantity)?;
let byte_count = (values.len() * 2) as u8;
pdu.push(byte_count)?;
for &value in values {
pdu.push_u16(value)?;
}
Ok(pdu)
}
pub fn parse_write_response(pdu: &ModbusPdu, expected_fc: u8) -> ModbusResult<bool> {
let data = pdu.as_slice();
if data.is_empty() {
return Err(ModbusError::Protocol {
message: "Empty response PDU".to_string(),
});
}
if data[0] & 0x80 != 0 {
let exception_code = if data.len() > 1 { data[1] } else { 0 };
return Err(ModbusError::exception(data[0] & 0x7F, exception_code));
}
if data[0] != expected_fc {
return Err(ModbusError::Protocol {
message: format!(
"Function code mismatch: expected {:02X}, got {:02X}",
expected_fc, data[0]
),
});
}
Ok(true)
}
}
pub fn registers_for_type(data_type: &str) -> usize {
let dt = data_type;
if dt.eq_ignore_ascii_case("bool")
|| dt.eq_ignore_ascii_case("boolean")
|| dt.eq_ignore_ascii_case("coil")
{
0 } else if dt.eq_ignore_ascii_case("uint16")
|| dt.eq_ignore_ascii_case("u16")
|| dt.eq_ignore_ascii_case("word")
|| dt.eq_ignore_ascii_case("int16")
|| dt.eq_ignore_ascii_case("i16")
|| dt.eq_ignore_ascii_case("short")
{
1
} else if dt.eq_ignore_ascii_case("uint32")
|| dt.eq_ignore_ascii_case("u32")
|| dt.eq_ignore_ascii_case("dword")
|| dt.eq_ignore_ascii_case("int32")
|| dt.eq_ignore_ascii_case("i32")
|| dt.eq_ignore_ascii_case("long")
|| dt.eq_ignore_ascii_case("float32")
|| dt.eq_ignore_ascii_case("f32")
|| dt.eq_ignore_ascii_case("float")
|| dt.eq_ignore_ascii_case("real")
{
2
} else if dt.eq_ignore_ascii_case("uint64")
|| dt.eq_ignore_ascii_case("u64")
|| dt.eq_ignore_ascii_case("qword")
|| dt.eq_ignore_ascii_case("int64")
|| dt.eq_ignore_ascii_case("i64")
|| dt.eq_ignore_ascii_case("longlong")
|| dt.eq_ignore_ascii_case("float64")
|| dt.eq_ignore_ascii_case("f64")
|| dt.eq_ignore_ascii_case("double")
|| dt.eq_ignore_ascii_case("lreal")
{
4
} else {
1 }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_uint16() {
let registers = [0x1234];
let value = decode_register_value(®isters, "uint16", 0, ByteOrder::BigEndian).unwrap();
assert_eq!(value, ModbusValue::U16(0x1234));
}
#[test]
fn test_decode_int16() {
let registers = [0xFFFF]; let value = decode_register_value(®isters, "int16", 0, ByteOrder::BigEndian).unwrap();
assert_eq!(value, ModbusValue::I16(-1));
}
#[test]
fn test_decode_uint32_big_endian() {
let registers = [0x1234, 0x5678];
let value = decode_register_value(®isters, "uint32", 0, ByteOrder::BigEndian).unwrap();
assert_eq!(value, ModbusValue::U32(0x12345678));
}
#[test]
fn test_decode_uint32_big_endian_swap() {
let registers = [0x5678, 0x1234]; let value =
decode_register_value(®isters, "uint32", 0, ByteOrder::BigEndianSwap).unwrap();
assert_eq!(value, ModbusValue::U32(0x12345678));
}
#[test]
fn test_decode_float32() {
let registers = [0x41C8, 0x0000];
let value = decode_register_value(®isters, "float32", 0, ByteOrder::BigEndian).unwrap();
if let ModbusValue::F32(f) = value {
assert!((f - 25.0).abs() < f32::EPSILON);
} else {
panic!("Expected F32");
}
}
#[test]
fn test_decode_bool_bit_extraction() {
let registers = [0b0000_0100]; let value = decode_register_value(®isters, "bool", 2, ByteOrder::BigEndian).unwrap();
assert_eq!(value, ModbusValue::Bool(true));
let value = decode_register_value(®isters, "bool", 0, ByteOrder::BigEndian).unwrap();
assert_eq!(value, ModbusValue::Bool(false));
}
#[test]
fn test_encode_uint32_roundtrip() {
let original = ModbusValue::U32(0x12345678);
for order in [
ByteOrder::BigEndian,
ByteOrder::LittleEndian,
ByteOrder::BigEndianSwap,
ByteOrder::LittleEndianSwap,
] {
let registers = encode_value(&original, order).unwrap();
let decoded = decode_register_value(®isters, "uint32", 0, order).unwrap();
assert_eq!(decoded, original, "Roundtrip failed for {:?}", order);
}
}
#[test]
fn test_encode_float32_roundtrip() {
let original = ModbusValue::F32(123.456);
for order in [
ByteOrder::BigEndian,
ByteOrder::LittleEndian,
ByteOrder::BigEndianSwap,
ByteOrder::LittleEndianSwap,
] {
let registers = encode_value(&original, order).unwrap();
let decoded = decode_register_value(®isters, "float32", 0, order).unwrap();
if let (ModbusValue::F32(orig), ModbusValue::F32(dec)) = (&original, &decoded) {
assert!(
(orig - dec).abs() < 0.001,
"Roundtrip failed for {:?}",
order
);
} else {
panic!("Type mismatch");
}
}
}
#[test]
fn test_clamp_to_data_type() {
assert_eq!(clamp_to_data_type(70000.0, "uint16"), 65535.0);
assert_eq!(clamp_to_data_type(-100.0, "uint16"), 0.0);
assert_eq!(clamp_to_data_type(40000.0, "int16"), 32767.0);
assert_eq!(clamp_to_data_type(-40000.0, "int16"), -32768.0);
}
#[test]
fn test_registers_for_type() {
assert_eq!(registers_for_type("bool"), 0);
assert_eq!(registers_for_type("uint16"), 1);
assert_eq!(registers_for_type("int32"), 2);
assert_eq!(registers_for_type("float64"), 4);
}
#[test]
fn test_build_fc05_pdu() {
let pdu = ModbusCodec::build_fc05_pdu(0x0100, true).unwrap();
assert_eq!(pdu.as_slice(), &[0x05, 0x01, 0x00, 0xFF, 0x00]);
}
#[test]
fn test_build_fc06_pdu() {
let pdu = ModbusCodec::build_fc06_pdu(0x0100, 0x1234).unwrap();
assert_eq!(pdu.as_slice(), &[0x06, 0x01, 0x00, 0x12, 0x34]);
}
#[test]
fn test_build_fc15_pdu() {
let pdu = ModbusCodec::build_fc15_pdu(0x0100, &[true, false, true]).unwrap();
assert_eq!(
pdu.as_slice(),
&[0x0F, 0x01, 0x00, 0x00, 0x03, 0x01, 0b0000_0101]
);
}
#[test]
fn test_build_fc16_pdu() {
let pdu = ModbusCodec::build_fc16_pdu(0x0100, &[0x1234, 0x5678]).unwrap();
assert_eq!(
pdu.as_slice(),
&[0x10, 0x01, 0x00, 0x00, 0x02, 0x04, 0x12, 0x34, 0x56, 0x78]
);
}
}