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
8.3 KiB
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.000s, 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 baselineBASELINE_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 wayRELEASE_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
DozeDeepexists 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.soshipsAggregateDetector,DetectorSetup,Filter,decision_threshold_min/max,global_sensitivityand aGESTURE_PROGRESSstream 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_controlandhaptic_gpio 88say the buzz is the nanoapp's job downstream; ours is the shell guessing. - Nothing proves which binary is reporting. sessiond takes an
inputfrom 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_rawtoday. 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.