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

8.3 KiB
Raw Blame History

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.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 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 dialdial toggle), never this producer's business.

Packaging

pkgs/blueline-edge-senseblueline-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}.rsInputTrigger::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.