From 6b4ea3f4aebfcb35231471980aab23b6b3fc1d02 Mon Sep 17 00:00:00 2001 From: Moritz Ruth Date: Mon, 5 Feb 2024 00:13:03 +0100 Subject: [PATCH] commit --- Cargo.lock | 258 +++++++++++- Cargo.toml | 2 + .../src/handler_communication.rs | 4 + crates/pa_volume_interface/src/lib.rs | 2 +- examples/full/deckster.toml | 8 +- examples/full/rumqttd.toml | 22 + handlers/pa_volume/src/handler.rs | 9 +- src/handler_host/mod.rs | 4 +- src/main.rs | 1 + src/model/config.rs | 17 + src/runner/io_worker.rs | 366 +++++++++++++++++ src/runner/mod.rs | 376 +----------------- src/runner/mqtt.rs | 125 ++++++ 13 files changed, 819 insertions(+), 375 deletions(-) create mode 100644 examples/full/rumqttd.toml create mode 100644 src/runner/io_worker.rs create mode 100644 src/runner/mqtt.rs diff --git a/Cargo.lock b/Cargo.lock index 198c150..7e980bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -280,6 +280,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "core-foundation-sys" version = "0.8.6" @@ -377,9 +387,11 @@ dependencies = [ "log", "loupedeck_serial", "once_cell", + "parse-display 0.9.0", "regex", "resvg", "rgb", + "rumqttc", "serde", "serde_json", "serde_regex", @@ -411,7 +423,7 @@ dependencies = [ "enum-map", "enum-ordinalize", "im", - "parse-display", + "parse-display 0.8.2", "rgb", "serde", "serde_with", @@ -645,6 +657,25 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "getrandom" version = "0.2.11" @@ -1061,6 +1092,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "owo-colors" version = "3.5.0" @@ -1077,7 +1114,7 @@ dependencies = [ "env_logger", "log", "pa_volume_interface", - "parse-display", + "parse-display 0.8.2", "regex", "serde", "serde_regex", @@ -1124,10 +1161,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6509d08722b53e8dafe97f2027b22ccbe3a5db83cb352931e9716b0aa44bc5c" dependencies = [ "once_cell", - "parse-display-derive", + "parse-display-derive 0.8.2", "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]] name = "parse-display-derive" version = "0.8.2" @@ -1139,7 +1187,21 @@ dependencies = [ "quote", "regex", "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", ] @@ -1155,6 +1217,12 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[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.28" @@ -1196,9 +1264,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "proc-macro2" -version = "1.0.76" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -1309,6 +1377,20 @@ dependencies = [ "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]] name = "roxmltree" version = "0.18.1" @@ -1324,6 +1406,24 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "rustc-demangle" version = "0.1.23" @@ -1345,6 +1445,49 @@ dependencies = [ "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]] name = "rustybuzz" version = "0.11.0" @@ -1393,12 +1536,54 @@ dependencies = [ "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]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "self_cell" version = "1.0.3" @@ -1558,6 +1743,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "slotmap" version = "1.0.7" @@ -1573,6 +1767,16 @@ version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "spin" version = "0.9.8" @@ -1605,7 +1809,19 @@ checksum = "78ad9e09554f0456d67a69c1584c9798ba733a5b50349a6c0d0948710523922d" dependencies = [ "proc-macro2", "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", ] @@ -1620,6 +1836,17 @@ dependencies = [ "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]] name = "svgtypes" version = "0.13.0" @@ -1785,6 +2012,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", + "socket2", "tokio-macros", "windows-sys 0.48.0", ] @@ -1800,6 +2028,16 @@ dependencies = [ "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]] name = "toml" version = "0.8.8" @@ -1956,6 +2194,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "usvg" version = "0.37.0" diff --git a/Cargo.toml b/Cargo.toml index fff62ff..32085c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ clap = { version = "4.4.12", features = ["derive"] } color-eyre = "0.6.2" cosmic-text = "0.10.0" derive_more = "0.99.17" +parse-display = "0.9.0" encode_unicode = "1.0.0" enum-map = "3.0.0-beta.2" enum-ordinalize = "4.3.0" @@ -32,6 +33,7 @@ toml = "0.8.8" walkdir = "2.4.0" once_cell = "1.19.0" is_executable = "1.0.1" +rumqttc = "0.23.0" [workspace] members = [ diff --git a/crates/deckster_shared/src/handler_communication.rs b/crates/deckster_shared/src/handler_communication.rs index f4c1740..df64f7f 100644 --- a/crates/deckster_shared/src/handler_communication.rs +++ b/crates/deckster_shared/src/handler_communication.rs @@ -5,12 +5,14 @@ use crate::path::{KeyPath, KnobPath}; use crate::style::{KeyStyle, KnobStyle}; #[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] pub enum RotationDirection { Clockwise, Counterclockwise, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] pub enum KnobEvent { Press, ButtonDown, @@ -20,6 +22,7 @@ pub enum KnobEvent { } #[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] pub enum KeyTouchEventKind { Start, Move, @@ -27,6 +30,7 @@ pub enum KeyTouchEventKind { } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "kebab-case")] pub enum KeyEvent { Press, Touch { touch_id: u8, x: u16, y: u16, kind: KeyTouchEventKind }, diff --git a/crates/pa_volume_interface/src/lib.rs b/crates/pa_volume_interface/src/lib.rs index dd719b6..0360daf 100644 --- a/crates/pa_volume_interface/src/lib.rs +++ b/crates/pa_volume_interface/src/lib.rs @@ -42,7 +42,7 @@ pub enum PaEntityMetadata { }, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct PaEntityState { id: PaEntityId, channel_volumes: ChannelVolumes, diff --git a/examples/full/deckster.toml b/examples/full/deckster.toml index 0784f5b..afb9846 100644 --- a/examples/full/deckster.toml +++ b/examples/full/deckster.toml @@ -1,12 +1,18 @@ inactive_button_color = "#000060" active_button_color = "#eeffff" label_font_family = "Inter" -buttons = { } +buttons = {} [initial] key_page = "default" knob_page = "default" +[mqtt] +client_id = "deckster_host" +topic_prefix = "deckster" +host = "localhost" +port = 1883 + [icon_packs.apps] path = "icons/apps" format = "svg" diff --git a/examples/full/rumqttd.toml b/examples/full/rumqttd.toml new file mode 100644 index 0000000..4cd0b0d --- /dev/null +++ b/examples/full/rumqttd.toml @@ -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" diff --git a/handlers/pa_volume/src/handler.rs b/handlers/pa_volume/src/handler.rs index e1c148c..e547549 100644 --- a/handlers/pa_volume/src/handler.rs +++ b/handlers/pa_volume/src/handler.rs @@ -210,9 +210,12 @@ async fn manage_knob(path: KnobPath, config: KnobConfig, mut events: broadcast:: loop { tokio::select! { Ok(volume_state) = volume_states.recv() => { - entity_state = volume_state.entities_by_id().values().find(|entity| state_matches(&config.target, entity)).map(Arc::clone); - update_knob_style(&entity_state); - update_knob_value(&entity_state); + let new_entity_state = volume_state.entities_by_id().values().find(|entity| state_matches(&config.target, entity)).map(Arc::clone); + if entity_state != new_entity_state { + entity_state = new_entity_state; + update_knob_style(&entity_state); + update_knob_value(&entity_state); + } } Ok((event_path, event)) = events.recv() => { diff --git a/src/handler_host/mod.rs b/src/handler_host/mod.rs index e57ae67..b589858 100644 --- a/src/handler_host/mod.rs +++ b/src/handler_host/mod.rs @@ -25,7 +25,7 @@ pub async fn start( key_configs: HashMap, knob_configs: HashMap, commands_sender: flume::Sender, - events_receiver: flume::Receiver, + mut events_receiver: tokio::sync::broadcast::Receiver, ) -> Result<()> { let handler_names: Vec = std::fs::read_dir(handlers_directory) .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 { - while let Ok(event) = events_receiver.recv_async().await { + while let Ok(event) = events_receiver.recv().await { let config = match &event { HandlerEvent::Key { path, .. } => { if let Some(config) = key_configs.get(path) { diff --git a/src/main.rs b/src/main.rs index 9613667..506a856 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,6 +72,7 @@ pub async fn main() -> Result<()> { buttons: deckster_file.buttons.into_iter().collect(), icon_packs: deckster_file.icon_packs, initial: deckster_file.initial, + mqtt: deckster_file.mqtt, } .validate()?; diff --git a/src/model/config.rs b/src/model/config.rs index 1e88393..9563563 100644 --- a/src/model/config.rs +++ b/src/model/config.rs @@ -23,6 +23,7 @@ pub struct File { pub icon_packs: Arc>, pub buttons: HashMap, // EnumMap pub initial: InitialConfig, + pub mqtt: Option, } #[derive(Debug)] @@ -41,6 +42,7 @@ pub struct Config { pub icon_packs: Arc>, pub buttons: EnumMap, pub initial: InitialConfig, + pub mqtt: Option, } fn inactive_button_color_default() -> RGB8Wrapper { @@ -63,6 +65,21 @@ pub struct InitialConfig { pub knob_page: String, } +#[derive(Debug, Deserialize)] +pub struct MqttConfig { + pub client_id: String, + pub host: String, + pub port: u16, + pub credentials: Option, + pub topic_prefix: String, +} + +#[derive(Debug, Deserialize)] +pub struct MqttConfigCredentials { + pub username: String, + pub password: String, +} + #[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum IconFormat { diff --git a/src/runner/io_worker.rs b/src/runner/io_worker.rs new file mode 100644 index 0000000..8bf8cd9 --- /dev/null +++ b/src/runner/io_worker.rs @@ -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, + device: LoupedeckDevice, + commands_sender: flume::Sender, + events_sender: broadcast::Sender, + graphics: GraphicsContext, +} + +impl IoWorkerContext { + pub fn create( + config_directory: &Path, + config: Arc, + device: LoupedeckDevice, + commands_sender: flume::Sender, + events_sender: broadcast::Sender, + ) -> 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) { + 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 { + 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, + } +} diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 8805b13..ac590d6 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -1,34 +1,26 @@ -use std::cell::RefCell; use std::path::Path; use std::sync::Arc; use std::thread; use color_eyre::eyre::{ContextCompat, WrapErr}; use color_eyre::Result; -use enum_ordinalize::Ordinalize; -use log::{error, info, trace}; -use rgb::RGB8; -use tiny_skia::IntSize; +use log::info; +use tokio::sync::broadcast; -use deckster_shared::handler_communication::KnobEvent; -use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent, KeyEvent, KeyTouchEventKind}; -use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition}; -use deckster_shared::state::{Key, Knob}; -use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics, LoupedeckDisplayRect, LoupedeckKnob}; +use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent}; +use deckster_shared::path::{KeyPath, KnobPath}; use loupedeck_serial::commands::VibrationPattern; use loupedeck_serial::device::LoupedeckDevice; -use loupedeck_serial::events::{LoupedeckEvent, RotationDirection}; use crate::handler_host; use crate::handler_host::KeyOrKnobConfig; -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; +use crate::runner::io_worker::{do_io_work, IoWorkerContext}; +use crate::runner::mqtt::start_mqtt_client; mod graphics; +mod io_worker; +mod mqtt; pub mod state; 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()); let (commands_sender, commands_receiver) = flume::bounded::(5); - let (events_sender, events_receiver) = flume::bounded::(5); + let events_sender = broadcast::Sender::::new(5); commands_sender .send(HandlerCommand::SetActivePages { @@ -98,10 +90,15 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> { key_configs, knob_configs, commands_sender.clone(), - events_receiver, + events_sender.subscribe(), ) .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…"); let device = available_device.connect().wrap_err("Connecting to the device failed.")?; info!("Connected."); @@ -123,346 +120,3 @@ pub async fn start(config_directory: &Path, config: Config) -> Result<()> { Ok(()) } - -enum IoWork { - DeviceEvent(LoupedeckEvent), - Command(HandlerCommand), -} - -struct IoWorkerContext { - config: Arc, - device: LoupedeckDevice, - commands_sender: flume::Sender, - events_sender: flume::Sender, - graphics: GraphicsContext, -} - -impl IoWorkerContext { - pub fn create( - config_directory: &Path, - config: Arc, - device: LoupedeckDevice, - commands_sender: flume::Sender, - events_sender: flume::Sender, - ) -> 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) { - 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 { - 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, - } -} diff --git a/src/runner/mqtt.rs b/src/runner/mqtt.rs new file mode 100644 index 0000000..4b54988 --- /dev/null +++ b/src/runner/mqtt.rs @@ -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, mut events_receiver: broadcast::Receiver) { + 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::>(); + 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::>(&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::>(&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::>(&event.payload).unwrap(); + + commands_sender + .send(HandlerCommand::SetKnobValue { + path: KnobPath { + page_id: page_id.to_owned(), + position, + }, + value, + }) + .unwrap(); + } + _ => {} + } + } + _ => {} + }; + } + } + }); +}