Skip to main content

navcore_signalk/
lib.rs

1//! Signal K as an adapter.
2//!
3//! A Signal K server aggregates NMEA 0183, NMEA 2000 and SeaTalk
4//! instrument data, so this crate never parses a sentence directly. It
5//! translates Signal K deltas into this workspace's own types. Ranking
6//! multiple data sources and assembling a complete fix belongs to
7//! [`fix`], which this crate depends on; this is the only crate in the
8//! workspace with a dependency on Signal K itself.
9//!
10//! # The two things it does
11//!
12//! [`delta`] turns what arrives on the wire into this workspace's units.
13//! Signal K is SI -- radians and metres per second -- and everything above
14//! `nav-math` is degrees and knots. One conversion, in one place, with the
15//! tests that keep it there.
16//!
17//! [`apply_measurement`] feeds one reading to a [`fix::FixBuilder`] under
18//! construction. It is a thin thing on purpose: `fix` does not know Signal K
19//! exists, so this is the one place a `Measurement` and a builder's setters
20//! meet, and it is the only part of assembling a fix that would need
21//! rewriting for a client reading raw NMEA instead.
22//!
23//! # What it deliberately does not do
24//!
25//! No sockets, no mDNS, no HTTP, no clock. Everything here is a pure
26//! function of what it was handed, which is what lets a recorded stream of
27//! deltas replay ashore and produce the same track, the same handovers
28//! between sources and the same moments of going stale. The shell that
29//! finds the server, holds the token and keeps the WebSocket open is thin
30//! by design, and sits above this.
31//!
32//! # What is not modelled here on purpose
33//!
34//! Signal K's own idea of a route, and course *calculations*. The route
35//! resource cannot express a leg, and the Course API states that it
36//! performs no course calculations of its own -- both are things this
37//! project already does better for itself in `routes` and `nav-math`;
38//! using them as vocabulary is worth it, using them as an engine is not.
39//! [`delta::Measurement::CourseNextPoint`] is exactly that vocabulary use,
40//! not an exception to it: it reads which point a route-following device
41//! (this one or another) currently considers the destination, the same
42//! plain fact `AnchorPosition` already is for the anchor watch -- no
43//! bearing, distance or ETA is asked of Signal K to get it, and computing
44//! any of those once a next point is known is `nav-math`'s job, same as
45//! it always was.
46
47#![forbid(unsafe_code)]
48
49pub mod delta;
50pub mod notification;
51pub mod polar;
52pub mod units;
53pub mod wind;
54
55pub use delta::{
56    AisClass, AisTargetStatus, Context, Delta, Depth, Measurement, NotificationSeverity, Offsets, Reading,
57    Sounding, parse,
58};
59pub use polar::{PolarResource, PolarResourceError};
60pub use units::Preferences;
61pub use wind::{DEFAULT_WIND_GOOD_FOR, Wind, WindTracker};
62
63/// Feeds one measurement to a fix under construction.
64///
65/// Returns whether it was a **position** -- the one reading that makes a
66/// fix fresh rather than merely updated, the same distinction
67/// [`fix::FixBuilder::set_position`] documents. Everything else this does
68/// not recognise as own-ship state (depth, the sounder's own geometry, the
69/// vessel's draft) is read elsewhere; this only ever touches the four
70/// fields a moving fix is made of.
71pub fn apply_measurement(builder: &mut fix::FixBuilder, measurement: Measurement) -> bool {
72    match measurement {
73        Measurement::Position(position) => {
74            builder.set_position(position);
75            true
76        }
77        Measurement::CourseOverGround(deg) => {
78            builder.set_course_over_ground(deg);
79            false
80        }
81        Measurement::SpeedOverGround(kn) => {
82            builder.set_speed_over_ground(kn);
83            false
84        }
85        Measurement::Heading(deg) => {
86            builder.set_heading(deg);
87            false
88        }
89        Measurement::VesselName(_)
90        | Measurement::AisTargetStatus(_)
91        | Measurement::ClosestApproachAlarm(_)
92        | Measurement::AisClass(_)
93        | Measurement::AisShipType(_)
94        | Measurement::Callsign(_)
95        | Measurement::ClosestApproach { .. }
96        | Measurement::Depth(_)
97        | Measurement::TransducerToKeel(_)
98        | Measurement::SurfaceToTransducer(_)
99        | Measurement::MaximumDraft(_)
100        | Measurement::DesignLength(_)
101        | Measurement::DesignBeam(_)
102        | Measurement::WindAngleApparent(_)
103        | Measurement::WindSpeedApparent(_)
104        | Measurement::WindDirectionTrue(_)
105        | Measurement::WindSpeedTrue(_)
106        | Measurement::AnchorPosition(_)
107        | Measurement::AnchorMaxRadius(_)
108        | Measurement::CourseNextPoint(_)
109        | Measurement::CoursePreviousPoint(_)
110        | Measurement::CourseActiveRouteHref(_)
111        | Measurement::ResourceChanged { .. } => false,
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118    use nav_math::Position;
119
120    #[test]
121    fn a_position_is_reported_as_making_the_fix_fresh() {
122        let mut builder = fix::FixBuilder::new();
123        assert!(apply_measurement(
124            &mut builder,
125            Measurement::Position(Position::new(45.5, 13.7))
126        ));
127    }
128
129    #[test]
130    fn course_speed_and_heading_are_not_reported_as_fresh() {
131        let mut builder = fix::FixBuilder::new();
132        assert!(!apply_measurement(
133            &mut builder,
134            Measurement::CourseOverGround(90.0)
135        ));
136        assert!(!apply_measurement(
137            &mut builder,
138            Measurement::SpeedOverGround(5.0)
139        ));
140        assert!(!apply_measurement(&mut builder, Measurement::Heading(90.0)));
141    }
142
143    #[test]
144    fn readings_outside_a_moving_fix_are_read_by_nobody_here() {
145        let mut builder = fix::FixBuilder::new();
146        assert!(!apply_measurement(
147            &mut builder,
148            Measurement::TransducerToKeel(0.8)
149        ));
150        assert!(!apply_measurement(&mut builder, Measurement::MaximumDraft(2.0)));
151        // And they touched nothing: course/speed are still unset, so the
152        // builder cannot yet complete.
153        assert!(builder.complete().is_none());
154    }
155}