Skip to main content

kinavis_alerts/
lib.rs

1//! Bridge alert management (BAM): aggregation, prioritisation and
2//! acknowledgement of conditions reported as events.
3//!
4//! Domain operations report a condition on every evaluation that finds it
5//! (guidance reports XTE exceeded on each call, traffic raises a CPA alarm on
6//! each assessment). [`AlertManager`] turns that stream into one alert per
7//! condition, with a priority, acknowledgement and clearance.
8//!
9//! # Alert lifecycle
10//!
11//! An event declares its condition via [`Reportable`]; the policy assigns a
12//! priority. A classified condition raises an alert keyed by [`AlertKind`]
13//! (condition + target, source or sensor). If an alert of that kind is already
14//! standing, it is repeated: its count increments and nothing new is
15//! annunciated. States follow IMO BAM:
16//!
17//! - [`AlertState::Active`] — raised, not acknowledged;
18//! - [`AlertState::Acknowledged`] — acknowledged, condition present;
19//! - [`AlertState::Rectified`] — condition gone, not yet acknowledged;
20//! - normal — gone and acknowledged; removed from the list.
21//!
22//! A condition ends either by an ending event (fix acquired ends fix lost, a
23//! healthy sensor ends its suspicion, target lost ends its CPA alarm) or by
24//! silence: an alert not repeated within [`AlertPolicy::rectify_after`] is
25//! rectified on the next [`AlertManager::tick`].
26//!
27//! # Policy
28//!
29//! Priority is the vessel's decision: [`AlertPolicy`] is the port,
30//! [`StandardPolicy`] a default. XTE exceeded may be a warning in open water
31//! and an alarm in a channel. What counts as a condition is the domain's:
32//! [`Reportable`] is implemented for every event type in the workspace;
33//! applications implement it for their own event unions.
34//!
35//! ```rust
36//! use kinavis_alerts::{AlertChange, AlertManager, AlertPriority, AlertState, StandardPolicy};
37//! use kinavis_kernel::{Distance, EventList, Instant, TargetId, Utc};
38//! use kinavis_traffic::TrafficEvent;
39//! use core::time::Duration;
40//!
41//! let mut alerts = AlertManager::new(StandardPolicy::default());
42//! let start = Instant::<Utc>::from_unix_seconds(1_789_000_000);
43//!
44//! // Every assessment reports the same target too close; one alert stands.
45//! for second in 0..3 {
46//!     let now = start.checked_add(Duration::from_secs(second)).unwrap();
47//!     let mut events = EventList::<TrafficEvent>::new();
48//!     events.push(TrafficEvent::CpaAlarm {
49//!         target: TargetId::new(7),
50//!         cpa: Distance::from_cables(3.0)?,
51//!         tcpa: Duration::from_secs(600),
52//!         at: now,
53//!     });
54//!     let changes = alerts.ingest(&events, now);
55//!     if second == 0 {
56//!         assert!(matches!(changes[0], AlertChange::Raised(_)));
57//!     } else {
58//!         assert!(changes.is_empty());
59//!     }
60//! }
61//! assert_eq!(alerts.len(), 1);
62//! let alarm = alerts.alerts()[0];
63//! assert_eq!(alarm.priority(), AlertPriority::Alarm);
64//! assert_eq!(alarm.occurrences(), 3);
65//!
66//! // Acknowledged, it stands quietly; when the target is lost it is over.
67//! alerts.acknowledge(alarm.id(), start)?;
68//! assert_eq!(alerts.alerts()[0].state(), AlertState::Acknowledged);
69//! let mut events = EventList::<TrafficEvent>::new();
70//! events.push(TrafficEvent::TargetLost { target: TargetId::new(7), last_seen: start });
71//! let changes = alerts.ingest(&events, start.checked_add(Duration::from_secs(60)).unwrap());
72//! assert!(changes.iter().any(|change| matches!(change, AlertChange::Cleared(_))));
73//! // The lost target is itself a caution, standing on its own.
74//! assert_eq!(alerts.len(), 1);
75//! assert_eq!(alerts.alerts()[0].priority(), AlertPriority::Caution);
76//! # Ok::<(), kinavis_kernel::KernelError>(())
77//! ```
78//!
79//! # Feature flags
80//!
81//! - `std` *(default)* — standard library maths in the kernel.
82//! - `libm` — for `no_std` targets: `--no-default-features --features libm`.
83//! - `serde` — serialisation of the value types.
84//!
85//! No allocation: the manager holds at most [`MAX_ALERTS`] alerts inline and
86//! reports overflow instead of dropping silently.
87
88#![cfg_attr(not(feature = "std"), no_std)]
89
90// The crate does not allocate; tests use `format!`.
91#[cfg(test)]
92extern crate alloc;
93
94mod manager;
95mod policy;
96mod reportable;
97
98use core::fmt;
99
100use kinavis_kernel::event::{NavigationIntegrity, PositionSource, SensorId, TargetId};
101use kinavis_kernel::time::{Instant, Utc};
102
103pub use manager::{AlertChange, AlertChanges, AlertManager, MAX_ALERTS, MAX_CHANGES};
104pub use policy::{AlertPolicy, StandardPolicy};
105pub use reportable::Reportable;
106
107/// Runs the `README.md` example as a doctest.
108#[cfg(doctest)]
109#[doc = include_str!("../README.md")]
110pub struct ReadmeExamples;
111
112/// Alert priority per BAM, in ascending order.
113///
114/// `#[non_exhaustive]`; match with a wildcard arm.
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
116#[non_exhaustive]
117#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
118pub enum AlertPriority {
119    /// Awareness; no immediate attention required.
120    Caution,
121    /// Attention required to prevent a hazard.
122    Warning,
123    /// Immediate attention and action required.
124    Alarm,
125    /// Immediate danger to life or the ship.
126    EmergencyAlarm,
127}
128
129impl fmt::Display for AlertPriority {
130    /// Formats as `alarm`, `warning`, `caution`, `emergency alarm`.
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        f.write_str(match self {
133            Self::Caution => "caution",
134            Self::Warning => "warning",
135            Self::Alarm => "alarm",
136            Self::EmergencyAlarm => "emergency alarm",
137        })
138    }
139}
140
141/// Alert state.
142///
143/// `#[non_exhaustive]`; match with a wildcard arm.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
145#[non_exhaustive]
146#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
147pub enum AlertState {
148    /// Raised, not acknowledged; annunciating.
149    Active,
150    /// Acknowledged; condition present.
151    Acknowledged,
152    /// Condition gone; removed once acknowledged.
153    Rectified,
154}
155
156/// Alert key: condition and subject.
157///
158/// Events of the same kind about the same subject aggregate into one alert.
159///
160/// `#[non_exhaustive]`; match with a wildcard arm.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
162#[non_exhaustive]
163#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
164pub enum AlertKind {
165    /// Cross-track error beyond the guidance limit.
166    CrossTrackExceeded,
167    /// Under-keel clearance below policy.
168    UnderKeelClearanceLow,
169    /// Outside the swinging circle.
170    AnchorDragging,
171    /// Target CPA/TCPA inside the limits.
172    Cpa {
173        /// Target.
174        target: TargetId,
175    },
176    /// Target removed from the picture.
177    TargetLost {
178        /// Target.
179        target: TargetId,
180    },
181    /// Position source stopped delivering.
182    FixLost {
183        /// Source.
184        source: PositionSource,
185    },
186    /// Navigation integrity below nominal.
187    IntegrityDegraded {
188        /// Integrity level.
189        to: NavigationIntegrity,
190    },
191    /// Sensor observations are suspect.
192    SensorSuspect {
193        /// Sensor.
194        sensor: SensorId,
195    },
196    /// An observation was rejected.
197    ObservationRejected,
198}
199
200/// Conditions an event ends.
201///
202/// `#[non_exhaustive]`; match with a wildcard arm.
203#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
204#[non_exhaustive]
205pub enum Ended {
206    /// None.
207    Nothing,
208    /// One condition.
209    One(AlertKind),
210    /// Every integrity degradation: an integrity change supersedes the standing
211    /// degradation and raises its own if it is one.
212    EveryIntegrityDegradation,
213}
214
215/// Alert identifier, unique while the alert stands.
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
217#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
218pub struct AlertId(u32);
219
220impl AlertId {
221    /// Numeric value.
222    #[must_use]
223    pub const fn number(self) -> u32 {
224        self.0
225    }
226}
227
228impl fmt::Display for AlertId {
229    /// Formats as `A17`.
230    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
231        write!(f, "A{}", self.0)
232    }
233}
234
235/// Standing alert.
236#[derive(Debug, Clone, Copy, PartialEq)]
237#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
238pub struct Alert {
239    id: AlertId,
240    kind: AlertKind,
241    priority: AlertPriority,
242    state: AlertState,
243    raised_at: Instant<Utc>,
244    last_reported_at: Instant<Utc>,
245    occurrences: u32,
246}
247
248impl Alert {
249    /// Identifier.
250    #[must_use]
251    pub const fn id(&self) -> AlertId {
252        self.id
253    }
254
255    /// Key.
256    #[must_use]
257    pub const fn kind(&self) -> AlertKind {
258        self.kind
259    }
260
261    /// Priority.
262    #[must_use]
263    pub const fn priority(&self) -> AlertPriority {
264        self.priority
265    }
266
267    /// State.
268    #[must_use]
269    pub const fn state(&self) -> AlertState {
270        self.state
271    }
272
273    /// Time raised.
274    #[must_use]
275    pub const fn raised_at(&self) -> Instant<Utc> {
276        self.raised_at
277    }
278
279    /// Time the condition was last reported.
280    #[must_use]
281    pub const fn last_reported_at(&self) -> Instant<Utc> {
282        self.last_reported_at
283    }
284
285    /// Number of reports, the first included.
286    #[must_use]
287    pub const fn occurrences(&self) -> u32 {
288        self.occurrences
289    }
290}