@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,1173 @@
1
+ import { incottButtonActionCode, incottDecodeButtonBinding, incottDecodeDebounce, incottDecodeDpiStage, incottDecodeDpiStageAxis, incottDecodeDpiCycle, incottDecodeFireKey, incottDecodeIdentity, incottDpiMaxForSensor, incottDecodeInputStatus, incottDecodeLiftOffDirect, incottDecodePerformanceMode, incottDecodePollingRate, incottDecodeReceiverLed, incottDecodeSleep, incottDecodeToggle, incottEncodeMacroBuffer, incottEncodeMacroChunkHeader, incottEncodeQuery, incottEncodeQueryDpiAxis, incottEncodeSetButtonBinding, incottEncodeSetDebounce, incottEncodeSetDpi, incottEncodeSetDpiCycle, incottEncodeSetFireKey, incottEncodeSetLiftOff, incottEncodeSetPerformanceMode, incottEncodeSetPollingRate, incottEncodeSetReceiverLed, incottEncodeSetSleep, incottEncodeSetToggle, incottFrameMatches, incottIsWiredProduct, incottMacroChunks, incottLiftOffLabel, incottLiftOffTenths, incottNormalizeProductName, incottPerformanceModeFromWire, incottPerformanceModeToWire, incottValidateDpi, INCOTT_BUTTON_ACTIONS, INCOTT_BUTTON_NAMES, INCOTT_BUTTON_WIRE_INDEX, INCOTT_CMD_QUERY_BUTTON, INCOTT_CMD_QUERY_DPI_STAGE, INCOTT_CMD_QUERY_DPI_STAGE_VALUE, INCOTT_CMD_QUERY_IDENTITY, INCOTT_CMD_QUERY_POLLING, INCOTT_CMD_QUERY_RECEIVER_LED, INCOTT_CMD_QUERY_SENSOR, INCOTT_CMD_QUERY_TIMING, INCOTT_DEBOUNCE_MAX_MS, INCOTT_DPI_AXIS, INCOTT_DPI_DEFAULT_STAGE_PRESETS, INCOTT_DPI_MAX, INCOTT_DPI_MIN, INCOTT_DPI_STAGE_COUNT, INCOTT_DPI_STEP, INCOTT_INPUT_REPORT_ID, INCOTT_PERFORMANCE_MODE_NAMES, INCOTT_POLLING_STEPS_HZ, INCOTT_POLLING_STEPS_HZ_WIRED, INCOTT_PRODUCT_IDS, INCOTT_SLEEP_OPTIONS, INCOTT_REPORT_ID, INCOTT_RESPONSE_LENGTH, INCOTT_SUB_ANGLE_SNAP, INCOTT_SUB_DEBOUNCE, INCOTT_SUB_FIRE_KEY, INCOTT_SUB_LOD, INCOTT_SUB_MOTION_SYNC, INCOTT_SUB_NONE, INCOTT_SUB_PERFORMANCE, INCOTT_SUB_RIPPLE, INCOTT_SUB_SLEEP, INCOTT_USAGE_PAGE, INCOTT_VENDOR_ID, } from "../../incott/index.js";
2
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3
+ /**
4
+ * Serializes feature-report request/response pairs for the Incott protocol.
5
+ *
6
+ * The device exposes a single shared response buffer, so overlapping requests
7
+ * and leftover frames both produce values that belong to a different query —
8
+ * probing the same query three times in separate runs returned three
9
+ * different payloads on real hardware. Every request therefore runs alone,
10
+ * discards whatever was already latched, and accepts a frame only when its
11
+ * command and sub-command echo the request. Ported intact from IncottHub
12
+ * (`src/drivers/incott/transaction.ts`), including the stale-frame regression
13
+ * this exists to prevent — the prior-art Go implementation (IncottHIDApp)
14
+ * matches on the command byte alone and is fooled by exactly this.
15
+ */
16
+ export class IncottTransactionQueue {
17
+ transport;
18
+ settleMs;
19
+ attempts;
20
+ sleep;
21
+ tail = Promise.resolve();
22
+ constructor(transport, options = {}) {
23
+ this.transport = transport;
24
+ this.settleMs = options.settleMs ?? 50;
25
+ this.attempts = options.attempts ?? 10;
26
+ this.sleep = options.sleep ?? defaultSleep;
27
+ }
28
+ /**
29
+ * Sends one request and returns the matching response frame, or null when
30
+ * no matching frame arrives. Never rejects: a device that stops answering
31
+ * yields null so the caller can render an em dash instead of a stale value.
32
+ */
33
+ request(payload, cmd, sub, axis = null) {
34
+ const run = this.tail.then(() => this.exchange(payload, cmd, sub, axis));
35
+ // Keep the chain alive even if one exchange throws.
36
+ this.tail = run.catch(() => undefined);
37
+ return run;
38
+ }
39
+ /**
40
+ * Sends a write, which the device does not answer. Queued alongside
41
+ * requests so ordering holds, but it never reads: waiting for a response
42
+ * that will never arrive would spend the entire attempt budget per write.
43
+ */
44
+ send(payload) {
45
+ const run = this.tail.then(async () => {
46
+ try {
47
+ await this.transport.sendFeatureReport(INCOTT_REPORT_ID, toArrayBuffer(payload));
48
+ }
49
+ catch {
50
+ // A failed write surfaces as a failed read-back in the client.
51
+ }
52
+ });
53
+ this.tail = run.catch(() => undefined);
54
+ return run;
55
+ }
56
+ /**
57
+ * Sends a 32-byte OUTPUT report, the macro upload's data path. Queued with
58
+ * everything else so it cannot interleave with the 8-byte header that
59
+ * announces it, and followed by a settle delay because the vendor waits
60
+ * 80 ms between the two halves of each chunk.
61
+ */
62
+ sendOutput(reportId, payload) {
63
+ const run = this.tail.then(async () => {
64
+ if (!this.transport.sendReport) {
65
+ throw new Error("This transport cannot send output reports, which the macro upload needs.");
66
+ }
67
+ await this.transport.sendReport(reportId, toArrayBuffer(payload));
68
+ await this.sleep(this.settleMs);
69
+ });
70
+ this.tail = run.catch(() => undefined);
71
+ return run;
72
+ }
73
+ async exchange(payload, cmd, sub, axis = null) {
74
+ try {
75
+ // Discard anything latched by a previous exchange before sending.
76
+ await this.read();
77
+ await this.transport.sendFeatureReport(INCOTT_REPORT_ID, toArrayBuffer(payload));
78
+ for (let attempt = 0; attempt < this.attempts; attempt += 1) {
79
+ if (this.settleMs > 0)
80
+ await this.sleep(this.settleMs);
81
+ const frame = await this.read();
82
+ if (frame && incottFrameMatches(frame, cmd, sub, axis))
83
+ return frame;
84
+ }
85
+ return null;
86
+ }
87
+ catch {
88
+ return null;
89
+ }
90
+ }
91
+ async read() {
92
+ try {
93
+ const view = await this.transport.receiveFeatureReport(INCOTT_REPORT_ID);
94
+ const out = new Uint8Array(INCOTT_RESPONSE_LENGTH);
95
+ const length = Math.min(view.byteLength, INCOTT_RESPONSE_LENGTH);
96
+ for (let index = 0; index < length; index += 1)
97
+ out[index] = view.getUint8(index);
98
+ return out;
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ }
104
+ }
105
+ /**
106
+ * Sends the identity query (`09 8f 00`) directly against a transport and
107
+ * checks for a well-formed reply — byte 0 the report id (`0x09`), byte 1 the
108
+ * echoed command (`0x8f`). Never throws: a transport whose `sendFeatureReport`
109
+ * rejects, or that answers with something else entirely, both resolve to
110
+ * `false` (via `IncottTransactionQueue`'s own catch-and-return-null path),
111
+ * exactly like a device that is not there.
112
+ *
113
+ * WHY THIS EXISTS (BUG, hardware-verified 2026-09-08): wired, the mouse
114
+ * exposes TWO top-level collections on usage page `0xFF05` on the same
115
+ * interface — identical as far as vendor/product id and usage page go — but
116
+ * only one of them actually answers feature reports. The other rejects every
117
+ * one at the OS level (`HidD_SetFeature: (0x00000001) Incorrect function` on
118
+ * Windows). Usage page alone cannot tell them apart; only sending something
119
+ * and checking for a real reply can. See `incottSelectCollection` for the
120
+ * ordered probe-and-pick algorithm this backs, and the comment on
121
+ * `IncottHidClient.open()` for why a WebHID-based client can only apply this
122
+ * to the single collection it was handed, not choose between two.
123
+ */
124
+ export async function incottProbeCollection(transport, options) {
125
+ const queue = new IncottTransactionQueue(transport, options);
126
+ const frame = await queue.request(incottEncodeQuery(INCOTT_CMD_QUERY_IDENTITY, INCOTT_SUB_NONE), INCOTT_CMD_QUERY_IDENTITY, null);
127
+ return frame !== null && frame[0] === INCOTT_REPORT_ID && frame[1] === INCOTT_CMD_QUERY_IDENTITY;
128
+ }
129
+ /**
130
+ * Picks the first candidate transport whose collection actually answers the
131
+ * identity probe, trying candidates in the given order and moving on to the
132
+ * next one when a candidate throws, never replies, or replies with garbage.
133
+ * Returns `null` when no candidate answers.
134
+ *
135
+ * Candidates must be pre-ordered by the CALLER, most likely first — this
136
+ * function only probes in the order it is given, it does not itself inspect
137
+ * usage pages. This is the general form of the wired dead-collection fix
138
+ * (see `incottProbeCollection`): a transport layer that genuinely has more
139
+ * than one candidate to try (for example a Node-hid-based tool with access
140
+ * to every top-level collection's own path) can use this directly. WebHID
141
+ * cannot: it hands `IncottHidClient` exactly one `HIDDevice` per collection,
142
+ * already chosen by the browser's picker/filters before this driver ever
143
+ * sees it, so `IncottHidClient.open()` uses `incottProbeCollection` on
144
+ * itself instead of calling this with more than one candidate.
145
+ */
146
+ export async function incottSelectCollection(candidates, options) {
147
+ for (const candidate of candidates) {
148
+ try {
149
+ if (await incottProbeCollection(candidate, options))
150
+ return candidate;
151
+ }
152
+ catch {
153
+ // Move on to the next candidate.
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+ /**
159
+ * Byte 2 echoes the sub-command for the DPI-stage-value, sensor and timing
160
+ * queries, so it must be matched there. For polling, receiver LED and
161
+ * identity it carries data instead, and matching it would reject valid
162
+ * responses (every polling rate but 1000 Hz, every LED mode but 0, and
163
+ * identity entirely).
164
+ *
165
+ * `0x82` (DPI stage value) was added on 2026-09-08, confirmed directly on
166
+ * hardware: `09 82 03` replies `09 82 03 …`. `0x81` (polling) was
167
+ * DELIBERATELY re-confirmed NOT to belong here on the same date: writing wire
168
+ * `1` then wire `0` and reading back showed byte 2 of the `0x81` reply follow
169
+ * the write (`0 -> 1 -> 0`) — it is data, not an echoed sub-command, and
170
+ * matching it as one would reject every polling rate but 1000 Hz.
171
+ *
172
+ * `0x8e` (battery) used to be registered here (added 2026-09-07 alongside the
173
+ * original, now-disproven DPI/battery corrections — see
174
+ * `INCOTT_CMD_QUERY_BATTERY`) but this driver no longer queries `0x8e` for
175
+ * anything: battery is read from the mouse's unsolicited input report
176
+ * instead (see `onInputReport` below), so there is no live query left for
177
+ * this list to matter to. `incottDecodeBattery`'s own frame-matching in
178
+ * `src/incott/index.ts` is unaffected by this list either way.
179
+ *
180
+ * `0x86` echoes the button index in the same position, and MUST stay in this
181
+ * list now that buttons are read: six reads go out back to back, and the
182
+ * device latches a single shared response buffer, so matching on the command
183
+ * byte alone would let button 2's reply satisfy button 3's request.
184
+ *
185
+ * `0x83` WAS in this list and has been removed. Its byte 2 is the DPI stage
186
+ * COUNT, not an echo — `09 83 00` answers `09 83 06 01` — so treating it as
187
+ * one only worked because this contributor's mouse has a six-stage cycle and
188
+ * the driver happened to send `06`. On a mouse configured for four stages
189
+ * every DPI read would have been rejected. See
190
+ * `INCOTT_DPI_STAGE_COUNT_DEFAULT`.
191
+ */
192
+ const SUB_ECHOING_QUERIES = [
193
+ INCOTT_CMD_QUERY_DPI_STAGE_VALUE,
194
+ INCOTT_CMD_QUERY_SENSOR,
195
+ INCOTT_CMD_QUERY_TIMING,
196
+ INCOTT_CMD_QUERY_BUTTON,
197
+ ];
198
+ function toggleWord(value) {
199
+ return value === null ? "an unreadable state" : value ? "on" : "off";
200
+ }
201
+ /**
202
+ * WebHID client for the Incott protocol (vendor 0x093A, products 0x522C /
203
+ * 0x622C). Ported from IncottHub, adapted to mouse-protocol's driver
204
+ * interface: the client owns `device`, `open()` and `close()` directly
205
+ * instead of taking an externally-managed transport, matching every other
206
+ * driver in this registry.
207
+ *
208
+ * FOUR bugs fixed 2026-09-08 from an owner's hardware probes in BOTH wireless
209
+ * and wired modes (see `docs/incott-testing.md`):
210
+ *
211
+ * 1. WIRED MODE WAS COMPLETELY BROKEN. The wired mouse exposes two
212
+ * identical-looking `0xFF05` collections; only one answers feature
213
+ * reports, and usage page alone cannot tell them apart. See `open()`'s
214
+ * identity probe and `incottProbeCollection`/`incottSelectCollection`.
215
+ * 2. `0x622C` means the connection is WIRED, not "charging" — charging is
216
+ * a consequence of being plugged in, not what the id encodes. See
217
+ * `incottIsWiredProduct` (renamed from `incottIsChargingProduct`) and
218
+ * `MouseStatus.connectionType` below.
219
+ * 3. The polling-rate ceiling depends on the connection: the full
220
+ * 125-8000 Hz ladder is wireless-only, and the mouse only reaches
221
+ * 1000 Hz over the cable — see `INCOTT_POLLING_STEPS_HZ_WIRED`.
222
+ * 4. The real model name ("Esports G23V2Pro") is only in the WIRED product
223
+ * string; wireless reports a generic name with no model in it at all.
224
+ * See `incottNormalizeProductName`.
225
+ *
226
+ * Polling rate (0x81) is verified readable on hardware, but `readStatus()`
227
+ * must not throw (see the write-a-driver doc): when it cannot be read after
228
+ * the transaction queue's full retry budget, the client falls back to an
229
+ * identity-only status — real `name`, `brand`, and whatever else was
230
+ * genuinely readable — with `ui.settingsReady: false` and
231
+ * `ui.valuesVerified: false` so the app hides the settings grid instead of
232
+ * rendering fabricated values. `MouseStatus.pollingRateHz` is non-nullable,
233
+ * so the degraded path still assigns it a number; see the comment at that
234
+ * assignment for why that is not a fabricated reading.
235
+ *
236
+ * DPI USED to have no known read-back at all; that changed 2026-09-08. DPI
237
+ * lives in a six-stage table (see `INCOTT_DPI_STAGE_COUNT` in
238
+ * `src/incott/index.ts`) with FOUR independent operations, not one:
239
+ *
240
+ * which stage is active read: `0x83`/`0x06` write: `0x03`/`0x06 <idx>`
241
+ * what a stage holds read: `0x82`/`<stage>` write: `0x02 <stage> <lo> <hi>`
242
+ *
243
+ * `readStatus()` reads which stage is active (`incottDecodeDpiStageIndex`)
244
+ * and every one of the six stages' stored values (`incottDecodeDpiStage`),
245
+ * populating `dpiStages`/`activeDpiStage` for OpenMouse's shared multi-stage
246
+ * DPI editor (`MouseUiHints.dpiStageEditor`) and deriving `dpi` as the active
247
+ * stage's own value — a genuine live read, not a write cache.
248
+ *
249
+ * `setActiveDpiStage()` and `setDpiStageValue()` map onto the select and edit
250
+ * operations above respectively, matching OpenMouse's existing generic
251
+ * contract (`requireClientMethod` in `openmouse/src/device/controller.ts`).
252
+ * `setDpi()` is kept for the plain preset row and is defined as "set the
253
+ * ACTIVE stage's value" — i.e. it is exactly `setDpiStageValue` at whichever
254
+ * stage is currently active — since that is the only meaning left for it now
255
+ * that a real select operation exists.
256
+ *
257
+ * A SECOND DPI BUG, found from an owner report and fixed the same day
258
+ * (2026-09-08): `setDpi()` used to be the *only* way to touch DPI, so the app
259
+ * called it every time the user picked a value from the plain preset row —
260
+ * indistinguishable, from the user's point of view, from "select the stage
261
+ * that already holds this value." It never did that: it read the active
262
+ * stage and overwrote *that* stage's stored value with whatever was picked,
263
+ * one stage at a time destroying the mouse's factory-programmed table.
264
+ * (IncottHIDApp's own `0x03`/`0x06` write — which that project mislabels "set
265
+ * DPI" — is actually the active-stage SELECT and never touches the table;
266
+ * see `INCOTT_CMD_SET_DPI_STAGE` in `src/incott/index.ts`.) Wiring
267
+ * `dpiStages`/`activeDpiStage`/`dpiStageEditor` here gives the app a real
268
+ * select control (`setActiveDpiStage`) so `setDpi` is no longer asked to do a
269
+ * select's job.
270
+ *
271
+ * `valuesVerified` is true once both the polling-rate query and the active
272
+ * stage's DPI read have answered — see `readStatus()`.
273
+ *
274
+ * A FIFTH bug, fixed 2026-09-08: battery used to be read from the `0x8e`
275
+ * feature-report reply, byte 6. Charging a unit through a full cycle (roughly
276
+ * 60% to roughly 97%) while polling that reply showed byte 6 never move at
277
+ * all — a constant, not a live reading, the same failure mode `0x89` byte 8
278
+ * suffered before it (see `INCOTT_CMD_QUERY_BATTERY` in
279
+ * `src/incott/index.ts`). The real battery level lives in an UNSOLICITED
280
+ * INPUT report the mouse emits on report id 0x09 while it is actively being
281
+ * used — see `onInputReport` below and `incottDecodeInputStatus`. The device
282
+ * only emits these while in use, so `readStatus()` reports `batteryPercent:
283
+ * null` / `batteryState: "Unknown"` until the first one arrives; nothing here
284
+ * fabricates a number or falls back to the disproven feature-report bytes.
285
+ * `batteryState`'s charging flag comes from the report's own high bit
286
+ * (`raw > 100`), not from the product id: `incottIsWiredProduct` (0x622C)
287
+ * still means the CONNECTION is wired, which is what `connectionType` below
288
+ * uses, and being wired does imply charging in practice, but the input report
289
+ * is the authoritative source for the charging state, not an inference from
290
+ * the product id.
291
+ *
292
+ * A SIXTH addition, 2026-09-10: performance mode (HP/Corded/LP) is now wired
293
+ * into OpenMouse's shared `powerModes`/`powerMode`/`setPowerMode` contract.
294
+ * The value-to-label mapping was hardware-confirmed by labelling every click
295
+ * in the vendor tool before recording its write — see `getPowerModes`,
296
+ * `setPowerMode`, and `INCOTT_SUB_PERFORMANCE` in `src/incott/index.ts` for
297
+ * the capture and the REVERSED-vs-UI warning (HP=2, Corded=1, LP=0). `readStatus()`
298
+ * populates `powerMode` from a live `0x84`/`0x05` read, leaving it `undefined`
299
+ * (never fabricated) when the collection is dead or the read fails.
300
+ */
301
+ export class IncottHidClient {
302
+ device;
303
+ queue;
304
+ /**
305
+ * Cached decode of the mouse's unsolicited input report (battery plus a
306
+ * packed DPI-stage/polling-rate snapshot) — see `onInputReport` and the
307
+ * class comment's battery section. `null` until the first report arrives
308
+ * (the mouse only emits them while actively in use), which is exactly when
309
+ * `readStatus()` must report `batteryPercent: null` / `batteryState:
310
+ * "Unknown"` rather than a fabricated value.
311
+ */
312
+ lastInputStatus = null;
313
+ /**
314
+ * Whether `open()`'s identity probe got a well-formed reply from the
315
+ * collection WebHID handed this client. `null` until `open()` runs (every
316
+ * unit test that skips `open()` and calls `readStatus()` directly keeps
317
+ * this `null`, so it behaves exactly as before this field existed);
318
+ * `false` means this specific collection is the wired mouse's dead
319
+ * look-alike (see the class comment and `open()`).
320
+ */
321
+ collectionVerified = null;
322
+ constructor(device, options) {
323
+ this.device = device;
324
+ // A real HIDDevice satisfies FeatureTransport structurally.
325
+ this.queue = new IncottTransactionQueue(device, options);
326
+ }
327
+ /**
328
+ * Decodes the mouse's unsolicited input report and caches the result for
329
+ * `readStatus()` — see the class comment's battery section and
330
+ * `incottDecodeInputStatus` in `src/incott/index.ts`.
331
+ *
332
+ * BYTE-INDEX CONVENTION: WebHID's `inputreport` event carries `reportId`
333
+ * separately from `data`, and `data` EXCLUDES the report id — unlike this
334
+ * driver's feature-report frames, which include it because
335
+ * `receiveFeatureReport` returns it at byte 0 (see the top-of-file comment
336
+ * in `src/incott/index.ts` for that asymmetry, which this mirrors). So
337
+ * `event.data.getUint8(0)`/`getUint8(1)` are this report's bytes 1/2 in
338
+ * node-hid terms — node-hid's raw buffer includes the report id at index 0,
339
+ * which is how `captures/incott-8k-wireless/input-report-battery.hex` is
340
+ * indexed. Get this backwards and every field reads off by one byte.
341
+ *
342
+ * Filtered to report id `INCOTT_INPUT_REPORT_ID` (0x09): the mouse's other
343
+ * HID collections (mouse/keyboard boot reports, etc.) are not this vendor
344
+ * collection's traffic and must not be mistaken for it.
345
+ */
346
+ onInputReport = (event) => {
347
+ if (event.reportId !== INCOTT_INPUT_REPORT_ID)
348
+ return;
349
+ if (event.data.byteLength < 2)
350
+ return;
351
+ const decoded = incottDecodeInputStatus(event.data.getUint8(0), event.data.getUint8(1));
352
+ if (decoded)
353
+ this.lastInputStatus = decoded;
354
+ };
355
+ /**
356
+ * Usage page alone cannot tell the wired mouse's two look-alike `0xFF05`
357
+ * collections apart (see the class comment and `open()`), so this stays
358
+ * permissive on purpose: it accepts ANY device on a vendor-defined usage
359
+ * page with a matching product id, including the one that turns out to be
360
+ * the dead collection. Rejecting it here would also reject the live one,
361
+ * since WebHID hands this method one collection at a time and both look
362
+ * identical from `vendorId`/`productId`/`usagePage` alone. The actual
363
+ * distinction is made by probing after `open()` — see `open()` and
364
+ * `readStatus()`'s graceful degradation when the probe fails.
365
+ */
366
+ static isSupported(device) {
367
+ if (device.vendorId !== INCOTT_VENDOR_ID)
368
+ return false;
369
+ if (!INCOTT_PRODUCT_IDS.includes(device.productId))
370
+ return false;
371
+ // Require BOTH the vendor page and a declared feature report `0x09`. The
372
+ // mouse exposes several vendor collections and only this one speaks the
373
+ // protocol: enumerated on hardware 2026-09-10, page 0xFF05 declares
374
+ // feature report 0x09, while 0xFF00 declares 0x03 and 0xFF01 declares
375
+ // 0x04. Matching on the usage page alone (or on any page >= 0xFF00)
376
+ // claims those siblings too, so the app lists the mouse once per
377
+ // collection and every card but one is inert.
378
+ return device.collections.some((collection) => collection.usagePage === INCOTT_USAGE_PAGE &&
379
+ collection.featureReports.some((report) => report.reportId === INCOTT_REPORT_ID));
380
+ }
381
+ /**
382
+ * WebHID request filters for the vendor collection, one per product id —
383
+ * both the wireless dongle's `0x522C` (`INCOTT_PRODUCT_ID`) and the wired
384
+ * `0x622C` (`INCOTT_PRODUCT_ID_WIRED`), hardware-verified 2026-09-08, so
385
+ * the picker offers the mouse in either mode.
386
+ */
387
+ static get filters() {
388
+ return INCOTT_PRODUCT_IDS.map((productId) => ({
389
+ vendorId: INCOTT_VENDOR_ID,
390
+ productId,
391
+ usagePage: INCOTT_USAGE_PAGE,
392
+ }));
393
+ }
394
+ /**
395
+ * The real writable range: 50-45000 DPI in steps of 50 (see `INCOTT_DPI_MIN`
396
+ * in `src/incott/index.ts`), returned densely so the shell's custom-DPI
397
+ * entry (which validates by exact membership, not by snapping to a nearby
398
+ * value) accepts any value in range — the same pattern already used for
399
+ * Razer's Viper V4 Pro (`100-50,000 in 50-DPI increments`) and for
400
+ * Zaunkoenig and Finalmouse. This is deliberately NOT
401
+ * `INCOTT_DPI_DEFAULT_STAGE_PRESETS` (the six round numbers the vendor
402
+ * calls default stage presets) — that list is too sparse for the shell to
403
+ * let a user dial in, say, 12000 DPI, which this mouse genuinely supports.
404
+ */
405
+ getDpiOptions() {
406
+ const count = (INCOTT_DPI_MAX - INCOTT_DPI_MIN) / INCOTT_DPI_STEP + 1;
407
+ return Array.from({ length: count }, (_, index) => INCOTT_DPI_MIN + index * INCOTT_DPI_STEP);
408
+ }
409
+ /** The vendor's six default DPI stage presets — a convenience list, not the writable range. See `INCOTT_DPI_DEFAULT_STAGE_PRESETS`. */
410
+ getDpiDefaultStagePresets() {
411
+ return [...INCOTT_DPI_DEFAULT_STAGE_PRESETS];
412
+ }
413
+ /** Longest debounce the firmware accepts, in milliseconds (verified: section 8 of the IncottHub spec). */
414
+ getDebounceMaxMs() {
415
+ return INCOTT_DEBOUNCE_MAX_MS;
416
+ }
417
+ /** The firmware accepts any integer 0-30 ms, so every value in range is offered. */
418
+ getDebounceOptions() {
419
+ return Array.from({ length: INCOTT_DEBOUNCE_MAX_MS + 1 }, (_, ms) => ms);
420
+ }
421
+ /** Curated presets within the verified 1-900s sleep-timer range; see INCOTT_SLEEP_OPTIONS. */
422
+ getSleepOptions() {
423
+ return [...INCOTT_SLEEP_OPTIONS];
424
+ }
425
+ /**
426
+ * Opens the device, then PROBES it with the identity query before trusting
427
+ * it for anything else — see `incottProbeCollection`.
428
+ *
429
+ * WIRED-MODE BUG, hardware-verified 2026-09-08: the wired mouse exposes
430
+ * two `0xFF05` collections on the same interface that are indistinguishable
431
+ * by vendor id, product id, or usage page; only one of them answers
432
+ * feature reports at all (the other fails every `HidD_SetFeature` call at
433
+ * the OS level). WebHID hands this client exactly ONE `HIDDevice`, already
434
+ * chosen by the browser's picker/filters before this driver ever sees
435
+ * it — a WebHID `HIDDevice` corresponds to a single collection, so there is
436
+ * no second candidate here for this method to fall back to the way
437
+ * `incottSelectCollection` can for a transport with real alternatives. All
438
+ * `open()` CAN do in the browser is find out, immediately and cheaply,
439
+ * whether the one collection it got is the live one.
440
+ *
441
+ * This never throws on a dead collection — it records the result in
442
+ * `collectionVerified` instead. `readStatus()` checks that flag and skips
443
+ * straight to a fully degraded, `ui.settingsReady: false` status (see
444
+ * there) rather than burning the full retry budget on roughly a dozen
445
+ * queries that a collection already known to be dead cannot answer either.
446
+ *
447
+ * Also attaches `onInputReport` (see the class comment's battery section):
448
+ * the mouse's battery lives only in an unsolicited input report it emits
449
+ * while actively in use, and WebHID delivers those solely as `inputreport`
450
+ * events on the device — there is no way to poll for one, so the listener
451
+ * must be attached here, before the device is ever read, or an in-use
452
+ * report could arrive and be missed before anything is listening.
453
+ */
454
+ async open() {
455
+ if (!this.device.opened)
456
+ await this.device.open();
457
+ this.device.addEventListener("inputreport", this.onInputReport);
458
+ const identity = incottDecodeIdentity(await this.query(INCOTT_CMD_QUERY_IDENTITY, INCOTT_SUB_NONE));
459
+ this.collectionVerified = identity !== null;
460
+ }
461
+ async close() {
462
+ this.device.removeEventListener("inputreport", this.onInputReport);
463
+ if (this.device.opened)
464
+ await this.device.close();
465
+ }
466
+ async readStatus() {
467
+ // `connectionType`/`batteryState` derive from the product id, which is a
468
+ // USB descriptor field read at enumeration time — genuinely available
469
+ // even when the collection this client was handed turns out to be the
470
+ // wired mouse's dead one (see `open()`). Hardware-verified 2026-09-08:
471
+ // 0x622C enumerates ONLY when plugged in over USB; 0x522C is the 2.4 GHz
472
+ // dongle. This used to be read as a "charging" flag
473
+ // (`incottIsChargingProduct`) — the wrong axis: 0x622C means the
474
+ // CONNECTION is wired, and charging is a consequence of that, not what
475
+ // the id itself encodes. See `incottIsWiredProduct`.
476
+ const wired = incottIsWiredProduct(this.device.productId);
477
+ // Fallback display name only — the model read from the device wins where
478
+ // one is available, and `name` is settled below once the identity query
479
+ // has answered. The product string names a model over the cable
480
+ // ("incott Esports G23V2Pro mouse") but is a generic "incott 8K wireless
481
+ // mouse" on the dongle, so it cannot be the primary source. `this.device`
482
+ // stays public, so `client.device.productName` remains available as the
483
+ // untouched raw string for anything that wants it.
484
+ const rawName = this.device.productName || "Incott wireless mouse";
485
+ // WIRED-MODE BUG, hardware-verified 2026-09-08 (see `open()`'s class
486
+ // comment): when `open()`'s identity probe found this collection dead,
487
+ // every one of the ~12 queries below would fail too, each only after
488
+ // burning its own retry budget. Skip straight to the degraded values
489
+ // instead of paying that cost for a foregone conclusion. `dead` stays
490
+ // `false` (not just falsy) whenever `open()` was never called or its
491
+ // probe succeeded, so every existing caller that reads status without
492
+ // opening first is unaffected.
493
+ const dead = this.collectionVerified === false;
494
+ // DPI is a genuine live read as of 2026-09-08 (see the class comment
495
+ // above): read which of the six stages is active, then every stage's own
496
+ // value. Per the driver's graceful-degradation contract, a device that
497
+ // stops answering yields `dpi: null` / an omitted `dpiStages` here rather
498
+ // than throwing or fabricating a value.
499
+ // One query answers both how many stages the cycle uses and which is
500
+ // live — see `incottDecodeDpiCycle`. The count is NOT assumed to be six:
501
+ // it is written back verbatim by every stage select, and publishing six
502
+ // rows for a four-stage cycle would offer stages the mouse never visits.
503
+ const dpiCycle = dead
504
+ ? null
505
+ : incottDecodeDpiCycle(await this.query(INCOTT_CMD_QUERY_DPI_STAGE, INCOTT_SUB_NONE));
506
+ const activeDpiStage = dpiCycle?.active ?? null;
507
+ // Six sequential queries, deliberately NOT parallelized: the device has a
508
+ // single shared response buffer (see `IncottTransactionQueue`'s class
509
+ // comment), and concurrent requests would corrupt each other's replies.
510
+ // `this.query` already serializes through that queue, so a plain
511
+ // sequential loop is enough.
512
+ const dpiStageReads = [];
513
+ for (let stage = 0; stage < INCOTT_DPI_STAGE_COUNT; stage += 1) {
514
+ dpiStageReads.push(dead ? null : incottDecodeDpiStage(await this.query(INCOTT_CMD_QUERY_DPI_STAGE_VALUE, stage), stage));
515
+ }
516
+ // `dpiStages` is populated only when every one of the six stages
517
+ // answered — a partial table is never fabricated with a placeholder for
518
+ // the stage(s) that did not. It is then trimmed to the stages the cycle
519
+ // actually uses: the device keeps all six stored values, but the ones
520
+ // past `count` are not in the rotation and must not be offered as if
521
+ // they were.
522
+ const dpiStages = dpiStageReads.every((value) => value !== null)
523
+ ? dpiStageReads.slice(0, dpiCycle?.count ?? dpiStageReads.length)
524
+ : null;
525
+ const dpi = activeDpiStage === null ? null : dpiStageReads[activeDpiStage] ?? null;
526
+ // The active stage's Y axis, which the plain stage read above does not
527
+ // distinguish — that returns X. Published so the shared "X n · Y n DPI"
528
+ // summary is truthful on a mouse whose axes differ; omitted rather than
529
+ // mirrored from X when the read fails, so the app falls back to one
530
+ // number instead of claiming the axes match.
531
+ const dpiY = dead || activeDpiStage === null
532
+ ? null
533
+ : await this.readDpiStageAxis(activeDpiStage, "y");
534
+ const pollingRateHz = dead
535
+ ? null
536
+ : incottDecodePollingRate(await this.query(INCOTT_CMD_QUERY_POLLING, INCOTT_SUB_NONE));
537
+ // The polling-rate (0x81) query is verified readable on hardware, but a
538
+ // device that stops answering must not throw readStatus() — the app
539
+ // degrades to identity-only fields instead (see `ui.settingsReady`
540
+ // below). `valuesVerified` also requires the active DPI stage's value to
541
+ // have answered.
542
+ const valuesVerified = dpi !== null && pollingRateHz !== null;
543
+ // Symmetric single-purpose reads, preferred over the packed byte-7
544
+ // nibble trick (`incottDecodeLiftOff`/`incottDecodeMotionSync`, still
545
+ // exported and tested): verified on hardware 2026-09-08 that both forms
546
+ // agree at every step (lift-off hw 0/1/2, motion sync 0/1/0).
547
+ const liftOffTenths = dead
548
+ ? null
549
+ : incottDecodeLiftOffDirect(await this.query(INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_LOD));
550
+ const motionSync = dead
551
+ ? null
552
+ : incottDecodeToggle(await this.query(INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_MOTION_SYNC), INCOTT_SUB_MOTION_SYNC);
553
+ const rippleControl = dead
554
+ ? null
555
+ : incottDecodeToggle(await this.query(INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_RIPPLE), INCOTT_SUB_RIPPLE);
556
+ const angleSnapping = dead
557
+ ? null
558
+ : incottDecodeToggle(await this.query(INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_ANGLE_SNAP), INCOTT_SUB_ANGLE_SNAP);
559
+ // Performance mode (HP/Corded/LP), hardware-confirmed 2026-09-10 — see
560
+ // `INCOTT_SUB_PERFORMANCE` and `setPowerMode`. A live read of the raw wire
561
+ // value, converted to its display name through `incottPerformanceModeFromWire`
562
+ // (the single table the value-to-label reversal lives in). `undefined`,
563
+ // never fabricated, when the collection is dead or the read fails/returns
564
+ // an out-of-range value.
565
+ const powerModeWire = dead
566
+ ? null
567
+ : incottDecodePerformanceMode(await this.query(INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_PERFORMANCE));
568
+ const powerMode = powerModeWire === null ? undefined : incottPerformanceModeFromWire(powerModeWire) ?? undefined;
569
+ const debounceMs = dead
570
+ ? null
571
+ : incottDecodeDebounce(await this.query(INCOTT_CMD_QUERY_TIMING, INCOTT_SUB_DEBOUNCE));
572
+ const sleepTimeout = dead
573
+ ? null
574
+ : incottDecodeSleep(await this.query(INCOTT_CMD_QUERY_TIMING, INCOTT_SUB_SLEEP));
575
+ // Battery comes from `lastInputStatus` — the mouse's unsolicited input
576
+ // report, cached by `onInputReport` — NOT from a feature-report query.
577
+ // Deliberately independent of `dead`/the feature-report collection probe:
578
+ // the input report arrives on the device regardless of whether this
579
+ // client's feature-report collection turned out to be the wired mouse's
580
+ // dead look-alike (see `open()`), so a dead collection must not suppress
581
+ // a battery reading that genuinely came in. `null` here means "no report
582
+ // has arrived yet" (the mouse only emits them while actively in use), not
583
+ // a failed read — see the class comment's battery section and
584
+ // `incottDecodeInputStatus` in `src/incott/index.ts`. This REPLACES the
585
+ // disproven `0x8e`/sub `0x01` byte-6 read (itself a replacement for the
586
+ // also-disproven `0x89` byte 8) — see `INCOTT_CMD_QUERY_BATTERY` and
587
+ // `incottDecodeBattery`, both kept only as codecs/regression coverage.
588
+ const batteryPercent = this.lastInputStatus?.batteryPercent ?? null;
589
+ // The charging bit comes from the input report itself (raw > 100), not
590
+ // from the product id: `wired` (0x622C) is a genuinely reliable signal
591
+ // that being charged is *likely* (the mouse is plugged into USB), but the
592
+ // report is the authoritative source for whether it actually is charging
593
+ // right now, so it is used here instead of inferring the state from the
594
+ // connection.
595
+ const batteryCharging = this.lastInputStatus?.charging ?? null;
596
+ // The identity reply carries the model, the sensor and the receiver type
597
+ // — see `incottDecodeIdentity` for the byte map. The raw hex is still
598
+ // published under `firmware` below, because the remaining bytes (7-8) are
599
+ // genuinely undecoded and a capture of them is what a second model's
600
+ // owner would need to send.
601
+ const identity = dead
602
+ ? null
603
+ : incottDecodeIdentity(await this.query(INCOTT_CMD_QUERY_IDENTITY, INCOTT_SUB_NONE));
604
+ // The model READ FROM THE DEVICE is preferred over the product string:
605
+ // it is the only source that works on the 2.4 GHz dongle, where the
606
+ // product string has no model in it. All six Incott models share the
607
+ // same two product ids, so this is the only thing that tells them apart
608
+ // at all. Falls back to the tidied product string whenever the identity
609
+ // query failed or returned a model code outside the known table — an
610
+ // unrecognised model reports whatever the device called itself rather
611
+ // than a guess.
612
+ const name = identity?.displayName ?? incottNormalizeProductName(rawName);
613
+ // The owner confirmed on hardware that the mouse only reaches 1000 Hz
614
+ // over the cable — 2000/4000/8000 Hz are wireless-only (see
615
+ // `INCOTT_POLLING_STEPS_HZ_WIRED`). Publishing the full ladder while
616
+ // wired would let the shell offer a rate the device silently refuses,
617
+ // which `setPollingRate`'s read-back verification would then report as a
618
+ // failure on every attempt.
619
+ const supportedPollingRates = wired ? [...INCOTT_POLLING_STEPS_HZ_WIRED] : [...INCOTT_POLLING_STEPS_HZ];
620
+ // The receiver LED belongs to the 2.4 GHz dongle and means nothing over
621
+ // the cable, so it is not read at all when wired — the card then hides
622
+ // itself rather than offering a control that cannot do anything. See
623
+ // `setReceiverLed`.
624
+ const receiverLedMode = dead || wired
625
+ ? null
626
+ : incottDecodeReceiverLed(await this.query(INCOTT_CMD_QUERY_RECEIVER_LED, INCOTT_SUB_NONE));
627
+ // Rapid-fire parameters, global to the device — see `INCOTT_SUB_FIRE_KEY`.
628
+ const fireKey = dead ? null : await this.getFireKey();
629
+ // All six bindings, or nothing. A partial read would render some buttons
630
+ // with a real assignment and the rest with a fabricated default, which is
631
+ // worse than hiding the remapper: the shared UI writes back whatever it
632
+ // shows, so a wrong reading becomes a wrong write the moment anything
633
+ // else on the card is changed.
634
+ const buttonMappings = dead ? null : await this.readButtonMappings();
635
+ const ui = {
636
+ family: "incott",
637
+ // The advanced section is the only place debounce, sleep, motion sync,
638
+ // angle snapping and ripple control render; Incott has no lighting and
639
+ // no onboard profiles, so those cards stay hidden on their own
640
+ // (nothing populates the fields that gate them).
641
+ showAdvancedSection: true,
642
+ // No command reports link quality.
643
+ hideSignalCard: true,
644
+ defaultDisplayName: name,
645
+ // false only when the DPI and/or polling-rate query failed above (this
646
+ // includes the wired dead-collection fast path, since every query is
647
+ // forced to `null` there too); the app hides the settings grid rather
648
+ // than render the placeholder numbers assigned to `dpi`/`pollingRateHz`
649
+ // below in that case.
650
+ settingsReady: valuesVerified,
651
+ valuesVerified,
652
+ // OpenMouse's device-overview card (App.tsx) hides the battery column
653
+ // outright for a wired connection unless either a real percent is
654
+ // already known OR this flag is set — a sane default for mice with no
655
+ // battery at all while corded, but WRONG here: this mouse has an
656
+ // internal battery it charges over the same USB cable, and until the
657
+ // first unsolicited input report arrives (see the class comment's
658
+ // battery section) `batteryPercent` is genuinely `null` regardless of
659
+ // connection. Without this flag a freshly connected wired unit would
660
+ // show no battery card at all instead of the em dash the app renders
661
+ // for a null percent; with it, the card always shows (em dash first,
662
+ // then the real reading once a report arrives).
663
+ forceShowBattery: true,
664
+ // The mouse cannot accept a rate outside `supportedPollingRates` for
665
+ // its current connection, so hide the ones it would refuse rather than
666
+ // let the shell offer a write that read-back verification would then
667
+ // report as failed.
668
+ hideUnsupportedPollingRates: true,
669
+ pollingNote: wired
670
+ ? "Up to 1,000 Hz over the cable; 8,000 Hz needs the wireless dongle."
671
+ : "Up to 8,000 Hz wireless; 1,000 Hz over the cable.",
672
+ // Set only when open()'s probe found this specific collection dead —
673
+ // see the class comment on `open()`. Wired mice expose a second,
674
+ // identical-looking 0xFF05 collection that never answers; WebHID hands
675
+ // this client one collection at a time and cannot pick between them,
676
+ // so the only remedy today is to reconnect and hope the browser offers
677
+ // the other one.
678
+ ...(dead
679
+ ? { statusNote: "This HID collection never answered — wired mice expose a second, identical-looking collection that does not work. Try reconnecting." }
680
+ : {}),
681
+ // Six FIXED stages (`countEditable: false` hides the count picker —
682
+ // this hardware has no command to add or remove a stage). 45000 is the
683
+ // higher of the two known PixArt sensor ceilings paired in the
684
+ // vendor's own device definition (PAW3395: 32000, PAW3950: 45000);
685
+ // which one is fitted to a given unit cannot currently be read, so a
686
+ // PAW3395 unit is expected to have a write above 32000 refused by the
687
+ // existing read-back verification in `setDpi`/`setDpiStageValue`
688
+ // rather than this module guessing which sensor is present.
689
+ // `countEditable` only while the cycle actually read: the count picker
690
+ // writes through `setDpiStageCount`, which needs a real current count
691
+ // to preserve the active stage, and offering it against an unreadable
692
+ // one would write a guess.
693
+ // The ceiling follows the FITTED SENSOR, which the identity reply
694
+ // reports: the PAW3395 models in this family stop at 32000 in the
695
+ // vendor's own table where the PAW3950 reaches 45000. Falls back to the
696
+ // higher value when identity could not be read, since narrowing on a
697
+ // guess would hide DPI the mouse can actually do.
698
+ dpiStageEditor: { maxStages: INCOTT_DPI_STAGE_COUNT, countEditable: dpiCycle !== null, minDpi: INCOTT_DPI_MIN, maxDpi: incottDpiMaxForSensor(identity?.sensorId ?? null), stepDpi: INCOTT_DPI_STEP },
699
+ };
700
+ return {
701
+ brand: "Incott",
702
+ name,
703
+ ui,
704
+ // A genuine live read (the active stage's own value) when it
705
+ // succeeded. Otherwise this is inert placeholder data, not a
706
+ // fabricated reading: MouseStatus.dpi is non-nullable, so a number must
707
+ // go here, but `ui.settingsReady: false` above means the app never
708
+ // renders it.
709
+ dpi: dpi ?? 0,
710
+ ...(dpiY !== null ? { dpiY, supportsSeparateDpiAxes: true } : {}),
711
+ // Omitted, not nulled, when unreadable or inapplicable: the app's
712
+ // Incott card renders only the fields that are present, so a wired
713
+ // connection simply has no receiver-LED control rather than a dead one.
714
+ ...(receiverLedMode !== null ? { incottReceiverLedMode: receiverLedMode } : {}),
715
+ ...(fireKey !== null
716
+ ? { incottFireKeyTimes: fireKey.times, incottFireKeyIntervalMs: fireKey.intervalMs }
717
+ : {}),
718
+ // All six stages' stored values, only when every one of them answered
719
+ // — see the loop above. Omitted (not fabricated) on a partial read.
720
+ ...(dpiStages !== null ? { dpiStages } : {}),
721
+ ...(activeDpiStage !== null ? { activeDpiStage } : {}),
722
+ // Same reasoning as `dpi` above: a real reading of the polling-rate
723
+ // query (0x81) when it succeeded, otherwise inert placeholder data
724
+ // behind `ui.settingsReady: false`.
725
+ pollingRateHz: pollingRateHz ?? 0,
726
+ supportedPollingRates,
727
+ activeProfile: null,
728
+ connectionType: wired ? "Wired" : "Wireless",
729
+ batteryPercent,
730
+ // Derived from the input report's own charging bit (`batteryCharging`,
731
+ // see above), NOT from `wired`/the product id: `0x622C` reliably means
732
+ // the connection is wired, and being wired does imply charging, but the
733
+ // report — when one has arrived — is the authoritative source for
734
+ // whether the mouse is actually charging right now. "Unknown" is for
735
+ // when no input report has arrived yet, regardless of connection.
736
+ batteryState: batteryCharging === null ? "Unknown" : batteryCharging ? "Charging" : "Discharging",
737
+ liftOffDistance: liftOffTenths === null ? null : incottLiftOffLabel(liftOffTenths),
738
+ supportedLiftOffDistances: ["Low", "Medium", "High"],
739
+ // Both fields together, or neither: the shared remapper only renders
740
+ // when it has the current assignments AND the list of actions it may
741
+ // write back.
742
+ ...(buttonMappings !== null
743
+ ? { buttonMappings, buttonOptions: INCOTT_BUTTON_ACTIONS.map(([label]) => label) }
744
+ : {}),
745
+ motionSync,
746
+ rippleControl,
747
+ angleSnapping,
748
+ debounceMs,
749
+ sleepTimeout,
750
+ // Named power/performance modes (HP/Corded/LP) — see `powerModeWire`
751
+ // above. `powerModes` is only advertised alongside a real `powerMode`
752
+ // reading, matching MCHOSE's identical three-way mode: never claim a
753
+ // set of options exists on a collection that just failed to answer.
754
+ ...(powerMode !== undefined ? { powerMode, powerModes: [...INCOTT_PERFORMANCE_MODE_NAMES] } : {}),
755
+ firmware: [identity ? `Identity ${identity.raw}` : "Identity unavailable"],
756
+ };
757
+ }
758
+ /**
759
+ * Sets the ACTIVE stage's stored DPI value — the only sane meaning left for
760
+ * a plain "set DPI" call now that a real stage-select operation exists (see
761
+ * `setActiveDpiStage` below and the class comment's account of the bug this
762
+ * used to have). This is exactly `setDpiStageValue(activeStage, dpi)`;
763
+ * confirms the write by reading that stage back, the same pattern every
764
+ * other setter in this client uses. Range/step are validated up front,
765
+ * before any device round-trip, so an invalid value never costs the query
766
+ * needed to find the active stage.
767
+ */
768
+ async setDpi(dpi) {
769
+ incottValidateDpi(dpi);
770
+ const stage = (await this.readDpiCycle())?.active ?? null;
771
+ if (stage === null)
772
+ throw new Error("Could not read the active DPI stage to write.");
773
+ await this.write(incottEncodeSetDpi(stage, dpi));
774
+ const got = incottDecodeDpiStage(await this.query(INCOTT_CMD_QUERY_DPI_STAGE_VALUE, stage), stage);
775
+ if (got !== dpi)
776
+ throw new Error(`The mouse kept ${got ?? "an unreadable"} DPI instead of ${dpi}.`);
777
+ return dpi;
778
+ }
779
+ /**
780
+ * Sets both DPI axes on the ACTIVE stage — the shape a generic X/Y control
781
+ * needs, where `setDpiStageAxis` is per-stage and per-axis.
782
+ *
783
+ * Mirrors the vendor's own `setResolution`: one write with the "both" flag
784
+ * when the axes match, two axis-flagged writes when they differ. Named to
785
+ * match the method proposed upstream for a brand-agnostic axis control; if
786
+ * the maintainers settle on a different name this is the one line to
787
+ * rename.
788
+ */
789
+ async setAxisDpi(dpiX, dpiY) {
790
+ incottValidateDpi(dpiX);
791
+ incottValidateDpi(dpiY);
792
+ const stage = (await this.readDpiCycle())?.active ?? null;
793
+ if (stage === null)
794
+ throw new Error("Could not read the active DPI stage to write.");
795
+ if (dpiX === dpiY) {
796
+ await this.setDpiStageAxis(stage, dpiX, "both");
797
+ return;
798
+ }
799
+ await this.setDpiStageAxis(stage, dpiX, "x");
800
+ await this.setDpiStageAxis(stage, dpiY, "y");
801
+ }
802
+ /**
803
+ * Uploads one macro into an on-device buffer.
804
+ *
805
+ * Ten 32-byte chunks, each announced by an 8-byte feature report and then
806
+ * carried by a 32-byte OUTPUT report on the same id — the only place this
807
+ * protocol uses an output report at all. Captured from the vendor tool
808
+ * 2026-09-11; see `captures/incott-8k-wireless/macro-upload-2026-09-11.hex`.
809
+ *
810
+ * The device does not acknowledge any of it, so unlike every other setter
811
+ * here there is nothing to verify against: there is no macro read command.
812
+ * Bind a button to `Macro <n>` and press it — that is the only confirmation
813
+ * available.
814
+ */
815
+ async uploadMacro(macro) {
816
+ const chunks = incottMacroChunks(incottEncodeMacroBuffer(macro));
817
+ for (const [index, chunk] of chunks.entries()) {
818
+ await this.write(incottEncodeMacroChunkHeader(index, macro.bufferId));
819
+ await this.queue.sendOutput(INCOTT_REPORT_ID, chunk);
820
+ }
821
+ }
822
+ /** Reads the DPI cycle (stage count + active stage) in one query. */
823
+ async readDpiCycle() {
824
+ return incottDecodeDpiCycle(await this.query(INCOTT_CMD_QUERY_DPI_STAGE, INCOTT_SUB_NONE));
825
+ }
826
+ /**
827
+ * SELECTS which DPI stage is active (`09 03 <count> <idx>`) — distinct from
828
+ * `setDpi`/`setDpiStageValue`, which EDIT a stage's stored value. Confirmed
829
+ * on hardware 2026-09-08 that a select never touches any stage's stored
830
+ * value (see `INCOTT_CMD_SET_DPI_STAGE` in `src/incott/index.ts`), so
831
+ * unlike every value setter in this client this one reads back the active
832
+ * index rather than a DPI value.
833
+ *
834
+ * The stage count is READ FIRST and written back unchanged. It shares the
835
+ * write with the index, so sending a constant here would silently resize a
836
+ * cycle that is not six stages long — see
837
+ * `INCOTT_DPI_STAGE_COUNT_DEFAULT`.
838
+ */
839
+ async setActiveDpiStage(stage) {
840
+ if (!Number.isInteger(stage) || stage < 0 || stage >= INCOTT_DPI_STAGE_COUNT) {
841
+ throw new RangeError(`DPI stage out of range: ${stage}`);
842
+ }
843
+ const cycle = await this.readDpiCycle();
844
+ if (cycle === null)
845
+ throw new Error("Could not read the DPI stage cycle to write.");
846
+ if (stage >= cycle.count) {
847
+ throw new RangeError(`DPI stage ${stage} is outside this mouse's ${cycle.count}-stage cycle.`);
848
+ }
849
+ await this.write(incottEncodeSetDpiCycle(cycle.count, stage));
850
+ const got = (await this.readDpiCycle())?.active ?? null;
851
+ if (got !== stage)
852
+ throw new Error(`The mouse kept DPI stage ${got ?? "an unreadable"} instead of ${stage}.`);
853
+ return stage;
854
+ }
855
+ /**
856
+ * Sets how many stages the DPI cycle rotates through (`09 03 <count>
857
+ * <idx>`). The stored value of every stage is left alone — stages above
858
+ * the new count keep their values and simply stop being visited.
859
+ *
860
+ * The active stage rides along in the same write, so it is clamped into
861
+ * the new cycle rather than left pointing past the end.
862
+ */
863
+ async setDpiStageCount(count) {
864
+ if (!Number.isInteger(count) || count < 1 || count > INCOTT_DPI_STAGE_COUNT) {
865
+ throw new RangeError(`DPI stage count out of range: ${count}`);
866
+ }
867
+ const cycle = await this.readDpiCycle();
868
+ if (cycle === null)
869
+ throw new Error("Could not read the DPI stage cycle to write.");
870
+ const active = Math.min(cycle.active, count - 1);
871
+ await this.write(incottEncodeSetDpiCycle(count, active));
872
+ const got = await this.readDpiCycle();
873
+ if (got?.count !== count) {
874
+ throw new Error(`The mouse kept ${got?.count ?? "an unreadable"} DPI stages instead of ${count}.`);
875
+ }
876
+ return count;
877
+ }
878
+ /**
879
+ * EDITS one stage's stored DPI value directly, by index — unlike `setDpi`,
880
+ * which always targets whichever stage happens to be active right now.
881
+ * This is the write OpenMouse's shared multi-stage DPI editor uses when the
882
+ * user edits a specific stage row rather than the active-stage preset.
883
+ */
884
+ async setDpiStageValue(stage, dpi) {
885
+ if (!Number.isInteger(stage) || stage < 0 || stage >= INCOTT_DPI_STAGE_COUNT) {
886
+ throw new RangeError(`DPI stage out of range: ${stage}`);
887
+ }
888
+ incottValidateDpi(dpi);
889
+ await this.write(incottEncodeSetDpi(stage, dpi));
890
+ const got = incottDecodeDpiStage(await this.query(INCOTT_CMD_QUERY_DPI_STAGE_VALUE, stage), stage);
891
+ if (got !== dpi)
892
+ throw new Error(`The mouse kept ${got ?? "an unreadable"} DPI instead of ${dpi} on stage ${stage}.`);
893
+ return dpi;
894
+ }
895
+ async setPollingRate(hz) {
896
+ await this.write(incottEncodeSetPollingRate(hz));
897
+ const got = incottDecodePollingRate(await this.query(INCOTT_CMD_QUERY_POLLING, INCOTT_SUB_NONE));
898
+ if (got !== hz)
899
+ throw new Error(`The mouse kept ${got ?? "an unreadable"} Hz instead of ${hz} Hz.`);
900
+ return hz;
901
+ }
902
+ async setLiftOffDistance(level) {
903
+ const tenths = incottLiftOffTenths(level);
904
+ await this.write(incottEncodeSetLiftOff(tenths));
905
+ // Symmetric single-purpose read (`0x84`/`0x01`), preferred over the
906
+ // packed byte-7 nibble form — see the comment in `readStatus()`.
907
+ const gotTenths = incottDecodeLiftOffDirect(await this.query(INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_LOD));
908
+ const got = gotTenths === null ? null : incottLiftOffLabel(gotTenths);
909
+ if (got !== level) {
910
+ throw new Error(`The mouse kept a ${got ?? "unreadable"} lift-off distance instead of ${level}.`);
911
+ }
912
+ return level;
913
+ }
914
+ async setMotionSync(on) {
915
+ await this.write(incottEncodeSetToggle("motionSync", on));
916
+ // Symmetric single-purpose read (`0x84`/`0x04`), preferred over the
917
+ // packed byte-7 nibble form — see the comment in `readStatus()`.
918
+ const got = incottDecodeToggle(await this.query(INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_MOTION_SYNC), INCOTT_SUB_MOTION_SYNC);
919
+ if (got !== on)
920
+ throw new Error(`The mouse kept Motion Sync ${toggleWord(got)} instead of ${on ? "on" : "off"}.`);
921
+ return on;
922
+ }
923
+ async setAngleSnapping(on) {
924
+ await this.write(incottEncodeSetToggle("angleSnapping", on));
925
+ const got = incottDecodeToggle(await this.query(INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_ANGLE_SNAP), INCOTT_SUB_ANGLE_SNAP);
926
+ if (got !== on)
927
+ throw new Error(`The mouse kept angle snapping ${toggleWord(got)} instead of ${on ? "on" : "off"}.`);
928
+ return on;
929
+ }
930
+ async setRippleControl(on) {
931
+ await this.write(incottEncodeSetToggle("rippleControl", on));
932
+ const got = incottDecodeToggle(await this.query(INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_RIPPLE), INCOTT_SUB_RIPPLE);
933
+ if (got !== on)
934
+ throw new Error(`The mouse kept ripple control ${toggleWord(got)} instead of ${on ? "on" : "off"}.`);
935
+ return on;
936
+ }
937
+ async setDebounceTime(ms) {
938
+ await this.write(incottEncodeSetDebounce(ms));
939
+ const got = incottDecodeDebounce(await this.query(INCOTT_CMD_QUERY_TIMING, INCOTT_SUB_DEBOUNCE));
940
+ if (got !== ms)
941
+ throw new Error(`The mouse kept ${got ?? "an unreadable"} ms debounce instead of ${ms} ms.`);
942
+ return ms;
943
+ }
944
+ async setSleepTimeout(seconds) {
945
+ await this.write(incottEncodeSetSleep(seconds));
946
+ const got = incottDecodeSleep(await this.query(INCOTT_CMD_QUERY_TIMING, INCOTT_SUB_SLEEP));
947
+ if (got !== seconds) {
948
+ throw new Error(`The mouse kept a ${got ?? "an unreadable"} s sleep timer instead of ${seconds} s.`);
949
+ }
950
+ return seconds;
951
+ }
952
+ /**
953
+ * Not called generically by the app (no shared receiver-LED control exists
954
+ * yet), kept for protocol parity with IncottHub.
955
+ *
956
+ * NOTE for whoever wires this up: the receiver LED is a property of the
957
+ * 2.4 GHz dongle and is meaningless when `incottIsWiredProduct(device.productId)`
958
+ * is true (checked 2026-09-08) — there is no dongle in that mode. Checked
959
+ * `MouseUiHints` in `src/drivers/mouse-types.ts` for an existing `hide*`
960
+ * flag to gate this with and found none scoped to the receiver LED
961
+ * specifically (the closest, `hideSignalCard`, is about link-quality
962
+ * telemetry, a different control); a new hint was deliberately NOT added to
963
+ * that shared contract for a control this driver does not yet advertise
964
+ * anywhere. Whoever wires a shared receiver-LED control into `MouseStatus`
965
+ * should either add a scoped `hide*` flag there at that time, or simply
966
+ * omit the wiring outright when wired, matching the other wireless-only
967
+ * behavior in this driver (see `INCOTT_POLLING_STEPS_HZ_WIRED`).
968
+ */
969
+ /**
970
+ * Reads all six button bindings, keyed by the physical button name.
971
+ *
972
+ * Returns null unless every button answered: see the call site in
973
+ * `readStatus` for why a partial read is not published. A binding the
974
+ * action table does not know (a keyboard key, a macro) reports its raw code
975
+ * as `Unknown (0x...)` rather than being shown as one of the offered
976
+ * actions — the shared remapper writes back what it displays, so labelling
977
+ * an unknown binding as a known action would rewrite it on the next edit.
978
+ */
979
+ async readButtonMappings() {
980
+ const mappings = {};
981
+ for (const name of INCOTT_BUTTON_NAMES) {
982
+ const index = INCOTT_BUTTON_WIRE_INDEX[name];
983
+ const binding = incottDecodeButtonBinding(await this.query(INCOTT_CMD_QUERY_BUTTON, index), index);
984
+ if (binding === null)
985
+ return null;
986
+ mappings[name] = binding.label ?? `Unknown (0x${binding.code.toString(16).padStart(8, "0")})`;
987
+ }
988
+ return mappings;
989
+ }
990
+ /**
991
+ * Reassigns one button, then reads it back and refuses to report success
992
+ * unless the device actually took the value.
993
+ *
994
+ * `button` is a physical name; the wire index it maps to is NOT the same
995
+ * number (Forward and Back are transposed — see
996
+ * `INCOTT_BUTTON_WIRE_INDEX`). This is a standalone command, so it cannot
997
+ * disturb DPI, polling or the other five buttons.
998
+ */
999
+ async setButtonMapping(button, actionLabel) {
1000
+ const name = INCOTT_BUTTON_NAMES.find((candidate) => candidate === button);
1001
+ if (name === undefined)
1002
+ throw new Error(`This mouse has no "${button}" button.`);
1003
+ const code = incottButtonActionCode(actionLabel);
1004
+ if (code === null)
1005
+ throw new Error(`Unknown button action "${actionLabel}".`);
1006
+ const index = INCOTT_BUTTON_WIRE_INDEX[name];
1007
+ await this.write(incottEncodeSetButtonBinding(index, code));
1008
+ const applied = incottDecodeButtonBinding(await this.query(INCOTT_CMD_QUERY_BUTTON, index), index);
1009
+ if (applied === null)
1010
+ throw new Error("The mouse did not confirm the button change.");
1011
+ if (applied.code !== code) {
1012
+ throw new Error(`The mouse kept ${applied.label ?? "another binding"} on ${name} instead of ${actionLabel}.`);
1013
+ }
1014
+ }
1015
+ /**
1016
+ * Reads the Fire Key (rapid-fire) parameters — how many clicks a button
1017
+ * bound to "Rapid fire" sends, and how far apart. Global to the device: the
1018
+ * command carries no button index.
1019
+ */
1020
+ async getFireKey() {
1021
+ return incottDecodeFireKey(await this.query(INCOTT_CMD_QUERY_TIMING, INCOTT_SUB_FIRE_KEY));
1022
+ }
1023
+ /**
1024
+ * Writes the Fire Key parameters and verifies the read-back.
1025
+ *
1026
+ * Not advertised through `MouseStatus`: the shared contract has no
1027
+ * rapid-fire field, so there is no generic control to publish this behind —
1028
+ * the same position `setReceiverLed` is in. Kept for protocol parity and
1029
+ * for whoever wires a control up.
1030
+ */
1031
+ async setFireKey(times, intervalMs) {
1032
+ await this.write(incottEncodeSetFireKey(times, intervalMs));
1033
+ const got = await this.getFireKey();
1034
+ if (got === null)
1035
+ throw new Error("The mouse did not confirm the fire key change.");
1036
+ if (got.times !== times || got.intervalMs !== intervalMs) {
1037
+ throw new Error(`The mouse kept ${got.times} clicks at ${got.intervalMs} ms instead of ${times} at ${intervalMs} ms.`);
1038
+ }
1039
+ return got;
1040
+ }
1041
+ async setReceiverLed(mode) {
1042
+ await this.write(incottEncodeSetReceiverLed(mode));
1043
+ const got = incottDecodeReceiverLed(await this.query(INCOTT_CMD_QUERY_RECEIVER_LED, INCOTT_SUB_NONE));
1044
+ if (got !== mode)
1045
+ throw new Error(`The mouse kept receiver LED mode ${got ?? "unreadable"} instead of ${mode}.`);
1046
+ return mode;
1047
+ }
1048
+ /**
1049
+ * Low-level codec method: writes the RAW 0-2 performance-mode value. Kept
1050
+ * for protocol parity and as the primitive `setPowerMode` below builds on.
1051
+ * Most callers should use `setPowerMode(name)` instead, which validates a
1052
+ * name against the hardware-confirmed HP/Corded/LP mapping (see
1053
+ * `INCOTT_SUB_PERFORMANCE` in `src/incott/index.ts`) and requires a
1054
+ * matching read-back before reporting success.
1055
+ *
1056
+ * This method is intentionally more lenient than `setPowerMode`: a `null`
1057
+ * read-back (the device not answering) does not throw here, since a raw
1058
+ * numeric call has no name to validate up front and existing callers/tests
1059
+ * rely on this tolerance. See `setPowerMode` for the stricter contract.
1060
+ */
1061
+ async setPerformanceMode(mode) {
1062
+ await this.write(incottEncodeSetPerformanceMode(mode));
1063
+ const got = incottDecodePerformanceMode(await this.query(INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_PERFORMANCE));
1064
+ if (got !== null && got !== mode) {
1065
+ throw new Error(`The mouse kept performance mode ${got} instead of ${mode}.`);
1066
+ }
1067
+ return mode;
1068
+ }
1069
+ /** See `setPerformanceMode`. Returns `null` when `0x84`/`0x05` does not answer. */
1070
+ async getPerformanceMode() {
1071
+ return incottDecodePerformanceMode(await this.query(INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_PERFORMANCE));
1072
+ }
1073
+ /**
1074
+ * The named power/performance modes this mouse offers, in the vendor UI's
1075
+ * own left-to-right display order (HP, Corded, LP) — see
1076
+ * `INCOTT_PERFORMANCE_MODE_NAMES`. This order is deliberately NOT the wire
1077
+ * value order: see `setPowerMode` and `INCOTT_SUB_PERFORMANCE` in
1078
+ * `src/incott/index.ts` for the reversal this table exists to get right.
1079
+ */
1080
+ getPowerModes() {
1081
+ return [...INCOTT_PERFORMANCE_MODE_NAMES];
1082
+ }
1083
+ /**
1084
+ * OpenMouse's shared power/performance-mode contract
1085
+ * (`requireClientMethod("setPowerMode", …)` in
1086
+ * `openmouse/src/device/controller.ts`) — takes the mode NAME, not the raw
1087
+ * wire value. Maps name -> wire through `incottPerformanceModeToWire`
1088
+ * (`INCOTT_PERFORMANCE_MODE_TO_WIRE` in `src/incott/index.ts`), the single
1089
+ * table the HP=2/Corded=1/LP=0 reversal lives in — hardware-confirmed
1090
+ * 2026-09-10 by labelling each click in the vendor tool before recording
1091
+ * its write; see `INCOTT_SUB_PERFORMANCE`'s doc comment.
1092
+ *
1093
+ * Rejects an unknown name BEFORE writing anything (same pattern as
1094
+ * `incottValidateDpi`/`setDpi` above), then writes and requires a matching,
1095
+ * non-null read-back — unlike the lower-level `setPerformanceMode`, a
1096
+ * `null` read-back here is treated as a failure, not tolerated: every
1097
+ * setter in this driver verifies its write, and an unverifiable write must
1098
+ * report failure rather than claim success.
1099
+ */
1100
+ async setPowerMode(name) {
1101
+ const wire = incottPerformanceModeToWire(name);
1102
+ if (wire === null) {
1103
+ throw new Error(`This mouse has no "${name}" performance mode.`);
1104
+ }
1105
+ await this.write(incottEncodeSetPerformanceMode(wire));
1106
+ const got = incottDecodePerformanceMode(await this.query(INCOTT_CMD_QUERY_SENSOR, INCOTT_SUB_PERFORMANCE));
1107
+ if (got === null) {
1108
+ throw new Error(`Could not read back the performance mode to confirm ${name}.`);
1109
+ }
1110
+ if (got !== wire) {
1111
+ throw new Error(`The mouse kept performance mode ${incottPerformanceModeFromWire(got) ?? got} instead of ${name}.`);
1112
+ }
1113
+ }
1114
+ /** Queries return an all-zero frame on failure, which every decoder rejects. */
1115
+ async query(cmd, sub) {
1116
+ const matchSub = SUB_ECHOING_QUERIES.includes(cmd) ? sub : null;
1117
+ return (await this.queue.request(incottEncodeQuery(cmd, sub), cmd, matchSub)) ?? new Uint8Array(INCOTT_RESPONSE_LENGTH);
1118
+ }
1119
+ /**
1120
+ * Reads ONE AXIS of one DPI stage. Separate from `query` because the axis
1121
+ * has to be matched in the reply as well as sent: X and Y on the same stage
1122
+ * are two requests with an identical command and sub-command, so the second
1123
+ * would otherwise accept the first's latched frame.
1124
+ */
1125
+ async queryDpiAxis(stage, axis) {
1126
+ const frame = await this.queue.request(incottEncodeQueryDpiAxis(stage, axis), INCOTT_CMD_QUERY_DPI_STAGE_VALUE, stage, INCOTT_DPI_AXIS[axis]);
1127
+ return frame ?? new Uint8Array(INCOTT_RESPONSE_LENGTH);
1128
+ }
1129
+ /** Reads one axis of one stage, or null when the device does not answer. */
1130
+ async readDpiStageAxis(stage, axis) {
1131
+ if (!Number.isInteger(stage) || stage < 0 || stage >= INCOTT_DPI_STAGE_COUNT) {
1132
+ throw new RangeError(`DPI stage out of range: ${stage}`);
1133
+ }
1134
+ return incottDecodeDpiStageAxis(await this.queryDpiAxis(stage, axis), stage, axis);
1135
+ }
1136
+ /**
1137
+ * Writes one axis of one stage and verifies it by reading that axis back.
1138
+ *
1139
+ * Independent X and Y are real on this device — hardware-verified
1140
+ * 2026-09-11, see `INCOTT_DPI_AXIS`. Writing `"both"` is what the ordinary
1141
+ * `setDpiStageValue` does.
1142
+ */
1143
+ async setDpiStageAxis(stage, dpi, axis) {
1144
+ if (!Number.isInteger(stage) || stage < 0 || stage >= INCOTT_DPI_STAGE_COUNT) {
1145
+ throw new RangeError(`DPI stage out of range: ${stage}`);
1146
+ }
1147
+ incottValidateDpi(dpi);
1148
+ await this.write(incottEncodeSetDpi(stage, dpi, axis));
1149
+ // "both" has no axis of its own to read back; X is the axis it lands on.
1150
+ const readAxis = axis === "both" ? "x" : axis;
1151
+ const got = await this.readDpiStageAxis(stage, readAxis);
1152
+ if (got !== dpi) {
1153
+ throw new Error(`The mouse kept ${got ?? "an unreadable"} ${readAxis.toUpperCase()} DPI instead of ${dpi}.`);
1154
+ }
1155
+ return dpi;
1156
+ }
1157
+ async write(payload) {
1158
+ await this.queue.send(payload);
1159
+ }
1160
+ }
1161
+ /**
1162
+ * WebHID's `sendFeatureReport` types its `data` parameter as `BufferSource`.
1163
+ * TypeScript's DOM lib makes `Uint8Array` generic over its backing buffer
1164
+ * type, defaulting a bare `Uint8Array` annotation to `Uint8Array<ArrayBufferLike>`,
1165
+ * which is not assignable to `BufferSource` (it admits `SharedArrayBuffer`).
1166
+ * Copying into a fresh array-like-constructed Uint8Array narrows the backing
1167
+ * buffer to a concrete `ArrayBuffer`, matching the pattern already used in
1168
+ * `src/drivers/corsair/hid.ts` and `src/drivers/zaunkoenig/hid.ts`.
1169
+ */
1170
+ function toArrayBuffer(payload) {
1171
+ return new Uint8Array(payload).buffer;
1172
+ }
1173
+ //# sourceMappingURL=hid.js.map