orin-status: report the CAN interface (up, bus state, frames/s); --can picks it
/ crosscheck (push) Successful in 1s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDwfd6Ki8UfG4hK6W5YpJz
This commit is contained in:
2026-10-13 17:58:41 +09:00
parent a1722736fd
commit 00e8cf9e4e
3 changed files with 64 additions and 3 deletions
+55
View File
@@ -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<String> {
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<u64> = 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())));
}
}
}
}
}
+8 -2
View File
@@ -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/<signal>`, 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<String, String> {
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