commit 64574bd2fa69d57e8219ec2cf9e9b6d86e395063 Author: Moritz Ruth Date: Thu Dec 28 18:39:46 2023 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40d9aca --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/.idea \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f8f6ceb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,558 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "color-eyre" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "deckster" +version = "0.1.0" +dependencies = [ + "color-eyre", + "loupedeck_serial", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enumset" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "226c0da7462c13fb57e5cc9e0dc8f0635e7d27f276a3a7fd30054647f669007d" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08b6c6ab82d70f08844964ba10c7babb716de2ecaeab9be5717918a5177d3af" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "eyre" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "io-kit-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4769cb30e5dcf1710fc6730d3e94f78c47723a014a567de385e113c737394640" +dependencies = [ + "core-foundation-sys", + "mach2", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "loupedeck_serial" +version = "0.1.0" +dependencies = [ + "bytes", + "enum-ordinalize", + "enumset", + "serialport", + "thiserror", +] + +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pkg-config" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" + +[[package]] +name = "proc-macro2" +version = "1.0.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serialport" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5a15d0be940df84846264b09b51b10b931fb2f275becb80934e3568a016828" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "core-foundation-sys", + "io-kit-sys", + "libudev", + "mach2", + "nix", + "regex", + "scopeguard", + "unescaper", + "winapi", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "syn" +version = "2.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee659fb5f3d355364e1f3e5bc10fb82068efbf824a1e9d1c9504244a6469ad53" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "unescaper" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f0f68e58d297ba8b22b8b5a96a87b863ba6bb46aaf51e19a4b02c5a6dd5b7f" +dependencies = [ + "thiserror", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..efde2fd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +members = [ + "deckster", + "loupedeck_serial" +] + +resolver = "2" \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..7a84c0d --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,55 @@ +# Blue Oak Model License + +Version 1.0.0 + +## Purpose + +This license gives everyone as much permission to work with +this software as possible, while protecting contributors +from liability. + +## Acceptance + +In order to receive this license, you must agree to its +rules. The rules of this license are both obligations +under that agreement and conditions to your license. +You must not do anything with this software that triggers +a rule that you cannot or will not follow. + +## Copyright + +Each contributor licenses you to do everything with this +software that would otherwise infringe that contributor's +copyright in it. + +## Notices + +You must ensure that everyone who gets a copy of +any part of this software from you, with or without +changes, also gets the text of this license or a link to +. + +## Excuse + +If anyone notifies you in writing that you have not +complied with [Notices](#notices), you can keep your +license by taking all practical steps to comply within 30 +days after the notice. If you do not do so, your license +ends immediately. + +## Patent + +Each contributor licenses you to do everything with this +software that would otherwise infringe any patent claims +they can license or become able to license. + +## Reliability + +No contributor can revoke this license. + +## No Liability + +***As far as the law allows, this software comes as is, +without any warranty or condition, and no contributor +will be liable to anyone for any damages related to this +software or this license, under any kind of legal claim.*** \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..acc7fb6 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# Deckster + +## Attribution +[foxxyz’s `loupedeck` library for JavaScript](https://github.com/foxxyz/loupedeck) +(licensed under the [MIT license](https://github.com/foxxyz/loupedeck/blob/e41e5d920130d9ef651e47173c68450b9c832b96/LICENSE)) +was used as a reference for and inspired the design of `loupedeck_serial`. \ No newline at end of file diff --git a/deckster/Cargo.toml b/deckster/Cargo.toml new file mode 100644 index 0000000..ef61dcd --- /dev/null +++ b/deckster/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "deckster" +version = "0.1.0" +edition = "2021" + +[dependencies] +loupedeck_serial = { path = "../loupedeck_serial" } +color-eyre = "0.6.2" \ No newline at end of file diff --git a/deckster/src/main.rs b/deckster/src/main.rs new file mode 100644 index 0000000..bf80c76 --- /dev/null +++ b/deckster/src/main.rs @@ -0,0 +1,14 @@ +use color_eyre::eyre::ContextCompat; +use color_eyre::Result; + +fn main() -> Result<()> { + let available_devices = loupedeck_serial::device::LoupedeckDevice::discover()?; + + let mut device = available_devices.first() + .wrap_err("at least one device should be connected")? + .open()?; + + device.connect()?; + + Ok(()) +} diff --git a/loupedeck_serial/Cargo.toml b/loupedeck_serial/Cargo.toml new file mode 100644 index 0000000..2659a7a --- /dev/null +++ b/loupedeck_serial/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "loupedeck_serial" +version = "0.1.0" +edition = "2021" + +[dependencies] +serialport = "4.3.0" +enum-ordinalize = "4.3.0" +enumset = "1.1.3" +bytes = "1.5.0" +thiserror = "1.0.52" \ No newline at end of file diff --git a/loupedeck_serial/src/characteristics.rs b/loupedeck_serial/src/characteristics.rs new file mode 100644 index 0000000..70fd207 --- /dev/null +++ b/loupedeck_serial/src/characteristics.rs @@ -0,0 +1,112 @@ +use enum_ordinalize::Ordinalize; +use enumset::{enum_set, EnumSet, EnumSetType}; + +#[derive(Debug, Ordinalize, EnumSetType)] +#[repr(u16)] +pub enum LoupedeckDeviceKnob { + KnobTopLeft = 0x01, + KnobCenterLeft = 0x02, + KnobBottomLeft = 0x03, + KnobTopRight = 0x04, + KnobCenterRight = 0x05, + KnobBottomRight = 0x06, +} + +#[derive(Debug, Ordinalize, EnumSetType)] +#[repr(u16)] +pub enum LoupedeckDeviceButton { + KnobTopLeft = 0x01, + KnobCenterLeft = 0x02, + KnobBottomLeft = 0x03, + KnobTopRight = 0x04, + KnobCenterRight = 0x05, + KnobBottomRight = 0x06, + N0 = 0x07, + N1 = 0x08, + N2 = 0x09, + N3 = 0x0a, + N4 = 0x0b, + N5 = 0x0c, + N6 = 0x0d, + N7 = 0x0e, +} + +#[derive(Debug)] +pub enum LoupedeckDeviceDisplayEndianness { + BigEndian, + LittleEndian +} + +#[derive(Debug)] +pub struct LoupedeckDeviceDisplayConfiguration { + pub id: u8, + pub width: usize, + pub height: usize, + pub offset: (usize, usize), + pub endianness: LoupedeckDeviceDisplayEndianness +} + +#[derive(Debug)] +pub struct LoupedeckDeviceCharacteristics { + pub vendor_id: u16, + pub product_id: u16, + pub name: &'static str, + pub available_knobs: EnumSet, + pub available_buttons: EnumSet, + pub key_grid_dimensions: (usize, usize), + pub displays: &'static [LoupedeckDeviceDisplayConfiguration] +} + +static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = LoupedeckDeviceCharacteristics { + vendor_id: 0x2ec2, + product_id: 0x0004, + name: "Loupedeck Live", + available_knobs: enum_set!(LoupedeckDeviceKnob::KnobTopLeft + | LoupedeckDeviceKnob::KnobCenterLeft + | LoupedeckDeviceKnob::KnobBottomLeft + | LoupedeckDeviceKnob::KnobTopRight + | LoupedeckDeviceKnob::KnobCenterRight + | LoupedeckDeviceKnob::KnobBottomRight), + available_buttons: enum_set!(LoupedeckDeviceButton::KnobTopLeft + | LoupedeckDeviceButton::KnobCenterLeft + | LoupedeckDeviceButton::KnobBottomLeft + | LoupedeckDeviceButton::KnobTopRight + | LoupedeckDeviceButton::KnobCenterRight + | LoupedeckDeviceButton::KnobBottomRight + | LoupedeckDeviceButton::N0 + | LoupedeckDeviceButton::N1 + | LoupedeckDeviceButton::N2 + | LoupedeckDeviceButton::N3 + | LoupedeckDeviceButton::N4 + | LoupedeckDeviceButton::N5 + | LoupedeckDeviceButton::N6 + | LoupedeckDeviceButton::N7), + key_grid_dimensions: (4, 3), + displays: &[ + LoupedeckDeviceDisplayConfiguration { // Left + id: 0x4d, + width: 60, + height: 270, + offset: (0, 0), + endianness: LoupedeckDeviceDisplayEndianness::LittleEndian, + }, + LoupedeckDeviceDisplayConfiguration { // Center + id: 0x4d, + width: 360, + height: 270, + offset: (60, 0), + endianness: LoupedeckDeviceDisplayEndianness::LittleEndian, + }, + LoupedeckDeviceDisplayConfiguration { // Right + id: 0x4d, + width: 60, + height: 270, + offset: (420, 0), + endianness: LoupedeckDeviceDisplayEndianness::LittleEndian, + }, + ] +}; + +pub static CHARACTERISTICS: [&LoupedeckDeviceCharacteristics; 1] = [ + &LOUPEDECK_LIVE_CHARACTERISTIC +]; \ No newline at end of file diff --git a/loupedeck_serial/src/device.rs b/loupedeck_serial/src/device.rs new file mode 100644 index 0000000..6e548d0 --- /dev/null +++ b/loupedeck_serial/src/device.rs @@ -0,0 +1,145 @@ +use std::io; +use std::io::{BufReader, Read, Write}; +use std::io::ErrorKind::TimedOut; +use std::sync::Arc; +use std::sync::mpsc::{Sender, sync_channel, SyncSender}; +use std::thread::spawn; +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use serialport::{SerialPort, SerialPortType}; +use thiserror::Error; +use crate::characteristics::{CHARACTERISTICS, LoupedeckDeviceCharacteristics}; +use crate::discovery::AvailableLoupedeckDevice; + +const WS_UPGRADE_REQUEST: &str = r#"GET /index.html +HTTP/1.1 +Connection: Upgrade +Upgrade: websocket +Sec-WebSocket-Key: 123abc + +"#; + +const WS_UPGRADE_RESPONSE_START: &str = "HTTP/1.1 101 Switching Protocols\r\n\ +Upgrade: websocket\r\n\ +Connection: Upgrade\r\n\ +Sec-WebSocket-Accept: ALtlZo9FMEUEQleXJmq++ukUQ1s="; + +const MESSAGE_START_BYTE: u8 = 0x82; +const MAX_MESSAGE_LENGTH: usize = u8::MAX as usize; + +#[derive(Debug)] +pub struct LoupedeckDevice { + pub(crate) port: Option>, + pub characteristics: &'static LoupedeckDeviceCharacteristics, +} + +#[derive(Debug, Error)] +pub enum ConnectError { + #[error("IO Error: {0}")] + IO(#[from] io::Error), + + #[error("The device did not respond with the expected handshake response.")] + WrongHandshakeResponse, +} + +impl LoupedeckDevice { + pub fn new_unchecked(port: Box, characteristics: &'static LoupedeckDeviceCharacteristics) -> LoupedeckDevice { + LoupedeckDevice { + port: Some(port), + characteristics, + } + } + + pub fn discover() -> Result, serialport::Error> { + let ports = serialport::available_ports()?; + + Ok(ports.iter().filter_map(|port| { + if let SerialPortType::UsbPort(info) = &port.port_type { + let characteristics = CHARACTERISTICS.iter() + .find(|c| c.vendor_id == info.vid && c.product_id == info.pid); + + if let Some(characteristics) = characteristics { + return Some(AvailableLoupedeckDevice { + port_name: port.port_name.clone(), + characteristics, + }); + } + } + + None + }).collect::>()) + } + + pub fn connect(&mut self) -> Result<(), ConnectError> { + let mut port = self.port.take().expect("already connected"); + port.write_all(WS_UPGRADE_REQUEST.as_bytes())?; + port.flush()?; + + let mut buf = [0; WS_UPGRADE_RESPONSE_START.len()]; + port.read_exact(&mut buf)?; + + if buf != WS_UPGRADE_RESPONSE_START.as_bytes() { + return Err(ConnectError::WrongHandshakeResponse); + } + + let (sender, receiver) = sync_channel::(8); + let handle = spawn(move || { + LoupedeckDevice::handle_messages(port, sender); + }); + + for x in receiver { + dbg!(&x[1]); + } + + handle.join().unwrap(); + + Ok(()) + } + + fn handle_messages(mut port: Box, sender: SyncSender) { + let mut buffer = BytesMut::new(); + + loop { + let mut chunk = BytesMut::zeroed(MAX_MESSAGE_LENGTH); + let read_result = port.read(&mut chunk); + + if let Err(err) = &read_result { + if err.kind() == TimedOut { + continue + } + } + + let len = read_result.unwrap(); + if len == 0 { + panic!("read 0 bytes"); + } + + chunk.truncate(len); + buffer.put(chunk); + + loop { + let start_index = buffer.iter().position(|b| *b == MESSAGE_START_BYTE); + + if let Some(start_index) = start_index { + if start_index > 0 { + buffer.advance(start_index); + + if buffer.remaining() == 0 { + break; + } + } + + let length = buffer[1] as usize + 2; + if length > buffer.remaining() { + break; + } else { + let mut message = buffer.split_to(length); + message.advance(2); + sender.send(message.freeze()).unwrap(); + } + } else { + break; + } + } + } + } +} \ No newline at end of file diff --git a/loupedeck_serial/src/discovery.rs b/loupedeck_serial/src/discovery.rs new file mode 100644 index 0000000..629ff82 --- /dev/null +++ b/loupedeck_serial/src/discovery.rs @@ -0,0 +1,33 @@ +use std::time::Duration; +use serialport::{DataBits, FlowControl, Parity, StopBits}; +use crate::characteristics::LoupedeckDeviceCharacteristics; +use crate::device::LoupedeckDevice; + +#[derive(Debug)] +pub struct AvailableLoupedeckDevice { + pub(crate) port_name: String, + pub(crate) characteristics: &'static LoupedeckDeviceCharacteristics, +} + +impl AvailableLoupedeckDevice { + pub fn port_name(&self) -> &str { + &self.port_name + } + + pub fn characteristics(&self) -> &'static LoupedeckDeviceCharacteristics { + self.characteristics + } + + pub fn open(&self) -> Result { + Ok(LoupedeckDevice { + port: Some(serialport::new(&self.port_name, 256000) + .data_bits(DataBits::Eight) + .stop_bits(StopBits::One) + .parity(Parity::None) + .flow_control(FlowControl::None) + .timeout(Duration::from_secs(10)) + .open()?), + characteristics: self.characteristics, + }) + } +} \ No newline at end of file diff --git a/loupedeck_serial/src/lib.rs b/loupedeck_serial/src/lib.rs new file mode 100644 index 0000000..9347fc6 --- /dev/null +++ b/loupedeck_serial/src/lib.rs @@ -0,0 +1,3 @@ +pub mod characteristics; +pub mod device; +pub mod discovery; \ No newline at end of file