diff options
| -rw-r--r-- | README.md | 84 | ||||
| -rwxr-xr-x | identify.sh | 3 | ||||
| -rwxr-xr-x | index.js | 105 | ||||
| -rw-r--r-- | lib/motion.js | 52 | ||||
| -rwxr-xr-x | run | 2 | 
5 files changed, 139 insertions, 107 deletions
| @@ -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 @@ -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 = []; +      }      }    }  }; @@ -0,0 +1,2 @@ +#!/bin/bash +./index.js `./identify.sh` | 
