///|
fn push_u16_be(output : Array[Byte], value : Int) -> Unit {
output.push(((value >> 8) & 0xFF).to_byte())
output.push((value & 0xFF).to_byte())
}
///|
fn push_u32_be(output : Array[Byte], value : Int) -> Unit {
output.push(((value >> 24) & 0xFF).to_byte())
output.push(((value >> 16) & 0xFF).to_byte())
output.push(((value >> 8) & 0xFF).to_byte())
output.push((value & 0xFF).to_byte())
}
///|
fn read_u16_be(data : Bytes, offset : Int) -> Int {
(data[offset].to_int() << 8) + data[offset + 1].to_int()
}
///|
fn read_u32_be(data : Bytes, offset : Int) -> Int {
(data[offset].to_int() << 24) +
(data[offset + 1].to_int() << 16) +
(data[offset + 2].to_int() << 8) +
data[offset + 3].to_int()
}
///|
/// AlarmCR block request — used during AR connect to negotiate alarm channels.
pub(all) struct AlarmCRBlockReq {
alarm_cr_type : Int
lt : Int
alarm_cr_properties : Int
rta_timeout_factor : Int
rta_retries : Int
local_alarm_reference : Int
max_alarm_data_length : Int
alarm_cr_tag_header_high : Int
alarm_cr_tag_header_low : Int
} derive(Eq, Debug)
///|
pub fn AlarmCRBlockReq::default() -> AlarmCRBlockReq {
AlarmCRBlockReq::{
alarm_cr_type: 1,
lt: alarm_lt,
alarm_cr_properties: 0,
rta_timeout_factor: default_rta_timeout_factor,
rta_retries: default_rta_retries,
local_alarm_reference: 0,
max_alarm_data_length: default_max_alarm_data_length,
alarm_cr_tag_header_high: default_alarm_cr_tag_header_high,
alarm_cr_tag_header_low: default_alarm_cr_tag_header_low,
}
}
///|
/// Encode an AlarmCRBlockReq into bytes (block body, without block header).
pub fn encode_alarm_cr_block_req(req : AlarmCRBlockReq) -> Bytes {
let output : Array[Byte] = []
push_u16_be(output, req.alarm_cr_type)
push_u16_be(output, req.lt)
push_u32_be(output, req.alarm_cr_properties)
push_u16_be(output, req.rta_timeout_factor)
push_u16_be(output, req.rta_retries)
push_u16_be(output, req.local_alarm_reference)
push_u16_be(output, req.max_alarm_data_length)
push_u16_be(output, req.alarm_cr_tag_header_high)
push_u16_be(output, req.alarm_cr_tag_header_low)
Bytes::from_array(output)
}
///|
/// AlarmCR block response — returned by device after connect request.
pub(all) struct AlarmCRBlockRes {
alarm_cr_type : Int
local_alarm_reference : Int
max_alarm_data_length : Int
} derive(Eq, Debug)
///|
/// Parse an AlarmCRBlockRes from bytes (block body).
pub fn parse_alarm_cr_block_res(
data : Bytes,
offset : Int,
) -> AlarmCRBlockRes raise @frame.FrameError {
guard data.length() >= offset + 6 else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
AlarmCRBlockRes::{
alarm_cr_type: read_u16_be(data, offset),
local_alarm_reference: read_u16_be(data, offset + 2),
max_alarm_data_length: read_u16_be(data, offset + 4),
}
}
///|
/// Alarm notification PDU — parsed from an alarm Ethernet frame payload.
pub(all) struct AlarmNotification {
dst_endpoint : Int
src_endpoint : Int
pdu_version : Int
pdu_type : Byte
add_flags : Byte
send_seq_num : Int
ack_seq_num : Int
alarm_data_length : Int
alarm_type : Int
api : Int
slot_number : Int
subslot_number : Int
module_ident_number : Int
submodule_ident_number : Int
} derive(Eq, Debug)
///|
pub(all) struct RtaPduHeader {
dst_endpoint : Int
src_endpoint : Int
pdu_version : Int
pdu_type : Int
pdu_type_raw : Byte
add_flags : Byte
send_seq_num : Int
ack_seq_num : Int
var_part_len : Int
} derive(Eq, Debug)
///|
pub(all) struct RtaError {
header : RtaPduHeader
pnio_status : Int
} derive(Eq, Debug)
///|
pub(all) struct AlarmAck {
dst_endpoint : Int
src_endpoint : Int
add_flags : Byte
send_seq_num : Int
ack_seq_num : Int
status : Int
} derive(Eq, Debug)
///|
pub(all) struct AlarmResponderState {
phase : String
expected_send_seq : Int
last_send_seq : Int
pending_ack_seq : Int
notification_count : Int
duplicate_count : Int
out_of_sequence_count : Int
ack_sent_count : Int
ack_fail_count : Int
} derive(Eq, Debug)
///|
pub(all) struct AlarmResponderDecision {
phase_before : String
phase_after : String
notification_send_seq : Int
notification_ack_seq : Int
expected_send_seq : Int
next_expected_send_seq : Int
duplicate : Bool
in_sequence : Bool
ack_required : Bool
} derive(Eq, Debug)
///|
pub fn AlarmResponderState::default() -> AlarmResponderState {
AlarmResponderState::{
phase: "W-Notify",
expected_send_seq: -1,
last_send_seq: -1,
pending_ack_seq: -1,
notification_count: 0,
duplicate_count: 0,
out_of_sequence_count: 0,
ack_sent_count: 0,
ack_fail_count: 0,
}
}
///|
pub fn alarm_sequence_next(seq : Int) -> Int {
(seq + 1) & 0xFFFF
}
///|
pub fn alarm_responder_on_notification(
state : AlarmResponderState,
notif : AlarmNotification,
) -> (AlarmResponderState, AlarmResponderDecision, AlarmAck) {
let expected = if state.expected_send_seq < 0 {
notif.send_seq_num
} else {
state.expected_send_seq
}
let duplicate = state.last_send_seq >= 0 &&
notif.send_seq_num == state.last_send_seq
let in_sequence = duplicate ||
state.expected_send_seq < 0 ||
notif.send_seq_num == state.expected_send_seq
let next_expected = if duplicate {
state.expected_send_seq
} else {
alarm_sequence_next(notif.send_seq_num)
}
let duplicate_delta = if duplicate { 1 } else { 0 }
let out_of_sequence_delta = if in_sequence { 0 } else { 1 }
let next_state = AlarmResponderState::{
phase: "W-User-Ack",
expected_send_seq: next_expected,
last_send_seq: notif.send_seq_num,
pending_ack_seq: notif.send_seq_num,
notification_count: state.notification_count + 1,
duplicate_count: state.duplicate_count + duplicate_delta,
out_of_sequence_count: state.out_of_sequence_count + out_of_sequence_delta,
ack_sent_count: state.ack_sent_count,
ack_fail_count: state.ack_fail_count,
}
let decision = AlarmResponderDecision::{
phase_before: state.phase,
phase_after: next_state.phase,
notification_send_seq: notif.send_seq_num,
notification_ack_seq: notif.ack_seq_num,
expected_send_seq: expected,
next_expected_send_seq: next_expected,
duplicate,
in_sequence,
ack_required: true,
}
(next_state, decision, AlarmAck::from_notification(notif))
}
///|
pub fn alarm_responder_on_ack_sent(
state : AlarmResponderState,
sent : Int,
) -> AlarmResponderState {
if sent > 0 {
AlarmResponderState::{
phase: "W-Notify",
expected_send_seq: state.expected_send_seq,
last_send_seq: state.last_send_seq,
pending_ack_seq: -1,
notification_count: state.notification_count,
duplicate_count: state.duplicate_count,
out_of_sequence_count: state.out_of_sequence_count,
ack_sent_count: state.ack_sent_count + 1,
ack_fail_count: state.ack_fail_count,
}
} else {
AlarmResponderState::{
phase: "W-User-Ack",
expected_send_seq: state.expected_send_seq,
last_send_seq: state.last_send_seq,
pending_ack_seq: state.pending_ack_seq,
notification_count: state.notification_count,
duplicate_count: state.duplicate_count,
out_of_sequence_count: state.out_of_sequence_count,
ack_sent_count: state.ack_sent_count,
ack_fail_count: state.ack_fail_count + 1,
}
}
}
///|
pub fn format_alarm_responder_decision(
prefix : String,
decision : AlarmResponderDecision,
) -> String {
let lines : Array[String] = []
lines.push(prefix + "_phase_before=" + decision.phase_before)
lines.push(prefix + "_phase_after=" + decision.phase_after)
lines.push(
prefix +
"_notification_send_seq=" +
decision.notification_send_seq.to_string(),
)
lines.push(
prefix +
"_notification_ack_seq=" +
decision.notification_ack_seq.to_string(),
)
lines.push(
prefix + "_expected_send_seq=" + decision.expected_send_seq.to_string(),
)
lines.push(
prefix +
"_next_expected_send_seq=" +
decision.next_expected_send_seq.to_string(),
)
lines.push(prefix + "_duplicate=" + decision.duplicate.to_string())
lines.push(prefix + "_in_sequence=" + decision.in_sequence.to_string())
lines.push(prefix + "_ack_required=" + decision.ack_required.to_string())
lines.join("\n")
}
///|
pub fn format_alarm_responder_state(
prefix : String,
state : AlarmResponderState,
) -> String {
let lines : Array[String] = []
lines.push(prefix + "_phase=" + state.phase)
lines.push(
prefix + "_expected_send_seq=" + state.expected_send_seq.to_string(),
)
lines.push(prefix + "_last_send_seq=" + state.last_send_seq.to_string())
lines.push(prefix + "_pending_ack_seq=" + state.pending_ack_seq.to_string())
lines.push(
prefix + "_notification_count=" + state.notification_count.to_string(),
)
lines.push(prefix + "_duplicate_count=" + state.duplicate_count.to_string())
lines.push(
prefix + "_out_of_sequence_count=" + state.out_of_sequence_count.to_string(),
)
lines.push(prefix + "_ack_sent_count=" + state.ack_sent_count.to_string())
lines.push(prefix + "_ack_fail_count=" + state.ack_fail_count.to_string())
lines.join("\n")
}
///|
pub fn parse_rta_pdu_header(
data : Bytes,
offset : Int,
) -> RtaPduHeader raise @frame.FrameError {
guard data.length() >= offset + 12 else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let pdu_type_raw = data[offset + 4]
RtaPduHeader::{
dst_endpoint: read_u16_be(data, offset),
src_endpoint: read_u16_be(data, offset + 2),
pdu_version: (pdu_type_raw.to_int() >> 4) & 0x0F,
pdu_type: pdu_type_raw.to_int() & 0x0F,
pdu_type_raw,
add_flags: data[offset + 5],
send_seq_num: read_u16_be(data, offset + 6),
ack_seq_num: read_u16_be(data, offset + 8),
var_part_len: read_u16_be(data, offset + 10),
}
}
///|
pub fn parse_rta_error(
data : Bytes,
offset : Int,
) -> RtaError raise @frame.FrameError {
let header = parse_rta_pdu_header(data, offset)
guard header.pdu_type == rta_pdu_type_err && header.var_part_len >= 4 else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
guard data.length() >= offset + 16 else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
RtaError::{ header, pnio_status: read_u32_be(data, offset + 12) }
}
///|
pub fn AlarmAck::from_notification(notif : AlarmNotification) -> AlarmAck {
AlarmAck::{
dst_endpoint: notif.src_endpoint,
src_endpoint: notif.dst_endpoint,
add_flags: b'\x00',
send_seq_num: notif.ack_seq_num,
ack_seq_num: notif.send_seq_num,
status: 0,
}
}
///|
/// Encode the RTA transport ACK body that follows the 2-byte FrameID.
pub fn encode_alarm_ack(ack : AlarmAck) -> Bytes {
let output : Array[Byte] = []
push_u16_be(output, ack.dst_endpoint)
push_u16_be(output, ack.src_endpoint)
output.push(alarm_pdu_type_ack)
output.push(ack.add_flags)
push_u16_be(output, ack.send_seq_num)
push_u16_be(output, ack.ack_seq_num)
push_u16_be(output, 0)
Bytes::from_array(output)
}
///|
fn push_eth_header(
output : Array[Byte],
destination : @frame.MacAddress,
source : @frame.MacAddress,
vlan_tci : Int,
) -> Unit {
for byte in destination.octets() {
output.push(byte)
}
for byte in source.octets() {
output.push(byte)
}
if vlan_tci >= 0 {
push_u16_be(output, 0x8100)
push_u16_be(output, vlan_tci)
}
push_u16_be(output, alarm_lt)
}
///|
fn pad_ethernet_min_frame(output : Array[Byte]) -> Unit {
while output.length() < 60 {
output.push(b'\x00')
}
}
///|
/// Encode a full Ethernet RTA transport ACK frame.
pub fn encode_alarm_ack_frame(
destination : @frame.MacAddress,
source : @frame.MacAddress,
frame_id : Int,
ack : AlarmAck,
vlan_tci? : Int = -1,
) -> Bytes {
let output : Array[Byte] = []
push_eth_header(output, destination, source, vlan_tci)
push_u16_be(output, frame_id)
for byte in encode_alarm_ack(ack) {
output.push(byte)
}
pad_ethernet_min_frame(output)
Bytes::from_array(output)
}
///|
/// Parse an AlarmNotification from Ethernet frame payload.
/// The payload starts after Ethernet header (14 bytes) + FrameID (2 bytes).
pub fn parse_alarm_notification(
data : Bytes,
offset : Int,
) -> AlarmNotification raise @frame.FrameError {
guard data.length() >= offset + 30 else {
raise @frame.FrameError::InvalidMacLength(data.length())
}
let header = parse_rta_pdu_header(data, offset)
AlarmNotification::{
dst_endpoint: header.dst_endpoint,
src_endpoint: header.src_endpoint,
pdu_version: header.pdu_version,
pdu_type: (header.pdu_type & 0xFF).to_byte(),
add_flags: header.add_flags,
send_seq_num: header.send_seq_num,
ack_seq_num: header.ack_seq_num,
alarm_data_length: header.var_part_len,
alarm_type: read_u16_be(data, offset + 12),
api: read_u32_be(data, offset + 14),
slot_number: read_u16_be(data, offset + 18),
subslot_number: read_u16_be(data, offset + 20),
module_ident_number: read_u32_be(data, offset + 22),
submodule_ident_number: read_u32_be(data, offset + 26),
}
}
///|
/// Format alarm notification as human-readable summary.
pub fn format_alarm_notification(notif : AlarmNotification) -> String {
let lines : Array[String] = []
lines.push("pdu_version=" + notif.pdu_version.to_string())
lines.push("pdu_type=" + rta_pdu_type_label(notif.pdu_type.to_int()))
lines.push("add_flags=0x" + @frame.uint16_to_hex(notif.add_flags.to_int()))
lines.push("alarm_type=" + alarm_type_label(notif.alarm_type))
lines.push("api=" + notif.api.to_string())
lines.push("slot=" + notif.slot_number.to_string())
lines.push("subslot=" + notif.subslot_number.to_string())
lines.push(
"module_ident=0x" + @frame.uint32_to_hex(notif.module_ident_number),
)
lines.push(
"submodule_ident=0x" + @frame.uint32_to_hex(notif.submodule_ident_number),
)
lines.push("send_seq=" + notif.send_seq_num.to_string())
lines.push("ack_seq=" + notif.ack_seq_num.to_string())
lines.push("dst_endpoint=" + notif.dst_endpoint.to_string())
lines.push("src_endpoint=" + notif.src_endpoint.to_string())
lines.join("\n")
}
///|
pub fn rta_pdu_type_label(pdu_type : Int) -> String {
match pdu_type {
1 => "DATA"
2 => "NACK"
3 => "ACK"
4 => "ERR"
_ => "Unknown(" + pdu_type.to_string() + ")"
}
}
///|
pub fn format_rta_error(err : RtaError) -> String {
let lines : Array[String] = []
lines.push("rta_pdu_version=" + err.header.pdu_version.to_string())
lines.push("rta_pdu_type=" + rta_pdu_type_label(err.header.pdu_type))
lines.push(
"rta_pdu_type_raw=0x" +
@frame.uint16_to_hex(err.header.pdu_type_raw.to_int()),
)
lines.push(
"rta_add_flags=0x" + @frame.uint16_to_hex(err.header.add_flags.to_int()),
)
lines.push("rta_send_seq=" + err.header.send_seq_num.to_string())
lines.push("rta_ack_seq=" + err.header.ack_seq_num.to_string())
lines.push("rta_var_part_len=" + err.header.var_part_len.to_string())
lines.push("rta_pnio_status=0x" + @frame.uint32_to_hex(err.pnio_status))
lines.push("rta_pnio_status_hint=" + rta_pnio_status_hint(err.pnio_status))
lines.join("\n")
}
///|
pub fn rta_pnio_status_hint(status : Int) -> String {
match status {
0xCF81FD02 => "rta_abort_instance_closed"
0xCF81FD05 => "rta_abort_ar_consumer_dht_expired"
_ => "unknown"
}
}
///|
pub fn format_alarm_ack(ack : AlarmAck) -> String {
let lines : Array[String] = []
lines.push("ack_dst_endpoint=" + ack.dst_endpoint.to_string())
lines.push("ack_src_endpoint=" + ack.src_endpoint.to_string())
lines.push("ack_pdu_type=ACK")
lines.push("ack_send_seq=" + ack.send_seq_num.to_string())
lines.push("ack_ack_seq=" + ack.ack_seq_num.to_string())
lines.join("\n")
}