use super::rta::{
build_frame, parse_frame, AlarmNotification, PduType, Priority, RtaBody, RtaData, RtaHeader,
UserData,
};
use crate::cm::{AbortReason, PnioStatus};
use crate::eth::{EthHeader, MacAddr};
use std::collections::VecDeque;
use std::time::{Duration, Instant};
use thiserror::Error;
const NOTIFICATION_FIXED_LEN: usize = 24;
const ALARM_ACK_TIMEOUT_FACTOR: u32 = 10;
#[derive(Debug, Clone)]
pub struct AlarmChannelConfig {
pub local_ref: u16,
pub remote_ref: u16,
pub rta_timeout: Duration,
pub rta_retries: u16,
pub max_alarm_data_length: u16,
pub peer_mac: MacAddr,
pub our_mac: MacAddr,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AlarmReq {
pub id: u32,
pub priority: Priority,
pub notification: AlarmNotification,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AlarmAction {
Send(Vec<u8>),
Acked {
id: u32,
status: PnioStatus,
},
Abort(AbortReason),
UnexpectedRx,
}
#[derive(Debug, Error, PartialEq)]
pub enum AlarmError {
#[error("alarm data {len} bytes exceeds the negotiated {max}")]
TooLong {
len: usize,
max: u16,
},
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct AlarmStats {
pub sent: u64,
pub acked: u64,
pub retries: u64,
pub unexpected_rx: u64,
pub send_failures: u64,
pub ack_timeouts: u64,
pub rx_err_rta: u64,
}
enum State {
Idle,
SentData {
req: AlarmReq,
seq: u16,
frame: Vec<u8>,
deadline: Instant,
},
AwaitAlarmAck {
req: AlarmReq,
deadline: Instant,
},
}
pub struct AlarmChannel {
cfg: AlarmChannelConfig,
state: State,
queue: VecDeque<AlarmReq>,
next_send_seq: u16,
last_sent_seq: u16,
last_rx_seq: u16,
attempt: u16,
stats: AlarmStats,
specifier_seq: u16,
}
impl AlarmChannel {
pub fn new(cfg: AlarmChannelConfig) -> Self {
AlarmChannel {
cfg,
state: State::Idle,
queue: VecDeque::new(),
next_send_seq: super::rta::SEQ_INIT,
last_sent_seq: super::rta::SEQ_NONE,
last_rx_seq: super::rta::SEQ_NONE,
attempt: 0,
stats: AlarmStats::default(),
specifier_seq: 0,
}
}
pub fn enqueue(&mut self, req: AlarmReq, now: Instant) -> Result<Vec<AlarmAction>, AlarmError> {
let len = NOTIFICATION_FIXED_LEN + data_len(&req.notification.data);
if len > self.cfg.max_alarm_data_length as usize {
return Err(AlarmError::TooLong {
len,
max: self.cfg.max_alarm_data_length,
});
}
self.queue.push_back(req);
Ok(self.send_next(now))
}
pub fn on_frame(&mut self, frame: &[u8], now: Instant) -> Vec<AlarmAction> {
let pdu = match parse_frame(frame) {
Ok(p) => p,
Err(_) => {
self.stats.unexpected_rx += 1;
return vec![AlarmAction::UnexpectedRx];
}
};
let src = match EthHeader::parse(frame) {
Ok((h, _)) => h.src,
Err(_) => {
self.stats.unexpected_rx += 1;
return vec![AlarmAction::UnexpectedRx];
}
};
if src != self.cfg.peer_mac {
self.stats.unexpected_rx += 1;
return vec![AlarmAction::UnexpectedRx];
}
match pdu.body {
RtaBody::Ack => self.on_transport_ack(pdu.header.ack_seq, now),
RtaBody::Data(data) => self.on_data(pdu.header, pdu.priority, data, now),
RtaBody::Err(status) => self.on_err_rta(status),
RtaBody::Nack => {
self.stats.unexpected_rx += 1;
vec![AlarmAction::UnexpectedRx]
}
}
}
pub fn on_tick(&mut self, now: Instant) -> Vec<AlarmAction> {
let awaiting_alarm_ack = match &self.state {
State::SentData { deadline, .. } => {
if now < *deadline {
return vec![];
}
false
}
State::AwaitAlarmAck { deadline, .. } => {
if now < *deadline {
return vec![];
}
true
}
State::Idle => return vec![],
};
if awaiting_alarm_ack {
self.drop_unacknowledged(now)
} else {
self.retry_or_abort(now)
}
}
pub fn next_deadline(&self) -> Option<Instant> {
match &self.state {
State::SentData { deadline, .. } | State::AwaitAlarmAck { deadline, .. } => {
Some(*deadline)
}
State::Idle => None,
}
}
pub fn err_rta(&mut self, status: PnioStatus) -> Vec<u8> {
let pdu = super::rta::RtaPdu {
priority: Priority::Low,
header: RtaHeader {
dst_ref: self.cfg.remote_ref,
src_ref: self.cfg.local_ref,
pdu_type: PduType::Err,
tack: false,
send_seq: self.last_sent_seq,
ack_seq: self.last_rx_seq,
},
body: RtaBody::Err(status),
};
build_frame(self.cfg.peer_mac, self.cfg.our_mac, &pdu)
}
pub fn in_flight(&self) -> Option<u32> {
match &self.state {
State::SentData { req, .. } | State::AwaitAlarmAck { req, .. } => Some(req.id),
State::Idle => None,
}
}
pub fn queued(&self) -> usize {
self.queue.len()
}
pub fn stats(&self) -> AlarmStats {
self.stats
}
pub fn next_specifier_sequence(&mut self) -> u16 {
let v = self.specifier_seq;
self.specifier_seq = (self.specifier_seq + 1) & 0x07FF;
v
}
fn send_next(&mut self, now: Instant) -> Vec<AlarmAction> {
if !matches!(self.state, State::Idle) {
return vec![];
}
let Some(req) = self.queue.pop_front() else {
return vec![];
};
let seq = self.next_send_seq;
let frame = build_data_frame(&self.cfg, &req, seq, self.last_rx_seq);
self.last_sent_seq = seq;
self.next_send_seq = if seq == super::rta::SEQ_INIT {
0
} else {
(seq + 1) & 0x7FFF
};
self.attempt = 0;
self.stats.sent += 1;
let deadline = now + self.cfg.rta_timeout;
let out = vec![AlarmAction::Send(frame.clone())];
self.state = State::SentData {
req,
seq,
frame,
deadline,
};
out
}
fn on_transport_ack(&mut self, ack_seq: u16, now: Instant) -> Vec<AlarmAction> {
if let State::SentData { seq, .. } = &self.state {
if ack_seq == *seq {
let old = std::mem::replace(&mut self.state, State::Idle);
if let State::SentData { req, .. } = old {
self.state = State::AwaitAlarmAck {
req,
deadline: now + self.cfg.rta_timeout * ALARM_ACK_TIMEOUT_FACTOR,
};
}
return vec![];
}
}
self.stats.unexpected_rx += 1;
vec![]
}
fn on_data(
&mut self,
header: RtaHeader,
priority: Priority,
data: RtaData,
now: Instant,
) -> Vec<AlarmAction> {
if header.send_seq == self.last_rx_seq {
return vec![AlarmAction::Send(
self.build_ack_rta(priority, header.send_seq),
)];
}
self.last_rx_seq = header.send_seq;
let mut actions = vec![AlarmAction::Send(
self.build_ack_rta(priority, header.send_seq),
)];
match data {
RtaData::Ack(a) => {
let matches_in_flight = matches!(
&self.state,
State::SentData { req, .. } | State::AwaitAlarmAck { req, .. }
if req.notification.alarm_type == a.alarm_type
&& req.notification.slot == a.slot
&& req.notification.subslot == a.subslot
);
if matches_in_flight {
let old = std::mem::replace(&mut self.state, State::Idle);
if let State::SentData { req, .. } | State::AwaitAlarmAck { req, .. } = old {
self.stats.acked += 1;
actions.push(AlarmAction::Acked {
id: req.id,
status: a.status,
});
}
actions.extend(self.send_next(now));
} else {
self.stats.unexpected_rx += 1;
actions.push(AlarmAction::UnexpectedRx);
}
}
RtaData::Notification(_) | RtaData::Unknown { .. } => {
self.stats.unexpected_rx += 1;
actions.push(AlarmAction::UnexpectedRx);
}
}
actions
}
fn on_err_rta(&mut self, status: PnioStatus) -> Vec<AlarmAction> {
self.stats.rx_err_rta += 1;
self.state = State::Idle;
self.queue.clear();
vec![AlarmAction::Abort(AbortReason::ControllerErrRta(status))]
}
fn retry_or_abort(&mut self, now: Instant) -> Vec<AlarmAction> {
if self.attempt < self.cfg.rta_retries {
self.attempt += 1;
self.stats.retries += 1;
let (req, seq, frame) = match std::mem::replace(&mut self.state, State::Idle) {
State::SentData {
req, seq, frame, ..
} => (req, seq, frame),
State::Idle | State::AwaitAlarmAck { .. } => {
unreachable!("on_tick only routes an expired SentData here")
}
};
let out = vec![AlarmAction::Send(frame.clone())];
self.state = State::SentData {
req,
seq,
frame,
deadline: now + self.cfg.rta_timeout,
};
out
} else {
self.stats.send_failures += 1;
self.state = State::Idle;
self.queue.clear();
vec![AlarmAction::Abort(AbortReason::AlarmSendFailed)]
}
}
fn drop_unacknowledged(&mut self, now: Instant) -> Vec<AlarmAction> {
let State::AwaitAlarmAck { req, .. } = std::mem::replace(&mut self.state, State::Idle)
else {
unreachable!("on_tick only routes an expired AwaitAlarmAck here")
};
self.stats.ack_timeouts += 1;
log::warn!(
"alarm id {} ({:?} on slot {} subslot {}) was delivered but the controller sent no Alarm-Ack within {:?}: dropping it, the AR stays up",
req.id,
req.notification.alarm_type,
req.notification.slot,
req.notification.subslot,
self.cfg.rta_timeout * ALARM_ACK_TIMEOUT_FACTOR,
);
self.send_next(now)
}
fn build_ack_rta(&self, priority: Priority, received_seq: u16) -> Vec<u8> {
let pdu = super::rta::RtaPdu {
priority,
header: RtaHeader {
dst_ref: self.cfg.remote_ref,
src_ref: self.cfg.local_ref,
pdu_type: PduType::Ack,
tack: false,
send_seq: self.last_sent_seq,
ack_seq: received_seq,
},
body: RtaBody::Ack,
};
build_frame(self.cfg.peer_mac, self.cfg.our_mac, &pdu)
}
}
fn build_data_frame(cfg: &AlarmChannelConfig, req: &AlarmReq, seq: u16, ack_seq: u16) -> Vec<u8> {
let pdu = super::rta::RtaPdu {
priority: req.priority,
header: RtaHeader {
dst_ref: cfg.remote_ref,
src_ref: cfg.local_ref,
pdu_type: PduType::Data,
tack: true,
send_seq: seq,
ack_seq,
},
body: RtaBody::Data(RtaData::Notification(req.notification.clone())),
};
build_frame(cfg.peer_mac, cfg.our_mac, &pdu)
}
fn data_len(data: &UserData) -> usize {
match data {
UserData::Raw(v) => v.len(),
UserData::Channel(_) => 6,
UserData::ExtChannel(_) => 12,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::alarm::rta::*;
use crate::cm::PnioStatus;
use crate::testutil::golden_alarm;
use std::time::{Duration, Instant};
const DEV: MacAddr = MacAddr([0x8c, 0xf3, 0x19, 0xcd, 0x19, 0xf8]);
const CPU: MacAddr = MacAddr([0xec, 0x1c, 0x5d, 0x61, 0xe7, 0x3c]);
fn cfg() -> AlarmChannelConfig {
AlarmChannelConfig {
local_ref: 0,
remote_ref: 0,
rta_timeout: Duration::from_millis(100),
rta_retries: 3,
max_alarm_data_length: 256,
peer_mac: CPU,
our_mac: DEV,
}
}
fn process_req(id: u32) -> AlarmReq {
AlarmReq {
id,
priority: Priority::High,
notification: AlarmNotification {
alarm_type: AlarmType::Process,
api: 0,
slot: 1,
subslot: 1,
module_ident: 0x30,
submodule_ident: 0x130,
specifier: AlarmSpecifier::default(),
usi: 0x0010,
data: UserData::Raw(vec![1]),
},
}
}
fn sends(actions: &[AlarmAction]) -> Vec<Vec<u8>> {
actions
.iter()
.filter_map(|a| {
if let AlarmAction::Send(f) = a {
Some(f.clone())
} else {
None
}
})
.collect()
}
#[test]
fn full_handshake_reproduces_the_process_alarm_goldens() {
let t0 = Instant::now();
let mut ch = AlarmChannel::new(cfg());
let out = ch.enqueue(process_req(7), t0).unwrap();
assert_eq!(sends(&out), vec![golden_alarm("alarm_process_notif")]);
assert_eq!(ch.in_flight(), Some(7));
let out = ch.on_frame(&golden_alarm("alarm_ack_rta_high_cpu"), t0);
assert!(out.is_empty(), "transport ack produces nothing to send");
let out = ch.on_frame(&golden_alarm("alarm_ack_high_cpu"), t0);
assert_eq!(sends(&out), vec![golden_alarm("alarm_ack_rta_high_dev")]);
assert!(out.contains(&AlarmAction::Acked {
id: 7,
status: PnioStatus::OK
}));
assert_eq!(ch.in_flight(), None);
assert_eq!(ch.stats().sent, 1);
assert_eq!(ch.stats().acked, 1);
}
#[test]
fn queue_is_fifo_and_one_in_flight() {
let t0 = Instant::now();
let mut ch = AlarmChannel::new(cfg());
let a = ch.enqueue(process_req(1), t0).unwrap();
let b = ch.enqueue(process_req(2), t0).unwrap();
assert_eq!(sends(&a).len(), 1);
assert!(sends(&b).is_empty());
assert_eq!(ch.queued(), 1);
ch.on_frame(&golden_alarm("alarm_ack_rta_high_cpu"), t0);
let out = ch.on_frame(&golden_alarm("alarm_ack_high_cpu"), t0);
let s = sends(&out);
assert_eq!(s.len(), 2);
let second = parse_frame(&s[1]).unwrap();
assert_eq!(
(second.header.send_seq, second.header.ack_seq),
(0x0000, 0xFFFF)
);
assert_eq!(ch.in_flight(), Some(2));
}
#[test]
fn retries_then_aborts_when_never_acked() {
let t0 = Instant::now();
let mut ch = AlarmChannel::new(cfg());
ch.enqueue(process_req(1), t0).unwrap();
let mut resends = 0;
let mut t = t0;
let mut aborted = None;
for _ in 0..5 {
t += Duration::from_millis(101);
let out = ch.on_tick(t);
resends += sends(&out).len();
if let Some(AlarmAction::Abort(r)) =
out.iter().find(|a| matches!(a, AlarmAction::Abort(_)))
{
aborted = Some(*r);
break;
}
}
assert_eq!(resends, 3);
assert_eq!(aborted, Some(crate::cm::AbortReason::AlarmSendFailed));
assert_eq!(ch.stats().retries, 3);
assert_eq!(ch.stats().send_failures, 1);
assert_eq!(ch.in_flight(), None);
}
#[test]
fn alarm_ack_overtaking_the_transport_ack_is_accepted() {
let t0 = Instant::now();
let mut ch = AlarmChannel::new(cfg());
ch.enqueue(process_req(7), t0).unwrap();
assert_eq!(ch.in_flight(), Some(7));
let out = ch.on_frame(&golden_alarm("alarm_ack_high_cpu"), t0);
assert_eq!(sends(&out), vec![golden_alarm("alarm_ack_rta_high_dev")]);
assert!(out.contains(&AlarmAction::Acked {
id: 7,
status: PnioStatus::OK
}));
assert_eq!(ch.in_flight(), None);
assert_eq!(ch.queued(), 0);
assert_eq!(ch.stats().acked, 1);
assert_eq!(ch.stats().unexpected_rx, 0);
assert_eq!(ch.next_deadline(), None);
}
#[test]
fn duplicate_data_is_re_acked_but_not_reprocessed() {
let t0 = Instant::now();
let mut ch = AlarmChannel::new(cfg());
ch.enqueue(process_req(1), t0).unwrap();
ch.on_frame(&golden_alarm("alarm_ack_rta_high_cpu"), t0);
let first = ch.on_frame(&golden_alarm("alarm_ack_high_cpu"), t0);
let again = ch.on_frame(&golden_alarm("alarm_ack_high_cpu"), t0);
assert_eq!(sends(&again), vec![golden_alarm("alarm_ack_rta_high_dev")]);
assert!(!again.iter().any(|a| matches!(a, AlarmAction::Acked { .. })));
assert!(first.iter().any(|a| matches!(a, AlarmAction::Acked { .. })));
}
#[test]
fn controller_err_rta_aborts() {
let t0 = Instant::now();
let mut ch = AlarmChannel::new(cfg());
ch.enqueue(process_req(1), t0).unwrap();
let out = ch.on_frame(&golden_alarm("alarm_err_rta_cpu_removed"), t0);
assert_eq!(
out,
vec![AlarmAction::Abort(
crate::cm::AbortReason::ControllerErrRta(PnioStatus::new(0xCF, 0x81, 0xFD, 0x11))
)]
);
assert_eq!(ch.in_flight(), None);
assert_eq!(ch.queued(), 0);
}
#[test]
fn err_rta_out_uses_current_counters_and_low_priority() {
let mut ch = AlarmChannel::new(cfg());
let f = ch.err_rta(PnioStatus::rta_abort(PnioStatus::RTA_ABORT_AR_REMOVED));
let pdu = parse_frame(&f).unwrap();
assert_eq!(pdu.priority, Priority::Low);
assert_eq!(
(pdu.header.send_seq, pdu.header.ack_seq),
(SEQ_NONE, SEQ_NONE)
);
assert_eq!(
pdu.body,
RtaBody::Err(PnioStatus::new(0xCF, 0x81, 0xFD, 17))
);
}
#[test]
fn too_long_is_refused_before_the_wire() {
let mut c = cfg();
c.max_alarm_data_length = 30;
let mut ch = AlarmChannel::new(c);
let mut r = process_req(1);
r.notification.data = UserData::Raw(vec![0; 40]);
assert!(matches!(
ch.enqueue(r, Instant::now()),
Err(AlarmError::TooLong { .. })
));
}
#[test]
fn frames_from_another_mac_and_garbage_are_unexpected() {
let t0 = Instant::now();
let mut ch = AlarmChannel::new(cfg());
let mut f = golden_alarm("alarm_ack_high_cpu");
f[6] ^= 0xFF;
assert_eq!(ch.on_frame(&f, t0), vec![AlarmAction::UnexpectedRx]);
assert_eq!(ch.on_frame(&[0u8; 10], t0), vec![AlarmAction::UnexpectedRx]);
assert_eq!(ch.stats().unexpected_rx, 2);
}
#[test]
fn await_alarm_ack_timeout_drops_the_alarm_and_keeps_the_ar() {
let t0 = Instant::now();
let mut ch = AlarmChannel::new(cfg());
let out = ch.enqueue(process_req(1), t0).unwrap();
assert_eq!(sends(&out), vec![golden_alarm("alarm_process_notif")]);
assert!(sends(&ch.enqueue(process_req(2), t0).unwrap()).is_empty());
let out = ch.on_frame(&golden_alarm("alarm_ack_rta_high_cpu"), t0);
assert!(out.is_empty(), "transport ack produces nothing to send");
assert_eq!(ch.in_flight(), Some(1));
let t = t0 + Duration::from_millis(101);
assert!(
ch.on_tick(t).is_empty(),
"one rta_timeout is not the deadline"
);
assert_eq!(ch.in_flight(), Some(1));
assert_eq!(ch.stats().retries, 0);
let t = t0 + Duration::from_millis(1001);
let out = ch.on_tick(t);
assert!(
!out.iter().any(|a| matches!(a, AlarmAction::Abort(_))),
"a slow controller application must not take the AR down: {out:?}"
);
let s = sends(&out);
assert_eq!(s.len(), 1, "exactly the next queued alarm: {out:?}");
let next = parse_frame(&s[0]).unwrap();
assert_eq!(next.header.send_seq, 0x0000, "a fresh DATA, not a resend");
assert_eq!(ch.in_flight(), Some(2));
assert_eq!(ch.queued(), 0);
assert_eq!(ch.stats().ack_timeouts, 1);
assert_eq!(ch.stats().retries, 0);
assert_eq!(ch.stats().send_failures, 0);
}
#[test]
fn transport_ack_while_idle_is_counted_and_produces_nothing() {
let t0 = Instant::now();
let mut ch = AlarmChannel::new(cfg());
assert_eq!(ch.in_flight(), None);
let out = ch.on_frame(&golden_alarm("alarm_ack_rta_high_cpu"), t0);
assert!(out.is_empty());
assert_eq!(ch.stats().unexpected_rx, 1);
}
#[test]
fn mismatched_alarm_ack_while_awaiting_is_unexpected_but_still_transport_acked() {
let t0 = Instant::now();
let mut ch = AlarmChannel::new(cfg());
ch.enqueue(process_req(1), t0).unwrap();
ch.on_frame(&golden_alarm("alarm_ack_rta_high_cpu"), t0);
assert_eq!(ch.in_flight(), Some(1));
let out = ch.on_frame(&golden_alarm("alarm_diag_ack_cpu"), t0);
assert!(
out.iter().any(|a| matches!(a, AlarmAction::Send(_))),
"the ACK-RTA is still sent: {out:?}"
);
assert!(out.contains(&AlarmAction::UnexpectedRx));
assert!(!out.iter().any(|a| matches!(a, AlarmAction::Acked { .. })));
assert_eq!(
ch.in_flight(),
Some(1),
"AwaitAlarmAck must not be disturbed"
);
}
#[test]
fn notification_from_the_controller_is_transport_acked_but_unexpected() {
let t0 = Instant::now();
let mut ch = AlarmChannel::new(cfg());
ch.enqueue(process_req(1), t0).unwrap();
ch.on_frame(&golden_alarm("alarm_ack_rta_high_cpu"), t0);
assert_eq!(ch.in_flight(), Some(1));
let mut f = golden_alarm("alarm_process_notif");
let (dst, src) = (f[0..6].to_vec(), f[6..12].to_vec());
f[0..6].copy_from_slice(&src);
f[6..12].copy_from_slice(&dst);
assert_eq!(&f[0..6], &DEV.0[..], "dst is now our own MAC");
assert_eq!(&f[6..12], &CPU.0[..], "src is now the controller's MAC");
let out = ch.on_frame(&f, t0);
assert!(
out.iter().any(|a| matches!(a, AlarmAction::Send(_))),
"the ACK-RTA is still sent: {out:?}"
);
assert!(out.contains(&AlarmAction::UnexpectedRx));
assert_eq!(ch.in_flight(), Some(1));
}
}