-
Notifications
You must be signed in to change notification settings - Fork 96
rust: add bitfield types [DEVINFRA-615] #1092
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
Conversation
bd34ff6
to
c948cc9
Compare
rust/sbp/src/messages/observation.rs
Outdated
NoExclusion = 0, | ||
|
||
/// Measurement was excluded by SPP RAIM, use with care | ||
MeasurementWasExcludedBySppRaimUseWithCare = 1, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some of the names are pretty long. We could have some manually curated set of replacements. Or we could add a name
/identifier
field to the part of the spec that defines bit field values instead of relying on the description
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could also drop the message name prefix part of the enum names if we moved each message into its own module
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have this problem in the C bitfield support too, I suggested the same thing with having a curated or manually specified name-- the problem with that is needing to go through and create these. Maybe we can add this in later, and only do it for names that are particularly long and awkward, then fallback back to generation of the name if the "curated" name doesn't exist.
I think this behavior is fine given the issue with backwards compatibility with regards to panicking, we don't want to break old code because a new bitfield value was introduced. The semantics of bitfields varies too much to have a good default value.
The current set of derives seems fine,
Implementing |
2620d0d
to
5c6e2a1
Compare
Added |
Two other options spring to mind:
Both would allow conserving and reporting any unknown values from parsed messages. I don't think the programs would be able to do much with these values, but it would be useful information to include in an error log message to indicate the version of I think I prefer the second one since it doesn't treat new/unknown bitfield values as an error case. We could still provide a |
62233b2
to
2bb487f
Compare
I've been going thru icbins (which does a like of bitfield stuff) and seeing what it looks like with these changes. A pretty common operation I've noticed is checking that a bitfield value is not equal to one of it's variants (typically an if msg.fix_mode() != Some(FixMode::Invalid) {
...
} But if the unknown type is inside the enum you'd have to do something like if !matches!(msg.fix_mode(), FixMode::Invalid | FixMode::UnknownBitfield(_)) {
...
} Not a big problem, just an observation that it's easier to inspect one variant of a enum wrapped in a type, than it is to check two enum variants. Additionally, in practice it's kind of nice to have the Unknown state as a separate type because you have more type-level info if let Some(fix_mode) = msg.fix_mode() {
// known by the compiler we have a valid fix mode
} vs. let fix_mode = msg.fix_mode();
if !matches!(fix_mode, FixMode::UnknownBitfield(_)) {
// only known by the programmer
} That being said, I do really like the idea of preserving the unknown value, so maybe option 1 is a good choice? I think we could even simplify it a bit and do |
Now that I think about it, maybe the |
Your justification for not including an unknown variant in the enums makes sense. I think it would still be good to include a documentation comment for the getter functions explaining that the "error" variant of the |
Yeah we could do something like /// Gets the [FixMode][FixMode] stored in the `flags` field.
///
/// Returns `Ok` if `flags` holds a known `FixMode`. Otherwise the value of the bitfield is returned
/// in the `Err` variant. This may be because of a malformed message, or because new variants of `FixMode`
/// were added (in which case you should update this library).
pub fn fix_mode(&self) -> Result<FixMode, u8> {
get_bit_range!(self.flags, u8, u8, 2, 0).try_into()
} Probably a better/shorter way to word this, but in general something like that would be easy to set up |
Another quirk I noticed in the spec is that few message have single-bit fields that don't have
Right now this will generate /// Sets the `safe_state_hopl` bitrange of `flags`.
pub fn set_safe_state_hopl(&mut self, safe_state_hopl: u8) {
set_bit_range!(&mut self.flags, safe_state_hopl, u32, u8, 29, 29);
} For single-bit fields we could switch it to accept/return |
Okay so an example of each case - If the spec has /// Gets the [FixMode][self::FixMode] stored in the `flags` bitfield.
///
/// Returns `Ok` if the bitrange contains a known `FixMode` variant.
/// Otherwise the value of the bitrange is returned as an `Err(u8)`. This may be because of a malformed message,
/// or because new variants of `FixMode` were added.
pub fn fix_mode(&self) -> Result<FixMode, u8> {
get_bit_range!(self.flags, u8, u8, 2, 0).try_into()
}
/// Sets the bitrange corresponding to the [FixMode][FixMode] of the `flags` bitfield.
pub fn set_fix_mode(&mut self, fix_mode: FixMode) {
set_bit_range!(&mut self.flags, fix_mode, u8, u8, 2, 0);
}
/// Fix mode
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FixMode {
/// Invalid
Invalid = 0,
/// Single Point Position (SPP)
SinglePointPosition = 1,
/// Differential GNSS (DGNSS)
DifferentialGnss = 2,
/// Float RTK
FloatRtk = 3,
/// Fixed RTK
FixedRtk = 4,
/// Dead Reckoning
DeadReckoning = 5,
/// SBAS Position
SbasPosition = 6,
}
impl std::fmt::Display for FixMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FixMode::Invalid => f.write_str("Invalid"),
FixMode::SinglePointPosition => f.write_str("Single Point Position (SPP)"),
FixMode::DifferentialGnss => f.write_str("Differential GNSS (DGNSS)"),
FixMode::FloatRtk => f.write_str("Float RTK"),
FixMode::FixedRtk => f.write_str("Fixed RTK"),
FixMode::DeadReckoning => f.write_str("Dead Reckoning"),
FixMode::SbasPosition => f.write_str("SBAS Position"),
}
}
}
impl TryFrom<u8> for FixMode {
type Error = u8;
fn try_from(i: u8) -> Result<Self, Self::Error> {
match i {
0 => Ok(FixMode::Invalid),
1 => Ok(FixMode::SinglePointPosition),
2 => Ok(FixMode::DifferentialGnss),
3 => Ok(FixMode::FloatRtk),
4 => Ok(FixMode::FixedRtk),
5 => Ok(FixMode::DeadReckoning),
6 => Ok(FixMode::SbasPosition),
i => Err(i),
}
}
} If the spec has no values and the bitfield's length > 1 /// Gets the `time_since_reference_epoch_in_milliseconds` stored in `tow`.
pub fn time_since_reference_epoch_in_milliseconds(&self) -> u32 {
get_bit_range!(self.tow, u32, u32, 29, 0)
}
/// Sets the `time_since_reference_epoch_in_milliseconds` bitrange of `tow`.
pub fn set_time_since_reference_epoch_in_milliseconds(
&mut self,
time_since_reference_epoch_in_milliseconds: u32,
) {
set_bit_range!(
&mut self.tow,
time_since_reference_epoch_in_milliseconds,
u32,
u32,
29,
0
);
} Should the setter here error is the value is greater than the bitrange would allow? Finally if the bitfield has length one we use bools /// Gets the `velocity_valid` flag.
pub fn velocity_valid(&self) -> bool {
((self.flags >> 21) & 1) == 1
}
/// Sets the `velocity_valid` flag.
pub fn set_velocity_valid(&mut self, velocity_valid: bool) {
self.flags ^= (!(velocity_valid as u32)) & (1 << 21)
} still need to make sure everything works correctly bits make my head hurt |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add an example for each case the ship it?
@jbangelo Any more feedback?
e106503
to
5a7d611
Compare
#define SBP_STATUS_REPORT_SYSTEM_STARLING (0) | ||
#define SBP_STATUS_REPORT_SYSTEM_PRECISION_GNSS_MODULE (1) | ||
#define SBP_STATUS_REPORT_SBP_MAJOR_PROTOCOL_VERSION_NUMBER_MASK (0x1ff) | ||
#define SBP_STATUS_REPORT_SBP_MAJOR_PROTOCOL_VERSION_NUMBER_MASK (0xff) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small error in the spec and just ran the c generator because I think it's the only other language using this feature
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like rust generator needs to run again but otherwise lgtm
3d65d36
to
b060d84
Compare
Something that's been missing from the rust client library is convenience functions around bitfields like in the c client. Pretty sure the generated code is not quite right, but looking to solicit some feedback on the approach generally.
For each message with a bitfield this generates
msg.<field_name>()
andmsg.set_<field_name>(val)
functions on the messages with the bitfield.So depending on whether or there is a set of values to work with, the generated code either produces getters/settings that work on integer types
Or ones that work with the newly defined enum
With that here are some open questions I'm still thinking about
Option<MsgBaselineEcefFixMode>
. I'm foreseeing this being unwrapped everywhere. Alternatives could bepanic
instead of returningNone
. Convenient but less robust. I'm not sure if the spec allows this, but if for example a new fix mode is added, older clients getting a message with that fix mode would panic, whereas right now they'd just returnNone
.Default
for these new types, and replaceNone
withDefault::default()
. This makes sense for types that have a good candidate for a default value likeInvalid
, but not sure all the bitfields have a sensible defaultif msg.fix_mode() == Some(MsgBaselineEcefFixMode::FloatRtk) { ... }
, which isn't so bad I guessDebug, Clone, Copy, PartialEq, Eq
and an impl ofDisplay
that is thedesc
field for the value. We can also implPartialOrd
,Ord
, andHash
if those would be useful (not sureOrd
makes sense semantically for all the different types). Also not sure if theDisplay
is worth keeping. The descriptions aren't super descriptive, but maybe still helpful._
because that was the easiest thing to doTryFrom
impl from the integer types to the enums to make that more convenient.#[inline]
throughout the library at some point. By default non-generic functions are not inlined across crates (my understanding of inlining in rust is basically 100% based on this post). We tend to use LTO for release builds which negates that issue but still might be worth taking a look at.