diff --git a/README.md b/README.md index f7d63b5..c0bbd47 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ needed. | `recorder` | `yukart-recorder.service` | Raw CAN + GNSS to disk, in sessions, under a storage bound; OBD-II values from the bus beside them | | `bus-bench` | not installed: a test tool | Raw CAN and decoded-signal load over MQTT, measured: loss, latency, broker CPU ([report](docs/impl/reports/BUS-BENCH.md)) | | `obd-poller` | `yukart-obd-poller.service` | OBD-II mode 01 polling, the single CAN writer: rpm, speed, throttle, load, MAP, MAF ~10 Hz; temperatures and fuel 1 Hz (`yukart/vehicle/*`) | -| `orin-status` | `yukart-orin-status.service` | The Orin's health on the bus, once a second: temperatures, CPU/GPU load, memory, power, fan, WiFi (with SSID)/LTE/tablet links, UPS battery (`yukart/host/*`) | +| `orin-status` | `yukart-orin-status.service` | The Orin's health on the bus, once a second: temperatures, CPU/GPU load, memory, power, fan, WiFi (with SSID)/LTE/tablet links, the CAN interface, UPS battery (`yukart/host/*`) | ```bash scripts/deploy-recorder.sh # build, glibc check, install on the car Orin diff --git a/src/orin-status/can.rs b/src/orin-status/can.rs new file mode 100644 index 0000000..735de9f --- /dev/null +++ b/src/orin-status/can.rs @@ -0,0 +1,55 @@ +//! The CAN interface the recorder reads (the PEAK on the OBD port): up or down, frames +//! per second from the sysfs counter, and the controller's bus state from `ip`. Shown so +//! a down interface is seen before a capture, not discovered as refused marks. + +use std::fs; +use std::path::Path; +use std::process::Command; +use std::time::Instant; + +use serde_json::{json, Value}; + +pub struct Can { + iface: String, + prev: Option<(Instant, u64)>, +} + +/// Controller state as `ip -j -d link show` reports it: ERROR-ACTIVE, ERROR-WARNING, +/// ERROR-PASSIVE, BUS-OFF or STOPPED. +fn bus_state(iface: &str) -> Option { + let out = Command::new("ip").args(["-j", "-d", "link", "show", iface]).output().ok()?; + let v: Value = serde_json::from_slice(&out.stdout).ok()?; + Some(v[0]["linkinfo"]["info_data"]["state"].as_str()?.to_string()) +} + +impl Can { + pub fn new(iface: &str) -> Self { + Can { iface: iface.to_string(), prev: None } + } + + pub fn read(&mut self, out: &mut Vec<(&'static str, Value)>) { + let dir = Path::new("/sys/class/net").join(&self.iface); + if !dir.exists() { + self.prev = None; + out.push(("can_up", json!(false))); + out.push(("can_bus_state", json!("absent"))); + return; + } + // operstate is "up" for an interface that is up, "unknown" on some CAN drivers: use the flags. + let flags = fs::read_to_string(dir.join("flags")).ok().and_then(|s| u32::from_str_radix(s.trim().trim_start_matches("0x"), 16).ok()); + out.push(("can_up", json!(flags.is_some_and(|f| f & 1 != 0)))); + if let Some(state) = bus_state(&self.iface) { + out.push(("can_bus_state", json!(state))); + } + let rx: Option = fs::read_to_string(dir.join("statistics/rx_packets")).ok().and_then(|s| s.trim().parse().ok()); + if let Some(rx) = rx { + let now = Instant::now(); + if let Some((t, p)) = self.prev.replace((now, rx)) { + let dt = now.duration_since(t).as_secs_f64(); + if dt > 0.0 && rx >= p { + out.push(("can_rx_fps", json!(((rx - p) as f64 / dt).round()))); + } + } + } + } +} diff --git a/src/orin-status/main.rs b/src/orin-status/main.rs index c3c2f86..cf244b1 100644 --- a/src/orin-status/main.rs +++ b/src/orin-status/main.rs @@ -1,7 +1,7 @@ //! orin-status — the Orin's own health on the bus, once a second. //! //! Temperatures, CPU and GPU load, the shared memory pool, module power, the fan, -//! network links (WiFi, LTE, the USB link to the tablet) and the UPS battery, each +//! network links (WiFi, LTE, the USB link to the tablet), the CAN interface and the UPS battery, each //! as an envelope on `yukart/host/`, retained. Names and units come from //! contracts' `signals.json` (source `host`). //! @@ -9,6 +9,7 @@ //! it stale, which is the honest picture. The service's own state goes on //! `yukart/system/orin-status`, with a last will for when it vanishes. +mod can; mod host; mod net; mod ups; @@ -25,6 +26,7 @@ use serde_json::{json, Value}; const USAGE: &str = "\ usage: orin-status [options] --broker HOST:PORT MQTT broker (127.0.0.1:1883) + --can IFACE CAN interface to report (can_pcan) --i2c DEV I²C bus of the UPS INA219 (/dev/i2c-7) --ups-addr ADDR INA219 address, hex (41) --once print one reading as JSON and exit (no broker)"; @@ -57,6 +59,7 @@ fn units() -> std::collections::HashMap { struct Sources { host: host::Host, net: net::Net, + can: can::Can, ups: ups::Ups, } @@ -67,6 +70,7 @@ impl Sources { let mut failed = Vec::new(); self.host.read(&mut out); self.net.read(&mut out); + self.can.read(&mut out); if !self.ups.read(&mut out) { failed.push("ups"); } @@ -77,6 +81,7 @@ impl Sources { fn main() -> ExitCode { let mut broker = "127.0.0.1:1883".to_string(); let mut i2c = "/dev/i2c-7".to_string(); + let mut can_iface = "can_pcan".to_string(); let mut addr: u16 = 0x41; let mut once = false; let mut args = std::env::args().skip(1); @@ -85,6 +90,7 @@ fn main() -> ExitCode { "--once" => once = true, "--broker" => broker = args.next().unwrap_or_default(), "--i2c" => i2c = args.next().unwrap_or_default(), + "--can" => can_iface = args.next().unwrap_or_default(), "--ups-addr" => match args.next().and_then(|v| u16::from_str_radix(v.trim_start_matches("0x"), 16).ok()) { Some(v) => addr = v, None => { @@ -100,7 +106,7 @@ fn main() -> ExitCode { } let units = units(); - let mut src = Sources { host: host::Host::discover(), net: net::Net::start(), ups: ups::Ups::new(&i2c, addr) }; + let mut src = Sources { host: host::Host::discover(), net: net::Net::start(), can: can::Can::new(&can_iface), ups: ups::Ups::new(&i2c, addr) }; if once { src.read(); // rates and CPU load need two readings