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

188 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).