///|
/// Internal byte helpers for little-endian IEC 104 fields.
fn low_byte(value : Int) -> Byte {
(value % 256).to_byte()
}
///|
fn high_byte(value : Int) -> Byte {
(value / 256 % 256).to_byte()
}
///|
fn read_u16(bytes : Bytes, offset : Int) -> Int {
bytes[offset].to_int() + bytes[offset + 1].to_int() * 256
}
///|
fn read_u32(bytes : Bytes, offset : Int) -> UInt {
bytes[offset].to_int().reinterpret_as_uint() |
(bytes[offset + 1].to_int().reinterpret_as_uint() << 8) |
(bytes[offset + 2].to_int().reinterpret_as_uint() << 16) |
(bytes[offset + 3].to_int().reinterpret_as_uint() << 24)
}
///|
fn append_u16(out : Array[Byte], value : Int) -> Unit {
out.push(low_byte(value))
out.push(high_byte(value))
}
///|
fn append_u32(out : Array[Byte], value : UInt) -> Unit {
out.push((value & 0xffU).to_byte())
out.push(((value >> 8) & 0xffU).to_byte())
out.push(((value >> 16) & 0xffU).to_byte())
out.push(((value >> 24) & 0xffU).to_byte())
}
///|
/// Encode one APCI frame, including the 0x68 start byte and length byte.
pub fn encode_frame(frame : Frame) -> Bytes {
let control : UInt = match frame.kind {
Information =>
(frame.send_sequence.reinterpret_as_uint() << 1) |
(frame.receive_sequence.reinterpret_as_uint() << 17)
Supervisory => 1U | (frame.receive_sequence.reinterpret_as_uint() << 17)
Unnumbered => frame.control.to_uint()
}
let payload = frame.payload.to_array()
let out : Array[Byte] = [b'\x68', (4 + payload.length()).to_byte()]
append_u32(out, control)
for byte in payload {
out.push(byte)
}
Bytes::from_array(out)
}
///|
/// Decode one complete APDU. Extra bytes after the declared APDU are rejected.
pub fn decode_frame(data : Bytes) -> Result[Frame, String] {
if data.length() < 6 {
return Err("APDU is shorter than six bytes")
}
if data[0] != b'\x68' {
return Err("invalid APDU start byte")
}
let length = data[1].to_int()
if length < 4 || data.length() != length + 2 {
return Err("APDU length mismatch")
}
let control = read_u32(data, 2)
if (control & 1U) == 0U {
Ok(
information_frame(
(control & 0xffffU).reinterpret_as_int() / 2,
(control >> 16).reinterpret_as_int() / 2,
data[6:].to_owned(),
),
)
} else if (control & 2U) == 0U {
Ok(supervisory_frame((control >> 16).reinterpret_as_int() / 2))
} else {
Ok(unnumbered_frame(control.to_uint16()))
}
}
///|
/// Encode an ASDU header followed by information objects.
pub fn encode_asdu(
header : AsduHeader,
objects : Array[InformationObject],
) -> Bytes {
let sequence_flag = if header.sequence { 0x80 } else { 0 }
let out : Array[Byte] = [
header.type_id.number().to_byte(),
(header.variable_count | sequence_flag).to_byte(),
]
append_u16(out, header.cause)
append_u16(out, header.common_address)
for object in objects {
match object {
Single(status, quality) => {
out.push(if status { 1 } else { 0 })
out.push(low_byte(quality))
}
Double(status, quality) => {
out.push(status.to_byte())
out.push(low_byte(quality))
}
Normalized(value, quality) => {
append_u16(out, value)
out.push(low_byte(quality))
}
ShortFloat(value, quality) => {
let bits = value.reinterpret_as_uint()
append_u32(out, bits)
out.push(low_byte(quality))
}
BitString(value, quality) => {
append_u32(out, value)
out.push(low_byte(quality))
}
}
}
Bytes::from_array(out)
}
///|
/// Decode an ASDU header and its information objects.
pub fn decode_asdu(
data : Bytes,
) -> Result[(AsduHeader, Array[InformationObject]), String] {
if data.length() < 6 {
return Err("ASDU header is incomplete")
}
let raw_count = data[1].to_int()
let header = {
type_id: type_id(data[0].to_int()),
variable_count: raw_count & 0x7f,
sequence: (raw_count & 0x80) != 0,
cause: read_u16(data, 2),
common_address: read_u16(data, 4),
}
let width = match header.type_id {
SinglePoint => 2
DoublePoint => 2
NormalizedValue => 3
ShortFloat => 5
BitString32 => 5
Unknown(_) => return Err("unsupported information type")
}
if data.length() != 6 + width * header.variable_count {
return Err("ASDU object length mismatch")
}
let objects : Array[InformationObject] = []
let mut offset = 6
for _ in 0.. {
objects.push(
Single(data[offset].to_int() != 0, data[offset + 1].to_int()),
)
offset += 2
}
DoublePoint => {
objects.push(Double(data[offset].to_int(), data[offset + 1].to_int()))
offset += 2
}
NormalizedValue => {
objects.push(
Normalized(read_u16(data, offset), data[offset + 2].to_int()),
)
offset += 3
}
ShortFloat => {
let raw = read_u32(data, offset)
objects.push(
ShortFloat(
Float::reinterpret_from_uint(raw),
data[offset + 4].to_int(),
),
)
offset += 5
}
BitString32 => {
objects.push(
BitString(read_u32(data, offset), data[offset + 4].to_int()),
)
offset += 5
}
Unknown(_) => return Err("unsupported information type")
}
}
Ok((header, objects))
}