Devices

Serial ports

Scales, sensors and instruments on a serial port: ports listed with their USB details, opened with baud, parity, data and stop bits and flow control, lines read whatever pieces they arrive in, reads up to a marker, exact byte counts, question and answer, timeouts that keep what came, and scale weight lines and sensor fields read.

Version 1.0.0 · 17 functions · 3 crates

#hardware#scales#sensors#uart

Functions

What use serial.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use serial_ports, serial_open from serial (Quillo 0.6).

serial_ports() → listor an error

The serial ports on this machine: { name, type }, and for a USB adapter its vid, pid, serial, manufacturer and product.

serial_open(name: str, path: str, options: any) → anyor an error

Opens a port under name: options takes baud (9600), data_bits (8), parity (none, even, odd), stop_bits (1), flow (none, hardware, software), timeout_ms (1000, the wait for reads that give none), line_ending ("\n"), and dtr. A port held by another program is an error saying so.

serial_configure(name: str, options: any) → anyor an error

Changes an open port's settings: baud, data_bits, parity, stop_bits, flow, timeout_ms.

serial_close(name: str) → boolor an error

Closes a port, so another program can have it; true when it was open.

serial_write(name: str, text: str) → intor an error

Writes text as it is; add your device's line ending. Answers the bytes written.

serial_write_bytes(name: str, data: list) → intor an error

Writes a list of byte values.

serial_read_line(name: str, options: any) → stror an error

Reads one line, without its ending or a carriage return. On a timeout it is an error and what came so far stays for the next read. Options: timeout_ms.

serial_read_lines(name: str, last: str, options: any) → listor an error

Reads lines until a line equals last (which is left out), or until nothing more comes for timeout_ms when last is empty, for a device that prints a block and stops.

serial_read_until(name: str, marker: str, options: any) → stror an error

Reads up to a marker, text like "END" or "> "; answers what came before it.

serial_read_bytes(name: str, count: int, options: any) → listor an error

Reads exactly count bytes, as a list of numbers, for binary protocols. On a timeout the ones that came stay for the next read.

serial_read_available(name: str, options: any) → stror an error

Whatever has arrived, as text, waiting at most options.timeout_ms (0 unless given) for the first byte.

serial_query(name: str, text: str, options: any) → stror an error

Question and answer: drops anything waiting, writes text, and reads the line that comes back, which is how most scales and meters work.

serial_clear(name: str) → intor an error

Drops whatever is waiting to be read; answers how many bytes went.

serial_signals(name: str) → anyor an error

The modem lines, null for each one the port cannot report: { cts, dsr, ri, cd }.

serial_parse_weight(line: str) → anyor an error

Reads a scale's weight line: the common ST,GS,+ 12.345kg (stable or unstable, gross or net), Mettler Toledo's MT-SICS S S 100.00 g and S D ..., and a bare 12.34 lb. Answers { value, unit, stable, kind }, or an error for an overload.

serial_parse_fields(line: str) → any

Reads a sensor's T=21.5;H=40 or temp:21.5, rh:40 line into a map, numbers as numbers. The separators are ;, , or spaces between pairs, and = or : inside them.

serial_checksum(text: str, kind: str) → stror an error

A checksum of a text, the two kinds serial protocols use: xor (NMEA's) or sum (the low byte of the total), as two hex digits.

Get it

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

Or with curl:

$ curl -O https://quillolang.org/plugins/files/serial.qwer$ curl -O https://quillolang.org/plugins/files/serial.qwe

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

main.qwe
use serial.qweruse serial_ports, serial_open from serial      // Quillo 0.6: only these

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

[dependencies]
serialport = { version = "4.10", default-features = false }hex = "0.4"serde_json = { version = "1", features = ["preserve_order"] }

Example

serial.qwe
// A weighing station on a serial port: a scale asked for its weight, a sensor's stream read in// pieces, a block of lines, a binary reply, the port's settings changed, and a device that says// nothing answered with a timeout rather than a hang.////   SERIAL_PORT=/dev/ttyUSB0 quillo run serial.qwe      then open http://localhost:8080/ports and /weigh//// Without SERIAL_PORT the routes that need a port say so. On Linux the user must be in `dialout`.use "serial.qwer"let PATH = env("SERIAL_PORT", "")let opened = nullfn scale() {    if PATH == "" { reply 503 "set SERIAL_PORT to a device" }    if !opened {        let got = serial_open("scale", PATH, { baud: 9600, parity: "even", data_bits: 7, stop_bits: 1, timeout_ms: 800 })        if is_err(got) { reply 503 got.error }        opened = got    }    opened}route GET /ports = { listed: len(serial_ports()?) >= 0, port_given: PATH != "" }route GET /open {    let s = scale()    { path_given: s.path == PATH, parity: s.parity, data_bits: s.data_bits, seen_by_device: serial_query("scale", "CONF?\r\n", {})? }}route GET /weigh {    scale()    let line = serial_query("scale", "W\r\n", {})?    let sics = serial_query("scale", "SI\r\n", {})?    { line: line, weight: serial_parse_weight(line)?, sics: serial_parse_weight(sics)? }}// The sensor sends its line in two pieces, a moment apart: read_line waits for the whole of it.route GET /sensor {    scale()    serial_write("scale", "SENSOR\r\n")?    let line = serial_read_line("scale", { timeout_ms: 1000 })?    { line: line, fields: serial_parse_fields(line) }}route GET /dump {    scale()    serial_write("scale", "DUMP\r\n")?    let log = serial_read_lines("scale", "END", {})?    serial_write("scale", "PROMPT\r\n")?    { log: log, before_prompt: serial_read_until("scale", "> ", {})?, count: len(log) }}route GET /binary {    scale()    let sent = serial_write_bytes("scale", [66, 73, 78, 13, 10])?    { sent: sent, got: serial_read_bytes("scale", 4, {})? }}route GET /silent {    scale()    let quiet = serial_query("scale", "QUIET\r\n", { timeout_ms: 150 })    serial_write("scale", "HALF\r\n")?    let half = serial_read_line("scale", { timeout_ms: 150 })    let rest = serial_read_available("scale", {})?    { quiet: quiet.error, half: half.error, rest: rest, cleared: serial_clear("scale")? }}route GET /reopen {    scale()    let closed = serial_close("scale")?    opened = null    { closed: closed, gone: serial_read_line("scale", {}).error, again: scale().parity, signals: len(serial_signals("scale")?) }}route GET /parse = {    gross: serial_parse_weight("ST,GS,+  12.345kg")?,    net_moving: serial_parse_weight("US,NT,-   0.120 kg")?,    bare: serial_parse_weight("   12.34 lb")?,    comma: serial_parse_weight("ST,GS,+ 1,500g")?,    sics_moving: serial_parse_weight("S D    99.98 g")?,    over: serial_parse_weight("S +").error,    busy: serial_parse_weight("S I").error,    too_much: serial_parse_weight("OL,GS,+ 999.99kg").error,    noise: serial_parse_weight("hello").error,    fields: serial_parse_fields("temp:21.5, rh:40 status=ok"),    nmea: serial_checksum("GPGLL,5300.97914,N,00259.98174,E,125926,A", "xor")?,    sum: serial_checksum("ABC", "sum")?,}route GET /errors = {    missing: serial_open("x", "/dev/no-such-port", {}).error,    baud: serial_open("x", "/dev/null", { baud: 0 }).error,    parity: serial_open("x", "/dev/null", { parity: "mark" }).error,    closed: serial_write("never", "x").error,}
$ quillo run serial.qwe$ curl 'localhost:8080/open'$ curl 'localhost:8080/weigh'$ curl 'localhost:8080/sensor'$ curl 'localhost:8080/dump'$ curl 'localhost:8080/binary'$ curl 'localhost:8080/silent'$ curl 'localhost:8080/reopen'$ curl 'localhost:8080/parse'$ curl 'localhost:8080/errors'

It talks to the Redis that REDIS_URL names, else Redis on this machine. The site's test runs it against a stand-in.

Source

serial.qwer
//! Serial ports for scales, sensors and instruments (the `serialport` crate): ports listed with their USB details, opened with baud, data bits, parity, stop bits and flow control, lines and bytes read with timeouts, reads up to a marker, question and answer, and the weight lines scales send, read.//! version: 1.0.0//!//! [dependencies]//! serialport = { version = "4.10", default-features = false }//! hex = "0.4"//! serde_json = { version = "1", features = ["preserve_order"] }use serde_json::{json, Map, Value};use serialport::{ClearBuffer, DataBits, FlowControl, Parity, SerialPort, SerialPortType, StopBits};use std::collections::HashMap;use std::io::{ErrorKind, Read, Write};use std::sync::{Arc, Mutex, OnceLock};use std::time::{Duration, Instant};const MAX_BUFFER: usize = 1 << 20;const DEFAULT_TIMEOUT: u64 = 1000;struct Port {    io: Box<dyn SerialPort>,    buf: Vec<u8>,    path: String,    timeout: u64,    line_ending: Vec<u8>,}fn ports() -> &'static Mutex<HashMap<String, Arc<Mutex<Port>>>> {    static P: OnceLock<Mutex<HashMap<String, Arc<Mutex<Port>>>>> = OnceLock::new();    P.get_or_init(|| Mutex::new(HashMap::new()))}fn port(name: &str) -> Result<Arc<Mutex<Port>>, String> {    ports().lock().map_err(|_| "lock")?.get(name).cloned().ok_or(format!("no serial port `{}`: call serial_open first", name))}fn with<T>(name: &str, f: impl FnOnce(&mut Port) -> Result<T, String>) -> Result<T, String> {    let p = port(name)?;    let mut g = p.lock().map_err(|_| "lock")?;    f(&mut g)}fn data_bits(o: &Value) -> Result<DataBits, String> {    Ok(match o.get("data_bits").and_then(|v| v.as_u64()).unwrap_or(8) {        5 => DataBits::Five,        6 => DataBits::Six,        7 => DataBits::Seven,        8 => DataBits::Eight,        n => return Err(format!("{} data bits: 5, 6, 7 or 8", n)),    })}fn parity(o: &Value) -> Result<Parity, String> {    Ok(match o.get("parity").and_then(|v| v.as_str()).unwrap_or("none") {        "none" | "n" => Parity::None,        "even" | "e" => Parity::Even,        "odd" | "o" => Parity::Odd,        p => return Err(format!("`{}` is not a parity: none, even or odd", p)),    })}fn stop_bits(o: &Value) -> Result<StopBits, String> {    Ok(match o.get("stop_bits").and_then(|v| v.as_u64()).unwrap_or(1) {        1 => StopBits::One,        2 => StopBits::Two,        n => return Err(format!("{} stop bits: 1 or 2", n)),    })}fn flow(o: &Value) -> Result<FlowControl, String> {    Ok(match o.get("flow").and_then(|v| v.as_str()).unwrap_or("none") {        "none" => FlowControl::None,        "hardware" | "rts" => FlowControl::Hardware,        "software" | "xon" => FlowControl::Software,        f => return Err(format!("`{}` is not flow control: none, hardware or software", f)),    })}/// The serial ports on this machine: `{ name, type }`, and for a USB adapter its `vid`, `pid`, `serial`, `manufacturer` and `product`.pub fn serial_ports() -> Result<Vec<Value>, String> {    let list = serialport::available_ports().map_err(|e| e.to_string())?;    Ok(list        .into_iter()        .map(|p| match p.port_type {            SerialPortType::UsbPort(u) => json!({                "name": p.port_name, "type": "usb", "vid": format!("{:04x}", u.vid), "pid": format!("{:04x}", u.pid),                "serial": u.serial_number, "manufacturer": u.manufacturer, "product": u.product,            }),            SerialPortType::PciPort => json!({ "name": p.port_name, "type": "pci" }),            SerialPortType::BluetoothPort => json!({ "name": p.port_name, "type": "bluetooth" }),            SerialPortType::Unknown => json!({ "name": p.port_name, "type": "unknown" }),        })        .collect())}fn builder(path: &str, baud: u32, o: &Value) -> Result<serialport::SerialPortBuilder, String> {    Ok(serialport::new(path, baud).data_bits(data_bits(o)?).parity(parity(o)?).stop_bits(stop_bits(o)?).flow_control(flow(o)?).timeout(Duration::from_millis(100)))}/// Opens a port under `name`: `options` takes `baud` (9600), `data_bits` (8), `parity` (none, even, odd), `stop_bits` (1), `flow` (none, hardware, software), `timeout_ms` (1000, the wait for reads that give none), `line_ending` ("\n"), and `dtr`. A port held by another program is an error saying so.pub fn serial_open(name: &str, path: &str, options: serde_json::Value) -> Result<Value, String> {    let baud = options.get("baud").and_then(|v| v.as_u64()).unwrap_or(9600);    if baud == 0 || baud > 4_000_000 {        return Err(format!("{} is not a baud rate", baud));    }    let baud = baud as u32;    let mut pseudo = false;    let io = match builder(path, baud, &options)?.open() {        Ok(p) => p,        // macOS sets speed with an ioctl that a pseudo-terminal refuses: it has no line speed.        Err(e) if cfg!(target_os = "macos") && e.to_string().contains("Not a typewriter") => {            pseudo = true;            builder(path, 0, &options)?.open().map_err(|e| format!("cannot open {}: {}", path, e))?        }        Err(e) => {            let m = e.to_string();            return Err(match e.kind() {                serialport::ErrorKind::NoDevice => format!("{}: no such port, or another program has it open ({})", path, m),                serialport::ErrorKind::Io(ErrorKind::NotFound) => format!("{}: no such port", path),                serialport::ErrorKind::Io(ErrorKind::PermissionDenied) => format!("{}: permission denied (on Linux, add the user to `dialout`)", path),                _ => format!("cannot open {}: {}", path, m),            });        }    };    let mut io = io;    if let Some(d) = options.get("dtr").and_then(|v| v.as_bool()) {        let _ = io.write_data_terminal_ready(d);    }    let timeout = options.get("timeout_ms").and_then(|v| v.as_u64()).unwrap_or(DEFAULT_TIMEOUT);    let line_ending = options.get("line_ending").and_then(|v| v.as_str()).unwrap_or("\n").as_bytes().to_vec();    if line_ending.is_empty() {        return Err("line_ending cannot be empty".into());    }    let p = Port { io, buf: vec![], path: path.to_string(), timeout, line_ending };    let info = describe(&p, pseudo);    ports().lock().map_err(|_| "lock")?.insert(name.to_string(), Arc::new(Mutex::new(p)));    Ok(info)}fn describe(p: &Port, pseudo: bool) -> Value {    json!({        "path": p.path,        "baud": p.io.baud_rate().ok(),        "data_bits": p.io.data_bits().ok().map(u8::from),        "parity": p.io.parity().ok().map(|x| x.to_string().to_lowercase()),        "stop_bits": p.io.stop_bits().ok().map(u8::from),        "pseudo_terminal": pseudo,    })}/// Changes an open port's settings: `baud`, `data_bits`, `parity`, `stop_bits`, `flow`, `timeout_ms`.pub fn serial_configure(name: &str, options: serde_json::Value) -> Result<Value, String> {    with(name, |p| {        if let Some(b) = options.get("baud").and_then(|v| v.as_u64()) {            p.io.set_baud_rate(b as u32).map_err(|e| e.to_string())?;        }        if options.get("data_bits").is_some() {            p.io.set_data_bits(data_bits(&options)?).map_err(|e| e.to_string())?;        }        if options.get("parity").is_some() {            p.io.set_parity(parity(&options)?).map_err(|e| e.to_string())?;        }        if options.get("stop_bits").is_some() {            p.io.set_stop_bits(stop_bits(&options)?).map_err(|e| e.to_string())?;        }        if options.get("flow").is_some() {            p.io.set_flow_control(flow(&options)?).map_err(|e| e.to_string())?;        }        if let Some(t) = options.get("timeout_ms").and_then(|v| v.as_u64()) {            p.timeout = t;        }        Ok(describe(p, false))    })}/// Closes a port, so another program can have it; true when it was open.pub fn serial_close(name: &str) -> Result<bool, String> {    Ok(ports().lock().map_err(|_| "lock")?.remove(name).is_some())}fn put(p: &mut Port, b: &[u8]) -> Result<(), String> {    p.io.write_all(b).map_err(|e| format!("cannot write to {}: {}", p.path, e))?;    p.io.flush().map_err(|e| format!("cannot write to {}: {}", p.path, e))}/// Writes text as it is; add your device's line ending. Answers the bytes written.pub fn serial_write(name: &str, text: &str) -> Result<i64, String> {    with(name, |p| {        put(p, text.as_bytes())?;        Ok(text.len() as i64)    })}/// Writes a list of byte values.pub fn serial_write_bytes(name: &str, data: Vec<i64>) -> Result<i64, String> {    let b: Vec<u8> = data.iter().map(|x| u8::try_from(*x).map_err(|_| format!("{} is not a byte", x))).collect::<Result<_, _>>()?;    with(name, |p| {        put(p, &b)?;        Ok(b.len() as i64)    })}// More bytes into the buffer, up to the deadline; false when none came.fn fill(p: &mut Port, end: Instant) -> Result<bool, String> {    let left = end.saturating_duration_since(Instant::now());    if left.is_zero() {        return Ok(false);    }    p.io.set_timeout(left).map_err(|e| e.to_string())?;    let mut t = [0u8; 1024];    match p.io.read(&mut t) {        Ok(0) => Ok(false),        Ok(n) => {            p.buf.extend(&t[..n]);            if p.buf.len() > MAX_BUFFER {                return Err(format!("{} sent over 1 MiB without what was being waited for", p.path));            }            Ok(true)        }        Err(e) if matches!(e.kind(), ErrorKind::TimedOut | ErrorKind::WouldBlock) => Ok(false),        Err(e) if e.kind() == ErrorKind::Interrupted => Ok(true),        Err(e) => Err(format!("cannot read {}: {}", p.path, e)),    }}fn find(b: &[u8], m: &[u8]) -> Option<usize> {    b.windows(m.len()).position(|w| w == m)}fn until(p: &mut Port, marker: &[u8], ms: u64, what: &str) -> Result<Vec<u8>, String> {    let end = Instant::now() + Duration::from_millis(ms);    let mut from = 0;    loop {        if let Some(i) = find(&p.buf[from..], marker) {            let at = from + i;            let out = p.buf[..at].to_vec();            p.buf.drain(..at + marker.len());            return Ok(out);        }        from = p.buf.len().saturating_sub(marker.len() - 1);        if !fill(p, end)? {            if Instant::now() >= end {                return Err(format!("timed out after {} ms waiting for {}; {} bytes are waiting", ms, what, p.buf.len()));            }        }    }}fn wait(o: &Value, p: &Port) -> u64 {    o.get("timeout_ms").and_then(|v| v.as_u64()).unwrap_or(p.timeout)}fn line(p: &mut Port, o: &Value) -> Result<String, String> {    let ending = p.line_ending.clone();    let got = until(p, &ending, wait(o, p), "a line")?;    let t = String::from_utf8_lossy(&got).to_string();    Ok(t.strip_suffix('\r').map(String::from).unwrap_or(t))}/// Reads one line, without its ending or a carriage return. On a timeout it is an error and what came so far stays for the next read. Options: `timeout_ms`.pub fn serial_read_line(name: &str, options: serde_json::Value) -> Result<String, String> {    with(name, |p| line(p, &options))}/// Reads lines until a line equals `last` (which is left out), or until nothing more comes for `timeout_ms` when `last` is empty, for a device that prints a block and stops.pub fn serial_read_lines(name: &str, last: &str, options: serde_json::Value) -> Result<Vec<String>, String> {    with(name, |p| {        let mut out = vec![];        loop {            match line(p, &options) {                Ok(l) if !last.is_empty() && l == last => return Ok(out),                Ok(l) => out.push(l),                Err(_) if last.is_empty() => return Ok(out),                Err(e) => return Err(format!("{} after {} lines", e, out.len())),            }        }    })}/// Reads up to a marker, text like "END" or "> "; answers what came before it.pub fn serial_read_until(name: &str, marker: &str, options: serde_json::Value) -> Result<String, String> {    if marker.is_empty() {        return Err("a marker cannot be empty".into());    }    with(name, |p| {        let ms = wait(&options, p);        let got = until(p, marker.as_bytes(), ms, &format!("{:?}", marker))?;        Ok(String::from_utf8_lossy(&got).to_string())    })}/// Reads exactly `count` bytes, as a list of numbers, for binary protocols. On a timeout the ones that came stay for the next read.pub fn serial_read_bytes(name: &str, count: i64, options: serde_json::Value) -> Result<Vec<u8>, String> {    if count < 0 || count as usize > MAX_BUFFER {        return Err("count must be from 0 to 1048576".into());    }    let n = count as usize;    with(name, |p| {        let ms = wait(&options, p);        let end = Instant::now() + Duration::from_millis(ms);        while p.buf.len() < n {            if !fill(p, end)? && Instant::now() >= end {                return Err(format!("timed out after {} ms: {} of {} bytes came", ms, p.buf.len(), n));            }        }        Ok(p.buf.drain(..n).collect())    })}/// Whatever has arrived, as text, waiting at most `options.timeout_ms` (0 unless given) for the first byte.pub fn serial_read_available(name: &str, options: serde_json::Value) -> Result<String, String> {    with(name, |p| {        let end = Instant::now() + Duration::from_millis(options.get("timeout_ms").and_then(|v| v.as_u64()).unwrap_or(0));        if p.buf.is_empty() {            fill(p, end)?;        }        while p.io.bytes_to_read().unwrap_or(0) > 0 {            if !fill(p, Instant::now() + Duration::from_millis(5))? {                break;            }        }        let t = String::from_utf8_lossy(&p.buf).to_string();        p.buf.clear();        Ok(t)    })}/// Question and answer: drops anything waiting, writes `text`, and reads the line that comes back, which is how most scales and meters work.pub fn serial_query(name: &str, text: &str, options: serde_json::Value) -> Result<String, String> {    with(name, |p| {        p.buf.clear();        p.io.clear(ClearBuffer::Input).map_err(|e| e.to_string())?;        put(p, text.as_bytes())?;        line(p, &options)    })}/// Drops whatever is waiting to be read; answers how many bytes went.pub fn serial_clear(name: &str) -> Result<i64, String> {    with(name, |p| {        let n = p.buf.len() as i64 + p.io.bytes_to_read().unwrap_or(0) as i64;        p.buf.clear();        p.io.clear(ClearBuffer::All).map_err(|e| e.to_string())?;        Ok(n)    })}/// The modem lines, null for each one the port cannot report: `{ cts, dsr, ri, cd }`.pub fn serial_signals(name: &str) -> Result<Value, String> {    with(name, |p| Ok(json!({ "cts": p.io.read_clear_to_send().ok(), "dsr": p.io.read_data_set_ready().ok(), "ri": p.io.read_ring_indicator().ok(), "cd": p.io.read_carrier_detect().ok() })))}fn weight_of(s: &str) -> Option<(f64, String)> {    let t = s.trim();    let end = t.rfind(|c: char| c.is_ascii_digit())?;    let unit = t[end + 1..].trim().to_string();    let start = t[..=end].rfind(|c: char| !(c.is_ascii_digit() || c == '.' || c == ' ' || c == '+' || c == '-' || c == ',')).map_or(0, |i| i + 1);    let n: String = t[start..=end].chars().filter(|c| !c.is_whitespace()).map(|c| if c == ',' { '.' } else { c }).collect();    n.parse().ok().map(|v| (v, unit))}/// Reads a scale's weight line: the common `ST,GS,+  12.345kg` (stable or unstable, gross or net), Mettler Toledo's MT-SICS `S S  100.00 g` and `S D ...`, and a bare `  12.34 lb`. Answers `{ value, unit, stable, kind }`, or an error for an overload.pub fn serial_parse_weight(line: &str) -> Result<Value, String> {    let t = line.trim();    if t.is_empty() {        return Err("an empty line".into());    }    if let Some(rest) = t.strip_prefix("S ") {        let rest = rest.trim_start();        match rest.chars().next() {            Some('+') if rest.len() == 1 => return Err("the scale is overloaded".into()),            Some('-') if rest.len() == 1 => return Err("the scale is underloaded".into()),            Some('I') => return Err("the scale is busy".into()),            Some('S') | Some('D') => {                let stable = rest.starts_with('S');                let (v, u) = weight_of(&rest[1..]).ok_or(format!("no weight in `{}`", t))?;                return Ok(json!({ "value": v, "unit": u, "stable": stable, "kind": "net" }));            }            _ => {}        }    }    let p: Vec<&str> = t.splitn(3, ',').collect();    if p.len() == 3 && p[0].len() == 2 && p[1].len() == 2 {        let state = p[0].to_uppercase();        if state == "OL" {            return Err("the scale is overloaded".into());        }        let kind = match p[1].to_uppercase().as_str() {            "GS" => "gross",            "NT" => "net",            "TR" => "tare",            _ => "unknown",        };        let (v, u) = weight_of(p[2]).ok_or(format!("no weight in `{}`", t))?;        return Ok(json!({ "value": v, "unit": u, "stable": state == "ST", "kind": kind }));    }    let (v, u) = weight_of(t).ok_or(format!("no weight in `{}`", t))?;    Ok(json!({ "value": v, "unit": u, "stable": Value::Null, "kind": "unknown" }))}/// Reads a sensor's `T=21.5;H=40` or `temp:21.5, rh:40` line into a map, numbers as numbers. The separators are `;`, `,` or spaces between pairs, and `=` or `:` inside them.pub fn serial_parse_fields(line: &str) -> Value {    let mut m = Map::new();    for p in line.split(|c: char| c == ';' || c == ',' || c == ' ' || c == '\t') {        if let Some((k, v)) = p.split_once(|c| c == '=' || c == ':') {            let (k, v) = (k.trim(), v.trim());            if k.is_empty() {                continue;            }            let val = match v.parse::<i64>() {                Ok(n) => json!(n),                Err(_) => match v.parse::<f64>() {                    Ok(f) if f.is_finite() => json!(f),                    _ => json!(v),                },            };            m.insert(k.to_string(), val);        }    }    Value::Object(m)}/// A checksum of a text, the two kinds serial protocols use: `xor` (NMEA's) or `sum` (the low byte of the total), as two hex digits.pub fn serial_checksum(text: &str, kind: &str) -> Result<String, String> {    let b = text.as_bytes();    let v = match kind {        "xor" => b.iter().fold(0u8, |a, x| a ^ x),        "sum" => b.iter().fold(0u8, |a, x| a.wrapping_add(*x)),        k => return Err(format!("`{}` is not a checksum: xor or sum", k)),    };    Ok(hex::encode_upper([v]))}
Aircraft 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 downloadsCAN bus and DBC files can_dbc.qwer · 1.0.0CAN bus frames made readable with a DBC file: messages and signals read with Intel or Motorola layout, scale, units, value names and multiplexing; frames decoded, frames built from values, and candump logs decoded.vehiclescarssensorsiotsignals8 views · 9 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 · 7 downloadsLoRaWAN lorawan.qwer · 1.0.0LoRaWAN 1.0 frames read on your own server: PHYPayload parsed, the MIC checked with AES-CMAC, FRMPayload decrypted, a replay refused, the 16-bit counter widened past 65535, MAC commands named, OTAA joins with their session keys, frames built for simulators, and Cayenne LPP sensor payloads both ways.iotsensorsradioencryption2 views · 9 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 downloadsOBD-II obd2.qwer · 1.0.0OBD-II answers from an ELM327 adapter decoded: speed, RPM, temperatures, load, fuel level and voltage, trouble codes such as P0133, and the PIDs a car supports.carsdevicesfleetsensorsiot8 views · 8 downloads
Thread
27Useful? Like it, or add to its thread below.

Thread 0 posts

On Serial ports: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.