Skip to main content

kinavis_kernel/
event.rs

1//! Events returned as values.
2//!
3//! Publish/subscribe needs listener storage (allocation), runs foreign code
4//! inside a calculation (unbounded time, possible panic, half-updated state
5//! visible) and makes delivery order incidental. Instead, a state-changing
6//! operation *returns* what happened in an [`EventList`] alongside its result;
7//! the caller alarms, logs or forwards. The list is `#[must_use]` and has fixed
8//! capacity with an explicit [`EventList::overflowed`] flag: a lost navigation
9//! event is worse than a late one.
10//!
11//! The kernel's events (fix acquired or lost, observation rejected, integrity
12//! or sensor health changed) are [`NavigationEvent`]. Other contexts define
13//! their own enums (guidance: waypoint reached; traffic: target lost) in their
14//! own [`EventList`]. The [`Event`] trait defines an event: a timestamp and a
15//! placeholder for unused slots. The kernel does not know outer contexts'
16//! events; the integrating crate (alert board, application) defines the union.
17
18use core::fmt;
19use core::ops::Deref;
20
21use crate::inline::{Inline, InlineStr};
22use crate::time::{Instant, Utc};
23use crate::units::Speed;
24
25/// Position source.
26///
27/// `#[non_exhaustive]`; match with a wildcard arm.
28#[non_exhaustive]
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31pub enum PositionSource {
32    /// Satellite fix.
33    Gnss,
34    /// Dead reckoning from the last fix.
35    DeadReckoning,
36    /// Estimator output.
37    Estimated,
38}
39
40/// Observation rejection reason.
41///
42/// `#[non_exhaustive]`; match with a wildcard arm.
43#[non_exhaustive]
44#[derive(Debug, Clone, Copy, PartialEq)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46pub enum RejectionReason {
47    /// Marked invalid by the source.
48    Invalid,
49    /// Older than the use case accepts.
50    Stale,
51    /// Timestamped before an already accepted observation and not applicable
52    /// retroactively: late observations are rejected by policy, or it is older
53    /// than the policy or history allows.
54    OutOfOrder,
55    /// Implied speed exceeds the plausible maximum.
56    ImplausibleJump {
57        /// Implied speed.
58        implied_speed: Speed,
59    },
60    /// Innovation outside the estimator's gate.
61    Improbable {
62        /// Normalised innovation squared.
63        normalised_innovation_squared: f64,
64    },
65}
66
67/// Integrity of the navigation solution as a whole.
68///
69/// Not per sensor (see [`SensorHealth`]) but combined: whether the position is
70/// held by an absolute source and whether its uncertainty is within the
71/// vessel's alert limit. Ordered best to worst.
72///
73/// `#[non_exhaustive]`; match with a wildcard arm.
74#[non_exhaustive]
75#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77pub enum NavigationIntegrity {
78    /// Absolute position aiding present; uncertainty within the alert limit.
79    Nominal,
80    /// No absolute position for longer than allowed: dead reckoning,
81    /// uncertainty growing, still within the alert limit.
82    DeadReckoning,
83    /// Uncertainty beyond the alert limit. The position is still reported as
84    /// the best available, but must not be relied on for the purpose the limit
85    /// protects.
86    Exceeded,
87}
88
89/// Health of one observation source, as judged by its consumer.
90///
91/// Judged by content, not by presence: detecting silence is the intake's job,
92/// since only it knows the expected rate.
93///
94/// `#[non_exhaustive]`; match with a wildcard arm.
95#[non_exhaustive]
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98pub enum SensorHealth {
99    /// Observations accepted.
100    Healthy,
101    /// Several consecutive observations rejected. Either the source is faulty
102    /// or the estimate has drifted and this source is right; the consumer
103    /// cannot tell which, and the operator should check.
104    Suspect,
105}
106
107/// Maximum sensor name length in an event, bytes.
108pub const SENSOR_NAME_BYTES: usize = 32;
109
110/// Observation source identifier: the source's name, truncated to
111/// [`SENSOR_NAME_BYTES`] so events stay plain values.
112#[derive(Clone, Copy, PartialEq, Eq)]
113#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
114pub struct SensorId(InlineStr<SENSOR_NAME_BYTES>);
115
116impl SensorId {
117    /// Source with this name.
118    #[must_use]
119    pub fn named(name: &str) -> Self {
120        Self(InlineStr::new(name))
121    }
122
123    /// Name.
124    #[must_use]
125    pub fn as_str(&self) -> &str {
126        self.0.as_str()
127    }
128}
129
130impl fmt::Debug for SensorId {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        fmt::Debug::fmt(&self.0, f)
133    }
134}
135
136impl fmt::Display for SensorId {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        fmt::Display::fmt(&self.0, f)
139    }
140}
141
142/// Target identifier: radar track number or AIS MMSI.
143///
144/// A plain number, as both sources provide and events can carry without
145/// allocation. Disambiguating sources that reuse numbers is the intake's job,
146/// before the traffic picture.
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
148#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
149pub struct TargetId(u32);
150
151impl TargetId {
152    /// Target with this number.
153    #[must_use]
154    pub const fn new(number: u32) -> Self {
155        Self(number)
156    }
157
158    /// Number.
159    #[must_use]
160    pub const fn number(self) -> u32 {
161        self.0
162    }
163}
164
165impl fmt::Display for TargetId {
166    /// Formats as `#123456789`.
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        write!(f, "#{}", self.0)
169    }
170}
171
172impl core::hash::Hash for SensorId {
173    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
174        self.as_str().hash(state);
175    }
176}
177
178impl PartialEq<str> for SensorId {
179    fn eq(&self, other: &str) -> bool {
180        self.as_str() == other
181    }
182}
183
184impl PartialEq<&str> for SensorId {
185    fn eq(&self, other: &&str) -> bool {
186        self.as_str() == *other
187    }
188}
189
190/// Kernel event.
191///
192/// `#[non_exhaustive]`; match with a wildcard arm.
193#[non_exhaustive]
194#[derive(Debug, Clone, Copy, PartialEq)]
195#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
196pub enum NavigationEvent {
197    /// Position source started delivering, or resumed after a gap.
198    FixAcquired {
199        /// Source.
200        source: PositionSource,
201        /// Time of the acquiring fix.
202        at: Instant<Utc>,
203    },
204    /// Position source stopped delivering usable fixes.
205    FixLost {
206        /// Source.
207        source: PositionSource,
208        /// Time the loss was established.
209        at: Instant<Utc>,
210        /// Time of the last usable fix.
211        last_good: Instant<Utc>,
212    },
213    /// Observation rejected.
214    ObservationRejected {
215        /// Reason.
216        reason: RejectionReason,
217        /// Time of the rejected observation.
218        at: Instant<Utc>,
219    },
220    /// Solution integrity changed.
221    IntegrityChanged {
222        /// Previous level.
223        from: NavigationIntegrity,
224        /// New level.
225        to: NavigationIntegrity,
226        /// Time of the estimate that crossed the threshold.
227        at: Instant<Utc>,
228    },
229    /// Source health changed.
230    SensorHealthChanged {
231        /// Source.
232        sensor: SensorId,
233        /// Previous health.
234        from: SensorHealth,
235        /// New health.
236        to: SensorHealth,
237        /// Time of the deciding observation.
238        at: Instant<Utc>,
239    },
240}
241
242/// Type storable in an [`EventList`]: something that happened at an instant.
243///
244/// Implemented by every context's event enum and by application unions. The
245/// placeholder fills unused slots so the store needs no allocator and no
246/// `unsafe`; it is never read.
247pub trait Event: Copy + PartialEq + fmt::Debug {
248    /// Unused-slot value; never observed.
249    const PLACEHOLDER: Self;
250
251    /// Time of the event.
252    fn at(&self) -> Instant<Utc>;
253}
254
255impl Event for NavigationEvent {
256    const PLACEHOLDER: Self = Self::FixAcquired {
257        source: PositionSource::Gnss,
258        at: Instant::UNIX_EPOCH,
259    };
260
261    fn at(&self) -> Instant<Utc> {
262        match self {
263            Self::FixAcquired { at, .. }
264            | Self::FixLost { at, .. }
265            | Self::ObservationRejected { at, .. }
266            | Self::IntegrityChanged { at, .. }
267            | Self::SensorHealthChanged { at, .. } => *at,
268        }
269    }
270}
271
272/// Default per-operation event capacity.
273///
274/// A step produces a few events at most (loss and acquisition, one or two
275/// rejections). Operations that can produce more (sweeping a traffic picture)
276/// return an [`EventList`] sized to their maximum, so nothing is lost by
277/// construction; overflow is still reported via [`EventList::overflowed`].
278pub const MAX_EVENTS: usize = 8;
279
280/// Events of one operation, in order.
281///
282/// List of `E` (default [`NavigationEvent`]) with fixed capacity `N`, no
283/// allocation. Dereferences to a slice.
284#[must_use = "an unread event list is a navigation event nobody acted on"]
285#[derive(Clone, Copy)]
286pub struct EventList<E: Event = NavigationEvent, const N: usize = MAX_EVENTS> {
287    events: Inline<E, N>,
288    overflowed: bool,
289}
290
291impl<E: Event> EventList<E, MAX_EVENTS> {
292    /// Empty list with capacity [`MAX_EVENTS`].
293    pub const fn new() -> Self {
294        Self::with_capacity()
295    }
296}
297
298impl<E: Event, const N: usize> EventList<E, N> {
299    /// Empty list with capacity `N`, for operations that can report more than
300    /// [`MAX_EVENTS`].
301    pub const fn with_capacity() -> Self {
302        Self {
303            events: Inline::new(E::PLACEHOLDER),
304            overflowed: false,
305        }
306    }
307
308    /// Records an event.
309    ///
310    /// When full the event is dropped and [`EventList::overflowed`] is set,
311    /// rather than failing the operation: the work is done and the caller needs
312    /// the result plus the fact that the report is incomplete.
313    pub fn push(&mut self, event: E) {
314        if self.events.push(event).is_err() {
315            self.overflowed = true;
316        }
317    }
318
319    /// Whether an event was dropped for lack of capacity.
320    ///
321    /// Requires action: the events present are the earliest; at least one later
322    /// one is missing.
323    #[must_use]
324    pub const fn overflowed(&self) -> bool {
325        self.overflowed
326    }
327
328    /// Events, in order.
329    #[must_use]
330    pub fn as_slice(&self) -> &[E] {
331        self.events.as_slice()
332    }
333}
334
335impl<E: Event> Default for EventList<E, MAX_EVENTS> {
336    fn default() -> Self {
337        Self::new()
338    }
339}
340
341impl<E: Event, const N: usize> Deref for EventList<E, N> {
342    type Target = [E];
343
344    fn deref(&self) -> &[E] {
345        self.as_slice()
346    }
347}
348
349impl<'a, E: Event, const N: usize> IntoIterator for &'a EventList<E, N> {
350    type Item = &'a E;
351    type IntoIter = core::slice::Iter<'a, E>;
352
353    fn into_iter(self) -> Self::IntoIter {
354        self.as_slice().iter()
355    }
356}
357
358impl<E: Event, const N: usize> fmt::Debug for EventList<E, N> {
359    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360        f.debug_struct("EventList")
361            .field("events", &self.events)
362            .field("overflowed", &self.overflowed)
363            .finish()
364    }
365}
366
367impl<E: Event, const N: usize, const M: usize> PartialEq<EventList<E, M>> for EventList<E, N> {
368    /// Equal when they hold the same events and the same overflow flag,
369    /// regardless of capacity.
370    fn eq(&self, other: &EventList<E, M>) -> bool {
371        self.overflowed == other.overflowed && self.as_slice() == other.as_slice()
372    }
373}
374
375#[cfg(test)]
376#[allow(clippy::cast_possible_wrap)]
377mod tests {
378    use super::*;
379
380    /// Capacity as a timestamp, for numbering test events.
381    const CAPACITY: i64 = MAX_EVENTS as i64;
382
383    fn acquired(seconds: i64) -> NavigationEvent {
384        NavigationEvent::FixAcquired {
385            source: PositionSource::Gnss,
386            at: Instant::from_unix_seconds(seconds),
387        }
388    }
389
390    #[test]
391    fn events_come_back_in_order() {
392        let mut list = EventList::new();
393        assert!(list.is_empty());
394        list.push(acquired(1));
395        list.push(NavigationEvent::ObservationRejected {
396            reason: RejectionReason::Stale,
397            at: Instant::from_unix_seconds(2),
398        });
399        assert_eq!(list.len(), 2);
400        assert_eq!(list.first(), Some(&acquired(1)));
401        assert!(matches!(
402            list.last(),
403            Some(NavigationEvent::ObservationRejected {
404                reason: RejectionReason::Stale,
405                ..
406            })
407        ));
408        assert_eq!((&list).into_iter().count(), 2);
409        assert!(!list.overflowed());
410    }
411
412    #[test]
413    fn a_full_list_keeps_the_earliest_and_says_it_lost_the_rest() {
414        let mut list = EventList::new();
415        for second in 0..CAPACITY {
416            list.push(acquired(second));
417        }
418        assert!(!list.overflowed());
419        list.push(acquired(99));
420        assert!(list.overflowed());
421        assert_eq!(list.len(), MAX_EVENTS);
422        assert_eq!(list.last(), Some(&acquired(CAPACITY - 1)));
423    }
424
425    #[test]
426    fn lists_compare_by_events_and_by_loss() {
427        let mut first = EventList::new();
428        let mut second = EventList::default();
429        first.push(acquired(1));
430        second.push(acquired(1));
431        assert_eq!(first, second);
432        for second_number in 0..=CAPACITY {
433            second.push(acquired(second_number));
434        }
435        assert_ne!(first, second);
436    }
437}