From: Jonathan McDowell Date: Sat, 26 Apr 2025 17:40:15 +0000 (+0100) Subject: Add initial support for setting button colours X-Git-Tag: v0.2.0~8 X-Git-Url: https://the.earth.li/gitweb/?a=commitdiff_plain;h=17f9cc094fb88ace15d06272d73421f17337a73b;p=mqttdeck.git Add initial support for setting button colours Initial rough 'n ready code to accept JSON commands over MQTT to set the colour of the buttons on the StreamDeck. --- diff --git a/Cargo.lock b/Cargo.lock index 4422e7b..6882f12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "ahash" version = "0.7.8" @@ -41,6 +47,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -132,6 +144,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-channel" version = "0.5.8" @@ -143,20 +164,48 @@ dependencies = [ ] [[package]] -name = "crossbeam-utils" -version = "0.8.15" +name = "crossbeam-deque" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ - "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", ] +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + [[package]] name = "dlv-list" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "elgato-streamdeck" version = "0.5.0" @@ -196,6 +245,40 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fdeflate" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8090f921a24b04994d9929e204f50b498a33ea6ba559ffaa05e04f7ee7fb5ab" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "futures" version = "0.3.28" @@ -302,6 +385,25 @@ dependencies = [ "wasi", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -354,8 +456,13 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", + "exr", + "gif", "jpeg-decoder", "num-traits", + "png", + "qoi", + "tiff", ] [[package]] @@ -386,6 +493,21 @@ name = "jpeg-decoder" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +dependencies = [ + "rayon", +] + +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" @@ -411,6 +533,16 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mqttdeck" version = "0.1.0" @@ -418,6 +550,8 @@ dependencies = [ "clap", "elgato-streamdeck", "hidapi", + "image", + "json", "paho-mqtt", "rust-ini", "tokio", @@ -520,6 +654,19 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -553,6 +700,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quote" version = "1.0.28" @@ -562,6 +718,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "rust-ini" version = "0.18.0" @@ -586,6 +762,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" version = "0.4.8" @@ -595,6 +777,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + [[package]] name = "strsim" version = "0.10.0" @@ -652,6 +840,17 @@ dependencies = [ "syn 2.0.18", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "tokio" version = "1.28.2" @@ -700,6 +899,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "winapi" version = "0.3.9" @@ -796,3 +1001,12 @@ name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index 1763faf..13d5bd9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ edition = "2021" clap = { version = "4.0.32", features = ["derive"] } elgato-streamdeck = { version = "0.5.0", features = ["async"] } hidapi = { version = "2.6.3", default-features = false, features = ["linux-static-hidraw"] } +image = "0.24.7" +json = "0.12.4" paho-mqtt = "0.12.5" rust-ini = "0.18.0" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } diff --git a/src/main.rs b/src/main.rs index aefcdca..00acdb1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use clap::Parser; use elgato_streamdeck::{list_devices, new_hidapi, AsyncStreamDeck, StreamDeckInput}; +use image::{DynamicImage, ImageBuffer, RgbImage}; use ini::Ini; use paho_mqtt as mqtt; use std::process; @@ -19,6 +20,89 @@ struct Args { verbose: bool, } +fn str_to_colour(colour: &str) -> Result<[u8; 3], &'static str> { + match colour { + "black" => Ok([0, 0, 0]), + "blue" => Ok([0, 0, 255]), + "cyan" => Ok([0, 255, 255]), + "green" => Ok([0, 255, 0]), + "magenta" => Ok([255, 0, 255]), + "red" => Ok([255, 0, 0]), + "white" => Ok([255, 255, 255]), + "yellow" => Ok([255, 255, 0]), + _ => Err("Unknown colour"), + } +} + +async fn send_command_response(cli: &mqtt::AsyncClient, topic: &str, resp: &str) { + let msg = mqtt::MessageBuilder::new() + .topic(topic) + .payload(resp) + .qos(1) + .finalize(); + + if let Err(e) = cli.publish(msg).wait() { + println!("Error sending response to command: {}", e); + } +} + +async fn handle_commands( + cli: mqtt::AsyncClient, + strm: mqtt::AsyncReceiver>, + deck: AsyncStreamDeck, +) { + while let Ok(msg_opt) = strm.recv().await { + if let Some(msg) = msg_opt { + let data = std::str::from_utf8(msg.payload()).unwrap(); + let command = json::parse(data).unwrap(); + let resp_topic = msg.topic().replace("/cmnd", "/result"); + + if command["action"].is_null() { + send_command_response(&cli, &resp_topic, "no action supplied").await; + continue; + } + + match command["action"].as_str() { + Some("set-colour") => { + if command["button"].is_null() { + send_command_response(&cli, &resp_topic, "no button supplied").await; + continue; + } + if command["colour"].is_null() { + send_command_response(&cli, &resp_topic, "no colour supplied").await; + continue; + } + + let colour = match str_to_colour(command["colour"].as_str().unwrap()) { + Ok(colour) => colour, + Err(e) => { + send_command_response(&cli, &resp_topic, e).await; + continue; + } + }; + + let mut img: RgbImage = ImageBuffer::new(72, 72); + + for (_, _, pixel) in img.enumerate_pixels_mut() { + *pixel = image::Rgb(colour); + } + + deck.set_button_image( + command["button"].as_u8().unwrap(), + DynamicImage::ImageRgb8(img), + ) + .await + .unwrap(); + send_command_response(&cli, &resp_topic, "OK").await; + } + _ => send_command_response(&cli, &resp_topic, "Unknown action").await, + } + } else { + println!("Lost connection. Attempting reconnect..."); + } + } +} + #[tokio::main] async fn main() { let args = Args::parse(); @@ -57,11 +141,15 @@ async fn main() { println!("Connecting to the MQTT server"); } - let cli = mqtt::AsyncClient::new(host).unwrap_or_else(|err| { + let mut cli = mqtt::AsyncClient::new(host).unwrap_or_else(|err| { println!("Error creating the MQTT client: {}", err); process::exit(1); }); + // Get an MQTT stream, allow for 10 messages + let strm = cli.get_stream(10); + let cmd_topic = format!("{}/cmnd", &conf["MQTT"]["prefix"]); + let ssl_opts = mqtt::SslOptionsBuilder::new().finalize(); let lwt_topic = format!("{}/LWT", &conf["MQTT"]["prefix"]); @@ -97,9 +185,15 @@ async fn main() { println!("Error sending LWT present message: {:?}", e); } + // Subscribe to our command topic + cli.subscribe(cmd_topic, 0); + let mut keystates = vec![false; kind.key_count() as usize]; let quit = false; + // Kick off a handler for our incoming MQTT commands + tokio::spawn(handle_commands(cli.clone(), strm, deck.clone())); + while !quit { let input = deck.read_input(5.0).await.unwrap();