diff --git a/Cargo.lock b/Cargo.lock index d0295a4..02cebdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -34,6 +34,32 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 1.0.109", +] + [[package]] name = "binrw" version = "0.8.0" @@ -53,7 +79,7 @@ dependencies = [ "owo-colors", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -62,6 +88,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bytemuck" version = "1.7.2" @@ -74,6 +109,15 @@ version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -89,10 +133,12 @@ dependencies = [ "log", "rgb", "rusb", + "serde", "serde_json", "strum", "strum_macros", "thiserror", + "uhid-virt", ] [[package]] @@ -106,6 +152,45 @@ dependencies = [ "structopt", ] +[[package]] +name = "cherryrgb_ncli" +version = "0.2.4" +dependencies = [ + "anyhow", + "cherryrgb", + "log", + "serde_json", + "simple_logger", + "structopt", +] + +[[package]] +name = "cherryrgb_service" +version = "0.2.4" +dependencies = [ + "anyhow", + "cherryrgb", + "ctrlc", + "file-mode", + "log", + "nix", + "rgb", + "serde_json", + "simple_logger", + "structopt", + "systemd-journal-logger", +] + +[[package]] +name = "clang-sys" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +dependencies = [ + "glob", + "libc", +] + [[package]] name = "clap" version = "2.33.3" @@ -132,6 +217,101 @@ dependencies = [ "winapi", ] +[[package]] +name = "cpufeatures" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ctrlc" +version = "3.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639" +dependencies = [ + "nix", + "windows-sys", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "enumflags2" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + +[[package]] +name = "file-mode" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773ea145485772b8d354624b32adbe20e776353d3e48c7b03ef44e3455e9815c" +dependencies = [ + "libc", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "heck" version = "0.3.3" @@ -162,6 +342,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "itoa" version = "1.0.6" @@ -174,11 +363,35 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" -version = "0.2.105" +version = "0.2.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013" +checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" + +[[package]] +name = "libsystemd" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b9597a67aa1c81a6624603e6bd0bcefb9e0f94c9c54970ec53771082104b4e" +dependencies = [ + "hmac", + "libc", + "log", + "nix", + "nom", + "once_cell", + "serde", + "sha2", + "thiserror", + "uuid", +] [[package]] name = "libusb1-sys" @@ -194,19 +407,83 @@ dependencies = [ [[package]] name = "log" -version = "0.4.14" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if", + "value-bag", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nix" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +dependencies = [ + "bitflags", + "cfg-if", + "libc", + "memoffset", + "pin-utils", + "static_assertions", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + [[package]] name = "owo-colors" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a61765925aec40abdb23812a3a1a01fafc6ffb9da22768b2ce665a9e84e527c" +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.22" @@ -222,7 +499,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "version_check", ] @@ -239,22 +516,37 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.30" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.10" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] +[[package]] +name = "regex" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" + [[package]] name = "rgb" version = "0.8.27" @@ -262,6 +554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fddb3b23626145d1776addfc307e1a1851f60ef6ca64f376bcb889697144cf0" dependencies = [ "bytemuck", + "serde", ] [[package]] @@ -274,6 +567,12 @@ dependencies = [ "libusb1-sys", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustversion" version = "1.0.12" @@ -291,18 +590,49 @@ name = "serde" version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] [[package]] name = "serde_json" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", "serde", ] +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + [[package]] name = "simple_logger" version = "1.13.0" @@ -315,6 +645,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.8.0" @@ -342,7 +678,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -361,18 +697,45 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 1.0.109", ] +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" -version = "1.0.80" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "systemd-journal-logger" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "356b5cb52ce54916cbfaee19b07d305c7ea8ce5435a088c58743d4a0211f3eff" +dependencies = [ + "libsystemd", + "log", ] [[package]] @@ -401,9 +764,41 @@ checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "uhid-virt" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f16e165f87ad3df8660688cb786c7ea0af76ff15037f8a0da3756bef8459499" +dependencies = [ + "enumflags2", + "libc", + "uhidrs-sys", +] + +[[package]] +name = "uhidrs-sys" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6432b20db843292d5843dec450eaf19b8a2f8603ec91e74e4ab916d4815d3c18" +dependencies = [ + "bindgen", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + [[package]] name = "unicode-segmentation" version = "1.8.0" @@ -417,10 +812,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] -name = "unicode-xid" -version = "0.2.2" +name = "uuid" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55a3fef2a1e3b3a00ce878640918820d3c51081576ac657d23af9fc7928fdb" +dependencies = [ + "serde", +] + +[[package]] +name = "value-bag" +version = "1.0.0-alpha.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" +dependencies = [ + "ctor", + "version_check", +] [[package]] name = "vcpkg" @@ -461,3 +869,69 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" diff --git a/Cargo.toml b/Cargo.toml index 7c98ac7..59b4810 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,19 @@ version = "0.2.4" edition = "2018" publish = false +[features] +uhid = ["cherryrgb/uhid"] + [workspace] +members = [ + "service", + "ncli", +] + +[workspace.package] +version = "0.2.4" +edition = "2018" +publish = false [dependencies] cherryrgb = { path = "cherryrgb" } diff --git a/README.md b/README.md index ddd7163..8a55889 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,9 @@ Get usage help ./cherryrgb_cli animation --help ./cherryrgb_cli custom-colors --help ``` +### Alternative CLI and service for Linux + +See [this](docs/UHID-driver.md) doc. ### Set LED animation @@ -135,8 +138,8 @@ This is a known issue in the keyboard firmware. It is mentioned here: - **Proper** way to fix it: **Contact Cherry Support** - - **Workaround**: Comment out the respective line in [`99-cherryrgb.rules`](https://github.com/skraus-dev/cherryrgb-rs/blob/master/udev/99-cherryrgb.rules) and reload/trigger the udev rule. +- See [this](docs/UHID-driver.md) doc for an alternative solution on Linux. ## Disclaimer diff --git a/cherryrgb/Cargo.toml b/cherryrgb/Cargo.toml index c71a1b1..93c34eb 100644 --- a/cherryrgb/Cargo.toml +++ b/cherryrgb/Cargo.toml @@ -7,13 +7,20 @@ repository = "https://github.com/skraus-dev/cherryrgb-rs" license = "MIT" homepage = "https://github.com/skraus-dev/cherryrgb-rs" +[features] +uhid = ["dep:uhid-virt"] + [dependencies] thiserror = "1" binrw = "0.8" hex = "0.4" log = "0.4" -rgb = "0.8" +rgb = { version = "0.8", features = ["serde"] } rusb = "0.9" +serde = { version = "1.0.160", features = ["derive"] } strum = "0.24.1" strum_macros = "0.24.3" serde_json = "1.0" + +[target.'cfg(all(target_os = "linux"))'.dependencies] +uhid-virt = { version = "0.0.6", optional = true } diff --git a/cherryrgb/src/extensions.rs b/cherryrgb/src/extensions.rs index 3b8e167..d89d57b 100644 --- a/cherryrgb/src/extensions.rs +++ b/cherryrgb/src/extensions.rs @@ -1,5 +1,6 @@ use binrw::{BinRead, BinReaderExt, BinResult, BinWrite, BinWriterExt, ReadOptions, WriteOptions}; use rgb::RGB8; +use serde::{Deserialize, Serialize}; use std::{ io::{Cursor, Read, Seek}, str::FromStr, @@ -24,7 +25,7 @@ where } /// Wrap around RGB8 type, to implement traits on it -#[derive(Clone, Default, Debug, PartialEq)] +#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct OwnRGB8(RGB8); impl OwnRGB8 { diff --git a/cherryrgb/src/lib.rs b/cherryrgb/src/lib.rs index a96bf97..28320de 100644 --- a/cherryrgb/src/lib.rs +++ b/cherryrgb/src/lib.rs @@ -54,6 +54,8 @@ mod extensions; mod models; +#[cfg(all(target_os = "linux", feature = "uhid"))] +mod vkbd; use binrw::BinReaderExt; use models::{Keymap, ProfileKey}; @@ -66,10 +68,14 @@ use thiserror::Error; // Re-exports pub use extensions::{OwnRGB8, ToVec}; pub use hex; +#[cfg(all(target_os = "linux", feature = "uhid"))] +pub use models::RpcAnimation; pub use models::{Brightness, CustomKeyLeds, LightingMode, Packet, Payload, Speed}; pub use rgb; pub use rusb; pub use strum; +#[cfg(all(target_os = "linux", feature = "uhid"))] +pub use vkbd::VirtKbd; // Constants /// USB Vendor ID - Cherry GmbH @@ -78,6 +84,8 @@ pub const CHERRY_USB_VID: u16 = 0x046a; const INTERFACE_NUM: u8 = 1; const INTERRUPT_EP: u8 = 0x82; static TIMEOUT: Duration = Duration::from_millis(1000); +#[cfg(all(target_os = "linux", feature = "uhid"))] +static SHORT_TIMEOUT: Duration = Duration::from_millis(100); /// (64 byte packet - 4 byte packet header - 4 byte payload header) const CHUNK_SIZE: usize = 56; @@ -220,6 +228,25 @@ impl CherryKeyboard { assert_eq!(device_desc.num_configurations(), 1); assert_eq!(config_desc.num_interfaces(), 2); + // This should find 2 endpoints with Interrupt inputs + for interface in config_desc.interfaces() { + for interface_desc in interface.descriptors() { + for endpoint_desc in interface_desc.endpoint_descriptors() { + if endpoint_desc.direction() == rusb::Direction::In + && endpoint_desc.transfer_type() == rusb::TransferType::Interrupt + { + log::debug!( + "Found Interrupt input: ci={} if={} se={} addr=0x{:02x}", + config_desc.number(), + interface_desc.interface_number(), + interface_desc.setting_number(), + endpoint_desc.address() + ); + } + } + } + } + // Skip kernel driver detachment for non-unix platforms if cfg!(unix) { device_handle @@ -407,6 +434,34 @@ impl CherryKeyboard { Ok(all_keys) } + /// forward a key event from our usb device to the virtual UHID keyboard, + /// filter out any bogus events while doing so. + #[cfg(all(target_os = "linux", feature = "uhid"))] + pub fn forward_filtered_keys(&self, vdevice: &mut VirtKbd) -> Result<(), CherryRgbError> { + let mut buf = [0; 64]; + match self + .device_handle + .read_interrupt(INTERRUPT_EP, &mut buf, SHORT_TIMEOUT) + { + Ok(len) => { + // Bogus event data has bit 3 set in the 3rd byte + if len >= 3 && buf[2] >= 8 { + log::debug!(" - BOGUS read {} bytes: {:?} filtered", len, &buf[..len]); + return Ok(()); + } + log::debug!(" - read {} bytes: {:?}", len, &buf[..len]); + vdevice.forward(&buf[..len]); + } + Err(err) => { + if err == rusb::Error::Timeout { + return Ok(()); + } + return Err(CherryRgbError::GeneralUsbError(err)); + } + } + Ok(()) + } + /// Just taken 1:1 from usb capture pub fn fetch_device_state(&self) -> Result<(), CherryRgbError> { log::trace!("Fetching device state - START"); diff --git a/cherryrgb/src/models.rs b/cherryrgb/src/models.rs index 46f6dfa..2a7748f 100644 --- a/cherryrgb/src/models.rs +++ b/cherryrgb/src/models.rs @@ -5,33 +5,54 @@ use crate::{ }; use binrw::{binrw, until_eof, BinRead, BinWrite, BinWriterExt}; +use serde::{Deserialize, Serialize}; use std::convert::TryFrom; -use strum_macros::{EnumString, EnumVariantNames}; +use strum_macros::{EnumProperty, EnumString, EnumVariantNames}; -/// Modes support: -/// -> C: Color -/// -> S: Speed +/// Mode attributes: +/// -> C: Supports color option +/// -> S: Supports speed option +/// -> U: Unofficial #[binrw] #[brw(repr = u8)] -#[derive(Clone, Eq, PartialEq, Debug, EnumString, EnumVariantNames)] +#[derive( + Clone, Eq, PartialEq, Debug, EnumString, EnumProperty, EnumVariantNames, Serialize, Deserialize, +)] #[strum(serialize_all = "snake_case")] pub enum LightingMode { - Wave = 0x00, // CS - Spectrum = 0x01, // S + #[strum(props(attr = "CS"))] + Wave = 0x00, // CS + #[strum(props(attr = "S"))] + Spectrum = 0x01, // S + #[strum(props(attr = "CS"))] Breathing = 0x02, // CS - Static = 0x03, // n/A - Radar = 0x04, // Unofficial - Vortex = 0x05, // Unofficial - Fire = 0x06, // Unofficial - Stars = 0x07, // Unofficial - Rain = 0x0B, // Unofficial (looks like Matrix :D) + #[strum(props(attr = "C"))] + Static = 0x03, // C + #[strum(props(attr = "U"))] + Radar = 0x04, // Unofficial + #[strum(props(attr = "U"))] + Vortex = 0x05, // Unofficial + #[strum(props(attr = "U"))] + Fire = 0x06, // Unofficial + #[strum(props(attr = "U"))] + Stars = 0x07, // Unofficial + #[strum(props(attr = "U"))] + Rain = 0x0B, // Unofficial (looks like Matrix :D) + #[strum(props(attr = ""))] Custom = 0x08, - Rolling = 0x0A, // S - Curve = 0x0C, // CS - WaveMid = 0x0E, // Unoffical - Scan = 0x0F, // C + #[strum(props(attr = "S"))] + Rolling = 0x0A, // S + #[strum(props(attr = "CS"))] + Curve = 0x0C, // CS + #[strum(props(attr = "yes"))] + WaveMid = 0x0E, // Unofficial + #[strum(props(attr = "C"))] + Scan = 0x0F, // C + #[strum(props(attr = "CS"))] Radiation = 0x12, // CS - Ripples = 0x13, // CS + #[strum(props(attr = "CS"))] + Ripples = 0x13, // CS + #[strum(props(attr = "CS"))] SingleKey = 0x15, // CS } @@ -39,7 +60,7 @@ pub enum LightingMode { /// Just defined here for completeness' sake #[binrw] #[brw(repr = u8)] -#[derive(Eq, PartialEq, Debug)] +#[derive(Eq, PartialEq, Debug, Serialize, Deserialize)] pub enum UsbPollingRate { Low, // 125Hz Medium, // 250 Hz @@ -50,7 +71,7 @@ pub enum UsbPollingRate { /// LED animation speed #[binrw] #[brw(repr = u8)] -#[derive(Clone, Eq, PartialEq, Debug, EnumString, EnumVariantNames)] +#[derive(Clone, Eq, PartialEq, Debug, EnumString, EnumVariantNames, Serialize, Deserialize)] #[strum(serialize_all = "snake_case")] pub enum Speed { VeryFast = 0, @@ -63,7 +84,7 @@ pub enum Speed { /// LED brightness #[binrw] #[brw(repr = u8)] -#[derive(Clone, Eq, PartialEq, Debug, EnumString, EnumVariantNames)] +#[derive(Clone, Eq, PartialEq, Debug, EnumString, EnumVariantNames, Serialize, Deserialize)] #[strum(serialize_all = "snake_case")] pub enum Brightness { Off = 0, @@ -209,7 +230,7 @@ where } /// Wrapper around custom LED color for all keys -#[derive(Default, Debug)] +#[derive(Default, Debug, Serialize, Deserialize)] pub struct CustomKeyLeds { key_leds: Vec, } @@ -320,3 +341,15 @@ impl CustomKeyLeds { Ok(result) } } + +/// Parameters for set_led_animation (sent serialized from +/// cherryrgb_ncli to cherryrgb_service). +#[cfg(all(target_os = "linux", feature = "uhid"))] +#[derive(Debug, Serialize, Deserialize)] +pub struct RpcAnimation { + pub mode: LightingMode, + pub brightness: Brightness, + pub speed: Speed, + pub color: Option, + pub rainbow: bool, +} diff --git a/cherryrgb/src/vkbd.rs b/cherryrgb/src/vkbd.rs new file mode 100644 index 0000000..49cb55e --- /dev/null +++ b/cherryrgb/src/vkbd.rs @@ -0,0 +1,145 @@ +#![cfg(all(target_os = "linux", feature = "uhid"))] + +use log::error; +use uhid_virt::{Bus, CreateParams, UHIDDevice}; + +/// Virtual HID device for injecting key events into the Linux HID subsystem +pub struct VirtKbd { + device: UHIDDevice, +} + +impl Default for VirtKbd { + fn default() -> Self { + Self::new() + } +} + +impl VirtKbd { + pub fn new() -> Self { + VirtKbd { + device: UHIDDevice::create(CreateParams { + name: String::from("cherryrgb"), + phys: String::from(""), // ? + uniq: String::from(""), // ? + bus: Bus::USB, + vendor: 0xdead, + product: 0xbeef, + version: 0, + country: 0, + #[rustfmt::skip] + rd_data: [ + // Annotated report descriptor generated by running hid-decode + // on the event device of my Cherry MX 10.0 N RGB + // hid-decode is part of hid-tools + // See: https://gitlab.freedesktop.org/libevdev/hid-tools + // + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x06, // Usage (Keyboard) + 0xa1, 0x01, // Collection (Application) + 0x85, 0x01, // Report ID (1) + 0x05, 0x07, // Usage Page (Keyboard) + 0x19, 0x04, // Usage Minimum (4) + 0x29, 0x70, // Usage Maximum (112) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x75, 0x01, // Report Size (1) + 0x95, 0x78, // Report Count (120) + 0x81, 0x02, // Input (Data,Var,Abs) + 0xc0, // End Collection + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x80, // Usage (System Control) + 0xa1, 0x01, // Collection (Application) + 0x85, 0x02, // Report ID (2) + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x19, 0x81, // Usage Minimum (129) + 0x29, 0x83, // Usage Maximum (131) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x95, 0x03, // Report Count (3) + 0x75, 0x01, // Report Size (1) + 0x81, 0x02, // Input (Data,Var,Abs) + 0x95, 0x01, // Report Count (1) + 0x75, 0x05, // Report Size (5) + 0x81, 0x01, // Input (Cnst,Arr,Abs) + 0xc0, // End Collection + 0x05, 0x0c, // Usage Page (Consumer Devices) + 0x09, 0x01, // Usage (Consumer Control) + 0xa1, 0x01, // Collection (Application) + 0x85, 0x03, // Report ID (3) + 0x15, 0x00, // Logical Minimum (0) + 0x26, 0xff, 0x1f, // Logical Maximum (8191) + 0x19, 0x00, // Usage Minimum (0) + 0x2a, 0xff, 0x1f, // Usage Maximum (8191) + 0x75, 0x10, // Report Size (16) + 0x95, 0x01, // Report Count (1) + 0x81, 0x00, // Input (Data,Arr,Abs) + 0xc0, // End Collection + 0x06, 0x1c, 0xff, // Usage Page (Vendor Usage Page 0xff1c) + 0x09, 0x92, // Usage (Vendor Usage 0x92) + 0xa1, 0x01, // Collection (Application) + 0x85, 0x04, // Report ID (4) + 0x19, 0x00, // Usage Minimum (0) + 0x2a, 0xff, 0x00, // Usage Maximum (255) + 0x15, 0x00, // Logical Minimum (0) + 0x26, 0xff, 0x00, // Logical Maximum (255) + 0x75, 0x08, // Report Size (8) + 0x95, 0x3f, // Report Count (63) + 0x91, 0x00, // Output (Data,Arr,Abs) + 0x19, 0x00, // Usage Minimum (0) + 0x29, 0xff, // Usage Maximum (255) + 0x81, 0x00, // Input (Data,Arr,Abs) + 0xc0, // End Collection + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x02, // Usage (Mouse) + 0xa1, 0x01, // Collection (Application) + 0x85, 0x05, // Report ID (5) + 0x09, 0x01, // Usage (Pointer) + 0xa1, 0x00, // Collection (Physical) + 0x05, 0x09, // Usage Page (Button) + 0x19, 0x01, // Usage Minimum (1) + 0x29, 0x05, // Usage Maximum (5) + 0x15, 0x00, // Logical Minimum (0) + 0x25, 0x01, // Logical Maximum (1) + 0x95, 0x05, // Report Count (5) + 0x75, 0x01, // Report Size (1) + 0x81, 0x02, // Input (Data,Var,Abs) + 0x95, 0x01, // Report Count (1) + 0x75, 0x03, // Report Size (3) + 0x81, 0x01, // Input (Cnst,Arr,Abs) + 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, 0x30, // Usage (X) + 0x09, 0x31, // Usage (Y) + 0x16, 0x00, 0x80, // Logical Minimum (-32768) + 0x26, 0xff, 0x7f, // Logical Maximum (32767) + 0x75, 0x10, // Report Size (16) + 0x95, 0x02, // Report Count (2) + 0x81, 0x06, // Input (Data,Var,Rel) + 0x09, 0x38, // Usage (Wheel) + 0x15, 0x81, // Logical Minimum (-127) + 0x25, 0x7f, // Logical Maximum (127) + 0x75, 0x08, // Report Size (8) + 0x95, 0x01, // Report Count (1) + 0x81, 0x06, // Input (Data,Var,Rel) + 0x05, 0x0c, // Usage Page (Consumer Devices) + 0x0a, 0x38, 0x02, // Usage (AC Pan) + 0x15, 0x81, // Logical Minimum (-127) + 0x25, 0x7f, // Logical Maximum (127) + 0x75, 0x08, // Report Size (8) + 0x95, 0x01, // Report Count (1) + 0x81, 0x06, // Input (Data,Var,Rel) + 0xc0, // End Collection + 0xc0, // End Collection + ] + .to_vec(), + }) + .map_err(|err| error!("Could not create VirtKbd: {:?}", err)) + .expect("Could not create VirtKbd"), + } + } + + /// Forward a single HID event. + /// See: CherryKeyboard::forward_filtered_keys() + pub fn forward(&mut self, input: &[u8]) { + self.device.write(input).unwrap(); + } +} diff --git a/docs/UHID-driver.md b/docs/UHID-driver.md new file mode 100644 index 0000000..752d553 --- /dev/null +++ b/docs/UHID-driver.md @@ -0,0 +1,61 @@ +## New UHID service and correspondig new cli + +### Preface + +The UHID feature is available **on Linux only**. +The motivation for this new was [this issue](https://github.com/skraus-dev/cherryrgb-rs/issues/22). + +### How to build +In order to build everything, you now must use the ``--all`` flag and enable the feature ``uhid`` +when building. E.g.: +``cargo build --all --features uhid`` +This creates 2 new binaries ``cherryrgb_service`` and ``cherryrgb_ncli``. + +The service should run as root and provides the UHID driver as well as socket server, listening on ``/run/cherryrgb.sock`` by default. +The cherryrgb_ncli client works **almost** identical to the cherryrgb_cli, except it communicates with the service +(using the unix socket). Therefore, it does not have the ``--product-id`` option anymore (which has been moved to +the service). Instead, it now has an option ``--socket``, which can be used to specify a non-standard socket-path. +The rest of the options and commands are identical to cherryrgb_cli. + +### Service options +The service has some new option to configure it properly: +``--socket`` can specify a path other than the default ``/run/cherryrgb.sock``. +``--socketmode`` can specify a mode (permissions) other that the default ``0644``. (The mode is specified **octal**). +``--socketgroup`` can specify a group other that the default ``root``. + +### Installation on a Linux system that uses systemd +For systems that provide systemd, there is a ``.service`` file and a configuration file in the hierarchy below ``service/etc/``. +If you want to use these, install everything like this: + +```shell +sudo cp target/*/cherryrgb_service /usr/libexec/ +sudo cp service/etc/systemd/system/cherryrgb.service /etc/systemd/system/ +sudo cp service/etc/default/cherryrgb /etc/default/ +``` + +Also, perhaps you want to edit ``/etc/default/cherryrgb``. +For example, on my local Fedora installation, administrators are in the group ``wheel``. +Therefore, I have CHERRYRGB_OPTIONS defined like this: +``` +CHERRYRGB_OPTIONS="--socketgroup wheel" +``` +After adjusting the config to your needs, run: +```shell +sudo systemctl start cherryrgb.service +``` +and check for errors with: +```shell +sudo systemctl status cherryrgb.service +``` + +If the service is running, you should be able to run ``cherryrgb_ncli`` with the same commands you used with cherryrgb_cli. +If everything worked out ok, enable the service to be started at boot: +```shell +sudo systemctl enable cherryrgb.service +``` + +Finally, in order to recover if the keyboard is unplugged/plugged, install the supplied udev rule: +```shell +sudo cp udev/99-cherryrgb-service.rules /etc/udev/rules.d/ +``` +If your systemctl binary is NOT located in /usr/bin you might need to adapt it's path in /etc/udev/rules.d/99-cherryrgb-service.rules diff --git a/ncli/Cargo.toml b/ncli/Cargo.toml new file mode 100644 index 0000000..192cc40 --- /dev/null +++ b/ncli/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "cherryrgb_ncli" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +cherryrgb = { path = "../cherryrgb" } +anyhow = "1.0" +structopt = "0.3" +log = "0.4" +serde_json = "1.0.96" + +[dependencies.simple_logger] +version = "1.13" +default-features = false +features = ["colors"] diff --git a/ncli/src/main.rs b/ncli/src/main.rs new file mode 100644 index 0000000..a005d58 --- /dev/null +++ b/ncli/src/main.rs @@ -0,0 +1,186 @@ +use std::{convert::TryFrom, io::Read, io::Write, path::PathBuf}; + +use anyhow::{Context, Result}; +use cherryrgb::strum::VariantNames; +use cherryrgb::{ + self, read_color_profile, rgb, Brightness, CustomKeyLeds, LightingMode, OwnRGB8, RpcAnimation, + Speed, +}; +use std::os::unix::net::UnixStream; +use structopt::StructOpt; + +#[derive(StructOpt, Debug)] +struct AnimationArgs { + /// Set LED mode + #[structopt(possible_values = LightingMode::VARIANTS)] + mode: LightingMode, + + /// Set speed + #[structopt(possible_values = Speed::VARIANTS)] + speed: Speed, + + /// Color (e.g ff00ff) + color: Option, + + /// Enable rainbow colors + #[structopt(short, long)] + rainbow: bool, +} + +#[derive(StructOpt, Debug)] +struct CustomColorOptions { + colors: Vec, +} + +#[derive(StructOpt, Debug)] +struct ColorProfileFileOptions { + #[structopt(parse(from_os_str))] + file_path: PathBuf, +} + +#[derive(StructOpt, Debug)] +enum CliCommand { + Animation(AnimationArgs), + CustomColors(CustomColorOptions), + ColorProfileFile(ColorProfileFileOptions), +} + +#[derive(StructOpt, Debug)] +#[structopt(name = "cherryrgb_ncli", about = "Test tool for Cherry RGB Keyboard")] +struct Opt { + /// Enable debug output + #[structopt(short, long)] + debug: bool, + + #[structopt( + name = "socket", + long, + help = "Path of socket to connect.", + default_value = "/run/cherryrgb.sock" + )] + socket_path: String, + + // Subcommand + #[structopt(subcommand)] + command: CliCommand, + + /// Set brightness + #[structopt(short, long, default_value = "full", possible_values = Brightness::VARIANTS)] + brightness: Brightness, +} + +struct UnixClient { + sock: UnixStream, +} + +/// UnixClient resembles CherryKeyboard, but connects to service +impl UnixClient { + pub fn new(path: String) -> Result { + let sock = + UnixStream::connect(path.clone()).context(format!("Could not connect to {path}"))?; + Ok(Self { sock }) + } + + /// Reset custom key colors to default + pub fn reset_custom_colors(&mut self) -> Result<(), anyhow::Error> { + self.sock + .write_all("reset_custom_colors".as_bytes()) + .context("I/O error writing to socket")?; + Ok(()) + } + + /// Set custom color for each individual key + pub fn set_custom_colors(&mut self, key_leds: CustomKeyLeds) -> Result<(), anyhow::Error> { + let json = serde_json::to_string(&key_leds).unwrap(); + self.sock + .write_all(format!("set_custom_colors={}", json).as_bytes()) + .context("I/O error writing to socket")?; + Ok(()) + } + + /// Set LED animation from different modes + pub fn set_led_animation>( + &mut self, + mode: LightingMode, + brightness: Brightness, + speed: Speed, + color: C, + rainbow: bool, + ) -> Result<(), anyhow::Error> { + let rpc = RpcAnimation { + mode, + brightness, + speed, + color: Some(color.into()), + rainbow, + }; + let json = serde_json::to_string(&rpc).unwrap(); + self.sock + .write_all(format!("set_led_animation={}", json).as_bytes()) + .context("I/O error writing to socket")?; + Ok(()) + } +} + +fn main() -> Result<()> { + let opt = Opt::from_args(); + + let loglevel = if opt.debug { + log::Level::Debug + } else { + log::Level::Info + }; + simple_logger::init_with_level(loglevel)?; + + let mut keyboard = UnixClient::new(opt.socket_path)?; + + match opt.command { + CliCommand::CustomColors(args) => { + keyboard.reset_custom_colors()?; + let mut keys = CustomKeyLeds::new(); + + for (index, color) in args.colors.into_iter().enumerate() { + keys.set_led(index, color)?; + } + + keyboard.set_custom_colors(keys)?; + } + CliCommand::ColorProfileFile(args) => { + let path_str = args + .file_path + .to_str() + .map_or(String::new(), |p| p.to_string()); + + let mut f = std::fs::File::open(&args.file_path) + .context(format!("color profile '{path_str}'"))?; + let mut json: String = String::new(); + + f.read_to_string(&mut json)?; + + let colors_from_file = + read_color_profile(&json).context("reading colors from color file")?; + + let keys = + CustomKeyLeds::try_from(colors_from_file).context("assembling custom key leds")?; + + keyboard.set_custom_colors(keys)?; + } + CliCommand::Animation(args) => { + let color = args.color.unwrap_or(rgb::RGB8::new(255, 255, 255).into()); + + log::info!( + "Setting: mode={:?} brightness={:?} speed={:?} color={:?}", + args.mode, + opt.brightness, + args.speed, + color + ); + + keyboard + .set_led_animation(args.mode, opt.brightness, args.speed, color, args.rainbow) + .context("Failed to set led animation")?; + } + } + + Ok(()) +} diff --git a/service/Cargo.toml b/service/Cargo.toml new file mode 100644 index 0000000..b5690d4 --- /dev/null +++ b/service/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "cherryrgb_service" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +cherryrgb = { path = "../cherryrgb" } +anyhow = "1.0" +structopt = "0.3" +log = "^0.4.17" +ctrlc = { version = "3.2.5", features = ["termination"] } +serde_json = "1.0.96" +rgb = { version = "0.8", features = ["serde"] } +file-mode = "0.1.2" +nix = "0.26.2" +systemd-journal-logger = "1.0.0" + +[dependencies.simple_logger] +version = "1.13" +default-features = false diff --git a/service/etc/default/cherryrgb b/service/etc/default/cherryrgb new file mode 100644 index 0000000..48e2a4e --- /dev/null +++ b/service/etc/default/cherryrgb @@ -0,0 +1,12 @@ +# Configuration for cherryrgb.service +# +# Possible cmdline options. Shown values are defaults +# +# --socket /run/cherryrgb.sock +# --socketmode 0664 +# --socketgroup root +# --product-id +# +# run cherryrgb_service -h for more info +# +CHERRYRGB_OPTIONS="--socketgroup wheel" diff --git a/service/etc/systemd/system/cherryrgb.service b/service/etc/systemd/system/cherryrgb.service new file mode 100644 index 0000000..1b02dfa --- /dev/null +++ b/service/etc/systemd/system/cherryrgb.service @@ -0,0 +1,9 @@ +[Unit] +Description = CherryRGB UHID daemon + +[Service] +EnvironmentFile = -/etc/default/cherryrgb +ExecStart = /usr/libexec/cherryrgb_service $CHERRYRGB_OPTIONS + +[Install] +WantedBy = multi-user.target diff --git a/service/src/main.rs b/service/src/main.rs new file mode 100644 index 0000000..bb75a52 --- /dev/null +++ b/service/src/main.rs @@ -0,0 +1,315 @@ +use anyhow::{anyhow, Context, Result}; +use cherryrgb::{self, CherryKeyboard, CustomKeyLeds, RpcAnimation, VirtKbd}; +use file_mode::ModePath; +use log::LevelFilter; +use nix::unistd::{chown, Group}; +use std::io::{Read, Write}; +use std::os::unix::net::{UnixListener, UnixStream}; +use std::path::Path; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::{thread, time}; +use structopt::StructOpt; +use systemd_journal_logger::{connected_to_journal, JournalLog}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const NAME: &str = env!("CARGO_PKG_NAME"); + +#[derive(StructOpt, Debug, Clone)] +#[structopt(name = NAME, about = "Service for cherryrgb_ncli")] +struct Opt { + /// Enable debug output + #[structopt(short, long)] + debug: bool, + + #[structopt( + long, + help = "Must be specified if multiple cherry products are detected." + )] + product_id: Option, + + #[structopt( + name = "socket", + long, + help = "Path of listening socket to create.", + default_value = "/run/cherryrgb.sock" + )] + socket_path: String, + + #[structopt( + name = "socketmode", + long, + help = "Permissions of the socket.", + default_value = "0664" + )] + socket_mode: String, + + #[structopt( + name = "socketgroup", + long, + help = "Group of the socket.", + default_value = "root" + )] + socket_group: String, +} + +/// Handle a single connection from cherryrgb_ncli +/// Try to read command (and possible +/// serialized parameters) from stream, then +/// execute command and return the result. +fn handle_client( + mut stream: UnixStream, + keyboard: Arc, + mutex: Arc>, +) -> Result<()> { + let mut msg = String::new(); + match stream.read_to_string(&mut msg) { + Ok(res) => res, + Err(err) => { + log::error!("Errror while receiving cmd: {:?}", err); + return Ok(()); + } + }; + if msg.starts_with("debug=on") { + log::set_max_level(LevelFilter::Debug); + return Ok(()); + } + if msg.starts_with("debug=off") { + log::set_max_level(LevelFilter::Info); + return Ok(()); + } + // Not really useful at the moment, because + // it just does logging, always returns an + // empty Ok Result and is not really required + // for LED-related operations. + /* + if msg.starts_with("fetch_device_state") { + let _guard = mutex.lock().unwrap(); + match keyboard.fetch_device_state() { + Ok(res) => res, + Err(err) => { + let emsg = format!("Fetching device state failed: {:?}", err); + let _ = stream.write_all(emsg.as_bytes()); + _ = stream.flush(); + log::error!("{}", emsg); + return Ok(()); + } + } + return Ok(()); + } + */ + if msg.starts_with("reset_custom_colors") { + let _guard = mutex.lock().unwrap(); + match keyboard.reset_custom_colors() { + Ok(res) => res, + Err(err) => { + let emsg = format!("Errror in reset_custom_colors: {:?}", err); + let _ = stream.write_all(format!("{}\n", emsg).as_bytes()); + log::error!("{}", emsg); + return Ok(()); + } + } + return Ok(()); + } + if let Some(stripped) = msg.strip_prefix("set_led_animation=") { + let params = stripped; + let args: RpcAnimation = match serde_json::from_str(params) { + Ok(res) => res, + Err(err) => { + log::error!( + "Unable to deserialize params for set_led_animation {:?}", + err + ); + return Ok(()); + } + }; + let color = args.color.unwrap_or(rgb::RGB8::new(255, 255, 255).into()); + let _guard = mutex.lock().unwrap(); + match keyboard.set_led_animation( + args.mode, + args.brightness, + args.speed, + color, + args.rainbow, + ) { + Ok(res) => res, + Err(err) => { + let emsg = format!("Errror in set_led_animation: {:?}", err); + let _ = stream.write_all(emsg.as_bytes()); + log::error!("{}", emsg); + return Ok(()); + } + } + return Ok(()); + } + if let Some(stripped) = msg.strip_prefix("set_custom_colors=") { + let params = stripped; + let key_leds: CustomKeyLeds = match serde_json::from_str(params) { + Ok(res) => res, + Err(err) => { + log::error!( + "Unable to deserialize params for set_custom_colors {:?}", + err + ); + return Ok(()); + } + }; + let _guard = mutex.lock().unwrap(); + match keyboard.set_custom_colors(key_leds) { + Ok(res) => res, + Err(err) => { + let emsg = format!("Errror in set_set_custom_colors: {:?}", err); + let _ = stream.write_all(emsg.as_bytes()); + log::error!("{}", emsg); + return Ok(()); + } + } + return Ok(()); + } + log::warn!("received invalid cmd: {:?}", msg.as_str().trim()); + Ok(()) +} + +fn socket_server( + opt: Arc, + keep_running: Arc, + keyboard: Arc, + mutex: Arc>, +) -> Result<()> { + log::debug!("Listening on {}", opt.socket_path); + let listener = UnixListener::bind(opt.socket_path.clone())?; + let mode = u32::from_str_radix(&opt.socket_mode, 8).unwrap(); + let spath = Path::new(opt.socket_path.as_str()); + spath.set_mode(mode).unwrap(); + let group = Group::from_name(opt.socket_group.as_str()) + .unwrap() + .unwrap(); + chown(spath, None, Some(group.gid)).unwrap(); + + // accept connections and process them, spawning a new thread for each one + log::debug!("Accept-loop"); + for stream in listener.incoming() { + match stream { + Ok(stream) => { + // connection succeeded + if keep_running.load(Ordering::SeqCst) { + log::debug!("Got connection on {}", opt.socket_path); + let keyboard_clone = Arc::clone(&keyboard); + let mutex_clone = Arc::clone(&mutex); + let tb = thread::Builder::new().name("handle_client".into()); + tb.spawn(|| handle_client(stream, keyboard_clone, mutex_clone)) + .unwrap(); + } else { + let _ = std::fs::remove_file(opt.socket_path.clone()); + break; + } + } + Err(err) => { + log::error!("stream error err={:?}", err); + // connection failed + break; + } + } + } + Ok(()) +} + +fn get_u16_from_string(pid: Option) -> Option { + let cpid = pid.clone(); + if let Some(stripped) = cpid?.strip_prefix("0x") { + let val = u16::from_str_radix(stripped, 16).ok()?; + return Some(val); + } + let val = pid?.as_str().parse::().ok()?; + Some(val) +} + +fn main() -> Result<()> { + let opt = Opt::from_args(); + + if connected_to_journal() { + // If the output streams of this process are directly connected to the + // systemd journal log directly to the journal to preserve structured + // log entries (e.g. proper multiline messages, metadata fields, etc.) + JournalLog::default() + .with_extra_fields(vec![("VERSION", env!("CARGO_PKG_VERSION"))]) + .with_syslog_identifier("cherryrgb".to_string()) + .install() + .unwrap(); + } else { + simple_logger::init()?; + } + if opt.debug { + log::set_max_level(LevelFilter::Debug); + } else { + log::set_max_level(LevelFilter::Info); + } + log::info!("{} {} starting", NAME, VERSION); + + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }) + .expect("Error setting Ctrl-C handler"); + + let aopt = Arc::new(opt.clone()); + // Mutex for accessing CherryKeyboard + let amutex = Arc::new(Mutex::new(0)); + + // Allow the usual hex specifiation (starting with 0x) for the product-id + let pid = get_u16_from_string(opt.product_id); + + // Search / init usb keyboard + let devices = match cherryrgb::find_devices(pid) { + Err(_err) => { + panic!("Failed to find any cherry keyboard"); + } + Ok(devices) => devices, + }; + + if devices.len() > 1 { + for (index, &dev) in devices.iter().enumerate() { + println!("{}) VEN_ID={}, PROD_ID={}", index, dev.0, dev.1); + } + return Err(anyhow!( + "More than one keyboard found, please provide --product-id" + )); + } + + let (vendor_id, product_id) = devices.first().unwrap().to_owned(); + let keyboard = + CherryKeyboard::new(vendor_id, product_id).context("Failed to create keyboard")?; + let mut vkb = VirtKbd::new(); + + let aopt_clone = Arc::clone(&aopt); + let akeyboard = Arc::new(keyboard); + let akeyboard_clone = Arc::clone(&akeyboard); + let server_running = Arc::clone(&running); + let driver_running = Arc::clone(&running); + let amutex_clone1 = Arc::clone(&amutex); + let amutex_clone2 = Arc::clone(&amutex); + let tb = thread::Builder::new().name("socket_server".into()); + let th = tb + .spawn(|| socket_server(aopt_clone, server_running, akeyboard_clone, amutex_clone1)) + .unwrap(); + log::debug!("Entering driver loop"); + while driver_running.load(Ordering::SeqCst) { + { + let _guard = amutex_clone2.lock().unwrap(); + if let Err(err) = Arc::clone(&akeyboard).forward_filtered_keys(&mut vkb) { + log::error!("Failed to forward key events, err={}", err); + break; + } + } + // Without this sleep, sometimes the mutex appears to be still locked + // in the handle_client() above. + thread::sleep(time::Duration::from_millis(100)); + } + running.store(false, Ordering::SeqCst); + // This triggers a break in the socket_server accept loop + let _ = UnixStream::connect(opt.socket_path); + _ = th.join(); + + Ok(()) +} diff --git a/udev/99-cherryrgb-service.rules b/udev/99-cherryrgb-service.rules new file mode 100644 index 0000000..cbe47ae --- /dev/null +++ b/udev/99-cherryrgb-service.rules @@ -0,0 +1,7 @@ +###################################### +# udev rule for Cherry RGB service # +###################################### + +# Uncomment the following line to restart the cheeryrgb_service whenever a Cherry keyboard is plugged in. +# You might need to adapt the absolute path to /usr/bin/systemctl if it is at a different location on your system +ACTION=="add" SUBSYSTEMS=="usb", ATTR{idVendor}=="046a", ATTR{idProduct}=="*", RUN+="/usr/bin/systemctl restart cherryrgb.service" diff --git a/udev/99-cherryrgb.rules b/udev/99-cherryrgb.rules index a5a1847..706de90 100644 --- a/udev/99-cherryrgb.rules +++ b/udev/99-cherryrgb.rules @@ -2,6 +2,9 @@ # udev rule for Cherry RGB keyboards # ###################################### +# The following line enables access to the Cherry keyboard device for all users +# It is necessary, when you want to use cherryrgb_cli as ordinary user. +# It is NOT necessary, if you use the cherryrgb_service which runs as root SUBSYSTEMS=="usb", ATTR{idVendor}=="046a", ATTR{idProduct}=="*", MODE="0666" # There is a known bug in the keyboard firmware that produces loads of keyevents