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
166 lines
8.3 KiB
Markdown
166 lines
8.3 KiB
Markdown
# 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 2502–2990 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 400–2000
|
||
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 2502–2990, 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.
|