├── .gitignore ├── 80-macropad.rules ├── Cargo.lock ├── Cargo.toml ├── README.org ├── images ├── keyboard-12-2.png ├── keyboard-15-3.jpg ├── keyboard-3-1.jpg ├── keyboard-4-0.png ├── keyboard-6-1.png └── keyboard-6-1_2.png ├── mapping.ron └── src ├── config.rs ├── consts.rs ├── decoder.rs ├── keyboard ├── k884x.rs ├── k8890.rs └── mod.rs ├── main.rs ├── mapping.rs ├── options.rs └── parse.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /80-macropad.rules: -------------------------------------------------------------------------------- 1 | ATTRS{idProduct}=="8840", ATTRS{idVendor}=="1189", MODE="666", GROUP="users" 2 | ATTRS{idProduct}=="8842", ATTRS{idVendor}=="1189", MODE="666", GROUP="users" 3 | ATTRS{idProduct}=="8890", ATTRS{idVendor}=="1189", MODE="666", GROUP="users" 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "android-tzdata" 16 | version = "0.1.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 19 | 20 | [[package]] 21 | name = "android_system_properties" 22 | version = "0.1.5" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 25 | dependencies = [ 26 | "libc", 27 | ] 28 | 29 | [[package]] 30 | name = "anstream" 31 | version = "0.6.19" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" 34 | dependencies = [ 35 | "anstyle", 36 | "anstyle-parse", 37 | "anstyle-query", 38 | "anstyle-wincon", 39 | "colorchoice", 40 | "is_terminal_polyfill", 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle" 46 | version = "1.0.11" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 49 | 50 | [[package]] 51 | name = "anstyle-parse" 52 | version = "0.2.7" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 55 | dependencies = [ 56 | "utf8parse", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-query" 61 | version = "1.1.3" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" 64 | dependencies = [ 65 | "windows-sys", 66 | ] 67 | 68 | [[package]] 69 | name = "anstyle-wincon" 70 | version = "3.0.9" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" 73 | dependencies = [ 74 | "anstyle", 75 | "once_cell_polyfill", 76 | "windows-sys", 77 | ] 78 | 79 | [[package]] 80 | name = "anyhow" 81 | version = "1.0.98" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 84 | 85 | [[package]] 86 | name = "autocfg" 87 | version = "1.4.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 90 | 91 | [[package]] 92 | name = "base64" 93 | version = "0.22.1" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 96 | 97 | [[package]] 98 | name = "bitflags" 99 | version = "2.9.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 102 | dependencies = [ 103 | "serde", 104 | ] 105 | 106 | [[package]] 107 | name = "bumpalo" 108 | version = "3.18.1" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" 111 | 112 | [[package]] 113 | name = "cc" 114 | version = "1.2.26" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" 117 | dependencies = [ 118 | "shlex", 119 | ] 120 | 121 | [[package]] 122 | name = "cfg-if" 123 | version = "1.0.0" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 126 | 127 | [[package]] 128 | name = "chrono" 129 | version = "0.4.41" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 132 | dependencies = [ 133 | "android-tzdata", 134 | "iana-time-zone", 135 | "num-traits", 136 | "serde", 137 | "windows-link", 138 | ] 139 | 140 | [[package]] 141 | name = "clap" 142 | version = "4.5.39" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" 145 | dependencies = [ 146 | "clap_builder", 147 | "clap_derive", 148 | ] 149 | 150 | [[package]] 151 | name = "clap_builder" 152 | version = "4.5.39" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" 155 | dependencies = [ 156 | "anstream", 157 | "anstyle", 158 | "clap_lex", 159 | "strsim", 160 | ] 161 | 162 | [[package]] 163 | name = "clap_derive" 164 | version = "4.5.32" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 167 | dependencies = [ 168 | "heck", 169 | "proc-macro2", 170 | "quote", 171 | "syn", 172 | ] 173 | 174 | [[package]] 175 | name = "clap_lex" 176 | version = "0.7.4" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 179 | 180 | [[package]] 181 | name = "colorchoice" 182 | version = "1.0.4" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 185 | 186 | [[package]] 187 | name = "core-foundation-sys" 188 | version = "0.8.7" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 191 | 192 | [[package]] 193 | name = "darling" 194 | version = "0.20.11" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" 197 | dependencies = [ 198 | "darling_core", 199 | "darling_macro", 200 | ] 201 | 202 | [[package]] 203 | name = "darling_core" 204 | version = "0.20.11" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" 207 | dependencies = [ 208 | "fnv", 209 | "ident_case", 210 | "proc-macro2", 211 | "quote", 212 | "strsim", 213 | "syn", 214 | ] 215 | 216 | [[package]] 217 | name = "darling_macro" 218 | version = "0.20.11" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" 221 | dependencies = [ 222 | "darling_core", 223 | "quote", 224 | "syn", 225 | ] 226 | 227 | [[package]] 228 | name = "deranged" 229 | version = "0.4.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 232 | dependencies = [ 233 | "powerfmt", 234 | "serde", 235 | ] 236 | 237 | [[package]] 238 | name = "either" 239 | version = "1.15.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 242 | 243 | [[package]] 244 | name = "enumset" 245 | version = "1.1.6" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "11a6b7c3d347de0a9f7bfd2f853be43fe32fa6fac30c70f6d6d67a1e936b87ee" 248 | dependencies = [ 249 | "enumset_derive", 250 | ] 251 | 252 | [[package]] 253 | name = "enumset_derive" 254 | version = "0.11.0" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "6da3ea9e1d1a3b1593e15781f930120e72aa7501610b2f82e5b6739c72e8eac5" 257 | dependencies = [ 258 | "darling", 259 | "proc-macro2", 260 | "quote", 261 | "syn", 262 | ] 263 | 264 | [[package]] 265 | name = "env_filter" 266 | version = "0.1.3" 267 | source = "registry+https://github.com/rust-lang/crates.io-index" 268 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 269 | dependencies = [ 270 | "log", 271 | "regex", 272 | ] 273 | 274 | [[package]] 275 | name = "env_logger" 276 | version = "0.11.8" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 279 | dependencies = [ 280 | "anstream", 281 | "anstyle", 282 | "env_filter", 283 | "jiff", 284 | "log", 285 | ] 286 | 287 | [[package]] 288 | name = "equivalent" 289 | version = "1.0.2" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 292 | 293 | [[package]] 294 | name = "fnv" 295 | version = "1.0.7" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 298 | 299 | [[package]] 300 | name = "hashbrown" 301 | version = "0.12.3" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 304 | 305 | [[package]] 306 | name = "hashbrown" 307 | version = "0.15.4" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" 310 | 311 | [[package]] 312 | name = "heck" 313 | version = "0.5.0" 314 | source = "registry+https://github.com/rust-lang/crates.io-index" 315 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 316 | 317 | [[package]] 318 | name = "hex" 319 | version = "0.4.3" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 322 | 323 | [[package]] 324 | name = "iana-time-zone" 325 | version = "0.1.63" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 328 | dependencies = [ 329 | "android_system_properties", 330 | "core-foundation-sys", 331 | "iana-time-zone-haiku", 332 | "js-sys", 333 | "log", 334 | "wasm-bindgen", 335 | "windows-core", 336 | ] 337 | 338 | [[package]] 339 | name = "iana-time-zone-haiku" 340 | version = "0.1.2" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 343 | dependencies = [ 344 | "cc", 345 | ] 346 | 347 | [[package]] 348 | name = "ident_case" 349 | version = "1.0.1" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 352 | 353 | [[package]] 354 | name = "indexmap" 355 | version = "1.9.3" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 358 | dependencies = [ 359 | "autocfg", 360 | "hashbrown 0.12.3", 361 | "serde", 362 | ] 363 | 364 | [[package]] 365 | name = "indexmap" 366 | version = "2.9.0" 367 | source = "registry+https://github.com/rust-lang/crates.io-index" 368 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 369 | dependencies = [ 370 | "equivalent", 371 | "hashbrown 0.15.4", 372 | "serde", 373 | ] 374 | 375 | [[package]] 376 | name = "indoc" 377 | version = "2.0.6" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" 380 | 381 | [[package]] 382 | name = "is_terminal_polyfill" 383 | version = "1.70.1" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 386 | 387 | [[package]] 388 | name = "itertools" 389 | version = "0.14.0" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 392 | dependencies = [ 393 | "either", 394 | ] 395 | 396 | [[package]] 397 | name = "itoa" 398 | version = "1.0.15" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 401 | 402 | [[package]] 403 | name = "jiff" 404 | version = "0.2.14" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" 407 | dependencies = [ 408 | "jiff-static", 409 | "log", 410 | "portable-atomic", 411 | "portable-atomic-util", 412 | "serde", 413 | ] 414 | 415 | [[package]] 416 | name = "jiff-static" 417 | version = "0.2.14" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" 420 | dependencies = [ 421 | "proc-macro2", 422 | "quote", 423 | "syn", 424 | ] 425 | 426 | [[package]] 427 | name = "js-sys" 428 | version = "0.3.77" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 431 | dependencies = [ 432 | "once_cell", 433 | "wasm-bindgen", 434 | ] 435 | 436 | [[package]] 437 | name = "libc" 438 | version = "0.2.172" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 441 | 442 | [[package]] 443 | name = "libusb1-sys" 444 | version = "0.7.0" 445 | source = "registry+https://github.com/rust-lang/crates.io-index" 446 | checksum = "da050ade7ac4ff1ba5379af847a10a10a8e284181e060105bf8d86960ce9ce0f" 447 | dependencies = [ 448 | "cc", 449 | "libc", 450 | "pkg-config", 451 | "vcpkg", 452 | ] 453 | 454 | [[package]] 455 | name = "log" 456 | version = "0.4.27" 457 | source = "registry+https://github.com/rust-lang/crates.io-index" 458 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 459 | 460 | [[package]] 461 | name = "macropad-tool" 462 | version = "0.0.1" 463 | dependencies = [ 464 | "anyhow", 465 | "clap", 466 | "enumset", 467 | "env_logger", 468 | "indoc", 469 | "itertools", 470 | "log", 471 | "nom", 472 | "num", 473 | "num-derive", 474 | "num-traits", 475 | "ron", 476 | "rusb", 477 | "serde", 478 | "serde_with", 479 | "serde_yaml", 480 | "strum", 481 | "strum_macros", 482 | ] 483 | 484 | [[package]] 485 | name = "memchr" 486 | version = "2.7.4" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 489 | 490 | [[package]] 491 | name = "nom" 492 | version = "8.0.0" 493 | source = "registry+https://github.com/rust-lang/crates.io-index" 494 | checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" 495 | dependencies = [ 496 | "memchr", 497 | ] 498 | 499 | [[package]] 500 | name = "num" 501 | version = "0.4.3" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" 504 | dependencies = [ 505 | "num-bigint", 506 | "num-complex", 507 | "num-integer", 508 | "num-iter", 509 | "num-rational", 510 | "num-traits", 511 | ] 512 | 513 | [[package]] 514 | name = "num-bigint" 515 | version = "0.4.6" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" 518 | dependencies = [ 519 | "num-integer", 520 | "num-traits", 521 | ] 522 | 523 | [[package]] 524 | name = "num-complex" 525 | version = "0.4.6" 526 | source = "registry+https://github.com/rust-lang/crates.io-index" 527 | checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" 528 | dependencies = [ 529 | "num-traits", 530 | ] 531 | 532 | [[package]] 533 | name = "num-conv" 534 | version = "0.1.0" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 537 | 538 | [[package]] 539 | name = "num-derive" 540 | version = "0.4.2" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" 543 | dependencies = [ 544 | "proc-macro2", 545 | "quote", 546 | "syn", 547 | ] 548 | 549 | [[package]] 550 | name = "num-integer" 551 | version = "0.1.46" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 554 | dependencies = [ 555 | "num-traits", 556 | ] 557 | 558 | [[package]] 559 | name = "num-iter" 560 | version = "0.1.45" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 563 | dependencies = [ 564 | "autocfg", 565 | "num-integer", 566 | "num-traits", 567 | ] 568 | 569 | [[package]] 570 | name = "num-rational" 571 | version = "0.4.2" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 574 | dependencies = [ 575 | "num-bigint", 576 | "num-integer", 577 | "num-traits", 578 | ] 579 | 580 | [[package]] 581 | name = "num-traits" 582 | version = "0.2.19" 583 | source = "registry+https://github.com/rust-lang/crates.io-index" 584 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 585 | dependencies = [ 586 | "autocfg", 587 | ] 588 | 589 | [[package]] 590 | name = "once_cell" 591 | version = "1.21.3" 592 | source = "registry+https://github.com/rust-lang/crates.io-index" 593 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 594 | 595 | [[package]] 596 | name = "once_cell_polyfill" 597 | version = "1.70.1" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 600 | 601 | [[package]] 602 | name = "pkg-config" 603 | version = "0.3.32" 604 | source = "registry+https://github.com/rust-lang/crates.io-index" 605 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 606 | 607 | [[package]] 608 | name = "portable-atomic" 609 | version = "1.11.1" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 612 | 613 | [[package]] 614 | name = "portable-atomic-util" 615 | version = "0.2.4" 616 | source = "registry+https://github.com/rust-lang/crates.io-index" 617 | checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" 618 | dependencies = [ 619 | "portable-atomic", 620 | ] 621 | 622 | [[package]] 623 | name = "powerfmt" 624 | version = "0.2.0" 625 | source = "registry+https://github.com/rust-lang/crates.io-index" 626 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 627 | 628 | [[package]] 629 | name = "proc-macro2" 630 | version = "1.0.95" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 633 | dependencies = [ 634 | "unicode-ident", 635 | ] 636 | 637 | [[package]] 638 | name = "quote" 639 | version = "1.0.40" 640 | source = "registry+https://github.com/rust-lang/crates.io-index" 641 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 642 | dependencies = [ 643 | "proc-macro2", 644 | ] 645 | 646 | [[package]] 647 | name = "regex" 648 | version = "1.11.1" 649 | source = "registry+https://github.com/rust-lang/crates.io-index" 650 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 651 | dependencies = [ 652 | "aho-corasick", 653 | "memchr", 654 | "regex-automata", 655 | "regex-syntax", 656 | ] 657 | 658 | [[package]] 659 | name = "regex-automata" 660 | version = "0.4.9" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 663 | dependencies = [ 664 | "aho-corasick", 665 | "memchr", 666 | "regex-syntax", 667 | ] 668 | 669 | [[package]] 670 | name = "regex-syntax" 671 | version = "0.8.5" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 674 | 675 | [[package]] 676 | name = "ron" 677 | version = "0.10.1" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "beceb6f7bf81c73e73aeef6dd1356d9a1b2b4909e1f0fc3e59b034f9572d7b7f" 680 | dependencies = [ 681 | "base64", 682 | "bitflags", 683 | "serde", 684 | "serde_derive", 685 | "unicode-ident", 686 | ] 687 | 688 | [[package]] 689 | name = "rusb" 690 | version = "0.9.4" 691 | source = "registry+https://github.com/rust-lang/crates.io-index" 692 | checksum = "ab9f9ff05b63a786553a4c02943b74b34a988448671001e9a27e2f0565cc05a4" 693 | dependencies = [ 694 | "libc", 695 | "libusb1-sys", 696 | ] 697 | 698 | [[package]] 699 | name = "rustversion" 700 | version = "1.0.21" 701 | source = "registry+https://github.com/rust-lang/crates.io-index" 702 | checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 703 | 704 | [[package]] 705 | name = "ryu" 706 | version = "1.0.20" 707 | source = "registry+https://github.com/rust-lang/crates.io-index" 708 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 709 | 710 | [[package]] 711 | name = "serde" 712 | version = "1.0.219" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 715 | dependencies = [ 716 | "serde_derive", 717 | ] 718 | 719 | [[package]] 720 | name = "serde_derive" 721 | version = "1.0.219" 722 | source = "registry+https://github.com/rust-lang/crates.io-index" 723 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 724 | dependencies = [ 725 | "proc-macro2", 726 | "quote", 727 | "syn", 728 | ] 729 | 730 | [[package]] 731 | name = "serde_json" 732 | version = "1.0.140" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 735 | dependencies = [ 736 | "itoa", 737 | "memchr", 738 | "ryu", 739 | "serde", 740 | ] 741 | 742 | [[package]] 743 | name = "serde_with" 744 | version = "3.12.0" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" 747 | dependencies = [ 748 | "base64", 749 | "chrono", 750 | "hex", 751 | "indexmap 1.9.3", 752 | "indexmap 2.9.0", 753 | "serde", 754 | "serde_derive", 755 | "serde_json", 756 | "serde_with_macros", 757 | "time", 758 | ] 759 | 760 | [[package]] 761 | name = "serde_with_macros" 762 | version = "3.12.0" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" 765 | dependencies = [ 766 | "darling", 767 | "proc-macro2", 768 | "quote", 769 | "syn", 770 | ] 771 | 772 | [[package]] 773 | name = "serde_yaml" 774 | version = "0.9.34+deprecated" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" 777 | dependencies = [ 778 | "indexmap 2.9.0", 779 | "itoa", 780 | "ryu", 781 | "serde", 782 | "unsafe-libyaml", 783 | ] 784 | 785 | [[package]] 786 | name = "shlex" 787 | version = "1.3.0" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 790 | 791 | [[package]] 792 | name = "strsim" 793 | version = "0.11.1" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 796 | 797 | [[package]] 798 | name = "strum" 799 | version = "0.27.1" 800 | source = "registry+https://github.com/rust-lang/crates.io-index" 801 | checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" 802 | 803 | [[package]] 804 | name = "strum_macros" 805 | version = "0.27.1" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" 808 | dependencies = [ 809 | "heck", 810 | "proc-macro2", 811 | "quote", 812 | "rustversion", 813 | "syn", 814 | ] 815 | 816 | [[package]] 817 | name = "syn" 818 | version = "2.0.101" 819 | source = "registry+https://github.com/rust-lang/crates.io-index" 820 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 821 | dependencies = [ 822 | "proc-macro2", 823 | "quote", 824 | "unicode-ident", 825 | ] 826 | 827 | [[package]] 828 | name = "time" 829 | version = "0.3.41" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" 832 | dependencies = [ 833 | "deranged", 834 | "itoa", 835 | "num-conv", 836 | "powerfmt", 837 | "serde", 838 | "time-core", 839 | "time-macros", 840 | ] 841 | 842 | [[package]] 843 | name = "time-core" 844 | version = "0.1.4" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" 847 | 848 | [[package]] 849 | name = "time-macros" 850 | version = "0.2.22" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" 853 | dependencies = [ 854 | "num-conv", 855 | "time-core", 856 | ] 857 | 858 | [[package]] 859 | name = "unicode-ident" 860 | version = "1.0.18" 861 | source = "registry+https://github.com/rust-lang/crates.io-index" 862 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 863 | 864 | [[package]] 865 | name = "unsafe-libyaml" 866 | version = "0.2.11" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" 869 | 870 | [[package]] 871 | name = "utf8parse" 872 | version = "0.2.2" 873 | source = "registry+https://github.com/rust-lang/crates.io-index" 874 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 875 | 876 | [[package]] 877 | name = "vcpkg" 878 | version = "0.2.15" 879 | source = "registry+https://github.com/rust-lang/crates.io-index" 880 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 881 | 882 | [[package]] 883 | name = "wasm-bindgen" 884 | version = "0.2.100" 885 | source = "registry+https://github.com/rust-lang/crates.io-index" 886 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 887 | dependencies = [ 888 | "cfg-if", 889 | "once_cell", 890 | "rustversion", 891 | "wasm-bindgen-macro", 892 | ] 893 | 894 | [[package]] 895 | name = "wasm-bindgen-backend" 896 | version = "0.2.100" 897 | source = "registry+https://github.com/rust-lang/crates.io-index" 898 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 899 | dependencies = [ 900 | "bumpalo", 901 | "log", 902 | "proc-macro2", 903 | "quote", 904 | "syn", 905 | "wasm-bindgen-shared", 906 | ] 907 | 908 | [[package]] 909 | name = "wasm-bindgen-macro" 910 | version = "0.2.100" 911 | source = "registry+https://github.com/rust-lang/crates.io-index" 912 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 913 | dependencies = [ 914 | "quote", 915 | "wasm-bindgen-macro-support", 916 | ] 917 | 918 | [[package]] 919 | name = "wasm-bindgen-macro-support" 920 | version = "0.2.100" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 923 | dependencies = [ 924 | "proc-macro2", 925 | "quote", 926 | "syn", 927 | "wasm-bindgen-backend", 928 | "wasm-bindgen-shared", 929 | ] 930 | 931 | [[package]] 932 | name = "wasm-bindgen-shared" 933 | version = "0.2.100" 934 | source = "registry+https://github.com/rust-lang/crates.io-index" 935 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 936 | dependencies = [ 937 | "unicode-ident", 938 | ] 939 | 940 | [[package]] 941 | name = "windows-core" 942 | version = "0.61.2" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 945 | dependencies = [ 946 | "windows-implement", 947 | "windows-interface", 948 | "windows-link", 949 | "windows-result", 950 | "windows-strings", 951 | ] 952 | 953 | [[package]] 954 | name = "windows-implement" 955 | version = "0.60.0" 956 | source = "registry+https://github.com/rust-lang/crates.io-index" 957 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 958 | dependencies = [ 959 | "proc-macro2", 960 | "quote", 961 | "syn", 962 | ] 963 | 964 | [[package]] 965 | name = "windows-interface" 966 | version = "0.59.1" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 969 | dependencies = [ 970 | "proc-macro2", 971 | "quote", 972 | "syn", 973 | ] 974 | 975 | [[package]] 976 | name = "windows-link" 977 | version = "0.1.1" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" 980 | 981 | [[package]] 982 | name = "windows-result" 983 | version = "0.3.4" 984 | source = "registry+https://github.com/rust-lang/crates.io-index" 985 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 986 | dependencies = [ 987 | "windows-link", 988 | ] 989 | 990 | [[package]] 991 | name = "windows-strings" 992 | version = "0.4.2" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 995 | dependencies = [ 996 | "windows-link", 997 | ] 998 | 999 | [[package]] 1000 | name = "windows-sys" 1001 | version = "0.59.0" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 1004 | dependencies = [ 1005 | "windows-targets", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "windows-targets" 1010 | version = "0.52.6" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 1013 | dependencies = [ 1014 | "windows_aarch64_gnullvm", 1015 | "windows_aarch64_msvc", 1016 | "windows_i686_gnu", 1017 | "windows_i686_gnullvm", 1018 | "windows_i686_msvc", 1019 | "windows_x86_64_gnu", 1020 | "windows_x86_64_gnullvm", 1021 | "windows_x86_64_msvc", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "windows_aarch64_gnullvm" 1026 | version = "0.52.6" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1029 | 1030 | [[package]] 1031 | name = "windows_aarch64_msvc" 1032 | version = "0.52.6" 1033 | source = "registry+https://github.com/rust-lang/crates.io-index" 1034 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1035 | 1036 | [[package]] 1037 | name = "windows_i686_gnu" 1038 | version = "0.52.6" 1039 | source = "registry+https://github.com/rust-lang/crates.io-index" 1040 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1041 | 1042 | [[package]] 1043 | name = "windows_i686_gnullvm" 1044 | version = "0.52.6" 1045 | source = "registry+https://github.com/rust-lang/crates.io-index" 1046 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1047 | 1048 | [[package]] 1049 | name = "windows_i686_msvc" 1050 | version = "0.52.6" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1053 | 1054 | [[package]] 1055 | name = "windows_x86_64_gnu" 1056 | version = "0.52.6" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1059 | 1060 | [[package]] 1061 | name = "windows_x86_64_gnullvm" 1062 | version = "0.52.6" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1065 | 1066 | [[package]] 1067 | name = "windows_x86_64_msvc" 1068 | version = "0.52.6" 1069 | source = "registry+https://github.com/rust-lang/crates.io-index" 1070 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1071 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "macropad-tool" 3 | version = "0.0.1" 4 | edition = "2021" 5 | description = "Tool for programming macropad" 6 | 7 | [dependencies] 8 | anyhow = "1.0.98" 9 | clap = { version = "4.5.39", features = ["derive"] } 10 | enumset = "1.1.6" 11 | env_logger = "0.11.8" 12 | indoc = "2.0.6" 13 | itertools = "0.14.0" 14 | log = "0.4.27" 15 | nom = "8.0.0" 16 | num = "0.4.3" 17 | num-derive = "0.4.2" 18 | num-traits = "0.2.19" 19 | ron = "0.10.1" 20 | rusb = "0.9.4" 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde_with = "3.12.0" 23 | serde_yaml = "0.9.34" 24 | strum = "0.27.1" 25 | strum_macros = "0.27.1" 26 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+title: Readme 2 | 3 | * Contents :toc: 4 | - [[#project-description][Project Description]] 5 | - [[#note][Note]] 6 | - [[#donations-welcome][DONATIONS WELCOME]] 7 | - [[#installation][Installation]] 8 | - [[#configuration-mappings][Configuration Mappings]] 9 | - [[#orientation][Orientation]] 10 | - [[#macropad-setup][Macropad Setup]] 11 | - [[#layers][Layers]] 12 | - [[#usage][Usage]] 13 | - [[#udev-rules-for-linux][udev rules for linux]] 14 | - [[#supported-keys][Supported keys]] 15 | - [[#validate-configuration][Validate configuration]] 16 | - [[#program-the-keyboard][Program the keyboard]] 17 | - [[#led-support][LED Support]] 18 | - [[#windows][Windows]] 19 | - [[#compiling][Compiling]] 20 | - [[#running-the-application][Running the Application]] 21 | 22 | * Project Description 23 | 24 | ** Note 25 | This started as a fun project to learn about rust and USB. It is a hobby project and changes/features are implemented 26 | as my free time permits. Please use caution before using this software as I assume no responsibility if it does not work or 27 | causes problems with your device. 28 | 29 | ** DONATIONS WELCOME 30 | [[https://www.buymeacoffee.com/kamaaina][https://cdn.buymeacoffee.com/buttons/default-orange.png]] 31 | 32 | Obviously I do not have all types of macropad nor do I intend to purchase them. However, if people want to donate macropads so 33 | support can be added for more devices, that is a big way to help grow this project. 34 | 35 | This is an utility for programming small keyboards like this one: 36 | 37 | [[images/keyboard-6-1.png]] 38 | [[images/keyboard-12-2.png]] 39 | [[images/keyboard-3-1.jpg]] 40 | [[images/keyboard-4-0.png]] 41 | [[images/keyboard-6-1_2.png]] 42 | [[images/keyboard-15-3.jpg]] 43 | 44 | Many of these keyboards are popular on AliExpress and Amazon, but the seller usually makes you 45 | download a windows exe file from a google drive account to program it. It also assumes 46 | - you use Windows (which I do not) 47 | - is clunky/shady (imho) 48 | - most importantly does not expose all keyboard features 49 | 50 | * Installation 51 | Clone the repository and build the tool 52 | 53 | #+begin_example 54 | cargo build --release 55 | #+end_example 56 | 57 | * Configuration Mappings 58 | The README.md file is written in emacs org mode. To get a sample configuration file, just tangle this file (C-c C-v t) from within emacs 59 | 60 | ** Orientation 61 | 62 | Normal macropad orienation is when buttons are on the left 63 | side and rotary encoders are on the right. However, you may want to use 64 | the macropad in another orienation. To avoid remapping button 65 | positions in your head, just set it here. 66 | 67 | Possible values are: 68 | (horizontal) 69 | - Normal: buttons on the left, rotary encoders on the right 70 | - UpsideDown: buttons on the right, rotary encoders on the left 71 | (vertical) 72 | - Clockwise: buttons on the top, rotary encoders on the bottom 73 | - CounterClockwise: buttons on the bottom, rotary encoders on the top 74 | 75 | #+begin_src ron 76 | ( 77 | device: ( 78 | // Normal, Clockwise, CounterClockwise, UpsideDown 79 | orientation: Normal, 80 | #+end_src 81 | 82 | ** Macropad Setup 83 | 84 | There are different models of macropad with different numbers 85 | of buttons and rotary encoders. Set it here for proper handling. 86 | Count rows and columns with the macropad in normal orienation, 87 | with rotary encoders on the right side. 88 | 89 | #+begin_src ron 90 | rows: 3, 91 | cols: 4, 92 | knobs: 2, 93 | ), 94 | #+end_src 95 | 96 | ** Layers 97 | 98 | The current layer is changed using a button on the side of the macropad 99 | and displayed with LEDs on top (only for the moment of changing). 100 | All macropads I saw had three layers 101 | 102 | #+begin_src ron 103 | layers: [ 104 | ( 105 | #+end_src 106 | 107 | *** Buttons 108 | Array of buttons. In horizontal orienations it's `rows` rows 109 | `columns` buttons each. In vertical: `columns` rows 110 | `rows` buttons 111 | 112 | Each entry is either a sequence of keychords or a mouse event. 113 | A keychord is a combination of one key with optional modifiers, 114 | like 'b', 'ctrl-alt-a' or 'win-rctrl-backspace'. It can also 115 | be just modifiers without a key: 'ctrl-alt' 116 | 117 | You can combine up to 17 chords into a sequence using commas: 'ctrl-c,ctrl-v' 118 | 119 | If you have a 0x884x product id, you can use the delay feature. This puts a delay between each key sequence. In the example below, 120 | when typeing out 'foo@bar.com' it will insert a 1000 msec delay between each keystroke. the maximum delay is 6000 msec. For all other product 121 | id's, the software will ignore the delay value when programming the macropad 122 | 123 | #+begin_src ron 124 | buttons: [ 125 | [(delay: 0, mapping: "ctrl-b"), (delay: 0, mapping: "ctrl-leftbracket"), (delay: 0, mapping: "ctrl-m") (delay: 0, mapping: "d")], 126 | [(delay: 0, mapping: "ctrl-e"), (delay: 0, mapping: "ctrl-rightbracket"), (delay: 0, mapping: "ctrl-slash"), (delay: 0, mapping: "d")], 127 | [(delay: 0, mapping: "space"), (delay: 1000, mapping: "f,o,o,shift-2,b,a,r,dot,c,o,m"), (delay: 0, mapping: "shift-p"), (delay: 0, mapping: "d")], 128 | #+end_src 129 | 130 | *** Rotary Encoders 131 | 132 | Rotary encoders (aka knobs) are listed from left to right if horizontal 133 | and from top to bottom if vertical. They can be rotated counter-clockwise (ccw) or clockwise (cw) 134 | and pressed down like a button 135 | 136 | #+begin_src ron 137 | knobs: [ 138 | (ccw: (delay: 0, mapping: "3"), press: (delay: 0, mapping: "3"), cw: (delay: 0, mapping: "3")), 139 | (ccw: (delay: 0, mapping: "volumedown"), press: (delay: 0, mapping: "mute"), cw: (delay: 0, mapping: "volumeup")), 140 | ], 141 | ), 142 | #+end_src 143 | 144 | *** Mouse Events 145 | 146 | Mouse events are clicks ('click', 'rclick', 'mclick') or 147 | wheel events ('wheelup', 'wheeldown') with one optional modifier, 148 | only 'ctrl', 'shift' and 'alt' are supported ('ctrl-wheeldown') 149 | Clicks may combine several buttons, like this: 'click-rclick' 150 | 151 | #+begin_src ron 152 | ( 153 | buttons: [ 154 | [(delay: 0, mapping: "click"), (delay: 0, mapping: "mclick"), (delay: 0, mapping: "rclick"), (delay: 0, mapping: "d")], 155 | [(delay: 0, mapping: "wheelup"), (delay: 0, mapping: "wheeldown"), (delay: 0, mapping: "space"), (delay: 0, mapping: "d")], 156 | [(delay: 0, mapping: "ctrl-wheelup"), (delay: 0, mapping: "ctrl-wheeldown"), (delay: 0, mapping: "right"), (delay: 0, mapping: "d")], 157 | ], 158 | knobs: [ 159 | (ccw: (delay: 0, mapping: "3"), press: (delay: 0, mapping: "3"), cw: (delay: 0, mapping: "3")), 160 | (ccw: (delay: 0, mapping: "volumedown"), press: (delay: 0, mapping: "mute"), cw: (delay: 0, mapping: "volumeup")), 161 | ], 162 | ), 163 | #+end_src 164 | 165 | *** Multimedia Support 166 | 167 | Multimedia commands are also supported. Howerver, they cannot be mixed with normal keys and modifiers 168 | 169 | #+begin_src ron 170 | ( 171 | buttons: [ 172 | [(delay: 0, mapping: "ctrl-m"), (delay: 0, mapping: "ctrl-slash"), (delay: 0, mapping: "space"), (delay: 0, mapping: "p")], 173 | [(delay: 0, mapping: "volumeup"), (delay: 0, mapping: "volumedown"), (delay: 0, mapping: "play"), (delay: 0, mapping: "next")], 174 | [(delay: 0, mapping: "ctrl-rightbracket"), (delay: 0, mapping: "ctrl-leftbracket"), (delay: 0, mapping: "right"), (delay: 0, mapping: "left")], 175 | ], 176 | knobs: [ 177 | (ccw: (delay: 0, mapping: "3"), press: (delay: 0, mapping: "3"), cw: (delay: 0, mapping: "3")), 178 | (ccw: (delay: 0, mapping: "volumedown"), press: (delay: 0, mapping: "mute"), cw: (delay: 0, mapping: "volumeup")), 179 | ], 180 | ), 181 | ], 182 | ) 183 | #+end_src 184 | 185 | * Usage 186 | 187 | ** udev rules for linux 188 | To access the device without being root, copy the 80-macropad.rules to /etc/udev/rules.d and reload udev 189 | 190 | #+begin_example 191 | sudo cp 80-macropad.rules /etc/udev/rules.d 192 | sudo udevadm trigger 193 | #+end_example 194 | 195 | ** Supported keys 196 | A list of supported keys can be found by running 197 | 198 | #+begin_example 199 | macropad-tool show-keys 200 | #+end_example 201 | 202 | ** Validate configuration 203 | 204 | #+begin_example 205 | macropad-tool validate -h 206 | macropad-tool validate # by default looks for a mapping.ron file 207 | macropad-tool validate -c # to specify a different configuration file 208 | #+end_example 209 | 210 | ** Program the keyboard 211 | Needs root access or ensure udev rules was added. For Windows, need Administrator command prompt 212 | 213 | #+begin_example 214 | macropad-tool program -h 215 | macropad-tool program # by defult looks for a mapping.ron file 216 | macropad-tool program -c # to specify a different configuration file 217 | #+end_example 218 | 219 | ** LED Support 220 | Some keyboards support LEDs and you can program the different modes via the led command 221 | 222 | #+begin_example 223 | macropad-tool led # Only for 884x model 224 | macropad-tool led 1 1 red 225 | macropad-tool led -h # the help menu about different modes/colors 226 | #+end_example 227 | 228 | * Windows 229 | 230 | ** Compiling 231 | Installing rust with the installer prompts to install visual studio community edition (which is free) and is sufficient to build the executable 232 | 233 | ** Running the Application 234 | - You will need to install the USB Development Kit to be able to talk to the macropad. https://github.com/daynix/UsbDk/releases 235 | -------------------------------------------------------------------------------- /images/keyboard-12-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamaaina/macropad_tool/4fbdc43e4e01ad89b5370680338811a22accd273/images/keyboard-12-2.png -------------------------------------------------------------------------------- /images/keyboard-15-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamaaina/macropad_tool/4fbdc43e4e01ad89b5370680338811a22accd273/images/keyboard-15-3.jpg -------------------------------------------------------------------------------- /images/keyboard-3-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamaaina/macropad_tool/4fbdc43e4e01ad89b5370680338811a22accd273/images/keyboard-3-1.jpg -------------------------------------------------------------------------------- /images/keyboard-4-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamaaina/macropad_tool/4fbdc43e4e01ad89b5370680338811a22accd273/images/keyboard-4-0.png -------------------------------------------------------------------------------- /images/keyboard-6-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamaaina/macropad_tool/4fbdc43e4e01ad89b5370680338811a22accd273/images/keyboard-6-1.png -------------------------------------------------------------------------------- /images/keyboard-6-1_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kamaaina/macropad_tool/4fbdc43e4e01ad89b5370680338811a22accd273/images/keyboard-6-1_2.png -------------------------------------------------------------------------------- /mapping.ron: -------------------------------------------------------------------------------- 1 | ( 2 | device: ( 3 | // Normal, Clockwise, CounterClockwise, UpsideDown 4 | orientation: Normal, 5 | rows: 2, 6 | cols: 3, 7 | knobs: 1, 8 | ), 9 | layers: [ 10 | // layer 1 11 | ( 12 | buttons: [ 13 | [(delay: 0, mapping: "ctrl-a,ctrl-s"), (delay: 0, mapping:"b"), (delay: 0, mapping: "c")], 14 | [(delay: 0, mapping: "space"), (delay: 0, mapping: "f"), (delay: 0, mapping: "g")], 15 | ], 16 | knobs: [ 17 | (ccw: (delay: 0, mapping: "volumedown"), press: (delay: 0, mapping: "mute"), cw: (delay: 0, mapping: "volumeup")), 18 | ], 19 | ), 20 | // layer 2 21 | ( 22 | buttons: [ 23 | [(delay: 0, mapping: "click"), (delay: 0, mapping: "rclick"), (delay: 0, mapping: "mclick")], 24 | [(delay: 0, mapping: "ctrl-wheelup"), (delay: 0, mapping: "ctrl-wheeldown"), (delay: 0, mapping: "2")], 25 | ], 26 | knobs: [ 27 | (ccw: (delay: 0, mapping: "2"), press: (delay: 0, mapping: "2"), cw: (delay: 0, mapping: "2")), 28 | ], 29 | ), 30 | // layer 3 31 | ( 32 | buttons: [ 33 | [(delay: 0, mapping: "ctrl-m"), (delay: 0, mapping: "ctrl-slash"), (delay: 0, mapping: "space")], 34 | [(delay: 0, mapping: "ctrl-rightbracket"), (delay: 0, mapping: "ctrl-leftbracket"), (delay: 0, mapping: "right")], 35 | ], 36 | knobs: [ 37 | (ccw: (delay: 0, mapping: "3"), press: (delay: 0, mapping: "3"), cw: (delay: 0, mapping: "3")), 38 | ], 39 | ), 40 | ], 41 | ) 42 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use strum_macros::EnumString; 3 | 4 | #[derive(Debug, EnumString, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)] 5 | #[strum(ascii_case_insensitive)] 6 | pub enum Orientation { 7 | Normal, 8 | UpsideDown, 9 | Clockwise, 10 | CounterClockwise, 11 | } 12 | 13 | fn transpose(v: Vec>) -> Vec> { 14 | assert!(!v.is_empty()); 15 | let len = v[0].len(); 16 | let mut iters: Vec<_> = v.into_iter().map(|n| n.into_iter()).collect(); 17 | (0..len) 18 | .map(|_| { 19 | iters 20 | .iter_mut() 21 | .map(|n| n.next().unwrap()) 22 | .collect::>() 23 | }) 24 | .collect() 25 | } 26 | 27 | /// Returns the key numbers transposed clockwise 28 | /// 29 | /// #Arguments 30 | /// `keys` - the key number maxtrix 31 | /// 32 | pub fn get_keys_clockwise(keys: Vec>) -> Vec> { 33 | let mut data: Vec> = vec![]; 34 | for i in keys.iter().rev() { 35 | data.push(i.to_vec()); 36 | } 37 | transpose(data) 38 | } 39 | 40 | /// Returns the key numbers transposed counter clockwise 41 | /// 42 | /// #Arguments 43 | /// `keys` - the key number maxtrix 44 | /// 45 | pub fn get_keys_counter_clockwise(keys: Vec>) -> Vec> { 46 | let mut data: Vec> = vec![]; 47 | for mut i in keys { 48 | i.reverse(); 49 | data.push(i); 50 | } 51 | transpose(data) 52 | } 53 | 54 | /// Returns the key numbers flipped upside down 55 | /// 56 | /// #Arguments 57 | /// `keys` - the key number maxtrix 58 | /// 59 | pub fn get_keys_upsidedown(keys: Vec>) -> Vec> { 60 | let mut data: Vec> = vec![]; 61 | for i in keys.iter().rev() { 62 | let mut tmp = i.clone(); 63 | tmp.reverse(); 64 | data.push(tmp.to_vec()); 65 | } 66 | data 67 | } 68 | 69 | #[cfg(test)] 70 | #[test] 71 | fn test_clockwise() { 72 | let mut keys = Vec::new(); 73 | let mut a = Vec::new(); 74 | for i in 1..=3 { 75 | a.push(i); 76 | } 77 | keys.push(a); 78 | 79 | let mut a = Vec::new(); 80 | for i in 4..=6 { 81 | a.push(i); 82 | } 83 | keys.push(a); 84 | 85 | let transposed = get_keys_clockwise(keys.clone()); 86 | assert_eq!(transposed[0], [4, 1]); 87 | assert_eq!(transposed[1], [5, 2]); 88 | assert_eq!(transposed[2], [6, 3]); 89 | } 90 | 91 | #[test] 92 | fn test_counter_clockwise() { 93 | let mut keys = Vec::new(); 94 | let mut a = Vec::new(); 95 | for i in 1..=3 { 96 | a.push(i); 97 | } 98 | keys.push(a); 99 | 100 | let mut a = Vec::new(); 101 | for i in 4..=6 { 102 | a.push(i); 103 | } 104 | keys.push(a); 105 | 106 | let transposed = get_keys_counter_clockwise(keys.clone()); 107 | assert_eq!(transposed[0], [3, 6]); 108 | assert_eq!(transposed[1], [2, 5]); 109 | assert_eq!(transposed[2], [1, 4]); 110 | } 111 | 112 | #[test] 113 | fn test_upside_down() { 114 | let mut keys = Vec::new(); 115 | let mut a = Vec::new(); 116 | for i in 1..=3 { 117 | a.push(i); 118 | } 119 | keys.push(a); 120 | 121 | let mut a = Vec::new(); 122 | for i in 4..=6 { 123 | a.push(i); 124 | } 125 | keys.push(a); 126 | 127 | let usd = get_keys_upsidedown(keys.clone()); 128 | assert_eq!(usd[0], [6, 5, 4]); 129 | assert_eq!(usd[1], [3, 2, 1]); 130 | } 131 | -------------------------------------------------------------------------------- /src/consts.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | // Supported devices listed by vendor/product IDs 4 | pub const VENDOR_ID: u16 = 0x1189; 5 | pub const PRODUCT_IDS: [u16; 3] = [0x8840, 0x8842, 0x8890]; 6 | 7 | /// Timeout for reading from USB 8 | /// 9 | pub const DEFAULT_TIMEOUT: Duration = Duration::from_millis(100); 10 | 11 | /// Number of layers on the macropad. Depending on the model, 12 | /// some layers are no accessible 13 | /// 14 | pub const NUM_LAYERS: u8 = 3; 15 | 16 | /// Read buffer size (in bytes) 17 | /// 18 | pub const READ_BUF_SIZE: u8 = 72; 19 | 20 | /// Maximum number of key presses that can be assigned to a key 21 | /// for a 0x884X macropad 22 | /// 23 | pub const MAX_KEY_PRESSES_884X: usize = 17; 24 | 25 | /// Maximum number of key presses that can be assigned to a key 26 | /// for a 0x8890 macropad 27 | /// 28 | pub const MAX_KEY_PRESSES_8890: usize = 5; 29 | 30 | /// Maximum delay for a keypress 31 | /// 32 | pub const MAX_DELAY: u16 = 6000; 33 | 34 | /// Packet size 35 | /// 36 | pub const PACKET_SIZE: usize = 65; 37 | -------------------------------------------------------------------------------- /src/decoder.rs: -------------------------------------------------------------------------------- 1 | use crate::keyboard::{MediaCode, WellKnownCode}; 2 | use anyhow::{anyhow, Result}; 3 | use log::debug; 4 | use num::FromPrimitive; 5 | 6 | pub struct Decoder {} 7 | 8 | #[derive(Debug)] 9 | pub struct KeyCode { 10 | modifier: u8, 11 | media_code: Option, 12 | wkc: Option, 13 | } 14 | 15 | /// Macropad information 16 | pub struct DeviceInformation { 17 | /// Number of keys on the macropad 18 | pub num_keys: u8, 19 | /// Number of rotary encoders on the macropad 20 | pub num_encoders: u8, 21 | } 22 | 23 | /// Mapping of a key/encoder for the device 24 | #[derive(Debug)] 25 | pub struct KeyMapping { 26 | /// Delay value which is used for msec delay between key presses 27 | /// Valid values are 0-6000 inclusive 28 | pub delay: u16, 29 | /// Layer to program 30 | pub layer: u8, 31 | /// Key index on layer to program 32 | pub key_number: u8, 33 | /// Essentially the keychord for the key 34 | pub keys: Vec, 35 | } 36 | 37 | impl Decoder { 38 | pub fn get_device_info(buf: &[u8]) -> DeviceInformation { 39 | DeviceInformation { 40 | num_keys: buf[2], 41 | num_encoders: buf[3], 42 | } 43 | } 44 | 45 | pub fn get_key_mapping(buf: &[u8]) -> Result { 46 | if buf[1] != 0xfa { 47 | return Err(anyhow!( 48 | "Message does not appear to be a response from device" 49 | )); 50 | } 51 | 52 | let mut key_press: Vec = Vec::new(); 53 | let mut i = 11; 54 | 55 | // check for type of key in byte 4 56 | // 0x01 - WellKnownKey 57 | // 0x02 - Multimedia 58 | // 0x03 - Mouse 59 | 60 | // can we do this or should we check if bit 0 and bit 1 is set? 61 | if buf[4] == 0x03 { 62 | // mouse wheel 63 | let mut wheel_mapping = String::new(); 64 | if buf[10] == 0x04 { 65 | let mod_key = Self::get_key(&[buf[11], buf[12]]); 66 | if mod_key.is_some() { 67 | let result = mod_key.unwrap(); 68 | wheel_mapping = Self::modifier_to_str(result.modifier); 69 | } 70 | } else if buf[10] == 0x01 { 71 | } 72 | 73 | // mouse click 74 | let mut click_type = wheel_mapping; 75 | if !click_type.is_empty() { 76 | click_type += "-"; 77 | } 78 | match buf[12] { 79 | 0x01 => click_type = "click".to_string(), 80 | 0x02 => click_type = "rclick".to_string(), 81 | 0x04 => click_type = "mclick".to_string(), 82 | _ => (), 83 | } 84 | 85 | // mouse wheel status 86 | if click_type.ends_with('-') { 87 | click_type.pop(); 88 | } 89 | let mut key_str = click_type; 90 | match buf[15] { 91 | 0x01 => { 92 | if !key_str.is_empty() { 93 | key_str += "-"; 94 | } 95 | key_str += "wheelup"; 96 | } 97 | 0xFF => { 98 | if !key_str.is_empty() { 99 | key_str += "-"; 100 | } 101 | key_str += "wheeldown"; 102 | } 103 | _ => (), 104 | } 105 | key_press.push(key_str); 106 | 107 | // TODO: is it possible to make a binding like wheelup-a? doesn't make much sense 108 | // but might need to add support for that. currently, not supported 109 | 110 | return Ok(KeyMapping { 111 | delay: u16::from_be_bytes([buf[5], buf[6]]), 112 | layer: buf[3], 113 | key_number: buf[2], 114 | keys: key_press, 115 | }); 116 | } // end buf[4] == 0x03 (Mouse) 117 | 118 | // Multimedia 119 | if buf[4] == 0x02 { 120 | let mut tmp = vec![0u8, 2]; 121 | tmp[1] = buf[i]; 122 | 123 | let val = Self::get_key(&tmp); 124 | 125 | let result = val.unwrap(); 126 | //println!("result: {:?}", result); 127 | let mut key_str = Self::modifier_to_str(result.modifier); 128 | if result.media_code.is_some() { 129 | if !key_str.is_empty() { 130 | key_str += "-"; 131 | } 132 | key_str += &result.media_code.unwrap().to_string(); 133 | } 134 | key_press.push(key_str); 135 | i += 1; 136 | } // end buf[4] == 0x02 (Multimedia) 137 | 138 | loop { 139 | let val = Self::get_key(&[buf[i], buf[i + 1]]); 140 | if val.is_none() { 141 | break; 142 | } 143 | 144 | // get the mapping 145 | let result = val.unwrap(); 146 | let mut key_str = Self::modifier_to_str(result.modifier); 147 | if result.wkc.is_some() { 148 | //println!("WKC!!!!"); 149 | if !key_str.is_empty() { 150 | key_str += "-"; 151 | } 152 | key_str += &result.wkc.unwrap().to_string(); 153 | } 154 | key_press.push(key_str); 155 | 156 | i += 2; 157 | if i > 45 { 158 | break; // end of mapping space in usb response 159 | } 160 | } 161 | Ok(KeyMapping { 162 | delay: u16::from_be_bytes([buf[5], buf[6]]), 163 | layer: buf[3], 164 | key_number: buf[2], 165 | keys: key_press, 166 | }) 167 | } 168 | 169 | fn get_key(buf: &[u8]) -> Option { 170 | let val = u16::from_be_bytes([buf[0], buf[1]]); 171 | debug!("val: 0x{:02x}", val); 172 | if val == 0 { 173 | return None; 174 | } 175 | 176 | // get the key combination 177 | let mut da_key = None; 178 | let mut mc_key = None; 179 | if buf[1] > 0 { 180 | da_key = Some(::from_u8(buf[1]))?; 181 | mc_key = Some(::from_u8(buf[1]))?; 182 | } 183 | 184 | Some(KeyCode { 185 | modifier: buf[0], 186 | media_code: mc_key, 187 | wkc: da_key, 188 | }) 189 | } 190 | 191 | pub fn modifier_to_str(modifier: u8) -> String { 192 | let mut retval = Vec::new(); 193 | for i in 0..=7 { 194 | if modifier >> i & 1 == 1 { 195 | match i { 196 | 0 => { 197 | retval.push("ctrl"); 198 | } 199 | 1 => { 200 | retval.push("shift"); 201 | } 202 | 2 => { 203 | retval.push("alt"); 204 | } 205 | 3 => { 206 | retval.push("win"); 207 | } 208 | 4 => { 209 | retval.push("rctrl"); 210 | } 211 | 5 => { 212 | retval.push("rshift"); 213 | } 214 | 6 => { 215 | retval.push("ralt"); 216 | } 217 | _ => { 218 | break; 219 | } 220 | } 221 | } 222 | } 223 | 224 | retval.join("-") 225 | } 226 | } 227 | 228 | #[cfg(test)] 229 | mod tests { 230 | 231 | use crate::decoder::Decoder; 232 | use anyhow::Result; 233 | 234 | #[test] 235 | fn modifier_test() { 236 | assert_eq!(Decoder::modifier_to_str(0x06), "shift-alt"); 237 | assert_eq!(Decoder::modifier_to_str(0x60), "rshift-ralt"); 238 | } 239 | 240 | #[test] 241 | fn decode_device() { 242 | // response for a 6 button 1 rotary encoder macropad 243 | let device = vec![ 244 | 0x03, 0xfb, 0x06, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 245 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 246 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 247 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 248 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 249 | ]; 250 | let mp = Decoder::get_device_info(&device); 251 | assert_eq!(mp.num_keys, 6); 252 | assert_eq!(mp.num_encoders, 1); 253 | } 254 | 255 | #[test] 256 | fn decode_key() -> Result<()> { 257 | env_logger::init(); 258 | // layer = 1 259 | // key = 1 260 | // mapping = ctrl+a 261 | let mut msg = vec![ 262 | 0x03, 0xfa, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x04, 0x00, 263 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 264 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 265 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 266 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 267 | ]; 268 | println!("test 1"); 269 | let mut key = Decoder::get_key_mapping(&msg)?; 270 | assert_eq!(key.layer, 1); 271 | println!("{:?}", key); 272 | assert_eq!(key.keys.len(), 1); 273 | assert_eq!(key.keys[0], "ctrl-a"); 274 | 275 | msg = vec![ 276 | 0x03, 0xfa, 0x02, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x06, 0x00, 0x00, 277 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 278 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 279 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 280 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 281 | ]; 282 | println!("\ntest 2"); 283 | key = Decoder::get_key_mapping(&msg)?; 284 | assert_eq!(key.layer, 1); 285 | println!("{:?}", key); 286 | assert_eq!(key.keys.len(), 1); 287 | assert_eq!(key.keys[0], "shift-alt"); 288 | 289 | msg = vec![ 290 | 0x03, 0xfa, 0x03, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x05, 0x05, 0x00, 291 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 292 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 293 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 294 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 295 | ]; 296 | println!("\ntest 3"); 297 | key = Decoder::get_key_mapping(&msg)?; 298 | assert_eq!(key.layer, 1); 299 | println!("{:?}", key); 300 | assert_eq!(key.keys.len(), 1); 301 | assert_eq!(key.keys[0], "ctrl-alt-b"); 302 | 303 | msg = vec![ 304 | 0x03, 0xfa, 0x04, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 305 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 306 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 307 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 308 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 309 | ]; 310 | println!("\ntest 4"); 311 | key = Decoder::get_key_mapping(&msg)?; 312 | assert_eq!(key.layer, 1); 313 | println!("{:?}", key); 314 | assert_eq!(key.keys.len(), 0); 315 | 316 | msg = vec![ 317 | 0x03, 0xfa, 0x05, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x05, 0x0e, 0x05, 318 | 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 319 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 320 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 321 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 322 | ]; 323 | println!("\ntest 5"); 324 | key = Decoder::get_key_mapping(&msg)?; 325 | assert_eq!(key.layer, 1); 326 | println!("{:?}", key); 327 | assert_eq!(key.keys.len(), 2); 328 | assert_eq!(key.keys[0], "ctrl-alt-k"); 329 | assert_eq!(key.keys[1], "ctrl-alt-a"); 330 | 331 | msg = vec![ 332 | 0x03, 0xfa, 0x06, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x0f, 0x00, 333 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 334 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 335 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 336 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 337 | ]; 338 | println!("\ntest 6"); 339 | key = Decoder::get_key_mapping(&msg)?; 340 | assert_eq!(key.layer, 1); 341 | println!("{:?}", key); 342 | assert_eq!(key.keys[0], "l"); 343 | 344 | msg = vec![ 345 | 0x03, 0xfa, 0x10, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 346 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 347 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 348 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 349 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 350 | ]; 351 | println!("\ntest 7"); 352 | key = Decoder::get_key_mapping(&msg)?; 353 | assert_eq!(key.layer, 1); 354 | println!("{:?}", key); 355 | assert_eq!(key.keys[0], "wheelup"); 356 | 357 | msg = vec![ 358 | 0x03, 0xfa, 0x11, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 359 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 360 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 361 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 362 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 363 | ]; 364 | println!("\ntest 8"); 365 | key = Decoder::get_key_mapping(&msg)?; 366 | assert_eq!(key.layer, 1); 367 | println!("{:?}", key); 368 | assert_eq!(key.keys[0], "click"); 369 | 370 | msg = vec![ 371 | 0x03, 0xfa, 0x12, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 372 | 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 373 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 374 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 375 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 376 | ]; 377 | println!("\ntest 9"); 378 | key = Decoder::get_key_mapping(&msg)?; 379 | assert_eq!(key.layer, 1); 380 | println!("{:?}", key); 381 | assert_eq!(key.keys[0], "wheeldown"); 382 | 383 | msg = vec![ 384 | 0x03, 0xfa, 0x13, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 385 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 386 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 387 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 388 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 389 | ]; 390 | println!("\ntest 10"); 391 | key = Decoder::get_key_mapping(&msg)?; 392 | assert_eq!(key.layer, 1); 393 | println!("{:?}", key); 394 | assert_eq!(key.keys[0], "ctrl-wheelup"); 395 | 396 | msg = vec![ 397 | 0x03, 0xfa, 0x13, 0x01, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x01, 0x00, 0x00, 398 | 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 399 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 400 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 401 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 402 | ]; 403 | println!("\ntest 11"); 404 | key = Decoder::get_key_mapping(&msg)?; 405 | assert_eq!(key.layer, 1); 406 | println!("{:?}", key); 407 | assert_eq!(key.keys[0], "ctrl-wheeldown"); 408 | 409 | msg = vec![ 410 | 0x03, 0xfa, 0x10, 0x03, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xea, 0x00, 0x00, 411 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 412 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 413 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 414 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 415 | ]; 416 | println!("\ntest 12"); 417 | key = Decoder::get_key_mapping(&msg)?; 418 | assert_eq!(key.layer, 3); 419 | println!("{:?}", key); 420 | assert_eq!(key.keys[0], "volumedown"); 421 | 422 | msg = vec![ 423 | 0x03, 0xfa, 0x11, 0x03, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xe2, 0x00, 0x00, 424 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 425 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 426 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 427 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 428 | ]; 429 | println!("\ntest 13"); 430 | key = Decoder::get_key_mapping(&msg)?; 431 | assert_eq!(key.layer, 3); 432 | println!("{:?}", key); 433 | assert_eq!(key.keys[0], "mute"); 434 | 435 | Ok(()) 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /src/keyboard/k884x.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | consts, 3 | decoder::{Decoder, KeyMapping}, 4 | keyboard::{ 5 | Configuration, Keyboard, LedColor, MediaCode, Messages, Modifier, MouseAction, MouseButton, 6 | WellKnownCode, 7 | }, 8 | mapping::Macropad, 9 | }; 10 | use anyhow::{anyhow, ensure, Result}; 11 | use log::{debug, info}; 12 | use num::ToPrimitive; 13 | use rusb::{Context, DeviceHandle}; 14 | use std::str::FromStr; 15 | 16 | /// 0x884x type keyboard 17 | pub struct Keyboard884x { 18 | /// rusb device handle 19 | handle: Option>, 20 | /// address of out endpoint 21 | out_endpoint: u8, 22 | /// address of in endpoint 23 | in_endpoint: u8, 24 | /// product id 25 | pid: u16, 26 | } 27 | 28 | impl Configuration for Keyboard884x { 29 | fn read_macropad_config(&mut self, layer: &u8) -> Result { 30 | let mut buf = vec![0; consts::READ_BUF_SIZE.into()]; 31 | 32 | // get the type of device 33 | self.send(&self.device_type())?; 34 | self.recieve(&mut buf)?; 35 | let device_info = Decoder::get_device_info(&buf); 36 | info!( 37 | "OUT: 0x{:02x} IN: 0x{:02x}", 38 | self.get_out_endpoint(), 39 | self.get_in_endpoint() 40 | ); 41 | debug!( 42 | "number of keys: {} number of rotary encoders: {}", 43 | device_info.num_keys, device_info.num_encoders 44 | ); 45 | 46 | // send message to get keys and process later so we don't slow the usb traffic 47 | // not sure if that would be an issue as i don't know the usb protocol. mabye 48 | // we could process here too?? 49 | let mut mappings: Vec = Vec::new(); 50 | if *layer > 0 { 51 | // specific layer 52 | self.send(&self.read_config(device_info.num_keys, device_info.num_encoders, *layer))?; 53 | // read keys for specified layer 54 | info!("reading keys for layer {}", layer); 55 | let data = self.read_config(device_info.num_keys, device_info.num_encoders, *layer); 56 | let _ = self.send(&data); 57 | 58 | // read all messages from device 59 | loop { 60 | let bytes_read = self.recieve(&mut buf)?; 61 | if bytes_read == 0 { 62 | break; 63 | } 64 | debug!("bytes read: {bytes_read}"); 65 | debug!("data: {:02x?}", buf); 66 | mappings.push(Decoder::get_key_mapping(&buf)?); 67 | } 68 | } else { 69 | // read keys for all layers 70 | for i in 1..=consts::NUM_LAYERS { 71 | self.send(&self.read_config(device_info.num_keys, device_info.num_encoders, i))?; 72 | info!("reading keys for layer {i}"); 73 | let data = self.read_config(device_info.num_keys, device_info.num_encoders, i); 74 | let _ = self.send(&data); 75 | 76 | // read all messages from device 77 | loop { 78 | let bytes_read = self.recieve(&mut buf)?; 79 | if bytes_read == 0 { 80 | break; 81 | } 82 | debug!("bytes read: {bytes_read}"); 83 | debug!("data: {:02x?}", buf); 84 | mappings.push(Decoder::get_key_mapping(&buf)?); 85 | } 86 | } 87 | } 88 | 89 | // process responses from device 90 | let rows_cols = Self::guestimate_rows_cols(device_info.num_keys)?; 91 | let mut mp = Macropad::new(rows_cols.0, rows_cols.1, device_info.num_encoders); 92 | let mut knob_idx = 0; 93 | let mut knob_type = 0; 94 | let mut last_layer = 0; 95 | for km in mappings { 96 | debug!("{:?}", km); 97 | if km.layer != last_layer { 98 | last_layer = km.layer; 99 | knob_idx = 0; 100 | knob_type = 0; 101 | } 102 | 103 | if km.key_number <= mp.device.rows * mp.device.cols { 104 | // button mappings 105 | let row_col = Self::get_position(&mp, km.key_number)?; 106 | debug!( 107 | " key: {} at row: {} col: {}", 108 | km.key_number, row_col.0, row_col.1 109 | ); 110 | 111 | mp.layers[(km.layer - 1) as usize].buttons[row_col.0][row_col.1].delay = km.delay; 112 | mp.layers[(km.layer - 1) as usize].buttons[row_col.0][row_col.1].mapping = 113 | km.keys.join(","); 114 | } else { 115 | // knobs 116 | debug!("knob idx: {} knob type: {}", knob_idx, knob_type); 117 | match knob_type { 118 | 0 => { 119 | mp.layers[(km.layer - 1) as usize].knobs[knob_idx].ccw.delay = km.delay; 120 | mp.layers[(km.layer - 1) as usize].knobs[knob_idx] 121 | .ccw 122 | .mapping = km.keys.join("-"); 123 | knob_type += 1; 124 | } 125 | 1 => { 126 | mp.layers[(km.layer - 1) as usize].knobs[knob_idx] 127 | .press 128 | .delay = km.delay; 129 | mp.layers[(km.layer - 1) as usize].knobs[knob_idx] 130 | .press 131 | .mapping = km.keys.join("-"); 132 | knob_type += 1; 133 | } 134 | 2 => { 135 | mp.layers[(km.layer - 1) as usize].knobs[knob_idx].cw.delay = km.delay; 136 | mp.layers[(km.layer - 1) as usize].knobs[knob_idx] 137 | .cw 138 | .mapping = km.keys.join("-"); 139 | knob_type = 0; 140 | knob_idx += 1; 141 | } 142 | _ => { 143 | unreachable!("should not get here!") 144 | } 145 | } 146 | } 147 | } 148 | Ok(mp) 149 | } 150 | } 151 | 152 | impl Messages for Keyboard884x { 153 | fn read_config(&self, keys: u8, encoders: u8, layer: u8) -> Vec { 154 | if self.pid == 0x8840 { 155 | vec![ 156 | 0x03, 0xfa, keys, encoders, layer, 0x06, 0x00, 0xcc, 0x80, 0x00, 0xc0, 0xcc, 0x80, 157 | 0x00, 0x7c, 0xf2, 0x02, 0x69, 0x00, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x14, 0x06, 0xc0, 158 | 0xcc, 0x80, 0x00, 0x49, 0x01, 0x00, 0x00, 0x06, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 159 | 0x00, 0xb0, 0xcc, 0x80, 0x00, 0x40, 0xcd, 0x80, 0x00, 0x88, 0x05, 0x00, 0x06, 0xc0, 160 | 0x0a, 0x10, 0x06, 0xe0, 0xcc, 0x80, 0x00, 0xc7, 0xb6, 0x48, 161 | ] 162 | } else { 163 | vec![ 164 | 0x03, 0xfa, keys, encoders, layer, 0x02, 0xe0, 0xcb, 0x80, 0x00, 0xa0, 0xcc, 0x80, 165 | 0x00, 0x7c, 0xf2, 0x02, 0x69, 0x00, 0x00, 0x00, 0x00, 0x4d, 0x00, 0x2c, 0x02, 0xa0, 166 | 0xcc, 0x80, 0x00, 0xe8, 0x00, 0x00, 0x00, 0xb9, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 167 | 0x00, 0x90, 0xcc, 0x80, 0x00, 0x20, 0xcd, 0x80, 0x00, 0xc0, 0x84, 0x26, 0x02, 0xa0, 168 | 0x62, 0x2f, 0x02, 0xc0, 0xcc, 0x80, 0x00, 0xc7, 0xb6, 0xc2, 169 | ] 170 | } 171 | } 172 | 173 | fn device_type(&self) -> Vec { 174 | if self.pid == 0x8840 { 175 | vec![ 176 | 0x03, 0xfb, 0xfb, 0xfb, 0x02, 0x06, 0x2c, 0xd0, 0x80, 0x00, 0xdc, 0xcf, 0x80, 0x00, 177 | 0xcc, 0xd2, 0x21, 0x01, 0xe0, 0xcf, 0x80, 0x00, 0x2c, 0xd0, 0x80, 0x00, 0x00, 0x00, 178 | 0x00, 0x00, 0xd0, 0x0d, 0x48, 0x00, 0xfc, 0xcf, 0x80, 0x00, 0xc0, 0x61, 0xbc, 0x06, 179 | 0x38, 0xd0, 0x80, 0x00, 0x70, 0xf5, 0x1e, 0x62, 0x98, 0xda, 0x11, 0x62, 0x0c, 0x80, 180 | 0x00, 0x00, 0x48, 0x09, 0x00, 0x06, 0xff, 0xff, 0xff, 181 | ] 182 | } else { 183 | vec![ 184 | 0x03, 0xfb, 0xfb, 0xfb, 0x1f, 0x02, 0x3c, 0xd0, 0x80, 0x00, 0xec, 0xcf, 0x80, 0x00, 185 | 0xcc, 0xd2, 0x9b, 0x00, 0xf0, 0xcf, 0x80, 0x00, 0x3c, 0xd0, 0x80, 0x00, 0x56, 0x83, 186 | 0xd2, 0x7b, 0xd0, 0x0d, 0x48, 0x00, 0x0c, 0xd0, 0x80, 0x00, 0xa8, 0x3d, 0x34, 0x02, 187 | 0x48, 0xd0, 0x80, 0x00, 0x70, 0xf5, 0x1e, 0x62, 0x98, 0xda, 0x11, 0x62, 0x0c, 0x80, 188 | 0x00, 0x00, 0x00, 0x82, 0x26, 0x02, 0xff, 0xff, 0xff, 189 | ] 190 | } 191 | } 192 | 193 | fn program_led(&self, mode: u8, layer: u8, color: LedColor) -> Vec { 194 | let mut m_c = ::to_u8(&color).unwrap(); 195 | m_c |= mode; 196 | debug!("mode and code: 0x{:02} layer: {layer}", m_c); 197 | let mut msg = vec![0x03, 0xfe, 0xb0, layer, 0x08]; 198 | msg.extend_from_slice(&[0; 5]); 199 | msg.extend_from_slice(&[0x01, 0x00, m_c]); 200 | msg.extend_from_slice(&[0; 52]); 201 | msg 202 | } 203 | 204 | fn end_program(&self) -> Vec { 205 | let mut msg = vec![0x03, 0xfd, 0xfe, 0xff]; 206 | msg.extend_from_slice(&[0; 61]); 207 | msg 208 | } 209 | } 210 | 211 | impl Keyboard for Keyboard884x { 212 | fn program(&mut self, macropad: &Macropad) -> Result<()> { 213 | // ensure the config we have matches the connected device we want to program 214 | let mut buf = vec![0; consts::READ_BUF_SIZE.into()]; 215 | 216 | // get the type of device 217 | self.send(&self.device_type())?; 218 | let bytes_read = self.recieve(&mut buf)?; 219 | 220 | if bytes_read > 0 { 221 | self.recieve(&mut buf)?; 222 | let device_info = Decoder::get_device_info(&buf); 223 | ensure!( 224 | device_info.num_keys == (macropad.device.rows * macropad.device.cols) 225 | && device_info.num_encoders == macropad.device.knobs, 226 | "Configuration file and macropad mismatch.\nLooks like you are trying to program a different macropad.\nDid you select the right configuration file?\n\n\ 227 | If you think your mapping is correct, use the -s option to skip this check and program your device. Some of the 0x8840 products do not support\n\ 228 | reading and so you must use this option when programming." 229 | ); 230 | } else { 231 | // we probably have the type from amazon, while have the same product id, does not 232 | // support reading. do not error out, but skip the check and continue to program 233 | println!("Unable perform sanity check - device does not support reading of configuration. Programming macropad."); 234 | } 235 | 236 | // get our layout of buttons relative to programming orientation 237 | let layout = self.get_layout( 238 | macropad.device.orientation, 239 | macropad.device.rows, 240 | macropad.device.cols, 241 | )?; 242 | debug!("layout: {layout:?}"); 243 | 244 | for (i, layer) in macropad.layers.iter().enumerate() { 245 | let lyr = (i + 1) as u8; 246 | let mut key_num; 247 | for (row_idx, row) in layer.buttons.iter().enumerate() { 248 | for (col_idx, btn) in row.iter().enumerate() { 249 | debug!("get position in layout: row_idx: {row_idx} col_idx: {col_idx}"); 250 | key_num = layout[row_idx][col_idx]; 251 | debug!( 252 | "program layer: {} key: 0x{:02x} to: {btn:?}", 253 | i + 1, 254 | key_num 255 | ); 256 | self.send(&self.build_key_msg(&btn.mapping, lyr, key_num, 0)?)?; 257 | if btn.delay > 0 { 258 | let mut msg = self.build_key_msg(&btn.mapping, lyr, key_num, btn.delay)?; 259 | msg[4] = 5; 260 | self.send(&msg)?; 261 | } 262 | } 263 | } 264 | 265 | // TODO: test 9x3 to see if the 3 knobs are top to bottom with key number 266 | key_num = 0x10; 267 | for knob in &layer.knobs { 268 | debug!( 269 | "layer: {} key: 0x{:02x} knob ccw {}", 270 | i + 1, 271 | key_num, 272 | knob.ccw.mapping 273 | ); 274 | self.send(&self.build_key_msg(&knob.ccw.mapping, lyr, key_num, 0)?)?; 275 | if knob.ccw.delay > 0 { 276 | let mut msg = 277 | self.build_key_msg(&knob.ccw.mapping, lyr, key_num, knob.ccw.delay)?; 278 | msg[4] = 5; 279 | self.send(&msg)?; 280 | } 281 | key_num += 1; 282 | 283 | debug!( 284 | "layer: {} key: 0x{:02x} knob press {}", 285 | i + 1, 286 | key_num, 287 | knob.press.mapping 288 | ); 289 | self.send(&self.build_key_msg(&knob.press.mapping, lyr, key_num, 0)?)?; 290 | if knob.press.delay > 0 { 291 | let mut msg = 292 | self.build_key_msg(&knob.press.mapping, lyr, key_num, knob.press.delay)?; 293 | msg[4] = 5; 294 | self.send(&msg)?; 295 | } 296 | key_num += 1; 297 | 298 | debug!( 299 | "layer: {} key: 0x{:02x} knob cw {}", 300 | i + 1, 301 | key_num, 302 | knob.cw.mapping 303 | ); 304 | self.send(&self.build_key_msg(&knob.cw.mapping, lyr, key_num, 0)?)?; 305 | if knob.cw.delay > 0 { 306 | let mut msg = 307 | self.build_key_msg(&knob.cw.mapping, lyr, key_num, knob.cw.delay)?; 308 | msg[4] = 5; 309 | self.send(&msg)?; 310 | } 311 | key_num += 1; 312 | } 313 | self.send(&self.end_program())?; 314 | } 315 | Ok(()) 316 | } 317 | 318 | fn set_led(&mut self, mode: u8, layer: u8, color: LedColor) -> Result<()> { 319 | self.send(&self.program_led(mode, layer, color))?; 320 | self.send(&self.end_program())?; 321 | Ok(()) 322 | } 323 | 324 | fn get_handle(&self) -> &DeviceHandle { 325 | self.handle.as_ref().unwrap() 326 | } 327 | 328 | fn get_out_endpoint(&self) -> u8 { 329 | self.out_endpoint 330 | } 331 | 332 | fn get_in_endpoint(&self) -> u8 { 333 | self.in_endpoint 334 | } 335 | } 336 | 337 | impl Keyboard884x { 338 | pub fn new( 339 | handle: Option>, 340 | out_endpoint: u8, 341 | in_endpoint: u8, 342 | pid: u16, 343 | ) -> Result { 344 | let keyboard = Self { 345 | handle, 346 | out_endpoint, 347 | in_endpoint, 348 | pid, 349 | }; 350 | 351 | Ok(keyboard) 352 | } 353 | 354 | fn build_key_msg( 355 | &self, 356 | key_chord: &str, 357 | layer: u8, 358 | key_pos: u8, 359 | delay: u16, 360 | ) -> Result> { 361 | let keys: Vec<_> = key_chord.split(',').collect(); 362 | let mut msg = vec![0x03, 0xfd, key_pos, layer, 0x01]; 363 | msg.extend_from_slice(&[0; 5]); 364 | msg.extend_from_slice(&[keys.len().try_into()?]); 365 | 366 | if delay > 0 { 367 | let bytes = delay.to_le_bytes(); 368 | msg[5] = bytes[0]; 369 | msg[6] = bytes[1]; 370 | } 371 | 372 | let mut cnt = 0; 373 | let mut mouse_action = 0u8; 374 | let mut mouse_click = 0u8; 375 | let mut media_key = false; 376 | let mut media_val = 0u8; 377 | for binding in &keys { 378 | let kc: Vec<_> = binding.split('-').collect(); 379 | let mut m_c = 0x00u8; 380 | let mut wkk = 0x00; 381 | for key in kc { 382 | debug!("=> {key}"); 383 | if let Ok(m) = Modifier::from_str(key) { 384 | let power = ::to_u8(&m).unwrap(); 385 | m_c |= 2u32.pow(power as u32) as u8; 386 | } else if let Ok(w) = WellKnownCode::from_str(key) { 387 | wkk = ::to_u8(&w).unwrap(); 388 | } else if let Ok(a) = MediaCode::from_str(key) { 389 | let value = ::to_u16(&a).unwrap(); 390 | m_c = (value & 0xFF) as u8; 391 | msg[4] = 0x02; 392 | msg[10] = ((value & 0xFF00) >> 8) as u8; 393 | media_val = msg[10]; 394 | if msg[10] == 0 { 395 | msg[10] = 0x02; 396 | } 397 | media_key = true; 398 | } else if let Ok(a) = MouseButton::from_str(key) { 399 | mouse_click = 400 | 2u32.pow(::to_u8(&a).unwrap().into()) as u8; 401 | msg[4] = 0x03; 402 | } else if let Ok(a) = MouseAction::from_str(key) { 403 | match a { 404 | MouseAction::WheelUp => mouse_action = 0x01, 405 | MouseAction::WheelDown => mouse_action = 0xff, 406 | _ => (), 407 | } 408 | msg[4] = 0x03; 409 | } 410 | } 411 | msg.extend_from_slice(&[m_c, wkk]); 412 | cnt += 1; 413 | } 414 | 415 | for _i in 0..=(consts::MAX_KEY_PRESSES_884X - cnt) { 416 | msg.extend_from_slice(&[0x00; 2]); 417 | } 418 | 419 | if media_key { 420 | msg[12] = media_val; 421 | } 422 | 423 | if mouse_click > 0 { 424 | msg[12] = mouse_click; 425 | } 426 | if mouse_action > 0 { 427 | msg[15] = mouse_action; 428 | } 429 | 430 | // last 18 bytes are always 0 431 | msg.extend_from_slice(&[0; 18]); 432 | 433 | Ok(msg) 434 | } 435 | 436 | fn get_position(mp: &Macropad, key_num: u8) -> Result<(usize, usize)> { 437 | let cols = mp.device.cols; 438 | let mut col; 439 | let mut row; 440 | 441 | if key_num % cols == 0 { 442 | row = key_num / cols; 443 | row = row.saturating_sub(1); 444 | } else { 445 | row = key_num / cols; 446 | } 447 | if key_num > cols { 448 | col = key_num % cols; 449 | if col == 0 { 450 | col = cols; 451 | } 452 | col -= 1; 453 | } else { 454 | col = key_num - 1; 455 | } 456 | Ok((row.into(), col.into())) 457 | } 458 | 459 | fn guestimate_rows_cols(num_keys: u8) -> Result<(u8, u8)> { 460 | match num_keys { 461 | 6 => Ok((2, 3)), 462 | 9 => Ok((3, 3)), 463 | 12 => Ok((3, 4)), 464 | 15 => Ok((3, 5)), 465 | _ => Err(anyhow!("unable to guess rows/cols for {num_keys}")), 466 | } 467 | } 468 | } 469 | 470 | #[cfg(test)] 471 | mod tests { 472 | use crate::{consts, keyboard::k884x::Keyboard884x, keyboard::Messages, LedColor}; 473 | 474 | #[test] 475 | fn ctrl_a_ctrl_s() -> anyhow::Result<()> { 476 | // ctrl-a,ctrl-s 477 | // 03 fd 01 01 01 00 00 00 00 00 02 01 04 01 16 00 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 478 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 479 | let msg = kbd.build_key_msg("ctrl-a,ctrl-s", 1u8, 1u8, 0)?; 480 | println!("{:02x?}", msg); 481 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 482 | assert_eq!(msg[10], 0x02, "checking number of keys to program"); 483 | assert_eq!(msg[11], 0x01, "checking for ctrl modifier"); 484 | assert_eq!(msg[12], 0x04, "checking for 'a' key"); 485 | assert_eq!(msg[13], 0x01, "checking for ctrl modifier"); 486 | assert_eq!(msg[14], 0x16, "checking for 's' key"); 487 | Ok(()) 488 | } 489 | 490 | #[test] 491 | fn well_known_key() -> anyhow::Result<()> { 492 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 493 | let msg = kbd.build_key_msg("a", 1u8, 1u8, 0)?; 494 | println!("{:02x?}", msg); 495 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 496 | assert_eq!(msg[10], 0x01, "checking number of keys to program"); 497 | assert_eq!(msg[11], 0x00, "checking for modifier"); 498 | assert_eq!(msg[12], 0x04, "checking for 'a' key"); 499 | Ok(()) 500 | } 501 | 502 | #[test] 503 | fn volume_down() -> anyhow::Result<()> { 504 | // 03 fd 10 01 02 00 00 00 00 00 02 ea 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 505 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 506 | let msg = kbd.build_key_msg("volumedown", 1u8, 1u8, 0)?; 507 | println!("{:02x?}", msg); 508 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 509 | assert_eq!(msg[4], 0x02, "checking byte 4"); 510 | for i in msg.iter().take(10).skip(5) { 511 | assert_eq!(*i, 0x00); 512 | } 513 | assert_eq!(msg[10], 0x02, "checking byte 10"); 514 | assert_eq!(msg[11], 0xea, "checking byte 11"); 515 | assert_eq!(msg[12], 0x00, "checking byte 12"); 516 | Ok(()) 517 | } 518 | 519 | #[test] 520 | fn mouse_ctrl_plus() -> anyhow::Result<()> { 521 | // 03 fd 01 02 03 00 00 00 00 00 01 01 00 00 00 01 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 522 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 523 | let msg = kbd.build_key_msg("ctrl-wheelup", 1u8, 1u8, 0)?; 524 | println!("{:02x?}", msg); 525 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 526 | assert_eq!(msg[4], 0x03, "checking byte 4"); 527 | for i in msg.iter().take(10).skip(5) { 528 | assert_eq!(*i, 0x00); 529 | } 530 | assert_eq!(msg[10], 0x01, "checking byte 10"); 531 | assert_eq!(msg[11], 0x01, "checking byte 11"); 532 | assert_eq!(msg[15], 0x01, "checking byte 15"); 533 | Ok(()) 534 | } 535 | 536 | #[test] 537 | fn mouse_wheelup() -> anyhow::Result<()> { 538 | // 03 fd 01 02 03 00 00 00 00 00 01 00 00 00 00 01 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 539 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 540 | let msg = kbd.build_key_msg("wheelup", 1u8, 1u8, 0)?; 541 | println!("{:02x?}", msg); 542 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 543 | assert_eq!(msg[4], 0x03, "checking byte 4"); 544 | for i in msg.iter().take(10).skip(5) { 545 | assert_eq!(*i, 0x00); 546 | } 547 | assert_eq!(msg[10], 0x01, "checking byte 10"); 548 | assert_eq!(msg[11], 0x00, "checking byte 11"); 549 | assert_eq!(msg[15], 0x01, "checking byte 15"); 550 | Ok(()) 551 | } 552 | 553 | #[test] 554 | fn mouse_ctrl_minus() -> anyhow::Result<()> { 555 | // 03 fd 02 02 03 00 00 00 00 00 01 01 00 00 00 ff 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 556 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 557 | let msg = kbd.build_key_msg("ctrl-wheeldown", 1u8, 1u8, 0)?; 558 | println!("{:02x?}", msg); 559 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 560 | assert_eq!(msg[4], 0x03, "checking byte 4"); 561 | for i in msg.iter().take(10).skip(5) { 562 | assert_eq!(*i, 0x00); 563 | } 564 | assert_eq!(msg[10], 0x01, "checking byte 10"); 565 | assert_eq!(msg[11], 0x01, "checking byte 11"); 566 | assert_eq!(msg[15], 0xff, "checking byte 15"); 567 | Ok(()) 568 | } 569 | 570 | #[test] 571 | fn mouse_left_click() -> anyhow::Result<()> { 572 | // 03 fd 01 02 03 00 00 00 00 00 01 00 01 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 573 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 574 | let msg = kbd.build_key_msg("click", 1u8, 1u8, 0)?; 575 | println!("{:02x?}", msg); 576 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 577 | assert_eq!(msg[4], 0x03, "checking byte 4"); 578 | for i in msg.iter().take(10).skip(5) { 579 | assert_eq!(*i, 0x00); 580 | } 581 | assert_eq!(msg[10], 0x01, "checking byte 10"); 582 | assert_eq!(msg[11], 0x00, "checking byte 11"); 583 | assert_eq!(msg[12], 0x01, "checking byte 12"); 584 | Ok(()) 585 | } 586 | 587 | #[test] 588 | fn mouse_middle_click() -> anyhow::Result<()> { 589 | // 03 fd 02 02 03 00 00 00 00 00 01 00 04 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 590 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 591 | let msg = kbd.build_key_msg("mclick", 1u8, 1u8, 0)?; 592 | println!("{:02x?}", msg); 593 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 594 | assert_eq!(msg[4], 0x03, "checking byte 4"); 595 | for i in msg.iter().take(10).skip(5) { 596 | assert_eq!(*i, 0x00); 597 | } 598 | assert_eq!(msg[10], 0x01, "checking byte 10"); 599 | assert_eq!(msg[11], 0x00, "checking byte 11"); 600 | assert_eq!(msg[12], 0x04, "checking byte 12"); 601 | Ok(()) 602 | } 603 | 604 | #[test] 605 | fn mouse_right_click() -> anyhow::Result<()> { 606 | // 03 fd 03 02 03 00 00 00 00 00 01 00 02 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 607 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 608 | let msg = kbd.build_key_msg("rclick", 1u8, 1u8, 0)?; 609 | println!("{:02x?}", msg); 610 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 611 | assert_eq!(msg[4], 0x03, "checking byte 4"); 612 | for i in msg.iter().take(10).skip(5) { 613 | assert_eq!(*i, 0x00); 614 | } 615 | assert_eq!(msg[10], 0x01, "checking byte 10"); 616 | assert_eq!(msg[11], 0x00, "checking byte 11"); 617 | assert_eq!(msg[12], 0x02, "checking byte 12"); 618 | Ok(()) 619 | } 620 | 621 | #[test] 622 | fn shift_p() -> anyhow::Result<()> { 623 | // 03 fd 06 01 01 00 00 00 00 00 01 02 13 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 624 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 625 | let msg = kbd.build_key_msg("shift-p", 1u8, 1u8, 0)?; 626 | println!("{:02x?}", msg); 627 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 628 | assert_eq!(msg[4], 0x01, "checking byte 4"); 629 | for i in msg.iter().take(10).skip(5) { 630 | assert_eq!(*i, 0x00); 631 | } 632 | assert_eq!(msg[10], 0x01, "checking byte 10"); 633 | assert_eq!(msg[11], 0x02, "checking byte 11"); 634 | assert_eq!(msg[12], 0x13, "checking byte 12"); 635 | Ok(()) 636 | } 637 | 638 | #[test] 639 | fn win_enter() -> anyhow::Result<()> { 640 | // 03 fd 11 03 01 00 00 00 00 00 01 08 28 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 641 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 642 | let msg = kbd.build_key_msg("win-enter", 1u8, 1u8, 0)?; 643 | println!("{:02x?}", msg); 644 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 645 | assert_eq!(msg[4], 0x01, "checking byte 4"); 646 | for i in msg.iter().take(10).skip(5) { 647 | assert_eq!(*i, 0x00); 648 | } 649 | assert_eq!(msg[10], 0x01, "checking byte 10"); 650 | assert_eq!(msg[11], 0x08, "checking byte 11"); 651 | assert_eq!(msg[12], 0x28, "checking byte 12"); 652 | Ok(()) 653 | } 654 | 655 | #[test] 656 | fn ctrl_shift_v() -> anyhow::Result<()> { 657 | // 03 fd 01 01 01 00 00 00 00 00 01 03 19 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 658 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 659 | let msg = kbd.build_key_msg("ctrl-shift-v", 1u8, 1u8, 0)?; 660 | println!("{:02x?}", msg); 661 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 662 | assert_eq!(msg[4], 0x01, "checking byte 4"); 663 | for i in msg.iter().take(10).skip(5) { 664 | assert_eq!(*i, 0x00); 665 | } 666 | assert_eq!(msg[10], 0x01, "checking byte 10"); 667 | assert_eq!(msg[11], 0x03, "checking byte 11"); 668 | assert_eq!(msg[12], 0x19, "checking byte 12"); 669 | Ok(()) 670 | } 671 | 672 | #[test] 673 | fn ctrl_alt_del() -> anyhow::Result<()> { 674 | // 03 fd 01 01 01 00 00 00 00 00 01 05 4c 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 675 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 676 | let msg = kbd.build_key_msg("ctrl-alt-delete", 1u8, 1u8, 0)?; 677 | println!("{:02x?}", msg); 678 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 679 | assert_eq!(msg[4], 0x01, "checking byte 4"); 680 | for i in msg.iter().take(10).skip(5) { 681 | assert_eq!(*i, 0x00); 682 | } 683 | assert_eq!(msg[10], 0x01, "checking byte 10"); 684 | assert_eq!(msg[11], 0x05, "checking byte 11"); 685 | assert_eq!(msg[12], 0x4c, "checking byte 12"); 686 | Ok(()) 687 | } 688 | 689 | #[test] 690 | fn ctrl_alt_f3() -> anyhow::Result<()> { 691 | // 03 fd 01 01 01 00 00 00 00 00 01 05 3c 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 692 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 693 | let msg = kbd.build_key_msg("ctrl-alt-f3", 1u8, 1u8, 0)?; 694 | println!("{:02x?}", msg); 695 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 696 | assert_eq!(msg[4], 0x01, "checking byte 4"); 697 | for i in msg.iter().take(10).skip(5) { 698 | assert_eq!(*i, 0x00); 699 | } 700 | assert_eq!(msg[10], 0x01, "checking byte 10"); 701 | assert_eq!(msg[11], 0x05, "checking byte 11"); 702 | assert_eq!(msg[12], 0x3c, "checking byte 12"); 703 | Ok(()) 704 | } 705 | 706 | #[test] 707 | fn led_mode3_blue_layer_3() -> anyhow::Result<()> { 708 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 709 | let msg = kbd.program_led(3, 3, LedColor::Blue); 710 | println!("{:02x?}", msg); 711 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 712 | assert_eq!(msg[0], 0x03, "checking first byte of led programming"); 713 | assert_eq!(msg[1], 0xfe, "checking second byte of led programming"); 714 | assert_eq!(msg[2], 0xb0, "checking third byte of led programming"); 715 | assert_eq!(msg[3], 0x03, "checking layer led"); 716 | assert_eq!(msg[4], 0x08, "checking fifth byte of led programming"); 717 | for i in msg.iter().take(10).skip(5) { 718 | assert_eq!(*i, 0x00); 719 | } 720 | assert_eq!(msg[10], 0x01, "checking eleventh byte of programming led"); 721 | assert_eq!(msg[12], 0x63, "checking mode and color of programming led"); 722 | Ok(()) 723 | } 724 | 725 | #[test] 726 | fn calculator() -> anyhow::Result<()> { 727 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 728 | let msg = kbd.build_key_msg("calculator", 1u8, 1u8, 0)?; 729 | println!("{:02x?}", msg); 730 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 731 | assert_eq!(msg[4], 0x02, "checking byte 4"); 732 | for i in msg.iter().take(10).skip(5) { 733 | assert_eq!(*i, 0x00); 734 | } 735 | assert_eq!(msg[10], 0x01, "checking byte 10"); 736 | assert_eq!(msg[11], 0x92, "checking byte 11"); 737 | assert_eq!(msg[12], 0x01, "checking byte 12"); 738 | Ok(()) 739 | } 740 | 741 | #[test] 742 | fn back() -> anyhow::Result<()> { 743 | let kbd = Keyboard884x::new(None, 0, 0, 0x8842)?; 744 | let msg = kbd.build_key_msg("webpageback", 1u8, 1u8, 0)?; 745 | println!("{:02x?}", msg); 746 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 747 | assert_eq!(msg[4], 0x02, "checking byte 4"); 748 | for i in msg.iter().take(10).skip(5) { 749 | assert_eq!(*i, 0x00); 750 | } 751 | assert_eq!(msg[10], 0x02, "checking byte 10"); 752 | assert_eq!(msg[11], 0x24, "checking byte 11"); 753 | assert_eq!(msg[12], 0x02, "checking byte 12"); 754 | Ok(()) 755 | } 756 | } 757 | -------------------------------------------------------------------------------- /src/keyboard/k8890.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | consts, 3 | keyboard::{ 4 | Configuration, Keyboard, LedColor, MediaCode, Messages, Modifier, MouseAction, MouseButton, 5 | WellKnownCode, 6 | }, 7 | Macropad, 8 | }; 9 | use anyhow::{anyhow, Result}; 10 | use log::debug; 11 | use num::ToPrimitive; 12 | use rusb::{Context, DeviceHandle}; 13 | use std::str::FromStr; 14 | 15 | pub struct Keyboard8890 { 16 | handle: Option>, 17 | out_endpoint: u8, 18 | led_programmed: bool, 19 | } 20 | 21 | impl Configuration for Keyboard8890 { 22 | fn read_macropad_config(&mut self, _layer: &u8) -> Result { 23 | Err(anyhow!("not supported for this macropad")) 24 | } 25 | } 26 | 27 | impl Messages for Keyboard8890 { 28 | fn read_config(&self, _keys: u8, _encoders: u8, _layer: u8) -> Vec { 29 | unimplemented!("reading configuration from this macropad is not supported"); 30 | } 31 | 32 | fn device_type(&self) -> Vec { 33 | unimplemented!("reading device type is not supported"); 34 | } 35 | 36 | fn program_led(&self, mode: u8, _layer: u8, _color: LedColor) -> Vec { 37 | let mut msg = vec![0x03, 0xb0, 0x18, mode]; 38 | let size = consts::PACKET_SIZE - msg.len(); 39 | msg.extend_from_slice(&vec![0; size]); 40 | msg 41 | } 42 | 43 | fn end_program(&self) -> Vec { 44 | let last_byte = if self.led_programmed { 0xa1 } else { 0xaa }; 45 | let mut msg = vec![0x03, 0xaa, last_byte]; 46 | let size = consts::PACKET_SIZE - msg.len(); 47 | msg.extend_from_slice(&vec![0; size]); 48 | msg 49 | } 50 | } 51 | 52 | impl Keyboard for Keyboard8890 { 53 | fn program(&mut self, macropad: &Macropad) -> Result<()> { 54 | debug!("programming keyboard - NOTE: hardcoding to layer 1"); 55 | 56 | // FIXME: currently hardcoding the layer to 1 as the only 8890 device 57 | // i have seen only has support for one layer. if we know of 58 | // one that has multiple layers, we should refactor this then 59 | self.send(&self.begin_programming(1))?; 60 | 61 | // get our layout of buttons relative to programming orientation 62 | let layout = self.get_layout( 63 | macropad.device.orientation, 64 | macropad.device.rows, 65 | macropad.device.cols, 66 | )?; 67 | debug!("layout: {layout:?}"); 68 | 69 | for (i, layer) in macropad.layers.iter().enumerate() { 70 | let mut key_num; 71 | for (row_idx, row) in layer.buttons.iter().enumerate() { 72 | for (col_idx, btn) in row.iter().enumerate() { 73 | debug!("get position in layout: row_idx: {row_idx} col_idx: {col_idx}"); 74 | key_num = layout[row_idx][col_idx]; 75 | debug!( 76 | "program layer: {} key: 0x{:02x} to: {btn:?}", 77 | i + 1, 78 | key_num 79 | ); 80 | let keys: Vec<_> = btn.mapping.split(',').collect(); 81 | if keys.len() > consts::MAX_KEY_PRESSES_8890 { 82 | return Err(anyhow!( 83 | "maximum key presses for this macropad is {}", 84 | consts::MAX_KEY_PRESSES_8890 85 | )); 86 | } 87 | for msg in self.map_key(btn.mapping.to_string(), key_num)? { 88 | self.send(&msg)?; 89 | } 90 | } 91 | } 92 | key_num = 0x0du8; 93 | for knob in &layer.knobs { 94 | debug!( 95 | "programming knob ccw: {} cw: {} push: {}", 96 | knob.ccw.mapping, knob.cw.mapping, knob.press.mapping 97 | ); 98 | let mut btn; 99 | for i in 0..3 { 100 | match i { 101 | 0 => btn = knob.ccw.clone(), 102 | 1 => btn = knob.press.clone(), 103 | 2 => btn = knob.cw.clone(), 104 | _ => unreachable!("should not get here"), 105 | } 106 | let keys: Vec<_> = btn.mapping.split(',').collect(); 107 | if keys.len() > consts::MAX_KEY_PRESSES_8890 { 108 | return Err(anyhow!( 109 | "maximum key presses for this macropad is {}", 110 | consts::MAX_KEY_PRESSES_8890 111 | )); 112 | } 113 | for msg in self.map_key(btn.mapping.to_string(), key_num)? { 114 | self.send(&msg)?; 115 | } 116 | key_num += 1; 117 | } 118 | } 119 | } 120 | self.send(&self.end_program())?; 121 | debug!("DONE - programming keyboard"); 122 | Ok(()) 123 | } 124 | 125 | fn set_led(&mut self, mode: u8, layer: u8, _color: LedColor) -> Result<()> { 126 | if mode > 2 { 127 | return Err(anyhow!("macropad supports modes 0, 1, and 2 only")); 128 | } 129 | self.led_programmed = true; 130 | self.send(&self.begin_programming(layer))?; 131 | self.send(&self.program_led(mode, layer, LedColor::Red))?; 132 | self.send(&self.end_program())?; 133 | Ok(()) 134 | } 135 | 136 | fn get_handle(&self) -> &DeviceHandle { 137 | self.handle.as_ref().unwrap() 138 | } 139 | 140 | fn get_out_endpoint(&self) -> u8 { 141 | self.out_endpoint 142 | } 143 | 144 | fn get_in_endpoint(&self) -> u8 { 145 | unimplemented!("reading configuration from this macropad is not supported"); 146 | } 147 | } 148 | 149 | impl Keyboard8890 { 150 | pub fn new(handle: Option>, out_endpoint: u8) -> Result { 151 | let keyboard = Self { 152 | handle, 153 | out_endpoint, 154 | led_programmed: false, 155 | }; 156 | 157 | Ok(keyboard) 158 | } 159 | 160 | pub fn begin_programming(&self, layer: u8) -> Vec { 161 | let mut msg = vec![0x03, 0xa1, layer]; 162 | let size = consts::PACKET_SIZE - msg.len(); 163 | msg.extend_from_slice(&vec![0; size]); 164 | msg 165 | } 166 | 167 | fn map_key(&self, key_chord: String, key_pos: u8) -> Result>> { 168 | let mut retval = Vec::new(); 169 | let mut prepend = Vec::new(); 170 | let kc: Vec<_> = key_chord.split(',').collect(); 171 | let mut prepended = false; 172 | for (i, key) in kc.iter().enumerate() { 173 | let mut msg = vec![0x03, key_pos, 0x00, 0x00, 0x00, 0x00, 0x00]; 174 | let mut remaining = consts::PACKET_SIZE - msg.len(); 175 | let km: Vec<_> = key.split('-').collect(); 176 | let mut mouse_action = 0u8; 177 | let mut mouse_click; 178 | let mut media_key = false; 179 | let mut media_val = 0u8; 180 | //let mut m_c; 181 | let mut wkk; 182 | for mod_key in km { 183 | debug!("=====> {mod_key}"); 184 | if let Ok(w) = WellKnownCode::from_str(mod_key) { 185 | msg[2] = 0x11; 186 | msg[3] = kc.len().try_into()?; 187 | msg.extend_from_slice(&[0; 3]); 188 | remaining -= 3; 189 | let mut first_msg = msg.clone(); 190 | first_msg.extend_from_slice(&vec![0; remaining]); 191 | msg[4] = (i + 1).try_into()?; 192 | if !prepended { 193 | prepend.push(first_msg); 194 | prepended = true; 195 | } 196 | wkk = ::to_u8(&w).unwrap(); 197 | msg[6] = wkk; 198 | } else if let Ok(a) = MediaCode::from_str(mod_key) { 199 | let value = ::to_u16(&a).unwrap(); 200 | msg[2] = 0x12; 201 | msg[3] = (value & 0xFF) as u8; 202 | media_val = ((value & 0xFF00) >> 8) as u8; 203 | media_key = true; 204 | } else if let Ok(a) = MouseButton::from_str(mod_key) { 205 | mouse_click = 206 | 2u32.pow(::to_u8(&a).unwrap().into()) as u8; 207 | msg[2] = 0x13; 208 | msg[3] = mouse_click; 209 | } else if let Ok(a) = MouseAction::from_str(mod_key) { 210 | match a { 211 | MouseAction::WheelUp => mouse_action = 0x01, 212 | MouseAction::WheelDown => mouse_action = 0xff, 213 | _ => (), 214 | } 215 | msg[2] = 0x13; 216 | msg[6] = mouse_action; 217 | } else { 218 | // modifier combo (eg. shift-m) 219 | let mapping = Keyboard8890::key_mapping(mod_key)?; 220 | msg[2] = 0x11; 221 | msg[3] = kc.len().try_into()?; 222 | msg.extend_from_slice(&[0; 3]); 223 | remaining -= 3; 224 | let mut first_msg = msg.clone(); 225 | first_msg.extend_from_slice(&vec![0; remaining]); 226 | msg[4] = (i + 1).try_into()?; 227 | if !prepended { 228 | prepend.push(first_msg); 229 | prepended = true; 230 | } 231 | msg[5] |= mapping.0; 232 | msg[6] = mapping.1; 233 | } 234 | } 235 | msg.extend_from_slice(&vec![0; remaining]); 236 | for i in &prepend { 237 | retval.push(i.clone()); 238 | } 239 | if media_key { 240 | msg[4] = media_val; 241 | } 242 | prepend.clear(); 243 | retval.push(msg); 244 | } 245 | 246 | Ok(retval) 247 | } 248 | 249 | fn key_mapping(key: &str) -> Result<(u8, u8)> { 250 | let mut mc = 0; 251 | let mut wkk = 0; 252 | let values: Vec<_> = key.split('-').collect(); 253 | for i in values { 254 | if let Ok(w) = WellKnownCode::from_str(i) { 255 | wkk = ::to_u8(&w).unwrap(); 256 | } 257 | if let Ok(m) = Modifier::from_str(i) { 258 | let power = ::to_u8(&m).unwrap(); 259 | mc = 2u32.pow(power as u32) as u8; 260 | } 261 | } 262 | Ok((mc, wkk)) 263 | } 264 | } 265 | 266 | #[cfg(test)] 267 | mod tests { 268 | use crate::{ 269 | consts, 270 | keyboard::{k8890::Keyboard8890, LedColor, Messages}, 271 | }; 272 | 273 | #[test] 274 | fn test_hello() -> anyhow::Result<()> { 275 | let kbd = Keyboard8890::new(None, 0)?; 276 | let msgs = kbd.map_key("h,e,l,l,o".to_string(), 4)?; 277 | println!("{:02x?}", msgs); 278 | assert_eq!(msgs.len(), 6, "number of messages created"); 279 | for i in msgs.iter().take(6) { 280 | assert_eq!((*i).len(), consts::PACKET_SIZE, "checking msg size"); 281 | } 282 | 283 | let expected = vec![0x03, 0x04, 0x11, 0x05, 0x00, 0x00]; 284 | assert_eq!(msgs[0].len(), consts::PACKET_SIZE, "checking msg size"); 285 | assert_eq!(&expected, &msgs[0][..6], "checking message"); 286 | 287 | let expected = vec![0x03, 0x04, 0x11, 0x05, 0x01, 0x00, 0x0b]; 288 | assert_eq!(msgs[1].len(), consts::PACKET_SIZE, "checking msg size"); 289 | assert_eq!(&expected, &msgs[1][..7], "checking message"); 290 | 291 | let expected = vec![0x03, 0x04, 0x11, 0x05, 0x02, 0x00, 0x08]; 292 | assert_eq!(msgs[1].len(), consts::PACKET_SIZE, "checking msg size"); 293 | assert_eq!(&expected, &msgs[2][..7], "checking message"); 294 | 295 | let expected = vec![0x03, 0x04, 0x11, 0x05, 0x03, 0x00, 0x0f]; 296 | assert_eq!(msgs[1].len(), consts::PACKET_SIZE, "checking msg size"); 297 | assert_eq!(&expected, &msgs[3][..7], "checking message"); 298 | 299 | let expected = vec![0x03, 0x04, 0x11, 0x05, 0x04, 0x00, 0x0f]; 300 | assert_eq!(msgs[1].len(), consts::PACKET_SIZE, "checking msg size"); 301 | assert_eq!(&expected, &msgs[4][..7], "checking message"); 302 | 303 | let expected = vec![0x03, 0x04, 0x11, 0x05, 0x05, 0x00, 0x12]; 304 | assert_eq!(msgs[1].len(), consts::PACKET_SIZE, "checking msg size"); 305 | assert_eq!(&expected, &msgs[5][..7], "checking message"); 306 | 307 | Ok(()) 308 | } 309 | 310 | #[test] 311 | fn ctrl_a_ctrl_s() -> anyhow::Result<()> { 312 | let kbd = Keyboard8890::new(None, 0)?; 313 | let msgs = kbd.map_key("ctrl-a,ctrl-s".to_string(), 3)?; 314 | println!("{:02x?}", msgs); 315 | for i in msgs.iter().take(3) { 316 | assert_eq!((*i).len(), consts::PACKET_SIZE, "checking msg size"); 317 | } 318 | assert_eq!(msgs.len(), 3, "number of messages created"); 319 | 320 | let expected = vec![0x03, 0x03, 0x11, 0x02, 0x00, 0x00]; 321 | assert_eq!(msgs[0].len(), consts::PACKET_SIZE, "checking msg size"); 322 | assert_eq!(&expected, &msgs[0][..6], "checking message"); 323 | 324 | let expected = vec![0x03, 0x03, 0x11, 0x02, 0x01, 0x01, 0x04]; 325 | assert_eq!(msgs[1].len(), consts::PACKET_SIZE, "checking msg size"); 326 | assert_eq!(&expected, &msgs[1][..7], "checking message"); 327 | 328 | let expected = vec![0x03, 0x03, 0x11, 0x02, 0x02, 0x01, 0x16]; 329 | assert_eq!(msgs[2].len(), consts::PACKET_SIZE, "checking msg size"); 330 | assert_eq!(&expected, &msgs[2][..7], "checking message"); 331 | Ok(()) 332 | } 333 | 334 | #[test] 335 | fn a_key() -> anyhow::Result<()> { 336 | let kbd = Keyboard8890::new(None, 0)?; 337 | let msgs = kbd.map_key("a".to_string(), 1)?; 338 | println!("{:02x?}", msgs); 339 | assert_eq!(msgs.len(), 2, "number of messages created"); 340 | let expected = vec![0x03, 0x01, 0x11, 0x01, 0x00, 0x00]; 341 | assert_eq!(msgs[0].len(), consts::PACKET_SIZE, "checking msg size"); 342 | assert_eq!(&expected, &msgs[0][..6], "checking message"); 343 | 344 | let expected = vec![0x03, 0x01, 0x11, 0x01, 0x01, 0x00, 0x04]; 345 | assert_eq!(msgs[1].len(), consts::PACKET_SIZE, "checking msg size"); 346 | assert_eq!(&expected, &msgs[1][..7], "checking message"); 347 | Ok(()) 348 | } 349 | 350 | #[test] 351 | fn led_mode2() -> anyhow::Result<()> { 352 | let mut kbd = Keyboard8890::new(None, 0)?; 353 | kbd.led_programmed = true; 354 | let msg = kbd.program_led(2, 1, LedColor::Red); 355 | println!("{:02x?}", msg); 356 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 357 | assert_eq!(msg[1], 0xb0, "checking first byte of led programming"); 358 | assert_eq!(msg[2], 0x18, "checking second byte of led programming"); 359 | assert_eq!(msg[3], 0x02, "checking led mode"); 360 | let msg = kbd.end_program(); 361 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 362 | assert_eq!(msg[0], 0x03, "checking first byte of end programming led"); 363 | assert_eq!(msg[1], 0xaa, "checking second byte of end programming led"); 364 | assert_eq!(msg[2], 0xa1, "checking third byte of end programming led"); 365 | Ok(()) 366 | } 367 | 368 | #[test] 369 | fn end_programming() -> anyhow::Result<()> { 370 | let kbd = Keyboard8890::new(None, 0)?; 371 | let msg = kbd.end_program(); 372 | println!("{:02x?}", msg); 373 | assert_eq!(msg.len(), consts::PACKET_SIZE, "checking msg size"); 374 | assert_eq!(msg[0], 0x03, "checking first byte of end programming"); 375 | assert_eq!(msg[1], 0xaa, "checking second byte of end programming"); 376 | assert_eq!(msg[2], 0xaa, "checking third byte of end programming"); 377 | Ok(()) 378 | } 379 | 380 | #[test] 381 | fn volume_up() -> anyhow::Result<()> { 382 | // 03 01 12 e9 000000... 383 | let kbd = Keyboard8890::new(None, 0)?; 384 | let msgs = kbd.map_key("volumeup".to_string(), 1)?; 385 | println!("{:02x?}", msgs); 386 | assert_eq!(msgs.len(), 1, "number of messages created"); 387 | let expected = vec![0x03, 0x01, 0x12, 0xe9, 0x00, 0x00, 0x00, 0x00]; 388 | assert_eq!(msgs[0].len(), consts::PACKET_SIZE, "checking msg size"); 389 | assert_eq!(&expected, &msgs[0][..8], "checking message"); 390 | Ok(()) 391 | } 392 | 393 | #[test] 394 | fn calculator() -> anyhow::Result<()> { 395 | // 03 01 12 e9 01 000000... 396 | let kbd = Keyboard8890::new(None, 0)?; 397 | let msgs = kbd.map_key("calculator".to_string(), 1)?; 398 | println!("{:02x?}", msgs); 399 | assert_eq!(msgs.len(), 1, "number of messages created"); 400 | let expected = vec![0x03, 0x01, 0x12, 0x92, 0x01, 0x00, 0x00, 0x00]; 401 | assert_eq!(msgs[0].len(), consts::PACKET_SIZE, "checking msg size"); 402 | assert_eq!(&expected, &msgs[0][..8], "checking message"); 403 | Ok(()) 404 | } 405 | 406 | #[test] 407 | fn back() -> anyhow::Result<()> { 408 | // 03 01 12 24 02 000000... 409 | let kbd = Keyboard8890::new(None, 0)?; 410 | let msgs = kbd.map_key("webpageback".to_string(), 1)?; 411 | println!("{:02x?}", msgs); 412 | assert_eq!(msgs.len(), 1, "number of messages created"); 413 | let expected = vec![0x03, 0x01, 0x12, 0x24, 0x02, 0x00, 0x00, 0x00]; 414 | assert_eq!(msgs[0].len(), consts::PACKET_SIZE, "checking msg size"); 415 | assert_eq!(&expected, &msgs[0][..8], "checking message"); 416 | Ok(()) 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /src/keyboard/mod.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod k884x; 2 | pub(crate) mod k8890; 3 | 4 | use crate::{config, config::Orientation, consts, mapping::Macropad}; 5 | 6 | use std::fmt::Display; 7 | 8 | use anyhow::{ensure, Result}; 9 | use enumset::{EnumSet, EnumSetType}; 10 | use log::debug; 11 | use num_derive::{FromPrimitive, ToPrimitive}; 12 | use rusb::{Context, DeviceHandle, Error::Timeout}; 13 | use strum_macros::{Display, EnumIter, EnumMessage, EnumString}; 14 | 15 | use itertools::Itertools as _; 16 | 17 | pub trait Messages { 18 | /// Returns the message to the macropad to get its configuration 19 | /// 20 | /// #Arguments 21 | /// `keys` - number of keys on device 22 | /// `encoders` - number of endoders on device 23 | /// `layer` - layer to read 24 | /// 25 | fn read_config(&self, keys: u8, encoders: u8, layer: u8) -> Vec; 26 | 27 | /// Returns the message to get the device type 28 | /// 29 | fn device_type(&self) -> Vec; 30 | 31 | /// Returns the message to program the LEDs on the macropad based on the 32 | /// specified `mode` and `color` 33 | /// 34 | /// #Arguments 35 | /// `mode` - preset mode of the LED 36 | /// `layer` - layer to program 37 | /// `color` - the color to use for the mode 38 | /// 39 | fn program_led(&self, mode: u8, layer: u8, color: LedColor) -> Vec; 40 | 41 | /// Returns the "end of programming" message for the device. This message 42 | /// effectively tell the device to 'save its configuration' so when it is 43 | /// unplugged, it retains its settings 44 | /// 45 | fn end_program(&self) -> Vec; 46 | } 47 | 48 | pub trait Configuration { 49 | /// Returns the Macropad with its configuration settings for the specified layer 50 | /// 51 | /// #Arguments 52 | /// `layer` - layer to read configuration for 53 | /// 54 | fn read_macropad_config(&mut self, layer: &u8) -> Result; 55 | 56 | /// Returns the layout button configuration for the specified orientation 57 | /// 58 | /// #Arguments 59 | /// `orientation` - orientation of the macropad 60 | /// `rows` - number of rows 61 | /// `cols` - number of columns 62 | /// 63 | fn get_layout(&self, orientation: Orientation, rows: u8, cols: u8) -> Result>> { 64 | // normalize layout to "normal" orientation 65 | let default_layout = if orientation == Orientation::Clockwise 66 | || orientation == Orientation::CounterClockwise 67 | { 68 | // transpose 69 | self.default_key_numbers(cols, rows) 70 | } else { 71 | self.default_key_numbers(rows, cols) 72 | }; 73 | debug!("default_layout: {default_layout:?}"); 74 | 75 | let layout = match orientation { 76 | Orientation::Clockwise => config::get_keys_clockwise(default_layout), 77 | Orientation::CounterClockwise => config::get_keys_counter_clockwise(default_layout), 78 | Orientation::UpsideDown => config::get_keys_upsidedown(default_layout), 79 | Orientation::Normal => default_layout, 80 | }; 81 | 82 | Ok(layout) 83 | } 84 | 85 | /// Returns the default 'normal' orientation button numbers for programming 86 | /// 87 | /// #Arguments 88 | /// `rows` - number of rows 89 | /// `cols` - number of columns 90 | /// 91 | fn default_key_numbers(&self, rows: u8, cols: u8) -> Vec> { 92 | let mut layout: Vec> = Vec::new(); 93 | let mut idx = 1u8; 94 | for _i in 0..rows { 95 | let mut tmp = Vec::new(); 96 | for _j in 0..cols { 97 | tmp.push(idx); 98 | idx += 1; 99 | } 100 | layout.push(tmp); 101 | } 102 | layout 103 | } 104 | } 105 | 106 | pub trait Keyboard: Messages + Configuration { 107 | /// Programs the macropad based on the specified `Macropad` 108 | /// 109 | /// #Arguments 110 | /// `macropad` - configuration to be programmed 111 | /// 112 | fn program(&mut self, macropad: &Macropad) -> Result<()>; 113 | 114 | /// Programs the LEDs on the macropad 115 | /// 116 | /// #Arguments 117 | /// `mode` - preset mode of the LED 118 | /// `layer` - layer to program 119 | /// `color` - the color to use for the mode 120 | /// 121 | fn set_led(&mut self, mode: u8, layer: u8, color: LedColor) -> Result<()>; 122 | 123 | /// Returns the handle of the device 124 | /// 125 | fn get_handle(&self) -> &DeviceHandle; 126 | 127 | /// Returns the out endpoint of the device (write) 128 | /// 129 | fn get_out_endpoint(&self) -> u8; 130 | 131 | /// Returns the in endpoint of the device (read) 132 | /// 133 | fn get_in_endpoint(&self) -> u8; 134 | 135 | /// Sends the specified `msg` over usb to the out endpoint as an 136 | /// USB Interrupt out message. Error is through if not all bytes 137 | /// of the message could be sent 138 | /// 139 | /// #Arguments 140 | /// `msg` - message to be sent 141 | /// 142 | fn send(&mut self, msg: &[u8]) -> Result<()> { 143 | let written = self.get_handle().write_interrupt( 144 | self.get_out_endpoint(), 145 | msg, 146 | consts::DEFAULT_TIMEOUT, 147 | )?; 148 | ensure!(written == msg.len(), "not all data written"); 149 | debug!("msg: {:02x?}", msg); 150 | debug!("--------------------------------------------------"); 151 | Ok(()) 152 | } 153 | 154 | /// Reads data from macropad and stores it in buf 155 | /// 156 | /// #Arguments 157 | /// `buf` - buffer to store the data that is read 158 | /// 159 | fn recieve(&mut self, buf: &mut [u8]) -> Result { 160 | let read = 161 | self.get_handle() 162 | .read_interrupt(self.get_in_endpoint(), buf, consts::DEFAULT_TIMEOUT); 163 | 164 | let mut bytes_read = 0; 165 | if read.is_err() { 166 | let e = read.err().unwrap(); 167 | match e { 168 | Timeout => { 169 | debug!("timeout on read"); 170 | return Ok(0); 171 | } 172 | 173 | _ => { 174 | eprintln!("error reading interrupt - {}", e); 175 | } 176 | }; 177 | } else { 178 | bytes_read = read.unwrap(); 179 | } 180 | 181 | debug!("bytes read: {bytes_read}"); 182 | debug!("data: {:02x?}", buf); 183 | 184 | Ok(bytes_read) 185 | } 186 | } 187 | 188 | #[derive(Debug, Default, ToPrimitive, Clone, Copy, Display, clap::ValueEnum)] 189 | pub enum LedColor { 190 | Red = 0x10, 191 | Orange = 0x20, 192 | Yellow = 0x30, 193 | Green = 0x40, 194 | #[default] 195 | Cyan = 0x50, 196 | Blue = 0x60, 197 | Purple = 0x70, 198 | } 199 | 200 | #[allow(unused)] 201 | #[derive(Debug, Clone, Copy, Display)] 202 | #[repr(u8)] 203 | pub enum KnobAction { 204 | #[strum(serialize = "ccw")] 205 | RotateCCW, 206 | #[strum(serialize = "press")] 207 | Press, 208 | #[strum(serialize = "cw")] 209 | RotateCW, 210 | } 211 | 212 | #[derive( 213 | Debug, ToPrimitive, FromPrimitive, EnumSetType, EnumString, EnumIter, EnumMessage, Display, 214 | )] 215 | #[strum(ascii_case_insensitive)] 216 | pub enum Modifier { 217 | #[strum(serialize = "ctrl")] 218 | Ctrl, 219 | #[strum(serialize = "shift")] 220 | Shift, 221 | #[strum(serialize = "alt", serialize = "opt")] 222 | Alt, 223 | #[strum(serialize = "win", serialize = "cmd")] 224 | Win, 225 | #[strum(serialize = "rctrl")] 226 | RightCtrl, 227 | #[strum(serialize = "rshift")] 228 | RightShift, 229 | #[strum(serialize = "ralt", serialize = "ropt")] 230 | RightAlt, 231 | #[strum(serialize = "rwin", serialize = "rcmd")] 232 | RightWin, 233 | } 234 | 235 | #[derive( 236 | Debug, 237 | FromPrimitive, 238 | ToPrimitive, 239 | Clone, 240 | Copy, 241 | PartialEq, 242 | Eq, 243 | EnumString, 244 | EnumIter, 245 | EnumMessage, 246 | Display, 247 | )] 248 | #[repr(u16)] 249 | #[strum(serialize_all = "lowercase")] 250 | #[strum(ascii_case_insensitive)] 251 | pub enum MediaCode { 252 | Next = 0xb5, 253 | #[strum(serialize = "previous", serialize = "prev")] 254 | Previous = 0xb6, 255 | Stop = 0xb7, 256 | Play = 0xcd, 257 | Mute = 0xe2, 258 | VolumeUp = 0xe9, 259 | VolumeDown = 0xea, 260 | Favorites = 0x182, 261 | Calculator = 0x192, 262 | ScreenLock = 0x19e, 263 | ScreenBrightnessUp = 0x6f, 264 | ScreenBrightnessDown = 0x70, 265 | WebPageHome = 0x0223, 266 | WebPageBack = 0x0224, 267 | WebPageForward = 0x0225, 268 | } 269 | 270 | #[derive( 271 | Debug, ToPrimitive, FromPrimitive, Clone, Copy, PartialEq, Eq, EnumString, EnumIter, Display, 272 | )] 273 | #[repr(u8)] 274 | #[strum(ascii_case_insensitive)] 275 | #[strum(serialize_all = "lowercase")] 276 | pub enum WellKnownCode { 277 | A = 0x04, 278 | B, 279 | C, 280 | D, 281 | E, 282 | F, 283 | G, 284 | H, 285 | I, 286 | J, 287 | K, 288 | L, 289 | M, 290 | N, 291 | O, 292 | P, 293 | Q, 294 | R, 295 | S, 296 | T, 297 | U, 298 | V, 299 | W, 300 | X, 301 | Y, 302 | Z, 303 | #[strum(serialize = "1")] 304 | N1, 305 | #[strum(serialize = "2")] 306 | N2, 307 | #[strum(serialize = "3")] 308 | N3, 309 | #[strum(serialize = "4")] 310 | N4, 311 | #[strum(serialize = "5")] 312 | N5, 313 | #[strum(serialize = "6")] 314 | N6, 315 | #[strum(serialize = "7")] 316 | N7, 317 | #[strum(serialize = "8")] 318 | N8, 319 | #[strum(serialize = "9")] 320 | N9, 321 | #[strum(serialize = "0")] 322 | N0, 323 | Enter, 324 | Escape, 325 | Backspace, 326 | Tab, 327 | Space, 328 | Minus, 329 | Equal, 330 | LeftBracket, 331 | RightBracket, 332 | Backslash, 333 | NonUSHash, 334 | Semicolon, 335 | Quote, 336 | Grave, 337 | Comma, 338 | Dot, 339 | Slash, 340 | CapsLock, 341 | F1, 342 | F2, 343 | F3, 344 | F4, 345 | F5, 346 | F6, 347 | F7, 348 | F8, 349 | F9, 350 | F10, 351 | F11, 352 | F12, 353 | PrintScreen, 354 | ScrollLock, 355 | Pause, 356 | Insert, 357 | Home, 358 | PageUp, 359 | Delete, 360 | End, 361 | PageDown, 362 | Right, 363 | Left, 364 | Down, 365 | Up, 366 | NumLock, 367 | NumPadSlash, 368 | NumPadAsterisk, 369 | NumPadMinus, 370 | NumPadPlus, 371 | NumPadEnter, 372 | NumPad1, 373 | NumPad2, 374 | NumPad3, 375 | NumPad4, 376 | NumPad5, 377 | NumPad6, 378 | NumPad7, 379 | NumPad8, 380 | NumPad9, 381 | NumPad0, 382 | NumPadDot, 383 | NonUSBackslash, 384 | Application, 385 | Power, 386 | NumPadEqual, 387 | F13, 388 | F14, 389 | F15, 390 | F16, 391 | F17, 392 | F18, 393 | F19, 394 | F20, 395 | F21, 396 | F22, 397 | F23, 398 | F24, 399 | } 400 | 401 | #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString, Display)] 402 | #[strum(ascii_case_insensitive)] 403 | #[repr(u8)] 404 | pub enum MouseModifier { 405 | Ctrl = 0x01, 406 | Shift = 0x02, 407 | Alt = 0x04, 408 | } 409 | 410 | #[derive(ToPrimitive, EnumString, Debug, EnumSetType, EnumIter, Display)] 411 | pub enum MouseButton { 412 | #[strum(serialize = "click")] 413 | Left, 414 | #[strum(serialize = "rclick")] 415 | Right, 416 | #[strum(serialize = "mclick")] 417 | Middle, 418 | } 419 | 420 | pub type MouseButtons = EnumSet; 421 | 422 | #[derive(Debug, Clone, Copy, PartialEq, Eq, EnumString)] 423 | #[strum(ascii_case_insensitive)] 424 | pub enum MouseAction { 425 | Click(MouseButtons), 426 | WheelUp, 427 | WheelDown, 428 | } 429 | 430 | impl Display for MouseAction { 431 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 432 | match self { 433 | MouseAction::Click(buttons) => { 434 | write!(f, "{}", buttons.iter().format("+"))?; 435 | } 436 | MouseAction::WheelUp => { 437 | write!(f, "wheelup")?; 438 | } 439 | MouseAction::WheelDown => { 440 | write!(f, "wheeldown")?; 441 | } 442 | } 443 | Ok(()) 444 | } 445 | } 446 | 447 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 448 | pub struct MouseEvent(pub MouseAction, pub Option); 449 | 450 | impl Display for MouseEvent { 451 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 452 | let Self(action, modifier) = self; 453 | if let Some(modifier) = modifier { 454 | write!(f, "{}-", modifier)?; 455 | } 456 | write!(f, "{}", action)?; 457 | Ok(()) 458 | } 459 | } 460 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod consts; 3 | mod decoder; 4 | mod keyboard; 5 | mod mapping; 6 | mod options; 7 | mod parse; 8 | 9 | use crate::consts::PRODUCT_IDS; 10 | use crate::decoder::Decoder; 11 | use crate::keyboard::{ 12 | k884x, k8890, Keyboard, MediaCode, Modifier, MouseAction, MouseButton, WellKnownCode, 13 | }; 14 | use crate::mapping::Macropad; 15 | use crate::options::Options; 16 | use crate::options::{Command, LedCommand}; 17 | 18 | use anyhow::{anyhow, ensure, Result}; 19 | use indoc::indoc; 20 | use itertools::Itertools; 21 | use keyboard::LedColor; 22 | use log::debug; 23 | use mapping::Mapping; 24 | use rusb::{Context, Device, DeviceDescriptor, Direction, TransferType}; 25 | 26 | use anyhow::Context as _; 27 | use clap::Parser as _; 28 | use rusb::UsbContext as _; 29 | use strum::EnumMessage as _; 30 | use strum::IntoEnumIterator as _; 31 | 32 | fn main() -> Result<()> { 33 | env_logger::init(); 34 | let options = Options::parse(); 35 | debug!("options: {:?}", options.devel_options); 36 | 37 | match &options.command { 38 | Command::ShowKeys => { 39 | println!("Modifiers: "); 40 | for m in Modifier::iter() { 41 | println!(" - {}", m.get_serializations().iter().join(" / ")); 42 | } 43 | 44 | println!(); 45 | println!("Keys:"); 46 | for c in WellKnownCode::iter() { 47 | println!(" - {c}"); 48 | } 49 | 50 | println!(); 51 | println!("Custom key syntax (use decimal code): <110>"); 52 | 53 | println!(); 54 | println!("Media keys:"); 55 | for c in MediaCode::iter() { 56 | println!(" - {}", c.get_serializations().iter().join(" / ")); 57 | } 58 | 59 | println!(); 60 | println!("Mouse actions:"); 61 | println!(" - {}", MouseAction::WheelDown); 62 | println!(" - {}", MouseAction::WheelUp); 63 | for b in MouseButton::iter() { 64 | println!(" - {b}"); 65 | } 66 | } 67 | 68 | Command::Validate { 69 | config_file, 70 | product_id, 71 | device_connected, 72 | } => { 73 | if *device_connected { 74 | debug!("validating with connected device"); 75 | if let Ok(device) = find_device(consts::VENDOR_ID, None) { 76 | // read the config for buttons/knobs and validate against file 77 | if device.2 != 0x8890 { 78 | // 0x8890 does not support reading configuration 79 | let mut keyboard = open_keyboard(&options).context("opening keyboard")?; 80 | let mut buf = vec![0; consts::READ_BUF_SIZE.into()]; 81 | 82 | // get the type of device 83 | keyboard.send(&keyboard.device_type())?; 84 | let bytes_read = keyboard.recieve(&mut buf)?; 85 | if bytes_read == 0 { 86 | return Err(anyhow!( 87 | "Unable to read from device to validate mappings. Please use -p option instead to specify your device." 88 | )); 89 | } 90 | let device_info = Decoder::get_device_info(&buf); 91 | debug!( 92 | "keys: {} encoders: {}", 93 | device_info.num_keys, device_info.num_encoders 94 | ); 95 | 96 | let macropad = Mapping::read(config_file); 97 | if device_info.num_keys != macropad.device.rows * macropad.device.cols { 98 | return Err(anyhow!( 99 | "Number of keys specified in config does not match device" 100 | )); 101 | } 102 | if device_info.num_encoders != macropad.device.knobs { 103 | return Err(anyhow!( 104 | "Number of knobs specified in config does not match device" 105 | )); 106 | } 107 | } 108 | Mapping::validate(config_file, Some(device.2)) 109 | .context("validating configuration file with connected device")?; 110 | println!("config is valid 👌") 111 | } else { 112 | return Err(anyhow!( 113 | "Unable to find connected device with vendor id: 0x{:02x}", 114 | consts::VENDOR_ID 115 | )); 116 | } 117 | } else if let Some(pid) = product_id { 118 | debug!("validating with supplied product id 0x{:02x}", pid); 119 | Mapping::validate(config_file, Some(*pid)) 120 | .context("validating configuration file against specified product id")?; 121 | println!("config is valid 👌") 122 | } else { 123 | // load and validate mapping 124 | println!("validating general ron formatting - unable to do more granular checking; use -p option to check against device"); 125 | Mapping::validate(config_file, None) 126 | .context("generic validation of configuration file")?; 127 | println!("config is valid 👌") 128 | } 129 | } 130 | 131 | Command::Program { config_file } => { 132 | let config = Mapping::read(config_file); 133 | let mut keyboard = open_keyboard(&options).context("opening keyboard")?; 134 | keyboard.program(&config).context("programming macropad")?; 135 | println!("successfully programmed device"); 136 | } 137 | 138 | Command::Led(LedCommand { 139 | index, 140 | layer, 141 | led_color, 142 | }) => { 143 | let mut keyboard = open_keyboard(&options).context("opening keyboard")?; 144 | 145 | // color is not supported on 0x8890 so don't require one to be passed 146 | let color = if led_color.is_some() { 147 | led_color.unwrap() 148 | } else { 149 | LedColor::Red 150 | }; 151 | keyboard 152 | .set_led(*index, *layer, color) 153 | .context("programming LED on macropad")?; 154 | } 155 | 156 | Command::Read { layer } => { 157 | debug!("dev options: {:?}", options.devel_options); 158 | let mut keyboard = open_keyboard(&options).context("opening keyboard")?; 159 | let macropad_config = keyboard 160 | .read_macropad_config(layer) 161 | .context("reading macropad configuration")?; 162 | Mapping::print(macropad_config); 163 | } 164 | } 165 | 166 | Ok(()) 167 | } 168 | 169 | pub fn find_interface_and_endpoint( 170 | device: &Device, 171 | interface_num: Option, 172 | endpoint_addr_out: Option, 173 | endpoint_addr_in: Option, 174 | ) -> Result<(u8, u8, u8)> { 175 | debug!("out: {endpoint_addr_out:?} in: {endpoint_addr_in:?}"); 176 | let conf_desc = device 177 | .config_descriptor(0) 178 | .context("get config #0 descriptor")?; 179 | 180 | // Get the numbers of interfaces to explore 181 | let interface_nums = match interface_num { 182 | Some(iface_num) => vec![iface_num], 183 | None => conf_desc.interfaces().map(|iface| iface.number()).collect(), 184 | }; 185 | 186 | // per usb spec, the max value for a usb endpoint is 7 bits (or 127) 187 | // so set the values to be invalid by default 188 | let mut out_if = 0xFF; 189 | let mut in_if = 0xFF; 190 | for iface_num in interface_nums { 191 | debug!("Probing interface {iface_num}"); 192 | 193 | // Look for an interface with the given number 194 | let intf = conf_desc 195 | .interfaces() 196 | .find(|iface| iface_num == iface.number()) 197 | .ok_or_else(|| { 198 | anyhow!( 199 | "interface #{} not found, interface numbers:\n{:#?}", 200 | iface_num, 201 | conf_desc.interfaces().map(|i| i.number()).format(", ") 202 | ) 203 | })?; 204 | 205 | // Check that it's a HID device 206 | let intf_desc = intf.descriptors().exactly_one().map_err(|_| { 207 | anyhow!( 208 | "only one interface descriptor is expected, got:\n{:#?}", 209 | intf.descriptors().format("\n") 210 | ) 211 | })?; 212 | 213 | let descriptors = intf_desc.endpoint_descriptors(); 214 | for endpoint in descriptors { 215 | // check packet size 216 | if endpoint.max_packet_size() != (consts::PACKET_SIZE - 1).try_into()? { 217 | continue; 218 | } 219 | 220 | debug!("==> {:?} direction: {:?}", endpoint, endpoint.direction()); 221 | if endpoint.transfer_type() == TransferType::Interrupt 222 | && endpoint.direction() == Direction::Out 223 | { 224 | if let Some(ea) = endpoint_addr_out { 225 | if endpoint.address() == ea { 226 | debug!("Found OUT endpoint {endpoint:?}"); 227 | out_if = endpoint.address(); 228 | } 229 | } else { 230 | debug!("Found OUT endpoint {endpoint:?}"); 231 | out_if = endpoint.address(); 232 | } 233 | } 234 | if endpoint.transfer_type() == TransferType::Interrupt 235 | && endpoint.direction() == Direction::In 236 | { 237 | if let Some(ea) = endpoint_addr_in { 238 | if endpoint.address() == ea { 239 | debug!("Found IN endpoint {endpoint:?}"); 240 | in_if = endpoint.address(); 241 | } 242 | } else { 243 | debug!("Found IN endpoint {endpoint:?}"); 244 | in_if = endpoint.address(); 245 | } 246 | } 247 | } 248 | debug!("ep OUT addr: 0x{out_if:02x} ep IN addr: 0x{in_if:02x}"); 249 | if out_if < 0xFF && in_if < 0xFF { 250 | return Ok((iface_num, out_if, in_if)); 251 | } else if out_if < 0xFF { 252 | return Ok((iface_num, out_if, 0xFF)); 253 | } 254 | } 255 | 256 | Err(anyhow!("No valid interface/endpoint combination found!")) 257 | } 258 | 259 | fn open_keyboard(options: &Options) -> Result> { 260 | // Find USB device based on the product id 261 | let (device, desc, id_product) = find_device( 262 | options.devel_options.vendor_id, 263 | options.devel_options.product_id, 264 | ) 265 | .context("find USB device")?; 266 | 267 | ensure!( 268 | desc.num_configurations() == 1, 269 | "only one device configuration is expected" 270 | ); 271 | 272 | // Find correct endpoint 273 | let (intf_num, endpt_addr_out, endpt_addr_in) = find_interface_and_endpoint( 274 | &device, 275 | options.devel_options.interface_number, 276 | options.devel_options.out_endpoint_address, 277 | options.devel_options.in_endpoint_address, 278 | )?; 279 | 280 | // Open device. 281 | let handle = device.open().context("open USB device")?; 282 | let _ = handle.set_auto_detach_kernel_driver(true); 283 | handle 284 | .claim_interface(intf_num) 285 | .context("claim interface")?; 286 | 287 | match id_product { 288 | 0x8840 | 0x8842 => { 289 | k884x::Keyboard884x::new(Some(handle), endpt_addr_out, endpt_addr_in, id_product) 290 | .map(|v| Box::new(v) as Box) 291 | } 292 | 0x8890 => k8890::Keyboard8890::new(Some(handle), endpt_addr_out) 293 | .map(|v| Box::new(v) as Box), 294 | _ => unreachable!("This shouldn't happen!"), 295 | } 296 | } 297 | 298 | pub fn find_device(vid: u16, pid: Option) -> Result<(Device, DeviceDescriptor, u16)> { 299 | debug!("vid: 0x{vid:02x}"); 300 | if let Some(prod_id) = pid { 301 | debug!("pid: 0x{prod_id:02x}"); 302 | } else { 303 | debug!("pid: None"); 304 | } 305 | let options = vec![ 306 | #[cfg(windows)] 307 | rusb::UsbOption::use_usbdk(), 308 | ]; 309 | let usb_context = rusb::Context::with_options(&options)?; 310 | 311 | let mut found = vec![]; 312 | for device in usb_context.devices().context("get USB device list")?.iter() { 313 | let desc = device.device_descriptor().context("get USB device info")?; 314 | debug!( 315 | "Bus {:03} Device {:03} ID {:04x}:{:04x}", 316 | device.bus_number(), 317 | device.address(), 318 | desc.vendor_id(), 319 | desc.product_id() 320 | ); 321 | let product_id = desc.product_id(); 322 | 323 | if desc.vendor_id() == vid { 324 | if let Some(prod_id) = pid { 325 | if PRODUCT_IDS.contains(&prod_id) { 326 | found.push((device, desc, product_id)); 327 | } 328 | } else { 329 | found.push((device, desc, product_id)); 330 | } 331 | } 332 | } 333 | 334 | match found.len() { 335 | 0 => Err(anyhow!( 336 | "macropad device not found. Use --vendor-id and --product-id to override defaults" 337 | )), 338 | 1 => Ok(found.pop().unwrap()), 339 | _ => { 340 | let mut addresses = vec![]; 341 | for (device, _desc, _product_id) in found { 342 | let address = (device.bus_number(), device.address()); 343 | addresses.push(address); 344 | } 345 | 346 | Err(anyhow!( 347 | indoc! {" 348 | Several compatible devices are found. 349 | Unfortunately, this model of keyboard doesn't have serial number. 350 | So specify USB address using --address option. 351 | 352 | Addresses: 353 | {} 354 | "}, 355 | addresses 356 | .iter() 357 | .map(|(bus, addr)| format!("{bus}:{addr}")) 358 | .join("\n") 359 | )) 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /src/mapping.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use log::debug; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | /// Mapping configuration of a macropad 6 | #[derive(Debug, Serialize, Deserialize)] 7 | pub struct Macropad { 8 | /// Device configuration 9 | pub device: Device, 10 | /// Layer configuration 11 | pub layers: Vec, 12 | } 13 | 14 | impl Macropad { 15 | /// Creates a new Macropad with the specified rows, cols, and knobs 16 | /// 17 | /// #Arguments 18 | /// `rows` - number of rows 19 | /// `cols` - number of columns 20 | /// `knobs` - number of rotary encoders 21 | /// 22 | pub fn new(rows: u8, cols: u8, knobs: u8) -> Self { 23 | Self { 24 | device: Device { 25 | orientation: Orientation::Normal, 26 | rows, 27 | cols, 28 | knobs, 29 | }, 30 | layers: vec![ 31 | Layer::new(rows, cols, knobs), 32 | Layer::new(rows, cols, knobs), 33 | Layer::new(rows, cols, knobs), 34 | ], 35 | } 36 | } 37 | } 38 | 39 | /// Device configuration 40 | #[derive(Debug, Serialize, Deserialize)] 41 | pub struct Device { 42 | /// Orientation of device 43 | pub orientation: Orientation, 44 | /// Number of rows 45 | pub rows: u8, 46 | /// Number of columns 47 | pub cols: u8, 48 | /// Number of knobs 49 | pub knobs: u8, 50 | } 51 | 52 | /// Layer configuration 53 | #[derive(Debug, Serialize, Deserialize)] 54 | pub struct Layer { 55 | /// Key mappings 56 | pub buttons: Vec>, 57 | /// Rotary encoder mappings 58 | pub knobs: Vec, 59 | } 60 | 61 | impl Layer { 62 | /// Creates a new empty mapping structure for a layer given device configuration 63 | /// 64 | /// #Arguments 65 | /// `rows` - number of rows 66 | /// `cols` - number of columns 67 | /// `knobs` - number of rotary encoders 68 | /// 69 | pub fn new(rows: u8, cols: u8, num_knobs: u8) -> Self { 70 | let mut buttons = Vec::new(); 71 | for _i in 0..rows { 72 | buttons.push(vec![Button::new(); cols.into()]); 73 | } 74 | 75 | let mut knobs = Vec::new(); 76 | for _i in 0..num_knobs { 77 | knobs.push(Knob { 78 | ccw: Button::new(), 79 | press: Button::new(), 80 | cw: Button::new(), 81 | }); 82 | } 83 | Self { buttons, knobs } 84 | } 85 | } 86 | 87 | /// Mapping for a button 88 | #[derive(Debug, Serialize, Deserialize, Clone)] 89 | pub struct Button { 90 | /// Delay value (only used if mapping is a keychord; has ',' for key presses) 91 | pub delay: u16, 92 | /// Mapping for the button 93 | pub mapping: String, 94 | } 95 | 96 | impl Button { 97 | /// Creates a new Button with 0 delay and empty mapping 98 | /// 99 | pub fn new() -> Self { 100 | Self { 101 | delay: 0, 102 | mapping: String::new(), 103 | } 104 | } 105 | } 106 | 107 | /// Mapping for a knob 108 | #[derive(Debug, Serialize, Deserialize)] 109 | pub struct Knob { 110 | /// Counter-Clockwise turn 111 | pub ccw: Button, 112 | /// Pressing the knob 113 | pub press: Button, 114 | /// Clockwise turn 115 | pub cw: Button, 116 | } 117 | 118 | use ron::de::from_reader; 119 | use ron::ser::{to_string_pretty, PrettyConfig}; 120 | use std::fs::File; 121 | use std::str::FromStr; 122 | 123 | use crate::config::Orientation; 124 | use crate::consts; 125 | use crate::keyboard::{MediaCode, Modifier, WellKnownCode}; 126 | 127 | pub struct Mapping {} 128 | 129 | impl Mapping { 130 | /// Reads the specified configuration file and returns a Macropad 131 | /// 132 | /// #Arguments 133 | /// `cfg_file` - configuration file to be read and parsed 134 | /// 135 | pub fn read(cfg_file: &str) -> Macropad { 136 | debug!("configuration file: {}", cfg_file); 137 | let f = File::open(cfg_file).expect("Failed opening file"); 138 | let config: Macropad = match from_reader(f) { 139 | Ok(x) => x, 140 | Err(e) => { 141 | println!("Failed to load config: {}", e); 142 | std::process::exit(1); 143 | } 144 | }; 145 | config 146 | } 147 | 148 | /// Prints the Macropad to stdout 149 | /// 150 | /// #Arguments 151 | /// `config` - macropad to be printed 152 | /// 153 | pub fn print(config: Macropad) { 154 | let pretty = PrettyConfig::new() 155 | .depth_limit(4) 156 | .separate_tuple_members(true) 157 | .enumerate_arrays(false); 158 | 159 | let s = to_string_pretty(&config, pretty).expect("Serialization failed"); 160 | println!("{s}"); 161 | } 162 | 163 | /// Validates the configuration against the specified product ID. If the product ID 164 | /// is not specified, does general validation. Returns `Result` on success; Err 165 | /// otherwise 166 | /// 167 | /// #Arguments 168 | /// `cfg_file` - configuration file to validate 169 | /// `pid` - Optional product id to validate against 170 | /// 171 | pub fn validate(cfg_file: &str, pid: Option) -> anyhow::Result<()> { 172 | // get the maximum number a key can be programmed for 173 | let mut max_programmable_keys = 0xff; 174 | if let Some(max) = pid { 175 | match max { 176 | 0x8840 | 0x8842 => max_programmable_keys = consts::MAX_KEY_PRESSES_884X, 177 | 0x8890 => max_programmable_keys = consts::MAX_KEY_PRESSES_8890, 178 | _ => { 179 | let err_msg = format!("Unknown product id 0x{:02x}", pid.unwrap()); 180 | return Err(anyhow!(err_msg)); 181 | } 182 | } 183 | } 184 | debug!("max_programmable_keys: {max_programmable_keys}"); 185 | debug!("pid: {pid:?}"); 186 | 187 | // check layers 188 | let cfg = Self::read(cfg_file); 189 | 190 | if cfg.layers.is_empty() || cfg.layers.len() > 3 { 191 | return Err(anyhow!("number of layers must be > 0 and < 4")); 192 | } 193 | 194 | // check rows/cols/knobs 195 | for (i, layer) in cfg.layers.iter().enumerate() { 196 | // row check 197 | if layer.buttons.len() != cfg.device.rows.into() { 198 | return Err(anyhow!( 199 | "number of rows mismatch at layer {}. Expected {} rows found {}", 200 | i + 1, 201 | cfg.device.rows, 202 | layer.buttons.len(), 203 | )); 204 | } 205 | 206 | // column check 207 | for (j, btn_mapping) in layer.buttons.iter().enumerate() { 208 | if btn_mapping.len() != cfg.device.cols.into() { 209 | return Err(anyhow!( 210 | "number of colums mismatch at layer {} row {}. Expected {} columns found {}", 211 | i + 1, 212 | j + 1, 213 | cfg.device.cols, 214 | btn_mapping.len() 215 | )); 216 | } 217 | 218 | // check the individual button 219 | for (k, btn) in btn_mapping.iter().enumerate() { 220 | let retval = Self::validate_key_mapping(btn, max_programmable_keys, pid); 221 | if retval.is_err() { 222 | return Err(anyhow!( 223 | "{} -- '{}' at layer {} row {} button {}", 224 | retval.err().unwrap(), 225 | btn.mapping, 226 | i + 1, 227 | j + 1, 228 | k + 1 229 | )); 230 | } 231 | } 232 | } 233 | 234 | // knob check 235 | if layer.knobs.len() != cfg.device.knobs.into() { 236 | return Err(anyhow!( 237 | "number of knobs mismatch at layer {}. Expected {} knobs found {}", 238 | i + 1, 239 | cfg.device.knobs, 240 | layer.knobs.len(), 241 | )); 242 | } 243 | 244 | // knob button mapping 245 | for (k, knob) in layer.knobs.iter().enumerate() { 246 | let retval = Self::validate_key_mapping(&knob.ccw, max_programmable_keys, pid); 247 | if retval.is_err() { 248 | return Err(anyhow!( 249 | "{} - key '{}' at layer {} knob {} in ccw", 250 | retval.err().unwrap(), 251 | &knob.ccw.mapping, 252 | i + 1, 253 | k + 1 254 | )); 255 | } 256 | let retval = Self::validate_key_mapping(&knob.press, max_programmable_keys, pid); 257 | if retval.is_err() { 258 | return Err(anyhow!( 259 | "{} - key '{}' at layer {} knob {} in press", 260 | retval.err().unwrap(), 261 | &knob.press.mapping, 262 | i + 1, 263 | k + 1 264 | )); 265 | } 266 | let retval = Self::validate_key_mapping(&knob.cw, max_programmable_keys, pid); 267 | if retval.is_err() { 268 | return Err(anyhow!( 269 | "{} - key '{}' at layer {} knob {} in cw", 270 | retval.err().unwrap(), 271 | &knob.cw.mapping, 272 | i + 1, 273 | k + 1 274 | )); 275 | } 276 | } 277 | } 278 | 279 | Ok(()) 280 | } 281 | 282 | fn validate_key_mapping(btn: &Button, max_size: usize, pid: Option) -> Result<()> { 283 | // ensure we don't go over max 284 | let keys: Vec<_> = btn.mapping.split(',').collect(); 285 | if keys.len() > max_size { 286 | return Err(anyhow!( 287 | "Too many keys to map. One key can be mapped to a maximum of {} key presses", 288 | max_size 289 | )); 290 | } 291 | 292 | // check delay 293 | if max_size == consts::MAX_KEY_PRESSES_8890 { 294 | if btn.delay > 0 { 295 | println!( 296 | "Warning - 0x8890 devices do not support the delay feature - delay value [{}] will be ignored", btn.delay 297 | ); 298 | } 299 | } else if btn.delay > consts::MAX_DELAY { 300 | return Err(anyhow!( 301 | "delay value [{}] must be between 0 and 6000 msec", 302 | btn.delay 303 | )); 304 | } 305 | 306 | // check individual keys 307 | for (i, k) in keys.iter().enumerate() { 308 | let single_key: Vec<_> = k.split('-').collect(); 309 | if max_size == consts::MAX_KEY_PRESSES_8890 && i > 0 && single_key.len() > 1 { 310 | return Err(anyhow!( 311 | "0x8890 macropad only supports modifier keys on first key in sequence" 312 | )); 313 | } 314 | for sk in single_key { 315 | let da_key = Self::uppercase_first(sk); 316 | // could be media, control, or regular key 317 | let mut found = false; 318 | let mut unsupported = ""; 319 | for i in 0..4 { 320 | match i { 321 | 0 => { 322 | found = Self::is_modifier_key(&da_key); 323 | } 324 | 1 => { 325 | found = Self::is_media_key(&da_key); 326 | // 0x8890 does not support keys > 0xff 327 | if pid.is_some() && pid.unwrap() == 0x8890 && found { 328 | unsupported = match da_key.as_str() { 329 | "Play" | "Previous" | "Next" | "Mute" | "Volumeup" 330 | | "Volumedown" => "", 331 | _ => &da_key, 332 | }; 333 | } 334 | } 335 | 2 => { 336 | found = Self::is_regular_key(&da_key); 337 | } 338 | 3 => { 339 | found = Self::is_mouse_action(&da_key); 340 | } 341 | _ => (), 342 | } 343 | if !unsupported.is_empty() { 344 | return Err(anyhow!("unsupported media key")); 345 | } 346 | if found { 347 | break; 348 | } 349 | } 350 | if !found { 351 | return Err(anyhow!("unknown key - {}", sk)); 352 | } 353 | } 354 | } 355 | Ok(()) 356 | } 357 | 358 | fn uppercase_first(data: &str) -> String { 359 | let mut result = String::new(); 360 | let mut first = true; 361 | for value in data.chars() { 362 | if first { 363 | result.push(value.to_ascii_uppercase()); 364 | first = false; 365 | } else { 366 | result.push(value); 367 | } 368 | } 369 | result 370 | } 371 | 372 | fn is_modifier_key(keystr: &str) -> bool { 373 | let ck = Modifier::from_str(keystr); 374 | if ck.is_ok() { 375 | return true; 376 | } 377 | false 378 | } 379 | 380 | fn is_media_key(keystr: &str) -> bool { 381 | let mk = MediaCode::from_str(keystr); 382 | if mk.is_ok() { 383 | return true; 384 | } 385 | false 386 | } 387 | 388 | fn is_regular_key(keystr: &str) -> bool { 389 | let rk = WellKnownCode::from_str(keystr); 390 | if rk.is_ok() { 391 | return true; 392 | } 393 | false 394 | } 395 | 396 | fn is_mouse_action(keystr: &str) -> bool { 397 | matches!( 398 | keystr.to_lowercase().as_str(), 399 | "wheelup" | "wheeldown" | "click" | "mclick" | "rclick" 400 | ) 401 | } 402 | } 403 | 404 | #[cfg(test)] 405 | mod tests { 406 | 407 | use crate::mapping::Button; 408 | use crate::{consts, mapping::Mapping}; 409 | 410 | #[test] 411 | fn mapping_read() { 412 | Mapping::read("./mapping.ron"); 413 | } 414 | 415 | #[test] 416 | fn mapping_print() { 417 | Mapping::print(Mapping::read("./mapping.ron")); 418 | } 419 | 420 | #[test] 421 | fn mapping_validate() -> anyhow::Result<()> { 422 | Mapping::validate("./mapping.ron", None)?; 423 | Ok(()) 424 | } 425 | 426 | #[test] 427 | fn mapping_mismatch() { 428 | assert!(Mapping::validate("./mapping.ron", Some(0x8890)).is_err()); 429 | } 430 | 431 | #[test] 432 | fn bad_delay_884x() { 433 | assert!(Mapping::validate_key_mapping( 434 | &Button { 435 | delay: 6001, 436 | mapping: "t,e,s,t".to_string() 437 | }, 438 | consts::MAX_KEY_PRESSES_884X, 439 | Some(0x8840) 440 | ) 441 | .is_err()); 442 | } 443 | 444 | #[test] 445 | fn test_delay() -> anyhow::Result<()> { 446 | Mapping::validate_key_mapping( 447 | &Button { 448 | delay: 6000, 449 | mapping: "t,e,s,t".to_string(), 450 | }, 451 | consts::MAX_KEY_PRESSES_884X, 452 | Some(0x8840), 453 | )?; 454 | Mapping::validate_key_mapping( 455 | &Button { 456 | delay: 1234, 457 | mapping: "t,e,s,t".to_string(), 458 | }, 459 | consts::MAX_KEY_PRESSES_8890, 460 | Some(0x8890), 461 | )?; 462 | Ok(()) 463 | } 464 | 465 | #[test] 466 | fn mapping_multiple_modifiers_8890() { 467 | assert!(Mapping::validate_key_mapping( 468 | &Button { 469 | delay: 0, 470 | mapping: "ctrl-a,shift-s".to_string() 471 | }, 472 | consts::MAX_KEY_PRESSES_8890, 473 | Some(0x8890) 474 | ) 475 | .is_err()); 476 | assert!(Mapping::validate_key_mapping( 477 | &Button { 478 | delay: 0, 479 | mapping: "alt-a,ctrl-s".to_string() 480 | }, 481 | consts::MAX_KEY_PRESSES_8890, 482 | Some(0x8890) 483 | ) 484 | .is_err()); 485 | assert!(Mapping::validate_key_mapping( 486 | &Button { 487 | delay: 0, 488 | mapping: "shift-a,alt-s".to_string() 489 | }, 490 | consts::MAX_KEY_PRESSES_8890, 491 | Some(0x8890) 492 | ) 493 | .is_err()); 494 | } 495 | 496 | #[test] 497 | fn mapping_max_size_8890() -> anyhow::Result<()> { 498 | Mapping::validate_key_mapping( 499 | &Button { 500 | delay: 0, 501 | mapping: "1,2,3,4,5".to_string(), 502 | }, 503 | consts::MAX_KEY_PRESSES_8890, 504 | Some(0x8890), 505 | )?; 506 | assert!(Mapping::validate_key_mapping( 507 | &Button { 508 | delay: 0, 509 | mapping: "1,2,3,4,5,6".to_string() 510 | }, 511 | consts::MAX_KEY_PRESSES_8890, 512 | Some(0x8890) 513 | ) 514 | .is_err()); 515 | Ok(()) 516 | } 517 | 518 | #[test] 519 | fn mapping_multiple_modifiers_8840() -> anyhow::Result<()> { 520 | Mapping::validate_key_mapping( 521 | &Button { 522 | delay: 0, 523 | mapping: "ctrl-a,shift-s".to_string(), 524 | }, 525 | consts::MAX_KEY_PRESSES_884X, 526 | Some(0x8840), 527 | )?; 528 | Ok(()) 529 | } 530 | 531 | #[test] 532 | fn mapping_max_size_8840() -> anyhow::Result<()> { 533 | Mapping::validate_key_mapping( 534 | &Button { 535 | delay: 0, 536 | mapping: "1,2,3,4,5,6,7,8,9,0,a,b,c,d,e,f,g".to_string(), 537 | }, 538 | consts::MAX_KEY_PRESSES_884X, 539 | Some(0x8840), 540 | )?; 541 | assert!(Mapping::validate_key_mapping( 542 | &Button { 543 | delay: 0, 544 | mapping: "1,2,3,4,5,6,7,8,9,0,a,b,c,d,e,f,g,h".to_string() 545 | }, 546 | consts::MAX_KEY_PRESSES_884X, 547 | Some(0x8840) 548 | ) 549 | .is_err()); 550 | Ok(()) 551 | } 552 | } 553 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | use crate::consts::VENDOR_ID; 2 | use crate::keyboard::LedColor; 3 | use crate::parse; 4 | use clap::{Args, Parser, Subcommand}; 5 | use std::num::ParseIntError; 6 | 7 | #[derive(Parser)] 8 | pub struct Options { 9 | #[command(subcommand)] 10 | pub command: Command, 11 | 12 | #[clap(flatten)] 13 | pub devel_options: DevelOptions, 14 | } 15 | 16 | #[derive(Args, Debug)] 17 | #[clap( 18 | hide(true), 19 | next_help_heading = "Development options (use with caution!)" 20 | )] 21 | pub struct DevelOptions { 22 | #[arg(long, default_value_t=VENDOR_ID, value_parser=u16_hex_or_decimal, hide=true)] 23 | pub vendor_id: u16, 24 | 25 | #[arg(long, value_parser=u16_hex_or_decimal, hide=true)] 26 | pub product_id: Option, 27 | 28 | #[arg(long, value_parser=parse_address, hide=true)] 29 | pub address: Option<(u8, u8)>, 30 | 31 | /// OUT endpoint address where data is written 32 | #[arg(long, value_parser=u8_hex_or_decimal, hide = true)] 33 | pub out_endpoint_address: Option, 34 | 35 | /// IN endpoint address where data is read 36 | #[arg(long, value_parser=u8_hex_or_decimal, hide = true)] 37 | pub in_endpoint_address: Option, 38 | 39 | #[arg(long, hide = true)] 40 | pub interface_number: Option, 41 | } 42 | 43 | /// Parses a hex or decimal value and returns the value as u16. Currently, 44 | /// this is used for clap to parse the command line arguments 45 | /// 46 | /// #Arguments 47 | /// `s` - the string to be parsed 48 | /// 49 | pub fn u16_hex_or_decimal(s: &str) -> Result { 50 | if s.to_ascii_lowercase().starts_with("0x") { 51 | u16::from_str_radix(&s[2..], 16) 52 | } else { 53 | s.parse::() 54 | } 55 | } 56 | 57 | /// Parses a hex or decimal value and returns the value as u8. Currently, 58 | /// this is used for clap to parse the command line arguments 59 | /// 60 | /// #Arguments 61 | /// `s` - the string to be parsed 62 | /// 63 | pub fn u8_hex_or_decimal(s: &str) -> Result { 64 | if s.to_ascii_lowercase().starts_with("0x") { 65 | u8::from_str_radix(&s[2..], 16) 66 | } else { 67 | s.parse::() 68 | } 69 | } 70 | 71 | fn parse_address(s: &str) -> std::result::Result<(u8, u8), nom::error::Error> { 72 | parse::from_str(parse::address, s) 73 | } 74 | 75 | #[derive(Subcommand)] 76 | pub enum Command { 77 | /// Show supported keys and modifiers 78 | ShowKeys, 79 | 80 | /// Validate key mappings config 81 | Validate { 82 | /// Configuration file in ron format 83 | #[clap(short, long, default_value = "./mapping.ron")] 84 | config_file: String, 85 | 86 | /// Product ID to validate mappings against (each product differs) 87 | #[clap(short, value_parser=u16_hex_or_decimal)] 88 | product_id: Option, 89 | 90 | /// Validate against connected device 91 | #[clap(short, default_value_t = false)] 92 | device_connected: bool, 93 | }, 94 | 95 | /// Program key mappings 96 | Program { 97 | /// Configuration file in ron format 98 | #[clap(short, long, default_value = "./mapping.ron")] 99 | config_file: String, 100 | }, 101 | 102 | /// Read configuration from device 103 | Read { 104 | /// Layer to read data for (layer is one based; 0 reads all layers) 105 | #[clap(short, long, default_value_t = 0)] 106 | layer: u8, 107 | }, 108 | 109 | /// Select LED backlight mode 110 | Led(LedCommand), 111 | } 112 | 113 | #[derive(Parser, Clone, Default, Debug)] 114 | pub struct LedCommand { 115 | /// Index of LED modes 116 | /// --------0x8840---------- 117 | /// 0 - LEDs off 118 | /// 1 - backlight always on with LedColor 119 | /// 2 - no backlight, shock with LedColor when key pressed 120 | /// 3 - no backlight, shock2 when LedColor when key pressed 121 | /// 4 - no backlight, light up key with LedColor when pressed 122 | /// 5 - backlight white always on 123 | /// --------0x8890---color is not supported------- 124 | /// 0 - LEDs off 125 | /// 1 - LED on for last pushed key 126 | /// 2 - cycle through colors & buttons 127 | #[clap(verbatim_doc_comment)] 128 | pub index: u8, 129 | 130 | // Layer to set the LED 131 | #[clap(default_value_t = 1)] 132 | pub layer: u8, 133 | 134 | // made this an option because the 884x supports color but the 8890 135 | // does not. defaults to Red, but since the 8890 does not accept 136 | // setting color, it just gets ignored 137 | /// Note: Not applicable for product id 0x8890 138 | /// Color to apply with mode 139 | #[arg(value_enum, verbatim_doc_comment)] 140 | pub led_color: Option, 141 | } 142 | -------------------------------------------------------------------------------- /src/parse.rs: -------------------------------------------------------------------------------- 1 | //! Collection of NOM parsers for various things. 2 | //! Generally only `parse` and `from_str` functions should be called 3 | //! from outside of this module, they ensures that whole input is 4 | //! consumed. 5 | //! Other functions are composable parsers for use within this module 6 | //! or as parameters for functions mentioned above. 7 | 8 | use nom::{ 9 | character::complete::{char, digit1}, 10 | combinator::{all_consuming, map_res}, 11 | error::{Error as NomError, ParseError}, 12 | sequence::separated_pair, 13 | Finish, IResult, Parser, 14 | }; 15 | use std::str::FromStr; 16 | 17 | /// Parses a string like "12:34" into (u8, u8) 18 | pub fn address(input: &str) -> IResult<&str, (u8, u8)> { 19 | let byte = map_res(digit1, u8::from_str); 20 | separated_pair(byte, char(':'), map_res(digit1, u8::from_str)).parse(input) 21 | } 22 | 23 | /// Runs a parser and ensures the entire input is consumed 24 | pub fn parse<'a, O, E, P>(parser: P, input: &'a str) -> Result 25 | where 26 | P: Parser<&'a str, Output = O, Error = E>, 27 | E: ParseError<&'a str>, 28 | { 29 | all_consuming(parser) 30 | .parse(input) 31 | .finish() 32 | .map(|(_, out)| out) 33 | } 34 | 35 | /// Like `parse`, but converts error input to `String` for `FromStr` 36 | pub fn from_str(parser: P, s: &str) -> Result> 37 | where 38 | for<'a> P: Parser<&'a str, Output = O, Error = NomError<&'a str>>, 39 | { 40 | match parse(parser, s) { 41 | Ok(value) => Ok(value), 42 | Err(NomError { input, code }) => Err(NomError { 43 | input: input.to_string(), 44 | code, 45 | }), 46 | } 47 | } 48 | --------------------------------------------------------------------------------