use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use thiserror::Error;
use crate::alarm::{
AlarmAction, AlarmChannel, AlarmChannelConfig, AlarmNotification, AlarmReq, Priority,
};
use crate::cm::model::DeviceModel;
use crate::cm::{AbortReason, ArParams, ArState, Cm, CmOutput, PnioStatus};
use crate::config::Slot;
use crate::dcp::{handle_dcp_frame, DcpConfig};
use crate::diag::{ChannelError, DiagStore, Diagnosis};
use crate::eth::poll::wait_any_readable;
use crate::eth::{EthTransport, TransportError};
use crate::im::{Im0, ImStore};
use crate::rpc::{RpcError, RpcTransport, Uuid};
use crate::rt::{IoImage, RtStats};
#[cfg(target_os = "linux")]
use crate::rt::{Layout, RtConfig, RtError, RtEvent, RtHandle, RtRunner, WatchdogState};
type StateChangeCallback = Box<dyn FnMut(ArState, Option<AbortReason>) + Send>;
#[derive(Debug, Clone)]
pub struct RtOptions {
pub iface: String,
pub cpu_pin: Option<usize>,
pub rt_priority: Option<u8>,
pub lock_memory: bool,
}
#[derive(Debug, Clone)]
pub struct DeviceSetup {
pub dcp: DcpConfig,
pub model: DeviceModel,
pub activity_seed: Uuid,
pub rt: Option<RtOptions>,
pub im0: Im0,
pub im_store: Option<PathBuf>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiagCommand {
Raise(Diagnosis),
Clear {
slot: Slot,
channel: u16,
error: ChannelError,
},
}
#[derive(Debug, Default)]
pub struct DiagShared {
pub queue: Mutex<VecDeque<DiagCommand>>,
pub active: Mutex<Vec<Diagnosis>>,
pub sent: AtomicU64,
pub acked: AtomicU64,
pub retries: AtomicU64,
pub unexpected_rx: AtomicU64,
pub send_failures: AtomicU64,
pub ack_timeouts: AtomicU64,
pub rx_err_rta: AtomicU64,
pub rx_no_channel: AtomicU64,
}
fn err_rta_code2(reason: &AbortReason) -> Option<u8> {
match reason {
AbortReason::ControllerErrRta(_) => None,
AbortReason::RtWatchdog => Some(PnioStatus::RTA_ABORT_DHT_WDT_EXPIRED),
AbortReason::AlarmSendFailed => Some(PnioStatus::RTA_ABORT_ALARM_SEND_FAILED),
AbortReason::RtSocket
| AbortReason::Shutdown
| AbortReason::ControllerRelease
| AbortReason::ControllerReconnect
| AbortReason::AppReadyFailed
| AbortReason::AppReadyRejected(_)
| AbortReason::ActivityTimeout
| AbortReason::External(_) => Some(PnioStatus::RTA_ABORT_AR_REMOVED),
}
}
const ALARM_POLL_INTERVAL: Duration = Duration::from_millis(20);
const RTA_TIMEOUT_UNIT: Duration = Duration::from_millis(100);
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct StepReport {
pub eth_frames: usize,
pub rpc_datagrams: usize,
pub sent: usize,
}
#[derive(Debug, Error)]
pub enum DeviceError {
#[error("Ethernet transport error: {0}")]
Eth(#[from] TransportError),
#[error("RPC transport error: {0}")]
Rpc(#[from] RpcError),
}
pub struct Device<E: EthTransport, R: RpcTransport> {
setup: DeviceSetup,
eth: E,
rpc: R,
cm: Cm,
on_state_change: Option<StateChangeCallback>,
image: Arc<IoImage>,
stats: Arc<RtStats>,
problem: Arc<AtomicBool>,
diag: DiagStore,
diag_shared: Arc<DiagShared>,
alarm: Option<AlarmChannel>,
next_alarm_id: u32,
alarm_epoch: u64,
#[cfg(target_os = "linux")]
runner: Option<RtHandle>,
#[cfg(target_os = "linux")]
runner_factory: Box<dyn Fn(RtConfig) -> Result<RtHandle, RtError> + Send>,
}
impl<E: EthTransport, R: RpcTransport> Device<E, R> {
pub fn new(setup: DeviceSetup, eth: E, rpc: R) -> Device<E, R> {
let mut cm = Cm::new(setup.model.clone(), setup.activity_seed);
cm.set_im(setup.im0.clone(), ImStore::load(setup.im_store.clone()));
let diag = DiagStore::from_model(&setup.model);
Device {
setup,
eth,
rpc,
cm,
on_state_change: None,
image: Arc::new(IoImage::empty()),
stats: Arc::new(RtStats::default()),
problem: Arc::new(AtomicBool::new(false)),
diag,
diag_shared: Arc::new(DiagShared::default()),
alarm: None,
next_alarm_id: 0,
alarm_epoch: 0,
#[cfg(target_os = "linux")]
runner: None,
#[cfg(target_os = "linux")]
runner_factory: Box::new(RtRunner::spawn),
}
}
pub fn state(&self) -> ArState {
self.cm.state()
}
pub fn eth(&self) -> &E {
&self.eth
}
pub fn rpc(&self) -> &R {
&self.rpc
}
pub fn image(&self) -> Arc<IoImage> {
self.image.clone()
}
pub fn rt_stats(&self) -> Arc<RtStats> {
self.stats.clone()
}
pub fn problem_indicator(&self) -> bool {
self.problem.load(Ordering::Relaxed)
}
pub fn diag_shared(&self) -> Arc<DiagShared> {
self.diag_shared.clone()
}
pub fn alarm_in_flight(&self) -> bool {
self.alarm
.as_ref()
.is_some_and(|ch| ch.in_flight().is_some())
}
pub fn poll_interval(&self, default: Duration) -> Duration {
let queued = !self
.diag_shared
.queue
.lock()
.unwrap_or_else(|e| e.into_inner())
.is_empty();
if self.alarm_in_flight() || queued {
ALARM_POLL_INTERVAL.min(default)
} else {
default
}
}
pub fn ar_params(&self) -> Option<ArParams> {
self.cm.context().map(|c| c.params.clone())
}
pub fn on_state_change(
&mut self,
f: impl FnMut(ArState, Option<AbortReason>) + Send + 'static,
) {
self.on_state_change = Some(Box::new(f));
}
pub fn run(&mut self, stop: &AtomicBool) -> Result<(), DeviceError> {
while !stop.load(Ordering::Relaxed) {
let wait = self.poll_interval(Duration::from_millis(200));
self.step(Instant::now(), Some(wait))?;
}
self.shutdown(Instant::now())
}
pub fn abort_with_err_rta(
&mut self,
reason: AbortReason,
now: Instant,
) -> Result<(), DeviceError> {
let mut report = StepReport::default();
self.abort_with_err_rta_in(reason, now, &mut report)
}
pub fn shutdown(&mut self, now: Instant) -> Result<(), DeviceError> {
if self.cm.state() == ArState::Idle {
return Ok(());
}
self.abort_with_err_rta(AbortReason::Shutdown, now)
}
pub fn step(
&mut self,
now: Instant,
wait: Option<Duration>,
) -> Result<StepReport, DeviceError> {
let alarm_deadline = self.alarm.as_ref().and_then(|ch| ch.next_deadline());
let deadline = match (self.cm.next_deadline(), alarm_deadline) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
};
let deadline_wait = deadline.map(|d| d.saturating_duration_since(now));
let effective_wait = match (wait, deadline_wait) {
(Some(w), Some(d)) => Some(w.min(d)),
(Some(w), None) => Some(w),
(None, Some(d)) => Some(d),
(None, None) => None,
};
let now = if let (Some(eth_fd), Some(rpc_fd)) = (self.eth.raw_fd(), self.rpc.raw_fd()) {
let mut fds = vec![eth_fd, rpc_fd];
#[cfg(target_os = "linux")]
if let Some(runner) = &self.runner {
fds.push(runner.event_fd());
}
wait_any_readable(&fds, effective_wait)
.map_err(|e| DeviceError::Eth(TransportError::Io(e)))?;
Instant::now()
} else {
now
};
let mut report = StepReport::default();
let mut pending: Vec<(u64, Vec<AlarmAction>)> = Vec::new();
while let Some(frame) = self.eth.recv(Some(Duration::ZERO))? {
report.eth_frames += 1;
if crate::alarm::is_alarm_frame(&frame) {
if frame.get(..6) != Some(&self.setup.dcp.mac.0[..]) {
log::trace!("alarm frame addressed to another station; dropping");
continue;
}
match &mut self.alarm {
Some(ch) => {
let a = ch.on_frame(&frame, now);
if !a.is_empty() {
pending.push((self.alarm_epoch, a));
}
}
None => {
log::debug!("alarm frame with no alarm channel open; dropping");
self.diag_shared
.rx_no_channel
.fetch_add(1, Ordering::Relaxed);
}
}
continue;
}
match handle_dcp_frame(&frame, &self.setup.dcp) {
Ok(Some(resp)) => self.eth.send(&resp)?,
Ok(None) => {}
Err(e) => log::warn!("dropping unparsable DCP frame: {e}"),
}
}
self.publish_alarm_stats();
self.drain_diag_queue(now, &mut pending);
while let Some((buf, from)) = self.rpc.recv(Some(Duration::ZERO))? {
report.rpc_datagrams += 1;
match self.cm.handle_datagram(&buf, from, now) {
Ok(out) => self.dispatch(out, now, &mut report)?,
Err(e) => log::warn!("dropping unparsable RPC datagram: {e}"),
}
}
#[cfg(target_os = "linux")]
self.drain_rt_events(now, &mut report)?;
let out = self.cm.tick(now);
self.dispatch(out, now, &mut report)?;
if let Some(ch) = &mut self.alarm {
let a = ch.on_tick(now);
if !a.is_empty() {
pending.push((self.alarm_epoch, a));
}
}
self.publish_alarm_stats();
for (epoch, batch) in pending {
self.apply_alarm_actions(batch, epoch, now, &mut report)?;
}
Ok(report)
}
fn drain_diag_queue(&mut self, now: Instant, pending: &mut Vec<(u64, Vec<AlarmAction>)>) {
let cmds: Vec<DiagCommand> = self
.diag_shared
.queue
.lock()
.unwrap_or_else(|e| e.into_inner())
.drain(..)
.collect();
if cmds.is_empty() {
return;
}
for cmd in cmds {
let notification = match cmd {
DiagCommand::Raise(d) => {
if !self.diag.knows(d.slot) {
log::warn!("diagnosis for unknown slot {}; dropping", d.slot.0);
}
self.diag.raise(d)
}
DiagCommand::Clear {
slot,
channel,
error,
} => self.diag.clear(slot, channel, error),
};
if let Some(n) = notification {
let a = self.queue_notification(n, now);
if !a.is_empty() {
pending.push((self.alarm_epoch, a));
}
}
}
self.publish_diag_state();
}
fn publish_diag_state(&self) {
*self
.diag_shared
.active
.lock()
.unwrap_or_else(|e| e.into_inner()) = self.diag.active();
self.problem
.store(self.diag.problem_indicator(), Ordering::Relaxed);
}
fn publish_alarm_stats(&self) {
let Some(ch) = &self.alarm else {
return;
};
let s = ch.stats();
let d = &self.diag_shared;
d.sent.store(s.sent, Ordering::Relaxed);
d.acked.store(s.acked, Ordering::Relaxed);
d.retries.store(s.retries, Ordering::Relaxed);
d.unexpected_rx.store(s.unexpected_rx, Ordering::Relaxed);
d.send_failures.store(s.send_failures, Ordering::Relaxed);
d.ack_timeouts.store(s.ack_timeouts, Ordering::Relaxed);
d.rx_err_rta.store(s.rx_err_rta, Ordering::Relaxed);
}
fn queue_notification(&mut self, mut n: AlarmNotification, now: Instant) -> Vec<AlarmAction> {
let id = self.next_alarm_id;
let Some(ch) = self.alarm.as_mut() else {
return Vec::new();
};
n.specifier.sequence = ch.next_specifier_sequence();
let req = AlarmReq {
id,
priority: Priority::Low,
notification: n,
};
match ch.enqueue(req, now) {
Ok(actions) => {
self.next_alarm_id = self.next_alarm_id.wrapping_add(1);
actions
}
Err(e) => {
log::error!("dropping diagnosis alarm: {e}");
Vec::new()
}
}
}
fn apply_alarm_actions(
&mut self,
actions: Vec<AlarmAction>,
epoch: u64,
now: Instant,
report: &mut StepReport,
) -> Result<(), DeviceError> {
if self.alarm.is_none() || self.alarm_epoch != epoch {
log::debug!(
"dropping {} alarm action(s) from a closed alarm channel",
actions.len()
);
return Ok(());
}
for action in actions {
match action {
AlarmAction::Send(frame) => self.eth.send(&frame)?,
AlarmAction::Acked { id, status } => {
log::debug!("alarm {id} acknowledged, status {:#010x}", status.to_u32());
}
AlarmAction::UnexpectedRx => {
log::debug!("unexpected frame on the alarm channel");
}
AlarmAction::Abort(reason) => {
log::warn!("alarm channel asked for an AR abort: {reason:?}");
self.abort_with_err_rta_in(reason, now, report)?;
}
}
}
self.publish_alarm_stats();
Ok(())
}
fn abort_with_err_rta_in(
&mut self,
reason: AbortReason,
now: Instant,
report: &mut StepReport,
) -> Result<(), DeviceError> {
if let (Some(code2), Some(ch)) = (err_rta_code2(&reason), self.alarm.as_mut()) {
let frame = ch.err_rta(PnioStatus::rta_abort(code2));
self.eth.send(&frame)?;
}
let out = self.cm.abort(reason, now);
self.dispatch(out, now, report)
}
fn open_alarm_channel(&mut self) {
let Some(params) = self.cm.context().map(|c| c.params.clone()) else {
return; };
self.alarm_epoch = self.alarm_epoch.wrapping_add(1);
self.alarm = Some(AlarmChannel::new(AlarmChannelConfig {
local_ref: params.alarm_ref_local,
remote_ref: params.alarm_ref_remote,
rta_timeout: RTA_TIMEOUT_UNIT * params.rta_timeout_factor.max(1) as u32,
rta_retries: params.rta_retries,
max_alarm_data_length: params.max_alarm_data_length_remote,
peer_mac: params.initiator_mac,
our_mac: self.setup.dcp.mac,
}));
}
fn dispatch(
&mut self,
out: CmOutput,
now: Instant,
report: &mut StepReport,
) -> Result<(), DeviceError> {
for o in out.send {
self.rpc.send(&o.bytes, o.to)?;
report.sent += 1;
}
for (state, reason) in out.notify {
if let Some(cb) = &mut self.on_state_change {
cb(state, reason);
}
match (state, reason) {
(ArState::Data, None) => {
#[cfg(target_os = "linux")]
self.start_runner();
#[cfg(not(target_os = "linux"))]
if self.setup.rt.is_some() {
log::error!("cyclic RT thread is Linux-only");
}
self.open_alarm_channel();
let epoch = self.alarm_epoch;
for n in self.diag.replay() {
let actions = self.queue_notification(n, now);
self.apply_alarm_actions(actions, epoch, now, report)?;
}
self.publish_diag_state();
}
(ArState::Idle, Some(_)) => {
#[cfg(target_os = "linux")]
self.stop_runner();
self.alarm = None;
}
_ => {}
}
}
Ok(())
}
}
#[cfg(target_os = "linux")]
impl<E: EthTransport, R: RpcTransport> Device<E, R> {
pub fn with_runner_factory(
&mut self,
f: impl Fn(RtConfig) -> Result<RtHandle, RtError> + Send + 'static,
) {
self.runner_factory = Box::new(f);
}
pub fn rt_running(&self) -> bool {
self.runner.as_ref().is_some_and(|r| r.is_running())
}
fn start_runner(&mut self) {
let Some(rt) = self.setup.rt.clone() else {
return;
};
let Some(params) = self.cm.context().map(|c| c.params.clone()) else {
return; };
let layout = match Layout::from_ar(¶ms, &self.setup.model) {
Ok(layout) => layout,
Err(e) => {
log::error!("cyclic layout build failed, AR stays up without cyclic data: {e}");
return;
}
};
self.image.rebuild(&layout);
let cfg = RtConfig {
iface: rt.iface,
our_mac: self.setup.dcp.mac,
cpu_mac: params.initiator_mac,
layout,
image: self.image.clone(),
stats: self.stats.clone(),
problem_indicator: self.problem.clone(),
cpu_pin: rt.cpu_pin,
rt_priority: rt.rt_priority,
lock_memory: rt.lock_memory,
};
match (self.runner_factory)(cfg) {
Ok(handle) => self.runner = Some(handle),
Err(e) => log::error!("RT runner spawn failed: {e}"),
}
}
fn stop_runner(&mut self) {
if let Some(runner) = self.runner.take() {
runner.stop();
if let Err(e) = runner.join(Duration::from_millis(500)) {
log::warn!("RT runner join timed out: {e}");
}
let mut v = self.image.validity();
v.watchdog = WatchdogState::Expired;
v.provider_run = false;
self.image.set_validity(v);
self.image.clear();
}
}
fn drain_rt_events(
&mut self,
now: Instant,
report: &mut StepReport,
) -> Result<(), DeviceError> {
while let Some(ev) = self.runner.as_ref().and_then(|r| r.take_event()) {
match ev {
RtEvent::WatchdogExpired => {
log::warn!("RT consumer watchdog expired; aborting the AR");
self.abort_with_err_rta_in(AbortReason::RtWatchdog, now, report)?;
}
RtEvent::SocketError(s) => {
log::error!("RT socket error, aborting the AR: {s}");
self.abort_with_err_rta_in(AbortReason::RtSocket, now, report)?;
}
RtEvent::SchedWarning(s) => log::warn!("RT scheduling warning: {s}"),
RtEvent::Exited => log::info!("RT thread exited"),
}
}
Ok(())
}
}
impl<E: EthTransport, R: RpcTransport> Drop for Device<E, R> {
fn drop(&mut self) {
#[cfg(target_os = "linux")]
self.stop_runner();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::alarm::{is_alarm_frame, parse_frame, AlarmType, RtaBody, RtaData};
use crate::cm::model::DeviceModel;
use crate::cm::PnioStatus;
use crate::config::{Direction, Slot};
use crate::dcp::{DcpConfig, DeviceProperties};
use crate::diag::{ChannelError, Diagnosis, Severity};
use crate::eth::EthTransport;
use crate::eth::{MacAddr, MockTransport};
use crate::rpc::{MockRpcTransport, Uuid};
#[cfg(target_os = "linux")]
use crate::rt::{Freshness, RtRunner};
#[cfg(target_os = "linux")]
use crate::testutil::golden_rt;
use crate::testutil::{golden, golden_alarm, RPC_OFF};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
const MAC: MacAddr = MacAddr([0x8c, 0xf3, 0x19, 0xcd, 0x19, 0xf8]);
fn setup() -> DeviceSetup {
DeviceSetup {
dcp: DcpConfig {
mac: MAC,
properties: DeviceProperties {
name_of_station: "rt-labs-dev".into(),
type_of_station: "P-Net Sample Application".into(),
vendor_id: 0x0493,
device_id: 0x0002,
device_role: 0x0100,
device_instance: 1,
device_options: vec![1, 2, 2, 2, 2, 3],
ip: [172, 16, 2, 10],
subnet: [255, 255, 255, 0],
gateway: [172, 16, 2, 10],
ip_block_info: 1,
},
},
model: DeviceModel::pnet_sample(MAC),
activity_seed: Uuid::parse_str("14af198a-1234-1056-8079-8cf319cd19f8").unwrap(),
rt: None,
im0: Im0::default(),
im_store: None,
}
}
const CPU_MAC: MacAddr = MacAddr([0xec, 0x1c, 0x5d, 0x61, 0xe7, 0x3f]);
#[derive(Clone)]
struct SharedEth(Arc<MockTransport>);
impl SharedEth {
fn new() -> SharedEth {
SharedEth(Arc::new(MockTransport::new()))
}
fn sent(&self) -> Vec<Vec<u8>> {
self.0.sent()
}
fn push_rx(&self, frame: Vec<u8>) {
self.0.push_rx(frame);
}
}
impl EthTransport for SharedEth {
fn send(&self, frame: &[u8]) -> Result<(), TransportError> {
self.0.send(frame)
}
fn recv_into(
&self,
buf: &mut [u8],
timeout: Option<Duration>,
) -> Result<Option<usize>, TransportError> {
self.0.recv_into(buf, timeout)
}
}
fn cpu_alarm(name: &str) -> Vec<u8> {
let mut f = golden_alarm(name);
f[6..12].copy_from_slice(&CPU_MAC.0);
f
}
fn feed_bring_up(
dev: &Device<SharedEth, MockRpcTransport>,
seq_base: Option<u32>,
session_key: Option<u16>,
) {
let cpu = "172.16.2.100:54766".parse().unwrap();
let cpu_cm = "172.16.2.100:34964".parse().unwrap();
for (i, name) in ["connect_req", "write_req", "prmend_req"]
.iter()
.enumerate()
{
let mut pdu = golden(name)[RPC_OFF..].to_vec();
if let Some(base) = seq_base {
pdu[64..68].copy_from_slice(&(base + i as u32).to_le_bytes());
}
if let (Some(key), 0) = (session_key, i) {
pdu[124..126].copy_from_slice(&key.to_be_bytes());
}
dev.rpc().push_rx(pdu, cpu);
}
dev.rpc()
.push_rx(golden("appready_res")[RPC_OFF..].to_vec(), cpu_cm);
}
fn device_in_data() -> (Device<SharedEth, MockRpcTransport>, SharedEth) {
let eth = SharedEth::new();
let mut dev = Device::new(setup(), eth.clone(), MockRpcTransport::new());
feed_bring_up(&dev, None, None);
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!(dev.state(), ArState::Data);
(dev, eth)
}
fn reconnect(dev: &mut Device<SharedEth, MockRpcTransport>) {
feed_bring_up(dev, Some(0x100), None);
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
}
fn line_break() -> Diagnosis {
Diagnosis {
slot: Slot(1),
channel: 0,
error: ChannelError::LineBreak,
severity: Severity::Fault,
direction: Direction::Input,
}
}
#[test]
fn diagnosis_raised_through_the_queue_hits_the_wire_and_the_problem_bit() {
let (mut dev, eth) = device_in_data();
let shared = dev.diag_shared();
shared
.queue
.lock()
.unwrap()
.push_back(DiagCommand::Raise(line_break()));
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
let sent = eth.sent();
let last = sent.last().expect("a notification was sent");
let pdu = parse_frame(last).unwrap();
let RtaBody::Data(RtaData::Notification(n)) = pdu.body else {
panic!("expected an AlarmNotification, got {:?}", pdu.body)
};
assert_eq!(n.alarm_type, AlarmType::Diagnosis);
assert_eq!(n.slot, 1);
assert_eq!(n.specifier.sequence, 0);
assert_eq!(pdu.header.send_seq, 0xFFFF);
assert!(dev.problem_indicator());
assert!(dev.alarm_in_flight());
assert_eq!(
dev.poll_interval(Duration::from_millis(200)),
Duration::from_millis(20)
);
assert_eq!(shared.active.lock().unwrap().len(), 1);
assert_eq!(shared.sent.load(Ordering::Relaxed), 1);
}
#[test]
fn controller_err_rta_aborts_and_drops_the_channel() {
let (mut dev, eth) = device_in_data();
eth.push_rx(cpu_alarm("alarm_err_rta_cpu_removed"));
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!(dev.state(), ArState::Idle);
assert!(!dev.alarm_in_flight());
assert!(
eth.sent().iter().all(|f| !is_alarm_frame(f)),
"no ERR-RTA answer to a controller abort"
);
assert_eq!(dev.diag_shared().rx_err_rta.load(Ordering::Relaxed), 1);
}
#[test]
fn shutdown_sends_err_rta_ar_removed() {
let (mut dev, eth) = device_in_data();
dev.shutdown(Instant::now()).unwrap();
let last = eth.sent().last().unwrap().clone();
let pdu = parse_frame(&last).unwrap();
assert_eq!(
pdu.body,
RtaBody::Err(PnioStatus::rta_abort(PnioStatus::RTA_ABORT_AR_REMOVED))
);
assert_eq!(dev.state(), ArState::Idle);
let before = eth.sent().len();
dev.shutdown(Instant::now()).unwrap();
assert_eq!(eth.sent().len(), before);
}
#[test]
fn active_diagnosis_is_replayed_on_the_next_data() {
let (mut dev, eth) = device_in_data();
dev.diag_shared()
.queue
.lock()
.unwrap()
.push_back(DiagCommand::Raise(line_break()));
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
eth.push_rx(cpu_alarm("alarm_err_rta_cpu_removed"));
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!(dev.state(), ArState::Idle);
let before = eth.sent().len();
reconnect(&mut dev);
assert_eq!(dev.state(), ArState::Data);
let replayed: Vec<Vec<u8>> = eth.sent()[before..]
.iter()
.filter(|f| is_alarm_frame(f))
.cloned()
.collect();
assert_eq!(replayed.len(), 1);
let RtaBody::Data(RtaData::Notification(n)) = parse_frame(&replayed[0]).unwrap().body
else {
panic!("expected a replayed AlarmNotification")
};
assert_eq!(n.alarm_type, AlarmType::Diagnosis);
assert_eq!(n.slot, 1);
assert!(dev.problem_indicator());
}
#[test]
fn stale_abort_does_not_tear_down_an_ar_rebuilt_in_the_same_step() {
let (mut dev, eth) = device_in_data();
eth.push_rx(cpu_alarm("alarm_err_rta_cpu_removed"));
feed_bring_up(&dev, Some(0x200), Some(3));
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!(dev.state(), ArState::Data, "the reconnected AR survives");
assert_eq!(dev.ar_params().unwrap().session_key, 3);
assert!(!dev.alarm_in_flight());
let before = eth.sent().len();
dev.diag_shared()
.queue
.lock()
.unwrap()
.push_back(DiagCommand::Raise(line_break()));
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
let fresh: Vec<Vec<u8>> = eth.sent()[before..]
.iter()
.filter(|f| is_alarm_frame(f))
.cloned()
.collect();
assert_eq!(fresh.len(), 1);
assert!(matches!(
parse_frame(&fresh[0]).unwrap().body,
RtaBody::Data(RtaData::Notification(_))
));
assert!(dev.alarm_in_flight());
}
#[test]
fn alarm_frames_addressed_to_another_station_are_ignored() {
let (mut dev, eth) = device_in_data();
let mut foreign = cpu_alarm("alarm_err_rta_cpu_removed");
foreign[..6].copy_from_slice(&[0x8c, 0xf3, 0x19, 0xcd, 0x19, 0xff]);
eth.push_rx(foreign);
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!(dev.state(), ArState::Data);
let shared = dev.diag_shared();
assert_eq!(shared.rx_no_channel.load(Ordering::Relaxed), 0);
assert_eq!(shared.rx_err_rta.load(Ordering::Relaxed), 0);
assert_eq!(shared.unexpected_rx.load(Ordering::Relaxed), 0);
}
#[test]
fn alarm_frames_without_a_channel_are_counted_and_dropped() {
let eth = MockTransport::new();
eth.push_rx(golden_alarm("alarm_ack_high_cpu"));
let mut dev = Device::new(setup(), eth, MockRpcTransport::new());
let r = dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!(r.eth_frames, 1);
assert_eq!(dev.eth().sent(), Vec::<Vec<u8>>::new());
assert_eq!(dev.diag_shared().rx_no_channel.load(Ordering::Relaxed), 1);
}
#[test]
fn full_bring_up_through_the_loop() {
let eth = MockTransport::new();
let rpc = MockRpcTransport::new();
let cpu = "172.16.2.100:54766".parse().unwrap();
let cpu_cm = "172.16.2.100:34964".parse().unwrap();
eth.push_rx(golden("dcp_set_req"));
rpc.push_rx(golden("connect_req")[RPC_OFF..].to_vec(), cpu);
rpc.push_rx(golden("write_req")[RPC_OFF..].to_vec(), cpu);
rpc.push_rx(golden("prmend_req")[RPC_OFF..].to_vec(), cpu);
rpc.push_rx(golden("appready_res")[RPC_OFF..].to_vec(), cpu_cm);
let mut dev = Device::new(setup(), eth, rpc);
let seen = Arc::new(Mutex::new(Vec::new()));
let s2 = seen.clone();
dev.on_state_change(move |st, why| s2.lock().unwrap().push((st, why)));
let r = dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!((r.eth_frames, r.rpc_datagrams), (1, 4));
assert_eq!(dev.state(), ArState::Data);
assert_eq!(dev.eth().sent(), vec![golden("dcp_set_res")]);
let sent = dev.rpc().sent();
assert_eq!(sent.len(), 4);
assert_eq!(sent[0].0, golden("connect_res")[RPC_OFF..]);
assert_eq!(sent[3].0, golden("appready_req")[RPC_OFF..]);
assert_eq!(sent[3].1, cpu_cm);
assert_eq!(
*seen.lock().unwrap(),
vec![(ArState::Connected, None), (ArState::Data, None)]
);
}
#[test]
fn garbage_is_dropped_and_loop_continues() {
let eth = MockTransport::new();
let rpc = MockRpcTransport::new();
let cpu = "172.16.2.100:54766".parse().unwrap();
rpc.push_rx(vec![1, 2, 3], cpu);
rpc.push_rx(golden("connect_req")[RPC_OFF..].to_vec(), cpu);
let mut dev = Device::new(setup(), eth, rpc);
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!(dev.state(), ArState::Connected);
}
#[test]
fn run_returns_when_flag_is_set_from_another_thread() {
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let mut dev = Device::new(setup(), MockTransport::new(), MockRpcTransport::new());
let flipper = {
let stop = stop.clone();
std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(30));
stop.store(true, std::sync::atomic::Ordering::Relaxed);
})
};
dev.run(&stop).unwrap();
flipper.join().unwrap();
}
struct FailingRpc(MockRpcTransport);
impl RpcTransport for FailingRpc {
fn send(&self, _buf: &[u8], _to: std::net::SocketAddr) -> Result<(), RpcError> {
Err(RpcError::Io(std::io::Error::other("boom")))
}
fn recv(
&self,
timeout: Option<Duration>,
) -> Result<Option<(Vec<u8>, std::net::SocketAddr)>, RpcError> {
self.0.recv(timeout)
}
}
#[test]
fn transport_error_propagates_out_of_run() {
let eth = MockTransport::new();
let rpc = FailingRpc(MockRpcTransport::new());
let cpu = "172.16.2.100:54766".parse().unwrap();
rpc.0
.push_rx(golden("connect_req")[RPC_OFF..].to_vec(), cpu);
let mut dev = Device::new(setup(), eth, rpc);
let err = dev.step(Instant::now(), Some(Duration::ZERO)).unwrap_err();
assert!(matches!(err, DeviceError::Rpc(_)));
}
#[cfg(target_os = "linux")]
#[test]
fn data_starts_the_runner_and_idle_stops_it() {
let eth = MockTransport::new();
let rpc = MockRpcTransport::new();
let cpu = "172.16.2.100:54766".parse().unwrap();
let cpu_cm = "172.16.2.100:34964".parse().unwrap();
rpc.push_rx(golden("connect_req")[RPC_OFF..].to_vec(), cpu);
rpc.push_rx(golden("prmend_req")[RPC_OFF..].to_vec(), cpu);
rpc.push_rx(golden("appready_res")[RPC_OFF..].to_vec(), cpu_cm);
let mut s = setup();
s.rt = Some(RtOptions {
iface: "mock".into(),
cpu_pin: None,
rt_priority: None,
lock_memory: false,
});
let mut dev = Device::new(s, eth, rpc);
dev.with_runner_factory(|cfg| RtRunner::spawn_with_transport(cfg, MockTransport::new()));
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!(dev.state(), ArState::Data);
assert!(dev.rt_running());
assert_eq!(dev.image().cells().len(), 7);
let mut rel = golden("prmend_req")[RPC_OFF..].to_vec();
rel[68] = 1; rel[64] = 9; rel[100] = 0x01;
rel[101] = 0x14; rel[128] = 0x00;
rel[129] = 0x04; dev.rpc().push_rx(rel, cpu);
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!(dev.state(), ArState::Idle);
assert!(!dev.rt_running());
assert_eq!(dev.image().validity().freshness(), Freshness::Stale);
assert!(dev.image().cells().is_empty());
}
#[cfg(target_os = "linux")]
#[test]
fn watchdog_event_aborts_the_ar() {
let eth = MockTransport::new();
let rpc = MockRpcTransport::new();
let cpu = "172.16.2.100:54766".parse().unwrap();
let cpu_cm = "172.16.2.100:34964".parse().unwrap();
rpc.push_rx(golden("connect_req")[RPC_OFF..].to_vec(), cpu);
rpc.push_rx(golden("prmend_req")[RPC_OFF..].to_vec(), cpu);
rpc.push_rx(golden("appready_res")[RPC_OFF..].to_vec(), cpu_cm);
let mut s = setup();
s.rt = Some(RtOptions {
iface: "mock".into(),
cpu_pin: None,
rt_priority: None,
lock_memory: false,
});
let mut dev = Device::new(s, eth, rpc);
dev.with_runner_factory(|mut cfg| {
cfg.layout.input_cr.cycle_step = 160;
cfg.layout.output_cr.cycle_step = 160;
cfg.layout.output_cr.watchdog = Duration::from_millis(10);
let mock = MockTransport::new();
mock.push_rx(golden_rt("rtc_cpu_8001"));
RtRunner::spawn_with_transport(cfg, mock)
});
let seen = Arc::new(Mutex::new(Vec::new()));
let s2 = seen.clone();
dev.on_state_change(move |st, why| s2.lock().unwrap().push((st, why)));
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!(dev.state(), ArState::Data);
assert!(dev.rt_running());
std::thread::sleep(Duration::from_millis(60));
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!(dev.state(), ArState::Idle);
assert_eq!(
seen.lock().unwrap().last(),
Some(&(ArState::Idle, Some(AbortReason::RtWatchdog)))
);
assert!(!dev.rt_running());
let last = dev
.eth()
.sent()
.last()
.expect("an ERR-RTA was sent")
.clone();
assert_eq!(
parse_frame(&last).unwrap().body,
RtaBody::Err(PnioStatus::rta_abort(PnioStatus::RTA_ABORT_DHT_WDT_EXPIRED))
);
}
struct FailingTransport;
impl EthTransport for FailingTransport {
fn send(&self, _frame: &[u8]) -> Result<(), TransportError> {
Err(TransportError::Io(std::io::Error::other("boom")))
}
fn recv_into(
&self,
_buf: &mut [u8],
_timeout: Option<Duration>,
) -> Result<Option<usize>, TransportError> {
Ok(None)
}
}
#[cfg(target_os = "linux")]
#[test]
fn socket_error_event_aborts_the_ar_with_rt_socket() {
let eth = MockTransport::new();
let rpc = MockRpcTransport::new();
let cpu = "172.16.2.100:54766".parse().unwrap();
let cpu_cm = "172.16.2.100:34964".parse().unwrap();
rpc.push_rx(golden("connect_req")[RPC_OFF..].to_vec(), cpu);
rpc.push_rx(golden("prmend_req")[RPC_OFF..].to_vec(), cpu);
rpc.push_rx(golden("appready_res")[RPC_OFF..].to_vec(), cpu_cm);
let mut s = setup();
s.rt = Some(RtOptions {
iface: "mock".into(),
cpu_pin: None,
rt_priority: None,
lock_memory: false,
});
let mut dev = Device::new(s, eth, rpc);
dev.with_runner_factory(|cfg| RtRunner::spawn_with_transport(cfg, FailingTransport));
let seen = Arc::new(Mutex::new(Vec::new()));
let s2 = seen.clone();
dev.on_state_change(move |st, why| s2.lock().unwrap().push((st, why)));
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!(dev.state(), ArState::Data);
assert!(dev.rt_running());
std::thread::sleep(Duration::from_millis(60));
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!(dev.state(), ArState::Idle);
assert_eq!(
seen.lock().unwrap().last(),
Some(&(ArState::Idle, Some(AbortReason::RtSocket)))
);
assert!(!dev.rt_running());
let last = dev
.eth()
.sent()
.last()
.expect("an ERR-RTA was sent")
.clone();
assert_eq!(
parse_frame(&last).unwrap().body,
RtaBody::Err(PnioStatus::rta_abort(PnioStatus::RTA_ABORT_AR_REMOVED))
);
}
struct NeverReadable(std::os::fd::RawFd);
impl NeverReadable {
#[allow(unsafe_code)] fn new() -> Self {
let mut fds = [0; 2];
let r = unsafe { libc::pipe(fds.as_mut_ptr()) };
assert_eq!(r, 0, "pipe");
NeverReadable(fds[0])
}
}
struct PollableEth(SharedEth, std::os::fd::RawFd);
impl EthTransport for PollableEth {
fn send(&self, frame: &[u8]) -> Result<(), TransportError> {
self.0.send(frame)
}
fn recv_into(
&self,
buf: &mut [u8],
timeout: Option<Duration>,
) -> Result<Option<usize>, TransportError> {
self.0.recv_into(buf, timeout)
}
fn raw_fd(&self) -> Option<std::os::fd::RawFd> {
Some(self.1)
}
}
struct PollableRpc(MockRpcTransport, std::os::fd::RawFd);
impl RpcTransport for PollableRpc {
fn send(&self, buf: &[u8], to: std::net::SocketAddr) -> Result<(), RpcError> {
self.0.send(buf, to)
}
fn recv(
&self,
timeout: Option<Duration>,
) -> Result<Option<(Vec<u8>, std::net::SocketAddr)>, RpcError> {
self.0.recv(timeout)
}
fn raw_fd(&self) -> Option<std::os::fd::RawFd> {
Some(self.1)
}
}
#[test]
fn poll_wait_does_not_age_the_alarm_retry_deadline() {
let eth = SharedEth::new();
let pipe = NeverReadable::new();
let mut dev = Device::new(
setup(),
PollableEth(eth.clone(), pipe.0),
PollableRpc(MockRpcTransport::new(), pipe.0),
);
let cpu = "172.16.2.100:54766".parse().unwrap();
let cpu_cm = "172.16.2.100:34964".parse().unwrap();
for name in ["connect_req", "write_req", "prmend_req"] {
dev.rpc().0.push_rx(golden(name)[RPC_OFF..].to_vec(), cpu);
}
dev.rpc()
.0
.push_rx(golden("appready_res")[RPC_OFF..].to_vec(), cpu_cm);
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
assert_eq!(dev.state(), ArState::Data);
dev.diag_shared()
.queue
.lock()
.unwrap()
.push_back(DiagCommand::Raise(Diagnosis {
slot: Slot(1),
channel: 0,
error: ChannelError::LineBreak,
severity: Severity::Fault,
direction: Direction::Input,
}));
let t0 = Instant::now();
dev.step(t0, Some(Duration::from_millis(150))).unwrap();
assert!(
t0.elapsed() >= Duration::from_millis(140),
"the poll must have waited"
);
let alarms_after_send = eth
.sent()
.iter()
.filter(|f| crate::alarm::is_alarm_frame(f))
.count();
assert_eq!(alarms_after_send, 1, "one notification sent");
dev.step(Instant::now(), Some(Duration::ZERO)).unwrap();
let alarms_now = eth
.sent()
.iter()
.filter(|f| crate::alarm::is_alarm_frame(f))
.count();
assert_eq!(alarms_now, 1, "no premature retransmission");
assert_eq!(
dev.diag_shared().retries.load(Ordering::Relaxed),
0,
"no retry counted"
);
}
}