Light drifts continuously; monitor-sensor emitted ~8 lines/sec and each was a socket round trip re-asserting the same Changing(false). The keepalive is what keeps silence meaningful, so only changes need reporting.
317 lines
13 KiB
Rust
317 lines
13 KiB
Rust
//! souveraine-sensord — one reporter for every iio-sensor-proxy source.
|
|
//!
|
|
//! REPORTER, NOT AN AUTHORITY. It reads sensors and tells sessiond. It decides
|
|
//! nothing, actuates nothing, and reads no lock state. Every decision belongs
|
|
//! to the device state machine (`DEVICE-STATE-MACHINE.md` §1) — the whole point
|
|
//! of that document is that we had seven actors each seeing one facet and
|
|
//! acting on it blindly.
|
|
//!
|
|
//! ## Why this replaces the scripts
|
|
//!
|
|
//! `blueline-proximity-lock` is a 113-line shell script implementing a contract
|
|
//! that is genuinely subtle: heartbeat inside `SOURCE_DOWN_AFTER`, seed the
|
|
//! last value from the startup probe banner, report both edges, pass the last
|
|
//! reading between a subshell and a background loop through a file in
|
|
//! `$XDG_RUNTIME_DIR`. All of that is correct and all of it would have to be
|
|
//! copied, verbatim and by hand, into a second script for light and a third for
|
|
//! accel — §12 says decide this before writing the second one, not after the
|
|
//! third.
|
|
//!
|
|
//! Copies drift. The `blueline-*` units are exactly the "script-based random
|
|
//! stuff" that has to become one orchestrated thing.
|
|
//!
|
|
//! ## The contract it implements (§10)
|
|
//!
|
|
//! `monitor-sensor` emits only on CHANGE, so a phone on a table is silent for
|
|
//! hours and is byte-for-byte indistinguishable from a dead SLPI. Silence must
|
|
//! therefore be made meaningful: every source re-sends its last known reading
|
|
//! every `KEEPALIVE`, comfortably inside the machine's 90 s `SOURCE_DOWN_AFTER`.
|
|
//! As long as this process and the sensor behind it live, sessiond hears from
|
|
//! each source on a schedule. If it stops hearing, something is actually wrong.
|
|
//!
|
|
//! Repeats are idempotent: the machine re-derives identical evidence and skips
|
|
//! the trail write, so a keepalive can never move the state.
|
|
//!
|
|
//! Health is deliberately NOT read from `net.hadess.SensorProxy`'s
|
|
//! `HasProximity`/`HasAccelerometer`. §10 records it answering wrongly in both
|
|
//! directions — `true` for hours after the stack died, `false` after remoteproc
|
|
//! had already recovered. What can be trusted is our own experience of whether
|
|
//! readings arrive.
|
|
|
|
use std::collections::HashMap;
|
|
use std::io::{BufRead, BufReader, Read, Write};
|
|
use std::os::unix::net::UnixStream;
|
|
use std::path::PathBuf;
|
|
use std::process::{Child, Command, Stdio};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Duration;
|
|
|
|
/// Re-send interval for each source's last known reading. Must stay well under
|
|
/// the machine's `SOURCE_DOWN_AFTER` (90 s) so a healthy source gets several
|
|
/// chances to be heard before it is called down.
|
|
const KEEPALIVE: Duration = Duration::from_secs(30);
|
|
|
|
/// How long to wait for sessiond to accept and answer. Short: it answers in
|
|
/// microseconds when healthy, and a reporter must never block on the authority.
|
|
const SESSIOND_TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
/// Backoff when `monitor-sensor` exits or the proxy has no sensors yet.
|
|
const RESPAWN_DELAY: Duration = Duration::from_secs(5);
|
|
|
|
/// The last reading we sent per source, in the exact JSON shape sessiond's
|
|
/// `SensorInput` expects. Shared with the keepalive thread.
|
|
type LastSeen = Arc<Mutex<HashMap<&'static str, serde_json::Value>>>;
|
|
|
|
fn socket_path() -> PathBuf {
|
|
let runtime = std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| {
|
|
format!("/run/user/{}", unsafe { libc_getuid() })
|
|
});
|
|
PathBuf::from(runtime).join("souveraine/sessiond.sock")
|
|
}
|
|
|
|
/// Avoid a libc dependency for one call.
|
|
unsafe fn libc_getuid() -> u32 {
|
|
std::fs::read_to_string("/proc/self/loginuid")
|
|
.ok()
|
|
.and_then(|s| s.trim().parse().ok())
|
|
.unwrap_or(1000)
|
|
}
|
|
|
|
/// Send one reading. Returns false when sessiond could not be reached — which
|
|
/// is information, not a fatal error: the daemon restarts independently of us.
|
|
fn report(sock: &PathBuf, source: &str, value: &serde_json::Value) -> bool {
|
|
let req = serde_json::json!({
|
|
"op": "sensor_input",
|
|
"source": source,
|
|
"value": value,
|
|
});
|
|
|
|
let attempt = || -> std::io::Result<()> {
|
|
let stream = UnixStream::connect(sock)?;
|
|
stream.set_read_timeout(Some(SESSIOND_TIMEOUT))?;
|
|
stream.set_write_timeout(Some(SESSIOND_TIMEOUT))?;
|
|
let mut w = stream.try_clone()?;
|
|
w.write_all(format!("{req}\n").as_bytes())?;
|
|
w.flush()?;
|
|
// Read the reply so the daemon is not left writing into a closed pipe.
|
|
let mut buf = [0u8; 4096];
|
|
let mut r = stream;
|
|
let _ = r.read(&mut buf)?;
|
|
Ok(())
|
|
};
|
|
|
|
match attempt() {
|
|
Ok(()) => true,
|
|
Err(e) => {
|
|
eprintln!("sessiond unreachable; {source} reading dropped: {e}");
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One source's parse rules: the monitor-sensor flag, the change line it emits,
|
|
/// and how to turn that line into the value sessiond wants.
|
|
struct Source {
|
|
name: &'static str,
|
|
flag: &'static str,
|
|
/// Substring identifying a change line for this source.
|
|
change_marker: &'static str,
|
|
/// Substring identifying this source's startup banner, used to seed the
|
|
/// keepalive before the first change arrives. Without a seed, a phone that
|
|
/// booted onto a table stays silent and a stack that was dead from boot is
|
|
/// invisible.
|
|
banner_marker: &'static str,
|
|
parse: fn(&str, &mut ParserState) -> Option<serde_json::Value>,
|
|
}
|
|
|
|
/// `Proximity value changed: 0` / banner `=== Has proximity sensor (near: 0)`
|
|
fn parse_proximity(line: &str, _state: &mut ParserState) -> Option<serde_json::Value> {
|
|
let near = !(line.trim_end().ends_with(": 0") || line.contains("near: 0"));
|
|
Some(serde_json::json!({ "near": near }))
|
|
}
|
|
|
|
/// `Light changed: 240.000000 lux`
|
|
///
|
|
/// The wire carries `SensorValue::Changing(bool)`, not lux. That is what the
|
|
/// deployed sessiond accepts, and sending a shape it does not know earns an
|
|
/// `invalid_argument` refusal rather than a reading.
|
|
///
|
|
/// §12 argues lux itself should reach the machine, because auto-brightness
|
|
/// needs a level and not an edge. That is a protocol change with a delivery-
|
|
/// order problem attached (TASK-28: a new reporter against an old daemon), so
|
|
/// it is deliberately not smuggled in here. Until then "changing" means the
|
|
/// level moved by more than `LIGHT_CHANGE_LUX` since the last reading we sent.
|
|
fn parse_light(line: &str, state: &mut ParserState) -> Option<serde_json::Value> {
|
|
let lux: f64 = line
|
|
.rsplit(':')
|
|
.next()?
|
|
.split_whitespace()
|
|
.next()?
|
|
.parse()
|
|
.ok()?;
|
|
let changing = match state.last_lux {
|
|
Some(prev) => (lux - prev).abs() > LIGHT_CHANGE_LUX,
|
|
// The startup banner is a level, not a change. Reporting "changing" on
|
|
// it would hand the confidence table a presence signal produced by
|
|
// nothing more than the reporter starting up.
|
|
None => false,
|
|
};
|
|
state.last_lux = Some(lux);
|
|
Some(serde_json::json!({ "changing": changing }))
|
|
}
|
|
|
|
/// `Accelerometer orientation changed: normal`
|
|
///
|
|
/// The wire carries `Moving(bool)`. iio-sensor-proxy reports orientation, not
|
|
/// motion, so movement is inferred from the orientation changing at all — which
|
|
/// is a weak signal and is exactly why §4 gives accel 0.3 and not more. A phone
|
|
/// being carried face-up in a steady hand reports nothing.
|
|
fn parse_accel(line: &str, state: &mut ParserState) -> Option<serde_json::Value> {
|
|
let orientation = line.rsplit(':').next()?.trim().to_string();
|
|
let moving = match &state.last_orientation {
|
|
Some(prev) => *prev != orientation,
|
|
None => false,
|
|
};
|
|
state.last_orientation = Some(orientation);
|
|
Some(serde_json::json!({ "moving": moving }))
|
|
}
|
|
|
|
/// Per-source memory the parsers need to turn levels into edges.
|
|
#[derive(Default)]
|
|
struct ParserState {
|
|
last_lux: Option<f64>,
|
|
last_orientation: Option<String>,
|
|
}
|
|
|
|
/// How much lux must move before it counts as "changing". A hand passing over
|
|
/// the sensor is a large, brief swing; room lighting drifts slowly. This is a
|
|
/// placeholder in the same sense §9.5's debounce was before the trail corrected
|
|
/// it — take the real number off the trail, not out of a comment.
|
|
const LIGHT_CHANGE_LUX: f64 = 15.0;
|
|
|
|
const SOURCES: &[Source] = &[
|
|
Source {
|
|
name: "proximity",
|
|
flag: "--proximity",
|
|
change_marker: "Proximity value changed: ",
|
|
banner_marker: "Has proximity",
|
|
parse: parse_proximity,
|
|
},
|
|
Source {
|
|
name: "light",
|
|
flag: "--light",
|
|
change_marker: "Light changed: ",
|
|
banner_marker: "Has ambient light sensor",
|
|
parse: parse_light,
|
|
},
|
|
Source {
|
|
// "accelerometer" on the wire, not "accel". SensorSource derives its
|
|
// JSON from snake_case variant names; `as_str()`'s shorter "accel" is
|
|
// the trail/health key only, and sending it here earns a refusal.
|
|
name: "accelerometer",
|
|
flag: "--accel",
|
|
change_marker: "Accelerometer orientation changed: ",
|
|
banner_marker: "Has accelerometer",
|
|
parse: parse_accel,
|
|
},
|
|
];
|
|
|
|
/// Re-send every source's last reading forever. This is the half that makes
|
|
/// silence meaningful; see the module docs.
|
|
fn spawn_keepalive(sock: PathBuf, last: LastSeen) {
|
|
std::thread::spawn(move || loop {
|
|
std::thread::sleep(KEEPALIVE);
|
|
let snapshot: Vec<(&'static str, serde_json::Value)> = {
|
|
let guard = match last.lock() {
|
|
Ok(g) => g,
|
|
Err(p) => p.into_inner(),
|
|
};
|
|
guard.iter().map(|(k, v)| (*k, v.clone())).collect()
|
|
};
|
|
for (source, value) in snapshot {
|
|
report(&sock, source, &value);
|
|
}
|
|
});
|
|
}
|
|
|
|
fn spawn_monitor() -> std::io::Result<Child> {
|
|
let mut cmd = Command::new("monitor-sensor");
|
|
for s in SOURCES {
|
|
cmd.arg(s.flag);
|
|
}
|
|
cmd.stdout(Stdio::piped()).stderr(Stdio::null()).spawn()
|
|
}
|
|
|
|
fn main() {
|
|
let sock = socket_path();
|
|
let last: LastSeen = Arc::new(Mutex::new(HashMap::new()));
|
|
spawn_keepalive(sock.clone(), Arc::clone(&last));
|
|
|
|
// One monitor-sensor for every source, respawned if it dies. Restarting the
|
|
// process is the recovery path for a sensor stack that came back after an
|
|
// SLPI reset; the keepalive keeps reporting the stale-but-last value in the
|
|
// meantime, and the machine's own freshness rules decide what that is worth.
|
|
loop {
|
|
let child = match spawn_monitor() {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
eprintln!("could not start monitor-sensor: {e}");
|
|
std::thread::sleep(RESPAWN_DELAY);
|
|
continue;
|
|
}
|
|
};
|
|
let stdout = match child.stdout {
|
|
Some(s) => s,
|
|
None => {
|
|
std::thread::sleep(RESPAWN_DELAY);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let mut state = ParserState::default();
|
|
for line in BufReader::new(stdout).lines() {
|
|
let line = match line {
|
|
Ok(l) => l,
|
|
Err(_) => break,
|
|
};
|
|
|
|
for s in SOURCES {
|
|
// Seed from the banner so the keepalive has something to send
|
|
// before the first change ever arrives.
|
|
let is_banner = line.contains(s.banner_marker);
|
|
if !is_banner && !line.contains(s.change_marker) {
|
|
continue;
|
|
}
|
|
let Some(value) = (s.parse)(&line, &mut state) else { continue };
|
|
// Only report when the value the machine sees actually
|
|
// changes. monitor-sensor emits a line per lux sample and
|
|
// ambient light drifts continuously — measured on device at
|
|
// ~8 lines/second, every one of them a socket round trip
|
|
// carrying `Changing(false)` again. The keepalive already
|
|
// guarantees sessiond hears from every source inside
|
|
// SOURCE_DOWN_AFTER, so silence between real changes is safe
|
|
// and is exactly what §10 arranged for.
|
|
let unchanged = {
|
|
let mut guard = match last.lock() {
|
|
Ok(g) => g,
|
|
Err(p) => p.into_inner(),
|
|
};
|
|
let same = guard.get(s.name) == Some(&value);
|
|
guard.insert(s.name, value.clone());
|
|
same
|
|
};
|
|
if unchanged {
|
|
break;
|
|
}
|
|
// Report both edges. "Far" matters as much as "near": it is
|
|
// what re-enables a wake, and it is what proves the sensor is
|
|
// still alive.
|
|
report(&sock, s.name, &value);
|
|
break;
|
|
}
|
|
}
|
|
|
|
eprintln!("monitor-sensor exited; respawning");
|
|
std::thread::sleep(RESPAWN_DELAY);
|
|
}
|
|
}
|