commit
This commit is contained in:
parent
66c86df2a4
commit
6b4ea3f4ae
13 changed files with 819 additions and 375 deletions
258
Cargo.lock
generated
258
Cargo.lock
generated
|
@ -280,6 +280,16 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
|
@ -377,9 +387,11 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"loupedeck_serial",
|
"loupedeck_serial",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"parse-display 0.9.0",
|
||||||
"regex",
|
"regex",
|
||||||
"resvg",
|
"resvg",
|
||||||
"rgb",
|
"rgb",
|
||||||
|
"rumqttc",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_regex",
|
"serde_regex",
|
||||||
|
@ -411,7 +423,7 @@ dependencies = [
|
||||||
"enum-map",
|
"enum-map",
|
||||||
"enum-ordinalize",
|
"enum-ordinalize",
|
||||||
"im",
|
"im",
|
||||||
"parse-display",
|
"parse-display 0.8.2",
|
||||||
"rgb",
|
"rgb",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
|
@ -645,6 +657,25 @@ version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.30"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.30"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.11"
|
version = "0.2.11"
|
||||||
|
@ -1061,6 +1092,12 @@ version = "1.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-probe"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "owo-colors"
|
name = "owo-colors"
|
||||||
version = "3.5.0"
|
version = "3.5.0"
|
||||||
|
@ -1077,7 +1114,7 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
"pa_volume_interface",
|
"pa_volume_interface",
|
||||||
"parse-display",
|
"parse-display 0.8.2",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_regex",
|
"serde_regex",
|
||||||
|
@ -1124,10 +1161,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6509d08722b53e8dafe97f2027b22ccbe3a5db83cb352931e9716b0aa44bc5c"
|
checksum = "c6509d08722b53e8dafe97f2027b22ccbe3a5db83cb352931e9716b0aa44bc5c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parse-display-derive",
|
"parse-display-derive 0.8.2",
|
||||||
"regex",
|
"regex",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parse-display"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06af5f9333eb47bd9ba8462d612e37a8328a5cb80b13f0af4de4c3b89f52dee5"
|
||||||
|
dependencies = [
|
||||||
|
"parse-display-derive 0.9.0",
|
||||||
|
"regex",
|
||||||
|
"regex-syntax 0.8.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parse-display-derive"
|
name = "parse-display-derive"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
@ -1139,7 +1187,21 @@ dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"regex",
|
"regex",
|
||||||
"regex-syntax 0.7.5",
|
"regex-syntax 0.7.5",
|
||||||
"structmeta",
|
"structmeta 0.2.0",
|
||||||
|
"syn 2.0.48",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parse-display-derive"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc9252f259500ee570c75adcc4e317fa6f57a1e47747d622e0bf838002a7b790"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"regex",
|
||||||
|
"regex-syntax 0.8.2",
|
||||||
|
"structmeta 0.3.0",
|
||||||
"syn 2.0.48",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1155,6 +1217,12 @@ version = "0.2.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-utils"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.28"
|
version = "0.3.28"
|
||||||
|
@ -1196,9 +1264,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.76"
|
version = "1.0.78"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c"
|
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
@ -1309,6 +1377,20 @@ dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"getrandom",
|
||||||
|
"libc",
|
||||||
|
"spin",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "roxmltree"
|
name = "roxmltree"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
|
@ -1324,6 +1406,24 @@ version = "0.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rumqttc"
|
||||||
|
version = "0.23.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d8941c6791801b667d52bfe9ff4fc7c968d4f3f9ae8ae7abdaaa1c966feafc8"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"flume",
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"rustls-native-certs",
|
||||||
|
"rustls-pemfile",
|
||||||
|
"rustls-webpki",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
|
@ -1345,6 +1445,49 @@ dependencies = [
|
||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.21.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"ring",
|
||||||
|
"rustls-webpki",
|
||||||
|
"sct",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-native-certs"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
|
||||||
|
dependencies = [
|
||||||
|
"openssl-probe",
|
||||||
|
"rustls-pemfile",
|
||||||
|
"schannel",
|
||||||
|
"security-framework",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pemfile"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.101.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustybuzz"
|
name = "rustybuzz"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
|
@ -1393,12 +1536,54 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "schannel"
|
||||||
|
version = "0.1.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sct"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework"
|
||||||
|
version = "2.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"core-foundation",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
"security-framework-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "security-framework-sys"
|
||||||
|
version = "2.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "self_cell"
|
name = "self_cell"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
|
@ -1558,6 +1743,15 @@ dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slotmap"
|
name = "slotmap"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
|
@ -1573,6 +1767,16 @@ version = "1.11.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
|
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.9.8"
|
version = "0.9.8"
|
||||||
|
@ -1605,7 +1809,19 @@ checksum = "78ad9e09554f0456d67a69c1584c9798ba733a5b50349a6c0d0948710523922d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"structmeta-derive",
|
"structmeta-derive 0.2.0",
|
||||||
|
"syn 2.0.48",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "structmeta"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e1575d8d40908d70f6fd05537266b90ae71b15dbbe7a8b7dffa2b759306d329"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"structmeta-derive 0.3.0",
|
||||||
"syn 2.0.48",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1620,6 +1836,17 @@ dependencies = [
|
||||||
"syn 2.0.48",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "structmeta-derive"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.48",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "svgtypes"
|
name = "svgtypes"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
|
@ -1785,6 +2012,7 @@ dependencies = [
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
@ -1800,6 +2028,16 @@ dependencies = [
|
||||||
"syn 2.0.48",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.24.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||||
|
dependencies = [
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.8"
|
version = "0.8.8"
|
||||||
|
@ -1956,6 +2194,12 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
|
checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "usvg"
|
name = "usvg"
|
||||||
version = "0.37.0"
|
version = "0.37.0"
|
||||||
|
|
|
@ -9,6 +9,7 @@ clap = { version = "4.4.12", features = ["derive"] }
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
cosmic-text = "0.10.0"
|
cosmic-text = "0.10.0"
|
||||||
derive_more = "0.99.17"
|
derive_more = "0.99.17"
|
||||||
|
parse-display = "0.9.0"
|
||||||
encode_unicode = "1.0.0"
|
encode_unicode = "1.0.0"
|
||||||
enum-map = "3.0.0-beta.2"
|
enum-map = "3.0.0-beta.2"
|
||||||
enum-ordinalize = "4.3.0"
|
enum-ordinalize = "4.3.0"
|
||||||
|
@ -32,6 +33,7 @@ toml = "0.8.8"
|
||||||
walkdir = "2.4.0"
|
walkdir = "2.4.0"
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
is_executable = "1.0.1"
|
is_executable = "1.0.1"
|
||||||
|
rumqttc = "0.23.0"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
|
|
|
@ -5,12 +5,14 @@ use crate::path::{KeyPath, KnobPath};
|
||||||
use crate::style::{KeyStyle, KnobStyle};
|
use crate::style::{KeyStyle, KnobStyle};
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum RotationDirection {
|
pub enum RotationDirection {
|
||||||
Clockwise,
|
Clockwise,
|
||||||
Counterclockwise,
|
Counterclockwise,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||||
pub enum KnobEvent {
|
pub enum KnobEvent {
|
||||||
Press,
|
Press,
|
||||||
ButtonDown,
|
ButtonDown,
|
||||||
|
@ -20,6 +22,7 @@ pub enum KnobEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum KeyTouchEventKind {
|
pub enum KeyTouchEventKind {
|
||||||
Start,
|
Start,
|
||||||
Move,
|
Move,
|
||||||
|
@ -27,6 +30,7 @@ pub enum KeyTouchEventKind {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||||
pub enum KeyEvent {
|
pub enum KeyEvent {
|
||||||
Press,
|
Press,
|
||||||
Touch { touch_id: u8, x: u16, y: u16, kind: KeyTouchEventKind },
|
Touch { touch_id: u8, x: u16, y: u16, kind: KeyTouchEventKind },
|
||||||
|
|
|
@ -42,7 +42,7 @@ pub enum PaEntityMetadata {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct PaEntityState {
|
pub struct PaEntityState {
|
||||||
id: PaEntityId,
|
id: PaEntityId,
|
||||||
channel_volumes: ChannelVolumes,
|
channel_volumes: ChannelVolumes,
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
inactive_button_color = "#000060"
|
inactive_button_color = "#000060"
|
||||||
active_button_color = "#eeffff"
|
active_button_color = "#eeffff"
|
||||||
label_font_family = "Inter"
|
label_font_family = "Inter"
|
||||||
buttons = { }
|
buttons = {}
|
||||||
|
|
||||||
[initial]
|
[initial]
|
||||||
key_page = "default"
|
key_page = "default"
|
||||||
knob_page = "default"
|
knob_page = "default"
|
||||||
|
|
||||||
|
[mqtt]
|
||||||
|
client_id = "deckster_host"
|
||||||
|
topic_prefix = "deckster"
|
||||||
|
host = "localhost"
|
||||||
|
port = 1883
|
||||||
|
|
||||||
[icon_packs.apps]
|
[icon_packs.apps]
|
||||||
path = "icons/apps"
|
path = "icons/apps"
|
||||||
format = "svg"
|
format = "svg"
|
||||||
|
|
22
examples/full/rumqttd.toml
Normal file
22
examples/full/rumqttd.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
id = 0
|
||||||
|
|
||||||
|
[router]
|
||||||
|
id = 0
|
||||||
|
max_connections = 10010
|
||||||
|
max_outgoing_packet_count = 200
|
||||||
|
max_segment_size = 104857600
|
||||||
|
max_segment_count = 10
|
||||||
|
|
||||||
|
[v4.1]
|
||||||
|
name = "v4-1"
|
||||||
|
listen = "0.0.0.0:1883"
|
||||||
|
next_connection_delay_ms = 1
|
||||||
|
|
||||||
|
[v4.1.connections]
|
||||||
|
connection_timeout_ms = 60000
|
||||||
|
max_payload_size = 20480
|
||||||
|
max_inflight_count = 100
|
||||||
|
dynamic_filters = true
|
||||||
|
|
||||||
|
[console]
|
||||||
|
listen = "0.0.0.0:3030"
|
|
@ -210,9 +210,12 @@ async fn manage_knob(path: KnobPath, config: KnobConfig, mut events: broadcast::
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
Ok(volume_state) = volume_states.recv() => {
|
Ok(volume_state) = volume_states.recv() => {
|
||||||
entity_state = volume_state.entities_by_id().values().find(|entity| state_matches(&config.target, entity)).map(Arc::clone);
|
let new_entity_state = volume_state.entities_by_id().values().find(|entity| state_matches(&config.target, entity)).map(Arc::clone);
|
||||||
update_knob_style(&entity_state);
|
if entity_state != new_entity_state {
|
||||||
update_knob_value(&entity_state);
|
entity_state = new_entity_state;
|
||||||
|
update_knob_style(&entity_state);
|
||||||
|
update_knob_value(&entity_state);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((event_path, event)) = events.recv() => {
|
Ok((event_path, event)) = events.recv() => {
|
||||||
|
|
|
@ -25,7 +25,7 @@ pub async fn start(
|
||||||
key_configs: HashMap<KeyPath, KeyOrKnobConfig>,
|
key_configs: HashMap<KeyPath, KeyOrKnobConfig>,
|
||||||
knob_configs: HashMap<KnobPath, KeyOrKnobConfig>,
|
knob_configs: HashMap<KnobPath, KeyOrKnobConfig>,
|
||||||
commands_sender: flume::Sender<HandlerCommand>,
|
commands_sender: flume::Sender<HandlerCommand>,
|
||||||
events_receiver: flume::Receiver<HandlerEvent>,
|
mut events_receiver: tokio::sync::broadcast::Receiver<HandlerEvent>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let handler_names: Vec<OsString> = std::fs::read_dir(handlers_directory)
|
let handler_names: Vec<OsString> = std::fs::read_dir(handlers_directory)
|
||||||
.wrap_err_with(|| format!("while reading the handlers directory: {}", handlers_directory.to_string_lossy()))?
|
.wrap_err_with(|| format!("while reading the handlers directory: {}", handlers_directory.to_string_lossy()))?
|
||||||
|
@ -125,7 +125,7 @@ pub async fn start(
|
||||||
}
|
}
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Ok(event) = events_receiver.recv_async().await {
|
while let Ok(event) = events_receiver.recv().await {
|
||||||
let config = match &event {
|
let config = match &event {
|
||||||
HandlerEvent::Key { path, .. } => {
|
HandlerEvent::Key { path, .. } => {
|
||||||
if let Some(config) = key_configs.get(path) {
|
if let Some(config) = key_configs.get(path) {
|
||||||
|
|
|
@ -72,6 +72,7 @@ pub async fn main() -> Result<()> {
|
||||||
buttons: deckster_file.buttons.into_iter().collect(),
|
buttons: deckster_file.buttons.into_iter().collect(),
|
||||||
icon_packs: deckster_file.icon_packs,
|
icon_packs: deckster_file.icon_packs,
|
||||||
initial: deckster_file.initial,
|
initial: deckster_file.initial,
|
||||||
|
mqtt: deckster_file.mqtt,
|
||||||
}
|
}
|
||||||
.validate()?;
|
.validate()?;
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ pub struct File {
|
||||||
pub icon_packs: Arc<HashMap<String, IconPack>>,
|
pub icon_packs: Arc<HashMap<String, IconPack>>,
|
||||||
pub buttons: HashMap<ButtonPosition, ButtonConfig>, // EnumMap
|
pub buttons: HashMap<ButtonPosition, ButtonConfig>, // EnumMap
|
||||||
pub initial: InitialConfig,
|
pub initial: InitialConfig,
|
||||||
|
pub mqtt: Option<MqttConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -41,6 +42,7 @@ pub struct Config {
|
||||||
pub icon_packs: Arc<HashMap<String, IconPack>>,
|
pub icon_packs: Arc<HashMap<String, IconPack>>,
|
||||||
pub buttons: EnumMap<ButtonPosition, ButtonConfig>,
|
pub buttons: EnumMap<ButtonPosition, ButtonConfig>,
|
||||||
pub initial: InitialConfig,
|
pub initial: InitialConfig,
|
||||||
|
pub mqtt: Option<MqttConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inactive_button_color_default() -> RGB8Wrapper {
|
fn inactive_button_color_default() -> RGB8Wrapper {
|
||||||
|
@ -63,6 +65,21 @@ pub struct InitialConfig {
|
||||||
pub knob_page: String,
|
pub knob_page: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct MqttConfig {
|
||||||
|
pub client_id: String,
|
||||||
|
pub host: String,
|
||||||
|
pub port: u16,
|
||||||
|
pub credentials: Option<MqttConfigCredentials>,
|
||||||
|
pub topic_prefix: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct MqttConfigCredentials {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum IconFormat {
|
pub enum IconFormat {
|
||||||
|
|
366
src/runner/io_worker.rs
Normal file
366
src/runner/io_worker.rs
Normal file
|
@ -0,0 +1,366 @@
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use enum_ordinalize::Ordinalize;
|
||||||
|
use log::{error, trace};
|
||||||
|
use resvg::usvg::tiny_skia_path::IntSize;
|
||||||
|
use rgb::RGB8;
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
|
use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent, KeyEvent, KeyTouchEventKind, KnobEvent};
|
||||||
|
use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition};
|
||||||
|
use deckster_shared::state::{Key, Knob};
|
||||||
|
use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics, LoupedeckDisplayRect, LoupedeckKnob};
|
||||||
|
use loupedeck_serial::device::LoupedeckDevice;
|
||||||
|
use loupedeck_serial::events::{LoupedeckEvent, RotationDirection};
|
||||||
|
|
||||||
|
use crate::icons::IconManager;
|
||||||
|
use crate::model::config::Config;
|
||||||
|
use crate::model::position::ButtonPosition;
|
||||||
|
use crate::runner::graphics::labels::LabelRenderer;
|
||||||
|
use crate::runner::graphics::{render_key, render_knob, GraphicsContext};
|
||||||
|
use crate::runner::state::State;
|
||||||
|
|
||||||
|
enum IoWork {
|
||||||
|
DeviceEvent(LoupedeckEvent),
|
||||||
|
Command(HandlerCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct IoWorkerContext {
|
||||||
|
config: Arc<Config>,
|
||||||
|
device: LoupedeckDevice,
|
||||||
|
commands_sender: flume::Sender<HandlerCommand>,
|
||||||
|
events_sender: broadcast::Sender<HandlerEvent>,
|
||||||
|
graphics: GraphicsContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IoWorkerContext {
|
||||||
|
pub fn create(
|
||||||
|
config_directory: &Path,
|
||||||
|
config: Arc<Config>,
|
||||||
|
device: LoupedeckDevice,
|
||||||
|
commands_sender: flume::Sender<HandlerCommand>,
|
||||||
|
events_sender: broadcast::Sender<HandlerEvent>,
|
||||||
|
) -> Self {
|
||||||
|
let buffer_endianness = device.characteristics().key_grid.display.endianness;
|
||||||
|
let label_renderer = RefCell::new(LabelRenderer::new(config.label_font_family.as_ref()));
|
||||||
|
let dpi = device.characteristics().key_grid.display.dpi;
|
||||||
|
let icon_packs = Arc::clone(&config.icon_packs);
|
||||||
|
|
||||||
|
IoWorkerContext {
|
||||||
|
config,
|
||||||
|
device,
|
||||||
|
commands_sender,
|
||||||
|
events_sender,
|
||||||
|
graphics: GraphicsContext {
|
||||||
|
buffer_endianness,
|
||||||
|
label_renderer,
|
||||||
|
icon_manager: IconManager::new(config_directory.to_path_buf(), icon_packs, dpi),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn do_io_work(context: IoWorkerContext, commands_receiver: flume::Receiver<HandlerCommand>) {
|
||||||
|
let mut state = State::create(&context.config);
|
||||||
|
let device_events_receiver = context.device.events();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let a = flume::Selector::new()
|
||||||
|
.recv(&device_events_receiver, |e| IoWork::DeviceEvent(e.unwrap()))
|
||||||
|
.recv(&commands_receiver, |c| IoWork::Command(c.unwrap()))
|
||||||
|
.wait();
|
||||||
|
|
||||||
|
match a {
|
||||||
|
IoWork::DeviceEvent(event) => {
|
||||||
|
if !handle_event(&context, &mut state, event) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IoWork::Command(command) => handle_command(&context, &mut state, command),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEvent) -> bool {
|
||||||
|
trace!("Handling event: {:?}", &event);
|
||||||
|
|
||||||
|
let send_key_event = |path: KeyPath, event: KeyEvent| {
|
||||||
|
trace!("Sending key event ({}): {:?}", &path, &event);
|
||||||
|
context.events_sender.send(HandlerEvent::Key { path, event }).unwrap();
|
||||||
|
};
|
||||||
|
|
||||||
|
let send_knob_event = |path: KnobPath, event: KnobEvent| {
|
||||||
|
trace!("Sending knob event ({:?}): {:?}", &path, &event);
|
||||||
|
context.events_sender.send(HandlerEvent::Knob { path, event }).unwrap();
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
|
LoupedeckEvent::Disconnected => return false,
|
||||||
|
LoupedeckEvent::ButtonDown { button } => {
|
||||||
|
let position = ButtonPosition::of(&button);
|
||||||
|
let button_config = &context.config.buttons[position];
|
||||||
|
|
||||||
|
context
|
||||||
|
.commands_sender
|
||||||
|
.send(HandlerCommand::SetActivePages {
|
||||||
|
key_page_id: button_config.key_page.as_ref().unwrap_or(&state.active_key_page_id).clone(),
|
||||||
|
knob_page_id: button_config.knob_page.as_ref().unwrap_or(&state.active_knob_page_id).clone(),
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
LoupedeckEvent::Touch { x, y, is_end, touch_id } => {
|
||||||
|
let characteristics = context.device.characteristics();
|
||||||
|
let display = characteristics.get_display_at_coordinates(x, y);
|
||||||
|
|
||||||
|
if let Some(display) = display {
|
||||||
|
if display == &characteristics.key_grid.display {
|
||||||
|
let key_index = characteristics.key_grid.get_key_at_global_coordinates(x, y);
|
||||||
|
if let Some(key_index) = key_index {
|
||||||
|
let position = KeyPosition {
|
||||||
|
x: (key_index % characteristics.key_grid.columns) as u16 + 1,
|
||||||
|
y: (key_index / characteristics.key_grid.columns) as u16 + 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = KeyPath {
|
||||||
|
page_id: state.active_key_page_id.clone(),
|
||||||
|
position,
|
||||||
|
};
|
||||||
|
|
||||||
|
let LoupedeckDisplayRect {
|
||||||
|
x: top_left_x, y: top_left_y, ..
|
||||||
|
} = characteristics.key_grid.get_local_key_rect(key_index).unwrap();
|
||||||
|
|
||||||
|
let kind = if is_end {
|
||||||
|
state.active_touch_ids.remove(&touch_id);
|
||||||
|
KeyTouchEventKind::End
|
||||||
|
} else {
|
||||||
|
let is_new = state.active_touch_ids.insert(touch_id);
|
||||||
|
if is_new {
|
||||||
|
KeyTouchEventKind::Start
|
||||||
|
} else {
|
||||||
|
KeyTouchEventKind::Move
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
send_key_event(
|
||||||
|
path.clone(),
|
||||||
|
KeyEvent::Touch {
|
||||||
|
touch_id,
|
||||||
|
x: x - top_left_x,
|
||||||
|
y: y - top_left_y,
|
||||||
|
kind,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if kind == KeyTouchEventKind::Start {
|
||||||
|
send_key_event(path.clone(), KeyEvent::Press);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LoupedeckEvent::KnobRotate { knob, direction } => {
|
||||||
|
let position: KnobPosition = get_position_of_loupedeck_knob(knob);
|
||||||
|
|
||||||
|
send_knob_event(
|
||||||
|
KnobPath {
|
||||||
|
page_id: state.active_knob_page_id.clone(),
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
KnobEvent::Rotate {
|
||||||
|
direction: match direction {
|
||||||
|
RotationDirection::Clockwise => deckster_shared::handler_communication::RotationDirection::Clockwise,
|
||||||
|
RotationDirection::Counterclockwise => deckster_shared::handler_communication::RotationDirection::Counterclockwise,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
LoupedeckEvent::KnobDown { knob } => {
|
||||||
|
let position: KnobPosition = get_position_of_loupedeck_knob(knob);
|
||||||
|
|
||||||
|
send_knob_event(
|
||||||
|
KnobPath {
|
||||||
|
page_id: state.active_knob_page_id.clone(),
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
KnobEvent::Press,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_command(context: &IoWorkerContext, state: &mut State, command: HandlerCommand) {
|
||||||
|
trace!("Handling command: {:?}", &command);
|
||||||
|
|
||||||
|
match command {
|
||||||
|
HandlerCommand::SetActivePages { key_page_id, knob_page_id } => {
|
||||||
|
state.active_key_page_id = key_page_id;
|
||||||
|
state.active_knob_page_id = knob_page_id;
|
||||||
|
|
||||||
|
for button in LoupedeckButton::VARIANTS {
|
||||||
|
let position = ButtonPosition::of(button);
|
||||||
|
|
||||||
|
context
|
||||||
|
.device
|
||||||
|
.set_button_color(*button, get_correct_button_color(context, state, position))
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let key_grid = &context.device.characteristics().key_grid;
|
||||||
|
for index in 0..(key_grid.rows * key_grid.columns) {
|
||||||
|
draw_key_at_index(context, state, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
for position in KnobPosition::VARIANTS {
|
||||||
|
draw_knob_at_position(context, state, *position);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.device.refresh_display(&key_grid.display).unwrap();
|
||||||
|
}
|
||||||
|
HandlerCommand::SetKeyStyle { path, value } => {
|
||||||
|
state.mutate_key_for_command("SetKeyStyle", &path, |k| {
|
||||||
|
k.style = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
draw_key_at_path_if_visible(context, state, path);
|
||||||
|
context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap();
|
||||||
|
}
|
||||||
|
HandlerCommand::SetKnobStyle { path, value } => {
|
||||||
|
state.mutate_knob_for_command("SetKnobStyle", &path, |k| {
|
||||||
|
k.style = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
draw_knob_at_path_if_visible(context, state, path);
|
||||||
|
context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap();
|
||||||
|
}
|
||||||
|
HandlerCommand::SetKnobValue { path, value } => {
|
||||||
|
if let Some(v) = value {
|
||||||
|
if !(0.0..=1.0).contains(&v) {
|
||||||
|
error!("Received SetKnobValue with an out-of-range value: {}", v);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.mutate_knob_for_command("SetKnobValue", &path, |k| {
|
||||||
|
k.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
draw_knob_at_path_if_visible(context, state, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// active -> config.active_button_color
|
||||||
|
// no actions defined -> #000000
|
||||||
|
// inactive -> config.inactive_button_color
|
||||||
|
fn get_correct_button_color(context: &IoWorkerContext, state: &State, button_position: ButtonPosition) -> RGB8 {
|
||||||
|
let button_config = &context.config.buttons[button_position];
|
||||||
|
|
||||||
|
if let Some(key_page) = &button_config.key_page {
|
||||||
|
if key_page == &state.active_key_page_id {
|
||||||
|
if let Some(knob_page) = &button_config.knob_page {
|
||||||
|
if knob_page == &state.active_knob_page_id {
|
||||||
|
return context.config.active_button_color.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if button_config.knob_page.is_none() {
|
||||||
|
return RGB8::new(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.config.inactive_button_color.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_key_index_for_position(key_grid: &LoupedeckDeviceKeyGridCharacteristics, position: KeyPosition) -> Option<u8> {
|
||||||
|
if (position.x - 1) >= key_grid.columns as u16 || (position.y - 1) >= key_grid.rows as u16 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let x = (position.x - 1) as u8;
|
||||||
|
let y = (position.y - 1) as u8;
|
||||||
|
Some(y * key_grid.columns + x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_key_position_for_index(key_grid: &LoupedeckDeviceKeyGridCharacteristics, index: u8) -> KeyPosition {
|
||||||
|
let x = index % key_grid.columns;
|
||||||
|
let y = index / key_grid.columns;
|
||||||
|
|
||||||
|
KeyPosition {
|
||||||
|
x: (x + 1) as u16,
|
||||||
|
y: (y + 1) as u16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_key(context: &IoWorkerContext, index: u8, key: Option<&Key>) {
|
||||||
|
let key_grid = &context.device.characteristics().key_grid;
|
||||||
|
let rect = key_grid.get_local_key_rect(index).unwrap();
|
||||||
|
|
||||||
|
let buffer = render_key(&context.graphics, IntSize::from_wh(rect.w as u32, rect.h as u32).unwrap(), key);
|
||||||
|
context
|
||||||
|
.device
|
||||||
|
.replace_framebuffer_area_raw(&key_grid.display, rect.x, rect.y, rect.w, rect.h, buffer)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_key_at_index(context: &IoWorkerContext, state: &State, index: u8) {
|
||||||
|
let position = get_key_position_for_index(&context.device.characteristics().key_grid, index);
|
||||||
|
|
||||||
|
draw_key(context, index, state.active_key_page().keys_by_position.get(&position));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_key_at_position_if_visible(context: &IoWorkerContext, state: &State, position: KeyPosition) {
|
||||||
|
let index = get_key_index_for_position(&context.device.characteristics().key_grid, position);
|
||||||
|
|
||||||
|
if let Some(index) = index {
|
||||||
|
draw_key(context, index, state.active_key_page().keys_by_position.get(&position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_key_at_path_if_visible(context: &IoWorkerContext, state: &State, path: KeyPath) {
|
||||||
|
if state.active_key_page_id == path.page_id {
|
||||||
|
draw_key_at_position_if_visible(context, state, path.position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_knob(context: &IoWorkerContext, position: KnobPosition, knob: Option<&Knob>) {
|
||||||
|
if let Some((display, rect)) = context.device.characteristics().get_display_and_rect_for_knob(match position {
|
||||||
|
KnobPosition::LeftTop => LoupedeckKnob::LeftTop,
|
||||||
|
KnobPosition::LeftMiddle => LoupedeckKnob::LeftMiddle,
|
||||||
|
KnobPosition::LeftBottom => LoupedeckKnob::LeftBottom,
|
||||||
|
KnobPosition::RightTop => LoupedeckKnob::RightTop,
|
||||||
|
KnobPosition::RightMiddle => LoupedeckKnob::RightMiddle,
|
||||||
|
KnobPosition::RightBottom => LoupedeckKnob::RightBottom,
|
||||||
|
}) {
|
||||||
|
let buffer = render_knob(&context.graphics, IntSize::from_wh(rect.w as u32, rect.h as u32).unwrap(), knob);
|
||||||
|
context
|
||||||
|
.device
|
||||||
|
.replace_framebuffer_area_raw(display, rect.x, rect.y, rect.w, rect.h, buffer)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_knob_at_position(context: &IoWorkerContext, state: &State, position: KnobPosition) {
|
||||||
|
draw_knob(context, position, Some(&state.active_knob_page().knobs_by_position[position]));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_knob_at_path_if_visible(context: &IoWorkerContext, state: &State, path: KnobPath) {
|
||||||
|
if state.active_knob_page_id == path.page_id {
|
||||||
|
draw_knob_at_position(context, state, path.position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_position_of_loupedeck_knob(value: LoupedeckKnob) -> KnobPosition {
|
||||||
|
match value {
|
||||||
|
LoupedeckKnob::LeftTop => KnobPosition::LeftTop,
|
||||||
|
LoupedeckKnob::LeftMiddle => KnobPosition::LeftMiddle,
|
||||||
|
LoupedeckKnob::LeftBottom => KnobPosition::LeftBottom,
|
||||||
|
LoupedeckKnob::RightTop => KnobPosition::RightTop,
|
||||||
|
LoupedeckKnob::RightMiddle => KnobPosition::RightMiddle,
|
||||||
|
LoupedeckKnob::RightBottom => KnobPosition::RightBottom,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,34 +1,26 @@
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
use color_eyre::eyre::{ContextCompat, WrapErr};
|
use color_eyre::eyre::{ContextCompat, WrapErr};
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use enum_ordinalize::Ordinalize;
|
use log::info;
|
||||||
use log::{error, info, trace};
|
use tokio::sync::broadcast;
|
||||||
use rgb::RGB8;
|
|
||||||
use tiny_skia::IntSize;
|
|
||||||
|
|
||||||
use deckster_shared::handler_communication::KnobEvent;
|
use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent};
|
||||||
use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent, KeyEvent, KeyTouchEventKind};
|
use deckster_shared::path::{KeyPath, KnobPath};
|
||||||
use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition};
|
|
||||||
use deckster_shared::state::{Key, Knob};
|
|
||||||
use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics, LoupedeckDisplayRect, LoupedeckKnob};
|
|
||||||
use loupedeck_serial::commands::VibrationPattern;
|
use loupedeck_serial::commands::VibrationPattern;
|
||||||
use loupedeck_serial::device::LoupedeckDevice;
|
use loupedeck_serial::device::LoupedeckDevice;
|
||||||
use loupedeck_serial::events::{LoupedeckEvent, RotationDirection};
|
|
||||||
|
|
||||||
use crate::handler_host;
|
use crate::handler_host;
|
||||||
use crate::handler_host::KeyOrKnobConfig;
|
use crate::handler_host::KeyOrKnobConfig;
|
||||||
use crate::icons::IconManager;
|
|
||||||
use crate::model::config::Config;
|
use crate::model::config::Config;
|
||||||
use crate::model::position::ButtonPosition;
|
use crate::runner::io_worker::{do_io_work, IoWorkerContext};
|
||||||
use crate::runner::graphics::labels::LabelRenderer;
|
use crate::runner::mqtt::start_mqtt_client;
|
||||||
use crate::runner::graphics::{render_key, render_knob, GraphicsContext};
|
|
||||||
use crate::runner::state::State;
|
|
||||||
|
|
||||||
mod graphics;
|
mod graphics;
|
||||||
|
mod io_worker;
|
||||||
|
mod mqtt;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
pub async fn start(config_directory: &Path, config: Config) -> Result<()> {
|
pub async fn start(config_directory: &Path, config: Config) -> Result<()> {
|
||||||
|
@ -40,7 +32,7 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> {
|
||||||
info!("Found {} device(s).", available_devices.len());
|
info!("Found {} device(s).", available_devices.len());
|
||||||
|
|
||||||
let (commands_sender, commands_receiver) = flume::bounded::<HandlerCommand>(5);
|
let (commands_sender, commands_receiver) = flume::bounded::<HandlerCommand>(5);
|
||||||
let (events_sender, events_receiver) = flume::bounded::<HandlerEvent>(5);
|
let events_sender = broadcast::Sender::<HandlerEvent>::new(5);
|
||||||
|
|
||||||
commands_sender
|
commands_sender
|
||||||
.send(HandlerCommand::SetActivePages {
|
.send(HandlerCommand::SetActivePages {
|
||||||
|
@ -98,10 +90,15 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> {
|
||||||
key_configs,
|
key_configs,
|
||||||
knob_configs,
|
knob_configs,
|
||||||
commands_sender.clone(),
|
commands_sender.clone(),
|
||||||
events_receiver,
|
events_sender.subscribe(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if let Some(mqtt_config) = &config.mqtt {
|
||||||
|
info!("Initializing MQTT client…");
|
||||||
|
start_mqtt_client(mqtt_config, commands_sender.clone(), events_sender.subscribe()).await;
|
||||||
|
}
|
||||||
|
|
||||||
info!("Connecting to the device…");
|
info!("Connecting to the device…");
|
||||||
let device = available_device.connect().wrap_err("Connecting to the device failed.")?;
|
let device = available_device.connect().wrap_err("Connecting to the device failed.")?;
|
||||||
info!("Connected.");
|
info!("Connected.");
|
||||||
|
@ -123,346 +120,3 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
enum IoWork {
|
|
||||||
DeviceEvent(LoupedeckEvent),
|
|
||||||
Command(HandlerCommand),
|
|
||||||
}
|
|
||||||
|
|
||||||
struct IoWorkerContext {
|
|
||||||
config: Arc<Config>,
|
|
||||||
device: LoupedeckDevice,
|
|
||||||
commands_sender: flume::Sender<HandlerCommand>,
|
|
||||||
events_sender: flume::Sender<HandlerEvent>,
|
|
||||||
graphics: GraphicsContext,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IoWorkerContext {
|
|
||||||
pub fn create(
|
|
||||||
config_directory: &Path,
|
|
||||||
config: Arc<Config>,
|
|
||||||
device: LoupedeckDevice,
|
|
||||||
commands_sender: flume::Sender<HandlerCommand>,
|
|
||||||
events_sender: flume::Sender<HandlerEvent>,
|
|
||||||
) -> Self {
|
|
||||||
let buffer_endianness = device.characteristics().key_grid.display.endianness;
|
|
||||||
let label_renderer = RefCell::new(LabelRenderer::new(config.label_font_family.as_ref()));
|
|
||||||
let dpi = device.characteristics().key_grid.display.dpi;
|
|
||||||
let icon_packs = Arc::clone(&config.icon_packs);
|
|
||||||
|
|
||||||
IoWorkerContext {
|
|
||||||
config,
|
|
||||||
device,
|
|
||||||
commands_sender,
|
|
||||||
events_sender,
|
|
||||||
graphics: GraphicsContext {
|
|
||||||
buffer_endianness,
|
|
||||||
label_renderer,
|
|
||||||
icon_manager: IconManager::new(config_directory.to_path_buf(), icon_packs, dpi),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_io_work(context: IoWorkerContext, commands_receiver: flume::Receiver<HandlerCommand>) {
|
|
||||||
let mut state = State::create(&context.config);
|
|
||||||
let device_events_receiver = context.device.events();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let a = flume::Selector::new()
|
|
||||||
.recv(&device_events_receiver, |e| IoWork::DeviceEvent(e.unwrap()))
|
|
||||||
.recv(&commands_receiver, |c| IoWork::Command(c.unwrap()))
|
|
||||||
.wait();
|
|
||||||
|
|
||||||
match a {
|
|
||||||
IoWork::DeviceEvent(event) => {
|
|
||||||
if !handle_event(&context, &mut state, event) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IoWork::Command(command) => handle_command(&context, &mut state, command),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEvent) -> bool {
|
|
||||||
trace!("Handling event: {:?}", &event);
|
|
||||||
|
|
||||||
let send_key_event = |path: KeyPath, event: KeyEvent| {
|
|
||||||
trace!("Sending key event ({}): {:?}", &path, &event);
|
|
||||||
context.events_sender.send(HandlerEvent::Key { path, event }).unwrap();
|
|
||||||
};
|
|
||||||
|
|
||||||
let send_knob_event = |path: KnobPath, event: KnobEvent| {
|
|
||||||
trace!("Sending knob event ({:?}): {:?}", &path, &event);
|
|
||||||
context.events_sender.send(HandlerEvent::Knob { path, event }).unwrap();
|
|
||||||
};
|
|
||||||
|
|
||||||
match event {
|
|
||||||
LoupedeckEvent::Disconnected => return false,
|
|
||||||
LoupedeckEvent::ButtonDown { button } => {
|
|
||||||
let position = ButtonPosition::of(&button);
|
|
||||||
let button_config = &context.config.buttons[position];
|
|
||||||
|
|
||||||
context
|
|
||||||
.commands_sender
|
|
||||||
.send(HandlerCommand::SetActivePages {
|
|
||||||
key_page_id: button_config.key_page.as_ref().unwrap_or(&state.active_key_page_id).clone(),
|
|
||||||
knob_page_id: button_config.knob_page.as_ref().unwrap_or(&state.active_knob_page_id).clone(),
|
|
||||||
})
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
LoupedeckEvent::Touch { x, y, is_end, touch_id } => {
|
|
||||||
let characteristics = context.device.characteristics();
|
|
||||||
let display = characteristics.get_display_at_coordinates(x, y);
|
|
||||||
|
|
||||||
if let Some(display) = display {
|
|
||||||
if display == &characteristics.key_grid.display {
|
|
||||||
let key_index = characteristics.key_grid.get_key_at_global_coordinates(x, y);
|
|
||||||
if let Some(key_index) = key_index {
|
|
||||||
let position = KeyPosition {
|
|
||||||
x: (key_index % characteristics.key_grid.columns) as u16 + 1,
|
|
||||||
y: (key_index / characteristics.key_grid.columns) as u16 + 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
let path = KeyPath {
|
|
||||||
page_id: state.active_key_page_id.clone(),
|
|
||||||
position,
|
|
||||||
};
|
|
||||||
|
|
||||||
let LoupedeckDisplayRect {
|
|
||||||
x: top_left_x, y: top_left_y, ..
|
|
||||||
} = characteristics.key_grid.get_local_key_rect(key_index).unwrap();
|
|
||||||
|
|
||||||
let kind = if is_end {
|
|
||||||
state.active_touch_ids.remove(&touch_id);
|
|
||||||
KeyTouchEventKind::End
|
|
||||||
} else {
|
|
||||||
let is_new = state.active_touch_ids.insert(touch_id);
|
|
||||||
if is_new {
|
|
||||||
KeyTouchEventKind::Start
|
|
||||||
} else {
|
|
||||||
KeyTouchEventKind::Move
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
send_key_event(
|
|
||||||
path.clone(),
|
|
||||||
KeyEvent::Touch {
|
|
||||||
touch_id,
|
|
||||||
x: x - top_left_x,
|
|
||||||
y: y - top_left_y,
|
|
||||||
kind,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if kind == KeyTouchEventKind::Start {
|
|
||||||
send_key_event(path.clone(), KeyEvent::Press);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LoupedeckEvent::KnobRotate { knob, direction } => {
|
|
||||||
let position: KnobPosition = get_position_of_loupedeck_knob(knob);
|
|
||||||
|
|
||||||
send_knob_event(
|
|
||||||
KnobPath {
|
|
||||||
page_id: state.active_knob_page_id.clone(),
|
|
||||||
position,
|
|
||||||
},
|
|
||||||
KnobEvent::Rotate {
|
|
||||||
direction: match direction {
|
|
||||||
RotationDirection::Clockwise => deckster_shared::handler_communication::RotationDirection::Clockwise,
|
|
||||||
RotationDirection::Counterclockwise => deckster_shared::handler_communication::RotationDirection::Counterclockwise,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
LoupedeckEvent::KnobDown { knob } => {
|
|
||||||
let position: KnobPosition = get_position_of_loupedeck_knob(knob);
|
|
||||||
|
|
||||||
send_knob_event(
|
|
||||||
KnobPath {
|
|
||||||
page_id: state.active_knob_page_id.clone(),
|
|
||||||
position,
|
|
||||||
},
|
|
||||||
KnobEvent::Press,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_command(context: &IoWorkerContext, state: &mut State, command: HandlerCommand) {
|
|
||||||
trace!("Handling command: {:?}", &command);
|
|
||||||
|
|
||||||
match command {
|
|
||||||
HandlerCommand::SetActivePages { key_page_id, knob_page_id } => {
|
|
||||||
state.active_key_page_id = key_page_id;
|
|
||||||
state.active_knob_page_id = knob_page_id;
|
|
||||||
|
|
||||||
for button in LoupedeckButton::VARIANTS {
|
|
||||||
let position = ButtonPosition::of(button);
|
|
||||||
|
|
||||||
context
|
|
||||||
.device
|
|
||||||
.set_button_color(*button, get_correct_button_color(context, state, position))
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let key_grid = &context.device.characteristics().key_grid;
|
|
||||||
for index in 0..(key_grid.rows * key_grid.columns) {
|
|
||||||
draw_key_at_index(context, state, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
for position in KnobPosition::VARIANTS {
|
|
||||||
draw_knob_at_position(context, state, *position);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.device.refresh_display(&key_grid.display).unwrap();
|
|
||||||
}
|
|
||||||
HandlerCommand::SetKeyStyle { path, value } => {
|
|
||||||
state.mutate_key_for_command("SetKeyStyle", &path, |k| {
|
|
||||||
k.style = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
draw_key_at_path_if_visible(context, state, path);
|
|
||||||
context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap();
|
|
||||||
}
|
|
||||||
HandlerCommand::SetKnobStyle { path, value } => {
|
|
||||||
state.mutate_knob_for_command("SetKnobStyle", &path, |k| {
|
|
||||||
k.style = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
draw_knob_at_path_if_visible(context, state, path);
|
|
||||||
context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap();
|
|
||||||
}
|
|
||||||
HandlerCommand::SetKnobValue { path, value } => {
|
|
||||||
if let Some(v) = value {
|
|
||||||
if !(0.0..=1.0).contains(&v) {
|
|
||||||
error!("Received SetKnobValue with an out-of-range value: {}", v);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state.mutate_knob_for_command("SetKnobValue", &path, |k| {
|
|
||||||
k.value = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
draw_knob_at_path_if_visible(context, state, path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// active -> config.active_button_color
|
|
||||||
// no actions defined -> #000000
|
|
||||||
// inactive -> config.inactive_button_color
|
|
||||||
fn get_correct_button_color(context: &IoWorkerContext, state: &State, button_position: ButtonPosition) -> RGB8 {
|
|
||||||
let button_config = &context.config.buttons[button_position];
|
|
||||||
|
|
||||||
if let Some(key_page) = &button_config.key_page {
|
|
||||||
if key_page == &state.active_key_page_id {
|
|
||||||
if let Some(knob_page) = &button_config.knob_page {
|
|
||||||
if knob_page == &state.active_knob_page_id {
|
|
||||||
return context.config.active_button_color.into();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if button_config.knob_page.is_none() {
|
|
||||||
return RGB8::new(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.config.inactive_button_color.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_key_index_for_position(key_grid: &LoupedeckDeviceKeyGridCharacteristics, position: KeyPosition) -> Option<u8> {
|
|
||||||
if (position.x - 1) >= key_grid.columns as u16 || (position.y - 1) >= key_grid.rows as u16 {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let x = (position.x - 1) as u8;
|
|
||||||
let y = (position.y - 1) as u8;
|
|
||||||
Some(y * key_grid.columns + x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_key_position_for_index(key_grid: &LoupedeckDeviceKeyGridCharacteristics, index: u8) -> KeyPosition {
|
|
||||||
let x = index % key_grid.columns;
|
|
||||||
let y = index / key_grid.columns;
|
|
||||||
|
|
||||||
KeyPosition {
|
|
||||||
x: (x + 1) as u16,
|
|
||||||
y: (y + 1) as u16,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_key(context: &IoWorkerContext, index: u8, key: Option<&Key>) {
|
|
||||||
let key_grid = &context.device.characteristics().key_grid;
|
|
||||||
let rect = key_grid.get_local_key_rect(index).unwrap();
|
|
||||||
|
|
||||||
let buffer = render_key(&context.graphics, IntSize::from_wh(rect.w as u32, rect.h as u32).unwrap(), key);
|
|
||||||
context
|
|
||||||
.device
|
|
||||||
.replace_framebuffer_area_raw(&key_grid.display, rect.x, rect.y, rect.w, rect.h, buffer)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_key_at_index(context: &IoWorkerContext, state: &State, index: u8) {
|
|
||||||
let position = get_key_position_for_index(&context.device.characteristics().key_grid, index);
|
|
||||||
|
|
||||||
draw_key(context, index, state.active_key_page().keys_by_position.get(&position));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_key_at_position_if_visible(context: &IoWorkerContext, state: &State, position: KeyPosition) {
|
|
||||||
let index = get_key_index_for_position(&context.device.characteristics().key_grid, position);
|
|
||||||
|
|
||||||
if let Some(index) = index {
|
|
||||||
draw_key(context, index, state.active_key_page().keys_by_position.get(&position));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_key_at_path_if_visible(context: &IoWorkerContext, state: &State, path: KeyPath) {
|
|
||||||
if state.active_key_page_id == path.page_id {
|
|
||||||
draw_key_at_position_if_visible(context, state, path.position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_knob(context: &IoWorkerContext, position: KnobPosition, knob: Option<&Knob>) {
|
|
||||||
if let Some((display, rect)) = context.device.characteristics().get_display_and_rect_for_knob(match position {
|
|
||||||
KnobPosition::LeftTop => LoupedeckKnob::LeftTop,
|
|
||||||
KnobPosition::LeftMiddle => LoupedeckKnob::LeftMiddle,
|
|
||||||
KnobPosition::LeftBottom => LoupedeckKnob::LeftBottom,
|
|
||||||
KnobPosition::RightTop => LoupedeckKnob::RightTop,
|
|
||||||
KnobPosition::RightMiddle => LoupedeckKnob::RightMiddle,
|
|
||||||
KnobPosition::RightBottom => LoupedeckKnob::RightBottom,
|
|
||||||
}) {
|
|
||||||
let buffer = render_knob(&context.graphics, IntSize::from_wh(rect.w as u32, rect.h as u32).unwrap(), knob);
|
|
||||||
context
|
|
||||||
.device
|
|
||||||
.replace_framebuffer_area_raw(display, rect.x, rect.y, rect.w, rect.h, buffer)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_knob_at_position(context: &IoWorkerContext, state: &State, position: KnobPosition) {
|
|
||||||
draw_knob(context, position, Some(&state.active_knob_page().knobs_by_position[position]));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_knob_at_path_if_visible(context: &IoWorkerContext, state: &State, path: KnobPath) {
|
|
||||||
if state.active_knob_page_id == path.page_id {
|
|
||||||
draw_knob_at_position(context, state, path.position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_position_of_loupedeck_knob(value: LoupedeckKnob) -> KnobPosition {
|
|
||||||
match value {
|
|
||||||
LoupedeckKnob::LeftTop => KnobPosition::LeftTop,
|
|
||||||
LoupedeckKnob::LeftMiddle => KnobPosition::LeftMiddle,
|
|
||||||
LoupedeckKnob::LeftBottom => KnobPosition::LeftBottom,
|
|
||||||
LoupedeckKnob::RightTop => KnobPosition::RightTop,
|
|
||||||
LoupedeckKnob::RightMiddle => KnobPosition::RightMiddle,
|
|
||||||
LoupedeckKnob::RightBottom => KnobPosition::RightBottom,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
125
src/runner/mqtt.rs
Normal file
125
src/runner/mqtt.rs
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use rumqttc::{Event, Incoming, MqttOptions, QoS};
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
|
use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent};
|
||||||
|
use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition};
|
||||||
|
use deckster_shared::style::{KeyStyle, KnobStyle};
|
||||||
|
|
||||||
|
use crate::model::config::MqttConfig;
|
||||||
|
|
||||||
|
pub async fn start_mqtt_client(config: &MqttConfig, commands_sender: flume::Sender<HandlerCommand>, mut events_receiver: broadcast::Receiver<HandlerEvent>) {
|
||||||
|
let topic_prefix = config.topic_prefix.to_owned();
|
||||||
|
|
||||||
|
let mut options = MqttOptions::new(&config.topic_prefix, &config.host, config.port);
|
||||||
|
options.set_keep_alive(Duration::from_secs(3));
|
||||||
|
options.set_clean_session(false);
|
||||||
|
|
||||||
|
if let Some(credentials) = &config.credentials {
|
||||||
|
options.set_credentials(&credentials.username, &credentials.password);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (client, mut event_loop) = rumqttc::AsyncClient::new(options, 32);
|
||||||
|
|
||||||
|
client.subscribe(format!("{topic_prefix}/keys/+/+/style"), QoS::ExactlyOnce).await.unwrap();
|
||||||
|
client.subscribe(format!("{topic_prefix}/knobs/+/+/style"), QoS::ExactlyOnce).await.unwrap();
|
||||||
|
client.subscribe(format!("{topic_prefix}/knobs/+/+/value"), QoS::ExactlyOnce).await.unwrap();
|
||||||
|
|
||||||
|
tokio::spawn({
|
||||||
|
let topic_prefix = topic_prefix.clone();
|
||||||
|
|
||||||
|
async move {
|
||||||
|
while let Ok(event) = events_receiver.recv().await {
|
||||||
|
match event {
|
||||||
|
HandlerEvent::Key { path, event } => {
|
||||||
|
client
|
||||||
|
.publish(
|
||||||
|
format!("{topic_prefix}/keys/{path}/events"),
|
||||||
|
QoS::ExactlyOnce,
|
||||||
|
false,
|
||||||
|
serde_json::to_string(&event).unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
HandlerEvent::Knob { path, event } => {
|
||||||
|
client
|
||||||
|
.publish(
|
||||||
|
format!("{topic_prefix}/knobs/{path}/events"),
|
||||||
|
QoS::ExactlyOnce,
|
||||||
|
false,
|
||||||
|
serde_json::to_string(&event).unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Ok(event) = event_loop.poll().await {
|
||||||
|
if let Event::Incoming(Incoming::Publish(event)) = event {
|
||||||
|
let segments = event.topic.strip_prefix(&topic_prefix).unwrap().split('/').collect::<Vec<&str>>();
|
||||||
|
let page_id = segments[1];
|
||||||
|
let position = segments[2];
|
||||||
|
let property = segments[3];
|
||||||
|
|
||||||
|
match segments[0] {
|
||||||
|
"keys" => {
|
||||||
|
if property == "style" {
|
||||||
|
let value = serde_json::from_slice::<Option<KeyStyle>>(&event.payload).unwrap();
|
||||||
|
|
||||||
|
commands_sender
|
||||||
|
.send(HandlerCommand::SetKeyStyle {
|
||||||
|
path: KeyPath {
|
||||||
|
page_id: page_id.to_owned(),
|
||||||
|
position: KeyPosition::from_str(position).unwrap(),
|
||||||
|
},
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"knobs" => {
|
||||||
|
let position = KnobPosition::from_str(position).unwrap();
|
||||||
|
|
||||||
|
match property {
|
||||||
|
"style" => {
|
||||||
|
let value = serde_json::from_slice::<Option<KnobStyle>>(&event.payload).unwrap();
|
||||||
|
|
||||||
|
commands_sender
|
||||||
|
.send(HandlerCommand::SetKnobStyle {
|
||||||
|
path: KnobPath {
|
||||||
|
page_id: page_id.to_owned(),
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
"value" => {
|
||||||
|
let value = serde_json::from_slice::<Option<f32>>(&event.payload).unwrap();
|
||||||
|
|
||||||
|
commands_sender
|
||||||
|
.send(HandlerCommand::SetKnobValue {
|
||||||
|
path: KnobPath {
|
||||||
|
page_id: page_id.to_owned(),
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue