Watch
1
0
Fork
You've already forked souveraine
0

sensord: stop claiming the accelerometer

Measured on the phone, idle, screen on, same session, one flag apart:

  monitor-sensor --proximity --light --accel   iio-sensor-proxy  15.3%
  monitor-sensor --proximity --light           iio-sensor-proxy   1.1%

Claiming the accelerometer makes iio-sensor-proxy poll the IIO device
continuously; nothing else here does. Fourteen points of a core, forever,
for the reading §4 weights least (+0.3) and calls a weak signal — and one
the keepalive had to actively decay, because monitor-sensor speaks only on
orientation change, so a stationary phone paid the full poll cost to
report nothing.

The parser, the motion decay and MOTION_WINDOW go with it; all three
existed only to turn orientation into a motion edge. The 2026-07-27
finding behind the decay is kept in comment — 165 Moving(true) against 8
Moving(false) on one boot — because whatever reports motion next owes a
decay too.

This does not say accel is unwanted. It says a subprocess holding a
continuous claim is the wrong way to get it; TASK-36's SLPI batching is
the right one. Restore the source together with that, or the cost returns.
This commit is contained in:
Fimeg 2026-08-04 16:17:21 -04:00
commit 6b67512c4f

View file

@ -202,34 +202,13 @@ fn parse_light(line: &str, state: &mut ParserState) -> Option<serde_json::Value>
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 changed = match &state.last_orientation {
Some(prev) => *prev != orientation,
None => false,
};
state.last_orientation = Some(orientation);
if changed {
state.last_orientation_change = Some(Instant::now());
}
Some(serde_json::json!({ "moving": accel_moving(state, Instant::now()) }))
}
/// Whether the device counts as moving right now: an orientation change inside
/// `MOTION_WINDOW`. Shared by the parser and the keepalive so the decay has
/// exactly one definition.
fn accel_moving(state: &ParserState, now: Instant) -> bool {
match state.last_orientation_change {
Some(t) => now.duration_since(t) < MOTION_WINDOW,
None => false,
}
}
// The accelerometer parser and its motion decay lived here. They went with the
// accel claim itself — see SOURCES for the measurement — because both existed
// only to turn iio-sensor-proxy's *orientation* into a motion edge, and there
// is no orientation stream when the source is not claimed. The inference was
// always weak (a phone carried face-up in a steady hand reported nothing),
// which is why §4 weighted it 0.3. TASK-36's SLPI batching reports motion
// directly and does not need any of this.
/// Per-source memory the parsers need to turn levels into edges.
#[derive(Default)]
@ -242,14 +221,9 @@ struct ParserState {
/// moment the level falls back inside the band.
pending_since: Option<Instant>,
pending_brightening: bool,
last_orientation: Option<String>,
/// When orientation last changed. `MOTION_WINDOW` decays from here, and the
/// keepalive reads it to clear a stale `moving: true`.
last_orientation_change: Option<Instant>,
}
/// Shared so the keepalive thread can decay motion; the read loop owns the
/// writes.
/// Shared between the read loop and the keepalive snapshot.
type Parser = Arc<Mutex<ParserState>>;
/// How much lux must move before it counts as "changing", measured against the
@ -278,19 +252,15 @@ const LIGHT_CHANGE_LUX: f64 = 15.0;
const LIGHT_BRIGHTENING_DEBOUNCE: Duration = Duration::from_millis(2000);
const LIGHT_DARKENING_DEBOUNCE: Duration = Duration::from_millis(4000);
/// How long after an orientation change the device still counts as moving.
///
/// iio-sensor-proxy emits a line only when orientation CHANGES, so "moving"
/// inferred from an edge can never fall back to false on its own — measured on
/// device 2026-07-27, boot 0 carried 165 `Moving(true)` against 8
/// `Moving(false)`, all the falses at startup. The keepalive then re-sent a
/// stuck `true` every 30 s, pinning accel's +0.3 into `observed_confidence` for
/// the life of the process and holding the machine permanently at or above the
/// 0.3 "suppress DPMS wake" band.
///
/// So motion decays. An orientation change means moving for this long, and the
/// keepalive clears it once the window passes.
const MOTION_WINDOW: Duration = Duration::from_secs(3);
// MOTION_WINDOW lived here: how long after an orientation change the device
// still counted as moving. Kept in the history rather than the binary, because
// the finding behind it survives the source being dropped and TASK-36 will need
// it again — measured on device 2026-07-27, boot 0 carried 165 `Moving(true)`
// against 8 `Moving(false)`, all the falses at startup. An edge-derived
// "moving" can never fall back to false on its own, so the keepalive re-sent a
// stuck `true` every 30 s, pinning accel's +0.3 into `observed_confidence` for
// the life of the process and holding the machine at or above the 0.3
// "suppress DPMS wake" band. Whatever reports motion next owes a decay.
/// No source may report more often than this.
///
@ -319,42 +289,45 @@ const SOURCES: &[Source] = &[
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,
},
// The accelerometer is NOT claimed, and that is a power decision.
//
// Measured on the phone 2026-08-04, idle, screen on, same session, one flag
// apart:
//
// monitor-sensor --proximity --light --accel iio-sensor-proxy 15.3%
// monitor-sensor --proximity --light iio-sensor-proxy 1.1%
//
// Claiming the accelerometer makes iio-sensor-proxy poll the IIO device
// continuously; nothing else here does. So ~14 points of a core were being
// spent, forever, on the one reading §4 gives the least weight (+0.3) and
// that §9's own note calls a weak signal — and which the keepalive had to
// actively decay because monitor-sensor only speaks on orientation change,
// meaning a stationary phone paid the full poll cost to report nothing.
//
// This is the cheap half of TASK-15. It does not say accel is unwanted: it
// says a subprocess holding a continuous claim is the wrong way to get it.
// TASK-36's SLPI batching is the right one — the sensor hub already
// aggregates motion and can report on an interval instead of being polled.
// Restore this entry only together with that, or the cost comes back.
//
// "accelerometer" was the name on the wire, not "accel": SensorSource
// derives its JSON from snake_case variant names, and the shorter form is
// the trail/health key only. Noted here so a future restore does not
// rediscover the refusal.
];
/// 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, parser: Parser) {
fn spawn_keepalive(sock: PathBuf, last: LastSeen) {
std::thread::spawn(move || loop {
std::thread::sleep(KEEPALIVE);
// Decay motion before snapshotting. monitor-sensor will not emit
// another accelerometer line until the orientation changes AGAIN, so if
// the keepalive did not re-derive this the last `moving: true` would be
// re-sent forever and accel's +0.3 would never leave the confidence sum.
{
let state = match parser.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
let moving = accel_moving(&state, Instant::now());
drop(state);
let mut guard = match last.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
if let Some(v) = guard.get_mut("accelerometer") {
*v = serde_json::json!({ "moving": moving });
}
}
// The motion decay that used to run here went with the accelerometer
// claim (see SOURCES). It existed because monitor-sensor only speaks on
// orientation change, so a stale `moving: true` would otherwise be
// re-sent forever and pin accel's +0.3 into the confidence sum. With
// the source unclaimed there is no accelerometer entry to decay, and
// leaving the decay in would have been a loop deriving a value nothing
// reports. It comes back with the source, not before.
let snapshot: Vec<(&'static str, serde_json::Value)> = {
let guard = match last.lock() {
Ok(g) => g,
@ -384,7 +357,7 @@ fn main() {
// reset the lux reference or re-arm motion, or every SLPI blip would look
// like a fresh change.
let parser: Parser = Arc::new(Mutex::new(ParserState::default()));
spawn_keepalive(sock.clone(), Arc::clone(&last), Arc::clone(&parser));
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