Skip to content

Commit

Permalink
Avoid using FromStr for parsing floats
Browse files Browse the repository at this point in the history
f32s FromStr impl is very bloated, although probably faster than this
solution. This solution results in about 18KB less flash usage for the
nrf sample crate though.
  • Loading branch information
hulthe committed Feb 16, 2024
1 parent c73530b commit 2aa0f59
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 38 deletions.
62 changes: 33 additions & 29 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,29 +1,33 @@
[package]
name = "sim7000-async"
version = "5.0.0"
authors = [
"Zoey Riordan <[email protected]>",
"Joakim Hulthe <[email protected]>",
]
description = "Drivers for the SIM7000 series of chips"
license = "Apache-2.0 OR MIT"
repository = "https://github.com/technocreatives/sim7000"
edition = "2021"
rust-version = "1.75"

[dependencies]
critical-section = "1.1.2"
defmt = { version = "0.3.2", optional = true }
embassy-executor = "0.5.0"
embassy-futures = "0.1.1"
embassy-sync = "0.5.0"
embassy-time = "0.3.0"
embedded-io-async = "0.6.0"
futures = { version = "0.3", default-features = false, features = ["async-await"] }
heapless = "0.7"
log = { version = "0.4", optional = true }

[features]
default = ["log"]
log = ["dep:log"]
defmt = ["dep:defmt", "embassy-time/defmt", "heapless/defmt-impl"]
[package]
name = "sim7000-async"
version = "5.0.0"
authors = [
"Zoey Riordan <[email protected]>",
"Joakim Hulthe <[email protected]>",
]
description = "Drivers for the SIM7000 series of chips"
license = "Apache-2.0 OR MIT"
repository = "https://github.com/technocreatives/sim7000"
edition = "2021"
rust-version = "1.75"

[dependencies]
critical-section = "1.1.2"
defmt = { version = "0.3.2", optional = true }
embassy-executor = "0.5.0"
embassy-futures = "0.1.1"
embassy-sync = "0.5.0"
embassy-time = "0.3.0"
embedded-io-async = "0.6.0"
futures = { version = "0.3", default-features = false, features = ["async-await"] }
heapless = "0.7"
log = { version = "0.4", optional = true }

[features]
default = ["log"]
log = ["dep:log"]
defmt = ["dep:defmt", "embassy-time/defmt", "heapless/defmt-impl"]

[dev-dependencies]
quickcheck = "1.0.3"
quickcheck_macros = "1.0.0"
10 changes: 9 additions & 1 deletion src/at_command/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use core::{
fmt::Debug,
num::{ParseFloatError, ParseIntError},
num::{IntErrorKind, ParseFloatError, ParseIntError},
};

pub mod generic_response;
Expand Down Expand Up @@ -180,6 +180,14 @@ impl From<&'static str> for AtParseErr {
}
}

impl From<IntErrorKind> for AtParseErr {
fn from(_: IntErrorKind) -> Self {
AtParseErr {
message: "Failed to parse integer",
}
}
}

impl From<ParseIntError> for AtParseErr {
fn from(_: ParseIntError) -> Self {
AtParseErr {
Expand Down
22 changes: 14 additions & 8 deletions src/at_command/unsolicited/ugnsinf.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use core::str::FromStr;

use crate::at_command::{AtParseErr, AtParseLine};
use crate::parse_f32;
use crate::util::collect_array;

#[derive(Debug, PartialEq)]
Expand Down Expand Up @@ -55,18 +56,23 @@ impl AtParseLine for GnssReport {
.or_else(|e| s.is_empty().then(T::default).ok_or(e))
}

// avoid parsing f32s with FromStr implementation because it uses 10KiB extra flash
fn parse_optional_f32(s: &str) -> Result<f32, AtParseErr> {
Ok(parse_f32(s).or_else(|e| s.is_empty().then_some(0.0).ok_or(e))?)
}

Ok(GnssReport::Fix(GnssFix {
latitude: latitude.parse()?,
longitude: longitude.parse()?,
altitude: msl_altitude.parse()?,
latitude: parse_f32(latitude)?,
longitude: parse_f32(longitude)?,
altitude: parse_f32(msl_altitude)?,

// The docs are unclear on what fields are optional, so just assume everything except
// the core values are.
speed_over_ground: parse_optional(speed_over_groud)?,
course_over_ground: parse_optional(course_over_ground)?,
hdop: parse_optional(hdop)?,
pdop: parse_optional(pdop)?,
vdop: parse_optional(vdop)?,
speed_over_ground: parse_optional_f32(speed_over_groud)?,
course_over_ground: parse_optional_f32(course_over_ground)?,
hdop: parse_optional_f32(hdop)?,
pdop: parse_optional_f32(pdop)?,
vdop: parse_optional_f32(vdop)?,
signal_noise_ratio: parse_optional(c_n0_max)?,

// The docs contradicts itself on what these values are and what they are called
Expand Down
73 changes: 73 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use core::{
use embassy_sync::{blocking_mutex, blocking_mutex::raw::RawMutex, waitqueue::WakerRegistration};
use heapless::Deque;

use crate::at_command::AtParseErr;

#[track_caller]
pub(crate) fn collect_array<T: Default + Copy, const N: usize>(
mut iter: impl Iterator<Item = T>,
Expand All @@ -19,6 +21,46 @@ pub(crate) fn collect_array<T: Default + Copy, const N: usize>(
Some(out)
}

/// A naive float parsing implementation, made to be less bloated than the FromStr impl for f64s.
///
/// Only supports basic decimal numbers that look like `-?\d+(.\d*)?`.
/// Does very litte sanity checking, may produce nonsensical results if given malformed strings.
pub(crate) fn parse_f64(s: &str) -> Result<f64, &'static str> {
let (int, frac) = s
.find('.')
.map(|decimal_place| {
let (int, frac) = s.split_at(decimal_place);
let frac = &frac[1..];
let frac = frac.trim_end_matches('0');

(int, frac)
})
.unwrap_or((s, ""));

const PARSE_ERR: &str = "float parse error";
const OVERFLOW_ERR: &str = "float parse overflow";

let decimal_place = frac.len() as u32;

let whole: i64 = if int.is_empty() { Ok(0) } else { int.parse() }.map_err(|_| PARSE_ERR)?;
let frac: u64 = if frac.is_empty() { Ok(0) } else { frac.parse() }.map_err(|_| PARSE_ERR)?;

let mut frac = i64::try_from(frac).map_err(|_| OVERFLOW_ERR)?;
if whole.is_negative() {
frac = -frac;
}
let (whole, frac) = (whole as f64, frac as f64);

let pow = 10u32.pow(decimal_place) as f64;
let num = whole + frac / pow;
Ok(num)
}

/// Shorthand for [parse_f64] as `f32`.
pub(crate) fn parse_f32(s: &str) -> Result<f32, AtParseErr> {
Ok(parse_f64(s)? as f32)
}

/// A signal with that keeps track of the last value signaled.
pub struct StateSignal<M: RawMutex, T> {
inner: blocking_mutex::Mutex<M, RefCell<StateSignalInner<T>>>,
Expand Down Expand Up @@ -154,3 +196,34 @@ impl<M: RawMutex, T: Debug, const N: usize> RingChannel<M, T, N> {
});
}
}

#[cfg(test)]
mod test {
use core::fmt::Write;
use heapless::String;
use quickcheck_macros::quickcheck;

#[quickcheck]
// we use ints here because quickcheck with floats is broken
fn parse_f64(int: i16, frac: u16) {
let mut s = String::<128>::new();
write!(&mut s, "{int}.{frac}").expect("buffer overflow when stringifying float");
let s = s.trim();

let parsed = super::parse_f64(s).expect("failed to parse f64");
let parsed2 = s.parse().unwrap();
assert_eq!(parsed, parsed2);
}

#[quickcheck]
// we use ints here because quickcheck with floats is broken
fn parse_f32(int: i16, frac: u16) {
let mut s = String::<128>::new();
write!(&mut s, "{int}.{frac}").expect("buffer overflow when stringifying float");
let s = s.trim();

let parsed = super::parse_f32(s).expect("failed to parse f32");
let parsed2 = s.parse().unwrap();
assert_eq!(parsed, parsed2);
}
}

0 comments on commit 2aa0f59

Please sign in to comment.