From 8fe619be84f53142d953622007a9778facdf3709 Mon Sep 17 00:00:00 2001 From: Mahdiar Naufal Date: Fri, 3 Jul 2026 23:37:00 +0200 Subject: [PATCH 1/2] Read right joycon gyro movement and detect whip movement --- joycon.js | 77 +++++++++++++++++++++++++++++++++++++++++++++++ main.js | 13 ++++++++ overlay.html | 4 +++ package-lock.json | 39 +++++++++++++++++++++++- package.json | 3 +- preload.js | 1 + 6 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 joycon.js diff --git a/joycon.js b/joycon.js new file mode 100644 index 0000000..0a2895a --- /dev/null +++ b/joycon.js @@ -0,0 +1,77 @@ +// joycon.js +const EventEmitter = require('events'); +const HID = require('node-hid'); + +class JoyConModule extends EventEmitter { + constructor() { + super(); + this.device = null; + this.gravity = { x: 0, y: 0, z: 0 }; + this.filterCoeff = 0.8; + this.lastAState = false; + this._processData = this._processData.bind(this); + } + + startListening() { + try { + // nintendo vendor id + const devices = HID.devices().filter(d => d.vendorId === 1406); + if (devices.length === 0) { + return false; + } + + this.device = new HID.HID(devices[0].path); + + // Wake up and enable IMU (Standard Full Mode 0x30) + this.device.write([0x01, 0x00, 0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40, 0x03, 0x30]); + this.device.write([0x01, 0x01, 0x00, 0x01, 0x40, 0x40, 0x00, 0x01, 0x40, 0x40, 0x40, 0x01]); + + this.device.on("data", (data) => this._processData(data)); + return true; + } catch (err) { + return false; + } + } + + _processData(data) { + if (data[0] !== 0x30) return; + + // ── 1. READ BUTTONS (Byte 3 contains Y, X, B, A for Right Joy-Con) ── + // The A button is the 4th bit (0x08) + const currentAState = (data[3] & 0x08) !== 0; + + // Check for "Falling Edge" (Button just got pressed down) + if (currentAState && !this.lastAState) { + this.emit('buttonA'); + } + this.lastAState = currentAState; + + // Loop through all 3 internal high-frequency sub-frames (spaced 5ms apart) + const frameOffsets = [13, 25, 37]; + + for (const offset of frameOffsets) { + const rawAccX = data.readInt16LE(offset); + const rawAccY = data.readInt16LE(offset + 2); + const rawAccZ = data.readInt16LE(offset + 4); + const gyroPitch = data.readInt16LE(offset + 8); + + // High-Pass Filter: Isolates gravity from true moving force + this.gravity.x = this.filterCoeff * this.gravity.x + (1 - this.filterCoeff) * rawAccX; + this.gravity.y = this.filterCoeff * this.gravity.y + (1 - this.filterCoeff) * rawAccY; + this.gravity.z = this.filterCoeff * this.gravity.z + (1 - this.filterCoeff) * rawAccZ; + + const userAccZ = rawAccZ - this.gravity.z; + + // Threshold evaluations + const violentForwardThrust = userAccZ < -3500; + const crispWristSnap = Math.abs(gyroPitch) > 5000; + + if (violentForwardThrust && crispWristSnap) { + this.emit('whip'); + break; + } + } + } +} + +module.exports = new JoyConModule(); \ No newline at end of file diff --git a/main.js b/main.js index 1360458..150106b 100644 --- a/main.js +++ b/main.js @@ -3,6 +3,7 @@ const path = require('path'); const fs = require('fs'); const os = require('os'); const { execFile } = require('child_process'); +const joycon = require('./joycon'); // ── Win32 FFI (Windows only) ──────────────────────────────────────────────── let keybd_event, VkKeyScanA; @@ -135,6 +136,8 @@ function createOverlay() { hasShadow: false, webPreferences: { preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, // Keep standard secure settings + nodeIntegration: false, }, }); overlay.setAlwaysOnTop(true, 'screen-saver'); @@ -285,6 +288,16 @@ app.whenReady().then(async () => { ]) ); tray.on('click', toggleOverlay); + joycon.startListening(); + joycon.on('buttonA', () => { + toggleOverlay(); + }); + + joycon.on('whip', () => { + if (overlay) { + overlay.webContents.send('joycon-whip-event'); + } + }); }); app.on('window-all-closed', e => e.preventDefault()); // keep alive in tray diff --git a/overlay.html b/overlay.html index 2354b86..c994e7e 100644 --- a/overlay.html +++ b/overlay.html @@ -449,6 +449,10 @@ if (whip && !dropping) dropping = true; }); +window.bridge.onJoyconWhip(() => { + playCrackSound(); + window.bridge.whipCrack(); +}); diff --git a/package-lock.json b/package-lock.json index d1ff527..f3fa762 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,8 @@ ], "dependencies": { "electron": "^33.0.0", - "koffi": "^2.9.0" + "koffi": "^2.9.0", + "node-hid": "^3.3.0" }, "bin": { "badclaude": "bin/openwhip.js", @@ -613,6 +614,29 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "license": "MIT" + }, + "node_modules/node-hid": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-hid/-/node-hid-3.3.0.tgz", + "integrity": "sha512-j+dFgJLRAE0nufQKXk3IfS6T6YuHhCgMvz4TrG0sgtb6DSCdYpfJ1etcdmeCmPQjUgO+yo32ktVrRliNs/+fmg==", + "hasInstallScript": true, + "license": "(MIT OR X11)", + "dependencies": { + "node-addon-api": "^3.2.1", + "pkg-prebuilds": "^1.0.0" + }, + "bin": { + "hid-showdevices": "src/show-devices.js" + }, + "engines": { + "node": ">=10.16" + } + }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -659,6 +683,19 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "license": "MIT" }, + "node_modules/pkg-prebuilds": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pkg-prebuilds/-/pkg-prebuilds-1.1.0.tgz", + "integrity": "sha512-jyai+KTQ2OwbN6iRYw88XbYOMgtpoSYJpjYebx7d9ihqz3txNi3ucsBt3va0iVWe6svSlaqpijMHFF/eJCMZzg==", + "license": "MIT", + "bin": { + "pkg-prebuilds-copy": "bin/copy.mjs", + "pkg-prebuilds-verify": "bin/verify.mjs" + }, + "engines": { + "node": ">= 14.15.0" + } + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", diff --git a/package.json b/package.json index 36e2ea9..8661ee5 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ }, "dependencies": { "electron": "^33.0.0", - "koffi": "^2.9.0" + "koffi": "^2.9.0", + "node-hid": "^3.3.0" } } diff --git a/preload.js b/preload.js index ae83618..790e5ea 100644 --- a/preload.js +++ b/preload.js @@ -5,4 +5,5 @@ contextBridge.exposeInMainWorld('bridge', { hideOverlay: () => ipcRenderer.send('hide-overlay'), onSpawnWhip: (fn) => ipcRenderer.on('spawn-whip', () => fn()), onDropWhip: (fn) => ipcRenderer.on('drop-whip', () => fn()), + onJoyconWhip: (fn) => ipcRenderer.on('joycon-whip-event', () => fn()), }); From 5ab6696d3e947ac312cbcc018963072e09353673 Mon Sep 17 00:00:00 2001 From: Mahdiar Naufal Date: Fri, 3 Jul 2026 23:58:00 +0200 Subject: [PATCH 2/2] newline --- joycon.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/joycon.js b/joycon.js index 0a2895a..aa82507 100644 --- a/joycon.js +++ b/joycon.js @@ -74,4 +74,4 @@ class JoyConModule extends EventEmitter { } } -module.exports = new JoyConModule(); \ No newline at end of file +module.exports = new JoyConModule();