From 033f6ef13e5bcef731c254d6805515dda8a2f039 Mon Sep 17 00:00:00 2001 From: Zak Kristjanson Date: Mon, 22 Dec 2025 19:20:20 -0800 Subject: [PATCH 1/5] Refactor stuff - make --all and --path actually work - add some tests - some linting --- src/main.rs | 138 ++++++++++++++++++++++++++++++++++++++++++++++---- src/qlight.rs | 8 ++- 2 files changed, 132 insertions(+), 14 deletions(-) diff --git a/src/main.rs b/src/main.rs index 922928a..89e3675 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use std::io::Write; +use std::{ffi::CString, io::Write}; use clap::{ArgGroup, Parser}; use hidapi::HidApi; @@ -29,9 +29,9 @@ enum Action { .args(&["all", "path"]) ))] struct SetArgs { - /// Apply the commands to a specific lights. Use `list` to get the paths. + /// Apply the commands to a specific light. Use `list` to get the paths. #[clap(long, value_name = "PATH")] - path: Option, + path: Vec, /// Apply the commands to all detected lights. #[clap(long)] @@ -52,7 +52,7 @@ struct SetArgs { fn parse_command(s: &str) -> Result { let Some((color, mode_name)) = s.split_once(':') else { - bail!("Expected format of [red,yellow,green,blue,white]:[on,off,blink] got {}", s); + bail!("Expected format of [red,yellow,green,blue,white]:[on,off,blink] got {s}"); }; let color = Color::try_from(color)?; @@ -75,8 +75,31 @@ fn list(_args: Args) -> Result<()> { } fn set(args: SetArgs) -> Result<()> { + + // Parse out --all and --path entries into a list of Lights + let hidapi = HidApi::new()?; + let mut lights = vec![]; + if args.all { + for device in Light::get_devices(&hidapi) { + let light = Light::new(device.open_device(&hidapi)?); + lights.push(light); + } + } else { + for path in &args.path { + let path_cstring = CString::new(path.as_str())?; + let device = hidapi.open_path(&path_cstring)?; + let light = Light::new(device); + lights.push(light); + } + } + + if lights.is_empty() { + bail!("No lights found"); + } + + // Calculate LightCommandSet let mut lightset = if args.reset { - LightCommandSet::default_off() + LightCommandSet::all_off() } else { LightCommandSet::default() }; @@ -84,10 +107,9 @@ fn set(args: SetArgs) -> Result<()> { for (color, lightmode) in &args.commands { lightset.set(*color, *lightmode); } - - let hidapi = HidApi::new()?; - for light in Light::get_devices(&hidapi) { - let light = Light::new(light.open_device(&hidapi)?); + + // Send command to the lights + for light in &lights { light.update(&lightset)?; } @@ -101,3 +123,101 @@ fn main() -> Result<()> { Action::List => list(cli), } } + +#[cfg(test)] +mod tests { + use super::*; + use qlight::{Color, LightMode}; + + #[test] + fn parse_command_ok_basic() { + let cmd = parse_command("red:on").expect("should parse"); + assert_eq!(cmd, (Color::Red, LightMode::On)); + } + + #[test] + fn parse_command_ok_case_insensitive() { + let cmd = parse_command("GrEeN:BlInK").expect("should parse case-insensitively"); + assert_eq!(cmd, (Color::Green, LightMode::Blink)); + } + + #[test] + fn parse_command_err_missing_colon() { + let err = parse_command("red").unwrap_err(); + let msg = format!("{err}"); + assert!(msg.contains("Expected format of"), "got: {msg}"); + } + + #[test] + fn parse_command_err_bad_color() { + let err = parse_command("purple:on").unwrap_err(); + let msg = format!("{err}"); + assert!(msg.contains("Expected one of [red, yellow, green, blue, white]"), "got: {msg}"); + } + + #[test] + fn parse_command_err_bad_mode() { + let err = parse_command("red:florb").unwrap_err(); + let msg = format!("{err}"); + assert!(msg.contains("Expected one of [on, off, blink]"), "got: {msg}"); + } + + #[test] + fn set_args_mutually_exclusive_all_and_path() { + // specifying both --all and --path should be rejected by clap as an argument conflict + let err = Args::try_parse_from([ + "qlight", + "set", + "--all", + "--path", + "/dev/fake1", + "red:on", + ]) + .unwrap_err(); + + assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict); + } + + #[test] + fn set_args_multiple_paths_allowed() { + // --path may be specified multiple times to target multiple devices + let args = Args::try_parse_from([ + "qlight", + "set", + "--path", + "/dev/fake1", + "--path", + "/dev/fake2", + "red:on", + ]) + .expect("should parse multiple --path entries"); + + match args.action { + Action::Set(set) => { + assert_eq!(set.path, vec!["/dev/fake1".to_string(), "/dev/fake2".to_string()]); + assert!(!set.all, "--all should not be set"); + } + _ => panic!("expected set subcommand"), + } + } + + #[test] + fn set_args_all_only_parses() { + // --all alone should parse and set `all` to true with no paths + let args = Args::try_parse_from([ + "qlight", + "set", + "--all", + "red:on", + ]) + .expect("should parse --all"); + + match args.action { + Action::Set(set) => { + assert!(set.all, "--all should be set"); + assert!(set.path.is_empty(), "no paths should be present"); + } + _ => panic!("expected set subcommand"), + } + } +} diff --git a/src/qlight.rs b/src/qlight.rs index 8a311ab..873632f 100644 --- a/src/qlight.rs +++ b/src/qlight.rs @@ -39,8 +39,7 @@ impl TryFrom<&str> for Color { "white" => Color::White, other => { return Err(ParseError(format!( - "Expected one of [red, yellow, green, blue, white], got {}", - other + "Expected one of [red, yellow, green, blue, white], got {other}" ))) } }; @@ -67,8 +66,7 @@ impl TryFrom<&str> for LightMode { "blink" => LightMode::Blink, other => { return Err(ParseError(format!( - "Expected one of [on, off, blink] in command, got {}", - other + "Expected one of [on, off, blink] in command, got {other}" ))) } }; @@ -112,7 +110,7 @@ pub struct LightCommandSet { } impl LightCommandSet { - pub fn default_off() -> Self { + pub fn all_off() -> Self { Self { red: LightMode::Off, yellow: LightMode::Off, From f899d3a582ba3ea2b42b33da7998ed7790be8745 Mon Sep 17 00:00:00 2001 From: Zak Kristjanson Date: Mon, 22 Dec 2025 19:30:22 -0800 Subject: [PATCH 2/5] Refactor into workspace --- Cargo.lock | 10 +++++++++- Cargo.toml | 14 ++++++-------- crates/qlight-cli/Cargo.toml | 14 ++++++++++++++ {src => crates/qlight-cli/src}/main.rs | 6 ++---- crates/qlight-core/Cargo.toml | 7 +++++++ src/qlight.rs => crates/qlight-core/src/lib.rs | 0 6 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 crates/qlight-cli/Cargo.toml rename {src => crates/qlight-cli/src}/main.rs (97%) create mode 100644 crates/qlight-core/Cargo.toml rename src/qlight.rs => crates/qlight-core/src/lib.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index 47cd039..dba853c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,12 +169,20 @@ dependencies = [ ] [[package]] -name = "qlight" +name = "qlight-cli" version = "0.0.1" dependencies = [ "anyhow", "clap", "hidapi", + "qlight-core", +] + +[[package]] +name = "qlight-core" +version = "0.0.1" +dependencies = [ + "hidapi", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 90eec2d..84cbaa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,7 @@ -[package] -name = "qlight" -version = "0.0.1" -edition = "2021" +[workspace] +members = ["crates/qlight-core", "crates/qlight-cli"] +resolver = "2" -[dependencies] -hidapi = "2.6.4" -clap = { version = "4.5.53", features = ["derive"] } -anyhow = "1.0.100" +[workspace.package] +version = "0.0.1" +edition = "2024" diff --git a/crates/qlight-cli/Cargo.toml b/crates/qlight-cli/Cargo.toml new file mode 100644 index 0000000..176d0e0 --- /dev/null +++ b/crates/qlight-cli/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "qlight-cli" +version.workspace = true +edition.workspace = true + +[[bin]] +name = "qlight" +path = "src/main.rs" + +[dependencies] +qlight-core = { path = "../qlight-core" } +hidapi = "2.6.4" +clap = { version = "4.5.53", features = ["derive"] } +anyhow = "1.0.100" diff --git a/src/main.rs b/crates/qlight-cli/src/main.rs similarity index 97% rename from src/main.rs rename to crates/qlight-cli/src/main.rs index 89e3675..48f5c24 100644 --- a/src/main.rs +++ b/crates/qlight-cli/src/main.rs @@ -2,12 +2,10 @@ use std::{ffi::CString, io::Write}; use clap::{ArgGroup, Parser}; use hidapi::HidApi; -use qlight::{Color, Light, LightCommand, LightMode, LightCommandSet}; +use qlight_core::{Color, Light, LightCommand, LightMode, LightCommandSet}; use anyhow::{bail, Result}; -mod qlight; - #[derive(Parser, Debug)] struct Args { #[command(subcommand)] @@ -127,7 +125,7 @@ fn main() -> Result<()> { #[cfg(test)] mod tests { use super::*; - use qlight::{Color, LightMode}; + use qlight_core::{Color, LightMode}; #[test] fn parse_command_ok_basic() { diff --git a/crates/qlight-core/Cargo.toml b/crates/qlight-core/Cargo.toml new file mode 100644 index 0000000..3329135 --- /dev/null +++ b/crates/qlight-core/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "qlight-core" +version.workspace = true +edition.workspace = true + +[dependencies] +hidapi = "2.6.4" diff --git a/src/qlight.rs b/crates/qlight-core/src/lib.rs similarity index 100% rename from src/qlight.rs rename to crates/qlight-core/src/lib.rs From f65d7678025adf9bd96bc02994f9604b72df4aa8 Mon Sep 17 00:00:00 2001 From: Zak Kristjanson Date: Tue, 23 Dec 2025 18:49:10 -0800 Subject: [PATCH 3/5] Add qlight-osc v1 --- Cargo.lock | 177 ++++++++++++++++++++++++++++++++++ Cargo.toml | 2 +- crates/qlight-core/src/lib.rs | 1 + crates/qlight-osc/Cargo.toml | 13 +++ crates/qlight-osc/src/main.rs | 128 ++++++++++++++++++++++++ 5 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 crates/qlight-osc/Cargo.toml create mode 100644 crates/qlight-osc/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index dba853c..547bc94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,12 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "cc" version = "1.0.77" @@ -141,18 +147,79 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchit" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea5f97102eb9e54ab99fb70bb175589073f554bdadfb74d9bd656482ea73e2a" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "pkg-config" version = "0.3.26" @@ -185,6 +252,19 @@ dependencies = [ "hidapi", ] +[[package]] +name = "qlight-osc" +version = "0.0.1" +dependencies = [ + "anyhow", + "hidapi", + "matchit", + "qlight-core", + "rosc", + "tracing", + "tracing-subscriber", +] + [[package]] name = "quote" version = "1.0.42" @@ -194,6 +274,31 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rosc" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e63d9e6b0d090be1485cf159b1e04c3973d2d3e1614963544ea2ff47a4a981" +dependencies = [ + "byteorder", + "nom", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + [[package]] name = "strsim" version = "0.11.1" @@ -211,6 +316,72 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + [[package]] name = "unicode-ident" version = "1.0.5" @@ -223,6 +394,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 84cbaa6..6063baa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/qlight-core", "crates/qlight-cli"] +members = ["crates/qlight-core", "crates/qlight-cli", "crates/qlight-osc"] resolver = "2" [workspace.package] diff --git a/crates/qlight-core/src/lib.rs b/crates/qlight-core/src/lib.rs index 873632f..5a80dbf 100644 --- a/crates/qlight-core/src/lib.rs +++ b/crates/qlight-core/src/lib.rs @@ -144,6 +144,7 @@ impl LightCommandSet { } } +#[derive(Debug)] pub struct Light { device: HidDevice, } diff --git a/crates/qlight-osc/Cargo.toml b/crates/qlight-osc/Cargo.toml new file mode 100644 index 0000000..4259613 --- /dev/null +++ b/crates/qlight-osc/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "qlight-osc" +version.workspace = true +edition.workspace = true + +[dependencies] +rosc = "~0.10" +qlight-core = { path = "../qlight-core" } +anyhow = "1.0.100" +hidapi = "2.6.4" +matchit = "0.9.0" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" diff --git a/crates/qlight-osc/src/main.rs b/crates/qlight-osc/src/main.rs new file mode 100644 index 0000000..e1863a7 --- /dev/null +++ b/crates/qlight-osc/src/main.rs @@ -0,0 +1,128 @@ +use anyhow::{Context, Result}; +use hidapi::HidApi; +use matchit::{Match, Router}; +use qlight_core::{Color, Light, LightCommandSet, LightMode}; +use rosc::OscPacket; +use std::ffi::CString; +use std::net::{SocketAddrV4, UdpSocket}; +use std::str::FromStr; +use tracing::{info, trace, warn}; + +#[derive(Debug)] +struct QlightOsc { + light: Light, + router: Router, +} + +// /lights/{name}/color 0 + +#[derive(Debug, Eq, PartialEq)] +enum Command { + Color, +} + +impl QlightOsc { + fn new(light: Light) -> Self { + let mut router = Router::new(); + router + .insert("/lights/{id}/{color}", Command::Color) + .expect("Failed to compile route"); + + QlightOsc { light, router } + } + + fn handle_packet(&mut self, packet: OscPacket) -> Result<()> { + match packet { + OscPacket::Message(msg) => { + let (_id, color) = match self.router.at(&msg.addr) { + Ok( + m @ Match { + value: Command::Color, + .. + }, + ) => ( + m.params + .get("id") + .expect("Color command should always have an id"), + m.params + .get("color") + .expect("Color command should always have a color"), + ), + _ => { + warn!("Ignoring message for unknown OSC path: {}", &msg.addr); + return Ok(()); + }, + }; + + let color = match color.to_lowercase().as_str() { + "red" => Color::Red, + "yellow" => Color::Yellow, + "green" => Color::Green, + "blue" => Color::Blue, + "white" => Color::White, + _ => { + warn!("Ignoring message {} with unknown color {}", &msg.addr, color); + return Ok(()); + } + }; + + let lightmode = match msg.args.as_slice() { + [rosc::OscType::Int(0)] => LightMode::Off, + [rosc::OscType::Int(1)] => LightMode::On, + [rosc::OscType::Int(2)] => LightMode::Blink, + _ => { + warn!("Ignoring message {} with unknown arguments {:?}", &msg.addr, msg.args); + return Ok(()); + } + }; + + let mut lcs: LightCommandSet = LightCommandSet::default(); + info!("Setting light {:?} to {:?}", color, lightmode); + lcs.set(color, lightmode); + + self.light.update(&lcs)?; + Ok(()) + } + OscPacket::Bundle(_bundle) => { + warn!("We don't support OSC Bundles... yet. Ignoring packet."); + Ok(()) + } + } + } +} + +fn main() -> Result<()> { + tracing_subscriber::fmt::init(); + + let addr = SocketAddrV4::from_str("127.0.0.1:8000") + .with_context(|| "Failed to parse IP".to_string())?; + let sock = UdpSocket::bind(addr).with_context(|| "Failed to bind to ip".to_string())?; + info!("Listening to {addr}"); + + let mut buf = [0u8; rosc::decoder::MTU]; + + let hidapi = HidApi::new().unwrap(); + let device = hidapi + .open_path(&CString::from_str("DevSrvsID:4301069978")?) + .with_context(|| "Failed to open HID Device".to_string())?; + + let mut qlightosc = QlightOsc::new(Light::new(device)); + + loop { + match sock.recv_from(&mut buf) { + Ok((size, addr)) => { + trace!("Received packet with size {size} from: {addr}"); + let (_, packet) = rosc::decoder::decode_udp(&buf[..size]) + .with_context(|| "Failed to read OSC packet".to_string())?; + + qlightosc.handle_packet(packet)?; + } + Err(e) => { + trace!("Error receiving from socket: {e}"); + break; + } + } + } + + Ok(()) +} From b4d43fd71c1c383d3f9400c2ee3382ec95c92778 Mon Sep 17 00:00:00 2001 From: Zak Kristjanson Date: Tue, 23 Dec 2025 21:35:47 -0800 Subject: [PATCH 4/5] Add basic reset functionality --- crates/qlight-osc/src/main.rs | 115 +++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 45 deletions(-) diff --git a/crates/qlight-osc/src/main.rs b/crates/qlight-osc/src/main.rs index e1863a7..28d09b7 100644 --- a/crates/qlight-osc/src/main.rs +++ b/crates/qlight-osc/src/main.rs @@ -14,11 +14,10 @@ struct QlightOsc { router: Router, } -// /lights/{name}/color 0 - #[derive(Debug, Eq, PartialEq)] enum Command { Color, + Reset, } impl QlightOsc { @@ -28,60 +27,49 @@ impl QlightOsc { .insert("/lights/{id}/{color}", Command::Color) .expect("Failed to compile route"); + router + .insert("/reset/{id}", Command::Reset) + .expect("Failed to compile route"); + QlightOsc { light, router } } fn handle_packet(&mut self, packet: OscPacket) -> Result<()> { match packet { OscPacket::Message(msg) => { - let (_id, color) = match self.router.at(&msg.addr) { - Ok( - m @ Match { - value: Command::Color, - .. - }, - ) => ( - m.params + match self.router.at(&msg.addr) { + Ok(m @ Match { + value: Command::Color, + .. + }) => { + let id = m.params .get("id") - .expect("Color command should always have an id"), - m.params + .expect("Color command should always have an id"); + let color_str = m.params .get("color") - .expect("Color command should always have a color"), - ), - _ => { - warn!("Ignoring message for unknown OSC path: {}", &msg.addr); - return Ok(()); - }, - }; - - let color = match color.to_lowercase().as_str() { - "red" => Color::Red, - "yellow" => Color::Yellow, - "green" => Color::Green, - "blue" => Color::Blue, - "white" => Color::White, - _ => { - warn!("Ignoring message {} with unknown color {}", &msg.addr, color); - return Ok(()); + .expect("Color command should always have a color"); + if let Some(lcs) = self.handle_color_command(&msg, id, color_str) { + self.light.update(&lcs)?; + } + Ok(()) + } + Ok(m @ Match { + value: Command::Reset, + .. + }) => { + let id = m.params + .get("id") + .expect("Reset command should always have an id"); + if let Some(lcs) = self.handle_reset_command(&msg, id) { + self.light.update(&lcs)?; + } + Ok(()) } - }; - - let lightmode = match msg.args.as_slice() { - [rosc::OscType::Int(0)] => LightMode::Off, - [rosc::OscType::Int(1)] => LightMode::On, - [rosc::OscType::Int(2)] => LightMode::Blink, _ => { - warn!("Ignoring message {} with unknown arguments {:?}", &msg.addr, msg.args); - return Ok(()); + warn!("Ignoring message for unknown OSC path: {}", &msg.addr); + Ok(()) } - }; - - let mut lcs: LightCommandSet = LightCommandSet::default(); - info!("Setting light {:?} to {:?}", color, lightmode); - lcs.set(color, lightmode); - - self.light.update(&lcs)?; - Ok(()) + } } OscPacket::Bundle(_bundle) => { warn!("We don't support OSC Bundles... yet. Ignoring packet."); @@ -89,6 +77,43 @@ impl QlightOsc { } } } + + fn handle_color_command(&mut self, msg: &rosc::OscMessage, _id: &str, color_str: &str) -> Option { + + let color = match color_str.to_lowercase().as_str() { + "red" => Color::Red, + "yellow" => Color::Yellow, + "green" => Color::Green, + "blue" => Color::Blue, + "white" => Color::White, + _ => { + warn!("Ignoring message {} with unknown color {}", &msg.addr, color_str); + return None; + } + }; + + let lightmode = match msg.args.as_slice() { + [rosc::OscType::Int(0)] => LightMode::Off, + [rosc::OscType::Int(1)] => LightMode::On, + [rosc::OscType::Int(2)] => LightMode::Blink, + _ => { + warn!("Ignoring message {} with unknown arguments {:?}", &msg.addr, msg.args); + return None; + } + }; + + let mut lcs: LightCommandSet = LightCommandSet::default(); + info!("Setting light {:?} to {:?}", color, lightmode); + lcs.set(color, lightmode); + + Some(lcs) + } + + fn handle_reset_command(&mut self, _msg: &rosc::OscMessage, _id: &str) -> Option { + let lcs: LightCommandSet = LightCommandSet::all_off(); + info!("Resetting light"); + Some(lcs) + } } fn main() -> Result<()> { From d4b93ca122cd94032aeb5d82b3a91609766e91fa Mon Sep 17 00:00:00 2001 From: Zak Kristjanson Date: Wed, 24 Dec 2025 20:32:43 -0800 Subject: [PATCH 5/5] Basic config file --- Cargo.lock | 420 +++++++++++++++++++++++++- crates/qlight-osc/Cargo.toml | 3 + crates/qlight-osc/config_example.toml | 4 + crates/qlight-osc/src/main.rs | 113 ++++++- 4 files changed, 525 insertions(+), 15 deletions(-) create mode 100644 crates/qlight-osc/config_example.toml diff --git a/Cargo.lock b/Cargo.lock index 547bc94..f8f5b1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "anstream" version = "0.6.21" @@ -58,6 +69,38 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -122,6 +165,102 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "config" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23738e11972c7643e4ec947840fc463b6a571afcd3e735bdfce7d03c7a784aca" +dependencies = [ + "async-trait", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml 0.5.11", + "yaml-rust", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dlv-list" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" @@ -141,12 +280,39 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -155,9 +321,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "log" @@ -214,6 +386,65 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "ordered-multimap" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" +dependencies = [ + "dlv-list", + "hashbrown 0.12.3", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "pest" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -257,10 +488,13 @@ name = "qlight-osc" version = "0.0.1" dependencies = [ "anyhow", + "config", "hidapi", "matchit", "qlight-core", "rosc", + "serde", + "toml 0.8.23", "tracing", "tracing-subscriber", ] @@ -274,6 +508,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ron" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" +dependencies = [ + "base64", + "bitflags", + "serde", +] + [[package]] name = "rosc" version = "0.10.1" @@ -284,6 +529,79 @@ dependencies = [ "nom", ] +[[package]] +name = "rust-ini" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -325,6 +643,56 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tracing" version = "0.1.44" @@ -382,6 +750,18 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unicode-ident" version = "1.0.5" @@ -400,6 +780,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "windows-link" version = "0.2.1" @@ -480,3 +872,27 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zmij" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e404bcd8afdaf006e529269d3e85a743f9480c3cef60034d77860d02964f3ba" diff --git a/crates/qlight-osc/Cargo.toml b/crates/qlight-osc/Cargo.toml index 4259613..b971fbc 100644 --- a/crates/qlight-osc/Cargo.toml +++ b/crates/qlight-osc/Cargo.toml @@ -11,3 +11,6 @@ hidapi = "2.6.4" matchit = "0.9.0" tracing = "0.1.44" tracing-subscriber = "0.3.22" +config = "0.13" +serde = { version = "1.0", features = ["derive"] } +toml = "0.8" diff --git a/crates/qlight-osc/config_example.toml b/crates/qlight-osc/config_example.toml new file mode 100644 index 0000000..ecaf5ed --- /dev/null +++ b/crates/qlight-osc/config_example.toml @@ -0,0 +1,4 @@ +listen="0.0.0.0:8000" + +[bindings.device1] +path="DevSrvsID:4301142453" diff --git a/crates/qlight-osc/src/main.rs b/crates/qlight-osc/src/main.rs index 28d09b7..cd39339 100644 --- a/crates/qlight-osc/src/main.rs +++ b/crates/qlight-osc/src/main.rs @@ -1,16 +1,81 @@ use anyhow::{Context, Result}; +use config::Config; use hidapi::HidApi; use matchit::{Match, Router}; use qlight_core::{Color, Light, LightCommandSet, LightMode}; use rosc::OscPacket; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::ffi::CString; use std::net::{SocketAddrV4, UdpSocket}; use std::str::FromStr; use tracing::{info, trace, warn}; +const DEFAULT_CONFIG_PATH: &str = "config.toml"; +const CONFIG_ENV_VAR: &str = "QLIGHT_OSC_CONFIG"; + #[derive(Debug)] +struct LightThing { + binding: DeviceBinding, + light: Option +} + +impl LightThing { + fn new(binding: DeviceBinding) -> Self { + Self { + binding, + light: Default::default() + } + } + + fn get_or_init_light(&mut self, hidapi: &HidApi) -> Result<&Light> { + if self.light.is_none() { + let path = &self.binding.path; + + let device = hidapi + .open_path(&CString::from_str(path)?) + .with_context(|| format!("Failed to open HID device at path: {path}"))?; + + self.light = Some(Light::new(device)); + } + + // At this point, we just put a light in if it doesn't exit. + Ok(self.light.as_ref().unwrap()) + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct DeviceBinding { + path: String, +} + +#[derive(Debug, Deserialize, Serialize)] +struct AppConfig { + listen: String, + bindings: Option> +} + +impl AppConfig { + fn load_default() -> Result { + let config_path = std::env::var(CONFIG_ENV_VAR) + .unwrap_or_else(|_| DEFAULT_CONFIG_PATH.to_string()); + + let config = Config::builder() + .add_source(config::File::with_name(&config_path).required(true)) + .build() + .with_context(|| format!("Failed to load config from {config_path}"))?; + + config + .try_deserialize::() + .with_context(|| "Failed to deserialize configuration") + } +} + +// #[derive(Debug)] struct QlightOsc { - light: Light, + // light: Light, + hidapi: HidApi, + light: LightThing, router: Router, } @@ -21,7 +86,7 @@ enum Command { } impl QlightOsc { - fn new(light: Light) -> Self { + fn new(hidapi: HidApi, binding: DeviceBinding) -> Self { let mut router = Router::new(); router .insert("/lights/{id}/{color}", Command::Color) @@ -31,7 +96,7 @@ impl QlightOsc { .insert("/reset/{id}", Command::Reset) .expect("Failed to compile route"); - QlightOsc { light, router } + QlightOsc { light: LightThing::new(binding), router, hidapi } } fn handle_packet(&mut self, packet: OscPacket) -> Result<()> { @@ -48,9 +113,15 @@ impl QlightOsc { let color_str = m.params .get("color") .expect("Color command should always have a color"); + + if let Some(lcs) = self.handle_color_command(&msg, id, color_str) { - self.light.update(&lcs)?; + match self.light.get_or_init_light(&self.hidapi) { + Ok(light) => { light.update(&lcs)?; }, + Err(e) => warn!("Failed to update {:?}: {}", self.light, e) + } } + Ok(()) } Ok(m @ Match { @@ -61,7 +132,10 @@ impl QlightOsc { .get("id") .expect("Reset command should always have an id"); if let Some(lcs) = self.handle_reset_command(&msg, id) { - self.light.update(&lcs)?; + match self.light.get_or_init_light(&self.hidapi) { + Ok(light) => { light.update(&lcs)?; }, + Err(e) => warn!("Failed to update {:?}: {}", self.light, e) + } } Ok(()) } @@ -119,19 +193,32 @@ impl QlightOsc { fn main() -> Result<()> { tracing_subscriber::fmt::init(); - let addr = SocketAddrV4::from_str("127.0.0.1:8000") - .with_context(|| "Failed to parse IP".to_string())?; - let sock = UdpSocket::bind(addr).with_context(|| "Failed to bind to ip".to_string())?; + // Load configuration from file (default: config.toml, or QLIGHT_OSC_CONFIG env var) + let config = AppConfig::load_default()?; + + let addr = SocketAddrV4::from_str(&config.listen) + .with_context(|| format!("Failed to parse listen address: {}", config.listen))?; + + let sock = UdpSocket::bind(addr).with_context(|| format!("Failed to bind to {addr}"))?; + info!("Listening to {addr}"); let mut buf = [0u8; rosc::decoder::MTU]; - let hidapi = HidApi::new().unwrap(); - let device = hidapi - .open_path(&CString::from_str("DevSrvsID:4301069978")?) - .with_context(|| "Failed to open HID Device".to_string())?; + let hidapi = HidApi::new()?; + + // Get the first device binding from config, or use the first detected device + let device_path = if let Some(bindings) = &config.bindings { + bindings + .values() + .next() + .with_context(|| "No device bindings found in config")? + } else { + return Err(anyhow::anyhow!("No device bindings configured")); + }; + - let mut qlightosc = QlightOsc::new(Light::new(device)); + let mut qlightosc = QlightOsc::new(hidapi, device_path.clone()); loop { match sock.recv_from(&mut buf) {