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}