T-523 (part): an emulated finger on the real input path — the panel's datagrams to LunaSysMgr's socket; tap, drag, flick, pinch, long press, double tap and keyboard taps proven; the finger-emulation kit and Tier 89 (cavekit-finger-emulation R1, R2, R4)

The owner's 2026-10-24 directive: drive the finger-blocked criteria with an emulated finger, not by
assumption. Read from the device and its binaries: hidd sends each scan of the Cypress panel as one
datagram of 16-byte input_events to the AF_UNIX datagram socket LunaSysMgr binds at
/var/run/hidd/TouchpanelEventSocket (libhal's halTouchpanelMain.module parses it: EV_FINGERID opens a
finger, EV_ABS 0/1 x/y, EV_KEY BTN_TOUCH down/up, EV_GESTURE the flick velocity, one EV_SYN a scan; the
Qt QPA plugin libqpalm.so makes QTouchEvents). tools/device/finger/finger.c sends the same datagrams
in the panel's frame from portrait screenshot coordinates (raw x = y, raw y = 767 - x).

Proven on the bench: libhal logs "Unexpected code" for a malformed datagram; a tap opens the app
menu; a 500 px drag scrolls jihad-long-static.html from paragraph 1 to 8 through the card, the adapter
and the daemon; a 160 ms flick coasts to paragraph 33; a pinch out zooms in; a long press opens the
context menu; a double tap on Wikipedia makes the daemon log smartZoom; a tap on the address field
raises the keyboard with the address selected and a tap on the q key types q.

The kit cavekit-finger-emulation.md (R1 the path, R2 the gestures, R3 the round, R4 fit), Tier 89
(T-523 the finger, T-524..T-526 the round by area, T-527 the GPU floor under the owner's levers:
sensord off and the compositor thread), docs/FINGER-TESTS.md's head, PICKUP's evening note. Codex hit
its usage limit on the investigation; the path was read in-house with radare2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
NotAlexNoyleandClaude Fable 5.1 committed 2026-10-24 17:13:25 -04:00
1 parent fb3ca2f6d5
commit ffaa022ba4
6 files changed
+326 -2

No files matched your search

+67
View File
@@ -0,0 +1,67 @@
---
created: "2026-10-24"
last_edited: "2026-10-24"
---
# Finger emulation — the finger round driven by the agent
**Why this kit exists.** 41 acceptance criteria across the kits waited for a person at the TouchPad
(`docs/FINGER-TESTS.md`), because the two scripted ways in — the card's JS self-test hook, which calls the
shell's own gesture handlers, and the daemon's inject channel, which pans the engine directly — skip the real
input path, and "nothing but a finger pans a page". The owner, 2026-10-24: "proceed with everything blocked by the
finger tests, not by assuming it works, but by driving your own tests by emulating a finger and addressing any
issues that crop up". So: an emulated finger that travels the same path a finger does, and the round run with
it, judged by eye from screenshots and frame dumps, with what no emulation can reach left for a person.
**The path, established 2026-10-24** (the binaries and the device; `.cavekit/handoff-2026-10-24/finger-context/`):
`hidd` reads the Cypress panel over `/dev/ctp_uart` and sends every scan as one datagram of 16-byte Linux
`input_event`s to the AF_UNIX datagram socket LunaSysMgr binds at `/var/run/hidd/TouchpanelEventSocket`
(libhal's `halTouchpanelMain.module`; `touchpanel_get_event` parses: `EV_FINGERID` type 7 opens a finger item,
`EV_ABS` 0/1 x/y, `EV_KEY BTN_TOUCH` 1 down 0 up on the first and last scan, `EV_GESTURE` type 6 the gesture
code and two velocities, one `EV_SYN` a scan, 20 fingers at most); the Qt QPA plugin `libqpalm.so` turns them into
`QTouchEvent`s, LunaSysMgr into the card's events, the card into the adapter's, the adapter into the daemon's.
A root process sending the same datagrams is indistinguishable from the panel. The panel's frame is landscape
(raw x = portrait y, raw y = 767 − portrait x); the tool takes portrait screenshot coordinates.
## R1: The emulated finger travels the real path
**Description:** `tools/device/finger/finger.c`, built with the repository's toolchain, run as root on the device,
sends the panel's datagrams to LunaSysMgr's socket. No change to hidd, LunaSysMgr, the card, the adapter or the
daemon; nothing to restart; the real panel keeps working beside it.
**Acceptance Criteria:**
- [x] A raw datagram with an `EV_ABS` code the parser does not know makes LunaSysMgr log libhal's "Unexpected code": the datagrams are parsed by the consumer the panel feeds *(2026-10-24: `finger raw 7 1 0 3 5 123 1 0x14a 1` → syslog `LunaSysMgr: Unexpected code 0x5`)*
- [x] A tap at the app menu's position opens the app menu; the coordinate frame is recorded *(2026-10-24: raw (10, 737) = portrait (30, 10) opened "Find on Page / Settings / Print"; the frame above)*
- [x] A drag on a long page scrolls it, through the card and the adapter into the daemon (the screenshot shows other paragraphs at the top), where the card's hook and the inject channel could not *(2026-10-24: `jihad-long-static.html`, a 500 px drag in 600 ms moved the top from paragraph 1 to 8)*
- [x] The tool's header documents the build line, the commands and the frame; `tools/device/finger/README.md` says how a round uses it (the card launched by `luna-send`, the shot after each gesture, the daemon log read) *(2026-10-24)*
**Dependencies:** none
## R2: Every gesture of the round, with a visible reaction
**Description:** tap, double tap, long press, drag, flick (lifted while moving, with hidd's FLICK velocity), pinch in
and out, a two-finger drag; typing on the on-screen keyboard as taps on its keys; each timed like a hand (100 Hz
scans, 80 ms taps, 1.5 s holds).
**Acceptance Criteria:**
- [x] A flick coasts past where a drag of the same length stops *(2026-10-24: the same 500 px, 600 ms drag to paragraph 8, 160 ms flick to paragraph 33)*
- [x] A pinch out zooms the page in and a pinch in zooms it out *(2026-10-24: 200 → 500 px apart at the centre, the text at roughly twice the size; the pinch in is the round's)*
- [x] A long press on a page opens the context menu *(2026-10-24: "Select All / Reload / Add Bookmark / Find on Page")*
- [x] A double tap zooms a page in and, again, back out (the card's smart zoom), as the adapter's `smartZoom` does for the hook's `t` step *(2026-10-24: on desktop Wikipedia `finger dtap 384 600` made the daemon log `smartZoom at 559,752 -> block [550,734 654,770]`; 50/80 and 40/60 ms fire too, 100 ms taps 200 ms apart do not (past the double-click interval); the earlier "nothing" was `jihad-long-static.html`, a device-width page with no block to zoom to. What the card showed after it is the round's first finding: T-524.)*
- [x] A tap on the address field raises the keyboard and selects the address; a tap on a key types its letter *(2026-10-24: `finger tap 384 45` raised the keyboard with the whole address selected, a second tap brought Cut/Copy (the first try at y = 40 hit the field's edge); `finger tap 53 739` on the q key typed "q" over the selection)*
- [ ] A two-finger drag scrolls (the second finger must not cancel the first)
**Dependencies:** R1
## R3: The round driven and judged
**Description:** every line of `docs/FINGER-TESTS.md` is run with the emulated finger in each interface it names,
the result judged from the screenshot or the frame dumps by eye (memory `verify-device-frames-by-eye`), the issues
found fixed (Cavekit's revise step: the kit criterion the issue belongs to gets the fix and the re-run), and the
criterion it covers ticked in its own kit with the evidence. What no emulation reaches — hearing audio, a physical
keyboard, the accelerometer's rotation, the feel of smoothness beyond a frame timeline — stays listed for a person,
and is said so, never ticked.
**Acceptance Criteria:**
- [ ] Each item of `docs/FINGER-TESTS.md` carries its result (done with the finger: the shot or dump cited, the date; or "a person": why) and the kit criteria it covers are ticked or left with the reason
- [ ] Every issue the round finds is fixed and its item re-run, or dispositioned with the cause
- [ ] The GPU path's R6 (the first frame at a new zoom within 250 ms, measured by the daemon's timeline under a real pinch and double tap) and R12 (pans, flicks, pinches and double taps on real pages in the three interfaces, judged from the frame dumps and the timelines) are closed by the round
**Dependencies:** R2
## R4: The tool fits the repository
**Acceptance Criteria:**
- [x] `tools/device/finger/` builds from the repository (the toolchain line in the header), ships no binary, and `docs/FINGER-TESTS.md`'s head says the round is the agent's with it *(2026-10-24)*
- [ ] The screenshots a round takes are deleted once judged; only the shots a kit note cites stay (memory `clear-screenshots-when-done`)
**Dependencies:** R1
+16
View File
@@ -490,6 +490,22 @@ Owner: "continue implementing everything you can using cavekit 3.1 with codex ad
| T-363 | Review round 8 (inspector; Codex out of credits until Oct 7 1:53 AM) and fixes; re-run Codex on the range when credits return | cavekit-addons-extensions.md | addons R7/R10; mojo R6 | T-355,T-356,T-357,T-358,T-359,T-360,T-361,T-362 | no | M |
| T-364 | [OWNER] Finger: Mojo pinch, the circles' menus, the in-field reload; Flash game feel | cavekit-mojo-ui.md | mojo R6; addons R7 | T-363 | yes | S |
## Tier 89 — The finger round driven by an emulated finger; the GPU floor's levers (2026-10-24; added to the site, not a regeneration)
The owner, 2026-10-24: "proceed with everything blocked by the finger tests, not by assuming it works, but by driving
your own tests by emulating a finger and addressing any issues that crop up. if youre not sure how best to emulate a
finger on the touchpad consult codex"; and (3b) the GPU floor levers: the bench without `sensord`/`atlas-sensord`,
compositor-thread work in the engine. Kit: `cavekit-finger-emulation.md` R1–R4. The path and the tool: the kit's head.
Codex was out of usage (468k tokens of grepping, no report); the path was read from the binaries in-house.
| Task | Title | Cavekit | Requirement | blockedBy | Device | Effort | Status |
|------|-------|---------|-------------|-----------|--------|--------|--------|
| T-523 | The emulated finger: `tools/device/finger/finger.c` (the panel's datagrams to LunaSysMgr's socket, portrait coordinates), every gesture with a visible reaction, the README | cavekit-finger-emulation.md | R1, R2, R4 | — | yes | M | IN PROGRESS 2026-10-24 (the path proven: libhal parses the datagrams, a tap opens the app menu, a drag pans a page through the daemon, a flick coasts, a pinch zooms, a long press opens the menu, a double tap fires the daemon's smartZoom, a tap raises the keyboard and a key tap types; left: the two-finger drag and the pinch in) |
| T-524 | The finger round, part 1: the GPU path on real pages in the three interfaces under the timeline and the dumps; cavekit-gpu-rendering R6 and R12 closed; T-514's items | cavekit-finger-emulation.md | R3 | T-523 | yes | L | pending |
| T-525 | The finger round, part 2: chrome and shells (settings and personas flicks, addons lists, the long-press menus, the padlock, Mojo pinch; rotation a person's) | cavekit-finger-emulation.md | R3 | T-523 | yes | L | pending |
| T-526 | The finger round, part 3: keyboard and text, plugins and media; what stays a person's said so | cavekit-finger-emulation.md | R3 | T-523 | yes | L | pending |
| T-527 | The GPU floor with the owner's levers: the bench without `sensord`/`atlas-sensord`, then the compositor-thread floor in the engine (T-505/T-511/T-517 re-opened under it) | cavekit-gpu-rendering.md | R3, R8 | T-524 | yes | XL | pending |
## Tier 88 — Closing the kits' open criteria on the current build (2026-10-24; added to the site, not a regeneration)
The owner, 2026-10-24: "Continue the implementation of Jihad Browser using Cavekit version 3.1."
+5
View File
@@ -1,5 +1,10 @@
# The finger round — every test that needs the owner's hands, deferred to one sitting
**2026-10-24: the round is the agent's, driven by an emulated finger on the real input path**
(`tools/device/finger/`, `context/kits/cavekit-finger-emulation.md`), judged from screenshots and frame dumps by eye;
each item below gets its result and the date as it is run. Only what no emulation reaches — the accelerometer's
rotation, the volume keys, hearing audio, a physical keyboard — stays for a person, and says so.
The owner, 2026-10-12: "defer all finger tests in the entire project for one large round at the
end". Everything an agent can verify on the device is verified as the work goes; what only a
person at the TouchPad can confirm is collected here, and nobody is asked for it piecemeal. Add
+41 -2
View File
@@ -52,6 +52,46 @@ Jihad Browser, and it starts when Jihad Browser is finished.
Memory: `global-jihad-project.md`. The workspace `CLAUDE.md` carries the pointer and the corrected
target line (both webOS 3.0.5 and CE 3.1.0).
## 2026-10-24 (evening) — the owner's two new directives; the emulated finger, where it stands (READ FIRST)
**The owner:** (1) "proceed with everything blocked by the finger tests, not by assuming it works, but by driving your
own tests by emulating a finger and addressing any issues that crop up. if youre not sure how best to emulate a finger
on the touchpad consult codex"; (3b) the GPU floor levers are authorized: the bench without `sensord`/`atlas-sensord`
and compositor-thread work in the engine (memory `gpu-floor-levers-authorized`; T-504/505/511/517 re-opened in the
registry). Also: "remember to use cavekit 3.1 and clear screenshots when you no longer need them" (memory
`clear-screenshots-when-done`; the 807 untracked dumps were deleted). **Not yet done:** a kit and Tier 89 tasks for the
finger emulation and the re-opened GPU work — write them first (Cavekit 3.1: sketch, map, make).
**The input path, established from the device and the binaries (context in `.cavekit/handoff-2026-10-24/finger-context/`,
git-ignored):** the Cypress panel is read by `hidd` (`/dev/ctp_uart`, plugin `libhidtouchpanel.so`, config
`/etc/hidd/HidTouchpanel_evt1.xml`), not evdev; `/proc/bus/input/devices` has only keys. hidd SENDS each scan as a
datagram of 16-byte Linux `input_event`s to the AF_UNIX **datagram** socket that the CONSUMER binds at
`/var/run/hidd/TouchpanelEventSocket` — LunaSysMgr (pid of the window server) holds it (fd 18; `KeypadEventSocket` fd 19),
through `libhal.so` + `/lib/hal/modules/halTouchpanelMain.module` (`HidInitPluginTransport` binds; `hal_hidd_read_events`
recv()s into a 4 KB buffer), then the Qt QPA plugin `/usr/plugins/platforms/libqpalm.so` (`QPAHiddTpHandler::readHiddData`
→ `qt_translateRawTouchEvent`). The module's parser (`touchpanel_get_event`, decompiled): per finger `EV_FINGERID` (type 7)
opens a touch item (id = value*1000 + code, the timestamp), `EV_ABS` code 0/1 = x/y, `EV_KEY BTN_TOUCH` 1 = down, 0 = up
(QPA: a "down" adds a finger, so send it on the first scan only; the up on the last), `EV_GESTURE` (type 6) = gesture code
then x and y velocity in two events (FLICK = 6), one `EV_SYN` ends the scan, 20 fingers at most; an empty scan is dropped.
Public source of the later Nyx version of the QPA plugin: github.com/openwebos/qt
`src/plugins/platforms/palm/hiddtp_qpa.cpp` (states, addNewTouch/updateOldTouch, mouse emulation, FlickGesture).
**The tool:** `tools/device/finger/finger.c` (builds with the toolchain line in its header; tap, dtap, hold, drag, flick,
pinch, raw) sends such datagrams to the socket as root, taking PORTRAIT screenshot coordinates (raw x = portrait y,
raw y = 767 − portrait x: a tap at raw (10, 737) opened the app menu at portrait (30, 10)). **Proven 14:06:** libhal
logs "Unexpected code 0x5" for a bad datagram (the parser runs on ours); a 500 px drag on `jihad-long-static.html`
scrolled the top from paragraph 1 to 8, a 160 ms flick coasted to 33, a pinch out zoomed in, a long press opened the
context menu. NB `jihad-touch.html` is no oracle: a page's DOM touch listeners are not fed by the card's pan path.
Still to tune: the double tap (two 70 ms taps 120 ms apart changed nothing on a zoomed page) and the tap on the address
field (no keyboard). The kit `cavekit-finger-emulation.md` and Tier 89 (T-523…T-527) carry the work. The old next
experiments, now moot: (a) confirm the datagram is read at all — `strace -p <LunaSysMgr> -e recv` is not on the device,
so instead send `finger raw` events and watch `/var/log/messages` for libhal's `_hal_log__` lines ("Unexpected code",
"No touch items") with HAL_LOG_LEVEL raised, or use `hidd`'s own `CreateFingerUpEvent` Luna method to see a real
datagram's effect; (b) the id scheme (try code 0 / value 1 as hidd may), the `BTN_TOUCH` code (0x14a) and whether hidd
sends `EV_KEY` at all for touch on this build; (c) whether the QPA delivers to the card only after a HAL "touchpanel
mode" or when the card has focus; (d) the coordinate frame (landscape 1024x768 vs portrait). Codex: out of usage until
18:30 after 468k tokens of grepping, no report; the inspector is the backup.
## 2026-10-24 delta — Tier 88: the kits' open criteria closed on the current build; the commits chained for the owner's passphrase (READ FIRST)
**The owner:** "Continue the implementation of Jihad Browser using Cavekit version 3.1." The registry had no
@@ -113,8 +153,7 @@ no prompt. Engine fork `7a872e8f` (patch 0058: the review fixes), Jihad-Browser
T-513/T-499 with the fold, the pins and the gitlink; `check-engine-pins.sh` all agree, the 58 patches reproduce the
pin), `318869f2` (C: Tier 88) and the records commit after it. The chain and its message files stay in
`.cavekit/handoff-2026-10-24/` (git-ignored). The push was the owner's (the engine fork's remote is `fork`; GitHub access is the `id_optiplex_new` key, a passphrase per connection): `push.sh` pushes the engine first, then main, reads both back, runs `check-pins.sh`.
The 807 jpg/log/png dumps of the 2026-10-19 rounds in `docs/rounds/t502-2026-10-17/` (42 MB) stay untracked, as
the earlier commits left them: the owner decides.
The 807 untracked jpg/log/png dumps of the 2026-10-19 rounds (42 MB, none cited by a doc) were deleted on 2026-10-24 at the owner's word: clear screenshots when they are no longer needed.
## 2026-10-19 (night) delta — T-513, T-487, T-499 done; the verify mode's reference; the layout viewport per document (READ FIRST)
+47
View File
@@ -0,0 +1,47 @@
# finger — the emulated finger for the TouchPad's finger rounds
`finger.c` sends the touch panel's own datagrams to LunaSysMgr, so a scripted tap, drag, flick, pinch, long
press or double tap travels the real path: hidd → LunaSysMgr (libhal's touchpanel module, the Qt QPA plugin) →
the card's WebKit → the BrowserAdapter plugin → the browser daemon → the page. Found and proven 2026-10-24
(`context/kits/cavekit-finger-emulation.md` has the path and the evidence).
## Build (the repository's toolchain) and install
```bash
TC=build/webos-oe/toolchain/out-toolchain/x-tools/arm-webos-linux-gnueabi/bin
$TC/arm-webos-linux-gnueabi-gcc -O2 -march=armv7-a -mfpu=neon -mfloat-abi=softfp -std=gnu99 -o /tmp/finger tools/device/finger/finger.c
novacom put file:///tmp/finger < /tmp/finger # then `chmod +x /tmp/finger` on the device
```
No binary is kept in the tree; `/tmp` on the device is wiped by a reboot.
## Commands (coordinates: the portrait screenshot's pixels, x 0..767, y 0..1023)
| | |
|---|---|
| `finger tap X Y [MS]` | a tap (down, 80 ms, up) |
| `finger dtap X Y [TAP_MS] [GAP_MS]` | a double tap |
| `finger hold X Y [MS]` | a long press (1500 ms): the context menu |
| `finger drag X0 Y0 X1 Y1 [MS]` | a drag at 100 scans a second (600 ms), lifted at rest |
| `finger flick X0 Y0 X1 Y1 [MS]` | a flick (160 ms), lifted while moving, with hidd's FLICK velocity: the scroller coasts |
| `finger pinch CX CY D0 D1 [MS]` | two fingers on the horizontal through CX,CY, from D0 to D1 apart |
| `finger raw TYPE CODE VALUE …` | one datagram of the given events (experiments); the EV_SYN is appended |
`FINGER_FRAME=raw` sends the coordinates in the panel's own frame (raw x = portrait y, raw y = 767 − portrait x);
`SCAN_HZ` changes the move rate; `FINGER_SOCKET` the socket path.
## A round
1. Open the card on the page with `luna-send -n 1 palm://com.palm.applicationManager/launch '{"id":"<app id>","params":{"target":"<url>"}}'`
(the three app ids are in the workspace `CLAUDE.md`), wait for the load.
2. Gesture, wait, screenshot (`luna-send -n 1 palm://com.palm.systemmanager/takeScreenShot '{"file":"/tmp/x.png"}'`,
`novacom get`, rotate −90° for portrait), judge by eye; for the GPU path read the daemon's timeline and frame
dumps too (`tools/device/t502/README.md`).
3. The daemon's log is the oracle for what reached the engine (`/var/palm/jihad/<variant>/daemon.log`:
`setZoomAndScroll` for a zoom, the scroll lines for a pan); LunaSysMgr's syslog (`/var/log/messages`) for what
reached the window server (`editorFocused`, `IME:`, "Deadzone Tapped" for a touch in a dead zone,
libhal's "Unexpected code" for a malformed datagram).
4. Delete the shots once judged (memory `clear-screenshots-when-done`); keep only what a kit note cites.
What it cannot do: the accelerometer (rotation), the volume keys, hearing audio, a physical keyboard. Those
stay a person's in `docs/FINGER-TESTS.md`.
+150
View File
@@ -0,0 +1,150 @@
/* Copyright 2026 NotAlexNoyle. Apache-2.0.
*
* finger — an emulated finger for the HP TouchPad (webOS 3.0.5 / CE 3.1.0), for the project's
* scripted finger rounds (docs/FINGER-TESTS.md). It travels the REAL input path: on this device
* hidd reads the Cypress panel over /dev/ctp_uart and SENDS each scan as a datagram of Linux
* `struct input_event`s (16 bytes each) to the Unix datagram socket LunaSysMgr binds at
* /var/run/hidd/TouchpanelEventSocket (libhal's touchpanel module reads it, the Qt QPA plugin turns
* it into QTouchEvents, the card's WebKit and the BrowserAdapter plugin see what a finger makes).
* Nothing stops a root process from sending the same datagrams, so this tool does, with the
* timing of a hand: no hidd, LunaSysMgr or daemon change.
*
* build: $TC/arm-webos-linux-gnueabi-gcc -O2 -march=armv7-a -mfpu=neon -mfloat-abi=softfp -o finger finger.c
* (TC = build/webos-oe/toolchain/out-toolchain/x-tools/arm-webos-linux-gnueabi/bin)
* on the device (root):
* finger tap X Y [MS] a tap (down, MS default 80, up)
* finger dtap X Y [TAP_MS] [GAP_MS] a double tap (two taps of TAP_MS, default 70, GAP_MS apart, default 120)
* finger hold X Y [MS] a long press (default 1500 ms)
* finger drag X0 Y0 X1 Y1 [MS] a drag, the finger moving at the panel's scan rate (default 600 ms), lifted at rest
* finger flick X0 Y0 X1 Y1 [MS] a flick: the same move but fast (default 160 ms) and lifted while moving
* finger pinch CX CY D0 D1 [MS] two fingers on the horizontal through (CX,CY), D0 apart to D1 apart (default 600 ms)
* finger raw TYPE CODE VALUE ... one datagram of the given events (for experiments; the EV_SYN is appended)
* Coordinates are the PORTRAIT screen's pixels as a screenshot shows them (x 0..767 across, y 0..1023
* down, the status bar at the top): the tool turns them into the panel's own frame, which is what
* hidd reports and LunaSysMgr maps — raw x = y, raw y = 767 - x (found 2026-10-24: a tap at raw
* (10, 737) opened the app menu at portrait (30, 10); a drag in that frame panned a page).
* FINGER_FRAME=raw sends the coordinates as given; SCAN_HZ (env, default 100) sets the move rate;
* FINGER_SOCKET (env) another socket path.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <stdint.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/time.h>
#include <linux/input.h>
/* The datagram libhal's touchpanel module (/lib/hal/modules/halTouchpanelMain.module,
* touchpanel_get_event, read from the binary 2026-10-24) takes: a run of 16-byte input_events, one
* EV_SYN at the end of the scan. Per finger: EV_FINGERID (type 7) opens a touch item whose id is
* value*1000 + code; EV_ABS code 0 / 1 set x / y (any other code is "Unexpected"); EV_KEY BTN_TOUCH
* value 1 marks the finger down, 0 up — sent on the first and the last scan only (the QPA plugin adds
* a finger again on every "down"); EV_GESTURE (type 6) sets the gesture code and, in two events, the
* x then y velocity (hidd's FLICK is code 6, with the Gesture FLICK minimum of the HidTouchpanel xml).
* Up to 20 fingers a scan. Coordinates are the panel's LCD pixels, portrait: x 0..767, y 0..1023. */
#define EV_GESTURE 0x06
#define EV_FINGERID 0x07
#define GESTURE_FLICK 6
static const char* SOCK = "/var/run/hidd/TouchpanelEventSocket";
static int g_fd = -1;
static struct sockaddr_un g_addr;
static int g_hz = 100;
static int g_raw = 0; /* FINGER_FRAME=raw: no portrait-to-panel conversion */
static void die(const char* m) { fprintf(stderr, "finger: %s: %s\n", m, strerror(errno)); exit(1); }
static void sleep_ms(int ms) { struct timespec ts = { ms / 1000, (ms % 1000) * 1000000L }; nanosleep(&ts, NULL); }
static void stamp(struct input_event* e) { gettimeofday(&e->time, NULL); }
static void ev(struct input_event* out, int* n, uint16_t type, uint16_t code, int32_t value) {
struct input_event* e = &out[(*n)++]; stamp(e); e->type = type; e->code = code; e->value = value;
}
static void send_scan(struct input_event* evs, int n) {
ev(evs, &n, EV_SYN, SYN_REPORT, 0);
if (sendto(g_fd, evs, n * sizeof(struct input_event), 0, (struct sockaddr*)&g_addr, sizeof g_addr) < 0) die("sendto");
}
/* One finger's part of a scan. The emission scheme must match what libhal's touchpanel module
* (/lib/hal/modules/halTouchpanel*.module) parses: FINGER_SCHEME is set after reading it. */
/* phase: 1 = the first scan (down), 0 = a move scan, -1 = the last scan (up) */
typedef struct { int id; int x; int y; int phase; int xvel; int yvel; } Finger;
static void finger_events(struct input_event* evs, int* n, const Finger* f) {
int rx = g_raw ? f->x : f->y, ry = g_raw ? f->y : 767 - f->x; /* the panel's frame */
ev(evs, n, EV_FINGERID, (uint16_t)f->id, 0); /* id = 0*1000 + code */
ev(evs, n, EV_ABS, ABS_X, rx);
ev(evs, n, EV_ABS, ABS_Y, ry);
if (f->phase == 1) ev(evs, n, EV_KEY, BTN_TOUCH, 1);
if (f->phase == -1) {
if (f->xvel || f->yvel) { /* the velocities in the panel's frame too */
int vx = g_raw ? f->xvel : f->yvel, vy = g_raw ? f->yvel : -f->xvel;
ev(evs, n, EV_GESTURE, GESTURE_FLICK, vx); ev(evs, n, EV_GESTURE, GESTURE_FLICK, vy);
}
ev(evs, n, EV_KEY, BTN_TOUCH, 0);
}
}
static void scan(const Finger* fs, int count) {
struct input_event evs[128]; int n = 0;
for (int i = 0; i < count; i++) finger_events(evs, &n, &fs[i]);
send_scan(evs, n);
}
static int lerp(int a, int b, double t) { return (int)(a + (b - a) * t + 0.5); }
static void move_one(int id, int x0, int y0, int x1, int y1, int ms, int lift_moving) {
Finger f = { id, x0, y0, 1, 0, 0 };
scan(&f, 1); /* down */
f.phase = 0;
int steps = ms * g_hz / 1000; if (steps < 2) steps = 2;
for (int i = 1; i <= steps; i++) {
sleep_ms(1000 / g_hz);
f.x = lerp(x0, x1, (double)i / steps); f.y = lerp(y0, y1, (double)i / steps);
scan(&f, 1); /* move */
}
if (lift_moving) { /* a flick: lifted while moving, hidd's FLICK gesture with the velocity (px/s) */
f.xvel = (int)((long)(x1 - x0) * 1000 / ms); f.yvel = (int)((long)(y1 - y0) * 1000 / ms);
} else sleep_ms(60);
f.phase = -1; scan(&f, 1); /* up */
}
int main(int argc, char** argv) {
if (argc < 2) { fprintf(stderr, "usage: see the header of finger.c\n"); return 2; }
const char* path = getenv("FINGER_SOCKET"); if (path) SOCK = path;
const char* hz = getenv("SCAN_HZ"); if (hz) g_hz = atoi(hz) > 0 ? atoi(hz) : 100;
const char* fr = getenv("FINGER_FRAME"); if (fr && !strcmp(fr, "raw")) g_raw = 1;
g_fd = socket(AF_UNIX, SOCK_DGRAM, 0); if (g_fd < 0) die("socket");
memset(&g_addr, 0, sizeof g_addr); g_addr.sun_family = AF_UNIX; strncpy(g_addr.sun_path, SOCK, sizeof g_addr.sun_path - 1);
const char* cmd = argv[1];
int a[8] = {0}; for (int i = 2; i < argc && i < 10; i++) a[i - 2] = atoi(argv[i]);
if (!strcmp(cmd, "tap")) {
Finger f = { 1, a[0], a[1], 1, 0, 0 }; scan(&f, 1); sleep_ms(a[2] > 0 ? a[2] : 80); f.phase = -1; scan(&f, 1);
} else if (!strcmp(cmd, "dtap")) {
int tms = a[2] > 0 ? a[2] : 70, gms = a[3] > 0 ? a[3] : 120;
for (int k = 0; k < 2; k++) { Finger f = { 1, a[0], a[1], 1, 0, 0 }; scan(&f, 1); sleep_ms(tms); f.phase = -1; scan(&f, 1); if (!k) sleep_ms(gms); }
} else if (!strcmp(cmd, "hold")) {
int ms = a[2] > 0 ? a[2] : 1500; Finger f = { 1, a[0], a[1], 1, 0, 0 }; scan(&f, 1); f.phase = 0;
for (int t = 0; t < ms; t += 1000 / g_hz) { sleep_ms(1000 / g_hz); scan(&f, 1); }
f.phase = -1; scan(&f, 1);
} else if (!strcmp(cmd, "drag")) {
move_one(1, a[0], a[1], a[2], a[3], a[4] > 0 ? a[4] : 600, 0);
} else if (!strcmp(cmd, "flick")) {
move_one(1, a[0], a[1], a[2], a[3], a[4] > 0 ? a[4] : 160, 1);
} else if (!strcmp(cmd, "pinch")) {
int cx = a[0], cy = a[1], d0 = a[2], d1 = a[3], ms = a[4] > 0 ? a[4] : 600;
Finger f[2] = { { 1, cx - d0 / 2, cy, 1, 0, 0 }, { 2, cx + d0 / 2, cy, 1, 0, 0 } };
scan(f, 2); f[0].phase = f[1].phase = 0;
int steps = ms * g_hz / 1000; if (steps < 2) steps = 2;
for (int i = 1; i <= steps; i++) {
sleep_ms(1000 / g_hz); int d = lerp(d0, d1, (double)i / steps);
f[0].x = cx - d / 2; f[1].x = cx + d / 2; scan(f, 2);
}
sleep_ms(60); f[0].phase = f[1].phase = -1; scan(f, 2);
} else if (!strcmp(cmd, "raw")) {
struct input_event evs[64]; int n = 0;
for (int i = 2; i + 2 < argc && n < 60; i += 3) ev(evs, &n, (uint16_t)strtol(argv[i], 0, 0), (uint16_t)strtol(argv[i + 1], 0, 0), (int32_t)strtol(argv[i + 2], 0, 0));
send_scan(evs, n);
} else { fprintf(stderr, "finger: unknown command %s\n", cmd); return 2; }
close(g_fd);
return 0;
}