///|
/// A range request planner that splits large logical reads into legal PDUs.
pub(all) struct PlannedRequest {
id : Int
frame : Frame
address : AddressRange
}
///|
pub fn plan_read_registers(
unit_id : Byte,
address : UInt16,
quantity : Int,
function? : Byte = 3,
) -> Result[Array[PlannedRequest], ModbusError] {
if function != 3 && function != 4 {
return Err(InvalidFunction)
}
let range = match AddressRange::new(address, quantity) {
Ok(value) => value
Err(error) => return Err(error)
}
let segments = match range.split(125) {
Ok(value) => value
Err(error) => return Err(error)
}
let out : Array[PlannedRequest] = []
for index, segment in segments {
let request = if function == 3 {
read_holding_registers(unit_id, segment.start, segment.length.to_uint16())
} else {
read_input_registers(unit_id, segment.start, segment.length.to_uint16())
}
match request {
Ok(frame) => out.push({ id: index, frame, address: segment })
Err(error) => return Err(error)
}
}
Ok(out)
}
///|
pub fn plan_read_coils(
unit_id : Byte,
address : UInt16,
quantity : Int,
discrete? : Bool = false,
) -> Result[Array[PlannedRequest], ModbusError] {
let range = match AddressRange::new(address, quantity) {
Ok(value) => value
Err(error) => return Err(error)
}
let segments = match range.split(2000) {
Ok(value) => value
Err(error) => return Err(error)
}
let out : Array[PlannedRequest] = []
for index, segment in segments {
let request = if discrete {
read_discrete_inputs(unit_id, segment.start, segment.length.to_uint16())
} else {
read_coils(unit_id, segment.start, segment.length.to_uint16())
}
match request {
Ok(frame) => out.push({ id: index, frame, address: segment })
Err(error) => return Err(error)
}
}
Ok(out)
}
///|
/// Plan a write block, respecting the protocol's maximum register quantity.
pub fn plan_write_registers(
unit_id : Byte,
address : UInt16,
values : Array[UInt16],
) -> Result[Array[PlannedRequest], ModbusError] {
if values.length() == 0 {
return Err(InvalidQuantity)
}
let range = match AddressRange::new(address, values.length()) {
Ok(value) => value
Err(error) => return Err(error)
}
let out : Array[PlannedRequest] = []
let mut offset = 0
let mut id = 0
while offset < values.length() {
let length = if values.length() - offset > 123 {
123
} else {
values.length() - offset
}
let chunk : Array[UInt16] = []
for index in offset..<(offset + length) {
chunk.push(values[index])
}
let current_address = (address.to_int() + offset).to_uint16()
match write_multiple_registers(unit_id, current_address, chunk) {
Ok(frame) => {
let segment = range.slice(offset, length).unwrap()
out.push({ id, frame, address: segment })
}
Err(error) => return Err(error)
}
offset += length
id += 1
}
Ok(out)
}
///|
/// A request plan summary used to size queues before dispatch.
pub(all) struct PlanSummary {
requests : Int
total_registers : Int
total_coils : Int
total_bytes : Int
}
///|
pub fn summarize_plan(items : Array[PlannedRequest]) -> PlanSummary {
let mut registers = 0
let mut coils = 0
let mut bytes = 0
for item in items {
let function = item.frame.pdu.function
if function == 1 || function == 2 {
coils += item.address.length
} else {
registers += item.address.length
}
bytes += item.frame.pdu.data.length()
}
{
requests: items.length(),
total_registers: registers,
total_coils: coils,
total_bytes: bytes,
}
}
///|
/// Validate that planned ranges are ordered and non-overlapping.
pub fn validate_plan_order(
items : Array[PlannedRequest],
) -> Result[Unit, ModbusError] {
for index in 1..