@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.
Files changed (547) hide show
  1. package/CONTRIBUTING.md +167 -0
  2. package/README.md +89 -0
  3. package/dist/asus/index.d.ts +52 -0
  4. package/dist/asus/index.d.ts.map +1 -0
  5. package/dist/asus/index.js +248 -0
  6. package/dist/asus/index.js.map +1 -0
  7. package/dist/atk/index.d.ts +200 -0
  8. package/dist/atk/index.d.ts.map +1 -0
  9. package/dist/atk/index.js +480 -0
  10. package/dist/atk/index.js.map +1 -0
  11. package/dist/bitmouse/index.d.ts +284 -0
  12. package/dist/bitmouse/index.d.ts.map +1 -0
  13. package/dist/bitmouse/index.js +396 -0
  14. package/dist/bitmouse/index.js.map +1 -0
  15. package/dist/compx/codec.d.ts +31 -0
  16. package/dist/compx/codec.d.ts.map +1 -0
  17. package/dist/compx/codec.js +50 -0
  18. package/dist/compx/codec.js.map +1 -0
  19. package/dist/corsair/index.d.ts +147 -0
  20. package/dist/corsair/index.d.ts.map +1 -0
  21. package/dist/corsair/index.js +223 -0
  22. package/dist/corsair/index.js.map +1 -0
  23. package/dist/dareu/index.d.ts +136 -0
  24. package/dist/dareu/index.d.ts.map +1 -0
  25. package/dist/dareu/index.js +344 -0
  26. package/dist/dareu/index.js.map +1 -0
  27. package/dist/drivers/asus/hid.d.ts +29 -0
  28. package/dist/drivers/asus/hid.d.ts.map +1 -0
  29. package/dist/drivers/asus/hid.js +419 -0
  30. package/dist/drivers/asus/hid.js.map +1 -0
  31. package/dist/drivers/atk/bitmouse-hid.d.ts +80 -0
  32. package/dist/drivers/atk/bitmouse-hid.d.ts.map +1 -0
  33. package/dist/drivers/atk/bitmouse-hid.js +501 -0
  34. package/dist/drivers/atk/bitmouse-hid.js.map +1 -0
  35. package/dist/drivers/atk/hid.d.ts +163 -0
  36. package/dist/drivers/atk/hid.d.ts.map +1 -0
  37. package/dist/drivers/atk/hid.js +1368 -0
  38. package/dist/drivers/atk/hid.js.map +1 -0
  39. package/dist/drivers/atk/products.d.ts +13 -0
  40. package/dist/drivers/atk/products.d.ts.map +1 -0
  41. package/dist/drivers/atk/products.js +13 -0
  42. package/dist/drivers/atk/products.js.map +1 -0
  43. package/dist/drivers/attackshark/dpi.d.ts +41 -0
  44. package/dist/drivers/attackshark/dpi.d.ts.map +1 -0
  45. package/dist/drivers/attackshark/dpi.js +215 -0
  46. package/dist/drivers/attackshark/dpi.js.map +1 -0
  47. package/dist/drivers/attackshark/hid.d.ts +124 -0
  48. package/dist/drivers/attackshark/hid.d.ts.map +1 -0
  49. package/dist/drivers/attackshark/hid.js +728 -0
  50. package/dist/drivers/attackshark/hid.js.map +1 -0
  51. package/dist/drivers/corsair/hid.d.ts +70 -0
  52. package/dist/drivers/corsair/hid.d.ts.map +1 -0
  53. package/dist/drivers/corsair/hid.js +414 -0
  54. package/dist/drivers/corsair/hid.js.map +1 -0
  55. package/dist/drivers/dareu/hid.d.ts +59 -0
  56. package/dist/drivers/dareu/hid.d.ts.map +1 -0
  57. package/dist/drivers/dareu/hid.js +478 -0
  58. package/dist/drivers/dareu/hid.js.map +1 -0
  59. package/dist/drivers/endgame/egg-op1-hid.d.ts +107 -0
  60. package/dist/drivers/endgame/egg-op1-hid.d.ts.map +1 -0
  61. package/dist/drivers/endgame/egg-op1-hid.js +605 -0
  62. package/dist/drivers/endgame/egg-op1-hid.js.map +1 -0
  63. package/dist/drivers/endgame/egg-we-control.d.ts +42 -0
  64. package/dist/drivers/endgame/egg-we-control.d.ts.map +1 -0
  65. package/dist/drivers/endgame/egg-we-control.js +87 -0
  66. package/dist/drivers/endgame/egg-we-control.js.map +1 -0
  67. package/dist/drivers/endgame/egg-we-hid.d.ts +58 -0
  68. package/dist/drivers/endgame/egg-we-hid.d.ts.map +1 -0
  69. package/dist/drivers/endgame/egg-we-hid.js +514 -0
  70. package/dist/drivers/endgame/egg-we-hid.js.map +1 -0
  71. package/dist/drivers/fantech/hid.d.ts +106 -0
  72. package/dist/drivers/fantech/hid.d.ts.map +1 -0
  73. package/dist/drivers/fantech/hid.js +295 -0
  74. package/dist/drivers/fantech/hid.js.map +1 -0
  75. package/dist/drivers/finalmouse/hid.d.ts +31 -0
  76. package/dist/drivers/finalmouse/hid.d.ts.map +1 -0
  77. package/dist/drivers/finalmouse/hid.js +181 -0
  78. package/dist/drivers/finalmouse/hid.js.map +1 -0
  79. package/dist/drivers/gearhub/hid.d.ts +191 -0
  80. package/dist/drivers/gearhub/hid.d.ts.map +1 -0
  81. package/dist/drivers/gearhub/hid.js +542 -0
  82. package/dist/drivers/gearhub/hid.js.map +1 -0
  83. package/dist/drivers/glorious/classic-hid.d.ts +62 -0
  84. package/dist/drivers/glorious/classic-hid.d.ts.map +1 -0
  85. package/dist/drivers/glorious/classic-hid.js +326 -0
  86. package/dist/drivers/glorious/classic-hid.js.map +1 -0
  87. package/dist/drivers/glorious/hid.d.ts +39 -0
  88. package/dist/drivers/glorious/hid.d.ts.map +1 -0
  89. package/dist/drivers/glorious/hid.js +203 -0
  90. package/dist/drivers/glorious/hid.js.map +1 -0
  91. package/dist/drivers/gwolves/hid.d.ts +27 -0
  92. package/dist/drivers/gwolves/hid.d.ts.map +1 -0
  93. package/dist/drivers/gwolves/hid.js +225 -0
  94. package/dist/drivers/gwolves/hid.js.map +1 -0
  95. package/dist/drivers/gwolves/products.d.ts +17 -0
  96. package/dist/drivers/gwolves/products.d.ts.map +1 -0
  97. package/dist/drivers/gwolves/products.js +58 -0
  98. package/dist/drivers/gwolves/products.js.map +1 -0
  99. package/dist/drivers/hyperx/hid.d.ts +59 -0
  100. package/dist/drivers/hyperx/hid.d.ts.map +1 -0
  101. package/dist/drivers/hyperx/hid.js +229 -0
  102. package/dist/drivers/hyperx/hid.js.map +1 -0
  103. package/dist/drivers/incott/hid.d.ts +513 -0
  104. package/dist/drivers/incott/hid.d.ts.map +1 -0
  105. package/dist/drivers/incott/hid.js +1173 -0
  106. package/dist/drivers/incott/hid.js.map +1 -0
  107. package/dist/drivers/index.d.ts +5 -0
  108. package/dist/drivers/index.d.ts.map +1 -0
  109. package/dist/drivers/index.js +5 -0
  110. package/dist/drivers/index.js.map +1 -0
  111. package/dist/drivers/keychron/m6-hid.d.ts +81 -0
  112. package/dist/drivers/keychron/m6-hid.d.ts.map +1 -0
  113. package/dist/drivers/keychron/m6-hid.js +486 -0
  114. package/dist/drivers/keychron/m6-hid.js.map +1 -0
  115. package/dist/drivers/keychron/nape-hid.d.ts +60 -0
  116. package/dist/drivers/keychron/nape-hid.d.ts.map +1 -0
  117. package/dist/drivers/keychron/nape-hid.js +432 -0
  118. package/dist/drivers/keychron/nape-hid.js.map +1 -0
  119. package/dist/drivers/ksnake/hid.d.ts +74 -0
  120. package/dist/drivers/ksnake/hid.d.ts.map +1 -0
  121. package/dist/drivers/ksnake/hid.js +368 -0
  122. package/dist/drivers/ksnake/hid.js.map +1 -0
  123. package/dist/drivers/lamzu/hid.d.ts +58 -0
  124. package/dist/drivers/lamzu/hid.d.ts.map +1 -0
  125. package/dist/drivers/lamzu/hid.js +414 -0
  126. package/dist/drivers/lamzu/hid.js.map +1 -0
  127. package/dist/drivers/lamzu-atlantis/hid.d.ts +148 -0
  128. package/dist/drivers/lamzu-atlantis/hid.d.ts.map +1 -0
  129. package/dist/drivers/lamzu-atlantis/hid.js +587 -0
  130. package/dist/drivers/lamzu-atlantis/hid.js.map +1 -0
  131. package/dist/drivers/logitech/bolt.d.ts +41 -0
  132. package/dist/drivers/logitech/bolt.d.ts.map +1 -0
  133. package/dist/drivers/logitech/bolt.js +105 -0
  134. package/dist/drivers/logitech/bolt.js.map +1 -0
  135. package/dist/drivers/logitech/hidpp.d.ts +525 -0
  136. package/dist/drivers/logitech/hidpp.d.ts.map +1 -0
  137. package/dist/drivers/logitech/hidpp.js +2774 -0
  138. package/dist/drivers/logitech/hidpp.js.map +1 -0
  139. package/dist/drivers/logitech/mode-status.d.ts +53 -0
  140. package/dist/drivers/logitech/mode-status.d.ts.map +1 -0
  141. package/dist/drivers/logitech/mode-status.js +52 -0
  142. package/dist/drivers/logitech/mode-status.js.map +1 -0
  143. package/dist/drivers/logitech/onboard-profiles.d.ts +298 -0
  144. package/dist/drivers/logitech/onboard-profiles.d.ts.map +1 -0
  145. package/dist/drivers/logitech/onboard-profiles.js +965 -0
  146. package/dist/drivers/logitech/onboard-profiles.js.map +1 -0
  147. package/dist/drivers/logitech/rgb-effects.d.ts +20 -0
  148. package/dist/drivers/logitech/rgb-effects.d.ts.map +1 -0
  149. package/dist/drivers/logitech/rgb-effects.js +93 -0
  150. package/dist/drivers/logitech/rgb-effects.js.map +1 -0
  151. package/dist/drivers/mchose/a5-gen1-hid.d.ts +36 -0
  152. package/dist/drivers/mchose/a5-gen1-hid.d.ts.map +1 -0
  153. package/dist/drivers/mchose/a5-gen1-hid.js +219 -0
  154. package/dist/drivers/mchose/a5-gen1-hid.js.map +1 -0
  155. package/dist/drivers/mchose/dock-hid.d.ts +26 -0
  156. package/dist/drivers/mchose/dock-hid.d.ts.map +1 -0
  157. package/dist/drivers/mchose/dock-hid.js +176 -0
  158. package/dist/drivers/mchose/dock-hid.js.map +1 -0
  159. package/dist/drivers/mchose/hid.d.ts +118 -0
  160. package/dist/drivers/mchose/hid.d.ts.map +1 -0
  161. package/dist/drivers/mchose/hid.js +550 -0
  162. package/dist/drivers/mchose/hid.js.map +1 -0
  163. package/dist/drivers/mchose/v3-hid.d.ts +109 -0
  164. package/dist/drivers/mchose/v3-hid.d.ts.map +1 -0
  165. package/dist/drivers/mchose/v3-hid.js +572 -0
  166. package/dist/drivers/mchose/v3-hid.js.map +1 -0
  167. package/dist/drivers/microsoft/hid.d.ts +25 -0
  168. package/dist/drivers/microsoft/hid.d.ts.map +1 -0
  169. package/dist/drivers/microsoft/hid.js +247 -0
  170. package/dist/drivers/microsoft/hid.js.map +1 -0
  171. package/dist/drivers/moddo/hid.d.ts +31 -0
  172. package/dist/drivers/moddo/hid.d.ts.map +1 -0
  173. package/dist/drivers/moddo/hid.js +154 -0
  174. package/dist/drivers/moddo/hid.js.map +1 -0
  175. package/dist/drivers/mouse-types.d.ts +388 -0
  176. package/dist/drivers/mouse-types.d.ts.map +1 -0
  177. package/dist/drivers/mouse-types.js +2 -0
  178. package/dist/drivers/mouse-types.js.map +1 -0
  179. package/dist/drivers/ninjutso/hid.d.ts +62 -0
  180. package/dist/drivers/ninjutso/hid.d.ts.map +1 -0
  181. package/dist/drivers/ninjutso/hid.js +680 -0
  182. package/dist/drivers/ninjutso/hid.js.map +1 -0
  183. package/dist/drivers/orbital/hid.d.ts +24 -0
  184. package/dist/drivers/orbital/hid.d.ts.map +1 -0
  185. package/dist/drivers/orbital/hid.js +85 -0
  186. package/dist/drivers/orbital/hid.js.map +1 -0
  187. package/dist/drivers/orbital/host-protocol.d.ts +59 -0
  188. package/dist/drivers/orbital/host-protocol.d.ts.map +1 -0
  189. package/dist/drivers/orbital/host-protocol.js +884 -0
  190. package/dist/drivers/orbital/host-protocol.js.map +1 -0
  191. package/dist/drivers/pulsar/pulsar-hid.d.ts +63 -0
  192. package/dist/drivers/pulsar/pulsar-hid.d.ts.map +1 -0
  193. package/dist/drivers/pulsar/pulsar-hid.js +381 -0
  194. package/dist/drivers/pulsar/pulsar-hid.js.map +1 -0
  195. package/dist/drivers/pulsar/pulsar-pro-hid.d.ts +46 -0
  196. package/dist/drivers/pulsar/pulsar-pro-hid.d.ts.map +1 -0
  197. package/dist/drivers/pulsar/pulsar-pro-hid.js +343 -0
  198. package/dist/drivers/pulsar/pulsar-pro-hid.js.map +1 -0
  199. package/dist/drivers/pulsar/pulsar-xs1-hid.d.ts +40 -0
  200. package/dist/drivers/pulsar/pulsar-xs1-hid.d.ts.map +1 -0
  201. package/dist/drivers/pulsar/pulsar-xs1-hid.js +251 -0
  202. package/dist/drivers/pulsar/pulsar-xs1-hid.js.map +1 -0
  203. package/dist/drivers/rawm/hid.d.ts +29 -0
  204. package/dist/drivers/rawm/hid.d.ts.map +1 -0
  205. package/dist/drivers/rawm/hid.js +210 -0
  206. package/dist/drivers/rawm/hid.js.map +1 -0
  207. package/dist/drivers/razer/cobra-hid.d.ts +63 -0
  208. package/dist/drivers/razer/cobra-hid.d.ts.map +1 -0
  209. package/dist/drivers/razer/cobra-hid.js +282 -0
  210. package/dist/drivers/razer/cobra-hid.js.map +1 -0
  211. package/dist/drivers/razer/hid-open.d.ts +13 -0
  212. package/dist/drivers/razer/hid-open.d.ts.map +1 -0
  213. package/dist/drivers/razer/hid-open.js +29 -0
  214. package/dist/drivers/razer/hid-open.js.map +1 -0
  215. package/dist/drivers/razer/hid.d.ts +231 -0
  216. package/dist/drivers/razer/hid.d.ts.map +1 -0
  217. package/dist/drivers/razer/hid.js +772 -0
  218. package/dist/drivers/razer/hid.js.map +1 -0
  219. package/dist/drivers/razer/viper-hid.d.ts +33 -0
  220. package/dist/drivers/razer/viper-hid.d.ts.map +1 -0
  221. package/dist/drivers/razer/viper-hid.js +177 -0
  222. package/dist/drivers/razer/viper-hid.js.map +1 -0
  223. package/dist/drivers/razer/viper-mini-hid.d.ts +58 -0
  224. package/dist/drivers/razer/viper-mini-hid.d.ts.map +1 -0
  225. package/dist/drivers/razer/viper-mini-hid.js +273 -0
  226. package/dist/drivers/razer/viper-mini-hid.js.map +1 -0
  227. package/dist/drivers/razer/viper-v4-pro-hid.d.ts +26 -0
  228. package/dist/drivers/razer/viper-v4-pro-hid.d.ts.map +1 -0
  229. package/dist/drivers/razer/viper-v4-pro-hid.js +154 -0
  230. package/dist/drivers/razer/viper-v4-pro-hid.js.map +1 -0
  231. package/dist/drivers/registry.d.ts +70 -0
  232. package/dist/drivers/registry.d.ts.map +1 -0
  233. package/dist/drivers/registry.js +146 -0
  234. package/dist/drivers/registry.js.map +1 -0
  235. package/dist/drivers/steelseries/aerox3-hid.d.ts +53 -0
  236. package/dist/drivers/steelseries/aerox3-hid.d.ts.map +1 -0
  237. package/dist/drivers/steelseries/aerox3-hid.js +172 -0
  238. package/dist/drivers/steelseries/aerox3-hid.js.map +1 -0
  239. package/dist/drivers/steelseries/aerox5-hid.d.ts +54 -0
  240. package/dist/drivers/steelseries/aerox5-hid.d.ts.map +1 -0
  241. package/dist/drivers/steelseries/aerox5-hid.js +173 -0
  242. package/dist/drivers/steelseries/aerox5-hid.js.map +1 -0
  243. package/dist/drivers/steelseries/aerox5-wireless-hid.d.ts +69 -0
  244. package/dist/drivers/steelseries/aerox5-wireless-hid.d.ts.map +1 -0
  245. package/dist/drivers/steelseries/aerox5-wireless-hid.js +234 -0
  246. package/dist/drivers/steelseries/aerox5-wireless-hid.js.map +1 -0
  247. package/dist/drivers/steelseries/aerox9-wireless-hid.d.ts +70 -0
  248. package/dist/drivers/steelseries/aerox9-wireless-hid.d.ts.map +1 -0
  249. package/dist/drivers/steelseries/aerox9-wireless-hid.js +227 -0
  250. package/dist/drivers/steelseries/aerox9-wireless-hid.js.map +1 -0
  251. package/dist/drivers/steelseries/hid.d.ts +48 -0
  252. package/dist/drivers/steelseries/hid.d.ts.map +1 -0
  253. package/dist/drivers/steelseries/hid.js +157 -0
  254. package/dist/drivers/steelseries/hid.js.map +1 -0
  255. package/dist/drivers/steelseries/prime-mini-wireless-hid.d.ts +62 -0
  256. package/dist/drivers/steelseries/prime-mini-wireless-hid.d.ts.map +1 -0
  257. package/dist/drivers/steelseries/prime-mini-wireless-hid.js +215 -0
  258. package/dist/drivers/steelseries/prime-mini-wireless-hid.js.map +1 -0
  259. package/dist/drivers/steelseries/prime-plus-hid.d.ts +51 -0
  260. package/dist/drivers/steelseries/prime-plus-hid.d.ts.map +1 -0
  261. package/dist/drivers/steelseries/prime-plus-hid.js +150 -0
  262. package/dist/drivers/steelseries/prime-plus-hid.js.map +1 -0
  263. package/dist/drivers/steelseries/rival3-wireless-hid.d.ts +61 -0
  264. package/dist/drivers/steelseries/rival3-wireless-hid.d.ts.map +1 -0
  265. package/dist/drivers/steelseries/rival3-wireless-hid.js +185 -0
  266. package/dist/drivers/steelseries/rival3-wireless-hid.js.map +1 -0
  267. package/dist/drivers/steelseries/rival310-hid.d.ts +59 -0
  268. package/dist/drivers/steelseries/rival310-hid.d.ts.map +1 -0
  269. package/dist/drivers/steelseries/rival310-hid.js +182 -0
  270. package/dist/drivers/steelseries/rival310-hid.js.map +1 -0
  271. package/dist/drivers/steelseries/rival650-hid.d.ts +68 -0
  272. package/dist/drivers/steelseries/rival650-hid.d.ts.map +1 -0
  273. package/dist/drivers/steelseries/rival650-hid.js +209 -0
  274. package/dist/drivers/steelseries/rival650-hid.js.map +1 -0
  275. package/dist/drivers/steelseries/sensei-ten-hid.d.ts +62 -0
  276. package/dist/drivers/steelseries/sensei-ten-hid.d.ts.map +1 -0
  277. package/dist/drivers/steelseries/sensei-ten-hid.js +186 -0
  278. package/dist/drivers/steelseries/sensei-ten-hid.js.map +1 -0
  279. package/dist/drivers/teevolution/hid.d.ts +82 -0
  280. package/dist/drivers/teevolution/hid.d.ts.map +1 -0
  281. package/dist/drivers/teevolution/hid.js +607 -0
  282. package/dist/drivers/teevolution/hid.js.map +1 -0
  283. package/dist/drivers/vendors.d.ts +254 -0
  284. package/dist/drivers/vendors.d.ts.map +1 -0
  285. package/dist/drivers/vendors.js +587 -0
  286. package/dist/drivers/vendors.js.map +1 -0
  287. package/dist/drivers/vgn/hid.d.ts +33 -0
  288. package/dist/drivers/vgn/hid.d.ts.map +1 -0
  289. package/dist/drivers/vgn/hid.js +213 -0
  290. package/dist/drivers/vgn/hid.js.map +1 -0
  291. package/dist/drivers/wallhack/keyboard-hid.d.ts +28 -0
  292. package/dist/drivers/wallhack/keyboard-hid.d.ts.map +1 -0
  293. package/dist/drivers/wallhack/keyboard-hid.js +80 -0
  294. package/dist/drivers/wallhack/keyboard-hid.js.map +1 -0
  295. package/dist/drivers/wallhack/mouse-hid.d.ts +45 -0
  296. package/dist/drivers/wallhack/mouse-hid.d.ts.map +1 -0
  297. package/dist/drivers/wallhack/mouse-hid.js +288 -0
  298. package/dist/drivers/wallhack/mouse-hid.js.map +1 -0
  299. package/dist/drivers/webhid.d.ts +59 -0
  300. package/dist/drivers/webhid.d.ts.map +1 -0
  301. package/dist/drivers/webhid.js +2 -0
  302. package/dist/drivers/webhid.js.map +1 -0
  303. package/dist/drivers/wlmouse/hid.d.ts +81 -0
  304. package/dist/drivers/wlmouse/hid.d.ts.map +1 -0
  305. package/dist/drivers/wlmouse/hid.js +576 -0
  306. package/dist/drivers/wlmouse/hid.js.map +1 -0
  307. package/dist/drivers/wooting/hid.d.ts +86 -0
  308. package/dist/drivers/wooting/hid.d.ts.map +1 -0
  309. package/dist/drivers/wooting/hid.js +320 -0
  310. package/dist/drivers/wooting/hid.js.map +1 -0
  311. package/dist/drivers/zaunkoenig/hid.d.ts +31 -0
  312. package/dist/drivers/zaunkoenig/hid.d.ts.map +1 -0
  313. package/dist/drivers/zaunkoenig/hid.js +162 -0
  314. package/dist/drivers/zaunkoenig/hid.js.map +1 -0
  315. package/dist/endgame-gear/op1.d.ts +165 -0
  316. package/dist/endgame-gear/op1.d.ts.map +1 -0
  317. package/dist/endgame-gear/op1.js +384 -0
  318. package/dist/endgame-gear/op1.js.map +1 -0
  319. package/dist/endgame-gear/wireless.d.ts +93 -0
  320. package/dist/endgame-gear/wireless.d.ts.map +1 -0
  321. package/dist/endgame-gear/wireless.js +242 -0
  322. package/dist/endgame-gear/wireless.js.map +1 -0
  323. package/dist/fantech/index.d.ts +9 -0
  324. package/dist/fantech/index.d.ts.map +1 -0
  325. package/dist/fantech/index.js +11 -0
  326. package/dist/fantech/index.js.map +1 -0
  327. package/dist/finalmouse/index.d.ts +41 -0
  328. package/dist/finalmouse/index.d.ts.map +1 -0
  329. package/dist/finalmouse/index.js +106 -0
  330. package/dist/finalmouse/index.js.map +1 -0
  331. package/dist/gearhub/index.d.ts +210 -0
  332. package/dist/gearhub/index.d.ts.map +1 -0
  333. package/dist/gearhub/index.js +282 -0
  334. package/dist/gearhub/index.js.map +1 -0
  335. package/dist/glorious/index.d.ts +89 -0
  336. package/dist/glorious/index.d.ts.map +1 -0
  337. package/dist/glorious/index.js +236 -0
  338. package/dist/glorious/index.js.map +1 -0
  339. package/dist/glorious-classic/index.d.ts +103 -0
  340. package/dist/glorious-classic/index.d.ts.map +1 -0
  341. package/dist/glorious-classic/index.js +304 -0
  342. package/dist/glorious-classic/index.js.map +1 -0
  343. package/dist/gwolves/index.d.ts +82 -0
  344. package/dist/gwolves/index.d.ts.map +1 -0
  345. package/dist/gwolves/index.js +197 -0
  346. package/dist/gwolves/index.js.map +1 -0
  347. package/dist/hyperx/index.d.ts +181 -0
  348. package/dist/hyperx/index.d.ts.map +1 -0
  349. package/dist/hyperx/index.js +303 -0
  350. package/dist/hyperx/index.js.map +1 -0
  351. package/dist/incott/index.d.ts +1067 -0
  352. package/dist/incott/index.d.ts.map +1 -0
  353. package/dist/incott/index.js +1482 -0
  354. package/dist/incott/index.js.map +1 -0
  355. package/dist/index.d.ts +30 -0
  356. package/dist/index.d.ts.map +1 -0
  357. package/dist/index.js +30 -0
  358. package/dist/index.js.map +1 -0
  359. package/dist/keychron/index.d.ts +194 -0
  360. package/dist/keychron/index.d.ts.map +1 -0
  361. package/dist/keychron/index.js +341 -0
  362. package/dist/keychron/index.js.map +1 -0
  363. package/dist/ksnake/index.d.ts +157 -0
  364. package/dist/ksnake/index.d.ts.map +1 -0
  365. package/dist/ksnake/index.js +314 -0
  366. package/dist/ksnake/index.js.map +1 -0
  367. package/dist/lamzu/atlantis.d.ts +237 -0
  368. package/dist/lamzu/atlantis.d.ts.map +1 -0
  369. package/dist/lamzu/atlantis.js +263 -0
  370. package/dist/lamzu/atlantis.js.map +1 -0
  371. package/dist/lamzu/index.d.ts +60 -0
  372. package/dist/lamzu/index.d.ts.map +1 -0
  373. package/dist/lamzu/index.js +86 -0
  374. package/dist/lamzu/index.js.map +1 -0
  375. package/dist/logitech/controls.d.ts +138 -0
  376. package/dist/logitech/controls.d.ts.map +1 -0
  377. package/dist/logitech/controls.js +184 -0
  378. package/dist/logitech/controls.js.map +1 -0
  379. package/dist/logitech/friendly-name.d.ts +50 -0
  380. package/dist/logitech/friendly-name.d.ts.map +1 -0
  381. package/dist/logitech/friendly-name.js +74 -0
  382. package/dist/logitech/friendly-name.js.map +1 -0
  383. package/dist/logitech/haptics.d.ts +97 -0
  384. package/dist/logitech/haptics.d.ts.map +1 -0
  385. package/dist/logitech/haptics.js +116 -0
  386. package/dist/logitech/haptics.js.map +1 -0
  387. package/dist/logitech/hosts.d.ts +55 -0
  388. package/dist/logitech/hosts.d.ts.map +1 -0
  389. package/dist/logitech/hosts.js +80 -0
  390. package/dist/logitech/hosts.js.map +1 -0
  391. package/dist/logitech/index.d.ts +127 -0
  392. package/dist/logitech/index.d.ts.map +1 -0
  393. package/dist/logitech/index.js +233 -0
  394. package/dist/logitech/index.js.map +1 -0
  395. package/dist/logitech/wheel.d.ts +117 -0
  396. package/dist/logitech/wheel.d.ts.map +1 -0
  397. package/dist/logitech/wheel.js +133 -0
  398. package/dist/logitech/wheel.js.map +1 -0
  399. package/dist/mchose/a5-gen1.d.ts +38 -0
  400. package/dist/mchose/a5-gen1.d.ts.map +1 -0
  401. package/dist/mchose/a5-gen1.js +60 -0
  402. package/dist/mchose/a5-gen1.js.map +1 -0
  403. package/dist/mchose/buttons.d.ts +66 -0
  404. package/dist/mchose/buttons.d.ts.map +1 -0
  405. package/dist/mchose/buttons.js +184 -0
  406. package/dist/mchose/buttons.js.map +1 -0
  407. package/dist/mchose/dock.d.ts +77 -0
  408. package/dist/mchose/dock.d.ts.map +1 -0
  409. package/dist/mchose/dock.js +133 -0
  410. package/dist/mchose/dock.js.map +1 -0
  411. package/dist/mchose/index.d.ts +299 -0
  412. package/dist/mchose/index.d.ts.map +1 -0
  413. package/dist/mchose/index.js +429 -0
  414. package/dist/mchose/index.js.map +1 -0
  415. package/dist/mchose/v3-buttons.d.ts +64 -0
  416. package/dist/mchose/v3-buttons.d.ts.map +1 -0
  417. package/dist/mchose/v3-buttons.js +258 -0
  418. package/dist/mchose/v3-buttons.js.map +1 -0
  419. package/dist/mchose/v3.d.ts +380 -0
  420. package/dist/mchose/v3.d.ts.map +1 -0
  421. package/dist/mchose/v3.js +608 -0
  422. package/dist/mchose/v3.js.map +1 -0
  423. package/dist/microsoft/index.d.ts +19 -0
  424. package/dist/microsoft/index.d.ts.map +1 -0
  425. package/dist/microsoft/index.js +22 -0
  426. package/dist/microsoft/index.js.map +1 -0
  427. package/dist/moddo/index.d.ts +72 -0
  428. package/dist/moddo/index.d.ts.map +1 -0
  429. package/dist/moddo/index.js +147 -0
  430. package/dist/moddo/index.js.map +1 -0
  431. package/dist/ninjutso/index.d.ts +74 -0
  432. package/dist/ninjutso/index.d.ts.map +1 -0
  433. package/dist/ninjutso/index.js +133 -0
  434. package/dist/ninjutso/index.js.map +1 -0
  435. package/dist/orbital/index.d.ts +14 -0
  436. package/dist/orbital/index.d.ts.map +1 -0
  437. package/dist/orbital/index.js +19 -0
  438. package/dist/orbital/index.js.map +1 -0
  439. package/dist/pulsar/index.d.ts +81 -0
  440. package/dist/pulsar/index.d.ts.map +1 -0
  441. package/dist/pulsar/index.js +185 -0
  442. package/dist/pulsar/index.js.map +1 -0
  443. package/dist/rawm/index.d.ts +55 -0
  444. package/dist/rawm/index.d.ts.map +1 -0
  445. package/dist/rawm/index.js +229 -0
  446. package/dist/rawm/index.js.map +1 -0
  447. package/dist/razer/codec.d.ts +504 -0
  448. package/dist/razer/codec.d.ts.map +1 -0
  449. package/dist/razer/codec.js +648 -0
  450. package/dist/razer/codec.js.map +1 -0
  451. package/dist/razer/devices.d.ts +156 -0
  452. package/dist/razer/devices.d.ts.map +1 -0
  453. package/dist/razer/devices.js +540 -0
  454. package/dist/razer/devices.js.map +1 -0
  455. package/dist/razer/index.d.ts +2 -0
  456. package/dist/razer/index.d.ts.map +1 -0
  457. package/dist/razer/index.js +2 -0
  458. package/dist/razer/index.js.map +1 -0
  459. package/dist/razer/v4.d.ts +19 -0
  460. package/dist/razer/v4.d.ts.map +1 -0
  461. package/dist/razer/v4.js +51 -0
  462. package/dist/razer/v4.js.map +1 -0
  463. package/dist/steelseries/aerox3.d.ts +236 -0
  464. package/dist/steelseries/aerox3.d.ts.map +1 -0
  465. package/dist/steelseries/aerox3.js +311 -0
  466. package/dist/steelseries/aerox3.js.map +1 -0
  467. package/dist/steelseries/aerox5-wireless.d.ts +300 -0
  468. package/dist/steelseries/aerox5-wireless.d.ts.map +1 -0
  469. package/dist/steelseries/aerox5-wireless.js +389 -0
  470. package/dist/steelseries/aerox5-wireless.js.map +1 -0
  471. package/dist/steelseries/aerox5.d.ts +228 -0
  472. package/dist/steelseries/aerox5.d.ts.map +1 -0
  473. package/dist/steelseries/aerox5.js +294 -0
  474. package/dist/steelseries/aerox5.js.map +1 -0
  475. package/dist/steelseries/aerox9-wireless.d.ts +213 -0
  476. package/dist/steelseries/aerox9-wireless.d.ts.map +1 -0
  477. package/dist/steelseries/aerox9-wireless.js +305 -0
  478. package/dist/steelseries/aerox9-wireless.js.map +1 -0
  479. package/dist/steelseries/devices.d.ts +82 -0
  480. package/dist/steelseries/devices.d.ts.map +1 -0
  481. package/dist/steelseries/devices.js +310 -0
  482. package/dist/steelseries/devices.js.map +1 -0
  483. package/dist/steelseries/index.d.ts +13 -0
  484. package/dist/steelseries/index.d.ts.map +1 -0
  485. package/dist/steelseries/index.js +13 -0
  486. package/dist/steelseries/index.js.map +1 -0
  487. package/dist/steelseries/prime-mini-wireless.d.ts +253 -0
  488. package/dist/steelseries/prime-mini-wireless.d.ts.map +1 -0
  489. package/dist/steelseries/prime-mini-wireless.js +351 -0
  490. package/dist/steelseries/prime-mini-wireless.js.map +1 -0
  491. package/dist/steelseries/prime-plus.d.ts +179 -0
  492. package/dist/steelseries/prime-plus.d.ts.map +1 -0
  493. package/dist/steelseries/prime-plus.js +275 -0
  494. package/dist/steelseries/prime-plus.js.map +1 -0
  495. package/dist/steelseries/rival3-wireless.d.ts +222 -0
  496. package/dist/steelseries/rival3-wireless.d.ts.map +1 -0
  497. package/dist/steelseries/rival3-wireless.js +308 -0
  498. package/dist/steelseries/rival3-wireless.js.map +1 -0
  499. package/dist/steelseries/rival3.d.ts +99 -0
  500. package/dist/steelseries/rival3.d.ts.map +1 -0
  501. package/dist/steelseries/rival3.js +147 -0
  502. package/dist/steelseries/rival3.js.map +1 -0
  503. package/dist/steelseries/rival310.d.ts +230 -0
  504. package/dist/steelseries/rival310.d.ts.map +1 -0
  505. package/dist/steelseries/rival310.js +322 -0
  506. package/dist/steelseries/rival310.js.map +1 -0
  507. package/dist/steelseries/rival650.d.ts +215 -0
  508. package/dist/steelseries/rival650.d.ts.map +1 -0
  509. package/dist/steelseries/rival650.js +284 -0
  510. package/dist/steelseries/rival650.js.map +1 -0
  511. package/dist/steelseries/sensei-ten.d.ts +283 -0
  512. package/dist/steelseries/sensei-ten.d.ts.map +1 -0
  513. package/dist/steelseries/sensei-ten.js +389 -0
  514. package/dist/steelseries/sensei-ten.js.map +1 -0
  515. package/dist/teevolution/index.d.ts +211 -0
  516. package/dist/teevolution/index.d.ts.map +1 -0
  517. package/dist/teevolution/index.js +458 -0
  518. package/dist/teevolution/index.js.map +1 -0
  519. package/dist/valkyrie/index.d.ts +19 -0
  520. package/dist/valkyrie/index.d.ts.map +1 -0
  521. package/dist/valkyrie/index.js +43 -0
  522. package/dist/valkyrie/index.js.map +1 -0
  523. package/dist/valkyrie/settings.d.ts +28 -0
  524. package/dist/valkyrie/settings.d.ts.map +1 -0
  525. package/dist/valkyrie/settings.js +84 -0
  526. package/dist/valkyrie/settings.js.map +1 -0
  527. package/dist/vgn/index.d.ts +59 -0
  528. package/dist/vgn/index.d.ts.map +1 -0
  529. package/dist/vgn/index.js +165 -0
  530. package/dist/vgn/index.js.map +1 -0
  531. package/dist/wallhack/index.d.ts +165 -0
  532. package/dist/wallhack/index.d.ts.map +1 -0
  533. package/dist/wallhack/index.js +226 -0
  534. package/dist/wallhack/index.js.map +1 -0
  535. package/dist/wlmouse/index.d.ts +4 -0
  536. package/dist/wlmouse/index.d.ts.map +1 -0
  537. package/dist/wlmouse/index.js +7 -0
  538. package/dist/wlmouse/index.js.map +1 -0
  539. package/dist/wooting/index.d.ts +225 -0
  540. package/dist/wooting/index.d.ts.map +1 -0
  541. package/dist/wooting/index.js +296 -0
  542. package/dist/wooting/index.js.map +1 -0
  543. package/dist/zaunkoenig/index.d.ts +49 -0
  544. package/dist/zaunkoenig/index.d.ts.map +1 -0
  545. package/dist/zaunkoenig/index.js +127 -0
  546. package/dist/zaunkoenig/index.js.map +1 -0
  547. 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