Relay browser touches through the harness into the real touchscreen
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user