Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/saf/device/edge-sense.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

166 lines
8.3 KiB
Markdown
Raw Permalink 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.

# PAF · Active Edge (squeeze)
The Pixel 3's squeeze gesture. **Working on Arch as of 2026-07-28** — force is
detected, and the gesture reaches the shell through the session authority.
Spine doc: current truth, updated in place. The narrative of how it was found
lives in `../../SouveraineOS/docs/tasks/13-active-edge-squeeze.md`, which keeps
each superseded conclusion verbatim beneath its correction.
## The hardware chain
Six strain gauges bonded to the frame → **MAX11261** ADC on the SLPI's own I²C
bus → the `max11259` SEE driver on the sensor island → published over SSC as the
data type **`elmyra_raw`**, SUID `15717249801838747009 / 6291402259395006624`.
Not evdev, not a GPIO key. There is no input node and nothing bridges the SUID
to one; the only way to it is an SSC client. `sns_touch_gesture` is a **different
sensor** — the FTS double-tap nanoapp on the touch controller (slave 0x49, bus 5,
irq 125). Opening that one and squeezing correctly produces nothing.
**Power: PM8998 GPIO 2**, gpiochip0 line 1, held by a devicetree `gpio-hog`
(`edge_sense_pwr_hog` in `sdm845-google-common.dtsi`). Confirm with:
sudo gpioinfo | sed -n '3p'
# line 1: unnamed output consumer="edge-sense"
## Why it did not work before, and what fixed it
Through 2026-07-27 the gauges converted and drifted but **ignored force**: 399
samples across 35 s of hard squeezes gave a median deflection of 49 and a max of
127, against a resting noise floor of ~50. The conclusion at the time was that
the analog front end is only configurable from inside the `elmyra` CHRE nanoapp
via `ashConfigureElmyraFilters`, and that an SSC client gets whatever the AFE
defaults to.
On 2026-07-28 the **same unchanged binary** with **no nanoapp loaded** measured
deflections of 25022990 under real squeezes.
What changed between the two measurements is the rail. It became a DT
`gpio-hog` (`318f7d0`) instead of a `gpioset` unit that ran during boot. The
likely mechanism is **ordering, not voltage**: the hog asserts the rail at
kernel probe, before SSC enumerates the MAX11261, where the unit powered it
after the ADC had already been probed unpowered. An AFE initialised without its
bridge excited stays wrong until reset.
That is the leading explanation and it is **not proven**. Confirming it means
booting a pre-hog kernel and re-measuring. The electrical prediction made at the
time — that a pinctrl node changes nothing, since PM8998 GPIO 2 reads `vin-0`
on both stacks because neither selects one — still stands and was never the
issue.
## Calibration is real and per-unit
`/persist/sensors/elmyra/calibration.{0..5}`, factory-written 2019, one file per
gauge:
touch_2_sensitivity: 0.2402884277733113
This unit: `0.240 0.295 0.176 0.359 0.329 0.342`. They normalise the gauges
against each other; the spread is why Google ships a factory calibration step
(`edgesense_roller_cal/`) at all. The producer reads them at startup and logs
them — if that line shows six `1.000`s, the persist partition is not mounted or
the files are gone, and detection will be lopsided rather than absent.
These are consumed on the **AP side** by our producer. Nothing serves them over
HexagonFS: `hexagonrpcd`'s virtual `/persist/sensors/` contains only `registry`
(`rpcd_builder.c`). That matters only if the nanoapp path is ever taken.
## The threshold model
From Google's own sweep, `edgesense_roller_cal/0_raw/sensor_data.csv`, which
captures the gauges at a known 10 N and 20 N — the loaded bank moves 4002000
counts per 10 N step. Raw values sit near ±100000, so an absolute threshold
against them is meaningless; the first version compared exactly that and never
fired.
- `DEFAULT_THRESHOLD 2500` — summed normalised deflection from a rolling baseline
- `BASELINE_ALPHA 0.02` — the baseline must follow temperature and grip drift,
and is frozen while a squeeze is held or the release reads as a squeeze the
other way
- `RELEASE_FRACTION 0.6` — hysteresis, same reason proximity has it
Observed squeezes land at 25022990, which is close to the line. The binary
takes the threshold as an argument, so tuning needs no rebuild.
## It reports; it does not decide
The producer does **not** call the shell. It asks `souveraine-sessiond` first:
{"op":"input","trigger":"squeeze"} → $XDG_RUNTIME_DIR/souveraine/sessiond.sock
and delivers `qs -c souveraine ipc call gesture squeeze` only on `ok:true`.
sessiond treats a squeeze as real user input — it resets the idle budget and
lands in the forensic trail — but applies the pocket veto first: a covered
proximity sensor refuses it with `refused_by_state`, exactly as it already
refused tap-to-wake, because a phone in a tight pocket *is* a squeezed chassis.
Calling the shell directly, which is what it did first, made this an eighth
blind actor: no idle reset, nothing in the trail, no veto, and no way for the
machine to notice the source dying.
**On an unreachable authority it fails closed** and says so. A squeeze that
cannot be checked is not delivered; the alternative would mean the pocket veto
silently ceases to exist every time sessiond restarts.
What a squeeze *means* is the shell's routing table (`services/Gestures.qml`,
`squeezeAction`, default `dial``dial toggle`), never this producer's business.
## Packaging
`pkgs/blueline-edge-sense``blueline-edge-sense 0.2.0-1`, aarch64, built by
`build.sh` on archdev. Ships the binary, the user unit, and a
`souveraine-sessiond.service.wants` symlink so it is **enabled by default** and
survives a reprovision — the reader wants the authority it reports to, not the
session at large. It hung off `graphical-session.target` until 2026-07-29,
which started nothing then; the target is live as of TASK-76, but sessiond
remains the correct thing to want.
Before 2026-07-28 the binary sat unowned in `/usr/bin` since 07-26 — one of
TASK-25's 5,624. Installing the package hit that exact conflict; the old copy is
at `/var/backups/unowned-preupgrade/blueline-edge-sense.20260728`.
## Known gaps
- **No detection while the AP sleeps.** This is an AP-side process sampling six
channels at 100 Hz forever, which is precisely what `DozeDeep` exists to kill.
Squeeze-to-wake from deep doze is only possible from the DSP side — the
nanoapp. Grip and doze are currently mutually exclusive and nothing in the
code knows it.
- **Our detector, not Google's.** `elmyra.so` ships `AggregateDetector`,
`DetectorSetup`, `Filter`, `decision_threshold_min/max`, `global_sensitivity`
and a `GESTURE_PROGRESS` stream that would give hold-vs-tap. Ours is a
baseline-delta threshold firing 2 counts above its own line.
- **No haptic confirmation from the right place.** `elmyra_haptics_control` and
`haptic_gpio 88` say the buzz is the nanoapp's job downstream; ours is the
shell guessing.
- **Nothing proves which binary is reporting.** sessiond takes an `input` from
whoever connects to the socket, and the shell takes a gesture from whoever
calls its IPC — both demonstrated from an SSH shell on 2026-07-28. TASK-41.
- **Inference posture unresolved.** TASK-40 designates the raw 100 Hz six-gauge
stream a bring-up instrument, not a product surface: it is a typing
side-channel and a grip biometric. Enabled anyway by Casey's call, because the
exposure is not created by this reader — any AP process linking libssc can
open `elmyra_raw` today. Confining *that* is TASK-40's half.
## Where the code is
| What | Where |
|---|---|
| Producer | `pkgs/blueline-edge-sense/edge-sense.c` |
| Package + unit | `pkgs/blueline-edge-sense/{PKGBUILD,build.sh,*.service}` |
| Rail (gpio-hog) | kernel `sdm845-google-common.dtsi`, `edge_sense_pwr_hog` |
| libssc fixes | `pkgs/libssc-souveraine` — single-output sensors, fastest advertised rate (1 Hz → 100 Hz) |
| Authority side | souveraine `src/sessiond/{protocol,device_state,server}.rs``InputTrigger::Squeeze`, `suppress_wake`, `note_input_gated` |
| Routing | souveraine `surfaces/quickshell/services/Gestures.qml` |
| Nanoapp host | `pkgs/hexagonrpc` (chrecd) — loads `elmyra`, sends `RECOGNIZER_START` |
## Verifying it live
systemctl --user status blueline-edge-sense
journalctl --user -u blueline-edge-sense -f
A healthy start logs the six sensitivities, the SSC open, and a six-value
baseline. A squeeze logs `SQUEEZE (deflection N)` then either `squeeze
delivered` or the authority's refusal — and a refusal is normal operation, not
a fault.