Skip to content

Commit c38cd61

Browse files
committed
Avoid using FromStr for parsing floats
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.
1 parent c73530b commit c38cd61

File tree

4 files changed

+128
-37
lines changed

4 files changed

+128
-37
lines changed

Cargo.toml

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,33 @@
1-
[package]
2-
name = "sim7000-async"
3-
version = "5.0.0"
4-
authors = [
5-
"Zoey Riordan <[email protected]>",
6-
"Joakim Hulthe <[email protected]>",
7-
]
8-
description = "Drivers for the SIM7000 series of chips"
9-
license = "Apache-2.0 OR MIT"
10-
repository = "https://github.com/technocreatives/sim7000"
11-
edition = "2021"
12-
rust-version = "1.75"
13-
14-
[dependencies]
15-
critical-section = "1.1.2"
16-
defmt = { version = "0.3.2", optional = true }
17-
embassy-executor = "0.5.0"
18-
embassy-futures = "0.1.1"
19-
embassy-sync = "0.5.0"
20-
embassy-time = "0.3.0"
21-
embedded-io-async = "0.6.0"
22-
futures = { version = "0.3", default-features = false, features = ["async-await"] }
23-
heapless = "0.7"
24-
log = { version = "0.4", optional = true }
25-
26-
[features]
27-
default = ["log"]
28-
log = ["dep:log"]
29-
defmt = ["dep:defmt", "embassy-time/defmt", "heapless/defmt-impl"]
1+
[package]
2+
name = "sim7000-async"
3+
version = "5.0.0"
4+
authors = [
5+
"Zoey Riordan <[email protected]>",
6+
"Joakim Hulthe <[email protected]>",
7+
]
8+
description = "Drivers for the SIM7000 series of chips"
9+
license = "Apache-2.0 OR MIT"
10+
repository = "https://github.com/technocreatives/sim7000"
11+
edition = "2021"
12+
rust-version = "1.75"
13+
14+
[dependencies]
15+
critical-section = "1.1.2"
16+
defmt = { version = "0.3.2", optional = true }
17+
embassy-executor = "0.5.0"
18+
embassy-futures = "0.1.1"
19+
embassy-sync = "0.5.0"
20+
embassy-time = "0.3.0"
21+
embedded-io-async = "0.6.0"
22+
futures = { version = "0.3", default-features = false, features = ["async-await"] }
23+
heapless = "0.7"
24+
log = { version = "0.4", optional = true }
25+
26+
[features]
27+
default = ["log"]
28+
log = ["dep:log"]
29+
defmt = ["dep:defmt", "embassy-time/defmt", "heapless/defmt-impl"]
30+
31+
[dev-dependencies]
32+
quickcheck = "1.0.3"
33+
quickcheck_macros = "1.0.0"

src/at_command/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use core::{
22
fmt::Debug,
3-
num::{ParseFloatError, ParseIntError},
3+
num::{IntErrorKind, ParseFloatError, ParseIntError},
44
};
55

66
pub mod generic_response;
@@ -180,6 +180,14 @@ impl From<&'static str> for AtParseErr {
180180
}
181181
}
182182

183+
impl From<IntErrorKind> for AtParseErr {
184+
fn from(_: IntErrorKind) -> Self {
185+
AtParseErr {
186+
message: "Failed to parse integer",
187+
}
188+
}
189+
}
190+
183191
impl From<ParseIntError> for AtParseErr {
184192
fn from(_: ParseIntError) -> Self {
185193
AtParseErr {

src/at_command/unsolicited/ugnsinf.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use core::str::FromStr;
22

33
use crate::at_command::{AtParseErr, AtParseLine};
4+
use crate::parse_f32;
45
use crate::util::collect_array;
56

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

59+
// avoid parsing f32s with FromStr implementation because it uses 10KiB extra flash
60+
fn parse_optional_f32(s: &str) -> Result<f32, AtParseErr> {
61+
Ok(parse_f32(s).or_else(|e| s.is_empty().then_some(0.0).ok_or(e))?)
62+
}
63+
5864
Ok(GnssReport::Fix(GnssFix {
59-
latitude: latitude.parse()?,
60-
longitude: longitude.parse()?,
61-
altitude: msl_altitude.parse()?,
65+
latitude: parse_f32(latitude)?,
66+
longitude: parse_f32(longitude)?,
67+
altitude: parse_f32(msl_altitude)?,
6268

6369
// The docs are unclear on what fields are optional, so just assume everything except
6470
// the core values are.
6571
speed_over_ground: parse_optional(speed_over_groud)?,
66-
course_over_ground: parse_optional(course_over_ground)?,
67-
hdop: parse_optional(hdop)?,
68-
pdop: parse_optional(pdop)?,
69-
vdop: parse_optional(vdop)?,
72+
course_over_ground: parse_optional_f32(course_over_ground)?,
73+
hdop: parse_optional_f32(hdop)?,
74+
pdop: parse_optional_f32(pdop)?,
75+
vdop: parse_optional_f32(vdop)?,
7076
signal_noise_ratio: parse_optional(c_n0_max)?,
7177

7278
// The docs contradicts itself on what these values are and what they are called

src/util.rs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use core::{
88
use embassy_sync::{blocking_mutex, blocking_mutex::raw::RawMutex, waitqueue::WakerRegistration};
99
use heapless::Deque;
1010

11+
use crate::at_command::AtParseErr;
12+
1113
#[track_caller]
1214
pub(crate) fn collect_array<T: Default + Copy, const N: usize>(
1315
mut iter: impl Iterator<Item = T>,
@@ -19,6 +21,46 @@ pub(crate) fn collect_array<T: Default + Copy, const N: usize>(
1921
Some(out)
2022
}
2123

24+
/// A naive float parsing implementation, made to be less bloated than the FromStr impl for f64s.
25+
///
26+
/// Only supports basic decimal numbers that look like `-?\d+(.\d*)?`.
27+
/// Does very litte sanity checking, may produce nonsensical results if given malformed strings.
28+
pub(crate) fn parse_f64(s: &str) -> Result<f64, &'static str> {
29+
let (int, frac) = s
30+
.find('.')
31+
.map(|decimal_place| {
32+
let (int, frac) = s.split_at(decimal_place);
33+
let frac = &frac[1..];
34+
let frac = frac.trim_end_matches('0');
35+
36+
(int, frac)
37+
})
38+
.unwrap_or((s, ""));
39+
40+
const PARSE_ERR: &str = "float parse error";
41+
const OVERFLOW_ERR: &str = "float parse overflow";
42+
43+
let decimal_place = frac.len() as u32;
44+
45+
let whole: i64 = if int.is_empty() { Ok(0) } else { int.parse() }.map_err(|_| PARSE_ERR)?;
46+
let frac: u64 = if frac.is_empty() { Ok(0) } else { frac.parse() }.map_err(|_| PARSE_ERR)?;
47+
48+
let mut frac = i64::try_from(frac).map_err(|_| OVERFLOW_ERR)?;
49+
if whole.is_negative() {
50+
frac = -frac;
51+
}
52+
let (whole, frac) = (whole as f64, frac as f64);
53+
54+
let pow = 10u32.pow(decimal_place) as f64;
55+
let num = whole + frac / pow;
56+
Ok(num)
57+
}
58+
59+
/// Shorthand for [parse_f64] as `f32`.
60+
pub(crate) fn parse_f32(s: &str) -> Result<f32, AtParseErr> {
61+
Ok(parse_f64(s)? as f32)
62+
}
63+
2264
/// A signal with that keeps track of the last value signaled.
2365
pub struct StateSignal<M: RawMutex, T> {
2466
inner: blocking_mutex::Mutex<M, RefCell<StateSignalInner<T>>>,
@@ -154,3 +196,34 @@ impl<M: RawMutex, T: Debug, const N: usize> RingChannel<M, T, N> {
154196
});
155197
}
156198
}
199+
200+
#[cfg(test)]
201+
mod test {
202+
use core::fmt::Write;
203+
use heapless::String;
204+
use quickcheck_macros::quickcheck;
205+
206+
#[quickcheck]
207+
// we use ints here because quickcheck with floats is broken
208+
fn parse_f64(int: i16, frac: u16) {
209+
let mut s = String::<128>::new();
210+
write!(&mut s, "{int}.{frac}").expect("buffer overflow when stringifying float");
211+
let s = s.trim();
212+
213+
let parsed = super::parse_f64(s).expect("failed to parse f64");
214+
let parsed2 = s.parse().unwrap();
215+
assert_eq!(parsed, parsed2);
216+
}
217+
218+
#[quickcheck]
219+
// we use ints here because quickcheck with floats is broken
220+
fn parse_f32(int: i16, frac: u16) {
221+
let mut s = String::<128>::new();
222+
write!(&mut s, "{int}.{frac}").expect("buffer overflow when stringifying float");
223+
let s = s.trim();
224+
225+
let parsed = super::parse_f32(s).expect("failed to parse f32");
226+
let parsed2 = s.parse().unwrap();
227+
assert_eq!(parsed, parsed2);
228+
}
229+
}

0 commit comments

Comments
 (0)