]> the.earth.li Git - mqttdeck.git/commitdiff
Add initial support for setting button colours
authorJonathan McDowell <noodles@earth.li>
Sat, 26 Apr 2025 17:40:15 +0000 (18:40 +0100)
committerJonathan McDowell <noodles@earth.li>
Sat, 26 Apr 2025 17:40:15 +0000 (18:40 +0100)
Initial rough 'n ready code to accept JSON commands over MQTT to set the
colour of the buttons on the StreamDeck.

Cargo.lock
Cargo.toml
src/main.rs

index 4422e7b9b9b2c5739ecb6a36d2fd784399dd6d4c..6882f12f5e931456a4e1fe88f9b89fc0d3167033 100644 (file)
@@ -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",
+]
index 1763faf1e2bee056329a4e40ee35605e32c2a560..13d5bd90b2af255bcec3b1ced782a98dcfb7aef8 100644 (file)
@@ -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"] }
index aefcdca49bced0bc47c5d3ff52da41c4bf9377b8..00acdb19c9ea2c746cc861af1065bfc3e84ef15d 100644 (file)
@@ -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<Option<mqtt::Message>>,
+    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();