#![allow(dead_code)]
#[allow(dead_code)]
pub fn nmea_checksum(sentence: &str) -> u8 {
sentence.bytes().fold(0u8, |acc, b| acc ^ b)
}
#[allow(dead_code)]
pub fn decimal_to_nmea(degrees: f64) -> (String, char) {
let abs = degrees.abs();
let deg = abs.floor() as u32;
let min = (abs - deg as f64) * 60.0;
let s = if degrees >= 0.0 { 'N' } else { 'S' };
(format!("{:02}{:09.6}", deg, min), s)
}
#[allow(dead_code)]
pub fn decimal_lon_to_nmea(lon: f64) -> (String, char) {
let abs = lon.abs();
let deg = abs.floor() as u32;
let min = (abs - deg as f64) * 60.0;
let s = if lon >= 0.0 { 'E' } else { 'W' };
(format!("{:03}{:09.6}", deg, min), s)
}
#[allow(dead_code)]
pub fn build_gga(
time_hhmmss: &str,
lat: f64,
lon: f64,
fix_quality: u8,
num_satellites: u8,
altitude_m: f64,
) -> String {
let (lat_str, lat_dir) = decimal_to_nmea(lat);
let (lon_str, lon_dir) = decimal_lon_to_nmea(lon);
let body = format!(
"GPGGA,{},{},{},{},{},{},{},{},,,M,,M,,",
time_hhmmss, lat_str, lat_dir, lon_str, lon_dir, fix_quality, num_satellites, altitude_m
);
let cs = nmea_checksum(body.as_str());
format!("${}*{:02X}\r\n", body, cs)
}
#[allow(dead_code)]
pub fn build_rmc(
time_hhmmss: &str,
status: char,
lat: f64,
lon: f64,
speed_knots: f64,
course_deg: f64,
date_ddmmyy: &str,
) -> String {
let (lat_str, lat_dir) = decimal_to_nmea(lat);
let (lon_str, lon_dir) = decimal_lon_to_nmea(lon);
let body = format!(
"GPRMC,{},{},{},{},{},{},{},{},{},,,A",
time_hhmmss,
status,
lat_str,
lat_dir,
lon_str,
lon_dir,
speed_knots,
course_deg,
date_ddmmyy
);
let cs = nmea_checksum(body.as_str());
format!("${}*{:02X}\r\n", body, cs)
}
#[allow(dead_code)]
pub fn export_positions_as_gga(positions: &[(f64, f64, f64)], base_time_s: u32) -> Vec<String> {
positions
.iter()
.enumerate()
.map(|(i, &(lat, lon, alt))| {
let t = base_time_s + i as u32;
let hh = (t / 3600) % 24;
let mm = (t / 60) % 60;
let ss = t % 60;
let time_str = format!("{:02}{:02}{:02}.00", hh, mm, ss);
build_gga(&time_str, lat, lon, 1, 8, alt)
})
.collect()
}
#[allow(dead_code)]
pub fn sentence_count(sentences: &[String]) -> usize {
sentences.len()
}
#[allow(dead_code)]
pub fn sentence_has_crlf(s: &str) -> bool {
s.ends_with("\r\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gga_starts_with_dollar() {
let s = build_gga("120000.00", 35.0, 139.0, 1, 8, 100.0);
assert!(s.starts_with('$'));
}
#[test]
fn gga_ends_with_crlf() {
let s = build_gga("120000.00", 35.0, 139.0, 1, 8, 100.0);
assert!(sentence_has_crlf(&s));
}
#[test]
fn gga_contains_gpgga() {
let s = build_gga("120000.00", 35.0, 139.0, 1, 8, 100.0);
assert!(s.contains("GPGGA"));
}
#[test]
fn rmc_starts_with_dollar() {
let s = build_rmc("120000.00", 'A', 35.0, 139.0, 0.0, 0.0, "070326");
assert!(s.starts_with('$'));
}
#[test]
fn rmc_contains_gprmc() {
let s = build_rmc("120000.00", 'A', 35.0, 139.0, 0.0, 0.0, "070326");
assert!(s.contains("GPRMC"));
}
#[test]
fn decimal_to_nmea_positive() {
let (val, dir) = decimal_to_nmea(35.6762);
assert_eq!(dir, 'N');
assert!(val.starts_with("35"));
}
#[test]
fn decimal_to_nmea_negative() {
let (_, dir) = decimal_to_nmea(-35.0);
assert_eq!(dir, 'S');
}
#[test]
fn decimal_lon_east() {
let (_, dir) = decimal_lon_to_nmea(139.0);
assert_eq!(dir, 'E');
}
#[test]
fn decimal_lon_west() {
let (_, dir) = decimal_lon_to_nmea(-100.0);
assert_eq!(dir, 'W');
}
#[test]
fn export_gga_count() {
let pts = vec![(35.0, 139.0, 0.0), (35.1, 139.1, 10.0)];
let sentences = export_positions_as_gga(&pts, 43200);
assert_eq!(sentence_count(&sentences), 2);
}
#[test]
fn checksum_nonzero() {
let cs = nmea_checksum("GPGGA,120000.00,3535.0000,N,13900.0000,E,1,8,100.0,,,M,,M,,");
let _ = cs;
}
}