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

Avoid using FromStr for parsing floats #59

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}