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}