@openmouse/protocol 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +167 -0
- package/README.md +89 -0
- package/dist/asus/index.d.ts +52 -0
- package/dist/asus/index.d.ts.map +1 -0
- package/dist/asus/index.js +248 -0
- package/dist/asus/index.js.map +1 -0
- package/dist/atk/index.d.ts +200 -0
- package/dist/atk/index.d.ts.map +1 -0
- package/dist/atk/index.js +480 -0
- package/dist/atk/index.js.map +1 -0
- package/dist/bitmouse/index.d.ts +284 -0
- package/dist/bitmouse/index.d.ts.map +1 -0
- package/dist/bitmouse/index.js +396 -0
- package/dist/bitmouse/index.js.map +1 -0
- package/dist/compx/codec.d.ts +31 -0
- package/dist/compx/codec.d.ts.map +1 -0
- package/dist/compx/codec.js +50 -0
- package/dist/compx/codec.js.map +1 -0
- package/dist/corsair/index.d.ts +147 -0
- package/dist/corsair/index.d.ts.map +1 -0
- package/dist/corsair/index.js +223 -0
- package/dist/corsair/index.js.map +1 -0
- package/dist/dareu/index.d.ts +136 -0
- package/dist/dareu/index.d.ts.map +1 -0
- package/dist/dareu/index.js +344 -0
- package/dist/dareu/index.js.map +1 -0
- package/dist/drivers/asus/hid.d.ts +29 -0
- package/dist/drivers/asus/hid.d.ts.map +1 -0
- package/dist/drivers/asus/hid.js +419 -0
- package/dist/drivers/asus/hid.js.map +1 -0
- package/dist/drivers/atk/bitmouse-hid.d.ts +80 -0
- package/dist/drivers/atk/bitmouse-hid.d.ts.map +1 -0
- package/dist/drivers/atk/bitmouse-hid.js +501 -0
- package/dist/drivers/atk/bitmouse-hid.js.map +1 -0
- package/dist/drivers/atk/hid.d.ts +163 -0
- package/dist/drivers/atk/hid.d.ts.map +1 -0
- package/dist/drivers/atk/hid.js +1368 -0
- package/dist/drivers/atk/hid.js.map +1 -0
- package/dist/drivers/atk/products.d.ts +13 -0
- package/dist/drivers/atk/products.d.ts.map +1 -0
- package/dist/drivers/atk/products.js +13 -0
- package/dist/drivers/atk/products.js.map +1 -0
- package/dist/drivers/attackshark/dpi.d.ts +41 -0
- package/dist/drivers/attackshark/dpi.d.ts.map +1 -0
- package/dist/drivers/attackshark/dpi.js +215 -0
- package/dist/drivers/attackshark/dpi.js.map +1 -0
- package/dist/drivers/attackshark/hid.d.ts +124 -0
- package/dist/drivers/attackshark/hid.d.ts.map +1 -0
- package/dist/drivers/attackshark/hid.js +728 -0
- package/dist/drivers/attackshark/hid.js.map +1 -0
- package/dist/drivers/corsair/hid.d.ts +70 -0
- package/dist/drivers/corsair/hid.d.ts.map +1 -0
- package/dist/drivers/corsair/hid.js +414 -0
- package/dist/drivers/corsair/hid.js.map +1 -0
- package/dist/drivers/dareu/hid.d.ts +59 -0
- package/dist/drivers/dareu/hid.d.ts.map +1 -0
- package/dist/drivers/dareu/hid.js +478 -0
- package/dist/drivers/dareu/hid.js.map +1 -0
- package/dist/drivers/endgame/egg-op1-hid.d.ts +107 -0
- package/dist/drivers/endgame/egg-op1-hid.d.ts.map +1 -0
- package/dist/drivers/endgame/egg-op1-hid.js +605 -0
- package/dist/drivers/endgame/egg-op1-hid.js.map +1 -0
- package/dist/drivers/endgame/egg-we-control.d.ts +42 -0
- package/dist/drivers/endgame/egg-we-control.d.ts.map +1 -0
- package/dist/drivers/endgame/egg-we-control.js +87 -0
- package/dist/drivers/endgame/egg-we-control.js.map +1 -0
- package/dist/drivers/endgame/egg-we-hid.d.ts +58 -0
- package/dist/drivers/endgame/egg-we-hid.d.ts.map +1 -0
- package/dist/drivers/endgame/egg-we-hid.js +514 -0
- package/dist/drivers/endgame/egg-we-hid.js.map +1 -0
- package/dist/drivers/fantech/hid.d.ts +106 -0
- package/dist/drivers/fantech/hid.d.ts.map +1 -0
- package/dist/drivers/fantech/hid.js +295 -0
- package/dist/drivers/fantech/hid.js.map +1 -0
- package/dist/drivers/finalmouse/hid.d.ts +31 -0
- package/dist/drivers/finalmouse/hid.d.ts.map +1 -0
- package/dist/drivers/finalmouse/hid.js +181 -0
- package/dist/drivers/finalmouse/hid.js.map +1 -0
- package/dist/drivers/gearhub/hid.d.ts +191 -0
- package/dist/drivers/gearhub/hid.d.ts.map +1 -0
- package/dist/drivers/gearhub/hid.js +542 -0
- package/dist/drivers/gearhub/hid.js.map +1 -0
- package/dist/drivers/glorious/classic-hid.d.ts +62 -0
- package/dist/drivers/glorious/classic-hid.d.ts.map +1 -0
- package/dist/drivers/glorious/classic-hid.js +326 -0
- package/dist/drivers/glorious/classic-hid.js.map +1 -0
- package/dist/drivers/glorious/hid.d.ts +39 -0
- package/dist/drivers/glorious/hid.d.ts.map +1 -0
- package/dist/drivers/glorious/hid.js +203 -0
- package/dist/drivers/glorious/hid.js.map +1 -0
- package/dist/drivers/gwolves/hid.d.ts +27 -0
- package/dist/drivers/gwolves/hid.d.ts.map +1 -0
- package/dist/drivers/gwolves/hid.js +225 -0
- package/dist/drivers/gwolves/hid.js.map +1 -0
- package/dist/drivers/gwolves/products.d.ts +17 -0
- package/dist/drivers/gwolves/products.d.ts.map +1 -0
- package/dist/drivers/gwolves/products.js +58 -0
- package/dist/drivers/gwolves/products.js.map +1 -0
- package/dist/drivers/hyperx/hid.d.ts +59 -0
- package/dist/drivers/hyperx/hid.d.ts.map +1 -0
- package/dist/drivers/hyperx/hid.js +229 -0
- package/dist/drivers/hyperx/hid.js.map +1 -0
- package/dist/drivers/incott/hid.d.ts +513 -0
- package/dist/drivers/incott/hid.d.ts.map +1 -0
- package/dist/drivers/incott/hid.js +1173 -0
- package/dist/drivers/incott/hid.js.map +1 -0
- package/dist/drivers/index.d.ts +5 -0
- package/dist/drivers/index.d.ts.map +1 -0
- package/dist/drivers/index.js +5 -0
- package/dist/drivers/index.js.map +1 -0
- package/dist/drivers/keychron/m6-hid.d.ts +81 -0
- package/dist/drivers/keychron/m6-hid.d.ts.map +1 -0
- package/dist/drivers/keychron/m6-hid.js +486 -0
- package/dist/drivers/keychron/m6-hid.js.map +1 -0
- package/dist/drivers/keychron/nape-hid.d.ts +60 -0
- package/dist/drivers/keychron/nape-hid.d.ts.map +1 -0
- package/dist/drivers/keychron/nape-hid.js +432 -0
- package/dist/drivers/keychron/nape-hid.js.map +1 -0
- package/dist/drivers/ksnake/hid.d.ts +74 -0
- package/dist/drivers/ksnake/hid.d.ts.map +1 -0
- package/dist/drivers/ksnake/hid.js +368 -0
- package/dist/drivers/ksnake/hid.js.map +1 -0
- package/dist/drivers/lamzu/hid.d.ts +58 -0
- package/dist/drivers/lamzu/hid.d.ts.map +1 -0
- package/dist/drivers/lamzu/hid.js +414 -0
- package/dist/drivers/lamzu/hid.js.map +1 -0
- package/dist/drivers/lamzu-atlantis/hid.d.ts +148 -0
- package/dist/drivers/lamzu-atlantis/hid.d.ts.map +1 -0
- package/dist/drivers/lamzu-atlantis/hid.js +587 -0
- package/dist/drivers/lamzu-atlantis/hid.js.map +1 -0
- package/dist/drivers/logitech/bolt.d.ts +41 -0
- package/dist/drivers/logitech/bolt.d.ts.map +1 -0
- package/dist/drivers/logitech/bolt.js +105 -0
- package/dist/drivers/logitech/bolt.js.map +1 -0
- package/dist/drivers/logitech/hidpp.d.ts +525 -0
- package/dist/drivers/logitech/hidpp.d.ts.map +1 -0
- package/dist/drivers/logitech/hidpp.js +2774 -0
- package/dist/drivers/logitech/hidpp.js.map +1 -0
- package/dist/drivers/logitech/mode-status.d.ts +53 -0
- package/dist/drivers/logitech/mode-status.d.ts.map +1 -0
- package/dist/drivers/logitech/mode-status.js +52 -0
- package/dist/drivers/logitech/mode-status.js.map +1 -0
- package/dist/drivers/logitech/onboard-profiles.d.ts +298 -0
- package/dist/drivers/logitech/onboard-profiles.d.ts.map +1 -0
- package/dist/drivers/logitech/onboard-profiles.js +965 -0
- package/dist/drivers/logitech/onboard-profiles.js.map +1 -0
- package/dist/drivers/logitech/rgb-effects.d.ts +20 -0
- package/dist/drivers/logitech/rgb-effects.d.ts.map +1 -0
- package/dist/drivers/logitech/rgb-effects.js +93 -0
- package/dist/drivers/logitech/rgb-effects.js.map +1 -0
- package/dist/drivers/mchose/a5-gen1-hid.d.ts +36 -0
- package/dist/drivers/mchose/a5-gen1-hid.d.ts.map +1 -0
- package/dist/drivers/mchose/a5-gen1-hid.js +219 -0
- package/dist/drivers/mchose/a5-gen1-hid.js.map +1 -0
- package/dist/drivers/mchose/dock-hid.d.ts +26 -0
- package/dist/drivers/mchose/dock-hid.d.ts.map +1 -0
- package/dist/drivers/mchose/dock-hid.js +176 -0
- package/dist/drivers/mchose/dock-hid.js.map +1 -0
- package/dist/drivers/mchose/hid.d.ts +118 -0
- package/dist/drivers/mchose/hid.d.ts.map +1 -0
- package/dist/drivers/mchose/hid.js +550 -0
- package/dist/drivers/mchose/hid.js.map +1 -0
- package/dist/drivers/mchose/v3-hid.d.ts +109 -0
- package/dist/drivers/mchose/v3-hid.d.ts.map +1 -0
- package/dist/drivers/mchose/v3-hid.js +572 -0
- package/dist/drivers/mchose/v3-hid.js.map +1 -0
- package/dist/drivers/microsoft/hid.d.ts +25 -0
- package/dist/drivers/microsoft/hid.d.ts.map +1 -0
- package/dist/drivers/microsoft/hid.js +247 -0
- package/dist/drivers/microsoft/hid.js.map +1 -0
- package/dist/drivers/moddo/hid.d.ts +31 -0
- package/dist/drivers/moddo/hid.d.ts.map +1 -0
- package/dist/drivers/moddo/hid.js +154 -0
- package/dist/drivers/moddo/hid.js.map +1 -0
- package/dist/drivers/mouse-types.d.ts +388 -0
- package/dist/drivers/mouse-types.d.ts.map +1 -0
- package/dist/drivers/mouse-types.js +2 -0
- package/dist/drivers/mouse-types.js.map +1 -0
- package/dist/drivers/ninjutso/hid.d.ts +62 -0
- package/dist/drivers/ninjutso/hid.d.ts.map +1 -0
- package/dist/drivers/ninjutso/hid.js +680 -0
- package/dist/drivers/ninjutso/hid.js.map +1 -0
- package/dist/drivers/orbital/hid.d.ts +24 -0
- package/dist/drivers/orbital/hid.d.ts.map +1 -0
- package/dist/drivers/orbital/hid.js +85 -0
- package/dist/drivers/orbital/hid.js.map +1 -0
- package/dist/drivers/orbital/host-protocol.d.ts +59 -0
- package/dist/drivers/orbital/host-protocol.d.ts.map +1 -0
- package/dist/drivers/orbital/host-protocol.js +884 -0
- package/dist/drivers/orbital/host-protocol.js.map +1 -0
- package/dist/drivers/pulsar/pulsar-hid.d.ts +63 -0
- package/dist/drivers/pulsar/pulsar-hid.d.ts.map +1 -0
- package/dist/drivers/pulsar/pulsar-hid.js +381 -0
- package/dist/drivers/pulsar/pulsar-hid.js.map +1 -0
- package/dist/drivers/pulsar/pulsar-pro-hid.d.ts +46 -0
- package/dist/drivers/pulsar/pulsar-pro-hid.d.ts.map +1 -0
- package/dist/drivers/pulsar/pulsar-pro-hid.js +343 -0
- package/dist/drivers/pulsar/pulsar-pro-hid.js.map +1 -0
- package/dist/drivers/pulsar/pulsar-xs1-hid.d.ts +40 -0
- package/dist/drivers/pulsar/pulsar-xs1-hid.d.ts.map +1 -0
- package/dist/drivers/pulsar/pulsar-xs1-hid.js +251 -0
- package/dist/drivers/pulsar/pulsar-xs1-hid.js.map +1 -0
- package/dist/drivers/rawm/hid.d.ts +29 -0
- package/dist/drivers/rawm/hid.d.ts.map +1 -0
- package/dist/drivers/rawm/hid.js +210 -0
- package/dist/drivers/rawm/hid.js.map +1 -0
- package/dist/drivers/razer/cobra-hid.d.ts +63 -0
- package/dist/drivers/razer/cobra-hid.d.ts.map +1 -0
- package/dist/drivers/razer/cobra-hid.js +282 -0
- package/dist/drivers/razer/cobra-hid.js.map +1 -0
- package/dist/drivers/razer/hid-open.d.ts +13 -0
- package/dist/drivers/razer/hid-open.d.ts.map +1 -0
- package/dist/drivers/razer/hid-open.js +29 -0
- package/dist/drivers/razer/hid-open.js.map +1 -0
- package/dist/drivers/razer/hid.d.ts +231 -0
- package/dist/drivers/razer/hid.d.ts.map +1 -0
- package/dist/drivers/razer/hid.js +772 -0
- package/dist/drivers/razer/hid.js.map +1 -0
- package/dist/drivers/razer/viper-hid.d.ts +33 -0
- package/dist/drivers/razer/viper-hid.d.ts.map +1 -0
- package/dist/drivers/razer/viper-hid.js +177 -0
- package/dist/drivers/razer/viper-hid.js.map +1 -0
- package/dist/drivers/razer/viper-mini-hid.d.ts +58 -0
- package/dist/drivers/razer/viper-mini-hid.d.ts.map +1 -0
- package/dist/drivers/razer/viper-mini-hid.js +273 -0
- package/dist/drivers/razer/viper-mini-hid.js.map +1 -0
- package/dist/drivers/razer/viper-v4-pro-hid.d.ts +26 -0
- package/dist/drivers/razer/viper-v4-pro-hid.d.ts.map +1 -0
- package/dist/drivers/razer/viper-v4-pro-hid.js +154 -0
- package/dist/drivers/razer/viper-v4-pro-hid.js.map +1 -0
- package/dist/drivers/registry.d.ts +70 -0
- package/dist/drivers/registry.d.ts.map +1 -0
- package/dist/drivers/registry.js +146 -0
- package/dist/drivers/registry.js.map +1 -0
- package/dist/drivers/steelseries/aerox3-hid.d.ts +53 -0
- package/dist/drivers/steelseries/aerox3-hid.d.ts.map +1 -0
- package/dist/drivers/steelseries/aerox3-hid.js +172 -0
- package/dist/drivers/steelseries/aerox3-hid.js.map +1 -0
- package/dist/drivers/steelseries/aerox5-hid.d.ts +54 -0
- package/dist/drivers/steelseries/aerox5-hid.d.ts.map +1 -0
- package/dist/drivers/steelseries/aerox5-hid.js +173 -0
- package/dist/drivers/steelseries/aerox5-hid.js.map +1 -0
- package/dist/drivers/steelseries/aerox5-wireless-hid.d.ts +69 -0
- package/dist/drivers/steelseries/aerox5-wireless-hid.d.ts.map +1 -0
- package/dist/drivers/steelseries/aerox5-wireless-hid.js +234 -0
- package/dist/drivers/steelseries/aerox5-wireless-hid.js.map +1 -0
- package/dist/drivers/steelseries/aerox9-wireless-hid.d.ts +70 -0
- package/dist/drivers/steelseries/aerox9-wireless-hid.d.ts.map +1 -0
- package/dist/drivers/steelseries/aerox9-wireless-hid.js +227 -0
- package/dist/drivers/steelseries/aerox9-wireless-hid.js.map +1 -0
- package/dist/drivers/steelseries/hid.d.ts +48 -0
- package/dist/drivers/steelseries/hid.d.ts.map +1 -0
- package/dist/drivers/steelseries/hid.js +157 -0
- package/dist/drivers/steelseries/hid.js.map +1 -0
- package/dist/drivers/steelseries/prime-mini-wireless-hid.d.ts +62 -0
- package/dist/drivers/steelseries/prime-mini-wireless-hid.d.ts.map +1 -0
- package/dist/drivers/steelseries/prime-mini-wireless-hid.js +215 -0
- package/dist/drivers/steelseries/prime-mini-wireless-hid.js.map +1 -0
- package/dist/drivers/steelseries/prime-plus-hid.d.ts +51 -0
- package/dist/drivers/steelseries/prime-plus-hid.d.ts.map +1 -0
- package/dist/drivers/steelseries/prime-plus-hid.js +150 -0
- package/dist/drivers/steelseries/prime-plus-hid.js.map +1 -0
- package/dist/drivers/steelseries/rival3-wireless-hid.d.ts +61 -0
- package/dist/drivers/steelseries/rival3-wireless-hid.d.ts.map +1 -0
- package/dist/drivers/steelseries/rival3-wireless-hid.js +185 -0
- package/dist/drivers/steelseries/rival3-wireless-hid.js.map +1 -0
- package/dist/drivers/steelseries/rival310-hid.d.ts +59 -0
- package/dist/drivers/steelseries/rival310-hid.d.ts.map +1 -0
- package/dist/drivers/steelseries/rival310-hid.js +182 -0
- package/dist/drivers/steelseries/rival310-hid.js.map +1 -0
- package/dist/drivers/steelseries/rival650-hid.d.ts +68 -0
- package/dist/drivers/steelseries/rival650-hid.d.ts.map +1 -0
- package/dist/drivers/steelseries/rival650-hid.js +209 -0
- package/dist/drivers/steelseries/rival650-hid.js.map +1 -0
- package/dist/drivers/steelseries/sensei-ten-hid.d.ts +62 -0
- package/dist/drivers/steelseries/sensei-ten-hid.d.ts.map +1 -0
- package/dist/drivers/steelseries/sensei-ten-hid.js +186 -0
- package/dist/drivers/steelseries/sensei-ten-hid.js.map +1 -0
- package/dist/drivers/teevolution/hid.d.ts +82 -0
- package/dist/drivers/teevolution/hid.d.ts.map +1 -0
- package/dist/drivers/teevolution/hid.js +607 -0
- package/dist/drivers/teevolution/hid.js.map +1 -0
- package/dist/drivers/vendors.d.ts +254 -0
- package/dist/drivers/vendors.d.ts.map +1 -0
- package/dist/drivers/vendors.js +587 -0
- package/dist/drivers/vendors.js.map +1 -0
- package/dist/drivers/vgn/hid.d.ts +33 -0
- package/dist/drivers/vgn/hid.d.ts.map +1 -0
- package/dist/drivers/vgn/hid.js +213 -0
- package/dist/drivers/vgn/hid.js.map +1 -0
- package/dist/drivers/wallhack/keyboard-hid.d.ts +28 -0
- package/dist/drivers/wallhack/keyboard-hid.d.ts.map +1 -0
- package/dist/drivers/wallhack/keyboard-hid.js +80 -0
- package/dist/drivers/wallhack/keyboard-hid.js.map +1 -0
- package/dist/drivers/wallhack/mouse-hid.d.ts +45 -0
- package/dist/drivers/wallhack/mouse-hid.d.ts.map +1 -0
- package/dist/drivers/wallhack/mouse-hid.js +288 -0
- package/dist/drivers/wallhack/mouse-hid.js.map +1 -0
- package/dist/drivers/webhid.d.ts +59 -0
- package/dist/drivers/webhid.d.ts.map +1 -0
- package/dist/drivers/webhid.js +2 -0
- package/dist/drivers/webhid.js.map +1 -0
- package/dist/drivers/wlmouse/hid.d.ts +81 -0
- package/dist/drivers/wlmouse/hid.d.ts.map +1 -0
- package/dist/drivers/wlmouse/hid.js +576 -0
- package/dist/drivers/wlmouse/hid.js.map +1 -0
- package/dist/drivers/wooting/hid.d.ts +86 -0
- package/dist/drivers/wooting/hid.d.ts.map +1 -0
- package/dist/drivers/wooting/hid.js +320 -0
- package/dist/drivers/wooting/hid.js.map +1 -0
- package/dist/drivers/zaunkoenig/hid.d.ts +31 -0
- package/dist/drivers/zaunkoenig/hid.d.ts.map +1 -0
- package/dist/drivers/zaunkoenig/hid.js +162 -0
- package/dist/drivers/zaunkoenig/hid.js.map +1 -0
- package/dist/endgame-gear/op1.d.ts +165 -0
- package/dist/endgame-gear/op1.d.ts.map +1 -0
- package/dist/endgame-gear/op1.js +384 -0
- package/dist/endgame-gear/op1.js.map +1 -0
- package/dist/endgame-gear/wireless.d.ts +93 -0
- package/dist/endgame-gear/wireless.d.ts.map +1 -0
- package/dist/endgame-gear/wireless.js +242 -0
- package/dist/endgame-gear/wireless.js.map +1 -0
- package/dist/fantech/index.d.ts +9 -0
- package/dist/fantech/index.d.ts.map +1 -0
- package/dist/fantech/index.js +11 -0
- package/dist/fantech/index.js.map +1 -0
- package/dist/finalmouse/index.d.ts +41 -0
- package/dist/finalmouse/index.d.ts.map +1 -0
- package/dist/finalmouse/index.js +106 -0
- package/dist/finalmouse/index.js.map +1 -0
- package/dist/gearhub/index.d.ts +210 -0
- package/dist/gearhub/index.d.ts.map +1 -0
- package/dist/gearhub/index.js +282 -0
- package/dist/gearhub/index.js.map +1 -0
- package/dist/glorious/index.d.ts +89 -0
- package/dist/glorious/index.d.ts.map +1 -0
- package/dist/glorious/index.js +236 -0
- package/dist/glorious/index.js.map +1 -0
- package/dist/glorious-classic/index.d.ts +103 -0
- package/dist/glorious-classic/index.d.ts.map +1 -0
- package/dist/glorious-classic/index.js +304 -0
- package/dist/glorious-classic/index.js.map +1 -0
- package/dist/gwolves/index.d.ts +82 -0
- package/dist/gwolves/index.d.ts.map +1 -0
- package/dist/gwolves/index.js +197 -0
- package/dist/gwolves/index.js.map +1 -0
- package/dist/hyperx/index.d.ts +181 -0
- package/dist/hyperx/index.d.ts.map +1 -0
- package/dist/hyperx/index.js +303 -0
- package/dist/hyperx/index.js.map +1 -0
- package/dist/incott/index.d.ts +1067 -0
- package/dist/incott/index.d.ts.map +1 -0
- package/dist/incott/index.js +1482 -0
- package/dist/incott/index.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/keychron/index.d.ts +194 -0
- package/dist/keychron/index.d.ts.map +1 -0
- package/dist/keychron/index.js +341 -0
- package/dist/keychron/index.js.map +1 -0
- package/dist/ksnake/index.d.ts +157 -0
- package/dist/ksnake/index.d.ts.map +1 -0
- package/dist/ksnake/index.js +314 -0
- package/dist/ksnake/index.js.map +1 -0
- package/dist/lamzu/atlantis.d.ts +237 -0
- package/dist/lamzu/atlantis.d.ts.map +1 -0
- package/dist/lamzu/atlantis.js +263 -0
- package/dist/lamzu/atlantis.js.map +1 -0
- package/dist/lamzu/index.d.ts +60 -0
- package/dist/lamzu/index.d.ts.map +1 -0
- package/dist/lamzu/index.js +86 -0
- package/dist/lamzu/index.js.map +1 -0
- package/dist/logitech/controls.d.ts +138 -0
- package/dist/logitech/controls.d.ts.map +1 -0
- package/dist/logitech/controls.js +184 -0
- package/dist/logitech/controls.js.map +1 -0
- package/dist/logitech/friendly-name.d.ts +50 -0
- package/dist/logitech/friendly-name.d.ts.map +1 -0
- package/dist/logitech/friendly-name.js +74 -0
- package/dist/logitech/friendly-name.js.map +1 -0
- package/dist/logitech/haptics.d.ts +97 -0
- package/dist/logitech/haptics.d.ts.map +1 -0
- package/dist/logitech/haptics.js +116 -0
- package/dist/logitech/haptics.js.map +1 -0
- package/dist/logitech/hosts.d.ts +55 -0
- package/dist/logitech/hosts.d.ts.map +1 -0
- package/dist/logitech/hosts.js +80 -0
- package/dist/logitech/hosts.js.map +1 -0
- package/dist/logitech/index.d.ts +127 -0
- package/dist/logitech/index.d.ts.map +1 -0
- package/dist/logitech/index.js +233 -0
- package/dist/logitech/index.js.map +1 -0
- package/dist/logitech/wheel.d.ts +117 -0
- package/dist/logitech/wheel.d.ts.map +1 -0
- package/dist/logitech/wheel.js +133 -0
- package/dist/logitech/wheel.js.map +1 -0
- package/dist/mchose/a5-gen1.d.ts +38 -0
- package/dist/mchose/a5-gen1.d.ts.map +1 -0
- package/dist/mchose/a5-gen1.js +60 -0
- package/dist/mchose/a5-gen1.js.map +1 -0
- package/dist/mchose/buttons.d.ts +66 -0
- package/dist/mchose/buttons.d.ts.map +1 -0
- package/dist/mchose/buttons.js +184 -0
- package/dist/mchose/buttons.js.map +1 -0
- package/dist/mchose/dock.d.ts +77 -0
- package/dist/mchose/dock.d.ts.map +1 -0
- package/dist/mchose/dock.js +133 -0
- package/dist/mchose/dock.js.map +1 -0
- package/dist/mchose/index.d.ts +299 -0
- package/dist/mchose/index.d.ts.map +1 -0
- package/dist/mchose/index.js +429 -0
- package/dist/mchose/index.js.map +1 -0
- package/dist/mchose/v3-buttons.d.ts +64 -0
- package/dist/mchose/v3-buttons.d.ts.map +1 -0
- package/dist/mchose/v3-buttons.js +258 -0
- package/dist/mchose/v3-buttons.js.map +1 -0
- package/dist/mchose/v3.d.ts +380 -0
- package/dist/mchose/v3.d.ts.map +1 -0
- package/dist/mchose/v3.js +608 -0
- package/dist/mchose/v3.js.map +1 -0
- package/dist/microsoft/index.d.ts +19 -0
- package/dist/microsoft/index.d.ts.map +1 -0
- package/dist/microsoft/index.js +22 -0
- package/dist/microsoft/index.js.map +1 -0
- package/dist/moddo/index.d.ts +72 -0
- package/dist/moddo/index.d.ts.map +1 -0
- package/dist/moddo/index.js +147 -0
- package/dist/moddo/index.js.map +1 -0
- package/dist/ninjutso/index.d.ts +74 -0
- package/dist/ninjutso/index.d.ts.map +1 -0
- package/dist/ninjutso/index.js +133 -0
- package/dist/ninjutso/index.js.map +1 -0
- package/dist/orbital/index.d.ts +14 -0
- package/dist/orbital/index.d.ts.map +1 -0
- package/dist/orbital/index.js +19 -0
- package/dist/orbital/index.js.map +1 -0
- package/dist/pulsar/index.d.ts +81 -0
- package/dist/pulsar/index.d.ts.map +1 -0
- package/dist/pulsar/index.js +185 -0
- package/dist/pulsar/index.js.map +1 -0
- package/dist/rawm/index.d.ts +55 -0
- package/dist/rawm/index.d.ts.map +1 -0
- package/dist/rawm/index.js +229 -0
- package/dist/rawm/index.js.map +1 -0
- package/dist/razer/codec.d.ts +504 -0
- package/dist/razer/codec.d.ts.map +1 -0
- package/dist/razer/codec.js +648 -0
- package/dist/razer/codec.js.map +1 -0
- package/dist/razer/devices.d.ts +156 -0
- package/dist/razer/devices.d.ts.map +1 -0
- package/dist/razer/devices.js +540 -0
- package/dist/razer/devices.js.map +1 -0
- package/dist/razer/index.d.ts +2 -0
- package/dist/razer/index.d.ts.map +1 -0
- package/dist/razer/index.js +2 -0
- package/dist/razer/index.js.map +1 -0
- package/dist/razer/v4.d.ts +19 -0
- package/dist/razer/v4.d.ts.map +1 -0
- package/dist/razer/v4.js +51 -0
- package/dist/razer/v4.js.map +1 -0
- package/dist/steelseries/aerox3.d.ts +236 -0
- package/dist/steelseries/aerox3.d.ts.map +1 -0
- package/dist/steelseries/aerox3.js +311 -0
- package/dist/steelseries/aerox3.js.map +1 -0
- package/dist/steelseries/aerox5-wireless.d.ts +300 -0
- package/dist/steelseries/aerox5-wireless.d.ts.map +1 -0
- package/dist/steelseries/aerox5-wireless.js +389 -0
- package/dist/steelseries/aerox5-wireless.js.map +1 -0
- package/dist/steelseries/aerox5.d.ts +228 -0
- package/dist/steelseries/aerox5.d.ts.map +1 -0
- package/dist/steelseries/aerox5.js +294 -0
- package/dist/steelseries/aerox5.js.map +1 -0
- package/dist/steelseries/aerox9-wireless.d.ts +213 -0
- package/dist/steelseries/aerox9-wireless.d.ts.map +1 -0
- package/dist/steelseries/aerox9-wireless.js +305 -0
- package/dist/steelseries/aerox9-wireless.js.map +1 -0
- package/dist/steelseries/devices.d.ts +82 -0
- package/dist/steelseries/devices.d.ts.map +1 -0
- package/dist/steelseries/devices.js +310 -0
- package/dist/steelseries/devices.js.map +1 -0
- package/dist/steelseries/index.d.ts +13 -0
- package/dist/steelseries/index.d.ts.map +1 -0
- package/dist/steelseries/index.js +13 -0
- package/dist/steelseries/index.js.map +1 -0
- package/dist/steelseries/prime-mini-wireless.d.ts +253 -0
- package/dist/steelseries/prime-mini-wireless.d.ts.map +1 -0
- package/dist/steelseries/prime-mini-wireless.js +351 -0
- package/dist/steelseries/prime-mini-wireless.js.map +1 -0
- package/dist/steelseries/prime-plus.d.ts +179 -0
- package/dist/steelseries/prime-plus.d.ts.map +1 -0
- package/dist/steelseries/prime-plus.js +275 -0
- package/dist/steelseries/prime-plus.js.map +1 -0
- package/dist/steelseries/rival3-wireless.d.ts +222 -0
- package/dist/steelseries/rival3-wireless.d.ts.map +1 -0
- package/dist/steelseries/rival3-wireless.js +308 -0
- package/dist/steelseries/rival3-wireless.js.map +1 -0
- package/dist/steelseries/rival3.d.ts +99 -0
- package/dist/steelseries/rival3.d.ts.map +1 -0
- package/dist/steelseries/rival3.js +147 -0
- package/dist/steelseries/rival3.js.map +1 -0
- package/dist/steelseries/rival310.d.ts +230 -0
- package/dist/steelseries/rival310.d.ts.map +1 -0
- package/dist/steelseries/rival310.js +322 -0
- package/dist/steelseries/rival310.js.map +1 -0
- package/dist/steelseries/rival650.d.ts +215 -0
- package/dist/steelseries/rival650.d.ts.map +1 -0
- package/dist/steelseries/rival650.js +284 -0
- package/dist/steelseries/rival650.js.map +1 -0
- package/dist/steelseries/sensei-ten.d.ts +283 -0
- package/dist/steelseries/sensei-ten.d.ts.map +1 -0
- package/dist/steelseries/sensei-ten.js +389 -0
- package/dist/steelseries/sensei-ten.js.map +1 -0
- package/dist/teevolution/index.d.ts +211 -0
- package/dist/teevolution/index.d.ts.map +1 -0
- package/dist/teevolution/index.js +458 -0
- package/dist/teevolution/index.js.map +1 -0
- package/dist/valkyrie/index.d.ts +19 -0
- package/dist/valkyrie/index.d.ts.map +1 -0
- package/dist/valkyrie/index.js +43 -0
- package/dist/valkyrie/index.js.map +1 -0
- package/dist/valkyrie/settings.d.ts +28 -0
- package/dist/valkyrie/settings.d.ts.map +1 -0
- package/dist/valkyrie/settings.js +84 -0
- package/dist/valkyrie/settings.js.map +1 -0
- package/dist/vgn/index.d.ts +59 -0
- package/dist/vgn/index.d.ts.map +1 -0
- package/dist/vgn/index.js +165 -0
- package/dist/vgn/index.js.map +1 -0
- package/dist/wallhack/index.d.ts +165 -0
- package/dist/wallhack/index.d.ts.map +1 -0
- package/dist/wallhack/index.js +226 -0
- package/dist/wallhack/index.js.map +1 -0
- package/dist/wlmouse/index.d.ts +4 -0
- package/dist/wlmouse/index.d.ts.map +1 -0
- package/dist/wlmouse/index.js +7 -0
- package/dist/wlmouse/index.js.map +1 -0
- package/dist/wooting/index.d.ts +225 -0
- package/dist/wooting/index.d.ts.map +1 -0
- package/dist/wooting/index.js +296 -0
- package/dist/wooting/index.js.map +1 -0
- package/dist/zaunkoenig/index.d.ts +49 -0
- package/dist/zaunkoenig/index.d.ts.map +1 -0
- package/dist/zaunkoenig/index.js +127 -0
- package/dist/zaunkoenig/index.js.map +1 -0
- package/package.json +193 -0
|
@@ -0,0 +1,1482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incott HID protocol — transport-independent codec.
|
|
3
|
+
*
|
|
4
|
+
* Derived from IncottHIDApp (`romkazor/IncottHIDApp`, MIT licensed; this
|
|
5
|
+
* module reuses its protocol knowledge, not its code) and verified against
|
|
6
|
+
* an "incott 8K wireless mouse" (093A:522C) on real hardware. Ported from
|
|
7
|
+
* IncottHub (https://github.com/vladidraganov/IncottHub), which documents the
|
|
8
|
+
* verification in `docs/superpowers/specs/2026-09-07-incotthub-design.md`
|
|
9
|
+
* sections 4, 5, 8 and 13.
|
|
10
|
+
*
|
|
11
|
+
* DPI and battery were corrected on 2026-09-07 against a second, higher-trust
|
|
12
|
+
* source: the owner instrumented Incott's own WebHID configurator
|
|
13
|
+
* (incott.net/mouse/, hooking HIDDevice.prototype.sendFeatureReport /
|
|
14
|
+
* receiveFeatureReport) and captured real traffic to a G23V2Pro. That capture
|
|
15
|
+
* is ground truth and disproved two assumptions this driver inherited from
|
|
16
|
+
* IncottHIDApp — see `captures/incott-8k-wireless/vendor-tool-session.hex`
|
|
17
|
+
* and `docs/incott-testing.md`.
|
|
18
|
+
*
|
|
19
|
+
* A THIRD round of verification on 2026-09-08 used node-hid directly against
|
|
20
|
+
* an "incott 8K wireless mouse" and, uniquely among the capture sessions
|
|
21
|
+
* above, performed real WRITES and read every one back before restoring the
|
|
22
|
+
* original value. It fixed the DPI stage-index write bug (see
|
|
23
|
+
* `INCOTT_DPI_STAGE_COUNT`), confirmed `0x82` is the per-stage DPI value read
|
|
24
|
+
* and that it echoes its sub-command, confirmed the polling-rate byte at
|
|
25
|
+
* `0x81`, confirmed the packed byte-7 nibble reads for lift-off and motion
|
|
26
|
+
* sync agree with their symmetric `0x84` single-purpose reads, and added a
|
|
27
|
+
* button-binding codec. See
|
|
28
|
+
* `captures/incott-8k-wireless/write-roundtrip.hex` and
|
|
29
|
+
* `docs/incott-testing.md`.
|
|
30
|
+
*
|
|
31
|
+
* A FIFTH round, 2026-09-08, RELOCATED battery entirely. Charging a unit
|
|
32
|
+
* through a full cycle (roughly 60% to roughly 97%) while polling the
|
|
33
|
+
* `0x8e`/sub `0x01` feature-report reply showed byte 6 never move at all —
|
|
34
|
+
* the exact frame `09 8e 01 5a 04 84 38 01` the entire time. A constant is
|
|
35
|
+
* not a battery reading; see `INCOTT_CMD_QUERY_BATTERY` and
|
|
36
|
+
* `incottDecodeBattery` for what this disproves and why the codec is kept
|
|
37
|
+
* anyway (regression coverage), and `docs/incott-testing.md` for the write-up.
|
|
38
|
+
* The real level lives in an UNSOLICITED INPUT report the mouse emits on
|
|
39
|
+
* report id 0x09 while it is actively being used — not a feature-report
|
|
40
|
+
* reply to any request this driver sends. See `incottDecodeInputStatus` for
|
|
41
|
+
* the codec and `src/drivers/incott/hid.ts`'s `onInputReport` for how the
|
|
42
|
+
* WebHID client listens for it and caches the result.
|
|
43
|
+
*
|
|
44
|
+
* A FOURTH fix, same day, closed a second DPI bug found by an owner report:
|
|
45
|
+
* `IncottHidClient.setDpi` was reading the active stage and then writing the
|
|
46
|
+
* requested DPI *into* that stage, silently overwriting whatever value the
|
|
47
|
+
* stage held — repeated use progressively destroyed the mouse's factory
|
|
48
|
+
* table (one owner's stage 2 was found changed from 1600 to 800 this way).
|
|
49
|
+
* DPI is genuinely four independent operations, not one: select the active
|
|
50
|
+
* stage (`INCOTT_CMD_SET_DPI_STAGE`/`incottEncodeSetActiveDpiStage`, `09 03
|
|
51
|
+
* 06 <idx>`), edit a stage's value (`INCOTT_CMD_SET_DPI`/`incottEncodeSetDpi`,
|
|
52
|
+
* `09 02 <idx> <lo> <hi>`), read which stage is active
|
|
53
|
+
* (`INCOTT_CMD_QUERY_DPI_STAGE`, `09 83 06`), and read a stage's value
|
|
54
|
+
* (`INCOTT_CMD_QUERY_DPI_STAGE_VALUE`, `09 82 <idx>`). Select and edit were
|
|
55
|
+
* verified as genuinely distinct operations: selecting stages 0, 3, 5, then 1
|
|
56
|
+
* in turn each read back correctly via `0x83`/`0x06`, and reading all six
|
|
57
|
+
* stages via `0x82` before and after showed the table's contents completely
|
|
58
|
+
* unchanged by those selects. See `INCOTT_CMD_SET_DPI_STAGE` for the full
|
|
59
|
+
* writeup, `src/drivers/incott/hid.ts` for how the driver now exposes
|
|
60
|
+
* `setActiveDpiStage`/`setDpiStageValue` as OpenMouse's existing generic
|
|
61
|
+
* multi-stage DPI editor contract, and
|
|
62
|
+
* `captures/incott-8k-wireless/write-roundtrip.hex` /
|
|
63
|
+
* `docs/incott-testing.md` for the capture.
|
|
64
|
+
*
|
|
65
|
+
* All traffic is HID feature reports on report ID 0x09. Requests are 9 bytes
|
|
66
|
+
* `[0x09, cmd, sub, ...args]`; WebHID's `sendFeatureReport` takes the report
|
|
67
|
+
* ID as a separate argument, so the payloads this module builds are 8 bytes
|
|
68
|
+
* and omit it. Responses are read with `receiveFeatureReport(0x09)`, which
|
|
69
|
+
* returns the report ID at byte 0, so decoders index frames that include it.
|
|
70
|
+
*
|
|
71
|
+
* Opcodes are paired: `0x0N` sets a value, `0x8N` queries it. The device
|
|
72
|
+
* latches a single shared response buffer — see `incottFrameMatches` and
|
|
73
|
+
* `src/drivers/incott/hid.ts` for how the driver layer avoids decoding a
|
|
74
|
+
* frame left over from a previous query.
|
|
75
|
+
*
|
|
76
|
+
* A SIXTH round, 2026-09-10, instrumented Incott's own web configurator again,
|
|
77
|
+
* this time with every click LABELLED, settling the performance-mode
|
|
78
|
+
* value-to-label mapping left open by the 2026-09-07 capture: HP=2, Corded=1,
|
|
79
|
+
* LP=0 — the REVERSE of the vendor UI's own left-to-right display order. See
|
|
80
|
+
* `INCOTT_SUB_PERFORMANCE` for the labelled capture,
|
|
81
|
+
* `INCOTT_PERFORMANCE_MODE_TO_WIRE`/`incottPerformanceModeToWire` for the
|
|
82
|
+
* table it lives in, and `src/drivers/incott/hid.ts`'s `setPowerMode`/
|
|
83
|
+
* `getPowerModes` for where it is now wired into OpenMouse's shared
|
|
84
|
+
* `powerMode`/`powerModes` contract. The same session also verified the
|
|
85
|
+
* receiver LED labels (previously prior art, now confirmed — see
|
|
86
|
+
* `INCOTT_RECEIVER_LED_MODES`), found a DPI-write axis byte at payload index
|
|
87
|
+
* 7 (see `incottEncodeSetDpi`), and established that onboard profiles are a
|
|
88
|
+
* vendor-software construct with no on-device select command. See
|
|
89
|
+
* `docs/incott-testing.md` and
|
|
90
|
+
* `captures/incott-8k-wireless/vendor-tool-session-2026-09-10.hex`.
|
|
91
|
+
*
|
|
92
|
+
* This module must not import WebHID types or talk to a device; see
|
|
93
|
+
* `src/drivers/incott/hid.ts` for the WebHID client.
|
|
94
|
+
*/
|
|
95
|
+
export const INCOTT_VENDOR_ID = 0x093a;
|
|
96
|
+
/** The 2.4 GHz dongle's product id — "incott 8K wireless mouse" in its product string. */
|
|
97
|
+
export const INCOTT_PRODUCT_ID = 0x522c;
|
|
98
|
+
/**
|
|
99
|
+
* The WIRED product id — hardware-verified 2026-09-08 by plugging the mouse
|
|
100
|
+
* in over USB and reading `device.productId` directly: it enumerates as
|
|
101
|
+
* `0x622C`, not `0x522C`. This used to be named `INCOTT_PRODUCT_ID_CHARGING`
|
|
102
|
+
* and `incottIsChargingProduct` treated it as a charging FLAG — that was the
|
|
103
|
+
* wrong axis. `0x622C` means the CONNECTION is wired; charging is a
|
|
104
|
+
* consequence of being plugged in, not the thing this id actually encodes.
|
|
105
|
+
* See `incottIsWiredProduct` (the renamed helper) and
|
|
106
|
+
* `IncottHidClient.readStatus`, which now derives both `connectionType` and
|
|
107
|
+
* `batteryState: "Charging"` from wired-ness rather than from a constant
|
|
108
|
+
* that claimed to mean "charging."
|
|
109
|
+
*/
|
|
110
|
+
export const INCOTT_PRODUCT_ID_WIRED = 0x622c;
|
|
111
|
+
export const INCOTT_PRODUCT_IDS = [
|
|
112
|
+
INCOTT_PRODUCT_ID,
|
|
113
|
+
INCOTT_PRODUCT_ID_WIRED,
|
|
114
|
+
];
|
|
115
|
+
/** The vendor collection that answers protocol requests. */
|
|
116
|
+
export const INCOTT_USAGE_PAGE = 0xff05;
|
|
117
|
+
/** Any vendor-defined page, used as a fallback when 0xFF05 is absent. */
|
|
118
|
+
export const INCOTT_VENDOR_USAGE_PAGE_MIN = 0xff00;
|
|
119
|
+
export const INCOTT_REPORT_ID = 0x09;
|
|
120
|
+
/** Payload length excluding the report ID (WebHID sends it separately). */
|
|
121
|
+
export const INCOTT_PAYLOAD_LENGTH = 8;
|
|
122
|
+
/** Length requested from receiveFeatureReport, including the report ID. */
|
|
123
|
+
export const INCOTT_RESPONSE_LENGTH = 64;
|
|
124
|
+
/** Set opcodes. Query opcodes are the same value with bit 7 set. */
|
|
125
|
+
export const INCOTT_CMD_SET_POLLING = 0x01;
|
|
126
|
+
/**
|
|
127
|
+
* Writes the actual DPI value (see `incottEncodeSetDpi`), NOT a preset index.
|
|
128
|
+
* Proven on hardware 2026-09-07 against a real G23V2Pro; this used to be
|
|
129
|
+
* `0x03`, which was never verified and is not what the vendor tool sends.
|
|
130
|
+
*/
|
|
131
|
+
export const INCOTT_CMD_SET_DPI = 0x02;
|
|
132
|
+
export const INCOTT_CMD_SET_SENSOR = 0x04;
|
|
133
|
+
export const INCOTT_CMD_SET_TIMING = 0x05;
|
|
134
|
+
/**
|
|
135
|
+
* Button binding write, `09 06 <button 0..5> <...payload>` — round-trip
|
|
136
|
+
* confirmed on hardware 2026-09-08: the vendor tool wrote
|
|
137
|
+
* `09 06 00 01 00 f0` to button 0, and reading it back via `0x86`/sub `00`
|
|
138
|
+
* afterwards returned the identical three payload bytes. See
|
|
139
|
+
* `incottEncodeSetButtonBinding` — this only encodes the raw binding; the
|
|
140
|
+
* meaning of its bytes (key code vs. macro vs. remap) is NOT established, so
|
|
141
|
+
* nothing here interprets them.
|
|
142
|
+
*/
|
|
143
|
+
export const INCOTT_CMD_SET_BUTTON = 0x06;
|
|
144
|
+
/**
|
|
145
|
+
* Announces one 32-byte chunk of a macro buffer:
|
|
146
|
+
* `09 07 <chunks> <chunk index> <bytes per chunk> <buffer id>`.
|
|
147
|
+
*
|
|
148
|
+
* Each header is followed by the chunk itself as a 32-byte OUTPUT report on
|
|
149
|
+
* the same id — see `IncottHidClient.uploadMacro`.
|
|
150
|
+
*/
|
|
151
|
+
export const INCOTT_CMD_MACRO_CHUNK = 0x07;
|
|
152
|
+
export const INCOTT_CMD_SET_RECEIVER_LED = 0x08;
|
|
153
|
+
/**
|
|
154
|
+
* Selects which of the six DPI stages is ACTIVE: `09 03 06 <idx>` (command
|
|
155
|
+
* `0x03`, sub-command `INCOTT_SUB_DPI_STAGE`, then the stage index). Pairs
|
|
156
|
+
* with `INCOTT_CMD_QUERY_DPI_STAGE` (`0x83`), which reads the active index
|
|
157
|
+
* back at the same sub-command — see `incottEncodeSetActiveDpiStage`.
|
|
158
|
+
*
|
|
159
|
+
* This is a SELECT, not an EDIT: it never touches the six-stage table's
|
|
160
|
+
* stored values. Verified on hardware 2026-09-08 — selecting stage 0, 3, 5,
|
|
161
|
+
* then 1 in turn each read back identically via `0x83`/`0x06`, and reading
|
|
162
|
+
* all six stages via `0x82` before and after showed the table completely
|
|
163
|
+
* unchanged by those selects.
|
|
164
|
+
*
|
|
165
|
+
* IMPORTANT MISLABEL TO NOT REPEAT: IncottHIDApp calls this command "set
|
|
166
|
+
* DPI" and treats its six "DPI presets" as if picking one changes the DPI
|
|
167
|
+
* value. It does not — it only changes which already-stored stage answers
|
|
168
|
+
* as active. The bug this driver used to have (`setDpi` writing the
|
|
169
|
+
* requested value into whichever stage happened to be active, silently
|
|
170
|
+
* overwriting the factory table one stage at a time) came directly from
|
|
171
|
+
* conflating this select with `INCOTT_CMD_SET_DPI` (`0x02`), the command
|
|
172
|
+
* that actually edits a stage's stored value. See `incottEncodeSetDpi` and
|
|
173
|
+
* `docs/incott-testing.md`.
|
|
174
|
+
*/
|
|
175
|
+
export const INCOTT_CMD_SET_DPI_STAGE = 0x03;
|
|
176
|
+
/**
|
|
177
|
+
* Returns the active DPI *stage index* (0-5), not a DPI value — see
|
|
178
|
+
* `incottDecodeDpiStageIndex`. Still `0x83`; only the interpretation of its
|
|
179
|
+
* payload changed.
|
|
180
|
+
*/
|
|
181
|
+
export const INCOTT_CMD_QUERY_DPI_STAGE = 0x83;
|
|
182
|
+
/**
|
|
183
|
+
* Reads the numeric DPI value stored in one of the six stages,
|
|
184
|
+
* `09 82 <stage 0..5>` -> little-endian uint16 at RESPONSE bytes 3-4 (same
|
|
185
|
+
* `wire = dpi/50 - 1` encoding as the write). Verified on hardware
|
|
186
|
+
* 2026-09-08 by reading all six stages back as 400/800/1600/2400/3200/6400
|
|
187
|
+
* (wire 0x07/0x0f/0x1f/0x2f/0x3f/0x7f), then writing 25000 to stage 2 and
|
|
188
|
+
* reading it back as exactly 25000, then restoring 1600 and reading that
|
|
189
|
+
* back too — six independent points on `dpi = (wire + 1) * 50`, plus a
|
|
190
|
+
* write/restore round-trip. This was previously `INCOTT_CMD_UNKNOWN_82`: an
|
|
191
|
+
* earlier opcode sweep only ever tried sub `0x00` and got back an
|
|
192
|
+
* uninterpreted payload (`09 82 00 07 00 …`) — the same "sub-commands are
|
|
193
|
+
* not optional" lesson `0x8e` (battery) taught. See `incottDecodeDpiStage`.
|
|
194
|
+
*/
|
|
195
|
+
export const INCOTT_CMD_QUERY_DPI_STAGE_VALUE = 0x82;
|
|
196
|
+
export const INCOTT_CMD_QUERY_POLLING = 0x81;
|
|
197
|
+
export const INCOTT_CMD_QUERY_SENSOR = 0x84;
|
|
198
|
+
export const INCOTT_CMD_QUERY_TIMING = 0x85;
|
|
199
|
+
export const INCOTT_CMD_QUERY_RECEIVER_LED = 0x88;
|
|
200
|
+
/**
|
|
201
|
+
* Answers, but byte 8 of the reply returned the same constant `0x5a` (90) on
|
|
202
|
+
* every capture ever taken, across every device state and both capture
|
|
203
|
+
* sessions (2026-09-07 read-only sweep and the later vendor-tool traffic).
|
|
204
|
+
* That is disproof, not confirmation, that byte 8 is a battery percentage —
|
|
205
|
+
* see `docs/incott-testing.md`. Nothing in this driver decodes this response
|
|
206
|
+
* any more. DISPROVEN A SECOND TIME on 2026-09-08: `0x8e`/sub `0x01` byte 6
|
|
207
|
+
* (see below), the value this comment used to point to as "the real battery
|
|
208
|
+
* percentage," is ALSO a constant — see `INCOTT_CMD_QUERY_BATTERY`. The real
|
|
209
|
+
* battery level lives in an unsolicited input report; see
|
|
210
|
+
* `incottDecodeInputStatus`. Kept only because the opcode itself still
|
|
211
|
+
* answers and its real meaning is an open question worth recording.
|
|
212
|
+
*/
|
|
213
|
+
export const INCOTT_CMD_QUERY_STATUS = 0x89;
|
|
214
|
+
export const INCOTT_CMD_QUERY_IDENTITY = 0x8f;
|
|
215
|
+
/**
|
|
216
|
+
* Battery percentage, but ONLY on sub-command `0x01` — the original opcode
|
|
217
|
+
* sweep swept every command with sub `0x00` and concluded `0x8e` was
|
|
218
|
+
* unimplemented because it never answered. Sub-commands are not optional on
|
|
219
|
+
* this opcode (or on `0x86`, which the vendor tool queries as `09 86 09`).
|
|
220
|
+
*
|
|
221
|
+
* DISPROVEN 2026-09-08, the same way `0x89` byte 8 was disproven before it
|
|
222
|
+
* (see `INCOTT_CMD_QUERY_STATUS`): charging a unit through a full cycle from
|
|
223
|
+
* roughly 60% to roughly 97% while polling this response showed byte 6 NEVER
|
|
224
|
+
* CHANGE — the identical frame `09 8e 01 5a 04 84 38 01` the entire time,
|
|
225
|
+
* `0x38` = 56 throughout. A value that does not move while the real battery
|
|
226
|
+
* level visibly does cannot be a battery reading; it is a constant of unknown
|
|
227
|
+
* meaning, exactly like `0x89` byte 8 before it. `incottDecodeBattery` is
|
|
228
|
+
* kept only as a codec (its mechanical byte-6 decode is unchanged and still
|
|
229
|
+
* tested) and for the driver-level regression test that battery is no longer
|
|
230
|
+
* sourced from here — see `IncottHidClient.readStatus` and
|
|
231
|
+
* `docs/incott-testing.md`. The real battery percentage is a field of the
|
|
232
|
+
* unsolicited input report the mouse emits while in use — see
|
|
233
|
+
* `incottDecodeInputStatus`.
|
|
234
|
+
*/
|
|
235
|
+
export const INCOTT_CMD_QUERY_BATTERY = 0x8e;
|
|
236
|
+
export const INCOTT_SUB_BATTERY = 0x01;
|
|
237
|
+
/**
|
|
238
|
+
* The report id the mouse's UNSOLICITED input reports arrive on — the same
|
|
239
|
+
* value as `INCOTT_REPORT_ID` (`0x09`), which this module otherwise uses only
|
|
240
|
+
* for feature-report request/response pairs. These input reports are NOT a
|
|
241
|
+
* reply to anything this driver sends: the device emits them on its own,
|
|
242
|
+
* only while it is actively being used (moved or clicked), carrying battery
|
|
243
|
+
* and a packed DPI-stage/polling-rate snapshot. See `incottDecodeInputStatus`
|
|
244
|
+
* and `src/drivers/incott/hid.ts`'s `onInputReport`.
|
|
245
|
+
*/
|
|
246
|
+
export const INCOTT_INPUT_REPORT_ID = INCOTT_REPORT_ID;
|
|
247
|
+
/**
|
|
248
|
+
* Button binding read, `09 86 <button 0..5>` -> response bytes 3-5 = the raw
|
|
249
|
+
* three-byte binding written by `INCOTT_CMD_SET_BUTTON` at the same index.
|
|
250
|
+
* Round-trip confirmed on hardware 2026-09-08 (see `INCOTT_CMD_SET_BUTTON`).
|
|
251
|
+
* The device has six buttons; reading indices 0-5 on the unit under test
|
|
252
|
+
* returned:
|
|
253
|
+
* 0 -> 01 00 f0 3 -> 01 00 f3
|
|
254
|
+
* 1 -> 01 00 f1 4 -> 01 00 f4
|
|
255
|
+
* 2 -> 01 00 f2 5 -> 07 00 03
|
|
256
|
+
* (left, right, middle, forward, back, DPI — physically, in some order).
|
|
257
|
+
* This was previously `INCOTT_CMD_UNKNOWN_86`. Sub-command `0x09` on the
|
|
258
|
+
* same command reads the onboard profile index instead — see
|
|
259
|
+
* `INCOTT_SUB_PROFILE_INDEX`. The binding itself is a 32-bit little-endian
|
|
260
|
+
* action word; see `incottDecodeButtonBinding`.
|
|
261
|
+
*/
|
|
262
|
+
export const INCOTT_CMD_QUERY_BUTTON = 0x86;
|
|
263
|
+
/** Button count: left, right, middle, forward, back, DPI. */
|
|
264
|
+
export const INCOTT_BUTTON_COUNT = 6;
|
|
265
|
+
/**
|
|
266
|
+
* Reads the onboard profile INDEX, `09 86 09` — not a button index, since
|
|
267
|
+
* buttons only go up to 5. Counterpart of the `09 06 09 <index>` write.
|
|
268
|
+
*
|
|
269
|
+
* Neither is implemented, and that is a finding rather than an omission: the
|
|
270
|
+
* index is real and sticks (0-3), but it gates nothing. Writing a setting
|
|
271
|
+
* while on one slot changes what every other slot reports, so there is a
|
|
272
|
+
* single settings store and the vendor replays every setting on a switch
|
|
273
|
+
* because the mouse holds none of them. Publishing OpenMouse's
|
|
274
|
+
* `profileCount`/`setProfile` contract — which describes ONBOARD profiles —
|
|
275
|
+
* would hand the user a selector that appears to work and does not. See
|
|
276
|
+
* `captures/incott-8k-wireless/profile-index-2026-09-11.hex`.
|
|
277
|
+
*/
|
|
278
|
+
export const INCOTT_SUB_PROFILE_INDEX = 0x09;
|
|
279
|
+
/**
|
|
280
|
+
* Physical buttons, in left-to-right display order.
|
|
281
|
+
*/
|
|
282
|
+
export const INCOTT_BUTTON_NAMES = ["Left", "Right", "Middle", "Forward", "Back", "DPI"];
|
|
283
|
+
/**
|
|
284
|
+
* Display order -> WIRE index. **These are not the same**, and assuming they
|
|
285
|
+
* were would silently swap two buttons.
|
|
286
|
+
*
|
|
287
|
+
* The vendor's per-model key table carries an explicit `matrix` field and
|
|
288
|
+
* addresses the device with it (`setMsK(dvar.key[i].matrix, code)`), not with
|
|
289
|
+
* the array position. For this family Forward sits at array index 3 with
|
|
290
|
+
* `matrix = 4`, and Back at array index 4 with `matrix = 3` — the two are
|
|
291
|
+
* transposed. Every other button's matrix equals its position.
|
|
292
|
+
*/
|
|
293
|
+
export const INCOTT_BUTTON_WIRE_INDEX = {
|
|
294
|
+
Left: 0,
|
|
295
|
+
Right: 1,
|
|
296
|
+
Middle: 2,
|
|
297
|
+
Forward: 4,
|
|
298
|
+
Back: 3,
|
|
299
|
+
DPI: 5,
|
|
300
|
+
};
|
|
301
|
+
/**
|
|
302
|
+
* Button actions, label -> 32-bit action word, in display order.
|
|
303
|
+
*
|
|
304
|
+
* Transcribed from the vendor bundle's `kf_hw()` encoder. One row is
|
|
305
|
+
* confirmed against this contributor's hardware: the factory DPI button reads
|
|
306
|
+
* back `07 00 03`, which is `0x00030007` little-endian — the value `kf_hw`
|
|
307
|
+
* returns for that function.
|
|
308
|
+
*
|
|
309
|
+
* NOT covered here: macros (`slot << 16 | 9`), which need the `0x07` upload
|
|
310
|
+
* command, and `fmeFAVOR`, which the vendor defines as a constant but has no
|
|
311
|
+
* case for in its own encoder, so there is no code to send.
|
|
312
|
+
*/
|
|
313
|
+
const MOUSE_AND_MEDIA_ACTIONS = [
|
|
314
|
+
["Left click", 0x00f00001],
|
|
315
|
+
["Right click", 0x00f10001],
|
|
316
|
+
["Middle click", 0x00f20001],
|
|
317
|
+
["Forward", 0x00f40001],
|
|
318
|
+
["Back", 0x00f30001],
|
|
319
|
+
["DPI cycle", 0x00030007],
|
|
320
|
+
["DPI +", 0x00010007],
|
|
321
|
+
["DPI -", 0x00020007],
|
|
322
|
+
["Rapid fire", 0x0218f00a],
|
|
323
|
+
["Profile switch", 0x0000f10a],
|
|
324
|
+
["Media player", 0x01830003],
|
|
325
|
+
["Play/Pause", 0x00cd0003],
|
|
326
|
+
["Stop", 0x00b70003],
|
|
327
|
+
["Previous track", 0x00b60003],
|
|
328
|
+
["Next track", 0x00b50003],
|
|
329
|
+
["Volume up", 0x00e90003],
|
|
330
|
+
["Volume down", 0x00ea0003],
|
|
331
|
+
["Mute", 0x00e20003],
|
|
332
|
+
["Email", 0x018a0003],
|
|
333
|
+
["Calculator", 0x01920003],
|
|
334
|
+
["File explorer", 0x01940003],
|
|
335
|
+
["Browser home", 0x02230003],
|
|
336
|
+
["Browser refresh", 0x02270003],
|
|
337
|
+
["Browser forward", 0x02250003],
|
|
338
|
+
["Browser back", 0x02240003],
|
|
339
|
+
["Browser search", 0x02210003],
|
|
340
|
+
["Disabled", 0x00000000],
|
|
341
|
+
];
|
|
342
|
+
/** HID keyboard modifier bits, as the vendor's encoder packs them at byte 1. */
|
|
343
|
+
const MODIFIER_CTRL = 0x01;
|
|
344
|
+
const MODIFIER_SHIFT = 0x02;
|
|
345
|
+
const MODIFIER_ALT = 0x04;
|
|
346
|
+
const MODIFIER_GUI = 0x08;
|
|
347
|
+
/**
|
|
348
|
+
* Standard HID keyboard usage codes. Labels are display names, not key-cap
|
|
349
|
+
* legends, so they stay readable in a flat picker.
|
|
350
|
+
*/
|
|
351
|
+
const KEY_USAGES = [
|
|
352
|
+
...Array.from({ length: 26 }, (_, i) => [String.fromCharCode(65 + i), 0x04 + i]),
|
|
353
|
+
...Array.from({ length: 9 }, (_, i) => [String(i + 1), 0x1e + i]),
|
|
354
|
+
["0", 0x27],
|
|
355
|
+
...Array.from({ length: 12 }, (_, i) => [`F${i + 1}`, 0x3a + i]),
|
|
356
|
+
["Enter", 0x28], ["Escape", 0x29], ["Backspace", 0x2a], ["Tab", 0x2b], ["Space", 0x2c],
|
|
357
|
+
["Insert", 0x49], ["Delete", 0x4c], ["Home", 0x4a], ["End", 0x4d],
|
|
358
|
+
["Page Up", 0x4b], ["Page Down", 0x4e],
|
|
359
|
+
["Up", 0x52], ["Down", 0x51], ["Left", 0x50], ["Right", 0x4f],
|
|
360
|
+
["Caps Lock", 0x39], ["Num Lock", 0x53], ["Scroll Lock", 0x47],
|
|
361
|
+
["Print Screen", 0x46], ["Pause", 0x48], ["Context Menu", 0x65],
|
|
362
|
+
["Left Ctrl", 0xe0], ["Left Shift", 0xe1], ["Left Alt", 0xe2], ["Left Windows", 0xe3],
|
|
363
|
+
["Right Ctrl", 0xe4], ["Right Shift", 0xe5], ["Right Alt", 0xe6], ["Right Windows", 0xe7],
|
|
364
|
+
];
|
|
365
|
+
/** Common chords, since the flat picker cannot express "any key + any modifier". */
|
|
366
|
+
const KEY_SHORTCUTS = [
|
|
367
|
+
["Ctrl + A", MODIFIER_CTRL, 0x04], ["Ctrl + C", MODIFIER_CTRL, 0x06],
|
|
368
|
+
["Ctrl + V", MODIFIER_CTRL, 0x19], ["Ctrl + X", MODIFIER_CTRL, 0x1b],
|
|
369
|
+
["Ctrl + Z", MODIFIER_CTRL, 0x1d], ["Ctrl + Y", MODIFIER_CTRL, 0x1c],
|
|
370
|
+
["Ctrl + S", MODIFIER_CTRL, 0x16], ["Ctrl + O", MODIFIER_CTRL, 0x12],
|
|
371
|
+
["Ctrl + N", MODIFIER_CTRL, 0x11], ["Ctrl + T", MODIFIER_CTRL, 0x17],
|
|
372
|
+
["Ctrl + W", MODIFIER_CTRL, 0x1a], ["Ctrl + F", MODIFIER_CTRL, 0x09],
|
|
373
|
+
["Ctrl + Shift + Escape", MODIFIER_CTRL | MODIFIER_SHIFT, 0x29],
|
|
374
|
+
["Alt + Tab", MODIFIER_ALT, 0x2b], ["Alt + F4", MODIFIER_ALT, 0x3d],
|
|
375
|
+
["Alt + Left", MODIFIER_ALT, 0x50], ["Alt + Right", MODIFIER_ALT, 0x4f],
|
|
376
|
+
["Win + D", MODIFIER_GUI, 0x07], ["Win + E", MODIFIER_GUI, 0x08],
|
|
377
|
+
["Win + L", MODIFIER_GUI, 0x0f], ["Win + R", MODIFIER_GUI, 0x15],
|
|
378
|
+
["Win + S", MODIFIER_GUI, 0x16], ["Win + Tab", MODIFIER_GUI, 0x2b],
|
|
379
|
+
];
|
|
380
|
+
/**
|
|
381
|
+
* A keyboard action word, from the vendor's `kf_hw()` keyboard branch:
|
|
382
|
+
*
|
|
383
|
+
* no modifier: (keycode & 255) << 8 | 128
|
|
384
|
+
* with modifier: (keycode & 255) << 16 | (modifiers & 255) << 8
|
|
385
|
+
*
|
|
386
|
+
* The two forms are genuinely different shapes, not one with a zero
|
|
387
|
+
* modifier — an unmodified key sets the `0x80` marker in the low byte and
|
|
388
|
+
* puts the keycode one byte lower than a chord does.
|
|
389
|
+
*/
|
|
390
|
+
export function incottKeyboardActionCode(keycode, modifiers = 0) {
|
|
391
|
+
return modifiers === 0
|
|
392
|
+
? (((keycode & 0xff) << 8) | 0x80) >>> 0
|
|
393
|
+
: (((keycode & 0xff) << 16) | ((modifiers & 0xff) << 8)) >>> 0;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Everything a button can be set to, in display order: the mouse, DPI and
|
|
397
|
+
* media actions above, then individual keys, then common chords.
|
|
398
|
+
*
|
|
399
|
+
* Keyboard bindings are enumerated rather than left out. The encoding is
|
|
400
|
+
* parametric (any of 256 keycodes against any of 256 modifier masks) and the
|
|
401
|
+
* shared `buttonOptions` contract is a flat list of labels, so the full space
|
|
402
|
+
* cannot be offered — but a curated list covers what people actually bind,
|
|
403
|
+
* and it is the same approach the MCHOSE driver in this repo already takes.
|
|
404
|
+
*/
|
|
405
|
+
export const INCOTT_BUTTON_ACTIONS = [
|
|
406
|
+
...MOUSE_AND_MEDIA_ACTIONS,
|
|
407
|
+
...KEY_USAGES.map(([label, usage]) => [label, incottKeyboardActionCode(usage)]),
|
|
408
|
+
...KEY_SHORTCUTS.map(([label, modifiers, usage]) => [label, incottKeyboardActionCode(usage, modifiers)]),
|
|
409
|
+
];
|
|
410
|
+
/** Macro loop modes, in wire order. */
|
|
411
|
+
export const INCOTT_MACRO_LOOP_MODES = ["untilKeyRelease", "untilAnyKey", "cycle"];
|
|
412
|
+
/** A macro buffer is always this long, in ten 32-byte chunks. */
|
|
413
|
+
export const INCOTT_MACRO_BUFFER_BYTES = 320;
|
|
414
|
+
export const INCOTT_MACRO_CHUNK_BYTES = 32;
|
|
415
|
+
export const INCOTT_MACRO_CHUNK_COUNT = INCOTT_MACRO_BUFFER_BYTES / INCOTT_MACRO_CHUNK_BYTES;
|
|
416
|
+
export const INCOTT_MACRO_BUFFER_COUNT = 10;
|
|
417
|
+
/** Steps occupy bytes 4..287 at four bytes each, so 71 fit. */
|
|
418
|
+
export const INCOTT_MACRO_MAX_STEPS = 71;
|
|
419
|
+
/**
|
|
420
|
+
* Builds the 320-byte macro buffer, transcribed from the vendor bundle's
|
|
421
|
+
* `juji_to_hw()`.
|
|
422
|
+
*
|
|
423
|
+
* [0] buffer id
|
|
424
|
+
* [1] loop mode (0 until key release, 1 until any key, 2 cycle)
|
|
425
|
+
* [2..3] cycle count, LE16
|
|
426
|
+
* [4+4n] event flags: bit 0 always set, bit 7 set for a RELEASE
|
|
427
|
+
* [5+4n] HID keyboard usage code
|
|
428
|
+
* [6..7+4n] delay after the event, LE16 milliseconds
|
|
429
|
+
* [288..293] the ASCII name "Macro" followed by '1' + buffer id
|
|
430
|
+
* [304..307] (steps + 1) * 4 + 128, LE32
|
|
431
|
+
* [308..311] 16, 0, 232, 232 — constant in every buffer the vendor builds
|
|
432
|
+
* [312..315] uid, LE32
|
|
433
|
+
* [316..317] steps * 2, LE16
|
|
434
|
+
* [318] step count
|
|
435
|
+
*
|
|
436
|
+
* The buffer goes out as ten 32-byte chunks, each announced by
|
|
437
|
+
* `incottEncodeMacroChunkHeader` and then carried by a 32-byte OUTPUT report
|
|
438
|
+
* on the same report id — the only place this protocol uses an output report
|
|
439
|
+
* at all. That transport is not recoverable from the vendor bundle (the call
|
|
440
|
+
* carrying each chunk is defined in none of the files its page loads, and the
|
|
441
|
+
* HID method names resolve through variables at runtime), so it was captured
|
|
442
|
+
* from the running tool instead: see
|
|
443
|
+
* `captures/incott-8k-wireless/macro-upload-2026-09-11.hex`, and
|
|
444
|
+
* `IncottHidClient.uploadMacro` for the sender.
|
|
445
|
+
*
|
|
446
|
+
* Pinned byte-for-byte to that capture. There is no macro READ command, so
|
|
447
|
+
* nothing here can be verified against the device after the fact.
|
|
448
|
+
*/
|
|
449
|
+
export function incottEncodeMacroBuffer(macro) {
|
|
450
|
+
if (!Number.isInteger(macro.bufferId) || macro.bufferId < 0 || macro.bufferId >= INCOTT_MACRO_BUFFER_COUNT) {
|
|
451
|
+
throw new RangeError(`Macro buffer id out of range: ${macro.bufferId}`);
|
|
452
|
+
}
|
|
453
|
+
if (macro.steps.length > INCOTT_MACRO_MAX_STEPS) {
|
|
454
|
+
throw new RangeError(`Macro has ${macro.steps.length} steps; the buffer holds ${INCOTT_MACRO_MAX_STEPS}`);
|
|
455
|
+
}
|
|
456
|
+
const out = new Uint8Array(INCOTT_MACRO_BUFFER_BYTES);
|
|
457
|
+
out[0] = macro.bufferId & 0xff;
|
|
458
|
+
out[1] = INCOTT_MACRO_LOOP_MODES.indexOf(macro.loop);
|
|
459
|
+
out[2] = macro.cycles & 0xff;
|
|
460
|
+
out[3] = (macro.cycles >> 8) & 0xff;
|
|
461
|
+
macro.steps.forEach((step, index) => {
|
|
462
|
+
const at = 4 + index * 4;
|
|
463
|
+
// Bit 0 is set on every event; bit 7 marks a release. A press is 0x01.
|
|
464
|
+
out[at] = (step.press ? 0x00 : 0x80) | 0x01;
|
|
465
|
+
out[at + 1] = step.key & 0xff;
|
|
466
|
+
out[at + 2] = step.delayMs & 0xff;
|
|
467
|
+
out[at + 3] = (step.delayMs >> 8) & 0xff;
|
|
468
|
+
});
|
|
469
|
+
// "Macro" + the 1-based buffer number, as the vendor names its slots.
|
|
470
|
+
out.set([0x4d, 0x61, 0x63, 0x72, 0x6f], 288);
|
|
471
|
+
out[293] = 0x31 + macro.bufferId;
|
|
472
|
+
// (steps + 1) * 4 + 128. An earlier transcription of this read
|
|
473
|
+
// `(steps + 1) * 132`, because the deobfuscation pass used to recover the
|
|
474
|
+
// vendor's source folded the constant `4 + 128` into `132` before anyone
|
|
475
|
+
// read it. Pinned to a real captured buffer now (2026-09-11).
|
|
476
|
+
const size = (macro.steps.length + 1) * 4 + 128;
|
|
477
|
+
out[304] = size & 0xff;
|
|
478
|
+
out[305] = (size >> 8) & 0xff;
|
|
479
|
+
out[306] = (size >> 16) & 0xff;
|
|
480
|
+
out[307] = (size >> 24) & 0xff;
|
|
481
|
+
out[308] = 16;
|
|
482
|
+
out[310] = 232;
|
|
483
|
+
out[311] = 232;
|
|
484
|
+
out[312] = macro.uid & 0xff;
|
|
485
|
+
out[313] = (macro.uid >>> 8) & 0xff;
|
|
486
|
+
out[314] = (macro.uid >>> 16) & 0xff;
|
|
487
|
+
out[315] = (macro.uid >>> 24) & 0xff;
|
|
488
|
+
const len = macro.steps.length * 2;
|
|
489
|
+
out[316] = len & 0xff;
|
|
490
|
+
out[317] = (len >> 8) & 0xff;
|
|
491
|
+
out[318] = macro.steps.length;
|
|
492
|
+
return out;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* The 8-byte header announcing one chunk of a macro buffer:
|
|
496
|
+
* `09 07 0a <chunk index> 20 <buffer id>`.
|
|
497
|
+
*
|
|
498
|
+
* See `incottEncodeMacroBuffer` for why nothing sends this yet. Note the
|
|
499
|
+
* vendor slices its own payload as `mda.slice(i * 32, i * 64)`, which yields
|
|
500
|
+
* an EMPTY chunk for `i = 0` and over-long ones after — visibly a bug in its
|
|
501
|
+
* own uploader, and not reproduced here.
|
|
502
|
+
*/
|
|
503
|
+
export function incottEncodeMacroChunkHeader(chunkIndex, bufferId) {
|
|
504
|
+
if (!Number.isInteger(chunkIndex) || chunkIndex < 0 || chunkIndex >= INCOTT_MACRO_CHUNK_COUNT) {
|
|
505
|
+
throw new RangeError(`Macro chunk index out of range: ${chunkIndex}`);
|
|
506
|
+
}
|
|
507
|
+
if (!Number.isInteger(bufferId) || bufferId < 0 || bufferId >= INCOTT_MACRO_BUFFER_COUNT) {
|
|
508
|
+
throw new RangeError(`Macro buffer id out of range: ${bufferId}`);
|
|
509
|
+
}
|
|
510
|
+
return payload(INCOTT_CMD_MACRO_CHUNK, INCOTT_MACRO_CHUNK_COUNT, chunkIndex, INCOTT_MACRO_CHUNK_BYTES, bufferId);
|
|
511
|
+
}
|
|
512
|
+
/** Splits a macro buffer into the ten chunks the upload sends. */
|
|
513
|
+
export function incottMacroChunks(buffer) {
|
|
514
|
+
if (buffer.length !== INCOTT_MACRO_BUFFER_BYTES) {
|
|
515
|
+
throw new RangeError(`Macro buffer must be ${INCOTT_MACRO_BUFFER_BYTES} bytes, got ${buffer.length}`);
|
|
516
|
+
}
|
|
517
|
+
return Array.from({ length: INCOTT_MACRO_CHUNK_COUNT }, (_, index) => buffer.slice(index * INCOTT_MACRO_CHUNK_BYTES, (index + 1) * INCOTT_MACRO_CHUNK_BYTES));
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Label for a 32-bit action word, or null when it is not one this driver
|
|
521
|
+
* knows.
|
|
522
|
+
*
|
|
523
|
+
* Macro bindings are named rather than listed: the vendor encodes them as
|
|
524
|
+
* `slot << 16 | 9` (confirmed 2026-09-11, where binding a button to macro
|
|
525
|
+
* slot 3 wrote `0x00030009`), so a label can be derived for any slot without
|
|
526
|
+
* putting ten entries in the picker. `incottButtonActionCode` deliberately
|
|
527
|
+
* does NOT reverse these — nothing can assign a macro until there is a UI to
|
|
528
|
+
* author one — so a macro binding reads back correctly and is left alone.
|
|
529
|
+
*/
|
|
530
|
+
export function incottButtonActionLabel(code) {
|
|
531
|
+
for (const [label, value] of INCOTT_BUTTON_ACTIONS) {
|
|
532
|
+
if (value === code)
|
|
533
|
+
return label;
|
|
534
|
+
}
|
|
535
|
+
if ((code & 0xffff) === INCOTT_BUTTON_MACRO_MARKER) {
|
|
536
|
+
const slot = code >>> 16;
|
|
537
|
+
if (slot < INCOTT_MACRO_BUFFER_COUNT)
|
|
538
|
+
return `Macro ${slot + 1}`;
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
/** Low half of a macro button binding: `slot << 16 | 9`. */
|
|
543
|
+
export const INCOTT_BUTTON_MACRO_MARKER = 0x0009;
|
|
544
|
+
/** 32-bit action word for a label, or null when the label is not in the table. */
|
|
545
|
+
export function incottButtonActionCode(label) {
|
|
546
|
+
for (const [name, value] of INCOTT_BUTTON_ACTIONS) {
|
|
547
|
+
if (name === label)
|
|
548
|
+
return value;
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* How many DPI stages the cycle uses by default — and the value this driver
|
|
554
|
+
* spent two sessions mistaking for a sub-command.
|
|
555
|
+
*
|
|
556
|
+
* `0x83`'s reply byte 2 is the stage COUNT, not an echo. Proven by the
|
|
557
|
+
* captures: the vendor sends `09 83 00` and gets `09 83 06 01` back, and a
|
|
558
|
+
* bare `09 83` sweep with no sub-command gets the same `06` — a byte the
|
|
559
|
+
* request never contained cannot be an echo. Byte 3 is the active index,
|
|
560
|
+
* which varies (`00`/`01`/`03`/`05`) while byte 2 stays `06`.
|
|
561
|
+
*
|
|
562
|
+
* This matters twice over:
|
|
563
|
+
* - `0x83` must NOT be treated as sub-echoing when matching responses. It
|
|
564
|
+
* belongs with `0x81`/`0x88`/`0x89`/`0x8f`, where byte 2 is data.
|
|
565
|
+
* - the `0x03` write carries the count alongside the index
|
|
566
|
+
* (`09 03 <count> <stage>`), so writing a hardcoded `06` while selecting
|
|
567
|
+
* a stage would reset a four-stage cycle back to six — the same shape of
|
|
568
|
+
* bug as the old `INCOTT_SUB_SET_DPI` constant below.
|
|
569
|
+
*/
|
|
570
|
+
export const INCOTT_DPI_STAGE_COUNT_DEFAULT = 6;
|
|
571
|
+
/**
|
|
572
|
+
* Number of DPI stages the table holds. Both the `0x02` write and the `0x82`
|
|
573
|
+
* read take a stage index in this range as their second payload byte — see
|
|
574
|
+
* `incottEncodeSetDpi` and `incottDecodeDpiStage`.
|
|
575
|
+
*
|
|
576
|
+
* THIS WAS WRONG until 2026-09-08: the driver used to hardcode that byte as a
|
|
577
|
+
* constant `INCOTT_SUB_SET_DPI = 0x01`, which could only ever write stage 1
|
|
578
|
+
* of the table. Reading stages 0-5 on real hardware returned six independent
|
|
579
|
+
* points on the DPI line (wire 0x07/0x0f/0x1f/0x2f/0x3f/0x7f ->
|
|
580
|
+
* 400/800/1600/2400/3200/6400), proving the byte is a stage index, not a
|
|
581
|
+
* fixed sub-command.
|
|
582
|
+
*/
|
|
583
|
+
export const INCOTT_DPI_STAGE_COUNT = 6;
|
|
584
|
+
export const INCOTT_SUB_LOD = 0x01;
|
|
585
|
+
export const INCOTT_SUB_RIPPLE = 0x02;
|
|
586
|
+
export const INCOTT_SUB_ANGLE_SNAP = 0x03;
|
|
587
|
+
export const INCOTT_SUB_MOTION_SYNC = 0x04;
|
|
588
|
+
/**
|
|
589
|
+
* Sub-command for the "Performance mode" sensor setting (labelled HP / Corded
|
|
590
|
+
* / LP in the vendor tool, described as trading performance for battery
|
|
591
|
+
* life). The value-to-label mapping is now CONFIRMED: captured 2026-09-10 by
|
|
592
|
+
* instrumenting Incott's own WebHID configurator with each click labelled
|
|
593
|
+
* (unlike the 2026-09-07 capture, which only recorded the raw writes):
|
|
594
|
+
*
|
|
595
|
+
* clicked "HP" -> TX 09 04 05 02
|
|
596
|
+
* clicked "Corded" -> TX 09 04 05 01
|
|
597
|
+
* clicked "LP" -> TX 09 04 05 00
|
|
598
|
+
*
|
|
599
|
+
* i.e. HP=2, Corded=1, LP=0. **This is the REVERSE of the vendor UI's
|
|
600
|
+
* left-to-right display order (HP | Corded | LP)** — exactly why this was
|
|
601
|
+
* captured with each click labelled rather than assumed from the on-screen
|
|
602
|
+
* order. See `INCOTT_PERFORMANCE_MODE_TO_WIRE`/`INCOTT_PERFORMANCE_MODE_FROM_WIRE`
|
|
603
|
+
* for the single named table this reversal lives in, and
|
|
604
|
+
* `incottEncodeSetPerformanceMode`.
|
|
605
|
+
*/
|
|
606
|
+
export const INCOTT_SUB_PERFORMANCE = 0x05;
|
|
607
|
+
export const INCOTT_SUB_DEBOUNCE = 0x01;
|
|
608
|
+
/**
|
|
609
|
+
* Fire Key (rapid-fire) parameters, `09 05 02 <times> <interval ms>`.
|
|
610
|
+
*
|
|
611
|
+
* The last unidentified command in this protocol, settled 2026-09-11. The
|
|
612
|
+
* vendor calls it `setFKeyPm(lp, ir)` and only ever calls it for a button
|
|
613
|
+
* bound to `favFIRE`, deriving both values from that button's `itemdata` and
|
|
614
|
+
* clamping them to 3 and 255 respectively. Its own UI names the two fields:
|
|
615
|
+
* "Fire Key" / "Keep left-clicking according to the interval and times".
|
|
616
|
+
*
|
|
617
|
+
* Read back at `0x85`/`0x02`. On this hardware: `09 85 02 03 0a` — three
|
|
618
|
+
* clicks, 10 ms apart.
|
|
619
|
+
*/
|
|
620
|
+
export const INCOTT_SUB_FIRE_KEY = 0x02;
|
|
621
|
+
/**
|
|
622
|
+
* Clicks per press, 1-3 — the ceiling the vendor clamps to.
|
|
623
|
+
*
|
|
624
|
+
* `INCOTT_FIRE_KEY_TIMES_HOLD` (0) is a real fourth setting, not an absence
|
|
625
|
+
* of one: it switches the button from a fixed burst to firing continuously
|
|
626
|
+
* while held. Confirmed against the vendor software 2026-09-11 by the device
|
|
627
|
+
* owner, which is the only way it could have been established — the value is
|
|
628
|
+
* in range for the write either way, so a round-trip proves nothing about
|
|
629
|
+
* what it MEANS.
|
|
630
|
+
*/
|
|
631
|
+
export const INCOTT_FIRE_KEY_MAX_TIMES = 3;
|
|
632
|
+
/**
|
|
633
|
+
* Fire key "times" value that means hold-to-fire: the button keeps clicking
|
|
634
|
+
* at the configured interval for as long as it is held, and stops on
|
|
635
|
+
* release. See `INCOTT_FIRE_KEY_MAX_TIMES`.
|
|
636
|
+
*/
|
|
637
|
+
export const INCOTT_FIRE_KEY_TIMES_HOLD = 0;
|
|
638
|
+
/** Milliseconds between clicks in a burst, one byte. */
|
|
639
|
+
export const INCOTT_FIRE_KEY_MAX_INTERVAL_MS = 255;
|
|
640
|
+
export const INCOTT_SUB_SLEEP = 0x03;
|
|
641
|
+
export const INCOTT_SUB_NONE = 0x00;
|
|
642
|
+
/**
|
|
643
|
+
* DPI lives in a six-stage table, each stage a linear value — not an index
|
|
644
|
+
* into a preset table. The wire value is little-endian at payload bytes 2-3
|
|
645
|
+
* (write) / response bytes 3-4 (read):
|
|
646
|
+
* wire = dpi / 50 - 1 dpi = (wire + 1) * 50
|
|
647
|
+
*
|
|
648
|
+
* Verified on hardware 2026-09-08 by reading all six stages back as
|
|
649
|
+
* 400/800/1600/2400/3200/6400 (wire 0x07/0x0f/0x1f/0x2f/0x3f/0x7f) — six
|
|
650
|
+
* independent points on this line — then writing 25000 to stage 2 (wire 499)
|
|
651
|
+
* and reading back exactly 25000, then restoring 1600 and reading that back
|
|
652
|
+
* too. (An earlier, narrower proof from 2026-09-07 against a G23V2Pro only
|
|
653
|
+
* ever exercised stage 1: `TX 09 02 01 0f 00` -> 800 DPI, `TX 09 02 01 f3 01`
|
|
654
|
+
* -> 25000 DPI.) See `incottEncodeSetDpi` and `incottDecodeDpiStage`.
|
|
655
|
+
*
|
|
656
|
+
* The vendor's own device definition (js/gvarG23-v102.js) pairs two PixArt
|
|
657
|
+
* sensor variants with different ceilings, both counting up from 50 in steps
|
|
658
|
+
* of 50:
|
|
659
|
+
* sensor 0x3395 (PAW3395): 32000
|
|
660
|
+
* sensor 0x3950 (PAW3950): 45000
|
|
661
|
+
* The fitted sensor IS readable — the identity reply carries it (see
|
|
662
|
+
* `incottDecodeIdentity`) — so `readStatus` narrows the ceiling it OFFERS to
|
|
663
|
+
* whichever sensor answered, via `incottDpiMaxForSensor`. This constant stays
|
|
664
|
+
* the higher of the two: it is the bound on what the protocol can express,
|
|
665
|
+
* and the encoder must keep accepting a value the app is entitled to send.
|
|
666
|
+
* That mirrors the polling rate, where `supportedPollingRates` narrows by
|
|
667
|
+
* connection while `incottEncodeSetPollingRate` still accepts the full
|
|
668
|
+
* ladder.
|
|
669
|
+
*/
|
|
670
|
+
export const INCOTT_DPI_MIN = 50;
|
|
671
|
+
export const INCOTT_DPI_MAX = 45000;
|
|
672
|
+
/** The PAW3395's ceiling in the vendor's own DPI table. */
|
|
673
|
+
export const INCOTT_DPI_MAX_PAW3395 = 32000;
|
|
674
|
+
/**
|
|
675
|
+
* The DPI ceiling to OFFER for a fitted sensor, matching the table the vendor
|
|
676
|
+
* builds in `getStDPI`.
|
|
677
|
+
*
|
|
678
|
+
* Worth being precise about what this is and is not. It is the vendor's UI
|
|
679
|
+
* limit, not a proven firmware limit: this contributor's PAW3950 accepted
|
|
680
|
+
* 45000 over the cable even though the connection-indexed sensor byte read
|
|
681
|
+
* PAW3395 there (see `captures/incott-8k-wireless/wired-dpi-ceiling-2026-09-11.hex`),
|
|
682
|
+
* so a real PAW3395 unit has never been tested and may well accept more. It
|
|
683
|
+
* is used because offering exactly what the vendor offers cannot surprise
|
|
684
|
+
* anyone, and because the alternative — advertising 45000 to a Ghero — risks
|
|
685
|
+
* a silent refusal on a model nobody here can test.
|
|
686
|
+
*/
|
|
687
|
+
export function incottDpiMaxForSensor(sensorId) {
|
|
688
|
+
return sensorId === INCOTT_SENSOR_PAW3395 ? INCOTT_DPI_MAX_PAW3395 : INCOTT_DPI_MAX;
|
|
689
|
+
}
|
|
690
|
+
export const INCOTT_DPI_STEP = 50;
|
|
691
|
+
/**
|
|
692
|
+
* NOT the writable DPI range (see `INCOTT_DPI_MIN`/`INCOTT_DPI_MAX` above for
|
|
693
|
+
* that) — the vendor's device definition calls this list the six DEFAULT
|
|
694
|
+
* STAGE PRESETS. `0x83`/`0x06` reports which of these six stages is active as
|
|
695
|
+
* an index 0-5 (see `incottDecodeDpiStageIndex`), not a DPI value. Kept as a
|
|
696
|
+
* convenience list of round numbers; do not use it to validate or decode a
|
|
697
|
+
* DPI write.
|
|
698
|
+
*/
|
|
699
|
+
export const INCOTT_DPI_DEFAULT_STAGE_PRESETS = [400, 800, 1600, 2400, 3200, 6400];
|
|
700
|
+
/**
|
|
701
|
+
* The full polling-rate ladder, available only over the 2.4 GHz wireless
|
|
702
|
+
* connection (`INCOTT_PRODUCT_ID`, `0x522C`). See
|
|
703
|
+
* `INCOTT_POLLING_STEPS_HZ_WIRED` for the lower ceiling the owner confirmed
|
|
704
|
+
* on hardware when the mouse is plugged in.
|
|
705
|
+
*/
|
|
706
|
+
export const INCOTT_POLLING_STEPS_HZ = [125, 250, 500, 1000, 2000, 4000, 8000];
|
|
707
|
+
/**
|
|
708
|
+
* Wired ceiling — HARDWARE-VERIFIED: the owner confirmed the mouse only
|
|
709
|
+
* reaches 1000 Hz over the USB cable (`INCOTT_PRODUCT_ID_WIRED`, `0x622C`);
|
|
710
|
+
* 2000/4000/8000 Hz are wireless-only. Offering one of those over the cable
|
|
711
|
+
* would produce a write the device silently refuses, which
|
|
712
|
+
* `IncottHidClient.setPollingRate`'s read-back verification would then
|
|
713
|
+
* report as a failure on every attempt — so `readStatus()` publishes this
|
|
714
|
+
* narrower list instead of the full one whenever `incottIsWiredProduct` is
|
|
715
|
+
* true. See `docs/incott-testing.md`.
|
|
716
|
+
*/
|
|
717
|
+
export const INCOTT_POLLING_STEPS_HZ_WIRED = [125, 250, 500, 1000];
|
|
718
|
+
export const INCOTT_POLLING_WIRE_TO_HZ = {
|
|
719
|
+
0: 1000, 1: 500, 2: 250, 3: 125, 4: 8000, 5: 4000, 6: 2000,
|
|
720
|
+
};
|
|
721
|
+
export const INCOTT_POLLING_HZ_TO_WIRE = {
|
|
722
|
+
1000: 0, 500: 1, 250: 2, 125: 3, 8000: 4, 4000: 5, 2000: 6,
|
|
723
|
+
};
|
|
724
|
+
/**
|
|
725
|
+
* Lift-off distance is carried in tenths of a millimetre.
|
|
726
|
+
*
|
|
727
|
+
* VERIFIED ON HARDWARE 2026-09-08. Setting 0.7 mm in Incott's own web
|
|
728
|
+
* configurator and then reading `0x84`/sub `0x01` returned byte 3 = `2`,
|
|
729
|
+
* so hardware 2 = 0.7 mm. That confirms IncottHIDApp's mapping, which is
|
|
730
|
+
* what `INCOTT_LOD_WIRE_TO_TENTHS` below encodes.
|
|
731
|
+
*
|
|
732
|
+
* It also disproves a reading of the vendor's own device definition
|
|
733
|
+
* (js/gvarG23-v102.js), which pairs `lodUI = [0.7, 1, 2]` with
|
|
734
|
+
* `lodHW = [0, 1, 2]`. Those two arrays are NOT index-aligned — taking
|
|
735
|
+
* them as a pair implies hardware 0 = 0.7 mm, which the device
|
|
736
|
+
* contradicts. Do not 'fix' this mapping from that file.
|
|
737
|
+
*
|
|
738
|
+
* Remaining nuance: only the 0.7 mm point was read directly. Hardware 0
|
|
739
|
+
* and 1 are 1 mm and 2 mm in that order per IncottHIDApp; since the
|
|
740
|
+
* mapping is a bijection over {0,1,2} and its 0.7 mm claim proved correct,
|
|
741
|
+
* the other two follow, but neither has been read back individually.
|
|
742
|
+
*/
|
|
743
|
+
export const INCOTT_LOD_STEPS_TENTHS = [7, 10, 20];
|
|
744
|
+
export const INCOTT_LOD_WIRE_TO_TENTHS = { 0: 10, 1: 20, 2: 7 };
|
|
745
|
+
export const INCOTT_LOD_TENTHS_TO_WIRE = { 10: 0, 20: 1, 7: 2 };
|
|
746
|
+
export const INCOTT_DEBOUNCE_MIN_MS = 0;
|
|
747
|
+
export const INCOTT_DEBOUNCE_MAX_MS = 30;
|
|
748
|
+
export const INCOTT_SLEEP_MIN_S = 1;
|
|
749
|
+
export const INCOTT_SLEEP_MAX_S = 900;
|
|
750
|
+
/** All three raw wire values (LP/Corded/HP) are now hardware-confirmed — see `INCOTT_SUB_PERFORMANCE`. */
|
|
751
|
+
export const INCOTT_PERFORMANCE_MODE_MIN = 0;
|
|
752
|
+
export const INCOTT_PERFORMANCE_MODE_MAX = 2;
|
|
753
|
+
/**
|
|
754
|
+
* The single named table the HP/Corded/LP value-to-label reversal lives in —
|
|
755
|
+
* see `INCOTT_SUB_PERFORMANCE` for the capture that confirmed it. Keys are the
|
|
756
|
+
* vendor tool's own display labels; `INCOTT_PERFORMANCE_MODE_NAMES` lists them
|
|
757
|
+
* in the vendor UI's own left-to-right order (HP, Corded, LP) for advertising
|
|
758
|
+
* to the app, while this table (and its inverse,
|
|
759
|
+
* `INCOTT_PERFORMANCE_MODE_FROM_WIRE`) hold the REVERSED wire values.
|
|
760
|
+
* `incottPerformanceModeToWire`/`incottPerformanceModeFromWire` are the
|
|
761
|
+
* intended entry points; the raw tables are exported for tests.
|
|
762
|
+
*/
|
|
763
|
+
export const INCOTT_PERFORMANCE_MODE_NAMES = ["HP", "Corded", "LP"];
|
|
764
|
+
/** name -> raw wire value. See `INCOTT_PERFORMANCE_MODE_NAMES`'s doc comment for the reversal warning. */
|
|
765
|
+
export const INCOTT_PERFORMANCE_MODE_TO_WIRE = {
|
|
766
|
+
HP: 2,
|
|
767
|
+
Corded: 1,
|
|
768
|
+
LP: 0,
|
|
769
|
+
};
|
|
770
|
+
/** raw wire value -> name. The inverse of `INCOTT_PERFORMANCE_MODE_TO_WIRE`. */
|
|
771
|
+
export const INCOTT_PERFORMANCE_MODE_FROM_WIRE = {
|
|
772
|
+
2: "HP",
|
|
773
|
+
1: "Corded",
|
|
774
|
+
0: "LP",
|
|
775
|
+
};
|
|
776
|
+
/** Validated name -> wire lookup for `incottEncodeSetPerformanceMode`/`IncottHidClient.setPowerMode`. Returns `null` for an unknown name rather than throwing, so callers can reject before writing anything. */
|
|
777
|
+
export function incottPerformanceModeToWire(name) {
|
|
778
|
+
return INCOTT_PERFORMANCE_MODE_TO_WIRE[name] ?? null;
|
|
779
|
+
}
|
|
780
|
+
/** Wire -> validated name lookup, the inverse of `incottPerformanceModeToWire`. Returns `null` for a value outside 0-2. */
|
|
781
|
+
export function incottPerformanceModeFromWire(wire) {
|
|
782
|
+
return INCOTT_PERFORMANCE_MODE_FROM_WIRE[wire] ?? null;
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* A curated subset of the verified 1-900s sleep-timer range to offer in the
|
|
786
|
+
* app's dropdown, in the same style as GEARHUB_SLEEP_OPTIONS and
|
|
787
|
+
* MCHOSE_SLEEP_OPTIONS: the firmware accepts any integer second count in
|
|
788
|
+
* range (see `incottEncodeSetSleep`), this is just a sane list of presets.
|
|
789
|
+
* 900s (15 minutes) is the documented maximum.
|
|
790
|
+
*/
|
|
791
|
+
export const INCOTT_SLEEP_OPTIONS = [10, 30, 60, 120, 300, 600, 900];
|
|
792
|
+
export const INCOTT_RECEIVER_LED_MODES = [
|
|
793
|
+
"Connect & polling rate",
|
|
794
|
+
"Battery status",
|
|
795
|
+
"Battery warning",
|
|
796
|
+
];
|
|
797
|
+
export const INCOTT_TOGGLE_SUB = {
|
|
798
|
+
motionSync: INCOTT_SUB_MOTION_SYNC,
|
|
799
|
+
angleSnapping: INCOTT_SUB_ANGLE_SNAP,
|
|
800
|
+
rippleControl: INCOTT_SUB_RIPPLE,
|
|
801
|
+
};
|
|
802
|
+
/**
|
|
803
|
+
* Builds a request payload. The report ID is deliberately absent: WebHID's
|
|
804
|
+
* sendFeatureReport takes it as a separate argument.
|
|
805
|
+
*/
|
|
806
|
+
function payload(...values) {
|
|
807
|
+
const out = new Uint8Array(INCOTT_PAYLOAD_LENGTH);
|
|
808
|
+
values.forEach((value, index) => {
|
|
809
|
+
out[index] = value & 0xff;
|
|
810
|
+
});
|
|
811
|
+
return out;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Throws `RangeError` unless `dpi` is a value the six-stage table can hold.
|
|
815
|
+
* Split out from `incottEncodeSetDpi` so `IncottHidClient.setDpi` can reject
|
|
816
|
+
* an invalid value before touching the device — determining which stage is
|
|
817
|
+
* active requires a query, and an obviously-invalid DPI should never cost a
|
|
818
|
+
* round-trip.
|
|
819
|
+
*/
|
|
820
|
+
export function incottValidateDpi(dpi) {
|
|
821
|
+
if (!Number.isInteger(dpi) ||
|
|
822
|
+
dpi < INCOTT_DPI_MIN ||
|
|
823
|
+
dpi > INCOTT_DPI_MAX ||
|
|
824
|
+
dpi % INCOTT_DPI_STEP !== 0) {
|
|
825
|
+
throw new RangeError(`DPI out of range: ${dpi}`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* EDITS the DPI value stored in one stage — distinct from
|
|
830
|
+
* `incottEncodeSetActiveDpiStage`, which only SELECTS which stage is active
|
|
831
|
+
* and never touches a stored value. `stage` is a table index 0-5, NOT a
|
|
832
|
+
* sub-command — see `INCOTT_DPI_STAGE_COUNT` for the hardware proof that this
|
|
833
|
+
* byte varies (the driver used to hardcode it as a constant `0x01`, which
|
|
834
|
+
* could only ever reach stage 1). The value itself is a plain little-endian
|
|
835
|
+
* uint16 "wire" value — see the comment on `INCOTT_DPI_MIN` for the
|
|
836
|
+
* conversion and the captured proof.
|
|
837
|
+
*
|
|
838
|
+
* PAYLOAD INDEX 7 IS AN AXIS BYTE, discovered 2026-09-10: the full write is
|
|
839
|
+
* `02 <stage> <lo> <hi> 00 00 00 <axis>`, where `axis` 0 = both axes, 1 = X
|
|
840
|
+
* only, 2 = Y only. This encoder always emits trailing zeros (see `payload`),
|
|
841
|
+
* and the `axis` argument selects it. Independent X/Y IS implemented and
|
|
842
|
+
* hardware-verified — the matching per-axis read is `incottEncodeQueryDpiAxis`,
|
|
843
|
+
* which an earlier probe concluded did not exist because X and Y happened to
|
|
844
|
+
* be equal at the time.
|
|
845
|
+
*/
|
|
846
|
+
/**
|
|
847
|
+
* Which axis a DPI write targets, and which one a read asks for.
|
|
848
|
+
*
|
|
849
|
+
* On the WRITE the value rides at payload byte 7; on the READ it is request
|
|
850
|
+
* byte 2 and the reply echoes it back at byte 8. Hardware-verified
|
|
851
|
+
* 2026-09-11: writing X=800/Y=1600, X=2400/Y=400 and X=1000/Y=1000 to one
|
|
852
|
+
* stage read back exactly, each axis independently.
|
|
853
|
+
*
|
|
854
|
+
* `both` is what a plain `incottEncodeSetDpi` sends, and what reading with no
|
|
855
|
+
* axis byte returns.
|
|
856
|
+
*/
|
|
857
|
+
export const INCOTT_DPI_AXIS = { both: 0, x: 1, y: 2 };
|
|
858
|
+
export function incottEncodeSetDpi(stage, dpi, axis = "both") {
|
|
859
|
+
if (!Number.isInteger(stage) || stage < 0 || stage >= INCOTT_DPI_STAGE_COUNT) {
|
|
860
|
+
throw new RangeError(`DPI stage out of range: ${stage}`);
|
|
861
|
+
}
|
|
862
|
+
incottValidateDpi(dpi);
|
|
863
|
+
const wire = dpi / INCOTT_DPI_STEP - 1;
|
|
864
|
+
return payload(INCOTT_CMD_SET_DPI, stage, wire & 0xff, (wire >> 8) & 0xff, 0, 0, 0, INCOTT_DPI_AXIS[axis]);
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* `09 82 <stage> <axis>` — reads one axis of one stage.
|
|
868
|
+
*
|
|
869
|
+
* WHY THIS EXISTS, given a plain `09 82 <stage>` already reads a value: this
|
|
870
|
+
* driver previously recorded that no per-axis read existed, on the strength
|
|
871
|
+
* of a probe where all three axis values came back identical. They were
|
|
872
|
+
* identical because X and Y were BOTH at the factory 1600 at the time — the
|
|
873
|
+
* probe could not tell "no per-axis read" from "per-axis read whose axes
|
|
874
|
+
* happen to match". Confirmed 2026-09-11 by setting them apart first.
|
|
875
|
+
*/
|
|
876
|
+
export function incottEncodeQueryDpiAxis(stage, axis) {
|
|
877
|
+
if (!Number.isInteger(stage) || stage < 0 || stage >= INCOTT_DPI_STAGE_COUNT) {
|
|
878
|
+
throw new RangeError(`DPI stage out of range: ${stage}`);
|
|
879
|
+
}
|
|
880
|
+
return payload(INCOTT_CMD_QUERY_DPI_STAGE_VALUE, stage, INCOTT_DPI_AXIS[axis]);
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Writes the DPI cycle: `09 03 <count> <stage>` — how many stages the cycle
|
|
884
|
+
* uses, and which one is active. Does NOT write a DPI value and does NOT
|
|
885
|
+
* alter any stage's stored value; see `INCOTT_CMD_SET_DPI_STAGE` for the
|
|
886
|
+
* hardware proof (select 0/3/5/1, table unchanged) and for why this is a
|
|
887
|
+
* distinct operation from `incottEncodeSetDpi`, which edits a stage's stored
|
|
888
|
+
* value.
|
|
889
|
+
*
|
|
890
|
+
* `count` IS REQUIRED, and callers must pass what the device currently
|
|
891
|
+
* reports rather than a constant — the byte used to be hardcoded `0x06` in
|
|
892
|
+
* the belief that it was a sub-command, which would silently reset a
|
|
893
|
+
* four-stage cycle to six every time a stage was selected. See
|
|
894
|
+
* `INCOTT_DPI_STAGE_COUNT_DEFAULT`.
|
|
895
|
+
*/
|
|
896
|
+
export function incottEncodeSetDpiCycle(count, stage) {
|
|
897
|
+
if (!Number.isInteger(count) || count < 1 || count > INCOTT_DPI_STAGE_COUNT) {
|
|
898
|
+
throw new RangeError(`DPI stage count out of range: ${count}`);
|
|
899
|
+
}
|
|
900
|
+
if (!Number.isInteger(stage) || stage < 0 || stage >= count) {
|
|
901
|
+
throw new RangeError(`DPI stage ${stage} out of range for a ${count}-stage cycle`);
|
|
902
|
+
}
|
|
903
|
+
return payload(INCOTT_CMD_SET_DPI_STAGE, count, stage);
|
|
904
|
+
}
|
|
905
|
+
export function incottEncodeSetPollingRate(hz) {
|
|
906
|
+
const wire = INCOTT_POLLING_HZ_TO_WIRE[hz];
|
|
907
|
+
if (wire === undefined)
|
|
908
|
+
throw new RangeError(`Unsupported polling rate: ${hz} Hz`);
|
|
909
|
+
// No sub-command: the wire value occupies the sub-command slot.
|
|
910
|
+
return payload(INCOTT_CMD_SET_POLLING, wire);
|
|
911
|
+
}
|
|
912
|
+
export function incottEncodeSetLiftOff(tenthsMm) {
|
|
913
|
+
const wire = INCOTT_LOD_TENTHS_TO_WIRE[tenthsMm];
|
|
914
|
+
if (wire === undefined)
|
|
915
|
+
throw new RangeError(`Unsupported lift-off distance: ${tenthsMm}`);
|
|
916
|
+
return payload(INCOTT_CMD_SET_SENSOR, INCOTT_SUB_LOD, wire);
|
|
917
|
+
}
|
|
918
|
+
export function incottEncodeSetToggle(kind, on) {
|
|
919
|
+
return payload(INCOTT_CMD_SET_SENSOR, INCOTT_TOGGLE_SUB[kind], on ? 0x01 : 0x00);
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Encodes a button binding write, `09 06 <button 0..5> <32-bit action, LE>`.
|
|
923
|
+
*
|
|
924
|
+
* The action is a 32-bit little-endian word — see `INCOTT_BUTTON_ACTIONS`
|
|
925
|
+
* for the labelled codes. Confirmed against hardware: this unit's factory
|
|
926
|
+
* binding for the DPI button reads back `07 00 03`, and the vendor's own
|
|
927
|
+
* encoder returns `0x00030007` for that function, which is the same word.
|
|
928
|
+
*
|
|
929
|
+
* `button` is the WIRE index, which is not the physical left-to-right order
|
|
930
|
+
* — see `INCOTT_BUTTON_WIRE_INDEX`.
|
|
931
|
+
*/
|
|
932
|
+
export function incottEncodeSetButtonBinding(button, code) {
|
|
933
|
+
if (!Number.isInteger(button) || button < 0 || button >= INCOTT_BUTTON_COUNT) {
|
|
934
|
+
throw new RangeError(`Button index out of range: ${button}`);
|
|
935
|
+
}
|
|
936
|
+
if (!Number.isInteger(code) || code < 0 || code > 0xffffffff) {
|
|
937
|
+
throw new RangeError(`Button action code out of range: ${code}`);
|
|
938
|
+
}
|
|
939
|
+
return payload(INCOTT_CMD_SET_BUTTON, button, code & 0xff, (code >>> 8) & 0xff, (code >>> 16) & 0xff, (code >>> 24) & 0xff);
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Encodes the raw 0-2 performance-mode value. The value-to-label mapping
|
|
943
|
+
* (HP=2 / Corded=1 / LP=0) is now CONFIRMED — see `INCOTT_SUB_PERFORMANCE`
|
|
944
|
+
* for the labelled capture and its REVERSED-vs-UI warning. Most callers
|
|
945
|
+
* should go through `incottPerformanceModeToWire`/`IncottHidClient.setPowerMode`
|
|
946
|
+
* with a name instead of a raw value; this function is the low-level codec
|
|
947
|
+
* both build on.
|
|
948
|
+
*/
|
|
949
|
+
export function incottEncodeSetPerformanceMode(mode) {
|
|
950
|
+
if (!Number.isInteger(mode) || mode < INCOTT_PERFORMANCE_MODE_MIN || mode > INCOTT_PERFORMANCE_MODE_MAX) {
|
|
951
|
+
throw new RangeError(`Performance mode out of range: ${mode}`);
|
|
952
|
+
}
|
|
953
|
+
return payload(INCOTT_CMD_SET_SENSOR, INCOTT_SUB_PERFORMANCE, mode);
|
|
954
|
+
}
|
|
955
|
+
export function incottEncodeSetDebounce(ms) {
|
|
956
|
+
if (!Number.isInteger(ms) || ms < INCOTT_DEBOUNCE_MIN_MS || ms > INCOTT_DEBOUNCE_MAX_MS) {
|
|
957
|
+
throw new RangeError(`Debounce out of range: ${ms} ms`);
|
|
958
|
+
}
|
|
959
|
+
return payload(INCOTT_CMD_SET_TIMING, INCOTT_SUB_DEBOUNCE, ms);
|
|
960
|
+
}
|
|
961
|
+
export function incottEncodeSetSleep(seconds) {
|
|
962
|
+
if (!Number.isInteger(seconds) || seconds < INCOTT_SLEEP_MIN_S || seconds > INCOTT_SLEEP_MAX_S) {
|
|
963
|
+
throw new RangeError(`Sleep timer out of range: ${seconds} s`);
|
|
964
|
+
}
|
|
965
|
+
return payload(INCOTT_CMD_SET_TIMING, INCOTT_SUB_SLEEP, seconds & 0xff, (seconds >> 8) & 0xff);
|
|
966
|
+
}
|
|
967
|
+
export function incottEncodeSetReceiverLed(mode) {
|
|
968
|
+
if (!Number.isInteger(mode) || mode < 0 || mode >= INCOTT_RECEIVER_LED_MODES.length) {
|
|
969
|
+
throw new RangeError(`Receiver LED mode out of range: ${mode}`);
|
|
970
|
+
}
|
|
971
|
+
return payload(INCOTT_CMD_SET_RECEIVER_LED, mode);
|
|
972
|
+
}
|
|
973
|
+
export function incottEncodeQuery(cmd, sub = INCOTT_SUB_NONE) {
|
|
974
|
+
return payload(cmd, sub);
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Identity byte 3 -> model, transcribed from the vendor configurator's own
|
|
978
|
+
* `readDps()` dispatch. That dispatch is authoritative for all six models in
|
|
979
|
+
* a way one owner's device can never be; only the `0x0e` row is confirmed
|
|
980
|
+
* against hardware here, since this contributor has only a G23V2.
|
|
981
|
+
*
|
|
982
|
+
* `0x08` and `0x0e` BOTH mean G23V2 — the vendor tests them in a single
|
|
983
|
+
* branch (`8 == rData[2] || 14 == rData[2]`). Two hardware revisions of one
|
|
984
|
+
* model is the obvious reading, but that is an inference; what is certain is
|
|
985
|
+
* that the vendor maps both to the same name.
|
|
986
|
+
*/
|
|
987
|
+
const INCOTT_MODEL_BY_CODE = new Map([
|
|
988
|
+
[0x01, "Ghero"],
|
|
989
|
+
[0x02, "G23"],
|
|
990
|
+
[0x03, "G24"],
|
|
991
|
+
[0x06, "Zero 29"],
|
|
992
|
+
[0x08, "G23V2"],
|
|
993
|
+
[0x09, "Zero 39"],
|
|
994
|
+
[0x0e, "G23V2"],
|
|
995
|
+
]);
|
|
996
|
+
/** PixArt PAW3395 — capped at 32000 DPI in the vendor's own DPI table. */
|
|
997
|
+
export const INCOTT_SENSOR_PAW3395 = 0x3395;
|
|
998
|
+
/** PixArt PAW3950 — capped at 45000 DPI, and what the "Pro" suffix means. */
|
|
999
|
+
export const INCOTT_SENSOR_PAW3950 = 0x3950;
|
|
1000
|
+
/** Identity byte 2 is a fixed `0x01` guard; the vendor rejects the device otherwise. */
|
|
1001
|
+
const INCOTT_IDENTITY_GUARD = 0x01;
|
|
1002
|
+
/** Identity sensor byte: `0xF1` selects the PAW3950 profile, anything else the PAW3395. */
|
|
1003
|
+
const INCOTT_IDENTITY_SENSOR_PAW3950 = 0xf1;
|
|
1004
|
+
/** Identity byte 4 — `0x02` is the 8 KHz receiver. */
|
|
1005
|
+
const INCOTT_IDENTITY_8K_RECEIVER = 0x02;
|
|
1006
|
+
/**
|
|
1007
|
+
* A response frame is trustworthy only when the report ID, the command echo
|
|
1008
|
+
* and (when one was sent) the sub-command echo all agree with the request.
|
|
1009
|
+
* The device latches a single shared response buffer, so a frame left over
|
|
1010
|
+
* from an earlier query will otherwise be decoded as a real value.
|
|
1011
|
+
*/
|
|
1012
|
+
export function incottFrameMatches(frame, cmd, sub, axis = null) {
|
|
1013
|
+
if (frame.length < 3)
|
|
1014
|
+
return false;
|
|
1015
|
+
if (frame[0] !== INCOTT_REPORT_ID)
|
|
1016
|
+
return false;
|
|
1017
|
+
if (frame[1] !== cmd)
|
|
1018
|
+
return false;
|
|
1019
|
+
if (sub !== null && frame[2] !== sub)
|
|
1020
|
+
return false;
|
|
1021
|
+
// The per-axis DPI read echoes the requested axis at byte 8. Reading X and
|
|
1022
|
+
// then Y on the SAME stage sends two requests whose command and sub-command
|
|
1023
|
+
// are identical, so without this the second read can be satisfied by the
|
|
1024
|
+
// first one's latched reply and both axes report the same number — which
|
|
1025
|
+
// is exactly the reading that made this driver conclude no per-axis read
|
|
1026
|
+
// existed. See `incottEncodeQueryDpiAxis`.
|
|
1027
|
+
if (axis !== null && frame[8] !== axis)
|
|
1028
|
+
return false;
|
|
1029
|
+
return true;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* `09 83` -> `<count> <active>` at response bytes 2 and 3.
|
|
1033
|
+
*
|
|
1034
|
+
* Byte 3 was proven not to be a DPI-value index on hardware 2026-09-07:
|
|
1035
|
+
* decoding it through `INCOTT_DPI_DEFAULT_STAGE_PRESETS` used to yield 800
|
|
1036
|
+
* DPI, matching the vendor UI only by coincidence (the device happened to be
|
|
1037
|
+
* on stage 1 of 6). Combine with `incottDecodeDpiStage` at `active` to get
|
|
1038
|
+
* the actual DPI value — see `IncottHidClient.readStatus`.
|
|
1039
|
+
*
|
|
1040
|
+
* Byte 2 was then mistaken for a sub-command echo, because the count on the
|
|
1041
|
+
* only device available is 6 and the driver happened to send `06`. It is
|
|
1042
|
+
* data: `09 83 00` and a bare `09 83` both answer `06`. Matching it as an
|
|
1043
|
+
* echo would reject every reply from a mouse whose cycle is not six stages
|
|
1044
|
+
* long, so this decoder matches on the COMMAND ONLY — see
|
|
1045
|
+
* `INCOTT_DPI_STAGE_COUNT_DEFAULT`.
|
|
1046
|
+
*/
|
|
1047
|
+
export function incottDecodeDpiCycle(frame) {
|
|
1048
|
+
if (!incottFrameMatches(frame, INCOTT_CMD_QUERY_DPI_STAGE, null))
|
|
1049
|
+
return null;
|
|
1050
|
+
const count = frame[2];
|
|
1051
|
+
const active = frame[3];
|
|
1052
|
+
if (count === undefined || active === undefined)
|
|
1053
|
+
return null;
|
|
1054
|
+
if (count < 1 || count > INCOTT_DPI_STAGE_COUNT)
|
|
1055
|
+
return null;
|
|
1056
|
+
// An active index outside the cycle is not a reading this driver can make
|
|
1057
|
+
// sense of, and guessing a fallback would put the DPI panel on the wrong
|
|
1058
|
+
// stage. Report nothing instead.
|
|
1059
|
+
if (active >= count)
|
|
1060
|
+
return null;
|
|
1061
|
+
return { count, active };
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* `09 82 <stage>` -> the DPI value stored in that stage, little-endian at
|
|
1065
|
+
* response bytes 3-4. This opcode used to be `INCOTT_CMD_UNKNOWN_82` — see
|
|
1066
|
+
* the comment on `INCOTT_CMD_QUERY_DPI_STAGE_VALUE` for the hardware proof
|
|
1067
|
+
* (six independent stage reads plus a write/read-back/restore round-trip on
|
|
1068
|
+
* stage 2). `0x82` echoes its sub-command like `0x83`/`0x84`/`0x85`/`0x8e`
|
|
1069
|
+
* (verified: `09 82 03` replies `09 82 03 …`), so `incottFrameMatches`
|
|
1070
|
+
* checking byte 2 against `stage` is safe here.
|
|
1071
|
+
*/
|
|
1072
|
+
export function incottDecodeDpiStage(frame, stage) {
|
|
1073
|
+
if (!incottFrameMatches(frame, INCOTT_CMD_QUERY_DPI_STAGE_VALUE, stage))
|
|
1074
|
+
return null;
|
|
1075
|
+
if (frame.length < 5)
|
|
1076
|
+
return null;
|
|
1077
|
+
const wire = frame[3] | (frame[4] << 8);
|
|
1078
|
+
const dpi = (wire + 1) * INCOTT_DPI_STEP;
|
|
1079
|
+
return dpi >= INCOTT_DPI_MIN && dpi <= INCOTT_DPI_MAX ? dpi : null;
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* One axis of one stage, from a reply to `incottEncodeQueryDpiAxis`. The
|
|
1083
|
+
* requested axis MUST be matched at byte 8 — see `incottFrameMatches`.
|
|
1084
|
+
*/
|
|
1085
|
+
export function incottDecodeDpiStageAxis(frame, stage, axis) {
|
|
1086
|
+
if (!incottFrameMatches(frame, INCOTT_CMD_QUERY_DPI_STAGE_VALUE, stage, INCOTT_DPI_AXIS[axis])) {
|
|
1087
|
+
return null;
|
|
1088
|
+
}
|
|
1089
|
+
if (frame.length < 5)
|
|
1090
|
+
return null;
|
|
1091
|
+
const wire = frame[3] | (frame[4] << 8);
|
|
1092
|
+
const dpi = (wire + 1) * INCOTT_DPI_STEP;
|
|
1093
|
+
return dpi >= INCOTT_DPI_MIN && dpi <= INCOTT_DPI_MAX ? dpi : null;
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* The polling value sits in byte 2, mirroring the write, which also carries
|
|
1097
|
+
* its value in the sub-command slot. CONFIRMED on hardware 2026-09-08: writing
|
|
1098
|
+
* wire `1` then wire `0` and reading back showed byte 2 follow, `0 -> 1 -> 0`.
|
|
1099
|
+
* `0x81` does NOT echo a sub-command the way `0x82`-`0x86`/`0x8e` do — byte 2
|
|
1100
|
+
* here is data, not an echo — which is why it stays out of
|
|
1101
|
+
* `SUB_ECHOING_QUERIES` in `src/drivers/incott/hid.ts`.
|
|
1102
|
+
*/
|
|
1103
|
+
export function incottDecodePollingRate(frame) {
|
|
1104
|
+
if (!incottFrameMatches(frame, INCOTT_CMD_QUERY_POLLING, null))
|
|
1105
|
+
return null;
|
|
1106
|
+
return INCOTT_POLLING_WIRE_TO_HZ[frame[2] ?? -1] ?? null;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Reads lift-off from the packed byte 7 of the `0x84`/sub `0x00` response
|
|
1110
|
+
* (high nibble). Superseded as the driver's primary read by
|
|
1111
|
+
* `incottDecodeLiftOffDirect` (see `INCOTT_SUB_LOD`'s symmetric
|
|
1112
|
+
* `0x84`/`0x01` read), which is clearer, but kept and still exercised: it was
|
|
1113
|
+
* cross-checked against the symmetric read on hardware 2026-09-08 — hardware
|
|
1114
|
+
* values 0, 1, 2 round-tripped identically through both forms — so this is
|
|
1115
|
+
* not wrong, just less direct.
|
|
1116
|
+
*/
|
|
1117
|
+
export function incottDecodeLiftOff(frame) {
|
|
1118
|
+
if (!incottFrameMatches(frame, INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_NONE))
|
|
1119
|
+
return null;
|
|
1120
|
+
if (frame.length < 8)
|
|
1121
|
+
return null;
|
|
1122
|
+
return INCOTT_LOD_WIRE_TO_TENTHS[frame[7] >> 4] ?? null;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Symmetric single-purpose read for lift-off: `09 84 01` -> response byte 3
|
|
1126
|
+
* is the raw hardware value (0-2), matching the sub-command the `0x04`/`0x01`
|
|
1127
|
+
* write uses (see `incottEncodeSetLiftOff`). PREFERRED over
|
|
1128
|
+
* `incottDecodeLiftOff`'s packed byte-7 nibble read: verified on hardware
|
|
1129
|
+
* 2026-09-08 by round-tripping hw 0, 1, 2 through both this form and the
|
|
1130
|
+
* nibble form and finding they agree at every step. The hw-to-millimetre
|
|
1131
|
+
* label mapping is still an open question — see `INCOTT_LOD_WIRE_TO_TENTHS`.
|
|
1132
|
+
*/
|
|
1133
|
+
export function incottDecodeLiftOffDirect(frame) {
|
|
1134
|
+
if (!incottFrameMatches(frame, INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_LOD))
|
|
1135
|
+
return null;
|
|
1136
|
+
const wire = frame[3];
|
|
1137
|
+
return wire !== undefined ? (INCOTT_LOD_WIRE_TO_TENTHS[wire] ?? null) : null;
|
|
1138
|
+
}
|
|
1139
|
+
/**
|
|
1140
|
+
* Reads motion sync from the packed byte 7 of the `0x84`/sub `0x00` response
|
|
1141
|
+
* (low nibble). Superseded as the driver's primary read by
|
|
1142
|
+
* `incottDecodeToggle(frame, INCOTT_SUB_MOTION_SYNC)` (the symmetric
|
|
1143
|
+
* `0x84`/`0x04` read), which is clearer, but kept and still exercised: it was
|
|
1144
|
+
* cross-checked against the symmetric read on hardware 2026-09-08 — 0/1/0
|
|
1145
|
+
* round-tripped identically through both forms — so this is not wrong, just
|
|
1146
|
+
* less direct.
|
|
1147
|
+
*/
|
|
1148
|
+
export function incottDecodeMotionSync(frame) {
|
|
1149
|
+
if (!incottFrameMatches(frame, INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_NONE))
|
|
1150
|
+
return null;
|
|
1151
|
+
if (frame.length < 8)
|
|
1152
|
+
return null;
|
|
1153
|
+
const nibble = frame[7] & 0x0f;
|
|
1154
|
+
return nibble === 0x01 ? true : nibble === 0x00 ? false : null;
|
|
1155
|
+
}
|
|
1156
|
+
export function incottDecodeToggle(frame, sub) {
|
|
1157
|
+
if (!incottFrameMatches(frame, INCOTT_CMD_QUERY_SENSOR, sub ?? null))
|
|
1158
|
+
return null;
|
|
1159
|
+
const value = frame[3];
|
|
1160
|
+
if (value === 0x01)
|
|
1161
|
+
return true;
|
|
1162
|
+
if (value === 0x00)
|
|
1163
|
+
return false;
|
|
1164
|
+
return null;
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* `09 84 05` -> response byte 3 carries the current raw 0-2 performance-mode
|
|
1168
|
+
* value. This follows the symmetric-read pattern every other `0x04`/`0x84`
|
|
1169
|
+
* sensor sub-command uses (lift-off, ripple, angle snap and motion sync all
|
|
1170
|
+
* pair a `0x04` write with an `0x84` read at the same sub-command), and
|
|
1171
|
+
* `0x84` is already in the sub-echoing set (`SUB_ECHOING_QUERIES` in
|
|
1172
|
+
* `src/drivers/incott/hid.ts`), so the existing transaction discipline covers
|
|
1173
|
+
* it. Callers must still treat `null` as "unreadable," not as a confirmed
|
|
1174
|
+
* value — see `IncottHidClient.setPowerMode`, which requires a non-null,
|
|
1175
|
+
* matching read-back before reporting success.
|
|
1176
|
+
*/
|
|
1177
|
+
export function incottDecodePerformanceMode(frame) {
|
|
1178
|
+
if (!incottFrameMatches(frame, INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_PERFORMANCE))
|
|
1179
|
+
return null;
|
|
1180
|
+
const value = frame[3];
|
|
1181
|
+
return value !== undefined && value >= INCOTT_PERFORMANCE_MODE_MIN && value <= INCOTT_PERFORMANCE_MODE_MAX
|
|
1182
|
+
? value
|
|
1183
|
+
: null;
|
|
1184
|
+
}
|
|
1185
|
+
export function incottDecodeDebounce(frame) {
|
|
1186
|
+
if (!incottFrameMatches(frame, INCOTT_CMD_QUERY_TIMING, INCOTT_SUB_DEBOUNCE))
|
|
1187
|
+
return null;
|
|
1188
|
+
const value = frame[3] ?? -1;
|
|
1189
|
+
return value >= 0 && value <= INCOTT_DEBOUNCE_MAX_MS ? value : null;
|
|
1190
|
+
}
|
|
1191
|
+
export function incottDecodeSleep(frame) {
|
|
1192
|
+
if (!incottFrameMatches(frame, INCOTT_CMD_QUERY_TIMING, INCOTT_SUB_SLEEP))
|
|
1193
|
+
return null;
|
|
1194
|
+
if (frame.length < 5)
|
|
1195
|
+
return null;
|
|
1196
|
+
const value = frame[3] | (frame[4] << 8);
|
|
1197
|
+
return value >= INCOTT_SLEEP_MIN_S && value <= INCOTT_SLEEP_MAX_S ? value : null;
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* `09 05 02 <times> <interval ms>` — see `INCOTT_SUB_FIRE_KEY`.
|
|
1201
|
+
*
|
|
1202
|
+
* These are the settings for whichever button is bound to "Rapid fire"; they
|
|
1203
|
+
* are global to the device rather than per-button, since the command carries
|
|
1204
|
+
* no button index.
|
|
1205
|
+
*/
|
|
1206
|
+
export function incottEncodeSetFireKey(times, intervalMs) {
|
|
1207
|
+
if (!Number.isInteger(times) || times < 0 || times > INCOTT_FIRE_KEY_MAX_TIMES) {
|
|
1208
|
+
throw new RangeError(`Fire key times out of range: ${times}`);
|
|
1209
|
+
}
|
|
1210
|
+
if (!Number.isInteger(intervalMs) || intervalMs < 0 || intervalMs > INCOTT_FIRE_KEY_MAX_INTERVAL_MS) {
|
|
1211
|
+
throw new RangeError(`Fire key interval out of range: ${intervalMs} ms`);
|
|
1212
|
+
}
|
|
1213
|
+
return payload(INCOTT_CMD_SET_TIMING, INCOTT_SUB_FIRE_KEY, times, intervalMs);
|
|
1214
|
+
}
|
|
1215
|
+
/** `09 85 02` -> times at byte 3, interval at byte 4. */
|
|
1216
|
+
export function incottDecodeFireKey(frame) {
|
|
1217
|
+
if (!incottFrameMatches(frame, INCOTT_CMD_QUERY_TIMING, INCOTT_SUB_FIRE_KEY))
|
|
1218
|
+
return null;
|
|
1219
|
+
if (frame.length < 5)
|
|
1220
|
+
return null;
|
|
1221
|
+
const times = frame[3];
|
|
1222
|
+
const intervalMs = frame[4];
|
|
1223
|
+
if (times > INCOTT_FIRE_KEY_MAX_TIMES)
|
|
1224
|
+
return null;
|
|
1225
|
+
return { times, intervalMs };
|
|
1226
|
+
}
|
|
1227
|
+
export function incottDecodeReceiverLed(frame) {
|
|
1228
|
+
if (frame[0] !== INCOTT_REPORT_ID || frame[1] !== INCOTT_CMD_QUERY_RECEIVER_LED)
|
|
1229
|
+
return null;
|
|
1230
|
+
const value = frame[2] ?? -1;
|
|
1231
|
+
return value >= 0 && value < INCOTT_RECEIVER_LED_MODES.length ? value : null;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* DISPROVEN AS A BATTERY READING, 2026-09-08 — kept as a codec only (its
|
|
1235
|
+
* mechanical byte-6 decode is unchanged and still exercised by tests) and for
|
|
1236
|
+
* `IncottHidClient.readStatus`'s regression test that battery is no longer
|
|
1237
|
+
* sourced from this frame. See `INCOTT_CMD_QUERY_BATTERY` for the full
|
|
1238
|
+
* write-up: a full charge cycle from roughly 60% to roughly 97% left this
|
|
1239
|
+
* frame's byte 6 completely unchanged (`09 8e 01 5a 04 84 38 01`, `0x38` = 56
|
|
1240
|
+
* throughout), the same disproof `0x89` byte 8 suffered earlier. Originally
|
|
1241
|
+
* "proven" on hardware 2026-09-07 against a single static capture
|
|
1242
|
+
* (`TX 09 8e 01` -> `RX 09 8e 01 5a 04 84 38 01 00`) that only ever showed one
|
|
1243
|
+
* reading was never distinguished from a constant until the later
|
|
1244
|
+
* full-cycle test. The real battery percentage is a field of the unsolicited
|
|
1245
|
+
* input report the mouse emits while in use — see `incottDecodeInputStatus`
|
|
1246
|
+
* and `IncottHidClient`'s class comment.
|
|
1247
|
+
*/
|
|
1248
|
+
export function incottDecodeBattery(frame) {
|
|
1249
|
+
if (!incottFrameMatches(frame, INCOTT_CMD_QUERY_BATTERY, INCOTT_SUB_BATTERY))
|
|
1250
|
+
return null;
|
|
1251
|
+
const value = frame[6];
|
|
1252
|
+
return value !== undefined && value >= 0 && value <= 100 ? value : null;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Raw byte 1 (this module's convention — see below) of the mouse's
|
|
1256
|
+
* unsolicited input report, above which the mouse is charging. At or below
|
|
1257
|
+
* this, the raw byte IS the battery percentage; above it, subtract
|
|
1258
|
+
* `INCOTT_INPUT_BATTERY_CHARGING_OFFSET` to get the percentage. See
|
|
1259
|
+
* `incottDecodeInputStatus`.
|
|
1260
|
+
*/
|
|
1261
|
+
export const INCOTT_INPUT_BATTERY_CHARGING_THRESHOLD = 100;
|
|
1262
|
+
/** See `INCOTT_INPUT_BATTERY_CHARGING_THRESHOLD`. */
|
|
1263
|
+
export const INCOTT_INPUT_BATTERY_CHARGING_OFFSET = 128;
|
|
1264
|
+
/**
|
|
1265
|
+
* Decodes the mouse's UNSOLICITED input report — battery plus a packed
|
|
1266
|
+
* DPI-stage/polling-rate snapshot. This is NOT a feature-report reply (no
|
|
1267
|
+
* request triggers it): the device emits it on report id
|
|
1268
|
+
* `INCOTT_INPUT_REPORT_ID` on its own, only while it is actively being used.
|
|
1269
|
+
*
|
|
1270
|
+
* BYTE-INDEX CONVENTION, mirroring the asymmetry this module's top-of-file
|
|
1271
|
+
* comment already documents for feature reports (WebHID's `sendFeatureReport`
|
|
1272
|
+
* takes the report id separately, so encoded payloads omit it, while
|
|
1273
|
+
* `receiveFeatureReport` returns it at byte 0, so decoded frames include it):
|
|
1274
|
+
* this function's `byte0`/`byte1` parameters EXCLUDE the report id, matching
|
|
1275
|
+
* WebHID's `inputreport` event, whose `data` DataView excludes it too
|
|
1276
|
+
* (`event.reportId` carries it separately). So `byte0` is
|
|
1277
|
+
* `event.data.getUint8(0)`, `byte1` is `event.data.getUint8(1)`.
|
|
1278
|
+
*
|
|
1279
|
+
* node-hid's raw input buffer, by contrast, INCLUDES the report id at index
|
|
1280
|
+
* 0 — the two hardware captures below were taken that way, so in node-hid
|
|
1281
|
+
* terms `byte0` is `buf[1]` and `byte1` is `buf[2]`. Get this backwards and
|
|
1282
|
+
* every field reads off by one byte. See
|
|
1283
|
+
* `captures/incott-8k-wireless/input-report-battery.hex` and
|
|
1284
|
+
* `src/drivers/incott/hid.ts`'s `onInputReport`, which unwraps the WebHID
|
|
1285
|
+
* event into this convention before calling this function.
|
|
1286
|
+
*
|
|
1287
|
+
* Hardware-verified 2026-09-08 across a full charge cycle (~60% to ~97%):
|
|
1288
|
+
* ```
|
|
1289
|
+
* discharging: 09 5f 10 04 00 0f 0f 10 (node-hid) byte0=0x5f=95 -> 95%
|
|
1290
|
+
* charging: 09 e1 10 04 00 0f 0f 10 (node-hid) byte0=0xe1=225 -> charging, 225-128=97%
|
|
1291
|
+
* ```
|
|
1292
|
+
* `raw > 100` means charging (subtract `INCOTT_INPUT_BATTERY_CHARGING_OFFSET`
|
|
1293
|
+
* for the percent); `raw <= 100` means discharging (raw IS the percent). This
|
|
1294
|
+
* is the same convention IncottHIDApp's `parseStatus` uses, and unlike that
|
|
1295
|
+
* project's other claims, this one is now independently hardware-verified in
|
|
1296
|
+
* both states — see `docs/incott-testing.md`.
|
|
1297
|
+
*
|
|
1298
|
+
* Byte 1's `0x10` on the unit under test decoded to DPI stage 1 / polling
|
|
1299
|
+
* index 0, and both independently matched what the feature reads returned at
|
|
1300
|
+
* the same moment (`09 83 06` -> stage 1, `09 81` byte 2 -> 0) — a
|
|
1301
|
+
* corroborating cross-check only; `IncottHidClient` still treats the feature
|
|
1302
|
+
* reads as authoritative for DPI stage and polling rate.
|
|
1303
|
+
*
|
|
1304
|
+
* Returns `null` when either byte is out of range, or when the derived
|
|
1305
|
+
* percent would fall outside 0-100 (a malformed or unrelated report).
|
|
1306
|
+
*/
|
|
1307
|
+
export function incottDecodeInputStatus(byte0, byte1) {
|
|
1308
|
+
if (byte0 === undefined || byte0 < 0 || byte0 > 255)
|
|
1309
|
+
return null;
|
|
1310
|
+
if (byte1 === undefined || byte1 < 0 || byte1 > 255)
|
|
1311
|
+
return null;
|
|
1312
|
+
const charging = byte0 > INCOTT_INPUT_BATTERY_CHARGING_THRESHOLD;
|
|
1313
|
+
const batteryPercent = charging ? byte0 - INCOTT_INPUT_BATTERY_CHARGING_OFFSET : byte0;
|
|
1314
|
+
if (batteryPercent < 0 || batteryPercent > 100)
|
|
1315
|
+
return null;
|
|
1316
|
+
return {
|
|
1317
|
+
batteryPercent,
|
|
1318
|
+
charging,
|
|
1319
|
+
dpiStageIndex: (byte1 >> 4) & 0x0f,
|
|
1320
|
+
pollingIndex: byte1 & 0x0f,
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* `09 86 <button 0..5>` -> response bytes 3-6 = the binding, 32-bit
|
|
1325
|
+
* little-endian.
|
|
1326
|
+
*
|
|
1327
|
+
* PREVIOUSLY A BUG: this read only bytes 3-5 and reported them as three
|
|
1328
|
+
* unnamed bytes, silently truncating the top byte. Every action in
|
|
1329
|
+
* `INCOTT_BUTTON_ACTIONS` whose code exceeds 24 bits — "Rapid fire"
|
|
1330
|
+
* (`0x0218F00A`) among them — decoded to a different value than was
|
|
1331
|
+
* written. The vendor reads the same four bytes
|
|
1332
|
+
* (`rData[5]<<24|rData[4]<<16|rData[3]<<8|rData[2]`).
|
|
1333
|
+
*/
|
|
1334
|
+
export function incottDecodeButtonBinding(frame, button) {
|
|
1335
|
+
if (!incottFrameMatches(frame, INCOTT_CMD_QUERY_BUTTON, button))
|
|
1336
|
+
return null;
|
|
1337
|
+
if (frame.length < 7)
|
|
1338
|
+
return null;
|
|
1339
|
+
const code = ((frame[6] << 24) | (frame[5] << 16) | (frame[4] << 8) | frame[3]) >>> 0;
|
|
1340
|
+
return { button, code, label: incottButtonActionLabel(code) };
|
|
1341
|
+
}
|
|
1342
|
+
/**
|
|
1343
|
+
* Decodes the identity reply (`09 8f 00` -> `09 8f 01 0e 02 f0 f1 00 ff`).
|
|
1344
|
+
*
|
|
1345
|
+
* Byte map, transcribed from the vendor configurator's `readDps()` and
|
|
1346
|
+
* confirmed byte-for-byte against this contributor's G23V2 (capture
|
|
1347
|
+
* 2026-09-07, `captures/incott-8k-wireless/query-sweep-0x80-0x8f.hex`):
|
|
1348
|
+
*
|
|
1349
|
+
* byte 2 guard, always 0x01 -- the vendor abandons the device otherwise
|
|
1350
|
+
* byte 3 model code -- 0x0e = G23V2, see INCOTT_MODEL_BY_CODE
|
|
1351
|
+
* byte 4 receiver type -- 0x02 = 8 KHz receiver
|
|
1352
|
+
* byte 5 sensor slot A -- 0xF0 -> PAW3395, 0xF1 -> PAW3950
|
|
1353
|
+
* byte 6 sensor slot B -- same encoding, 0x00 when unpopulated
|
|
1354
|
+
*
|
|
1355
|
+
* THE SENSOR SLOT MOVES WITH THE RECEIVER, so neither byte alone is "the"
|
|
1356
|
+
* sensor. Two wired captures of the same mouse:
|
|
1357
|
+
*
|
|
1358
|
+
* cable + dongle (2026-09-08): 09 8f 01 0e 02 f0 f1 00 ff
|
|
1359
|
+
* cable only (2026-09-11): 09 8f 01 0e 00 f1 00 00 00
|
|
1360
|
+
*
|
|
1361
|
+
* With the dongle present, byte 4 reports the receiver and the `0xF1` sits at
|
|
1362
|
+
* byte 6; with the dongle gone, byte 4 is `0x00` and the same `0xF1` sits at
|
|
1363
|
+
* byte 5. This is why the vendor indexes by connection
|
|
1364
|
+
* (`let i = this.iswireless ? 5 : 4` over its report-id-less buffer, i.e.
|
|
1365
|
+
* frame bytes 6:5) — that is correct behaviour, not the cosmetic bug an
|
|
1366
|
+
* earlier version of this comment claimed.
|
|
1367
|
+
*
|
|
1368
|
+
* WHY ANY SLOT, NOT THE CONNECTION-INDEXED ONE: the fitted sensor is settled
|
|
1369
|
+
* independently of this frame. A PAW3395 stops at 32000 DPI in the vendor's
|
|
1370
|
+
* own table, and this mouse stored and read back 45000 over the CABLE
|
|
1371
|
+
* (`captures/incott-8k-wireless/wired-dpi-ceiling-2026-09-11.hex`). It is a
|
|
1372
|
+
* PAW3950 on every link, so the answer is whichever slot carries `0xF1`. The
|
|
1373
|
+
* vendor's index disagrees in exactly one configuration — cable AND dongle
|
|
1374
|
+
* attached, where it reads byte 5's `0xF0` and drops the "Pro" — and that is
|
|
1375
|
+
* the one case where a capability byte cannot be describing this mouse.
|
|
1376
|
+
*
|
|
1377
|
+
* The same capture also shows there is NO wired DPI ceiling to model: 45000
|
|
1378
|
+
* is accepted over the cable, so `INCOTT_DPI_MAX` stays flat across links,
|
|
1379
|
+
* unlike the polling rate (`INCOTT_POLLING_STEPS_HZ_WIRED`).
|
|
1380
|
+
*
|
|
1381
|
+
* Returns `null` ONLY when the report id or command echo is wrong — `open()`
|
|
1382
|
+
* uses that as its collection-liveness probe. A frame that is well-formed
|
|
1383
|
+
* but too short, or whose guard byte is not `0x01`, still yields an identity
|
|
1384
|
+
* carrying `raw` with every decoded field left null: an unreadable model is
|
|
1385
|
+
* reported as unknown, never guessed.
|
|
1386
|
+
*/
|
|
1387
|
+
export function incottDecodeIdentity(frame) {
|
|
1388
|
+
if (frame[0] !== INCOTT_REPORT_ID || frame[1] !== INCOTT_CMD_QUERY_IDENTITY)
|
|
1389
|
+
return null;
|
|
1390
|
+
const raw = Array.from(frame.slice(2, 9))
|
|
1391
|
+
.map((byte) => byte.toString(16).padStart(2, "0"))
|
|
1392
|
+
.join(" ");
|
|
1393
|
+
const unknown = {
|
|
1394
|
+
raw,
|
|
1395
|
+
model: null,
|
|
1396
|
+
modelCode: null,
|
|
1397
|
+
displayName: null,
|
|
1398
|
+
sensorId: null,
|
|
1399
|
+
isPro: false,
|
|
1400
|
+
is8KReceiver: false,
|
|
1401
|
+
};
|
|
1402
|
+
if (frame.length < 7)
|
|
1403
|
+
return unknown;
|
|
1404
|
+
if (frame[2] !== INCOTT_IDENTITY_GUARD)
|
|
1405
|
+
return unknown;
|
|
1406
|
+
const modelCode = frame[3];
|
|
1407
|
+
const model = INCOTT_MODEL_BY_CODE.get(modelCode) ?? null;
|
|
1408
|
+
const sensorId = frame[5] === INCOTT_IDENTITY_SENSOR_PAW3950 || frame[6] === INCOTT_IDENTITY_SENSOR_PAW3950
|
|
1409
|
+
? INCOTT_SENSOR_PAW3950
|
|
1410
|
+
: INCOTT_SENSOR_PAW3395;
|
|
1411
|
+
const isPro = sensorId === INCOTT_SENSOR_PAW3950;
|
|
1412
|
+
return {
|
|
1413
|
+
raw,
|
|
1414
|
+
model,
|
|
1415
|
+
modelCode,
|
|
1416
|
+
displayName: model === null ? null : isPro ? `${model} Pro` : model,
|
|
1417
|
+
sensorId,
|
|
1418
|
+
isPro,
|
|
1419
|
+
is8KReceiver: frame[4] === INCOTT_IDENTITY_8K_RECEIVER,
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
/**
|
|
1423
|
+
* True when this product id is the WIRED one (`0x622C`) rather than the 2.4
|
|
1424
|
+
* GHz dongle's (`0x522C`, `INCOTT_PRODUCT_ID`) — hardware-verified 2026-09-08,
|
|
1425
|
+
* see `INCOTT_PRODUCT_ID_WIRED`. Formerly `incottIsChargingProduct`: it
|
|
1426
|
+
* always returned exactly this, but under a name that mislabelled the
|
|
1427
|
+
* connection as a charging flag. Charging is real for this product id, but
|
|
1428
|
+
* it is a CONSEQUENCE of being plugged in, not what the id itself encodes —
|
|
1429
|
+
* see `IncottHidClient.readStatus`, which now sets `connectionType` from
|
|
1430
|
+
* this helper directly and derives `batteryState: "Charging"` from it rather
|
|
1431
|
+
* than from a same-named constant.
|
|
1432
|
+
*/
|
|
1433
|
+
export function incottIsWiredProduct(productId) {
|
|
1434
|
+
return productId === INCOTT_PRODUCT_ID_WIRED;
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Tidies the raw HID product string for display: drops a single leading
|
|
1438
|
+
* vendor word ("incott") and a single trailing "mouse", case-insensitively,
|
|
1439
|
+
* leaving whatever sits between. Falls back to the untouched raw string if
|
|
1440
|
+
* stripping both would leave nothing.
|
|
1441
|
+
*
|
|
1442
|
+
* "incott Esports G23V2Pro mouse" -> "Esports G23V2Pro"
|
|
1443
|
+
* "incott 8K wireless mouse" -> "8K wireless"
|
|
1444
|
+
*
|
|
1445
|
+
* WHY THIS EXISTS: the product string names a model only over the CABLE
|
|
1446
|
+
* ("incott Esports G23V2Pro mouse", hardware-verified 2026-09-08); the
|
|
1447
|
+
* wireless dongle reports a generic "incott 8K wireless mouse" with no model
|
|
1448
|
+
* in it at all. This function only TIDIES whatever raw string the device
|
|
1449
|
+
* actually reported; it never invents one.
|
|
1450
|
+
*
|
|
1451
|
+
* This is now the FALLBACK, not the primary source. The identity reply's
|
|
1452
|
+
* byte layout has since been decoded (`incottDecodeIdentity`), so
|
|
1453
|
+
* `IncottHidClient.readStatus` prefers the model read from the device —
|
|
1454
|
+
* which works wirelessly too — and drops back to this function only when the
|
|
1455
|
+
* identity query fails or reports a model code the table does not know.
|
|
1456
|
+
*/
|
|
1457
|
+
export function incottNormalizeProductName(raw) {
|
|
1458
|
+
const words = raw.trim().split(/\s+/).filter((word) => word.length > 0);
|
|
1459
|
+
if (words.length === 0)
|
|
1460
|
+
return raw;
|
|
1461
|
+
const start = words[0].toLowerCase() === "incott" ? 1 : 0;
|
|
1462
|
+
const endExclusive = words.length > start && words[words.length - 1].toLowerCase() === "mouse"
|
|
1463
|
+
? words.length - 1
|
|
1464
|
+
: words.length;
|
|
1465
|
+
const trimmed = words.slice(start, Math.max(start, endExclusive)).join(" ");
|
|
1466
|
+
return trimmed.length > 0 ? trimmed : raw;
|
|
1467
|
+
}
|
|
1468
|
+
/** Maps the lift-off wire value (in tenths of a millimetre) to the shared Low/Medium/High stops. */
|
|
1469
|
+
export function incottLiftOffLabel(tenthsMm) {
|
|
1470
|
+
if (tenthsMm === 7)
|
|
1471
|
+
return "Low";
|
|
1472
|
+
if (tenthsMm === 10)
|
|
1473
|
+
return "Medium";
|
|
1474
|
+
if (tenthsMm === 20)
|
|
1475
|
+
return "High";
|
|
1476
|
+
return null;
|
|
1477
|
+
}
|
|
1478
|
+
/** Maps a Low/Medium/High stop back to tenths of a millimetre for `incottEncodeSetLiftOff`. */
|
|
1479
|
+
export function incottLiftOffTenths(level) {
|
|
1480
|
+
return level === "Low" ? 7 : level === "Medium" ? 10 : 20;
|
|
1481
|
+
}
|
|
1482
|
+
//# sourceMappingURL=index.js.map
|