Relay browser touches through the harness into the real touchscreen

This commit is contained in:
flamingcow
2026-08-31 14:59:01 -07:00
parent 7fa48da73c
commit 10d97ee82a
2 changed files with 196 additions and 1 deletions
+163
View File
@@ -0,0 +1,163 @@
package main
import (
"encoding/binary"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"unsafe"
"golang.org/x/sys/unix"
)
const (
evSyn = 0x00
evKey = 0x01
evAbs = 0x03
btnTouch = 0x14a
absX = 0x00
absY = 0x01
inputPropDirect = 0x01
sizeofInputEvent = 24
)
func evIoc(nr, size uintptr) uintptr {
const iocRead = 2
return iocRead<<30 | size<<16 | 'E'<<8 | nr
}
type touchDev struct {
f *os.File
minX, maxX, minY, maxY int32
}
var touchscreen *touchDev
// Picks the direct-input device that reports absolute X and Y — the same
// probe cabletest runs, so both ends name the same hardware. Events written
// to the node are fed into the input core as if the hardware produced them,
// so cabletest sees a finger without carrying any injection code.
func openTouchscreen() (*touchDev, error) {
paths, err := filepath.Glob("/dev/input/event*")
if err != nil {
return nil, err
}
for _, p := range paths {
f, err := os.OpenFile(p, os.O_RDWR, 0)
if err != nil {
continue
}
var props, absBits uint64
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
evIoc(0x09, 8), uintptr(unsafe.Pointer(&props))); errno != 0 {
f.Close()
continue
}
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
evIoc(0x20+evAbs, 8), uintptr(unsafe.Pointer(&absBits))); errno != 0 {
f.Close()
continue
}
if props&(1<<inputPropDirect) == 0 ||
absBits&(1<<absX) == 0 || absBits&(1<<absY) == 0 {
f.Close()
continue
}
t := &touchDev{f: f}
if t.minX, t.maxX, err = axisRange(f, absX); err != nil {
f.Close()
return nil, fmt.Errorf("%s x range: %w", p, err)
}
if t.minY, t.maxY, err = axisRange(f, absY); err != nil {
f.Close()
return nil, fmt.Errorf("%s y range: %w", p, err)
}
if t.maxX <= t.minX || t.maxY <= t.minY {
f.Close()
return nil, fmt.Errorf("%s: degenerate axes x %d..%d y %d..%d",
p, t.minX, t.maxX, t.minY, t.maxY)
}
return t, nil
}
return nil, fmt.Errorf("no touchscreen found among /dev/input/event*")
}
func axisRange(f *os.File, axis uintptr) (min, max int32, err error) {
var info [6]int32
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(),
evIoc(0x40+axis, 24), uintptr(unsafe.Pointer(&info[0]))); errno != 0 {
return 0, 0, errno
}
return info[1], info[2], nil
}
type inputEvent struct {
typ, code uint16
val int32
}
// Timestamps are left zero: evdev stamps injected events at delivery.
func (t *touchDev) send(evs []inputEvent) error {
buf := make([]byte, len(evs)*sizeofInputEvent)
for i, e := range evs {
o := i * sizeofInputEvent
binary.NativeEndian.PutUint16(buf[o+16:], e.typ)
binary.NativeEndian.PutUint16(buf[o+18:], e.code)
binary.NativeEndian.PutUint32(buf[o+20:], uint32(e.val))
}
_, err := t.f.Write(buf)
return err
}
// The page speaks portrait canvas pixels; the device speaks the panel's
// landscape axes, so the quarter turn cabletest applies on the way in is
// inverted on the way out.
func touch(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
return
}
f := strings.Fields(string(body))
if len(f) == 1 && f[0] == "up" {
if err := touchscreen.send([]inputEvent{{evKey, btnTouch, 0}, {evSyn, 0, 0}}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
if len(f) != 3 || (f[0] != "down" && f[0] != "move") {
http.Error(w, "want down x y, move x y, or up", http.StatusBadRequest)
return
}
cx, errX := strconv.Atoi(f[1])
cy, errY := strconv.Atoi(f[2])
if errX != nil || errY != nil {
http.Error(w, "bad coordinates", http.StatusBadRequest)
return
}
latestMu.RLock()
k := latest
latestMu.RUnlock()
if k.buf == nil {
http.Error(w, "no frame to locate the touch on", http.StatusServiceUnavailable)
return
}
cx = min(max(cx, 0), k.ph-1)
cy = min(max(cy, 0), k.pw-1)
xl, yl := cy, k.ph-1-cx
t := touchscreen
rawX := t.minX + int32(int64(xl)*int64(t.maxX-t.minX)/int64(k.pw-1))
rawY := t.minY + int32(int64(yl)*int64(t.maxY-t.minY)/int64(k.ph-1))
evs := []inputEvent{{evAbs, absX, rawX}, {evAbs, absY, rawY}}
if f[0] == "down" {
evs = append(evs, inputEvent{evKey, btnTouch, 1})
}
evs = append(evs, inputEvent{evSyn, 0, 0})
if err := t.send(evs); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}