From 740695e64e5cd3d0b9e2b59d7e557c6e91c8ee54 Mon Sep 17 00:00:00 2001 From: Gautham Date: Wed, 8 Jan 2025 18:17:14 -0500 Subject: [PATCH 01/14] [CONSULT-469] add initial code for field parsing --- Cargo.lock | 8 + Cargo.toml | 1 + micro-rdk-nmea/Cargo.toml | 15 + micro-rdk-nmea/README.md | 5 + micro-rdk-nmea/src/lib.rs | 1 + micro-rdk-nmea/src/parse_helpers/enums.rs | 328 ++++++++++++++++++ micro-rdk-nmea/src/parse_helpers/errors.rs | 17 + micro-rdk-nmea/src/parse_helpers/mod.rs | 3 + micro-rdk-nmea/src/parse_helpers/parsers.rs | 356 ++++++++++++++++++++ micro-rdk/src/common/app_client.rs | 1 - 10 files changed, 734 insertions(+), 1 deletion(-) create mode 100644 micro-rdk-nmea/Cargo.toml create mode 100644 micro-rdk-nmea/README.md create mode 100644 micro-rdk-nmea/src/lib.rs create mode 100644 micro-rdk-nmea/src/parse_helpers/enums.rs create mode 100644 micro-rdk-nmea/src/parse_helpers/errors.rs create mode 100644 micro-rdk-nmea/src/parse_helpers/mod.rs create mode 100644 micro-rdk-nmea/src/parse_helpers/parsers.rs diff --git a/Cargo.lock b/Cargo.lock index 1d3269023..5dd0cc6b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2827,6 +2827,14 @@ dependencies = [ "micro-rdk", ] +[[package]] +name = "micro-rdk-nmea" +version = "0.4.0-rc2" +dependencies = [ + "micro-rdk", + "thiserror 2.0.4", +] + [[package]] name = "micro-rdk-server" version = "0.4.0-rc2" diff --git a/Cargo.toml b/Cargo.toml index 57d667007..43c2a5500 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "micro-rdk-ffi", "examples/modular-drivers", "etc/ota-dev-server", + "micro-rdk-nmea", ] default-members = [ diff --git a/micro-rdk-nmea/Cargo.toml b/micro-rdk-nmea/Cargo.toml new file mode 100644 index 000000000..531fd28fa --- /dev/null +++ b/micro-rdk-nmea/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "micro-rdk-nmea" +authors.workspace = true +description.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true +rust-version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +micro-rdk = { path = "../micro-rdk", features = ["native"] } +thiserror = { workspace = true } diff --git a/micro-rdk-nmea/README.md b/micro-rdk-nmea/README.md new file mode 100644 index 000000000..5ed5d3d33 --- /dev/null +++ b/micro-rdk-nmea/README.md @@ -0,0 +1,5 @@ +# The micro-RDK NMEA 2000 Message Parser (WIP) + +This is a library that will eventually supply logic to parse NMEA 2000 messages from byte data into +structs in Rust that can serialize into instances of `GenericReadingsResult` for use in the implementation of +micro-RDK sensors. diff --git a/micro-rdk-nmea/src/lib.rs b/micro-rdk-nmea/src/lib.rs new file mode 100644 index 000000000..4f0b0806c --- /dev/null +++ b/micro-rdk-nmea/src/lib.rs @@ -0,0 +1 @@ +pub mod parse_helpers; diff --git a/micro-rdk-nmea/src/parse_helpers/enums.rs b/micro-rdk-nmea/src/parse_helpers/enums.rs new file mode 100644 index 000000000..1ca9de76c --- /dev/null +++ b/micro-rdk-nmea/src/parse_helpers/enums.rs @@ -0,0 +1,328 @@ +pub trait Lookup: Sized { + fn from_value(val: u32) -> Self; + fn to_string(&self) -> String; +} + +/// For generating a lookup data type found in an NMEA message. The first argument is the name of the +/// enum type that will be generated. Each successive argument is a tuple with +/// (raw number value, name of enum instance, string representation) +macro_rules! lookup { + ( $name:ident, $(($value:expr, $var:ident, $label:expr)),*, $default:ident) => { + #[derive(Copy, Clone, Debug)] + pub enum $name { + $($var),*, + $default + } + + impl Lookup for $name { + fn from_value(val: u32) -> Self { + match val { + $($value => Self::$var),*, + _ => Self::$default + } + } + + fn to_string(&self) -> String { + match self { + $(Self::$var => $label),*, + Self::$default => "could not parse" + }.to_string() + } + } + }; + +} + +// Examples below taken from https://canboat.github.io/canboat/canboat.html + +lookup!( + WaterReference, + (0, PaddleWheel, "Paddle Wheel"), + (1, PitotTube, "Pitot Tube"), + (2, Doppler, "Doppler"), + (3, Correlation, "Correlation (ultra sound)"), + (4, Electromagnetic, "Electro Magnetic"), + CouldNotParse +); + +lookup!( + TemperatureSource, + (0, SeaTemperature, "Sea Temperature"), + (1, OutsideTemperature, "Outside Temperature"), + (2, InsideTemperature, "Inside Temperature"), + (3, EngineRoomTemperature, "Engine Room Temperature"), + (4, MainCabinTemperature, "Main Cabin Temeprature"), + (5, LiveWellTemperature, "Live Well Temperature"), + (6, BaitWellTemperature, "Bait Well Temperature"), + (7, RefrigerationTemperature, "Refrigeration Temperature"), + (8, HeatingSystemTemperature, "Heating System Temperature"), + (9, DewPointTemperature, "Dew Point Temperature"), + ( + 10, + ApparentWindChillTemerature, + "Apparent Wind Chill Temperature" + ), + ( + 11, + TheoreticalWindChillTemperature, + "Theoretical Wind Chill Temperature" + ), + (12, HeatIndexTemperature, "Heat Index Temperature"), + (13, FreezerTemperature, "Freezer Temperature"), + (14, ExhaustGasTemperature, "Exhaust Gas Temperature"), + (15, ShaftSealTemeprature, "Shaft Seal Temperature"), + CouldNotParse +); + +lookup!( + SystemTimeSource, + (0, Gps, "GPS"), + (1, Glonass, "GLONASS"), + (2, RadioStation, "Radio Station"), + (3, LocalCesiumClock, "Local Cesium clock"), + (4, LocalRubidiumClock, "Local Rubidium clock"), + (5, LocalCrystalClock, "Local Crystal clock"), + CouldNotParse +); + +lookup!( + MagneticVariationSource, + (0, Manual, "Manual"), + (1, AutomaticChart, "Automatic Chart"), + (2, AutomaticTable, "Automatic Table"), + (3, AutomaticCalculation, "Automatic Calculation"), + (4, Wmm2000, "WMM 2000"), + (5, Wmm2005, "WMM 2005"), + (6, Wmm2010, "WMM 2010"), + (7, Wmm2015, "WMM 2015"), + (8, Wmm2020, "WMM 2020"), + CouldNotParse +); + +lookup!( + RepeatIndicator, + (0, Initial, "Initial"), + (1, FirstRetransmission, "First retransmission"), + (2, SecondRetransmission, "Second retransmission"), + (3, ThirdRetransmission, "Third retransmission"), + (4, FinalRetransmission, "Final retransmission"), + CouldNotParse +); + +lookup!( + AisMessageId, + ( + 1, + ScheduledClassAPositionReport, + "Scheduled Class A position report" + ), + ( + 2, + AssignedScheduledClassAPositionReport, + "Assigned scheduled Class A position report" + ), + ( + 3, + InterrogatedClassAPositionReport, + "Interrogated Class A position report" + ), + (4, BaseStationReport, "Base station report"), + (5, StaticVoyageRelatedData, "Static and voyage related data"), + (6, BinaryAddressedMessage, "Binary addressed message"), + (7, BinaryAcknowledgement, "Binary acknowledgement"), + (8, BinaryBroadcastMessage, "Binary broadcast message"), + ( + 9, + StandardSarAircraftPositionReport, + "Standard SAR aircraft position report" + ), + (10, UtcDateInquiry, "UTC/date inquiry"), + (11, UtcDateResponse, "UTC/date response"), + ( + 12, + SafetyRelatedAddressedMessage, + "Safety related addressed message" + ), + ( + 13, + SafetyRelatedAcknowlegdement, + "Safety related acknowledgement" + ), + ( + 14, + SafetyRelatedBroadcastMessage, + "Satety related broadcast message" + ), + (15, Interrogation, "Interrogation"), + (16, AssignmentModeCommand, "Assignment mode command"), + ( + 17, + DgnssBroadcastBinaryMessage, + "DGNSS broadcast binary message" + ), + ( + 18, + StandardClassBPositionReport, + "Standard Class B position report" + ), + ( + 19, + ExtendedClassBPositionReport, + "Extended Class B position report" + ), + ( + 20, + DataLinkManagementMessage, + "Data link management message" + ), + (21, AtonReport, "ATON report"), + (22, ChannelManagement, "Channel management"), + (23, GroupAssignmentCommand, "Group assignment command"), + (24, StaticDataReport, "Static data report"), + (25, SingleSlotBinaryMessage, "Single slot binary message"), + ( + 26, + MultipleSlotBinaryMessage, + "Multiple slot binary message" + ), + ( + 27, + PositionReportForLongRangeApplications, + "Position report for long range applications" + ), + CouldNotParse +); + +lookup!( + PositionAccuracy, + (0, Low, "Low"), + (1, High, "High"), + CouldNotParse +); + +lookup!( + RaimFlag, + (0, NotInUse, "not in use"), + (1, InUse, "in use"), + CouldNotParse +); + +lookup!( + TimeStamp, + (60, NotAvailable, "Not available"), + (61, ManualInputMode, "Manual input mode"), + (62, DeadReckoningMode, "Dead reckoning mode"), + ( + 63, + PositioningSystemIsInoperative, + "Positioning system is inoperative" + ), + CouldNotParse +); + +lookup!( + AisTransceiver, + (0, ChannelAVdlReception, "Channel A VDL reception"), + (1, ChannelBVdlReception, "Channel B VDL reception"), + (2, ChannelAVdlTransmission, "Channel A VDL transmission"), + (3, ChannelBVdlTransmission, "Channel B VDL transmission"), + ( + 4, + OwnInformationNotBroadcast, + "Own information not broadcast" + ), + (5, Reserved, "Reserved"), + CouldNotParse +); + +lookup!( + NavStatus, + (0, UnderWayUsingEngine, "Under way using engine"), + (1, AtAnchor, "At anchor"), + (2, NotUnderCommand, "Not under command"), + (3, RestrictedManeuverability, "Restricted maneuverability"), + (4, ConstrainedByHerDraught, "Constrained by her draught"), + (5, Moored, "Moored"), + (6, Aground, "Aground"), + (7, EngagedInFishing, "Engaged in Fishing"), + (8, UnderWaySailing, "Under way sailing"), + ( + 9, + HazardousMaterialHighSpeed, + "Hazardous material - High Speed" + ), + ( + 10, + HazardousMaterialWingInGround, + "Hazardous material - Wing in Ground" + ), + ( + 11, + PowerDrivenVesslTowingAstern, + "Power-driven vessl towing astern" + ), + ( + 12, + PowerDrivenVesslPushingAhead, + "Power-driven vessl pushing ahead or towing alongside" + ), + (14, AisSart, "AIS-SART"), + CouldNotParse +); + +lookup!( + AisSpecialManeuver, + (0, NotAvailable, "Not available"), + ( + 1, + NotEngagedInSpecialManeuver, + "Not engaged in special maneuver" + ), + (2, EngagedInSpecialManeuver, "Engaged in special maneuver"), + (3, Reserved, "Reserved"), + CouldNotParse +); + +lookup!( + DirectionReference, + (0, True, "True"), + (1, Magnetic, "Magnetic"), + (2, Error, "Error"), + CouldNotParse +); + +lookup!( + Gns, + (0, Gps, "GPS"), + (1, Glonass, "GLONASS"), + (2, GpsGlonass, "GPS+GLONASS"), + (3, GpsSbasWaas, "GPS+SBAS/WAAS"), + (4, GpsSbasWaasGlonass, "GPS+SBAS/WAAS+GLONASS"), + (5, Chayka, "Chayka"), + (6, Integrated, "integrated"), + (7, Surveyed, "surveyed"), + (8, Galileo, "Galileo"), + CouldNotParse +); + +lookup!( + GnsMethod, + (0, NoGnss, "no GNSS"), + (1, GnssFix, "GNSS fix"), + (2, DgnssFix, "DGNSS fix"), + (3, PreciseGnss, "Precise GNSS"), + (4, RtkFixedInteger, "RTK Fixed Integer"), + (5, RtkFloat, "RTK float"), + (6, EstimatedDrMode, "Estimated (DR) mode"), + (7, ManualInput, "Manual Input"), + (8, SimulateMode, "Simulate mode"), + CouldNotParse +); + +lookup!( + GnsIntegrity, + (0, NoIntegrityChecking, "No integrity checking"), + (1, Safe, "Safe"), + (2, Caution, "Caution"), + CouldNotParse +); diff --git a/micro-rdk-nmea/src/parse_helpers/errors.rs b/micro-rdk-nmea/src/parse_helpers/errors.rs new file mode 100644 index 000000000..e460397b8 --- /dev/null +++ b/micro-rdk-nmea/src/parse_helpers/errors.rs @@ -0,0 +1,17 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum NumberFieldError { + #[error("field bit size {0} too large for max size {0}")] + ImproperBitSize(usize, usize), +} + +#[derive(Debug, Error)] +pub enum NmeaParseError { + #[error(transparent)] + NumberFieldError(#[from] NumberFieldError), + #[error(transparent)] + TryFromSliceError(#[from] std::array::TryFromSliceError), + #[error("found unsupported PGN {0}")] + UnsupportedPgn(u32), +} diff --git a/micro-rdk-nmea/src/parse_helpers/mod.rs b/micro-rdk-nmea/src/parse_helpers/mod.rs new file mode 100644 index 000000000..74e2db44a --- /dev/null +++ b/micro-rdk-nmea/src/parse_helpers/mod.rs @@ -0,0 +1,3 @@ +pub mod enums; +pub mod errors; +pub mod parsers; diff --git a/micro-rdk-nmea/src/parse_helpers/parsers.rs b/micro-rdk-nmea/src/parse_helpers/parsers.rs new file mode 100644 index 000000000..5e94693db --- /dev/null +++ b/micro-rdk-nmea/src/parse_helpers/parsers.rs @@ -0,0 +1,356 @@ +use std::marker::PhantomData; + +use micro_rdk::common::sensor::GenericReadingsResult; + +use super::{ + enums::Lookup, + errors::{NmeaParseError, NumberFieldError}, +}; + +/// Trait for reading a data type (`FieldType`) from a byte slice. +pub trait FieldReader { + type FieldType; + + /// Takes a byte slice (`data`) and starting index. Returns a tuple consisting of + /// the next index to read from the byte sequence and the parsed value. + fn read_from_data( + &self, + data: &[u8], + start_idx: usize, + ) -> Result<(usize, Self::FieldType), NmeaParseError>; +} + +/// A field reader for parsing a basic number type. A reader with bit_size n will read its value +/// as the first n bits of `size_of::()` bytes with the remaining bits as zeroes (the resulting bytes +/// will be parsed as Little-Endian). See invocation of the number_field macro below for the currently +/// supported number types. +pub struct NumberField { + bit_size: usize, + _marker: PhantomData, +} + +impl NumberField { + pub fn new(bit_size: usize) -> Result { + let max_bit_size = std::mem::size_of::() * 8; + if bit_size > max_bit_size { + Err(NumberFieldError::ImproperBitSize(bit_size, max_bit_size)) + } else { + Ok(Self { + bit_size, + _marker: Default::default(), + }) + } + } +} + +impl Default for NumberField +where + T: Sized, +{ + fn default() -> Self { + Self { + bit_size: size_of::() * 8, + _marker: Default::default(), + } + } +} + +macro_rules! number_field { + ($($t:ty),*) => { + $( + impl FieldReader for NumberField<$t> { + type FieldType = $t; + + fn read_from_data(&self, data: &[u8], start_idx: usize) -> Result<(usize, Self::FieldType), NmeaParseError> { + let type_size = std::mem::size_of::<$t>(); + match self.bit_size { + x if x == (type_size * 8) => { + let value: &[u8] = &data[start_idx..(start_idx + type_size)]; + Ok((start_idx + type_size, <$t>::from_le_bytes(value.try_into()?))) + } + x => { + let shift = (type_size * 8) - x; + let end_idx = start_idx + (x / 8); + let value: &[u8] = &data[start_idx..(end_idx + 1)]; + let value = <$t>::from_le_bytes(value.try_into()?); + Ok((end_idx, value >> shift << shift)) + } + } + } + } + )* + }; +} + +number_field!(u8, i8, u16, i16, u32, i32, u64, i64); + +/// A field reader for parsing data into a field type that implements the `Lookup` trait. +/// The `bit_size` property is used to parse a raw number value first with a methodology similar to the one +/// defined in the documentation for NumberField, after which the raw value is passed to `Lookup::from_value` +pub struct LookupField { + bit_size: usize, + _marker: PhantomData, +} + +impl LookupField { + pub fn new(bit_size: usize) -> Self { + Self { + bit_size, + _marker: Default::default(), + } + } +} + +impl FieldReader for LookupField +where + T: Lookup, +{ + type FieldType = T; + + fn read_from_data( + &self, + data: &[u8], + start_idx: usize, + ) -> Result<(usize, Self::FieldType), NmeaParseError> { + let end_idx = start_idx + (self.bit_size / 8); + let data_slice: &[u8] = &data[start_idx..(end_idx + 1)]; + let enum_value = match self.bit_size { + 8 => data[start_idx] as u32, + 16 => u16::from_le_bytes(data_slice.try_into()?) as u32, + 32 => u32::from_le_bytes(data_slice.try_into()?), + x if x < 8 => { + let shift = 8 - x; + (data[start_idx] >> shift) as u32 + } + x if x < 16 => { + let shift = 16 - x; + let raw_val = u16::from_le_bytes(data_slice.try_into()?); + (raw_val >> shift) as u32 + } + x if x < 32 => { + let shift = 32 - x; + let raw_val = u32::from_le_bytes(data_slice.try_into()?); + raw_val >> shift + } + _ => unreachable!("lookup field raw value cannot be more than 32 bits"), + }; + Ok((end_idx, T::from_value(enum_value))) + } +} + +/// A field reader for parsing an array [T; N] from a byte slice of data, where +/// T is a number type supported by `NumberField`. +pub struct ArrayField(PhantomData>); + +impl ArrayField { + pub fn new() -> Self { + Self(Default::default()) + } +} + +impl FieldReader for ArrayField +where + NumberField: FieldReader, + as FieldReader>::FieldType: Default + Copy, +{ + type FieldType = [ as FieldReader>::FieldType; N]; + + fn read_from_data( + &self, + data: &[u8], + start_idx: usize, + ) -> Result<(usize, Self::FieldType), NmeaParseError> { + let mut res: [ as FieldReader>::FieldType; N] = [Default::default(); N]; + let mut end_idx = start_idx; + let field_reader: NumberField = Default::default(); + for i in 0..N { + let (bytes_read, next_elem) = field_reader.read_from_data(data, end_idx)?; + res[i] = next_elem; + end_idx = bytes_read + } + Ok((end_idx, res)) + } +} + +/// Some NMEA 2000 messages have a set of fields that may be repeated a number of times (usually specified by the value +/// of another field). This trait is for structs that implement this set of fields, most likely using the `FieldsetDerive` macro. +pub trait FieldSet: Sized { + fn from_bytes(data: &[u8], current_index: usize) -> Result<(usize, Self), NmeaParseError>; + fn to_readings(&self) -> Result; +} + +/// A field reader that parses a vector of structs representing a field set (see `FieldSet`) from a byte slice. +/// The expected length of the vector must be provided on initialization. +pub struct FieldSetList { + length: usize, + _marker: PhantomData, +} + +impl FieldSetList { + pub fn new(length: usize) -> Self { + Self { + length, + _marker: Default::default(), + } + } +} + +impl FieldReader for FieldSetList +where + T: FieldSet, +{ + type FieldType = Vec; + + fn read_from_data( + &self, + data: &[u8], + start_idx: usize, + ) -> Result<(usize, Self::FieldType), NmeaParseError> { + let mut idx = start_idx; + let mut res = Vec::new(); + for _ in 0..self.length { + let (new_start, new_elem) = T::from_bytes(data, idx)?; + idx = new_start; + res.push(new_elem); + } + Ok((idx, res)) + } +} + +#[cfg(test)] +mod tests { + use crate::parse_helpers::{ + enums::MagneticVariationSource, errors::NmeaParseError, parsers::FieldReader, + }; + + use super::{ArrayField, FieldSet, FieldSetList, LookupField, NumberField}; + + #[test] + fn number_field_test() { + let single_byte_reader = NumberField::::new(4); + assert!(single_byte_reader.is_ok()); + let single_byte_reader = single_byte_reader.unwrap(); + + // 125 = 01111101, first four bits as byte => 01110000 = 112 + let data_vec: Vec = vec![100, 6, 125, 73]; + let data: &[u8] = &data_vec[..]; + let res = single_byte_reader.read_from_data(data, 2); + assert!(res.is_ok()); + let (next_index, res) = res.unwrap(); + assert_eq!(next_index, 2); + assert_eq!(res, 112); + + let reader = NumberField::::new(12); + assert!(reader.is_ok()); + let reader = reader.unwrap(); + let data_vec: Vec = vec![100, 6, 125, 179, 152, 113]; + let data: &[u8] = &data_vec[..]; + + // [179, 152] is 39091 in u16, reading the first 12 bits and ignoring the last 4 + // should yield 39088 + let res = reader.read_from_data(data, 3); + assert!(res.is_ok()); + let (next_index, res) = res.unwrap(); + assert_eq!(next_index, 4); + assert_eq!(res, 39088); + } + + #[test] + fn lookup_field_test() { + let reader = LookupField::::new(4); + + let data_vec: Vec = vec![100, 6, 111, 77, 152, 113]; + let data: &[u8] = &data_vec[..]; + + let res = reader.read_from_data(data, 3); + let (next_index, res) = res.unwrap(); + assert_eq!(next_index, 3); + assert!(matches!(res, MagneticVariationSource::Wmm2000)); + } + + #[test] + fn array_field_test() { + let data_vec: Vec = vec![100, 6, 111, 77, 152, 113, 42, 42]; + let data: &[u8] = &data_vec[..]; + + let reader = ArrayField::::new(); + + let res = reader.read_from_data(data, 2); + assert!(res.is_ok()); + let (next_index, res) = res.unwrap(); + + assert_eq!(next_index, 5); + assert_eq!(res, [111, 77, 152]); + + let reader = ArrayField::::new(); + + let res = reader.read_from_data(data, 1); + assert!(res.is_ok()); + let (next_index, res) = res.unwrap(); + + // [6, 11] = 28422, [77, 152] = 38989 + assert_eq!(next_index, 5); + assert_eq!(res, [28422, 38989]); + + let reader = ArrayField::::new(); + + let res = reader.read_from_data(data, 0); + assert!(res.is_ok()); + let (next_index, res) = res.unwrap(); + + // [100, 6, 111, 77] = 1299121764, [152, 113, 42, 42] = 707424664 + assert_eq!(next_index, 8); + assert_eq!(res, [1299121764, 707424664]); + } + + #[derive(Debug, PartialEq, Eq)] + struct TestFieldSet { + a: u16, + b: u8, + c: u16, + } + + impl FieldSet for TestFieldSet { + fn from_bytes(data: &[u8], current_index: usize) -> Result<(usize, Self), NmeaParseError> { + let a_data: &[u8] = &data[current_index..(current_index + 2)]; + let a = u16::from_le_bytes(a_data.try_into()?); + let b = data[current_index + 2]; + let c_data: &[u8] = &data[(current_index + 3)..(current_index + 5)]; + let c = u16::from_le_bytes(c_data.try_into()?); + + Ok((current_index + 5, TestFieldSet { a, b, c })) + } + + fn to_readings( + &self, + ) -> Result { + Err(NmeaParseError::UnsupportedPgn(0)) + } + } + + #[test] + fn fieldset_field_test() { + let data_vec: Vec = vec![100, 6, 111, 77, 152, 113, 42, 42, 1, 2, 3]; + let data: &[u8] = &data_vec[..]; + + let reader = FieldSetList::::new(2); + let res = reader.read_from_data(data, 1); + assert!(res.is_ok()); + let (next_index, res) = res.unwrap(); + + let expected_at_0 = TestFieldSet { + a: 28422, // [6, 11] + b: 77, + c: 29080, // [152, 113] + }; + let expected_at_1 = TestFieldSet { + a: 10794, // [42, 42] + b: 1, + c: 770, // [2, 3] + }; + + assert_eq!(next_index, 11); + assert_eq!(res[0], expected_at_0); + assert_eq!(res[1], expected_at_1); + } +} diff --git a/micro-rdk/src/common/app_client.rs b/micro-rdk/src/common/app_client.rs index 5fd92cce4..09900cfcf 100755 --- a/micro-rdk/src/common/app_client.rs +++ b/micro-rdk/src/common/app_client.rs @@ -63,7 +63,6 @@ pub enum AppClientError { AppConfigHeaderDateMissingError, #[error(transparent)] AppGrpcClientError(#[from] GrpcClientError), - #[cfg(feature = "data")] #[error("request timeout")] AppClientRequestTimeout, #[error("empty body")] From 05c5886248329ec4d0f7f7c97bdd9c964247ef07 Mon Sep 17 00:00:00 2001 From: Gautham Date: Fri, 10 Jan 2025 15:01:41 -0500 Subject: [PATCH 02/14] bug fix --- micro-rdk-nmea/src/parse_helpers/parsers.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/micro-rdk-nmea/src/parse_helpers/parsers.rs b/micro-rdk-nmea/src/parse_helpers/parsers.rs index 5e94693db..7de3dcf87 100644 --- a/micro-rdk-nmea/src/parse_helpers/parsers.rs +++ b/micro-rdk-nmea/src/parse_helpers/parsers.rs @@ -69,10 +69,16 @@ macro_rules! number_field { Ok((start_idx + type_size, <$t>::from_le_bytes(value.try_into()?))) } x => { - let shift = (type_size * 8) - x; let end_idx = start_idx + (x / 8); + let value: &[u8] = &data[start_idx..(end_idx + 1)]; - let value = <$t>::from_le_bytes(value.try_into()?); + let padding: Vec = vec![0; (type_size - value.len())]; + let padding_slice: &[u8] = &padding[..]; + let join = [value, padding_slice].concat(); + let full_value = &join[..]; + + let shift = x % 8; + let value = <$t>::from_le_bytes(full_value.try_into()?); Ok((end_idx, value >> shift << shift)) } } From f8856c22fbb219f9ef2616396bed99f907242153 Mon Sep 17 00:00:00 2001 From: Gautham Date: Fri, 10 Jan 2025 15:38:21 -0500 Subject: [PATCH 03/14] address some review comments --- Cargo.toml | 2 +- micro-rdk-nmea/Cargo.toml | 2 +- micro-rdk-nmea/README.md | 2 +- micro-rdk-nmea/src/parse_helpers/enums.rs | 15 ++++++++------- micro-rdk-nmea/src/parse_helpers/parsers.rs | 12 ++++++------ 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 43c2a5500..a8c072100 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,11 @@ members = [ "micro-rdk", "micro-rdk-installer", "micro-rdk-macros", + "micro-rdk-nmea", "micro-rdk-server", "micro-rdk-ffi", "examples/modular-drivers", "etc/ota-dev-server", - "micro-rdk-nmea", ] default-members = [ diff --git a/micro-rdk-nmea/Cargo.toml b/micro-rdk-nmea/Cargo.toml index 531fd28fa..25b0aee22 100644 --- a/micro-rdk-nmea/Cargo.toml +++ b/micro-rdk-nmea/Cargo.toml @@ -11,5 +11,5 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -micro-rdk = { path = "../micro-rdk", features = ["native"] } +micro-rdk = { workspace = true, features = ["native"] } thiserror = { workspace = true } diff --git a/micro-rdk-nmea/README.md b/micro-rdk-nmea/README.md index 5ed5d3d33..233943213 100644 --- a/micro-rdk-nmea/README.md +++ b/micro-rdk-nmea/README.md @@ -1,5 +1,5 @@ # The micro-RDK NMEA 2000 Message Parser (WIP) -This is a library that will eventually supply logic to parse NMEA 2000 messages from byte data into +This library supplies logic to parse NMEA 2000 messages from byte data into structs in Rust that can serialize into instances of `GenericReadingsResult` for use in the implementation of micro-RDK sensors. diff --git a/micro-rdk-nmea/src/parse_helpers/enums.rs b/micro-rdk-nmea/src/parse_helpers/enums.rs index 1ca9de76c..d709dd4fc 100644 --- a/micro-rdk-nmea/src/parse_helpers/enums.rs +++ b/micro-rdk-nmea/src/parse_helpers/enums.rs @@ -1,7 +1,4 @@ -pub trait Lookup: Sized { - fn from_value(val: u32) -> Self; - fn to_string(&self) -> String; -} +pub trait Lookup: Sized + From + ToString {} /// For generating a lookup data type found in an NMEA message. The first argument is the name of the /// enum type that will be generated. Each successive argument is a tuple with @@ -14,14 +11,16 @@ macro_rules! lookup { $default } - impl Lookup for $name { - fn from_value(val: u32) -> Self { - match val { + impl From for $name { + fn from(value: u32) -> Self { + match value { $($value => Self::$var),*, _ => Self::$default } } + } + impl ToString for $name { fn to_string(&self) -> String { match self { $(Self::$var => $label),*, @@ -29,6 +28,8 @@ macro_rules! lookup { }.to_string() } } + + impl Lookup for $name {} }; } diff --git a/micro-rdk-nmea/src/parse_helpers/parsers.rs b/micro-rdk-nmea/src/parse_helpers/parsers.rs index 7de3dcf87..c8af613b3 100644 --- a/micro-rdk-nmea/src/parse_helpers/parsers.rs +++ b/micro-rdk-nmea/src/parse_helpers/parsers.rs @@ -22,8 +22,8 @@ pub trait FieldReader { /// A field reader for parsing a basic number type. A reader with bit_size n will read its value /// as the first n bits of `size_of::()` bytes with the remaining bits as zeroes (the resulting bytes -/// will be parsed as Little-Endian). See invocation of the number_field macro below for the currently -/// supported number types. +/// will be parsed as Little-Endian). See invocation of the generate_number_field_readers macro below +/// for the currently supported number types. pub struct NumberField { bit_size: usize, _marker: PhantomData, @@ -55,14 +55,14 @@ where } } -macro_rules! number_field { +macro_rules! generate_number_field_readers { ($($t:ty),*) => { $( impl FieldReader for NumberField<$t> { type FieldType = $t; fn read_from_data(&self, data: &[u8], start_idx: usize) -> Result<(usize, Self::FieldType), NmeaParseError> { - let type_size = std::mem::size_of::<$t>(); + let type_size = std::mem::size_of::(); match self.bit_size { x if x == (type_size * 8) => { let value: &[u8] = &data[start_idx..(start_idx + type_size)]; @@ -88,7 +88,7 @@ macro_rules! number_field { }; } -number_field!(u8, i8, u16, i16, u32, i32, u64, i64); +generate_number_field_readers!(u8, i8, u16, i16, u32, i32, u64, i64); /// A field reader for parsing data into a field type that implements the `Lookup` trait. /// The `bit_size` property is used to parse a raw number value first with a methodology similar to the one @@ -140,7 +140,7 @@ where } _ => unreachable!("lookup field raw value cannot be more than 32 bits"), }; - Ok((end_idx, T::from_value(enum_value))) + Ok((end_idx, enum_value.into())) } } From e10fb751840e82a8941b4a08dd9a334295b66e44 Mon Sep 17 00:00:00 2001 From: Gautham Date: Mon, 13 Jan 2025 09:45:30 -0500 Subject: [PATCH 04/14] name change --- micro-rdk-nmea/src/parse_helpers/enums.rs | 38 ++++++++++----------- micro-rdk-nmea/src/parse_helpers/parsers.rs | 4 +-- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/micro-rdk-nmea/src/parse_helpers/enums.rs b/micro-rdk-nmea/src/parse_helpers/enums.rs index d709dd4fc..4a4b68d96 100644 --- a/micro-rdk-nmea/src/parse_helpers/enums.rs +++ b/micro-rdk-nmea/src/parse_helpers/enums.rs @@ -1,9 +1,9 @@ -pub trait Lookup: Sized + From + ToString {} +pub trait NmeaEnumeratedField: Sized + From + ToString {} /// For generating a lookup data type found in an NMEA message. The first argument is the name of the /// enum type that will be generated. Each successive argument is a tuple with /// (raw number value, name of enum instance, string representation) -macro_rules! lookup { +macro_rules! define_nmea_enum { ( $name:ident, $(($value:expr, $var:ident, $label:expr)),*, $default:ident) => { #[derive(Copy, Clone, Debug)] pub enum $name { @@ -29,14 +29,14 @@ macro_rules! lookup { } } - impl Lookup for $name {} + impl NmeaEnumeratedField for $name {} }; } // Examples below taken from https://canboat.github.io/canboat/canboat.html -lookup!( +define_nmea_enum!( WaterReference, (0, PaddleWheel, "Paddle Wheel"), (1, PitotTube, "Pitot Tube"), @@ -46,7 +46,7 @@ lookup!( CouldNotParse ); -lookup!( +define_nmea_enum!( TemperatureSource, (0, SeaTemperature, "Sea Temperature"), (1, OutsideTemperature, "Outside Temperature"), @@ -75,7 +75,7 @@ lookup!( CouldNotParse ); -lookup!( +define_nmea_enum!( SystemTimeSource, (0, Gps, "GPS"), (1, Glonass, "GLONASS"), @@ -86,7 +86,7 @@ lookup!( CouldNotParse ); -lookup!( +define_nmea_enum!( MagneticVariationSource, (0, Manual, "Manual"), (1, AutomaticChart, "Automatic Chart"), @@ -100,7 +100,7 @@ lookup!( CouldNotParse ); -lookup!( +define_nmea_enum!( RepeatIndicator, (0, Initial, "Initial"), (1, FirstRetransmission, "First retransmission"), @@ -110,7 +110,7 @@ lookup!( CouldNotParse ); -lookup!( +define_nmea_enum!( AisMessageId, ( 1, @@ -194,21 +194,21 @@ lookup!( CouldNotParse ); -lookup!( +define_nmea_enum!( PositionAccuracy, (0, Low, "Low"), (1, High, "High"), CouldNotParse ); -lookup!( +define_nmea_enum!( RaimFlag, (0, NotInUse, "not in use"), (1, InUse, "in use"), CouldNotParse ); -lookup!( +define_nmea_enum!( TimeStamp, (60, NotAvailable, "Not available"), (61, ManualInputMode, "Manual input mode"), @@ -221,7 +221,7 @@ lookup!( CouldNotParse ); -lookup!( +define_nmea_enum!( AisTransceiver, (0, ChannelAVdlReception, "Channel A VDL reception"), (1, ChannelBVdlReception, "Channel B VDL reception"), @@ -236,7 +236,7 @@ lookup!( CouldNotParse ); -lookup!( +define_nmea_enum!( NavStatus, (0, UnderWayUsingEngine, "Under way using engine"), (1, AtAnchor, "At anchor"), @@ -271,7 +271,7 @@ lookup!( CouldNotParse ); -lookup!( +define_nmea_enum!( AisSpecialManeuver, (0, NotAvailable, "Not available"), ( @@ -284,7 +284,7 @@ lookup!( CouldNotParse ); -lookup!( +define_nmea_enum!( DirectionReference, (0, True, "True"), (1, Magnetic, "Magnetic"), @@ -292,7 +292,7 @@ lookup!( CouldNotParse ); -lookup!( +define_nmea_enum!( Gns, (0, Gps, "GPS"), (1, Glonass, "GLONASS"), @@ -306,7 +306,7 @@ lookup!( CouldNotParse ); -lookup!( +define_nmea_enum!( GnsMethod, (0, NoGnss, "no GNSS"), (1, GnssFix, "GNSS fix"), @@ -320,7 +320,7 @@ lookup!( CouldNotParse ); -lookup!( +define_nmea_enum!( GnsIntegrity, (0, NoIntegrityChecking, "No integrity checking"), (1, Safe, "Safe"), diff --git a/micro-rdk-nmea/src/parse_helpers/parsers.rs b/micro-rdk-nmea/src/parse_helpers/parsers.rs index c8af613b3..8329d113f 100644 --- a/micro-rdk-nmea/src/parse_helpers/parsers.rs +++ b/micro-rdk-nmea/src/parse_helpers/parsers.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use micro_rdk::common::sensor::GenericReadingsResult; use super::{ - enums::Lookup, + enums::NmeaEnumeratedField, errors::{NmeaParseError, NumberFieldError}, }; @@ -109,7 +109,7 @@ impl LookupField { impl FieldReader for LookupField where - T: Lookup, + T: NmeaEnumeratedField, { type FieldType = T; From 30edd34bd6de0cbd591a4fab5a85106c6fb31b03 Mon Sep 17 00:00:00 2001 From: Gautham Date: Wed, 15 Jan 2025 15:39:00 -0500 Subject: [PATCH 05/14] cursor based redesign --- Cargo.lock | 1 + micro-rdk-nmea/Cargo.toml | 1 + micro-rdk-nmea/src/parse_helpers/errors.rs | 2 + micro-rdk-nmea/src/parse_helpers/parsers.rs | 274 +++++++++++--------- 4 files changed, 157 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5dd0cc6b2..af198c0c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2831,6 +2831,7 @@ dependencies = [ name = "micro-rdk-nmea" version = "0.4.0-rc2" dependencies = [ + "bitvec", "micro-rdk", "thiserror 2.0.4", ] diff --git a/micro-rdk-nmea/Cargo.toml b/micro-rdk-nmea/Cargo.toml index 25b0aee22..717d5ca9f 100644 --- a/micro-rdk-nmea/Cargo.toml +++ b/micro-rdk-nmea/Cargo.toml @@ -11,5 +11,6 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bitvec = "1.0.1" micro-rdk = { workspace = true, features = ["native"] } thiserror = { workspace = true } diff --git a/micro-rdk-nmea/src/parse_helpers/errors.rs b/micro-rdk-nmea/src/parse_helpers/errors.rs index e460397b8..0b10e97d8 100644 --- a/micro-rdk-nmea/src/parse_helpers/errors.rs +++ b/micro-rdk-nmea/src/parse_helpers/errors.rs @@ -12,6 +12,8 @@ pub enum NmeaParseError { NumberFieldError(#[from] NumberFieldError), #[error(transparent)] TryFromSliceError(#[from] std::array::TryFromSliceError), + #[error("end of buffer exceeded")] + EndOfBufferExceeded, #[error("found unsupported PGN {0}")] UnsupportedPgn(u32), } diff --git a/micro-rdk-nmea/src/parse_helpers/parsers.rs b/micro-rdk-nmea/src/parse_helpers/parsers.rs index 8329d113f..6ca27b1b8 100644 --- a/micro-rdk-nmea/src/parse_helpers/parsers.rs +++ b/micro-rdk-nmea/src/parse_helpers/parsers.rs @@ -1,4 +1,9 @@ -use std::marker::PhantomData; +use bitvec::prelude::*; +use std::{ + marker::PhantomData, + rc::Rc, + sync::atomic::{AtomicUsize, Ordering}, +}; use micro_rdk::common::sensor::GenericReadingsResult; @@ -7,17 +12,59 @@ use super::{ errors::{NmeaParseError, NumberFieldError}, }; -/// Trait for reading a data type (`FieldType`) from a byte slice. +/// Cursor over a byte slice of data. The cursor can be moved by either consuming +/// child-instances of `DataRead` or calling the `advance` function +pub struct DataCursor<'a> { + data: &'a [u8], + bit_position: Rc, +} + +struct DataRead<'a> { + data: &'a [u8], + bit_position: Rc, + bit_size: usize, +} + +impl<'a> Drop for DataRead<'a> { + fn drop(&mut self) { + self.bit_position.fetch_add(self.bit_size, Ordering::SeqCst); + } +} + +impl<'a> DataCursor<'a> { + pub fn new(data: &'a [u8]) -> Self { + Self { + data, + bit_position: Rc::new(AtomicUsize::new(0)), + } + } + + fn read(&self, bits: usize) -> DataRead { + let bit_position = self.bit_position.load(Ordering::SeqCst); + let end_byte_position = (bit_position + bits).div_ceil(8); + let start_byte_position = bit_position / 8; + DataRead { + data: &self.data[start_byte_position..end_byte_position], + bit_position: self.bit_position.clone(), + bit_size: bits, + } + } + + pub fn advance(&self, bits: usize) -> Result<(), NmeaParseError> { + let advanced_position = self.bit_position.fetch_add(bits, Ordering::SeqCst); + if advanced_position > (self.data.len() * 8) { + self.bit_position.fetch_sub(bits, Ordering::SeqCst); + Err(NmeaParseError::EndOfBufferExceeded) + } else { + Ok(()) + } + } +} + +/// Trait for reading a data type (`FieldType`) from a DataCursor. pub trait FieldReader { type FieldType; - - /// Takes a byte slice (`data`) and starting index. Returns a tuple consisting of - /// the next index to read from the byte sequence and the parsed value. - fn read_from_data( - &self, - data: &[u8], - start_idx: usize, - ) -> Result<(usize, Self::FieldType), NmeaParseError>; + fn read_from_cursor(&self, cursor: &DataCursor) -> Result; } /// A field reader for parsing a basic number type. A reader with bit_size n will read its value @@ -58,30 +105,43 @@ where macro_rules! generate_number_field_readers { ($($t:ty),*) => { $( - impl FieldReader for NumberField<$t> { - type FieldType = $t; + impl TryFrom> for $t { + type Error = NumberFieldError; + fn try_from(value: DataRead) -> Result { + let max_size = std::mem::size_of::(); + if (value.bit_size / 8) > max_size { + return Err(NumberFieldError::ImproperBitSize(value.bit_size, max_size * 8)); + } - fn read_from_data(&self, data: &[u8], start_idx: usize) -> Result<(usize, Self::FieldType), NmeaParseError> { - let type_size = std::mem::size_of::(); - match self.bit_size { - x if x == (type_size * 8) => { - let value: &[u8] = &data[start_idx..(start_idx + type_size)]; - Ok((start_idx + type_size, <$t>::from_le_bytes(value.try_into()?))) - } - x => { - let end_idx = start_idx + (x / 8); - - let value: &[u8] = &data[start_idx..(end_idx + 1)]; - let padding: Vec = vec![0; (type_size - value.len())]; - let padding_slice: &[u8] = &padding[..]; - let join = [value, padding_slice].concat(); - let full_value = &join[..]; - - let shift = x % 8; - let value = <$t>::from_le_bytes(full_value.try_into()?); - Ok((end_idx, value >> shift << shift)) + let bits = &value.data[..].view_bits::(); + + let start_idx = value.bit_position.load(Ordering::SeqCst) % 8; + let end_idx = start_idx + value.bit_size; + let mut bit_vec = bits[start_idx..end_idx].to_bitvec(); + if bit_vec.len() != (max_size * 8) { + let last_bit_start = bit_vec.len() - (value.bit_size % 8); + let _ = &bit_vec[last_bit_start..].reverse(); + + // pad the bit vector with 0 bits until we have enough bytes to + // parse the number + for _ in (0..(max_size * 8 - value.bit_size)) { + bit_vec.push(false); } + + + let last_bit_start = bit_vec.len() - 8; + let _ = &bit_vec[last_bit_start..].reverse(); } + + Ok(bit_vec.load_le::<$t>()) + } + } + + impl FieldReader for NumberField<$t> { + type FieldType = $t; + + fn read_from_cursor(&self, cursor: &DataCursor) -> Result { + Ok(cursor.read(self.bit_size).try_into()?) } } )* @@ -113,34 +173,15 @@ where { type FieldType = T; - fn read_from_data( - &self, - data: &[u8], - start_idx: usize, - ) -> Result<(usize, Self::FieldType), NmeaParseError> { - let end_idx = start_idx + (self.bit_size / 8); - let data_slice: &[u8] = &data[start_idx..(end_idx + 1)]; + fn read_from_cursor(&self, cursor: &DataCursor) -> Result { + let data_read = cursor.read(self.bit_size); let enum_value = match self.bit_size { - 8 => data[start_idx] as u32, - 16 => u16::from_le_bytes(data_slice.try_into()?) as u32, - 32 => u32::from_le_bytes(data_slice.try_into()?), - x if x < 8 => { - let shift = 8 - x; - (data[start_idx] >> shift) as u32 - } - x if x < 16 => { - let shift = 16 - x; - let raw_val = u16::from_le_bytes(data_slice.try_into()?); - (raw_val >> shift) as u32 - } - x if x < 32 => { - let shift = 32 - x; - let raw_val = u32::from_le_bytes(data_slice.try_into()?); - raw_val >> shift - } - _ => unreachable!("lookup field raw value cannot be more than 32 bits"), - }; - Ok((end_idx, enum_value.into())) + x if x <= 8 => Ok(u8::try_from(data_read)? as u32), + x if x <= 16 => Ok(u16::try_from(data_read)? as u32), + x if x <= 32 => Ok(u16::try_from(data_read)? as u32), + x => Err(NumberFieldError::ImproperBitSize(x, 32)), + }?; + Ok(enum_value.into()) } } @@ -161,20 +202,14 @@ where { type FieldType = [ as FieldReader>::FieldType; N]; - fn read_from_data( - &self, - data: &[u8], - start_idx: usize, - ) -> Result<(usize, Self::FieldType), NmeaParseError> { + fn read_from_cursor(&self, cursor: &DataCursor) -> Result { let mut res: [ as FieldReader>::FieldType; N] = [Default::default(); N]; - let mut end_idx = start_idx; let field_reader: NumberField = Default::default(); for i in 0..N { - let (bytes_read, next_elem) = field_reader.read_from_data(data, end_idx)?; + let next_elem = field_reader.read_from_cursor(cursor)?; res[i] = next_elem; - end_idx = bytes_read } - Ok((end_idx, res)) + Ok(res) } } @@ -183,6 +218,7 @@ where pub trait FieldSet: Sized { fn from_bytes(data: &[u8], current_index: usize) -> Result<(usize, Self), NmeaParseError>; fn to_readings(&self) -> Result; + fn from_data(cursor: &DataCursor) -> Result; } /// A field reader that parses a vector of structs representing a field set (see `FieldSet`) from a byte slice. @@ -207,106 +243,94 @@ where { type FieldType = Vec; - fn read_from_data( - &self, - data: &[u8], - start_idx: usize, - ) -> Result<(usize, Self::FieldType), NmeaParseError> { - let mut idx = start_idx; + fn read_from_cursor(&self, cursor: &DataCursor) -> Result { let mut res = Vec::new(); for _ in 0..self.length { - let (new_start, new_elem) = T::from_bytes(data, idx)?; - idx = new_start; - res.push(new_elem); + let elem = T::from_data(&cursor)?; + res.push(elem); } - Ok((idx, res)) + Ok(res) } } #[cfg(test)] mod tests { use crate::parse_helpers::{ - enums::MagneticVariationSource, errors::NmeaParseError, parsers::FieldReader, + enums::MagneticVariationSource, + errors::NmeaParseError, + parsers::{DataCursor, FieldReader}, }; use super::{ArrayField, FieldSet, FieldSetList, LookupField, NumberField}; #[test] fn number_field_test() { - let single_byte_reader = NumberField::::new(4); - assert!(single_byte_reader.is_ok()); - let single_byte_reader = single_byte_reader.unwrap(); - - // 125 = 01111101, first four bits as byte => 01110000 = 112 let data_vec: Vec = vec![100, 6, 125, 73]; let data: &[u8] = &data_vec[..]; - let res = single_byte_reader.read_from_data(data, 2); + let cursor = DataCursor::new(data); + + let reader = NumberField::::new(4); + assert!(reader.is_ok()); + let reader = reader.unwrap(); + assert!(cursor.advance(16).is_ok()); + let res = reader.read_from_cursor(&cursor); assert!(res.is_ok()); - let (next_index, res) = res.unwrap(); - assert_eq!(next_index, 2); - assert_eq!(res, 112); + // 125 = 01111101, first four bits as byte => 01110000 = 112 + assert_eq!(res.unwrap(), 7); let reader = NumberField::::new(12); assert!(reader.is_ok()); let reader = reader.unwrap(); + let data_vec: Vec = vec![100, 6, 125, 179, 152, 113]; let data: &[u8] = &data_vec[..]; + let cursor = DataCursor::new(data); // [179, 152] is 39091 in u16, reading the first 12 bits and ignoring the last 4 - // should yield 39088 - let res = reader.read_from_data(data, 3); + // should yield [10110011 10011000] => [10110011 00001001] => (37043 in Little-Endian) + assert!(cursor.advance(24).is_ok()); + let res = reader.read_from_cursor(&cursor); assert!(res.is_ok()); - let (next_index, res) = res.unwrap(); - assert_eq!(next_index, 4); - assert_eq!(res, 39088); + assert_eq!(res.unwrap(), 2483); } #[test] fn lookup_field_test() { let reader = LookupField::::new(4); - let data_vec: Vec = vec![100, 6, 111, 77, 152, 113]; + let data_vec: Vec = vec![100, 6, 111, 138, 152, 113]; let data: &[u8] = &data_vec[..]; + let cursor = DataCursor::new(data); - let res = reader.read_from_data(data, 3); - let (next_index, res) = res.unwrap(); - assert_eq!(next_index, 3); - assert!(matches!(res, MagneticVariationSource::Wmm2000)); + assert!(cursor.advance(24).is_ok()); + let res = reader.read_from_cursor(&cursor); + assert!(res.is_ok()); + assert!(matches!(res.unwrap(), MagneticVariationSource::Wmm2020)); } #[test] fn array_field_test() { let data_vec: Vec = vec![100, 6, 111, 77, 152, 113, 42, 42]; let data: &[u8] = &data_vec[..]; + let cursor = DataCursor::new(data); let reader = ArrayField::::new(); + assert!(cursor.advance(16).is_ok()); + let res = reader.read_from_cursor(&cursor); + assert_eq!(res.unwrap(), [111, 77, 152]); - let res = reader.read_from_data(data, 2); - assert!(res.is_ok()); - let (next_index, res) = res.unwrap(); - - assert_eq!(next_index, 5); - assert_eq!(res, [111, 77, 152]); - + let cursor = DataCursor::new(data); let reader = ArrayField::::new(); - - let res = reader.read_from_data(data, 1); - assert!(res.is_ok()); - let (next_index, res) = res.unwrap(); - // [6, 11] = 28422, [77, 152] = 38989 - assert_eq!(next_index, 5); - assert_eq!(res, [28422, 38989]); + assert!(cursor.advance(8).is_ok()); + let res = reader.read_from_cursor(&cursor); + assert_eq!(res.unwrap(), [28422, 38989]); + let cursor = DataCursor::new(data); let reader = ArrayField::::new(); - - let res = reader.read_from_data(data, 0); - assert!(res.is_ok()); - let (next_index, res) = res.unwrap(); - // [100, 6, 111, 77] = 1299121764, [152, 113, 42, 42] = 707424664 - assert_eq!(next_index, 8); - assert_eq!(res, [1299121764, 707424664]); + let res = reader.read_from_cursor(&cursor); + assert_eq!(res.unwrap(), [1299121764, 707424664]); } #[derive(Debug, PartialEq, Eq)] @@ -332,17 +356,26 @@ mod tests { ) -> Result { Err(NmeaParseError::UnsupportedPgn(0)) } + + fn from_data(cursor: &super::DataCursor) -> Result { + let a = cursor.read(16).try_into()?; + let b = cursor.read(8).try_into()?; + let c = cursor.read(16).try_into()?; + Ok(TestFieldSet { a, b, c }) + } } #[test] fn fieldset_field_test() { let data_vec: Vec = vec![100, 6, 111, 77, 152, 113, 42, 42, 1, 2, 3]; let data: &[u8] = &data_vec[..]; + let cursor = DataCursor::new(data); let reader = FieldSetList::::new(2); - let res = reader.read_from_data(data, 1); + assert!(cursor.advance(8).is_ok()); + let res = reader.read_from_cursor(&cursor); assert!(res.is_ok()); - let (next_index, res) = res.unwrap(); + let res = res.unwrap(); let expected_at_0 = TestFieldSet { a: 28422, // [6, 11] @@ -355,7 +388,6 @@ mod tests { c: 770, // [2, 3] }; - assert_eq!(next_index, 11); assert_eq!(res[0], expected_at_0); assert_eq!(res[1], expected_at_1); } From f9405231183bfc0bad779f3df8bbff95915e8835 Mon Sep 17 00:00:00 2001 From: Gautham Date: Wed, 15 Jan 2025 15:47:14 -0500 Subject: [PATCH 06/14] handle some edge cases --- micro-rdk-nmea/src/parse_helpers/parsers.rs | 42 +++++++++++++-------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/micro-rdk-nmea/src/parse_helpers/parsers.rs b/micro-rdk-nmea/src/parse_helpers/parsers.rs index 6ca27b1b8..e37b389b0 100644 --- a/micro-rdk-nmea/src/parse_helpers/parsers.rs +++ b/micro-rdk-nmea/src/parse_helpers/parsers.rs @@ -39,14 +39,18 @@ impl<'a> DataCursor<'a> { } } - fn read(&self, bits: usize) -> DataRead { + fn read(&self, bits: usize) -> Result { let bit_position = self.bit_position.load(Ordering::SeqCst); let end_byte_position = (bit_position + bits).div_ceil(8); - let start_byte_position = bit_position / 8; - DataRead { - data: &self.data[start_byte_position..end_byte_position], - bit_position: self.bit_position.clone(), - bit_size: bits, + if end_byte_position > data.len() { + Err(NmeaParseError::EndOfBufferExceeded) + } else { + let start_byte_position = bit_position / 8; + Ok(DataRead { + data: &self.data[start_byte_position..end_byte_position], + bit_position: self.bit_position.clone(), + bit_size: bits, + }) } } @@ -141,7 +145,7 @@ macro_rules! generate_number_field_readers { type FieldType = $t; fn read_from_cursor(&self, cursor: &DataCursor) -> Result { - Ok(cursor.read(self.bit_size).try_into()?) + Ok(cursor.read(self.bit_size)?.try_into()?) } } )* @@ -159,10 +163,14 @@ pub struct LookupField { } impl LookupField { - pub fn new(bit_size: usize) -> Self { - Self { - bit_size, - _marker: Default::default(), + pub fn new(bit_size: usize) -> Result { + if bit_size > 32 { + Err(NumberFieldError::ImproperBitSize(bit_size, 32)) + } else { + Ok(Self { + bit_size, + _marker: Default::default(), + }) } } } @@ -174,12 +182,12 @@ where type FieldType = T; fn read_from_cursor(&self, cursor: &DataCursor) -> Result { - let data_read = cursor.read(self.bit_size); + let data_read = cursor.read(self.bit_size)?; let enum_value = match self.bit_size { x if x <= 8 => Ok(u8::try_from(data_read)? as u32), x if x <= 16 => Ok(u16::try_from(data_read)? as u32), x if x <= 32 => Ok(u16::try_from(data_read)? as u32), - x => Err(NumberFieldError::ImproperBitSize(x, 32)), + _ => unreachable!("malformed lookup field detected"), }?; Ok(enum_value.into()) } @@ -297,6 +305,8 @@ mod tests { #[test] fn lookup_field_test() { let reader = LookupField::::new(4); + assert!(reader.is_ok()); + let reader = reader.unwrap(); let data_vec: Vec = vec![100, 6, 111, 138, 152, 113]; let data: &[u8] = &data_vec[..]; @@ -358,9 +368,9 @@ mod tests { } fn from_data(cursor: &super::DataCursor) -> Result { - let a = cursor.read(16).try_into()?; - let b = cursor.read(8).try_into()?; - let c = cursor.read(16).try_into()?; + let a = cursor.read(16)?.try_into()?; + let b = cursor.read(8)?.try_into()?; + let c = cursor.read(16)?.try_into()?; Ok(TestFieldSet { a, b, c }) } } From feb9871ed25617fdc1a71608f6e201d9da67d91e Mon Sep 17 00:00:00 2001 From: Gautham Date: Wed, 15 Jan 2025 16:05:57 -0500 Subject: [PATCH 07/14] move dependency --- Cargo.toml | 1 + micro-rdk-nmea/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a8c072100..b4f9462dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ async-std-openssl = "0.6.3" atomic-waker = "1.1.2" base64 = "0.22.1" bitfield = "0.17.0" +bitvec = "1.0.1" bytecodec = "0.4.15" bytes = "1.9.0" cargo_metadata = "0.19.1" diff --git a/micro-rdk-nmea/Cargo.toml b/micro-rdk-nmea/Cargo.toml index 717d5ca9f..8410ed80c 100644 --- a/micro-rdk-nmea/Cargo.toml +++ b/micro-rdk-nmea/Cargo.toml @@ -11,6 +11,6 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bitvec = "1.0.1" +bitvec = { workspace = true } micro-rdk = { workspace = true, features = ["native"] } thiserror = { workspace = true } From 65c6b9a989009c1785eaab79c229a50aac5bb275 Mon Sep 17 00:00:00 2001 From: Gautham Date: Wed, 15 Jan 2025 16:12:27 -0500 Subject: [PATCH 08/14] oops --- micro-rdk-nmea/src/parse_helpers/parsers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/micro-rdk-nmea/src/parse_helpers/parsers.rs b/micro-rdk-nmea/src/parse_helpers/parsers.rs index e37b389b0..0bf309413 100644 --- a/micro-rdk-nmea/src/parse_helpers/parsers.rs +++ b/micro-rdk-nmea/src/parse_helpers/parsers.rs @@ -283,7 +283,7 @@ mod tests { assert!(cursor.advance(16).is_ok()); let res = reader.read_from_cursor(&cursor); assert!(res.is_ok()); - // 125 = 01111101, first four bits as byte => 01110000 = 112 + // 125 = 01111101, first four bits as byte => 00000111 = 7 assert_eq!(res.unwrap(), 7); let reader = NumberField::::new(12); @@ -295,7 +295,7 @@ mod tests { let cursor = DataCursor::new(data); // [179, 152] is 39091 in u16, reading the first 12 bits and ignoring the last 4 - // should yield [10110011 10011000] => [10110011 00001001] => (37043 in Little-Endian) + // should yield [10110011 10011000] => [10110011 00001001] => 2483 ( in Little-Endian) assert!(cursor.advance(24).is_ok()); let res = reader.read_from_cursor(&cursor); assert!(res.is_ok()); From d443cea99c8a33eef28746a3eb594a43853a4a3d Mon Sep 17 00:00:00 2001 From: Gautham Date: Wed, 15 Jan 2025 16:21:28 -0500 Subject: [PATCH 09/14] ok now the tests pass again --- micro-rdk-nmea/src/parse_helpers/parsers.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/micro-rdk-nmea/src/parse_helpers/parsers.rs b/micro-rdk-nmea/src/parse_helpers/parsers.rs index 0bf309413..5f510ceef 100644 --- a/micro-rdk-nmea/src/parse_helpers/parsers.rs +++ b/micro-rdk-nmea/src/parse_helpers/parsers.rs @@ -42,7 +42,7 @@ impl<'a> DataCursor<'a> { fn read(&self, bits: usize) -> Result { let bit_position = self.bit_position.load(Ordering::SeqCst); let end_byte_position = (bit_position + bits).div_ceil(8); - if end_byte_position > data.len() { + if end_byte_position > self.data.len() { Err(NmeaParseError::EndOfBufferExceeded) } else { let start_byte_position = bit_position / 8; @@ -184,9 +184,9 @@ where fn read_from_cursor(&self, cursor: &DataCursor) -> Result { let data_read = cursor.read(self.bit_size)?; let enum_value = match self.bit_size { - x if x <= 8 => Ok(u8::try_from(data_read)? as u32), - x if x <= 16 => Ok(u16::try_from(data_read)? as u32), - x if x <= 32 => Ok(u16::try_from(data_read)? as u32), + x if x <= 8 => Ok::(u8::try_from(data_read)? as u32), + x if x <= 16 => Ok::(u16::try_from(data_read)? as u32), + x if x <= 32 => Ok::(u16::try_from(data_read)? as u32), _ => unreachable!("malformed lookup field detected"), }?; Ok(enum_value.into()) From 189157e36e8fe52d4004b145875cd2311d106de3 Mon Sep 17 00:00:00 2001 From: Gautham Date: Fri, 17 Jan 2025 11:46:03 -0500 Subject: [PATCH 10/14] bugfix --- micro-rdk-nmea/src/parse_helpers/parsers.rs | 29 +++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/micro-rdk-nmea/src/parse_helpers/parsers.rs b/micro-rdk-nmea/src/parse_helpers/parsers.rs index 5f510ceef..a96a22fe7 100644 --- a/micro-rdk-nmea/src/parse_helpers/parsers.rs +++ b/micro-rdk-nmea/src/parse_helpers/parsers.rs @@ -123,18 +123,17 @@ macro_rules! generate_number_field_readers { let end_idx = start_idx + value.bit_size; let mut bit_vec = bits[start_idx..end_idx].to_bitvec(); if bit_vec.len() != (max_size * 8) { + // the last byte is incomplete, we reverse the remaining bits of the + // incomplete byte so it can be padded with zeros, then we reverse the bits + // of the now complete last byte for the proper bit order let last_bit_start = bit_vec.len() - (value.bit_size % 8); let _ = &bit_vec[last_bit_start..].reverse(); - // pad the bit vector with 0 bits until we have enough bytes to - // parse the number for _ in (0..(max_size * 8 - value.bit_size)) { bit_vec.push(false); } - - let last_bit_start = bit_vec.len() - 8; - let _ = &bit_vec[last_bit_start..].reverse(); + let _ = &bit_vec[last_bit_start..(last_bit_start + 8)].reverse(); } Ok(bit_vec.load_le::<$t>()) @@ -183,12 +182,7 @@ where fn read_from_cursor(&self, cursor: &DataCursor) -> Result { let data_read = cursor.read(self.bit_size)?; - let enum_value = match self.bit_size { - x if x <= 8 => Ok::(u8::try_from(data_read)? as u32), - x if x <= 16 => Ok::(u16::try_from(data_read)? as u32), - x if x <= 32 => Ok::(u16::try_from(data_read)? as u32), - _ => unreachable!("malformed lookup field detected"), - }?; + let enum_value = u32::try_from(data_read)?; Ok(enum_value.into()) } } @@ -300,6 +294,19 @@ mod tests { let res = reader.read_from_cursor(&cursor); assert!(res.is_ok()); assert_eq!(res.unwrap(), 2483); + + let reader = NumberField::::new(3); + assert!(reader.is_ok()); + let reader = reader.unwrap(); + + let data_vec: Vec = vec![154, 6, 125, 179, 152, 113]; + let data: &[u8] = &data_vec[..]; + let cursor = DataCursor::new(data); + + // 154 is 10011010, reading the first 3 bits should yield 100 = 4 + let res = reader.read_from_cursor(&cursor); + assert!(res.is_ok()); + assert_eq!(res.unwrap(), 4); } #[test] From 84ad67e92ce8a69380954b9520a905fa7b8392d2 Mon Sep 17 00:00:00 2001 From: Gautham Date: Fri, 17 Jan 2025 15:20:20 -0500 Subject: [PATCH 11/14] use shifting and remove bitvec dependency --- Cargo.lock | 1 - Cargo.toml | 1 - micro-rdk-nmea/Cargo.toml | 1 - micro-rdk-nmea/src/parse_helpers/parsers.rs | 36 +++++++-------------- 4 files changed, 12 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af198c0c1..5dd0cc6b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2831,7 +2831,6 @@ dependencies = [ name = "micro-rdk-nmea" version = "0.4.0-rc2" dependencies = [ - "bitvec", "micro-rdk", "thiserror 2.0.4", ] diff --git a/Cargo.toml b/Cargo.toml index b4f9462dd..a8c072100 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,6 @@ async-std-openssl = "0.6.3" atomic-waker = "1.1.2" base64 = "0.22.1" bitfield = "0.17.0" -bitvec = "1.0.1" bytecodec = "0.4.15" bytes = "1.9.0" cargo_metadata = "0.19.1" diff --git a/micro-rdk-nmea/Cargo.toml b/micro-rdk-nmea/Cargo.toml index 8410ed80c..25b0aee22 100644 --- a/micro-rdk-nmea/Cargo.toml +++ b/micro-rdk-nmea/Cargo.toml @@ -11,6 +11,5 @@ rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bitvec = { workspace = true } micro-rdk = { workspace = true, features = ["native"] } thiserror = { workspace = true } diff --git a/micro-rdk-nmea/src/parse_helpers/parsers.rs b/micro-rdk-nmea/src/parse_helpers/parsers.rs index a96a22fe7..a189b43f0 100644 --- a/micro-rdk-nmea/src/parse_helpers/parsers.rs +++ b/micro-rdk-nmea/src/parse_helpers/parsers.rs @@ -1,4 +1,3 @@ -use bitvec::prelude::*; use std::{ marker::PhantomData, rc::Rc, @@ -110,33 +109,21 @@ macro_rules! generate_number_field_readers { ($($t:ty),*) => { $( impl TryFrom> for $t { - type Error = NumberFieldError; + type Error = NmeaParseError; fn try_from(value: DataRead) -> Result { let max_size = std::mem::size_of::(); if (value.bit_size / 8) > max_size { - return Err(NumberFieldError::ImproperBitSize(value.bit_size, max_size * 8)); + Err(NumberFieldError::ImproperBitSize(value.bit_size, max_size * 8).into()) + } else if value.bit_size == (max_size * 8) { + Ok(<$t>::from_le_bytes(value.data[..].try_into()?)) + } else { + let byte_idx = value.bit_size / 8; + let data_cpy = &mut value.data[..].to_vec(); + data_cpy.resize(max_size, 0); + let shift = 8 - (value.bit_size % 8); + data_cpy[byte_idx] = data_cpy[byte_idx] >> shift; + Ok(<$t>::from_le_bytes(data_cpy[..].try_into()?)) } - - let bits = &value.data[..].view_bits::(); - - let start_idx = value.bit_position.load(Ordering::SeqCst) % 8; - let end_idx = start_idx + value.bit_size; - let mut bit_vec = bits[start_idx..end_idx].to_bitvec(); - if bit_vec.len() != (max_size * 8) { - // the last byte is incomplete, we reverse the remaining bits of the - // incomplete byte so it can be padded with zeros, then we reverse the bits - // of the now complete last byte for the proper bit order - let last_bit_start = bit_vec.len() - (value.bit_size % 8); - let _ = &bit_vec[last_bit_start..].reverse(); - - for _ in (0..(max_size * 8 - value.bit_size)) { - bit_vec.push(false); - } - - let _ = &bit_vec[last_bit_start..(last_bit_start + 8)].reverse(); - } - - Ok(bit_vec.load_le::<$t>()) } } @@ -305,6 +292,7 @@ mod tests { // 154 is 10011010, reading the first 3 bits should yield 100 = 4 let res = reader.read_from_cursor(&cursor); + println!("res: {:?}", res); assert!(res.is_ok()); assert_eq!(res.unwrap(), 4); } From ed06621120c15af1bef62851fc1b2166f39edca6 Mon Sep 17 00:00:00 2001 From: Gautham Date: Tue, 21 Jan 2025 16:03:18 -0500 Subject: [PATCH 12/14] refactor DataCursor to take ownership of underlying data and consume it --- micro-rdk-nmea/src/parse_helpers/errors.rs | 4 +- micro-rdk-nmea/src/parse_helpers/parsers.rs | 195 ++++++++------------ 2 files changed, 81 insertions(+), 118 deletions(-) diff --git a/micro-rdk-nmea/src/parse_helpers/errors.rs b/micro-rdk-nmea/src/parse_helpers/errors.rs index 0b10e97d8..a5ea929b3 100644 --- a/micro-rdk-nmea/src/parse_helpers/errors.rs +++ b/micro-rdk-nmea/src/parse_helpers/errors.rs @@ -12,8 +12,8 @@ pub enum NmeaParseError { NumberFieldError(#[from] NumberFieldError), #[error(transparent)] TryFromSliceError(#[from] std::array::TryFromSliceError), - #[error("end of buffer exceeded")] - EndOfBufferExceeded, + #[error("not enough data to parse next field")] + NotEnoughData, #[error("found unsupported PGN {0}")] UnsupportedPgn(u32), } diff --git a/micro-rdk-nmea/src/parse_helpers/parsers.rs b/micro-rdk-nmea/src/parse_helpers/parsers.rs index a189b43f0..7168e5e7f 100644 --- a/micro-rdk-nmea/src/parse_helpers/parsers.rs +++ b/micro-rdk-nmea/src/parse_helpers/parsers.rs @@ -11,63 +11,51 @@ use super::{ errors::{NmeaParseError, NumberFieldError}, }; -/// Cursor over a byte slice of data. The cursor can be moved by either consuming -/// child-instances of `DataRead` or calling the `advance` function -pub struct DataCursor<'a> { - data: &'a [u8], - bit_position: Rc, +/// Cursor that consumes can consume bytes from a data vector by bit size. +pub struct DataCursor { + data: Vec, } -struct DataRead<'a> { - data: &'a [u8], - bit_position: Rc, - bit_size: usize, -} - -impl<'a> Drop for DataRead<'a> { - fn drop(&mut self) { - self.bit_position.fetch_add(self.bit_size, Ordering::SeqCst); - } -} - -impl<'a> DataCursor<'a> { - pub fn new(data: &'a [u8]) -> Self { - Self { - data, - bit_position: Rc::new(AtomicUsize::new(0)), - } +impl DataCursor { + pub fn new(data: Vec) -> Self { + DataCursor { data } } - fn read(&self, bits: usize) -> Result { - let bit_position = self.bit_position.load(Ordering::SeqCst); - let end_byte_position = (bit_position + bits).div_ceil(8); - if end_byte_position > self.data.len() { - Err(NmeaParseError::EndOfBufferExceeded) - } else { - let start_byte_position = bit_position / 8; - Ok(DataRead { - data: &self.data[start_byte_position..end_byte_position], - bit_position: self.bit_position.clone(), - bit_size: bits, - }) + pub fn read(&mut self, bit_size: usize) -> Result, NmeaParseError> { + if bit_size / 8 > self.data.len() { + return Err(NmeaParseError::NotEnoughData); } - } - - pub fn advance(&self, bits: usize) -> Result<(), NmeaParseError> { - let advanced_position = self.bit_position.fetch_add(bits, Ordering::SeqCst); - if advanced_position > (self.data.len() * 8) { - self.bit_position.fetch_sub(bits, Ordering::SeqCst); - Err(NmeaParseError::EndOfBufferExceeded) - } else { - Ok(()) + let mut res = Vec::new(); + res.extend(self.data.drain(..(bit_size / 8))); + + // If the next bit_size takes us into the middle of a byte, then + // we want the next byte of the result to be shifted so as to ignore the remaining bits. + // The first byte of the data should have the bits that were included in the result + // set to zero. + // + // This is due to the way number fields in NMEA messages appear to be formatted + // from observation of successfully parsed data, which is that overflowing bits + // are read in MsB order, but the resulting bytes are read in Little-Endian. + // + // For example, let's say the data is [179, 152], and we want to read a u16 from a bit size of 12 + // + // [179, 152] is 39091 in u16, reading the first 12 bits and ignoring the last 4 + // should yield [10110011 10011000] => [10110011 00001001] => 2483 (in Little-Endian) + if bit_size % 8 != 0 { + res.push(self.data[0] >> (8 - (bit_size % 8))); + let mask: u8 = 255 >> (bit_size % 8); + if let Some(first_byte) = self.data.get_mut(0) { + *first_byte |= mask; + } } + Ok(res) } } /// Trait for reading a data type (`FieldType`) from a DataCursor. pub trait FieldReader { type FieldType; - fn read_from_cursor(&self, cursor: &DataCursor) -> Result; + fn read_from_cursor(&self, cursor: &mut DataCursor) -> Result; } /// A field reader for parsing a basic number type. A reader with bit_size n will read its value @@ -108,30 +96,18 @@ where macro_rules! generate_number_field_readers { ($($t:ty),*) => { $( - impl TryFrom> for $t { - type Error = NmeaParseError; - fn try_from(value: DataRead) -> Result { - let max_size = std::mem::size_of::(); - if (value.bit_size / 8) > max_size { - Err(NumberFieldError::ImproperBitSize(value.bit_size, max_size * 8).into()) - } else if value.bit_size == (max_size * 8) { - Ok(<$t>::from_le_bytes(value.data[..].try_into()?)) - } else { - let byte_idx = value.bit_size / 8; - let data_cpy = &mut value.data[..].to_vec(); - data_cpy.resize(max_size, 0); - let shift = 8 - (value.bit_size % 8); - data_cpy[byte_idx] = data_cpy[byte_idx] >> shift; - Ok(<$t>::from_le_bytes(data_cpy[..].try_into()?)) - } - } - } - impl FieldReader for NumberField<$t> { type FieldType = $t; - fn read_from_cursor(&self, cursor: &DataCursor) -> Result { - Ok(cursor.read(self.bit_size)?.try_into()?) + fn read_from_cursor(&self, cursor: &mut DataCursor2) -> Result { + let mut data = cursor.read(self.bit_size)?; + let max_size = std::mem::size_of::(); + if self.bit_size / 8 > max_size { + Err(NumberFieldError::ImproperBitSize(self.bit_size, max_size * 8).into()) + } else { + data.resize(max_size, 0); + Ok(<$t>::from_le_bytes(data[..].try_into()?)) + } } } )* @@ -167,9 +143,9 @@ where { type FieldType = T; - fn read_from_cursor(&self, cursor: &DataCursor) -> Result { - let data_read = cursor.read(self.bit_size)?; - let enum_value = u32::try_from(data_read)?; + fn read_from_cursor(&self, cursor: &mut DataCursor) -> Result { + let number_parser = NumberField::::new(self.bit_size)?; + let enum_value = number_parser.read_from_cursor(cursor)?; Ok(enum_value.into()) } } @@ -191,7 +167,7 @@ where { type FieldType = [ as FieldReader>::FieldType; N]; - fn read_from_cursor(&self, cursor: &DataCursor) -> Result { + fn read_from_cursor(&self, cursor: &mut DataCursor) -> Result { let mut res: [ as FieldReader>::FieldType; N] = [Default::default(); N]; let field_reader: NumberField = Default::default(); for i in 0..N { @@ -205,9 +181,8 @@ where /// Some NMEA 2000 messages have a set of fields that may be repeated a number of times (usually specified by the value /// of another field). This trait is for structs that implement this set of fields, most likely using the `FieldsetDerive` macro. pub trait FieldSet: Sized { - fn from_bytes(data: &[u8], current_index: usize) -> Result<(usize, Self), NmeaParseError>; fn to_readings(&self) -> Result; - fn from_data(cursor: &DataCursor) -> Result; + fn from_data(cursor: &mut DataCursor) -> Result; } /// A field reader that parses a vector of structs representing a field set (see `FieldSet`) from a byte slice. @@ -232,10 +207,10 @@ where { type FieldType = Vec; - fn read_from_cursor(&self, cursor: &DataCursor) -> Result { + fn read_from_cursor(&self, cursor: &mut DataCursor) -> Result { let mut res = Vec::new(); for _ in 0..self.length { - let elem = T::from_data(&cursor)?; + let elem = T::from_data(cursor)?; res.push(elem); } Ok(res) @@ -255,14 +230,13 @@ mod tests { #[test] fn number_field_test() { let data_vec: Vec = vec![100, 6, 125, 73]; - let data: &[u8] = &data_vec[..]; - let cursor = DataCursor::new(data); + let mut cursor = DataCursor::new(data_vec); let reader = NumberField::::new(4); assert!(reader.is_ok()); let reader = reader.unwrap(); - assert!(cursor.advance(16).is_ok()); - let res = reader.read_from_cursor(&cursor); + assert!(cursor.read(16).is_ok()); + let res = reader.read_from_cursor(&mut cursor); assert!(res.is_ok()); // 125 = 01111101, first four bits as byte => 00000111 = 7 assert_eq!(res.unwrap(), 7); @@ -272,13 +246,12 @@ mod tests { let reader = reader.unwrap(); let data_vec: Vec = vec![100, 6, 125, 179, 152, 113]; - let data: &[u8] = &data_vec[..]; - let cursor = DataCursor::new(data); + let mut cursor = DataCursor::new(data_vec); // [179, 152] is 39091 in u16, reading the first 12 bits and ignoring the last 4 // should yield [10110011 10011000] => [10110011 00001001] => 2483 ( in Little-Endian) - assert!(cursor.advance(24).is_ok()); - let res = reader.read_from_cursor(&cursor); + assert!(cursor.read(24).is_ok()); + let res = reader.read_from_cursor(&mut cursor); assert!(res.is_ok()); assert_eq!(res.unwrap(), 2483); @@ -287,11 +260,10 @@ mod tests { let reader = reader.unwrap(); let data_vec: Vec = vec![154, 6, 125, 179, 152, 113]; - let data: &[u8] = &data_vec[..]; - let cursor = DataCursor::new(data); + let mut cursor = DataCursor::new(data_vec); // 154 is 10011010, reading the first 3 bits should yield 100 = 4 - let res = reader.read_from_cursor(&cursor); + let res = reader.read_from_cursor(&mut cursor); println!("res: {:?}", res); assert!(res.is_ok()); assert_eq!(res.unwrap(), 4); @@ -304,11 +276,10 @@ mod tests { let reader = reader.unwrap(); let data_vec: Vec = vec![100, 6, 111, 138, 152, 113]; - let data: &[u8] = &data_vec[..]; - let cursor = DataCursor::new(data); + let mut cursor = DataCursor::new(data_vec); - assert!(cursor.advance(24).is_ok()); - let res = reader.read_from_cursor(&cursor); + assert!(cursor.read(24).is_ok()); + let res = reader.read_from_cursor(&mut cursor); assert!(res.is_ok()); assert!(matches!(res.unwrap(), MagneticVariationSource::Wmm2020)); } @@ -316,25 +287,24 @@ mod tests { #[test] fn array_field_test() { let data_vec: Vec = vec![100, 6, 111, 77, 152, 113, 42, 42]; - let data: &[u8] = &data_vec[..]; - let cursor = DataCursor::new(data); + let mut cursor = DataCursor::new(data_vec.clone()); let reader = ArrayField::::new(); - assert!(cursor.advance(16).is_ok()); - let res = reader.read_from_cursor(&cursor); + assert!(cursor.read(16).is_ok()); + let res = reader.read_from_cursor(&mut cursor); assert_eq!(res.unwrap(), [111, 77, 152]); - let cursor = DataCursor::new(data); + let mut cursor = DataCursor::new(data_vec.clone()); let reader = ArrayField::::new(); // [6, 11] = 28422, [77, 152] = 38989 - assert!(cursor.advance(8).is_ok()); - let res = reader.read_from_cursor(&cursor); + assert!(cursor.read(8).is_ok()); + let res = reader.read_from_cursor(&mut cursor); assert_eq!(res.unwrap(), [28422, 38989]); - let cursor = DataCursor::new(data); + let mut cursor = DataCursor::new(data_vec); let reader = ArrayField::::new(); // [100, 6, 111, 77] = 1299121764, [152, 113, 42, 42] = 707424664 - let res = reader.read_from_cursor(&cursor); + let res = reader.read_from_cursor(&mut cursor); assert_eq!(res.unwrap(), [1299121764, 707424664]); } @@ -346,26 +316,20 @@ mod tests { } impl FieldSet for TestFieldSet { - fn from_bytes(data: &[u8], current_index: usize) -> Result<(usize, Self), NmeaParseError> { - let a_data: &[u8] = &data[current_index..(current_index + 2)]; - let a = u16::from_le_bytes(a_data.try_into()?); - let b = data[current_index + 2]; - let c_data: &[u8] = &data[(current_index + 3)..(current_index + 5)]; - let c = u16::from_le_bytes(c_data.try_into()?); - - Ok((current_index + 5, TestFieldSet { a, b, c })) - } - fn to_readings( &self, ) -> Result { Err(NmeaParseError::UnsupportedPgn(0)) } - fn from_data(cursor: &super::DataCursor) -> Result { - let a = cursor.read(16)?.try_into()?; - let b = cursor.read(8)?.try_into()?; - let c = cursor.read(16)?.try_into()?; + fn from_data(cursor: &mut super::DataCursor) -> Result { + let u16_reader = NumberField::::default(); + let u8_reader = NumberField::::default(); + + let a = u16_reader.read_from_cursor(cursor)?; + let b = u8_reader.read_from_cursor(cursor)?; + let c = u16_reader.read_from_cursor(cursor)?; + Ok(TestFieldSet { a, b, c }) } } @@ -373,12 +337,11 @@ mod tests { #[test] fn fieldset_field_test() { let data_vec: Vec = vec![100, 6, 111, 77, 152, 113, 42, 42, 1, 2, 3]; - let data: &[u8] = &data_vec[..]; - let cursor = DataCursor::new(data); + let mut cursor = DataCursor::new(data_vec); let reader = FieldSetList::::new(2); - assert!(cursor.advance(8).is_ok()); - let res = reader.read_from_cursor(&cursor); + assert!(cursor.read(8).is_ok()); + let res = reader.read_from_cursor(&mut cursor); assert!(res.is_ok()); let res = res.unwrap(); From 120febade812e144d0e4377356abeb63a5221234 Mon Sep 17 00:00:00 2001 From: Gautham Date: Tue, 21 Jan 2025 16:08:50 -0500 Subject: [PATCH 13/14] other review comments --- micro-rdk-nmea/src/parse_helpers/enums.rs | 35 ++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/micro-rdk-nmea/src/parse_helpers/enums.rs b/micro-rdk-nmea/src/parse_helpers/enums.rs index 4a4b68d96..f48bfcbbc 100644 --- a/micro-rdk-nmea/src/parse_helpers/enums.rs +++ b/micro-rdk-nmea/src/parse_helpers/enums.rs @@ -3,6 +3,9 @@ pub trait NmeaEnumeratedField: Sized + From + ToString {} /// For generating a lookup data type found in an NMEA message. The first argument is the name of the /// enum type that will be generated. Each successive argument is a tuple with /// (raw number value, name of enum instance, string representation) +/// +/// Note: we implement From rather than TryFrom because our equivalent library +/// written in Go does not fail on unrecognized lookups. macro_rules! define_nmea_enum { ( $name:ident, $(($value:expr, $var:ident, $label:expr)),*, $default:ident) => { #[derive(Copy, Clone, Debug)] @@ -43,7 +46,7 @@ define_nmea_enum!( (2, Doppler, "Doppler"), (3, Correlation, "Correlation (ultra sound)"), (4, Electromagnetic, "Electro Magnetic"), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( @@ -72,7 +75,7 @@ define_nmea_enum!( (13, FreezerTemperature, "Freezer Temperature"), (14, ExhaustGasTemperature, "Exhaust Gas Temperature"), (15, ShaftSealTemeprature, "Shaft Seal Temperature"), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( @@ -83,7 +86,7 @@ define_nmea_enum!( (3, LocalCesiumClock, "Local Cesium clock"), (4, LocalRubidiumClock, "Local Rubidium clock"), (5, LocalCrystalClock, "Local Crystal clock"), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( @@ -97,7 +100,7 @@ define_nmea_enum!( (6, Wmm2010, "WMM 2010"), (7, Wmm2015, "WMM 2015"), (8, Wmm2020, "WMM 2020"), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( @@ -107,7 +110,7 @@ define_nmea_enum!( (2, SecondRetransmission, "Second retransmission"), (3, ThirdRetransmission, "Third retransmission"), (4, FinalRetransmission, "Final retransmission"), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( @@ -191,21 +194,21 @@ define_nmea_enum!( PositionReportForLongRangeApplications, "Position report for long range applications" ), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( PositionAccuracy, (0, Low, "Low"), (1, High, "High"), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( RaimFlag, (0, NotInUse, "not in use"), (1, InUse, "in use"), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( @@ -218,7 +221,7 @@ define_nmea_enum!( PositioningSystemIsInoperative, "Positioning system is inoperative" ), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( @@ -233,7 +236,7 @@ define_nmea_enum!( "Own information not broadcast" ), (5, Reserved, "Reserved"), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( @@ -268,7 +271,7 @@ define_nmea_enum!( "Power-driven vessl pushing ahead or towing alongside" ), (14, AisSart, "AIS-SART"), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( @@ -281,7 +284,7 @@ define_nmea_enum!( ), (2, EngagedInSpecialManeuver, "Engaged in special maneuver"), (3, Reserved, "Reserved"), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( @@ -289,7 +292,7 @@ define_nmea_enum!( (0, True, "True"), (1, Magnetic, "Magnetic"), (2, Error, "Error"), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( @@ -303,7 +306,7 @@ define_nmea_enum!( (6, Integrated, "integrated"), (7, Surveyed, "surveyed"), (8, Galileo, "Galileo"), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( @@ -317,7 +320,7 @@ define_nmea_enum!( (6, EstimatedDrMode, "Estimated (DR) mode"), (7, ManualInput, "Manual Input"), (8, SimulateMode, "Simulate mode"), - CouldNotParse + UnknownLookupField ); define_nmea_enum!( @@ -325,5 +328,5 @@ define_nmea_enum!( (0, NoIntegrityChecking, "No integrity checking"), (1, Safe, "Safe"), (2, Caution, "Caution"), - CouldNotParse + UnknownLookupField ); From 6248b5cfe657d6e051816ae389dcf82728fb7aec Mon Sep 17 00:00:00 2001 From: Gautham Date: Wed, 22 Jan 2025 10:50:53 -0500 Subject: [PATCH 14/14] oops --- micro-rdk-nmea/src/parse_helpers/parsers.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/micro-rdk-nmea/src/parse_helpers/parsers.rs b/micro-rdk-nmea/src/parse_helpers/parsers.rs index 7168e5e7f..3d5a1ab19 100644 --- a/micro-rdk-nmea/src/parse_helpers/parsers.rs +++ b/micro-rdk-nmea/src/parse_helpers/parsers.rs @@ -1,8 +1,4 @@ -use std::{ - marker::PhantomData, - rc::Rc, - sync::atomic::{AtomicUsize, Ordering}, -}; +use std::marker::PhantomData; use micro_rdk::common::sensor::GenericReadingsResult; @@ -99,7 +95,7 @@ macro_rules! generate_number_field_readers { impl FieldReader for NumberField<$t> { type FieldType = $t; - fn read_from_cursor(&self, cursor: &mut DataCursor2) -> Result { + fn read_from_cursor(&self, cursor: &mut DataCursor) -> Result { let mut data = cursor.read(self.bit_size)?; let max_size = std::mem::size_of::(); if self.bit_size / 8 > max_size {