Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CONSULT-469] - introduction of NMEA message macro #386

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

gvaradarajan
Copy link
Member

This is the first PR actually introducing the procedural macro. As explained in the previous commit, it is recommended that one uses cargo expand in the micro-rdk-nmea directory to view the code that is generated by the macro used in the example PGN structs.

This PR includes

  • parsing logic for the message header
  • logic for parsing a message struct used by the procedural macro itself
  • procedural code generation for messages using number and lookup field types

The next PR will include

  • procedural code generation support for array and fieldset fields (also understood as "repeated fields")

@gvaradarajan gvaradarajan requested a review from a team as a code owner January 22, 2025 17:38
Copy link
Member

@npmenard npmenard left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done with first pass. I like what i am seeing, the code generation is very mechanical and doesn't have too much custom logic.

I see that you generate *_raw function to access fields, was it impossible to reference the field immediately in the code gen? Do you foresee special logic in these functions?Do you mind adding a couple more examples showing up how to use field annotations?
I think conversion is hard to read at the moment, however I recall this is mostly how goNMEA does it so I would leave it as is but add a tickets for potential further work

We should also avoid returning errors in to_readings as much as possible

#max_token
let result = match result {
x if x == max => { return Err(#error_ident::FieldNotPresent(#name_as_string_ident.to_string())); },
x if x == (max - 1) => { return Err(#error_ident::FieldError(#name_as_string_ident.to_string())); },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting, if the field has an invalid value the err will bubble out and to_readings won't return an hashmap. I think we should return a value here, ideally it should be a string like "invalid" or "absent". This may imply that this function will return a value::Kind so that to_reading don't have extra logic to wrap the error in a value::Kind::String.
How is this case handle in gonmea? if this is how it's done then let's leave it as is

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Digging through the GoNMEA logic, it appears that the library does not return a message instance if any number field produces an error value. However, what is missing is that number fields less than 4 bits in size use an alternate logic. I'll update with this change

});
statements
.struct_initialization
.push(quote! { source_id: source_id.unwrap(), });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we should remove the unwrap either make it mandatory or default to a sensible value

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it shouldn't be an option in the first place, will fix

quote! {#crate_ident::google::protobuf}
}

pub(crate) fn determine_supported_numeric(field_type: &Type) -> bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_supported_integer_type

use proc_macro2::Span;
use syn::DeriveInput;

fn get_statements(input: &DeriveInput) -> Result<PgnComposition, TokenStream> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering why you have function that do not take self. It is impossible to have a Struct that holds all statements, fields etc... that can mutate self?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember why, I'll try changing it to an instance method and see if I run into any problems

Copy link
Member

@acmorrow acmorrow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also done with a first pass through

Comment on lines 15 to 16
micro-rdk = { path = "../micro-rdk", features = ["native"] }
micro-rdk-nmea-macros = { path = "../micro-rdk-nmea-macros" }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously micro-rdk came via a workspace = true, which I prefer here. I'd expect the same would work for micro-rdk-nmea-macros if you introduced it in the root Cargo.toml.

micro-rdk-nmea-macros/src/attributes.rs Show resolved Hide resolved
// `MacroAttributes` represents the important information derived from the macro attributes
// attached to the field of a struct using the PgnMessageDerive macro.
//
// bits -> size in bits of the field in the NMEA message byte slice
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe : instead of ->. Also, maybe, `bits`, etc. since you did it for `MacroAttributes` above?

Also, should this be a /// doc comment?

let mut data = Vec::<u8>::new();
let res = general_purpose::STANDARD.decode_vec(water_depth_str, &mut data);
assert!(res.is_ok());
let message = WaterDepth::from_bytes(data[33..].to_vec(), Some(13));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 33 bytes in? What does Some(13) mean here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first part is metadata, I'll put a comment in

micro-rdk-nmea/src/lib.rs Show resolved Hide resolved
Comment on lines 247 to 261
pub fn src(&self) -> u16 {
self.src
}

pub fn dst(&self) -> u16 {
self.dst
}

pub fn priority(&self) -> u16 {
self.priority
}

pub fn timestamp(&self) -> Timestamp {
self.timestamp.clone()
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these trivial accessors needed? Is there some reason that if you had a mut NmeaMessageMetadata that it should be impossible to alter the values?

Copy link
Member Author

@gvaradarajan gvaradarajan Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In principle, I think data coming from a different source should be left alone as much as possible? I think you wouldn't want to alter the data unless it was absolutely necessary for conversion purposes

@@ -213,16 +213,82 @@ where
}
}

// The first 32 bytes of the unparsed message stores metadata appearing in the header
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm having difficulty lining up this comment with what I found at the link, which seems to talk about 39 bits?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should look into this further and not have referenced the link. When examining the raw data that we are currently pushing up (and also looking at the logic in the GoNMEA library), it was clear that the format of the message after processing CAN frames was a 32-byte header followed by the message contents. I admit to a poor understanding of what happens in between receipt of the data from a CAN bus and seeing the data in this format

// this data from the CAN frame). For an explanation of the fields in this header, see
// the section labeled "ISO-11783 and NMEA2000 header" at https://canboat.github.io/canboat/canboat.html
pub struct NmeaMessageMetadata {
timestamp: Timestamp,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why as a proto Timestamp, rather than some non-proto type, something from std, etc.?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, it was laziness. It was already in seconds and milliseconds and it was easier to skip to serializing it for the API immediately

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I'm going to change it to something not proto

RadPerSecToDegPerSec,
}

impl TryFrom<String> for UnitConversion {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It bothers me a lot that impl<T> TryFrom<T> for UnitConversion where T: AsRef<str> { cannot work, per https://users.rust-lang.org/t/confusion-interpreting-error-when-using-tryfrom-with-asref/83672.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah me too


impl TryFrom<String> for UnitConversion {
type Error = TokenStream;
fn try_from(value: String) -> Result<Self, Self::Error> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't actually need to consume the string here, right? Maybe this should be &str?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, you're right, that's better

@gvaradarajan
Copy link
Member Author

Done with first pass. I like what i am seeing, the code generation is very mechanical and doesn't have too much custom logic.

I see that you generate *_raw function to access fields, was it impossible to reference the field immediately in the code gen? Do you foresee special logic in these functions?Do you mind adding a couple more examples showing up how to use field annotations? I think conversion is hard to read at the moment, however I recall this is mostly how goNMEA does it so I would leave it as is but add a tickets for potential further work

We should also avoid returning errors in to_readings as much as possible

@npmenard Regarding the raw accessors: they are not used by the auto-generated code, I included them for ease in debugging potential data issues within a single specific message instance. Should I remove them?

Copy link
Member

@acmorrow acmorrow left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

#[derive(Debug)]
pub(crate) struct MacroAttributes {
pub(crate) bits: Option<usize>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bits shouldn't be optional

Comment on lines 58 to 59
Type::Path(type_path) if type_path.path.is_ident("i64") => 64,
Type::Path(type_path) if type_path.path.is_ident("u64") => 64,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sort these in

Comment on lines 193 to 202
x if x > 4 => quote! {
#max_token
let result = match result {
x if x == max => { return Err(#error_ident::FieldNotPresent(#name_as_string_ident.to_string())); },
x => {
(x as f64) * #scale_token
}
};
},
x if x >= 4 => quote! {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These don't look like they are exclusive

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants