///|
/// Convert a byte array to a printable uppercase hexadecimal string.
pub fn bytes_to_hex_string(bytes : Array[Byte]) -> String {
let builder = StringBuilder()
for byte in bytes {
builder.write_char(hex_char(byte >> 4))
builder.write_char(hex_char(byte & 15))
}
builder.to_string()
}
///|
fn hex_char(value : Byte) -> Char {
if value < 10 {
(value + 48).to_char()
} else {
(value + 55).to_char()
}
}
///|
/// Convert a string containing hexadecimal digits and optional whitespace to bytes.
pub fn hex_string_to_bytes(text : String) -> Result[Array[Byte], ModbusError] {
let filtered : Array[Byte] = []
for character in text {
if !character.is_ascii_whitespace() {
filtered.push(character.to_int().to_byte())
}
}
decode_hex(filtered)
}
///|
/// Format a complete frame for logs without exposing transport framing.
pub fn format_frame(frame : Frame) -> String {
let payload = pdu_bytes(frame)
"unit=" +
frame.unit_id.to_string() +
" function=" +
function_name(frame.pdu.function) +
" data=" +
bytes_to_hex_string(payload)
}
///|
/// Format a frame with its RTU, ASCII, or TCP wire representation.
pub fn format_wire_frame(
mode : Mode,
transaction_id : UInt16,
frame : Frame,
) -> String {
let bytes = encode_mode(mode, frame)
let prefix = if mode == Tcp {
"tx=" + transaction_id.to_string() + " "
} else {
""
}
prefix + mode_name(mode) + " " + bytes_to_hex_string(bytes)
}
///|
/// Parse a wire representation in the selected mode.
pub fn parse_wire_frame(
mode : Mode,
text : String,
) -> Result[(UInt16, Frame), ModbusError] {
let bytes = if mode == Ascii {
let raw : Array[Byte] = []
for character in text {
raw.push(character.to_int().to_byte())
}
raw
} else {
match hex_string_to_bytes(text) {
Ok(value) => value
Err(error) => return Err(error)
}
}
decode_transaction(mode, bytes)
}
///|
/// A bounded text trace used by CLI tools and embedded diagnostics.
pub struct FrameTextLog {
entries : Array[String]
max_entries : Int
}
///|
pub fn FrameTextLog::new(
max_entries? : Int = 256,
) -> Result[FrameTextLog, ModbusError] {
if max_entries < 1 {
Err(CapacityExceeded)
} else {
Ok({ entries: [], max_entries })
}
}
///|
pub fn FrameTextLog::push(self : FrameTextLog, line : String) -> Unit {
if self.entries.length() >= self.max_entries {
discard_string_prefix(self.entries, 1)
}
self.entries.push(line)
}
///|
pub fn FrameTextLog::record(
self : FrameTextLog,
direction : String,
frame : Frame,
) -> Unit {
self.push(direction + " " + format_frame(frame))
}
///|
pub fn FrameTextLog::record_wire(
self : FrameTextLog,
direction : String,
mode : Mode,
transaction_id : UInt16,
frame : Frame,
) -> Unit {
self.push(direction + " " + format_wire_frame(mode, transaction_id, frame))
}
///|
pub fn FrameTextLog::length(self : FrameTextLog) -> Int {
self.entries.length()
}
///|
pub fn FrameTextLog::lines(self : FrameTextLog) -> Array[String] {
let out : Array[String] = []
for line in self.entries {
out.push(line)
}
out
}
///|
pub fn FrameTextLog::clear(self : FrameTextLog) -> Unit {
self.entries.clear()
}
///|
fn discard_string_prefix(values : Array[String], count : Int) -> Unit {
let retained : Array[String] = []
for index in count.. String {
"modbus error: " + error_name(error)
}