NMEA GPS
NMEA 0183 sentences from GPS receivers read: position, time, speed, course, altitude, satellites and fix quality, with checksums checked.
Version 1.0.0 · 3 functions · 0 crates
Functions
What use nmea.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use nmea_parse, nmea_fixes from nmea (Quillo 0.6).
nmea_parse(sentence: str) → anyor an errorOne sentence read: $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47 gives { type: "GGA", talker: "GP", checksum_ok, time, lat, lon, fix, satellites, hdop, altitude_m, ... }. RMC gives speed (knots and km/h), course and date; GSA and GSV the satellites; an unknown type gives its raw fields. Coordinates are decimal degrees, south and west negative.
nmea_fixes(log: str) → anyor an errorA receiver's log (one sentence per line) as fixes: each RMC or GGA starts or adds to the fix of its time, giving [{ time, date, lat, lon, speed_kmh, course, altitude_m, satellites, fix }]. Lines with a bad checksum or that do not parse are counted in skipped, not used.
nmea_sentence(body: str) → strA sentence completed with its checksum: GPGLL,4916.45,N,12311.12,W,225444,A gives $GPGLL,4916.45,N,12311.12,W,225444,A*31. For sending commands to a receiver or making test data.
Get it
$ quillo install nmea # nmea.qwer into this folder$ quillo install nmea --example # and its example, nmea.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/nmea.qwer$ curl -O https://quillolang.org/plugins/files/nmea.qwe$ curl -O https://quillolang.org/plugins/files/nmea_sample.logThen, in your program, all of it or only what you call:
use nmea.qweruse nmea_parse, nmea_fixes from nmea // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
Example
// A boat tracker's log, uploaded from its GPS: the fixes on a map line, and one sentence read// as it comes from a serial port.//// quillo run nmea.qwe then open http://localhost:8080/trackuse "nmea.qwer"route GET /track { let t = nmea_fixes(read_file("nmea_sample.log")?)? { points: len(t.fixes), skipped: t.skipped, last: t.fixes[len(t.fixes) - 1] }}route GET /line ? { s: str = "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47" } = nmea_parse(s)?route GET /make = { sentence: nmea_sentence("GPGLL,4916.45,N,12311.12,W,225444,A") }$ quillo run nmea.qwe$ curl 'localhost:8080/track'$ curl 'localhost:8080/line'$ curl 'localhost:8080/make'Source
//! NMEA 0183 sentences from GPS receivers, boats and trackers read: position, time, speed, course, altitude, satellites and fix quality from GGA, RMC, GLL, VTG, GSA and GSV lines, checksums checked.//! version: 1.0.0//!//! [dependencies]use serde_json::{json, Map, Value};const MAX_LOG: usize = 64 << 20;fn checksum(body: &str) -> u8 { body.bytes().fold(0u8, |a, b| a ^ b)}/// `4916.45,N` as decimal degrees.fn coord(v: &str, hemi: &str, deg_digits: usize) -> Option<f64> { if v.len() < deg_digits + 2 { return None; } let d: f64 = v[..deg_digits].parse().ok()?; let m: f64 = v[deg_digits..].parse().ok()?; let x = d + m / 60.0; Some(if hemi == "S" || hemi == "W" { -x } else { x })}fn time(v: &str) -> Option<String> { if v.len() < 6 || !v[..6].chars().all(|c| c.is_ascii_digit()) { return None; } Some(format!("{}:{}:{}", &v[0..2], &v[2..4], &v[4..]))}fn num(v: &str) -> Value { v.parse::<f64>().map(|f| json!(f)).unwrap_or(Value::Null)}fn int(v: &str) -> Value { v.parse::<i64>().map(|f| json!(f)).unwrap_or(Value::Null)}fn r(x: f64, d: f64) -> f64 { (x * d).round() / d}fn parse(line: &str) -> Result<Value, String> { let line = line.trim(); let body = line.strip_prefix('$').or_else(|| line.strip_prefix('!')).ok_or("an NMEA sentence starts with $")?; let (body, sum) = match body.rsplit_once('*') { Some((b, s)) => (b, Some(s)), None => (body, None), }; let valid = match sum { Some(s) => u8::from_str_radix(s.trim(), 16).map(|x| x == checksum(body)).unwrap_or(false), None => false, }; let f: Vec<&str> = body.split(',').collect(); let head = f[0]; if head.len() < 5 { return Err(format!("`{}` is not a sentence type", head)); } let (talker, kind) = head.split_at(head.len() - 3); let g = |i: usize| f.get(i).copied().unwrap_or(""); let mut m = Map::new(); m.insert("type".into(), json!(kind)); m.insert("talker".into(), json!(talker)); m.insert("checksum_ok".into(), json!(valid)); let mut put = |k: &str, v: Value| { m.insert(k.into(), v); }; match kind { "GGA" => { put("time", json!(time(g(1)))); put("lat", json!(coord(g(2), g(3), 2).map(|x| r(x, 1e7)))); put("lon", json!(coord(g(4), g(5), 3).map(|x| r(x, 1e7)))); let q = g(6).parse::<i64>().unwrap_or(0); put("fix_quality", json!(q)); put("fix", json!(["none", "gps", "dgps", "pps", "rtk", "float rtk", "estimated", "manual", "simulation"].get(q as usize).copied().unwrap_or("unknown"))); put("satellites", int(g(7))); put("hdop", num(g(8))); put("altitude_m", num(g(9))); put("geoid_m", num(g(11))); } "RMC" => { put("time", json!(time(g(1)))); put("active", json!(g(2) == "A")); put("lat", json!(coord(g(3), g(4), 2).map(|x| r(x, 1e7)))); put("lon", json!(coord(g(5), g(6), 3).map(|x| r(x, 1e7)))); let knots = g(7).parse::<f64>().ok(); put("speed_knots", json!(knots)); put("speed_kmh", json!(knots.map(|k| r(k * 1.852, 100.0)))); put("course", num(g(8))); let d = g(9); // Two-digit years: 80 to 99 are the 1900s, the rest the 2000s. let century = if d.len() == 6 && d[4..6] >= *"80" { "19" } else { "20" }; put("date", json!(if d.len() == 6 { Some(format!("{}{}-{}-{}", century, &d[4..6], &d[2..4], &d[0..2])) } else { None })); let var = g(10).parse::<f64>().ok().map(|v| if g(11) == "W" { -v } else { v }); put("magnetic_variation", json!(var)); } "GLL" => { put("lat", json!(coord(g(1), g(2), 2).map(|x| r(x, 1e7)))); put("lon", json!(coord(g(3), g(4), 3).map(|x| r(x, 1e7)))); put("time", json!(time(g(5)))); put("active", json!(g(6) == "A")); } "VTG" => { put("course", num(g(1))); put("course_magnetic", num(g(3))); put("speed_knots", num(g(5))); put("speed_kmh", num(g(7))); } "GSA" => { put("mode", json!(match g(2) { "2" => "2d", "3" => "3d", _ => "none" })); let sats: Vec<i64> = (3..15).filter_map(|i| g(i).parse().ok()).collect(); put("satellites_used", json!(sats)); put("pdop", num(g(15))); put("hdop", num(g(16))); put("vdop", num(g(17))); } "GSV" => { put("messages", int(g(1))); put("message", int(g(2))); put("in_view", int(g(3))); let mut sats = Vec::new(); let mut i = 4; while i + 3 < f.len() + 1 && !g(i).is_empty() { sats.push(json!({ "prn": int(g(i)), "elevation": int(g(i + 1)), "azimuth": int(g(i + 2)), "snr": int(g(i + 3)) })); i += 4; } put("satellites", json!(sats)); } _ => put("fields", json!(f[1..].to_vec())), } Ok(Value::Object(m))}/// One sentence read: `$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47` gives `{ type: "GGA", talker: "GP", checksum_ok, time, lat, lon, fix, satellites, hdop, altitude_m, ... }`./// RMC gives speed (knots and km/h), course and date; GSA and GSV the satellites; an unknown type gives its raw `fields`. Coordinates are decimal degrees, south and west negative.pub fn nmea_parse(sentence: &str) -> Result<Value, String> { if sentence.len() > 1024 { return Err("a sentence is at most 1 KB (82 characters by the standard)".into()); } parse(sentence)}/// A receiver's log (one sentence per line) as fixes: each RMC or GGA starts or adds to the fix of its time, giving/// `[{ time, date, lat, lon, speed_kmh, course, altitude_m, satellites, fix }]`. Lines with a bad checksum or that do not parse are counted in `skipped`, not used.pub fn nmea_fixes(log: &str) -> Result<Value, String> { if log.len() > MAX_LOG { return Err("a log is at most 64 MB".into()); } let mut fixes: Vec<Map<String, Value>> = Vec::new(); let mut skipped = 0; for line in log.lines().filter(|l| !l.trim().is_empty()) { let Ok(s) = parse(line) else { skipped += 1; continue }; if s["checksum_ok"] != true { skipped += 1; continue; } let kind = s["type"].as_str().unwrap_or(""); if kind != "RMC" && kind != "GGA" { continue; } if s["lat"].is_null() { continue; } let same = fixes.last().map(|f| f.get("time") == s.get("time")).unwrap_or(false); if !same { if fixes.len() >= 1_000_000 { break; } fixes.push(Map::new()); } let fx = fixes.last_mut().expect("just pushed"); for k in ["time", "date", "lat", "lon", "speed_kmh", "course", "altitude_m", "satellites", "fix"] { if let Some(v) = s.get(k) { if !v.is_null() { fx.insert(k.into(), v.clone()); } } } } Ok(json!({ "fixes": fixes, "skipped": skipped }))}/// A sentence completed with its checksum: `GPGLL,4916.45,N,12311.12,W,225444,A` gives `$GPGLL,4916.45,N,12311.12,W,225444,A*31`. For sending commands to a receiver or making test data.pub fn nmea_sentence(body: &str) -> String { let b = body.trim().trim_start_matches('$'); let b = b.split('*').next().unwrap_or(b); format!("${}*{:02X}", b, checksum(b))}Related
ais.qwer · 1.0.0Ship AIS messages decoded from a receiver's NMEA lines: positions, speeds and courses, names, call signs, sizes, destinations and ETAs, class B yachts and navigation marks, multi-part messages joined and checksums checked.6 views · 7 downloadsAircraft ADS-B adsb.qwer · 1.0.0Aircraft ADS-B and Mode S messages from a 1090 MHz receiver decoded: call signs, altitudes, positions from even and odd pairs or near the receiver, speeds, tracks, vertical rates and squawks, CRC-checked, and the aircraft seen put together.7 views · 7 downloadsGPX tracks gpx.qwer · 1.0.0GPX tracks from watches and phones read: distance, climbing, duration, moving pace, per-kilometre splits, and the points for a map line.7 views · 8 downloadsG-code for 3D printers gcode.qwer · 1.0.0G-code from 3D printers and CNC machines read: print time with acceleration, filament length, weight and cost, the print's size, layers and their heights, temperatures and the slicer's notes, and a layer drawn as SVG.6 views · 7 downloadsGarmin FIT files fit.qwer · 1.0.0Garmin FIT files from watches and bike computers read: the activity's distance, time, pace, heart rate, cadence, power and ascent, its laps, the track's points, any message as data, and the track as GPX.5 views · 8 downloadsModbus frames modbus.qwer · 1.0.0Modbus RTU and TCP frames built and read without a device: register and coil requests, answers and exceptions explained, CRC-16 and MBAP headers, registers turned into integers, floats and text in any word order and back, and a TCP client for a real PLC or meter.8 views · 10 downloadsThread 0 posts
On NMEA GPS: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.