aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexis Hovorka <[email protected]>2021-05-16 22:40:33 -0600
committerAlexis Hovorka <[email protected]>2021-05-16 22:40:33 -0600
commit2297530cb1149752445b586b337e5c8d3b7bb108 (patch)
treeffed174b8edff46ccdb533aa1b486e6c216695da
parente97ee92acc19b30c8e3c044049ea5951d8c45d63 (diff)
[feat] Long press 3+ fingers to set home positionHEADmain
-rw-r--r--README.md84
-rwxr-xr-xidentify.sh3
-rwxr-xr-xindex.js105
-rw-r--r--lib/motion.js52
-rwxr-xr-xrun2
5 files changed, 139 insertions, 107 deletions
diff --git a/README.md b/README.md
index dba118d..ac08ae1 100644
--- a/README.md
+++ b/README.md
@@ -1,83 +1,3 @@
-# node-exclusive-keyboard
-Keylogger for NodeJS and Linux that grabs the input device exclusively.
+# node-dotkey
-Useful for capturing USB input devices that act like keyboards, so that their inputs do no pollute other processes like terminals.
-
-Based on [node-keylogger](https://github.com/taosx/node-keylogger/) and [node-ioctl](https://github.com/santigimeno/node-ioctl).
-
-## Installation
-```bash
-npm install --save exclusive-keyboard
-```
-
-## Usage
-
-Set access control right to device for user `username`:
-```bash
-sudo setfacl -m u:username:r /dev/input/by-id/usb-Logitech_Logitech_USB_Keyboard-event-kbd
-```
-
-```js
-const ExclusiveKeyboard = require('exclusive-keyboard');
-
-const keyboard = new ExclusiveKeyboard('by-id/usb-Logitech_Logitech_USB_Keyboard-event-kbd', true);
-keyboard.on('keyup', console.log);
-keyboard.on('keydown', console.log);
-keyboard.on('keypress', console.log);
-keyboard.on('close', console.log);
-keyboard.on('error', console.error);
-```
-
-## API
-
-### `new ExclusiveKeyboard(dev, exclusive)`
-* `dev` (string): Device name (part after '/dev/input/'). Example: 'event0' would use '/dev/input/event0'
-* `exclusive` (boolean): If true, grab device exclusively using ioctl EVIOCGRAB (default: true)
-
-### `close()`
-Releases the grabbed device and closes the file descriptor. Emits 'close' event when done.
-
-### ExclusiveKeyboard.Keys
-Mapping of key codes to key ids, see `keycodes.js`.
-
-### Event `keyup(event)`
-Example event:
-```js
-{
- timeS: 39234,
- timeMS: 3812,
- keyCode: 71,
- keyId: 'KEY_KP7',
- type: 'keyup',
- dev: 'by-id/usb-SEM_Trust_Numpad-event-kbd'
-}
-```
-
-### Event `keypress(event)`
-Example event:
-```js
-{
- timeS: 39234,
- timeMS: 3812,
- keyCode: 71,
- keyId: 'KEY_KP7',
- type: 'keypress',
- dev: 'by-id/usb-SEM_Trust_Numpad-event-kbd'
-}
-```
-
-### Event `keydown(event)`
-```js
-{
- timeS: 39234,
- timeMS: 3812,
- keyCode: 71,
- keyId: 'KEY_KP7',
- type: 'keydown',
- dev: 'by-id/usb-SEM_Trust_Numpad-event-kbd'
-}
-```
-
-### Event `error(error)`
-
-### Event `close()`
+Must not be touching touchpad when starting listener
diff --git a/identify.sh b/identify.sh
new file mode 100755
index 0000000..5096f3e
--- /dev/null
+++ b/identify.sh
@@ -0,0 +1,3 @@
+cat /proc/bus/input/devices | grep "^[NH]" |
+ grep -A1 "Logitech Rechargeable Touchpad T650" |
+ tail -n1 | cut -d= -f2- | tr " " "\n" | grep "^event" | head -n1
diff --git a/index.js b/index.js
index 72c8f66..ff1d144 100755
--- a/index.js
+++ b/index.js
@@ -2,12 +2,111 @@
const Device = require("./lib/device");
const Motion = require("./lib/motion");
+//const clone = x => JSON.parse(JSON.stringify(x)); // Deep
+//const clone = x => Object.assign({}, x); // Shallow
+
const path = "/dev/input/"+process.argv[2];
console.log("Opening %s", path);
const device = new Device(path);
-const motion = new Motion(device);
+const motion = new Motion(device, 600/*ms for long*/);
+
device.on("open", () => { console.log(device.id); device.grab(); });
+//device.on("EV_ABS", e => console.log(e));
+//device.on("EV_SYN", e => console.log(e));
+
motion.on("error", console.error)
- .on("short", e => console.log("short", e))
- .on("long", e => console.log("long", e));
+ //.on("short", e => console.log("short", e))
+ //.on("long", e => console.log("long", e))
+ .on("short", doShort).on("long", doLong);
+
+const flipBits = (n,w) => parseInt(n.toString(2).padStart(w,0).split("").reverse().join(""),2);
+
+const eucDist = (a,b) => Math.sqrt((a.x-b.x)**2 + (a.y-b.y)**2);
+//const avgDist = (a,b) => (eucDist(a.i,b.i) + eucDist(a.f,b.f))/2;
+const selfDist = a => eucDist(a.i, a.f);
+const getAngle2 = (a,b) => Math.atan2(a.y-b.y, b.x-a.x)*180/Math.PI;
+const getAngle = a => getAngle2(a.i,a.f);
+const isLeft = (a,b,c) => ((b.x-a.x)*(c.y-a.y) - (b.y-a.y)*(c.x-a.x))>0; // What side of a-b is c on
+
+const state = {
+ homed: false,
+ homes: [],
+ angle: 0,
+ fingerDist: 0,
+};
+
+const dotRadius = 0.3; // Tap/reach boundary in terms of between-finger distance
+const swipeLength = 0.2; // Tap/swipe threshold in terms of between-finger distance
+
+function doLong(e) {
+ if (e.length < 3) return; // Pinky finger optional!
+
+ const between = [];
+ for (let i=0;i<e.length;i++)
+ for (let j=i+1;j<e.length;j++)
+ between.push({a:i, b:j, dist:eucDist(e[i].m, e[j].m)});
+ between.sort((a,b) => a.dist-b.dist);
+
+ const fingerDist = between[0].dist;
+ if (e.map(p => selfDist(p)).reduce((a,d) =>
+ a || (d >= fingerDist*swipeLength), false)) return; // Moved too far
+
+ const biggest = between.pop();
+ const sides = e.reduce((a,p,i) => {
+ if (i != biggest.a && i != biggest.b)
+ a.push({i:i,s:isLeft(e[biggest.a].m, e[biggest.b].m, p.m)});
+ return a; }, []);
+ const side = 2*sides.reduce((a,s)=>a+s.s,0)>sides.length;
+
+ const indexFinger = biggest[side?"b":"a"];
+
+ state.homes = [];
+ state.homes.push(e[indexFinger].m);
+ sides.map(f => Object.assign({d:eucDist(e[indexFinger].m, e[f.i].m)}, f))
+ .sort((a,b) => a.d-b.d).forEach(f => state.homes.push(e[f.i].m));
+ state.homes.push(e[biggest[side?"a":"b"]].m);
+
+ state.angle = getAngle2(state.homes[0], state.homes[state.homes.length-1]);
+ state.fingerDist = fingerDist;
+ state.homed = true;
+
+ //console.log(JSON.stringify(state.homes), state.angle, state.fingerDist);
+ console.log("Homed");
+}
+
+const prettyFingers = n => "("+n.toString(2).padStart(state.homes.length,0)
+ .split("").map(f => f=="1"?"#":" ").reverse().join("")+")";
+
+function doShort(e) {
+ if (!state.homed) return;
+
+ // Gesture types:
+ // - Tap:
+ // - inside tap | 0
+ // - reaching tap | 1
+ // - Swipe:
+ // - Vertical:
+ // - from in down | 2
+ // - from up in | 3
+ // - swipe up | 4
+ // - Horizontal:
+ // - from in left | 5
+ // - from in right | 6
+
+ const isTap = (e.reduce((a,p) => a+selfDist(p),0) /
+ e.length) < state.fingerDist * swipeLength;
+
+ if (isTap) {
+ const fingers = e.map(p => state.homes
+ .map((f,i) => ({i,d:eucDist(f, p.m)}))
+ .sort((a,b)=>a.d-b.d)[0]);
+ const fingerCode = fingers.reduce((a,f) => a+2**f.i, 0);
+
+ // TODO reaching taps
+ console.log("tap", prettyFingers(fingerCode));
+
+ } else { // TODO swipes
+ console.log("swipe");
+ }
+}
diff --git a/lib/motion.js b/lib/motion.js
index 781d54b..3e24962 100644
--- a/lib/motion.js
+++ b/lib/motion.js
@@ -1,18 +1,19 @@
const util = require("util")
, EventEmitter = require("events").EventEmitter;
-const longTime = 600;
+//const slotToPoint = s => ({xi:s.xi, yi:s.yi, xf:s.x, yf:s.y});
+const slotToPoint = s => ({
+ i:{x:s.xi, y:s.yi}, f:{x:s.x, y:s.y},
+ m:{x:(s.xi+s.x)/2, y:(s.yi+s.y)/2}
+});
-//const clone = x => JSON.parse(JSON.stringify(x)); // Deep
-const clone = x => Object.assign({}, x); // Shallow
-const slotToPoint = s => ({xi:s.xi, yi:s.yi, xf:s.x, yf:s.y});
-
-function Motion(device) {
+function Motion(device, longTime) {
this.device = device;
- this.slotsUsed = 0;
+ this.slotsActive = 0;
this.slots = [];
this.points = [];
this.longed = false;
+ this.longTime = longTime || 600;
device.on("EV_ABS", e => this.doABS(e));
device.on("EV_SYN", e => this.doSYN(e));
@@ -44,13 +45,16 @@ switch (e.code) {
break;
case "ABS_MT_TRACKING_ID":
- if (!this.currentSlot) return;
+ if (this.currentSlot === undefined) {
+ this.addSlot();
+ this.currentSlot = this.slots[0];
+ }
+
if (e.value >= 0) {
this.currentSlot.id = e.value;
- if (this.slotsUsed++ === 0) {
- //this.start = e.time;
+ if (this.slotsActive++ === 0) {
this.longTimeout = setTimeout(() =>
- this.emitLong(), longTime);
+ this.emitLong(), this.longTime);
}
} else {
@@ -61,26 +65,25 @@ switch (e.code) {
this.currentSlot.yi = -1;
this.currentSlot.x = 0;
this.currentSlot.y = 0;
- if (--this.slotsUsed === 0) {
- //this.end = e.time;
+ if (--this.slotsActive === 0) {
clearTimeout(this.longTimeout);
}
} break;
case "ABS_MT_POSITION_X":
- if (!this.currentSlot) return;
+ if (this.currentSlot === undefined) return;
if (this.currentSlot.id < 0) return;
- this.currentSlot.x = e.value;
if (this.currentSlot.xi < 0)
this.currentSlot.xi = e.value;
+ this.currentSlot.x = e.value;
break;
case "ABS_MT_POSITION_Y":
- if (!this.currentSlot) return;
+ if (this.currentSlot === undefined) return;
if (this.currentSlot.id < 0) return;
- this.currentSlot.y = e.value;
if (this.currentSlot.yi < 0)
this.currentSlot.yi = e.value;
+ this.currentSlot.y = e.value;
break;
//default:
@@ -88,11 +91,16 @@ switch (e.code) {
}};
Motion.prototype.doSYN = function(e) {
- if (e.code === "SYN_REPORT" && this.slotsUsed === 0) {
- if (this.longed) { this.longed = false;
- } else if (this.points.length > 0) {
- this.emit("short", this.points);
- this.points = [];
+ if (e.code === "SYN_REPORT") {
+ //console.log("SYN", this.slotsActive, this.points.length);
+ if (this.slotsActive <= 0) {
+ this.slotsActive = 0;
+
+ if (this.longed) { this.longed = false;
+ } else if (this.points.length > 0) {
+ this.emit("short", this.points);
+ this.points = [];
+ }
}
}
};
diff --git a/run b/run
new file mode 100755
index 0000000..0ce9b46
--- /dev/null
+++ b/run
@@ -0,0 +1,2 @@
+#!/bin/bash
+./index.js `./identify.sh`