Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/tasks/08-device-state-manager.md
Fimeg bde961c6f2 saf: one spine — device, state, and work under the index
PAF becomes saf/device (history kept), STATE.md dissolves
into saf/state.md with the dated era archived, the substrate
SAF moves up from souveraine, and every agreement points at
saf/INDEX.md and nowhere else. one map, nothing to remember
2026-08-18 09:47:30 -04:00

11 KiB
Raw Blame History

TASK 08 — Device state manager / power profiles

Status: in progress. The shell-side state projection and reason-tracked inhibitors exist; the durable manager and device tiers remain open. Largest of the tasks; sub-divide before starting.

Goal

One owner for device power states instead of scattered timeout listeners. Android is the reference — it has tiers of idle (screen-off, doze-light, doze-deep) with promotion rules. We have exactly one blunt s2idle and it wedges touch (TASK-09).

Wants (from handoff doc item 12):

  • (a) Idle tiers with device-aware gating. The souveraine settings device flag should gate which states a device may enter. blueline: no suspend until TASK-09 closes. SailfishOS mce (libwakelock.c + datapipe.c pub/sub) and FuriLabs batman (dual-source idle: logind IdleHint + wlr-output-management) are the steal-this prior art (both researched 2026-07-17 with full findings).

  • (b) Charge-aware profiles. UNBLOCKED 2026-07-24 — the kernel work is done and running. The old note here (no charge-threshold sysfs on mainline, needs software current-pacing) is obsolete: qcom_smbx.c now exposes read/write charge control, live on the device since kernel 7.1.1-sdm845-g58892902459c:

    /sys/class/power_supply/pmi8998-charger/
      charge_control_end_threshold   99      (percent, ABI-correct)
      charge_type                    N/A     (Trickle/Fast/Taper/…)
      voltage_max                    4395000 (CV target)
      constant_charge_current_max    1950000 (capped at 1C = 2.97A)
    

    upower exposes ChargeType + PercentageTrusted over D-Bus (the Souveraine fork, souveraine/charge-type). Both properties existed but reported defaults until 2026-07-25 — do not trust a D-Bus property merely because it is present. ChargeType read unknown and PercentageTrusted read no on hardware for days. Three fixes were needed, and neither of the first two moved the symptom on its own: 628283f (kernel spells NONE as N/A), 718e921 (SDM845 splits fuel gauge from charger — qcom-battery has no charge_type; walk the kernel's device links to pmi8998-charger), and f974714 — the one that actually did it: up-enumerator-udev.c tries UP_TYPE_DEVICE_SUPPLY_BATTERY first for every power_supply and only falls back to UP_TYPE_DEVICE_SUPPLY, so a real battery never ran the refresh where the fork set these. beb94dc fixes PercentageTrusted, whose heuristic tested charge_full — absent here, and not what upower reads the percentage from anyway (it uses capacity). Verified on device 2026-07-25: charge-type: fast against the charger's Fast. Note charge_type (active regime) is a different attribute from charge_types (the settable profile list up-device-supply-battery.c already uses for thresholds) — easy to conflate when wiring (b). Hardware gaps that are not bugs: charge_full, charge_now, energy_*, cycle_count and model/serial are all absent, so energy-full falls back to design (13.068 Wh), health reads a meaningless 100%, and charge-cycles is N/A. Also seen: temp = 570 → 57 °C while charging — verify whether that is real before building thermal policy on it. pop-os/system76-power charge_thresholds.rs is still the capability-probing pattern to copy — probe, do not assume, since the attrs exist only on this kernel. Cosmetic: 4.4V encodes to 4.395V so the threshold getter reads 99, not 100. Errs low, safe. Full design + review history: saf/device/evidence/power/HANDOFF-upower-charge-control.md.

  • (c) USB role awareness. The USB-C role/mode plumbing installed earlier has NO UI toggle. Shell should show current mode (gadget/host/charge-only) and switch it. abrauchli/usbeehive (Rust MIT, typec.rs/pd.rs sysfs) is liftable as a dependency. No existing OSS GUI toggle to fork — build fresh.

  • (d) Wifi handoff gating. Radio transitions belong to the same state model, not ad-hoc.

  • (e) Bounded maintenance/update leases. A deploy or system update may inhibit automatic idle lock while it mutates live session components, but never bypass an existing lock or disable manual lock. The lease has an owner/reason/deadline, survives a shell reload, expires on deploy failure, disconnect or timeout, and restores the exact prior idle state.

Context

  • Idle coordinator already exists: souveraine b18fa33, native path ENABLED on phone (dim 120s / lock 300s). See handoff doc "Idle manager, first real pass" for the hard-won Wayland protocol-error lesson (inhibit checked INSIDE handlers, monitor objects never destroyed).
  • hypridle is the redundant fallback (300s lock, 600s screen-off, suspend DISABLED per TASK-09 interim).
  • Session.qml already implements state(), reason-tracked inhibit() / uninhibit(), and the Idle.inhibit projection. A 2026-07-21 audit found that no IpcHandler actually exposed those methods despite comments saying it did; a worktree fix adds the missing local session IPC seam.
  • Measured maintenance failure (2026-07-21 19:29): the phone auto-locked during a live shell deploy. Quickshell reloaded while the compositor lock was secure, hit FATAL: Tried to show lockscreen surfaces without active lock, crashed, and sessiond correctly fail-closed. Repeated shell owners then contended for notifications/polkit until cleanup. A deploy must never race the idle lock again.
  • The conclusions want to land in power-profiles-daemon as the hint layer.

Two things the machine knows and nothing says (open, 2026-07-26)

(f) sensors_degraded reaches no surface. Source health landed (DEVICE-STATE-MACHINE.md §10): a source that reported and went silent for 90 s is down, not quiet, and every forensic snapshot carries sensors_degraded. The trail knows. No screen does. The crash reporter structurally cannot help — all three of its sources key on something failing, and the SLPI outage exited status 0 (archive/05). This is the surfacing half: an ambient indicator when a source that used to work has stopped, legible on the lock surface, saying which and since when. device_state already ships sensor_health over IPC.

(g) The confidence gates are computed, logged, and never branched on. §4's threshold table (<0.3 / 0.30.6 / 0.60.8 / >0.8) and the cross-sensor disagreement rules are written to the trail and ignored. This needs a decision before code, and the decision moved on 2026-07-26: the table was written when proximity suppressed DPMS wake and blanked locked screens, and proximity now does neither — it vetoes tap-to-wake and is otherwise evidence. So the question is no longer "what should confidence gate" but "is anything left for it to gate". The honest candidate is the doze promotion rate and possibly nothing else. Answer that, then wire it or delete the table and record why.

(h) Thermal policy — the zones are there and the policy is empty (2026-08-04)

Measured on the device, so this is inventory rather than plan:

  • 23 thermal zones, all on the step_wise governor. power_allocator (IPA — the PID that allocates a power budget across cooling devices) is available on every zone and used by none.
  • Cooling devices: cpufreq-cpu0 (17 states), cpufreq-cpu4 (23), devfreq-5000000.gpu (6), plus cx and ebi.
  • Trip points: passive at CPU 90/95 °C, GPU 85 °C, PMIC 105 °C; hot 90, critical 110. Nothing below 85 °C. So nothing throttles until the die is already hot, and there is no gentle first response at all.
  • cpuidle: two states (WFI, little-rail power collapse), governor menu, and menu is the only one built. Android downstream had far deeper cluster and rail collapse; upstream teo is not available to compare against.
  • GPU: simple_ondemand devfreq, idling at 257 MHz.
  • Battery reads 41 °C at 22% while idle — and note the existing warning above about temp = 570 while charging. Verify the sensor before building policy on it; a skin model fed a lying thermistor is worse than no model.

The Android prior art, and which parts transfer. The HAL types zones (CPU/GPU/SKIN/BATTERY/USB_PORT/POWER_AMPLIFIER) and maps each to a severity ladder — NONE → LIGHT → MODERATE → SEVERE → CRITICAL → EMERGENCY → SHUTDOWN. That ladder is the interesting part and it is ours to define, because it is policy, not hardware. PowerManager.getThermalHeadroom() is its app-facing half: a normalized 01 forecast some seconds ahead, so software degrades itself before the kernel throttles it. Skin temperature is a virtual sensor — a weighted model over die zones, because there is no thermistor on the glass; on SDM845 those coefficients lived in Qualcomm's per-device thermal-engine.conf, which is the one piece of blueline-specific prior art worth going and reading.

How it lands here, and the trap to avoid. §12's rule is not optional and an outside adviser will propose breaking it: a thermal daemon that reads a zone and calls a governor is the eighth blind actor. Temperature enters through sensor_input like proximity and lux; the response leaves as an Action from tick(), through the executor table that already owns Dim/Restore/Blank. One writer, one trail. The severity ladder is then a DeviceStatePolicy field the agent can read and rewrite, which is what makes "the phone is getting warm" something she can act on instead of something that happens to her.

Do the cheap thing first. TASK-15's 2026-08-04 measurement found ~36% of a core burning at idle in the sensor stack, with the SoC nowhere near any trip. No thermal policy would have helped that. Fix the spinning before modelling the heat.

Sub-tasks (do separately)

  1. State machine + pub/sub (mce/datapipe shape), device-flag gating.
  2. Charge-aware profile (needs the PMIC investigation first).
  3. USB role UI toggle.
  4. Wifi handoff in the state model.
  5. Maintenance/update lease in the durable manager. Interim deploy.sh may use a systemd-timer-backed hypridle lease, but it must first refuse an already locked session and schedule automatic recovery before stopping idle handling.

Start with (1) — it's the spine the others hang off.

Acceptance

One daemon owns device power state. session state IPC reports the current tier. A plugged-in-at-100% phone doesn't float-charge blindly. USB mode is visible and switchable from the shell.

An update lease is visible in the same state projection with owner, reason and deadline. Manual lock during a lease wins immediately. Kill the deploy/SSH mid-run: automatic idle handling returns within the bounded timeout.

Connects to

TASK-09 (suspend must be safe before it's a tier), idle coordinator (b18fa33), session-authority layer (handoff item 4).