Relay browser touches through the harness into the real touchscreen
This commit is contained in:
+33
-1
@@ -22,7 +22,7 @@ const page = `<!doctype html>
|
|||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
<title>cabletest</title>
|
<title>cabletest</title>
|
||||||
<body style="margin:0;background:#000;min-height:100vh;display:grid;place-items:center">
|
<body style="margin:0;background:#000;min-height:100vh;display:grid;place-items:center">
|
||||||
<canvas style="max-width:100vw;max-height:100vh"></canvas>
|
<canvas style="max-width:100vw;max-height:100vh;touch-action:none"></canvas>
|
||||||
<script>
|
<script>
|
||||||
(async () => {
|
(async () => {
|
||||||
const cv = document.querySelector('canvas'), g = cv.getContext('2d');
|
const cv = document.querySelector('canvas'), g = cv.getContext('2d');
|
||||||
@@ -59,6 +59,34 @@ addEventListener('keydown', e => {
|
|||||||
fetch('keys', {method: 'POST', body: e.key});
|
fetch('keys', {method: 'POST', body: e.key});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
(() => {
|
||||||
|
const cv = document.querySelector('canvas');
|
||||||
|
let q = Promise.resolve(), held = -1, lastMove = 0;
|
||||||
|
const send = b => q = q.then(() => fetch('touch', {method: 'POST', body: b}).catch(() => {}));
|
||||||
|
const at = e => {
|
||||||
|
const r = cv.getBoundingClientRect();
|
||||||
|
return Math.round((e.clientX - r.left) * cv.width / r.width) + ' ' +
|
||||||
|
Math.round((e.clientY - r.top) * cv.height / r.height);
|
||||||
|
};
|
||||||
|
cv.addEventListener('pointerdown', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
held = e.pointerId;
|
||||||
|
cv.setPointerCapture(held);
|
||||||
|
send('down ' + at(e));
|
||||||
|
});
|
||||||
|
cv.addEventListener('pointermove', e => {
|
||||||
|
if (e.pointerId !== held || e.timeStamp - lastMove < 50) return;
|
||||||
|
lastMove = e.timeStamp;
|
||||||
|
send('move ' + at(e));
|
||||||
|
});
|
||||||
|
const lift = e => {
|
||||||
|
if (e.pointerId !== held) return;
|
||||||
|
held = -1;
|
||||||
|
send('up');
|
||||||
|
};
|
||||||
|
cv.addEventListener('pointerup', lift);
|
||||||
|
cv.addEventListener('pointercancel', lift);
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -87,6 +115,9 @@ func serveStream() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if touchscreen, err = openTouchscreen(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
ln, err := net.Listen("tcp", streamAddr)
|
ln, err := net.Listen("tcp", streamAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -104,6 +135,7 @@ func serveStream() error {
|
|||||||
})
|
})
|
||||||
mux.HandleFunc("/frames", frames)
|
mux.HandleFunc("/frames", frames)
|
||||||
mux.HandleFunc("/keys", keys)
|
mux.HandleFunc("/keys", keys)
|
||||||
|
mux.HandleFunc("/touch", touch)
|
||||||
go http.Serve(ln, mux)
|
go http.Serve(ln, mux)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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