Devices

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

#gps#devices#marine#tracking#import

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 error

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.

nmea_fixes(log: str) → anyor an error

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.

nmea_sentence(body: str) → str

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.

Get it

$ quillo install nmea               # nmea.qwer into this folder$ quillo install nmea --example     # and its example, nmea.qwe

Or 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.log

Then, in your program, all of it or only what you call:

main.qwe
use nmea.qweruse nmea_parse, nmea_fixes from nmea      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]

Example

nmea.qwe
// 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.qwer
//! 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))}
Ship AIS messages 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.marinegpslocationsensorstracking6 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.sensorslocationtrackinggpssignals7 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.gpssportsmapsfilesimport7 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.filesdevicescalculatorssvg6 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.sportsgpsfilessensorshealth5 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.iotsensorsdevicesbinarynetwork8 views · 10 downloads
Thread
78Useful? Like it, or add to its thread below.

Thread 0 posts

On NMEA GPS: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.