From 09c49dd0c9d04cb2f6983c76cf51659571f2a5e8 Mon Sep 17 00:00:00 2001 From: Ralph Ursprung Date: Sun, 29 Sep 2024 19:08:38 +0200 Subject: [PATCH 1/7] simplify GHA required build settings by adding a single `build-results` job which depends on all other jobs we can simplify the setting of required builds in the repository. currently, all builds - including all variations of the build matrix! - need to be manually specified. once this has been merged the settings can be changed to require only this one job (which will fail if any of the other jobs failed). this way it's also easier to add/remove jobs or change the build matrix as it no longer requires changing the settings on the repository. this is inspired by [this discussion on GH][discussion]. [discussion]: https://github.com/orgs/community/discussions/26822 --- .github/workflows/CI.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 51085c6..6d3734e 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -87,3 +87,17 @@ jobs: with: sarif_file: examples/stm32f4-event-printer/rust-clippy-results.sarif wait-for-processing: true + + # simplify GH settings: have one single build to be required + build-results: + name: Final Results + if: ${{ always() }} + runs-on: ubuntu-latest + needs: [lib, stm32f4-event-printer] + steps: + - name: check for failed builds of the library + if: ${{ needs.lib.result != 'success' }} + run: exit 1 + - name: check for failed builds of the example + if: ${{ needs.stm32f4-event-printer.result != 'success' }} + run: exit 1 From abe2f20a3be0339febe00c899edd3d4a015b5efd Mon Sep 17 00:00:00 2001 From: Ralph Ursprung Date: Sun, 29 Sep 2024 19:48:06 +0200 Subject: [PATCH 2/7] implement `core::error::Error` this API has been stabilised with Rust 1.81.0. accordingly the MSRV has been raised. --- .github/workflows/CI.yml | 6 ++-- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- README.md | 2 +- src/button_event.rs | 14 +++++++++ src/lib.rs | 66 +++++++++++++++++++++++++++++++++++++--- 6 files changed, 82 insertions(+), 10 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6d3734e..22abb5f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,12 +13,12 @@ jobs: strategy: fail-fast: false matrix: - rust: [1.62.0, stable] + rust: [1.81.0, stable] features: ['use_alloc', 'use_alloc,defmt', 'use_heapless', 'use_heapless,defmt'] exclude: - - rust: 1.62.0 + - rust: 1.81.0 features: 'use_alloc,defmt' - - rust: 1.62.0 + - rust: 1.81.0 features: 'use_heapless,defmt' runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e37b31..cc6ed66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - ReleaseDate +### Changed +* The MSRV has been updated to 1.81.0 due to `core::error::Error` being implemented ## [0.2.0] - 2023-11-14 ### Added diff --git a/Cargo.toml b/Cargo.toml index 39c3a76..3797aae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "adafruit-bluefruit-protocol" version = "0.2.0" edition = "2021" -rust-version = "1.62" +rust-version = "1.81" description = "A `no_std` parser for the Adafruit Bluefruit LE Connect controller protocol." repository = "https://github.com/rust-embedded-community/adafruit-bluefruit-protocol-rs" diff --git a/README.md b/README.md index 47d6fd3..bf607be 100644 --- a/README.md +++ b/README.md @@ -30,5 +30,5 @@ A simple example for the STM32F4 microcontrollers is [available](examples/stm32f For the changelog please see the dedicated [CHANGELOG.md](CHANGELOG.md). ## Minimum Supported Rust Version (MSRV) -This crate is guaranteed to compile on stable Rust 1.62 and up. It *might* +This crate is guaranteed to compile on stable Rust 1.81 and up. It *might* compile with older versions but that may change in any new patch release. diff --git a/src/button_event.rs b/src/button_event.rs index 0cae4e3..3e07f37 100644 --- a/src/button_event.rs +++ b/src/button_event.rs @@ -1,6 +1,8 @@ //! Implements the [`ButtonEvent`] and its parsing from the protocol. use super::ProtocolParseError; +use core::error::Error; +use core::fmt::{Display, Formatter}; /// Errors which can be raised while parsing a button event. #[derive(PartialEq, Eq, Debug)] @@ -12,6 +14,18 @@ pub enum ButtonParseError { UnknownButtonState(u8), } +impl Display for ButtonParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + use ButtonParseError::*; + match self { + UnknownButton(button) => write!(f, "Unknown button: {:#x}", button), + UnknownButtonState(state) => write!(f, "Unknown button state: {:#x}", state), + } + } +} + +impl Error for ButtonParseError {} + /// Lists all possible buttons which can be sent in the event. #[derive(PartialEq, Eq, Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] diff --git a/src/lib.rs b/src/lib.rs index 6a1fcac..8452bd3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,6 +66,8 @@ use heapless::Vec; extern crate alloc; #[cfg(feature = "use_alloc")] use alloc::vec::Vec; +use core::error::Error; +use core::fmt::{Display, Formatter}; #[cfg(feature = "location_event")] use location_event::LocationEvent; #[cfg(feature = "magnetometer_event")] @@ -101,8 +103,7 @@ pub enum ProtocolParseError { /// The message contained an event which is not known to the current implementation. /// This can mean that: /// * the message was malformed or - /// * that a newer protocol version has been used or - /// * that the event type has not been enabled as a feature. + /// * that a newer protocol version has been used. UnknownEvent(Option), /// The message contained an event which is known to the library but has not been selected as a feature and can thus not be parsed. Select the feature when compiling the library to handle this message. DisabledControllerDataPackageType(ControllerDataPackageType), @@ -117,6 +118,44 @@ pub enum ProtocolParseError { InvalidFloatSize(usize), } +impl Display for ProtocolParseError { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + use ProtocolParseError::*; + match self { + UnknownEvent(event) => write!(f, "Unknown event type: {:?}", event), + DisabledControllerDataPackageType(event) => { + write!(f, "Disabled event type: {:?}", event) + } + ButtonParseError(_) => write!(f, "Error while parsing button event"), + InvalidLength(expected, actual) => write!( + f, + "Invalid message length: expected {} but received {}", + expected, actual + ), + InvalidCrc(expected, actual) => write!( + f, + "Invalid CRC: expected {:#x} but calculated {:#x}", + expected, actual + ), + InvalidFloatSize(length) => write!( + f, + "Failed to parse float from a message with size {}", + length + ), + } + } +} + +impl Error for ProtocolParseError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + use ProtocolParseError::*; + match self { + ButtonParseError(e) => Some(e), + _ => None, + } + } +} + /// Lists all data packages which can be sent by the controller. Internal state used during parsing. Use [`ControllerEvent`] to return the actual event. #[derive(PartialEq, Eq, Debug)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] @@ -340,7 +379,7 @@ fn try_f32_from_le_bytes(input: &[u8]) -> Result { #[cfg(test)] mod tests { - use crate::button_event::{Button, ButtonState}; + use crate::button_event::{Button, ButtonParseError, ButtonState}; use crate::{check_crc, parse, try_f32_from_le_bytes, ControllerEvent, ProtocolParseError}; fn assert_is_button_event( @@ -359,16 +398,33 @@ mod tests { #[test] fn test_parse() { - let input = b"\x00!B11:!B10;\x00\x00!\x00\x00\x00\x00"; + let input = b"\x00!B11:!B10;\x00\x00!\x00\x00\x00\x00!B138"; #[cfg(feature = "use_heapless")] let result = parse::<4>(input); #[cfg(feature = "use_alloc")] let result = parse(input); - assert_eq!(result.len(), 3); + assert_eq!(result.len(), 4); assert_is_button_event(&result[0], Button::Button1, ButtonState::Pressed); assert_is_button_event(&result[1], Button::Button1, ButtonState::Released); assert_eq!(result[2], Err(ProtocolParseError::UnknownEvent(Some(0)))); + if let Err(e) = &result[3] { + assert_eq!( + e, + &ProtocolParseError::ButtonParseError(ButtonParseError::UnknownButtonState(b'3')) + ); + #[cfg(feature = "use_alloc")] + { + use alloc::string::ToString; + use core::error::Error; + assert_eq!( + e.source().unwrap().to_string(), + "Unknown button state: 0x33" + ); + } + } else { + assert!(false, "expected an error"); + } } #[test] From 4dab854b3a576a77b481185c05d1164e50e8de0c Mon Sep 17 00:00:00 2001 From: Ralph Ursprung Date: Mon, 30 Sep 2024 19:08:37 +0200 Subject: [PATCH 3/7] rename `use_..` features this resolves the [C-FEATURE] finding from the API guidelines which states: > Do not include words in the name of a Cargo feature that convey zero > meaning, as in `use-abc` or `with-abc`. Name the feature `abc` > directly. note that this is a breaking change for existing consumers which must rename their usage of the feature. [C-FEATURE]: https://rust-lang.github.io/api-guidelines/naming.html#c-feature --- .github/workflows/CI.yml | 6 ++-- CHANGELOG.md | 1 + Cargo.toml | 4 +-- README.md | 4 +-- examples/stm32f4-event-printer/Cargo.toml | 2 +- src/lib.rs | 34 +++++++++++------------ 6 files changed, 26 insertions(+), 25 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 22abb5f..af3d531 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -14,12 +14,12 @@ jobs: fail-fast: false matrix: rust: [1.81.0, stable] - features: ['use_alloc', 'use_alloc,defmt', 'use_heapless', 'use_heapless,defmt'] + features: ['alloc', 'alloc,defmt', 'heapless', 'heapless,defmt'] exclude: - rust: 1.81.0 - features: 'use_alloc,defmt' + features: 'alloc,defmt' - rust: 1.81.0 - features: 'use_heapless,defmt' + features: 'heapless,defmt' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index cc6ed66..9213922 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - ReleaseDate ### Changed * The MSRV has been updated to 1.81.0 due to `core::error::Error` being implemented +* **BREAKING**: the features `use_alloc` and `use_heapless` have been renamed to `alloc` and `heapless` respectively. ## [0.2.0] - 2023-11-14 ### Added diff --git a/Cargo.toml b/Cargo.toml index 3797aae..a3b78c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,8 @@ serde = { version = "1.0", features = ["derive"], optional = true } [features] default = ["accelerometer_event", "button_event", "color_event", "gyro_event", "location_event", "magnetometer_event", "quaternion_event"] -use_heapless = ["dep:heapless"] -use_alloc = [] +heapless = ["dep:heapless"] +alloc = [] defmt = ["dep:defmt", "heapless?/defmt-03"] diff --git a/README.md b/README.md index bf607be..4803bb8 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Note that this work is not affiliated with Adafruit. ## Mandatory Features This crate is `no_std` and you can choose whether you want to use -[`heapless::Vec`](https://docs.rs/heapless/0.8.0/heapless/struct.Vec.html) by selecting the feature `use_heapless` or -[`alloc::vec::Vec`](https://doc.rust-lang.org/alloc/vec/struct.Vec.html) by selecting the feature `use_alloc`. +[`heapless::Vec`](https://docs.rs/heapless/0.8.0/heapless/struct.Vec.html) by selecting the feature `heapless` or +[`alloc::vec::Vec`](https://doc.rust-lang.org/alloc/vec/struct.Vec.html) by selecting the feature `alloc`. If you select neither or both you'll get a compile error. ## Optional Features diff --git a/examples/stm32f4-event-printer/Cargo.toml b/examples/stm32f4-event-printer/Cargo.toml index 6b6afb4..6091d84 100644 --- a/examples/stm32f4-event-printer/Cargo.toml +++ b/examples/stm32f4-event-printer/Cargo.toml @@ -17,7 +17,7 @@ defmt = "0.3.6" defmt-rtt = "0.4" # use `adafruit-bluefruit-protocol = "0.1"` in reality; path used here to ensure that the example always compiles against the latest master -adafruit-bluefruit-protocol = { path = "../..", features = ["defmt", "use_heapless"] } +adafruit-bluefruit-protocol = { path = "../..", features = ["defmt", "heapless"] } [profile.release] codegen-units = 1 diff --git a/src/lib.rs b/src/lib.rs index 8452bd3..9628358 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,11 +29,11 @@ )))] compile_error!("at least one event type must be selected in the features!"); -#[cfg(not(any(feature = "use_alloc", feature = "use_heapless")))] -compile_error!("you must choose either 'use_alloc' or 'use_heapless' as a feature!"); +#[cfg(not(any(feature = "alloc", feature = "heapless")))] +compile_error!("you must choose either 'alloc' or 'heapless' as a feature!"); -#[cfg(all(feature = "use_alloc", feature = "use_heapless"))] -compile_error!("you must choose either 'use_alloc' or 'use_heapless' as a feature but not both!"); +#[cfg(all(feature = "alloc", feature = "heapless"))] +compile_error!("you must choose either 'alloc' or 'heapless' as a feature but not both!"); #[cfg(feature = "accelerometer_event")] pub mod accelerometer_event; @@ -59,12 +59,12 @@ use color_event::ColorEvent; use core::cmp::min; #[cfg(feature = "gyro_event")] use gyro_event::GyroEvent; -#[cfg(feature = "use_heapless")] +#[cfg(feature = "heapless")] use heapless::Vec; -#[cfg(feature = "use_alloc")] +#[cfg(feature = "alloc")] extern crate alloc; -#[cfg(feature = "use_alloc")] +#[cfg(feature = "alloc")] use alloc::vec::Vec; use core::error::Error; use core::fmt::{Display, Formatter}; @@ -207,17 +207,17 @@ impl TryFrom for ControllerDataPackageType { } } -#[cfg(feature = "use_heapless")] +#[cfg(feature = "heapless")] type ParseResult = Vec, MAX_RESULTS>; -#[cfg(feature = "use_alloc")] +#[cfg(feature = "alloc")] type ParseResult = Vec>; -#[cfg(feature = "use_alloc")] +#[cfg(feature = "alloc")] const MAX_RESULTS: usize = 0; /// Parse the input string for commands. Unexpected content will be ignored if it's not formatted like a command! -pub fn parse<#[cfg(feature = "use_heapless")] const MAX_RESULTS: usize>( +pub fn parse<#[cfg(feature = "heapless")] const MAX_RESULTS: usize>( input: &[u8], ) -> ParseResult { /// Simple state machine for the parser, represents whether the parser is seeking a start or has found it. @@ -239,11 +239,11 @@ pub fn parse<#[cfg(feature = "use_heapless")] const MAX_RESULTS: usize>( } ParserState::ParseCommand => { let data_package = extract_and_parse_command(&input[(pos - 1)..]); - #[cfg(feature = "use_alloc")] + #[cfg(feature = "alloc")] result.push(data_package); - #[cfg(feature = "use_heapless")] + #[cfg(feature = "heapless")] result.push(data_package).ok(); - #[cfg(feature = "use_heapless")] + #[cfg(feature = "heapless")] if result.len() == MAX_RESULTS { return result; } @@ -399,9 +399,9 @@ mod tests { #[test] fn test_parse() { let input = b"\x00!B11:!B10;\x00\x00!\x00\x00\x00\x00!B138"; - #[cfg(feature = "use_heapless")] + #[cfg(feature = "heapless")] let result = parse::<4>(input); - #[cfg(feature = "use_alloc")] + #[cfg(feature = "alloc")] let result = parse(input); assert_eq!(result.len(), 4); @@ -413,7 +413,7 @@ mod tests { e, &ProtocolParseError::ButtonParseError(ButtonParseError::UnknownButtonState(b'3')) ); - #[cfg(feature = "use_alloc")] + #[cfg(feature = "alloc")] { use alloc::string::ToString; use core::error::Error; From 29cca9079db4267433c05b1e5a16b95a36783d14 Mon Sep 17 00:00:00 2001 From: Ralph Ursprung Date: Mon, 30 Sep 2024 19:28:55 +0200 Subject: [PATCH 4/7] add `Copy`, `Clone` and `Hash` on error & event types note that `Hash` cannot be added to types which use floats underneath. for the same reason `Eq` cannot be added (but `PartialEq` is present). this resolves [C-COMMON-TRAITS] of the API guidelines. [C-COMMON-TRAITS]: https://rust-lang.github.io/api-guidelines/interoperability.html#c-common-traits --- CHANGELOG.md | 2 ++ src/accelerometer_event.rs | 2 +- src/button_event.rs | 8 ++++---- src/color_event.rs | 2 +- src/gyro_event.rs | 2 +- src/lib.rs | 6 +++--- src/location_event.rs | 2 +- src/magnetometer_event.rs | 2 +- src/quaternion_event.rs | 2 +- 9 files changed, 15 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9213922..4a503bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - ReleaseDate +### Added +* `Copy`, `Clone` and `Hash` on error & event types (where possible) ### Changed * The MSRV has been updated to 1.81.0 due to `core::error::Error` being implemented * **BREAKING**: the features `use_alloc` and `use_heapless` have been renamed to `alloc` and `heapless` respectively. diff --git a/src/accelerometer_event.rs b/src/accelerometer_event.rs index 51f5fef..88ceda8 100644 --- a/src/accelerometer_event.rs +++ b/src/accelerometer_event.rs @@ -3,7 +3,7 @@ use super::{try_f32_from_le_bytes, ProtocolParseError}; /// Represents an accelerometer event from the protocol. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Copy, Clone)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] // the names are already obvious enough diff --git a/src/button_event.rs b/src/button_event.rs index 3e07f37..311e0fe 100644 --- a/src/button_event.rs +++ b/src/button_event.rs @@ -5,7 +5,7 @@ use core::error::Error; use core::fmt::{Display, Formatter}; /// Errors which can be raised while parsing a button event. -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Hash, Clone, Copy)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ButtonParseError { /// The message contained an unknown button. For the known buttons see [`Button`]. @@ -27,7 +27,7 @@ impl Display for ButtonParseError { impl Error for ButtonParseError {} /// Lists all possible buttons which can be sent in the event. -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[allow(missing_docs)] // the names are already obvious enough pub enum Button { @@ -59,7 +59,7 @@ impl Button { } /// The state of the button. -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] // the names are already obvious enough @@ -80,7 +80,7 @@ impl ButtonState { } /// Represents a button event from the protocol. -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[allow(missing_docs)] // the names are already obvious enough pub struct ButtonEvent { diff --git a/src/color_event.rs b/src/color_event.rs index 386f4d6..0f1070b 100644 --- a/src/color_event.rs +++ b/src/color_event.rs @@ -5,7 +5,7 @@ use super::ProtocolParseError; use rgb::RGB8; /// Represents a color event from the protocol. -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] // the names are already obvious enough diff --git a/src/gyro_event.rs b/src/gyro_event.rs index a081fde..6c2ee51 100644 --- a/src/gyro_event.rs +++ b/src/gyro_event.rs @@ -3,7 +3,7 @@ use super::{try_f32_from_le_bytes, ProtocolParseError}; /// Represents a gyro event from the protocol. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Copy, Clone)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] // the names are already obvious enough diff --git a/src/lib.rs b/src/lib.rs index 9628358..a977292 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,7 +76,7 @@ use magnetometer_event::MagnetometerEvent; use quaternion_event::QuaternionEvent; /// Lists all (supported) events which can be sent by the controller. These come with the parsed event data and are the result of a [`parse`] call. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Copy, Clone)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[allow(missing_docs)] // the names are already obvious enough pub enum ControllerEvent { @@ -97,7 +97,7 @@ pub enum ControllerEvent { } /// Represents the different kinds of errors which can happen when the protocol is being parsed. -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Copy, Clone, Hash)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub enum ProtocolParseError { /// The message contained an event which is not known to the current implementation. @@ -157,7 +157,7 @@ impl Error for ProtocolParseError { } /// Lists all data packages which can be sent by the controller. Internal state used during parsing. Use [`ControllerEvent`] to return the actual event. -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Hash, Clone, Copy)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[allow(missing_docs)] // the names are already obvious enough pub enum ControllerDataPackageType { diff --git a/src/location_event.rs b/src/location_event.rs index 8095dc7..cd798b2 100644 --- a/src/location_event.rs +++ b/src/location_event.rs @@ -3,7 +3,7 @@ use super::{try_f32_from_le_bytes, ProtocolParseError}; /// Represents a location event from the protocol. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Copy, Clone)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] // the names are already obvious enough diff --git a/src/magnetometer_event.rs b/src/magnetometer_event.rs index b4fd063..56bc7cf 100644 --- a/src/magnetometer_event.rs +++ b/src/magnetometer_event.rs @@ -3,7 +3,7 @@ use super::{try_f32_from_le_bytes, ProtocolParseError}; /// Represents a magnetometer event from the protocol. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Copy, Clone)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] // the names are already obvious enough diff --git a/src/quaternion_event.rs b/src/quaternion_event.rs index 379eb2c..640c887 100644 --- a/src/quaternion_event.rs +++ b/src/quaternion_event.rs @@ -3,7 +3,7 @@ use super::{try_f32_from_le_bytes, ProtocolParseError}; /// Represents a [quaternion](https://en.wikipedia.org/wiki/Quaternion) event from the protocol. -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Copy, Clone)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[allow(missing_docs)] // the names are already obvious enough From a7ec442d316fd5fa50997e7acf68faed3aa6c878 Mon Sep 17 00:00:00 2001 From: Ralph Ursprung Date: Mon, 30 Sep 2024 20:11:26 +0200 Subject: [PATCH 5/7] document that `parse` is the entry point to be used --- README.md | 3 +++ src/lib.rs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/README.md b/README.md index 4803bb8..2f2d186 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ If you select neither or both you'll get a compile error. but you can opt to only select the event(s) you are interested in which will result in a small binary size. If other events are received, a `ProtocolParseError::DisabledControllerDataPackageType` will be returned. +## Usage +The entry point to use this crate is the `parse` function. + ## Examples A simple example for the STM32F4 microcontrollers is [available](examples/stm32f4-event-printer/README.md). diff --git a/src/lib.rs b/src/lib.rs index a977292..6824b5e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ //! This implements the [Adafruit Bluefruit LE Connect controller protocol](https://learn.adafruit.com/bluefruit-le-connect/controller) //! which is e.g. used by the [Adafruit Bluefruit LE UART Friend](https://learn.adafruit.com/introducing-the-adafruit-bluefruit-le-uart-friend). //! +//! The entry point to use this crate is the [`parse`] function. +//! //! ## Optional features //! * `defmt`: you can enable the `defmt` feature to get a `defmt::Format` implementation for all structs & enums and a `defmt::debug!` call for each command being parsed. //! * `rgb`: if enabled, `From for RGB8` is implemented to support the [RGB crate](https://crates.io/crates/rgb). From 339f7e7cf397189b9d97f4392afd1b6ee33a993b Mon Sep 17 00:00:00 2001 From: Ralph Ursprung Date: Mon, 30 Sep 2024 20:35:35 +0200 Subject: [PATCH 6/7] document the "sans I/O" behaviour of this crate --- README.md | 3 ++- src/lib.rs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2f2d186..0df23a1 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ If you select neither or both you'll get a compile error. If other events are received, a `ProtocolParseError::DisabledControllerDataPackageType` will be returned. ## Usage -The entry point to use this crate is the `parse` function. +The entry point to use this crate is the `parse` function. Note that this is a [sans I/O](https://sans-io.readthedocs.io/) +crate, i.e. you have to talk to the Adafruit device, the `parse` function just expects a byte sequence. ## Examples A simple example for the STM32F4 microcontrollers is [available](examples/stm32f4-event-printer/README.md). diff --git a/src/lib.rs b/src/lib.rs index 6824b5e..337e02a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,8 @@ //! This implements the [Adafruit Bluefruit LE Connect controller protocol](https://learn.adafruit.com/bluefruit-le-connect/controller) //! which is e.g. used by the [Adafruit Bluefruit LE UART Friend](https://learn.adafruit.com/introducing-the-adafruit-bluefruit-le-uart-friend). //! -//! The entry point to use this crate is the [`parse`] function. +//! The entry point to use this crate is the [`parse`] function. Note that this is a [sans I/O](https://sans-io.readthedocs.io/) +//! crate, i.e. you have to talk to the Adafruit device, the `parse` function just expects a byte sequence. //! //! ## Optional features //! * `defmt`: you can enable the `defmt` feature to get a `defmt::Format` implementation for all structs & enums and a `defmt::debug!` call for each command being parsed. From 6240970146ea02b95db56deafbd23b86b525bf50 Mon Sep 17 00:00:00 2001 From: Ralph Ursprung Date: Sun, 13 Oct 2024 00:07:57 +0200 Subject: [PATCH 7/7] rework API to use implement `Iterator` this completely revamps the API. the `parse` function is gone, instead you now construct a new `Parser` using `Parser::new` and then use the iterator API on it which will yield a `next` until the whole input has been parsed. due to this there's now no longer a need to return a list of values from the API as instead we return the individual events (or errors) step by step. thus the dependency on `alloc` and `heapless` has been removed. to get the same result as before (everything collected into a vector) the `collect` API of `Iterator` can be used by consumers. --- .github/workflows/CI.yml | 15 +- CHANGELOG.md | 3 +- Cargo.toml | 6 +- README.md | 10 +- examples/stm32f4-event-printer/Cargo.lock | 25 +-- examples/stm32f4-event-printer/Cargo.toml | 2 +- .../src/adafruit_bluefruit_le_uart_friend.rs | 6 +- src/lib.rs | 209 +++++++++--------- 8 files changed, 122 insertions(+), 154 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index af3d531..04a8783 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -14,12 +14,7 @@ jobs: fail-fast: false matrix: rust: [1.81.0, stable] - features: ['alloc', 'alloc,defmt', 'heapless', 'heapless,defmt'] - exclude: - - rust: 1.81.0 - features: 'alloc,defmt' - - rust: 1.81.0 - features: 'heapless,defmt' + features: [''] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -31,17 +26,17 @@ jobs: - name: Install required cargo components run: cargo +stable install clippy-sarif sarif-fmt - name: build - run: cargo build --features ${{ matrix.features }} + run: cargo build ${{ matrix.features }} - name: check - run: cargo check --features ${{ matrix.features }} + run: cargo check ${{ matrix.features }} - name: test - run: cargo test --features ${{ matrix.features }} + run: cargo test ${{ matrix.features }} - name: check formatting run: cargo fmt --all -- --check - name: audit run: cargo audit - name: clippy (lib) - run: cargo clippy --features ${{ matrix.features }} --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt + run: cargo clippy ${{ matrix.features }} --message-format=json | clippy-sarif | tee rust-clippy-results.sarif | sarif-fmt continue-on-error: true - name: Upload analysis results to GitHub uses: github/codeql-action/upload-sarif@v3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a503bc..fdca254 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `Copy`, `Clone` and `Hash` on error & event types (where possible) ### Changed * The MSRV has been updated to 1.81.0 due to `core::error::Error` being implemented -* **BREAKING**: the features `use_alloc` and `use_heapless` have been renamed to `alloc` and `heapless` respectively. +* **BREAKING**: The `parse` API has been replaced with `Parser::new` where `Parser` now implements `Iterator` and the `next` function returns each parsed command + * Accordingly, the features `use_alloc` and `use_heapless` have been removed. ## [0.2.0] - 2023-11-14 ### Added diff --git a/Cargo.toml b/Cargo.toml index a3b78c7..10c72e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ license = "MIT OR Apache-2.0" [dependencies] defmt = { version = "0.3", optional = true } -heapless = { version = "0.8", optional = true } rgb = { version = "0.8", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } @@ -20,10 +19,7 @@ serde = { version = "1.0", features = ["derive"], optional = true } [features] default = ["accelerometer_event", "button_event", "color_event", "gyro_event", "location_event", "magnetometer_event", "quaternion_event"] -heapless = ["dep:heapless"] -alloc = [] - -defmt = ["dep:defmt", "heapless?/defmt-03"] +defmt = ["dep:defmt"] accelerometer_event = [] button_event = [] diff --git a/README.md b/README.md index 0df23a1..f192079 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,6 @@ which is e.g. used by the [Adafruit Bluefruit LE UART Friend](https://learn.adaf Note that this work is not affiliated with Adafruit. -## Mandatory Features -This crate is `no_std` and you can choose whether you want to use -[`heapless::Vec`](https://docs.rs/heapless/0.8.0/heapless/struct.Vec.html) by selecting the feature `heapless` or -[`alloc::vec::Vec`](https://doc.rust-lang.org/alloc/vec/struct.Vec.html) by selecting the feature `alloc`. -If you select neither or both you'll get a compile error. - ## Optional Features * `defmt`: you can enable the [`defmt`](https://defmt.ferrous-systems.com/) feature to get a `defmt::Format` implementation for all structs & enums and a `defmt::debug!` call for each command being parsed. * `rgb`: if enabled, `From for RGB8` is implemented to support the [RGB crate](https://crates.io/crates/rgb). @@ -24,8 +18,8 @@ If you select neither or both you'll get a compile error. If other events are received, a `ProtocolParseError::DisabledControllerDataPackageType` will be returned. ## Usage -The entry point to use this crate is the `parse` function. Note that this is a [sans I/O](https://sans-io.readthedocs.io/) -crate, i.e. you have to talk to the Adafruit device, the `parse` function just expects a byte sequence. +The entry point to use this crate is `Parser` which implements `Iterator` to access the events in the input. +Note that this is a [sans I/O](https://sans-io.readthedocs.io/) crate, i.e. you have to talk to the Adafruit device, the parser just expects a byte sequence. ## Examples A simple example for the STM32F4 microcontrollers is [available](examples/stm32f4-event-printer/README.md). diff --git a/examples/stm32f4-event-printer/Cargo.lock b/examples/stm32f4-event-printer/Cargo.lock index 8df482e..0c76744 100644 --- a/examples/stm32f4-event-printer/Cargo.lock +++ b/examples/stm32f4-event-printer/Cargo.lock @@ -7,7 +7,6 @@ name = "adafruit-bluefruit-protocol" version = "0.2.0" dependencies = [ "defmt", - "heapless 0.8.0", ] [[package]] @@ -100,7 +99,7 @@ dependencies = [ "bare-metal 1.0.0", "cortex-m", "cortex-m-rtic-macros", - "heapless 0.7.17", + "heapless", "rtic-core", "rtic-monotonic", "version_check", @@ -271,15 +270,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "hash32" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" -dependencies = [ - "byteorder", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -293,23 +283,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" dependencies = [ "atomic-polyfill", - "hash32 0.2.1", + "hash32", "rustc_version 0.4.0", "spin", "stable_deref_trait", ] -[[package]] -name = "heapless" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" -dependencies = [ - "defmt", - "hash32 0.3.1", - "stable_deref_trait", -] - [[package]] name = "indexmap" version = "1.9.3" diff --git a/examples/stm32f4-event-printer/Cargo.toml b/examples/stm32f4-event-printer/Cargo.toml index 6091d84..d14a281 100644 --- a/examples/stm32f4-event-printer/Cargo.toml +++ b/examples/stm32f4-event-printer/Cargo.toml @@ -17,7 +17,7 @@ defmt = "0.3.6" defmt-rtt = "0.4" # use `adafruit-bluefruit-protocol = "0.1"` in reality; path used here to ensure that the example always compiles against the latest master -adafruit-bluefruit-protocol = { path = "../..", features = ["defmt", "heapless"] } +adafruit-bluefruit-protocol = { path = "../..", features = ["defmt"] } [profile.release] codegen-units = 1 diff --git a/examples/stm32f4-event-printer/src/adafruit_bluefruit_le_uart_friend.rs b/examples/stm32f4-event-printer/src/adafruit_bluefruit_le_uart_friend.rs index 0b72269..940d3cd 100644 --- a/examples/stm32f4-event-printer/src/adafruit_bluefruit_le_uart_friend.rs +++ b/examples/stm32f4-event-printer/src/adafruit_bluefruit_le_uart_friend.rs @@ -84,8 +84,10 @@ impl BluefruitLEUARTFriend { filled_buffer ); - let event = adafruit_bluefruit_protocol::parse::<4>(filled_buffer); - defmt::info!("received event(s) over bluetooth: {}", &event); + let parser = adafruit_bluefruit_protocol::Parser::new(filled_buffer); + for event in parser { + defmt::info!("received event over bluetooth: {:?}", &event); + } // switch out the buffers filled_buffer.fill(0); diff --git a/src/lib.rs b/src/lib.rs index 337e02a..5361df4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,8 @@ //! This implements the [Adafruit Bluefruit LE Connect controller protocol](https://learn.adafruit.com/bluefruit-le-connect/controller) //! which is e.g. used by the [Adafruit Bluefruit LE UART Friend](https://learn.adafruit.com/introducing-the-adafruit-bluefruit-le-uart-friend). //! -//! The entry point to use this crate is the [`parse`] function. Note that this is a [sans I/O](https://sans-io.readthedocs.io/) -//! crate, i.e. you have to talk to the Adafruit device, the `parse` function just expects a byte sequence. +//! The entry point to use this crate is [`Parser`]. Note that this is a [sans I/O](https://sans-io.readthedocs.io/) +//! crate, i.e. you have to talk to the Adafruit device, the parser just expects a byte sequence. //! //! ## Optional features //! * `defmt`: you can enable the `defmt` feature to get a `defmt::Format` implementation for all structs & enums and a `defmt::debug!` call for each command being parsed. @@ -32,12 +32,6 @@ )))] compile_error!("at least one event type must be selected in the features!"); -#[cfg(not(any(feature = "alloc", feature = "heapless")))] -compile_error!("you must choose either 'alloc' or 'heapless' as a feature!"); - -#[cfg(all(feature = "alloc", feature = "heapless"))] -compile_error!("you must choose either 'alloc' or 'heapless' as a feature but not both!"); - #[cfg(feature = "accelerometer_event")] pub mod accelerometer_event; #[cfg(feature = "button_event")] @@ -62,13 +56,7 @@ use color_event::ColorEvent; use core::cmp::min; #[cfg(feature = "gyro_event")] use gyro_event::GyroEvent; -#[cfg(feature = "heapless")] -use heapless::Vec; -#[cfg(feature = "alloc")] -extern crate alloc; -#[cfg(feature = "alloc")] -use alloc::vec::Vec; use core::error::Error; use core::fmt::{Display, Formatter}; #[cfg(feature = "location_event")] @@ -78,7 +66,7 @@ use magnetometer_event::MagnetometerEvent; #[cfg(feature = "quaternion_event")] use quaternion_event::QuaternionEvent; -/// Lists all (supported) events which can be sent by the controller. These come with the parsed event data and are the result of a [`parse`] call. +/// Lists all (supported) events which can be sent by the controller. These come with the parsed event data. #[derive(PartialEq, Debug, Copy, Clone)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[allow(missing_docs)] // the names are already obvious enough @@ -210,52 +198,111 @@ impl TryFrom for ControllerDataPackageType { } } -#[cfg(feature = "heapless")] -type ParseResult = - Vec, MAX_RESULTS>; - -#[cfg(feature = "alloc")] -type ParseResult = Vec>; -#[cfg(feature = "alloc")] -const MAX_RESULTS: usize = 0; - -/// Parse the input string for commands. Unexpected content will be ignored if it's not formatted like a command! -pub fn parse<#[cfg(feature = "heapless")] const MAX_RESULTS: usize>( - input: &[u8], -) -> ParseResult { - /// Simple state machine for the parser, represents whether the parser is seeking a start or has found it. - enum ParserState { - SeekStart, - ParseCommand, +/// Parse the input string for commands. +/// +/// Null bytes (`b"\x00"`) will be skipped completely, unparseable content will return `Some(Err(ProtocolParseError))` +/// but will not fail the parsing completely (i.e. you can continue to get the next entry until you reach the end of the input). +/// +/// ## Example +/// ``` +/// # use adafruit_bluefruit_protocol::button_event::{Button, ButtonParseError, ButtonState}; +/// # use adafruit_bluefruit_protocol::ControllerEvent::ButtonEvent; +/// # use adafruit_bluefruit_protocol::{ControllerEvent, Parser, ProtocolParseError}; +/// +/// /// internal test helper +/// fn assert_is_button_event( +/// event: &Result, +/// button: Button, +/// button_state: ButtonState, +/// ) { +/// match event { +/// Ok(ButtonEvent(event)) => { +/// assert_eq!(event.button(), &button); +/// assert_eq!(event.state(), &button_state) +/// } +/// _ => assert!(false), +/// } +/// } +/// +/// // the example input contains some null bytes, two button events and two malformed events. +/// let input = b"\x00!B11:!B10;\x00\x00!\x00\x00\x00\x00!B138"; +/// let mut parser = Parser::new(input); +/// +/// assert_is_button_event( +/// &parser.next().unwrap(), +/// Button::Button1, +/// ButtonState::Pressed, +/// ); +/// assert_is_button_event( +/// &parser.next().unwrap(), +/// Button::Button1, +/// ButtonState::Released, +/// ); +/// assert_eq!( +/// parser.next().unwrap(), +/// Err(ProtocolParseError::UnknownEvent(Some(0))) +/// ); +/// if let Err(e) = &parser.next().unwrap() { +/// assert_eq!( +/// e, +/// &ProtocolParseError::ButtonParseError(ButtonParseError::UnknownButtonState(b'3')) +/// ); +/// # { +/// // test the `core::error::Error` implementation +/// # extern crate alloc; +/// # use alloc::string::ToString; +/// # use core::error::Error; +/// assert_eq!( +/// e.source().unwrap().to_string(), +/// "Unknown button state: 0x33" +/// ); +/// # } +/// } else { +/// assert!(false, "expected an error"); +/// } +/// assert_eq!(parser.next(), None); +/// ``` +#[derive(Debug, Copy, Clone)] +pub struct Parser<'a> { + input: &'a [u8], + curr_pos: usize, +} + +impl<'a> Parser<'a> { + /// Create a new parser. The input is parsed step by step on each invocation of `next`. + pub fn new(input: &'a [u8]) -> Self { + Self { input, curr_pos: 0 } } - let mut state = ParserState::SeekStart; +} - let mut result = Vec::new(); +impl Iterator for Parser<'_> { + type Item = Result; - for pos in 0..input.len() { - let byte = input[pos]; - match state { - ParserState::SeekStart => { - if byte == b'!' { - state = ParserState::ParseCommand + fn next(&mut self) -> Option { + /// Simple state machine for the parser, represents whether the parser is seeking a start or has found it. + enum ParserState { + SeekStart, + ParseCommand, + } + let mut state = ParserState::SeekStart; + + for pos in self.curr_pos..self.input.len() { + let byte = self.input[pos]; + match state { + ParserState::SeekStart => { + if byte == b'!' { + state = ParserState::ParseCommand + } } - } - ParserState::ParseCommand => { - let data_package = extract_and_parse_command(&input[(pos - 1)..]); - #[cfg(feature = "alloc")] - result.push(data_package); - #[cfg(feature = "heapless")] - result.push(data_package).ok(); - #[cfg(feature = "heapless")] - if result.len() == MAX_RESULTS { - return result; + ParserState::ParseCommand => { + self.curr_pos = pos; + return Some(extract_and_parse_command(&self.input[(pos - 1)..])); } - state = ParserState::SeekStart; - } - }; - } + }; + } - result + None + } } /// Extract a command and then try to parse it. @@ -382,58 +429,12 @@ fn try_f32_from_le_bytes(input: &[u8]) -> Result { #[cfg(test)] mod tests { - use crate::button_event::{Button, ButtonParseError, ButtonState}; - use crate::{check_crc, parse, try_f32_from_le_bytes, ControllerEvent, ProtocolParseError}; - - fn assert_is_button_event( - event: &Result, - button: Button, - button_state: ButtonState, - ) { - match event { - Ok(ControllerEvent::ButtonEvent(event)) => { - assert_eq!(event.button(), &button); - assert_eq!(event.state(), &button_state) - } - _ => assert!(false), - } - } - - #[test] - fn test_parse() { - let input = b"\x00!B11:!B10;\x00\x00!\x00\x00\x00\x00!B138"; - #[cfg(feature = "heapless")] - let result = parse::<4>(input); - #[cfg(feature = "alloc")] - let result = parse(input); - - assert_eq!(result.len(), 4); - assert_is_button_event(&result[0], Button::Button1, ButtonState::Pressed); - assert_is_button_event(&result[1], Button::Button1, ButtonState::Released); - assert_eq!(result[2], Err(ProtocolParseError::UnknownEvent(Some(0)))); - if let Err(e) = &result[3] { - assert_eq!( - e, - &ProtocolParseError::ButtonParseError(ButtonParseError::UnknownButtonState(b'3')) - ); - #[cfg(feature = "alloc")] - { - use alloc::string::ToString; - use core::error::Error; - assert_eq!( - e.source().unwrap().to_string(), - "Unknown button state: 0x33" - ); - } - } else { - assert!(false, "expected an error"); - } - } + use crate::{check_crc, try_f32_from_le_bytes, ProtocolParseError}; #[test] fn test_check_crc_ok() { let input = b"!B11:"; - let data = &input[0..input.len() - 1]; + let data = &input[..input.len() - 1]; let crc = input.last().unwrap(); assert!(check_crc(data, &crc).is_ok()); @@ -443,7 +444,7 @@ mod tests { fn test_check_crc_err() { let input = b"!B11;"; // should either be "!B11:" or "!B10;" let correct_crc = b':'; - let data = &input[0..input.len() - 1]; + let data = &input[..input.len() - 1]; let crc = input.last().unwrap(); assert_eq!(