From 736ce66be508f4fcca5b9d27820566a2f5995772 Mon Sep 17 00:00:00 2001 From: girlglock Date: Thu, 9 Apr 2026 23:58:42 +0200 Subject: [PATCH 1/7] Add support for Wooting Analog Interface v2 --- AnalogSense.js | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/AnalogSense.js b/AnalogSense.js index 62ed347..2c5f288 100644 --- a/AnalogSense.js +++ b/AnalogSense.js @@ -340,7 +340,7 @@ class AsProvider } } -class AsProviderWooting extends AsProvider +class AsProviderWootingLegacy extends AsProvider { static populateFilters(filters) { @@ -374,6 +374,39 @@ class AsProviderWooting extends AsProvider } } +class AsProviderWooting extends AsProvider +{ + //wooting boards with Analog Interface v2 firmware (v5.3.0+) + static populateFilters(filters) + { + filters.push({ usagePage: 0xFF53, vendorId: 0x31E3 }); + } + + startListening(handler) + { + this.dev.oninputreport = function(event) + { + const active_keys = []; + for (let i = 0; i + 3 < event.data.byteLength; i += 4) + { + const scancode = event.data.getUint8(i + 1); + if (scancode == 0) + { + break; + } + const value = (event.data.getUint8(i + 3) << 8) | event.data.getUint8(i + 2); + active_keys.push({ scancode, value: value / 65535 }); + } + handler(active_keys); + }; + } + + stopListening() + { + this.dev.oninputreport = undefined; + } +} + class AsProviderRazerHuntsman extends AsProvider { static populateFilters(filters) @@ -871,6 +904,7 @@ class AsProviderBytech extends AsProvider window.analogsense = { providers: [ AsProviderWooting, + AsProviderWootingLegacy, AsProviderRazerHuntsman, AsProviderRazerHuntsmanV3, AsProviderNuphy, From 8928c711bee52e98db8578cfac7217c5db055535 Mon Sep 17 00:00:00 2001 From: girlglock Date: Fri, 10 Apr 2026 00:25:57 +0200 Subject: [PATCH 2/7] Add very important comment for old wooting firmware --- AnalogSense.js | 1 + 1 file changed, 1 insertion(+) diff --git a/AnalogSense.js b/AnalogSense.js index 2c5f288..a7483a6 100644 --- a/AnalogSense.js +++ b/AnalogSense.js @@ -342,6 +342,7 @@ class AsProvider class AsProviderWootingLegacy extends AsProvider { + //wooting boards with Analog Interface v1 firmware (v5.2.4) static populateFilters(filters) { filters.push({ usagePage: 0xFF54, vendorId: 0x31E3 }); From 334f497cf439756af6782f8525fb55ed0e5638a6 Mon Sep 17 00:00:00 2001 From: girlglock Date: Fri, 10 Apr 2026 03:20:06 +0200 Subject: [PATCH 3/7] v2 lowbyte fallback --- AnalogSense.js | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/AnalogSense.js b/AnalogSense.js index a7483a6..9fc089d 100644 --- a/AnalogSense.js +++ b/AnalogSense.js @@ -72,7 +72,10 @@ const keys = [ { "name": "Space", "wooting": 0x2C, "razer": 0x3D, "bytech": 70 }, { "name": "Right Alt", "wooting": 0xE6, "razer": 0x3E, "nuphy": 0x4000, "bytech": 71 }, { "name": "Right Meta", "wooting": 0xE7, "nuphy": 0x8000 }, - { "name": "Fn", "wooting": 0x409, "razer": 0x3B, "nuphy": 0xff05, "bytech": 72 }, + { "name": "Fn", "wooting": 0x409, "wooting_v2": 0xAD09, "razer": 0x3B, "nuphy": 0xff05, "bytech": 72 }, + { "name": "Prev Track", "wooting_v2": 0x67B6 }, + { "name": "Play/Pause", "wooting_v2": 0x69B5 }, + { "name": "Next Track", "wooting_v2": 0x68CD }, { "name": "Context Menu", "wooting": 0x65, "razer": 0x81 }, { "name": "Right Ctrl", "wooting": 0xE4, "razer": 0x40, "nuphy": 0x1000, "bytech": 73 }, { "name": "Print Screen", "wooting": 0x46, "razer": 0x7C }, @@ -106,8 +109,13 @@ const keys = [ { "name": "Numpad 0", "wooting": 0x62, "razer": 0x63 }, { "name": "Numpad .", "wooting": 0x63, "razer": 0x68 } ]; -const wooting_to_name = {}; Object.values(keys).forEach(key => wooting_to_name[key.wooting] = key.name); -const razer_to_wooting = {}; Object.values(keys).forEach(key => razer_to_wooting[key.razer] = key.wooting); +const wooting_to_name = {}; Object.values(keys).forEach(key => { if ("wooting" in key) wooting_to_name[key.wooting] = key.name; }); +const wooting_v2_to_name = {}; Object.values(keys).forEach(key => { + if ("wooting_v2" in key) { + wooting_v2_to_name[key.wooting_v2] = key.name; + } +}); +const razer_to_wooting = {}; Object.values(keys).forEach(key => { if ("razer" in key) razer_to_wooting[key.razer] = key.wooting; }); const nuphy_to_wooting = {}; Object.values(keys).forEach(key => nuphy_to_wooting[key.nuphy ?? key.wooting] = key.wooting); const bytech_to_wooting = {}; Object.values(keys).forEach(key => { if ("bytech" in key) bytech_to_wooting[key.bytech] = key.wooting; }); @@ -390,7 +398,7 @@ class AsProviderWooting extends AsProvider const active_keys = []; for (let i = 0; i + 3 < event.data.byteLength; i += 4) { - const scancode = event.data.getUint8(i + 1); + const scancode = (event.data.getUint8(i) << 8) | event.data.getUint8(i + 1); if (scancode == 0) { break; @@ -1025,6 +1033,15 @@ window.analogsense = { { return wooting_to_name[scancode]; } + if (scancode in wooting_v2_to_name) + { + return wooting_v2_to_name[scancode]; + } + const lowByte = scancode & 0xFF; + if (lowByte in wooting_to_name) + { + return wooting_to_name[lowByte]; + } return String(Number(scancode)); }, razerScancodeToHidScancode: function(scancode) From 97a9e189b9136c7c85ad6d91b647b280f54e98a7 Mon Sep 17 00:00:00 2001 From: girlglock Date: Fri, 10 Apr 2026 16:53:17 +0200 Subject: [PATCH 4/7] scrap previous v2 approach --- AnalogSense.js | 120 +++++++++++++++++++------------------------------ 1 file changed, 45 insertions(+), 75 deletions(-) diff --git a/AnalogSense.js b/AnalogSense.js index 9fc089d..a9f2d68 100644 --- a/AnalogSense.js +++ b/AnalogSense.js @@ -72,10 +72,7 @@ const keys = [ { "name": "Space", "wooting": 0x2C, "razer": 0x3D, "bytech": 70 }, { "name": "Right Alt", "wooting": 0xE6, "razer": 0x3E, "nuphy": 0x4000, "bytech": 71 }, { "name": "Right Meta", "wooting": 0xE7, "nuphy": 0x8000 }, - { "name": "Fn", "wooting": 0x409, "wooting_v2": 0xAD09, "razer": 0x3B, "nuphy": 0xff05, "bytech": 72 }, - { "name": "Prev Track", "wooting_v2": 0x67B6 }, - { "name": "Play/Pause", "wooting_v2": 0x69B5 }, - { "name": "Next Track", "wooting_v2": 0x68CD }, + { "name": "Fn", "wooting": 0x409, "razer": 0x3B, "nuphy": 0xff05, "bytech": 72 }, { "name": "Context Menu", "wooting": 0x65, "razer": 0x81 }, { "name": "Right Ctrl", "wooting": 0xE4, "razer": 0x40, "nuphy": 0x1000, "bytech": 73 }, { "name": "Print Screen", "wooting": 0x46, "razer": 0x7C }, @@ -109,13 +106,8 @@ const keys = [ { "name": "Numpad 0", "wooting": 0x62, "razer": 0x63 }, { "name": "Numpad .", "wooting": 0x63, "razer": 0x68 } ]; -const wooting_to_name = {}; Object.values(keys).forEach(key => { if ("wooting" in key) wooting_to_name[key.wooting] = key.name; }); -const wooting_v2_to_name = {}; Object.values(keys).forEach(key => { - if ("wooting_v2" in key) { - wooting_v2_to_name[key.wooting_v2] = key.name; - } -}); -const razer_to_wooting = {}; Object.values(keys).forEach(key => { if ("razer" in key) razer_to_wooting[key.razer] = key.wooting; }); +const wooting_to_name = {}; Object.values(keys).forEach(key => wooting_to_name[key.wooting] = key.name); +const razer_to_wooting = {}; Object.values(keys).forEach(key => razer_to_wooting[key.razer] = key.wooting); const nuphy_to_wooting = {}; Object.values(keys).forEach(key => nuphy_to_wooting[key.nuphy ?? key.wooting] = key.wooting); const bytech_to_wooting = {}; Object.values(keys).forEach(key => { if ("bytech" in key) bytech_to_wooting[key.bytech] = key.wooting; }); @@ -348,70 +340,58 @@ class AsProvider } } -class AsProviderWootingLegacy extends AsProvider -{ - //wooting boards with Analog Interface v1 firmware (v5.2.4) - static populateFilters(filters) - { - filters.push({ usagePage: 0xFF54, vendorId: 0x31E3 }); +class AsProviderWooting extends AsProvider { + static populateFilters(filters) { + filters.push({ usagePage: 0xFF54, vendorId: 0x31E3 }); // analog interface v1 + filters.push({ usagePage: 0xFF53, vendorId: 0x31E3 }); // analog interface v2 filters.push({ usagePage: 0xFF54, vendorId: 0x03EB, productId: 0xFF01 }); // Wooting One with old firmware filters.push({ usagePage: 0xFF54, vendorId: 0x03EB, productId: 0xFF02 }); // Wooting Two with old firmware } - startListening(handler) - { - this.dev.oninputreport = function(event) - { - const active_keys = []; - for (let i = 0; i < event.data.byteLength; ) - { - const scancode = (event.data.getUint8(i++) << 8) | event.data.getUint8(i++); - if (scancode == 0) - { - break; - } - const value = event.data.getUint8(i++); - active_keys.push({ scancode, value: value / 255 }); - } - handler(active_keys); - }; - } + startListening(handler) { + const isV2 = this.dev.collections.some(c => c.usagePage === 0xFF53); - stopListening() - { - this.dev.oninputreport = undefined; - } -} + if (isV2) { + this.dev.oninputreport = function (event) { + const active_keys = []; + const data = event.data; + //each entry is 4 bytes (pos, keycode, namespace + analog lo, analog depth + //namespace 0 are regular hid keys, non zero are media keys + //analog value is 10bit 0 to1023 + for (let i = 0; i + 3 < data.byteLength; i += 4) { + const matrix_pos = data.getUint8(i); + const scancode_lo = data.getUint8(i + 1); + if (scancode_lo === 0) break; + const packed = data.getUint8(i + 2); + const value_hi = data.getUint8(i + 3); -class AsProviderWooting extends AsProvider -{ - //wooting boards with Analog Interface v2 firmware (v5.3.0+) - static populateFilters(filters) - { - filters.push({ usagePage: 0xFF53, vendorId: 0x31E3 }); - } + const scancode_hi = (packed >> 2) & 0xf; + const value_lo = (packed >> 6) & 0x3; - startListening(handler) - { - this.dev.oninputreport = function(event) - { - const active_keys = []; - for (let i = 0; i + 3 < event.data.byteLength; i += 4) - { - const scancode = (event.data.getUint8(i) << 8) | event.data.getUint8(i + 1); - if (scancode == 0) - { - break; + const scancode = (scancode_hi << 8) | scancode_lo; + const value = (value_hi << 2) | value_lo; + + active_keys.push({ scancode, value: value / 1023 }); } - const value = (event.data.getUint8(i + 3) << 8) | event.data.getUint8(i + 2); - active_keys.push({ scancode, value: value / 65535 }); - } - handler(active_keys); - }; + handler(active_keys); + }; + } + else { + //v1 big eddie u16 scancodes , u8 value, up to 16 keys + this.dev.oninputreport = function (event) { + const active_keys = []; + for (let i = 0; i < event.data.byteLength;) { + const scancode = (event.data.getUint8(i++) << 8) | event.data.getUint8(i++); + if (scancode == 0) break; + const value = event.data.getUint8(i++); + active_keys.push({ scancode, value: value / 255 }); + } + handler(active_keys); + }; + } } - stopListening() - { + stopListening() { this.dev.oninputreport = undefined; } } @@ -913,7 +893,6 @@ class AsProviderBytech extends AsProvider window.analogsense = { providers: [ AsProviderWooting, - AsProviderWootingLegacy, AsProviderRazerHuntsman, AsProviderRazerHuntsmanV3, AsProviderNuphy, @@ -1033,15 +1012,6 @@ window.analogsense = { { return wooting_to_name[scancode]; } - if (scancode in wooting_v2_to_name) - { - return wooting_v2_to_name[scancode]; - } - const lowByte = scancode & 0xFF; - if (lowByte in wooting_to_name) - { - return wooting_to_name[lowByte]; - } return String(Number(scancode)); }, razerScancodeToHidScancode: function(scancode) From 084b7500f2effe6d7c9ba1752e81b97a2f7e23df Mon Sep 17 00:00:00 2001 From: girlglock Date: Fri, 10 Apr 2026 17:17:42 +0200 Subject: [PATCH 5/7] split v1 and v2 into separate providers --- AnalogSense.js | 116 +++++++++++++++++++++++++++++-------------------- 1 file changed, 68 insertions(+), 48 deletions(-) diff --git a/AnalogSense.js b/AnalogSense.js index a9f2d68..e4147d2 100644 --- a/AnalogSense.js +++ b/AnalogSense.js @@ -340,58 +340,77 @@ class AsProvider } } -class AsProviderWooting extends AsProvider { - static populateFilters(filters) { - filters.push({ usagePage: 0xFF54, vendorId: 0x31E3 }); // analog interface v1 - filters.push({ usagePage: 0xFF53, vendorId: 0x31E3 }); // analog interface v2 - filters.push({ usagePage: 0xFF54, vendorId: 0x03EB, productId: 0xFF01 }); // Wooting One with old firmware - filters.push({ usagePage: 0xFF54, vendorId: 0x03EB, productId: 0xFF02 }); // Wooting Two with old firmware +//wooting analog interface v1 +class AsProviderWootingV1 extends AsProvider +{ + static populateFilters(filters) + { + filters.push({ usagePage: 0xFF54, vendorId: 0x31E3 }); + filters.push({ usagePage: 0xFF54, vendorId: 0x03EB, productId: 0xFF01 }); // Wooting One old firmware + filters.push({ usagePage: 0xFF54, vendorId: 0x03EB, productId: 0xFF02 }); // Wooting Two old firmware } - startListening(handler) { - const isV2 = this.dev.collections.some(c => c.usagePage === 0xFF53); + startListening(handler) + { + //v1 big eddie u16 scancodes , u8 value, up to 16 keys + this.dev.oninputreport = function(event) + { + const active_keys = []; + for (let i = 0; i < event.data.byteLength; ) + { + const scancode = (event.data.getUint8(i++) << 8) | event.data.getUint8(i++); + if (scancode == 0) break; + const value = event.data.getUint8(i++); + active_keys.push({ scancode, value: value / 255 }); + } + handler(active_keys); + }; + } - if (isV2) { - this.dev.oninputreport = function (event) { - const active_keys = []; - const data = event.data; - //each entry is 4 bytes (pos, keycode, namespace + analog lo, analog depth - //namespace 0 are regular hid keys, non zero are media keys - //analog value is 10bit 0 to1023 - for (let i = 0; i + 3 < data.byteLength; i += 4) { - const matrix_pos = data.getUint8(i); - const scancode_lo = data.getUint8(i + 1); - if (scancode_lo === 0) break; - const packed = data.getUint8(i + 2); - const value_hi = data.getUint8(i + 3); - - const scancode_hi = (packed >> 2) & 0xf; - const value_lo = (packed >> 6) & 0x3; - - const scancode = (scancode_hi << 8) | scancode_lo; - const value = (value_hi << 2) | value_lo; - - active_keys.push({ scancode, value: value / 1023 }); - } - handler(active_keys); - }; - } - else { - //v1 big eddie u16 scancodes , u8 value, up to 16 keys - this.dev.oninputreport = function (event) { - const active_keys = []; - for (let i = 0; i < event.data.byteLength;) { - const scancode = (event.data.getUint8(i++) << 8) | event.data.getUint8(i++); - if (scancode == 0) break; - const value = event.data.getUint8(i++); - active_keys.push({ scancode, value: value / 255 }); - } - handler(active_keys); - }; - } + stopListening() + { + this.dev.oninputreport = undefined; } +} - stopListening() { +//wooting analog interface v2 (firmware 2.13.0 and up | wootility 5.3.0 and up) +class AsProviderWootingV2 extends AsProvider +{ + static populateFilters(filters) + { + filters.push({ usagePage: 0xFF53, vendorId: 0x31E3 }); + } + + startListening(handler) + { + this.dev.oninputreport = function(event) + { + const active_keys = []; + const data = event.data; + //each entry is 4 bytes (pos, keycode, namespace + analog lo, analog depth + //namespace 0 are regular hid keys, non zero are media keys + //analog value is 10bit 0 to1023 + for (let i = 0; i + 4 <= data.byteLength; i += 4) + { + const keycode = data.getUint8(i + 1); + if (keycode === 0) break; + const packed = data.getUint8(i + 2); + const value_hi = data.getUint8(i + 3); + + const keyNamespace = (packed >> 2) & 0xf; + const value_lo = (packed >> 6) & 0x3; + + const scancode = (keyNamespace << 8) | keycode; + const value = (value_hi << 2) | value_lo; + + active_keys.push({ scancode, value: value / 1023 }); + } + handler(active_keys); + }; + } + + stopListening() + { this.dev.oninputreport = undefined; } } @@ -892,7 +911,8 @@ class AsProviderBytech extends AsProvider window.analogsense = { providers: [ - AsProviderWooting, + AsProviderWootingV1, + AsProviderWootingV2, AsProviderRazerHuntsman, AsProviderRazerHuntsmanV3, AsProviderNuphy, From 994cbf245e46c327eced2a5ae0eb2dc6fee1c271 Mon Sep 17 00:00:00 2001 From: girlglock Date: Wed, 15 Apr 2026 22:27:48 +0200 Subject: [PATCH 6/7] use value and full scancode for nulls --- AnalogSense.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AnalogSense.js b/AnalogSense.js index e4147d2..e30891b 100644 --- a/AnalogSense.js +++ b/AnalogSense.js @@ -389,11 +389,10 @@ class AsProviderWootingV2 extends AsProvider const data = event.data; //each entry is 4 bytes (pos, keycode, namespace + analog lo, analog depth //namespace 0 are regular hid keys, non zero are media keys - //analog value is 10bit 0 to1023 + //analog value is 10bit 0 to 1023 for (let i = 0; i + 4 <= data.byteLength; i += 4) { const keycode = data.getUint8(i + 1); - if (keycode === 0) break; const packed = data.getUint8(i + 2); const value_hi = data.getUint8(i + 3); @@ -403,6 +402,9 @@ class AsProviderWootingV2 extends AsProvider const scancode = (keyNamespace << 8) | keycode; const value = (value_hi << 2) | value_lo; + if (keyNamespace === 0 && keycode === 0) break; + if (value === 0) continue; + active_keys.push({ scancode, value: value / 1023 }); } handler(active_keys); From e0e09abb71324d7ea5523d8fffa3c01af3b47ada Mon Sep 17 00:00:00 2001 From: girlglock Date: Wed, 15 Apr 2026 23:25:06 +0200 Subject: [PATCH 7/7] compare scancode to 0 instead --- AnalogSense.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AnalogSense.js b/AnalogSense.js index e30891b..c90c240 100644 --- a/AnalogSense.js +++ b/AnalogSense.js @@ -402,7 +402,7 @@ class AsProviderWootingV2 extends AsProvider const scancode = (keyNamespace << 8) | keycode; const value = (value_hi << 2) | value_lo; - if (keyNamespace === 0 && keycode === 0) break; + if (scancode === 0) break; if (value === 0) continue; active_keys.push({ scancode, value: value / 1023 });