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!(