164 lines
4.3 KiB
Go
164 lines
4.3 KiB
Go
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)
|
|
}
|
|
}
|