diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e38bac..3dbfc9c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,7 @@ repos: - id: test name: cargo test - entry: cargo test --all + entry: cargo test --all --features hardware-tests -- --include-ignored language: system pass_filenames: false stages: [pre-push] diff --git a/Cargo.lock b/Cargo.lock index b2e0887..2d48b33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,10 +2,17 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dsp_thomann" version = "0.1.0" dependencies = [ + "dotenvy", "strum", "strum_macros", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index ecb8674..f09719d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,9 @@ name = "dsp_thomann" version = "0.1.0" edition = "2024" +[dev-dependencies] +dotenvy = "0.15" + [dependencies] thiserror = "2.0.19" strum = "0.28.0" @@ -10,4 +13,7 @@ strum_macros = "0.28.0" [lib] name = "dsp_thomann" -path = "src/lib.rs" \ No newline at end of file +path = "src/lib.rs" + +[features] +hardware-tests = [] \ No newline at end of file diff --git a/src/dsp408/client/dsp408.rs b/src/dsp408/client/dsp408.rs index e50a4f6..b4ff180 100644 --- a/src/dsp408/client/dsp408.rs +++ b/src/dsp408/client/dsp408.rs @@ -1427,3 +1427,256 @@ impl fmt::Debug for DSP408 { .finish() } } + +#[cfg(feature = "hardware-tests")] +#[test] +fn full_command_sweep_against_real_hardware() { + use std::sync::Once; + use std::{env, str::FromStr}; + + static INIT: Once = Once::new(); + + fn init_test_env() { + INIT.call_once(|| { + dotenvy::dotenv().ok(); + }); + } + + fn dsp_host() -> IpAddr { + IpAddr::from_str(&env::var("DSP_HOST").unwrap_or_else(|_| "192.168.49.30".into())) + .expect("DSP_HOST must be a valid IP") + } + + fn dsp_port() -> u16 { + env::var("DSP_PORT") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(9761) + } + + fn dsp_device_id() -> u8 { + env::var("DSP_DEVICE_ID") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(1) + } + + fn destructive_allowed() -> bool { + matches!( + env::var("DSP_ALLOW_DESTRUCTIVE") + .unwrap_or_default() + .as_str(), + "1" | "true" | "yes" + ) + } + + init_test_env(); + + let host = dsp_host(); + let port = dsp_port(); + let device_id = dsp_device_id(); + + let mut dsp = DSP408::new(host, port, device_id).expect("invalid connection params"); + + // ---------- Connect ---------- + dsp.connect().expect("connect() should succeed"); + assert!( + dsp.connected(), + "device should report connected after connect()" + ); + + { + let state = dsp + .state() + .expect("state should be populated after connect"); + assert!(!state.name.is_empty(), "device name should be non-empty"); + assert!( + !state.presets.names.is_empty(), + "preset name list should be populated" + ); + assert!( + !state.current_config.input_states.is_empty(), + "input map should be populated" + ); + assert!( + !state.current_config.output_states.is_empty(), + "output map should be populated" + ); + } + + // ---------- Delay unit ---------- + let ok = dsp + .set_delay_unit(types::DelayUnit::Millisecond) + .expect("set_delay_unit"); + assert!(ok); + assert_eq!( + dsp.state().unwrap().current_config.delay_unit, + types::DelayUnit::Millisecond + ); + + // ---------- Input channel exercise ---------- + let input = types::InputChannel::InA; + let in_ch = types::Channel::Input(input); + + let ok = dsp.set_mute(in_ch, true).expect("set_mute"); + assert!(ok); + assert!(dsp.state().unwrap().current_config.input_states[&input].mute); + + let ok = dsp.set_mute(in_ch, false).expect("unmute"); + assert!(ok); + assert!(!dsp.state().unwrap().current_config.input_states[&input].mute); + + let ok = dsp.set_channel_gain(in_ch, -3.0).expect("set_channel_gain"); + assert!(ok); + assert!((dsp.state().unwrap().current_config.input_states[&input].gain - (-3.0)).abs() < 0.01); + + let ok = dsp + .set_channel_inverse_gain(in_ch, true) + .expect("set_channel_inverse_gain"); + assert!(ok); + assert!(dsp.state().unwrap().current_config.input_states[&input].phase_inverted); + dsp.set_channel_inverse_gain(in_ch, false) + .expect("revert phase"); + + let ok = dsp + .set_channel_delay(in_ch, 1.5, types::DelayUnit::Millisecond) + .expect("set_channel_delay"); + assert!(ok); + + let ok = dsp + .set_channel_name(in_ch, "Test InA") + .expect("set_channel_name"); + assert!(ok); + assert_eq!( + dsp.state().unwrap().current_config.input_states[&input].name, + "Test InA" + ); + + let ok = dsp + .set_low_pass(in_ch, 120.0, types::CrossoverFilter::Bw24) + .expect("set_low_pass"); + assert!(ok); + + let ok = dsp + .set_high_pass(in_ch, 80.0, types::CrossoverFilter::Bw24) + .expect("set_high_pass"); + assert!(ok); + + let ok = dsp + .set_peq_band( + in_ch, + types::PEQBand::B1, + 2.0, + 1000.0, + 1.0, + types::PEQFilter::Peak, + false, + ) + .expect("set_peq_band"); + assert!(ok); + + let ok = dsp.set_bypass_peq(in_ch, true).expect("set_bypass_peq"); + assert!(ok); + dsp.set_bypass_peq(in_ch, false).expect("un-bypass peq"); + + let ok = dsp.set_gate(input, -40.0, 5, 100, 200).expect("set_gate"); + assert!(ok); + + let ok = dsp + .set_geq_band(input, types::DiscreteFrequency::F100, 1.0) + .expect("set_geq_band"); + assert!(ok); + + let ok = dsp.set_geq_bypass(input, true).expect("set_geq_bypass"); + assert!(ok); + dsp.set_geq_bypass(input, false).expect("un-bypass geq"); + + let ok = dsp + .set_input_source(types::InputSource { + r#type: types::InputType::Analog, // adjust to your enum + frequency: types::DiscreteFrequency::F1000, // adjust to your enum + }) + .expect("set_input_source"); + assert!(ok); + + // // ---------- Output channel exercise ---------- + let output = types::OutputChannel::Out1; + let out_ch = types::Channel::Output(output); + + let ok = dsp + .set_compressor(output, types::Ratio::Limit, 5, 50, 2, -12.0) + .expect("set_compressor"); + assert!(ok); + + let ok = dsp.set_limiter(output, -3.0, 1, 50).expect("set_limiter"); + assert!(ok); + + let ok = dsp + .set_matrix_gain(input, output, -6.0) + .expect("set_matrix_gain"); + assert!(ok); + + let ok = dsp + .set_matrix_routing(output, vec![input]) + .expect("set_matrix_routing"); + assert!(ok); + assert_eq!( + dsp.state().unwrap().current_config.output_states[&output] + .matrix_routes + .connected, + vec![input] + ); + + // // ---------- Copy / link channels ---------- + let other_output = types::OutputChannel::Out2; + let ok = dsp + .copy_channel(out_ch, types::Channel::Output(other_output)) + .expect("copy_channel"); + assert!(ok); + + let ok = dsp + .link_channels(out_ch, vec![types::Channel::Output(other_output)]) + .expect("link_channels"); + assert!(ok); + + // // ---------- Preset name / modification status ---------- + let ok = dsp + .set_current_preset_name("Integration Te") + .expect("set_current_preset_name"); + assert!(ok); + + let status = dsp + .get_preset_modification_status() + .expect("get_preset_modification_status"); + println!("Preset modification status: {:?}", status); + + let name = dsp.get_preset_name(1).expect("get_preset_name"); + println!("Preset 1 name: {}", name); + + // // ---------- Meters ---------- + let meters = dsp.get_meter_levels().expect("get_meter_levels"); + println!("Meters: {:?}", meters); + + // // ---------- Destructive ops (opt-in only) ---------- + if destructive_allowed() { + let ok = dsp + .store_current_configuration(1, false) + .expect("store_current_configuration"); + assert!(ok); + assert!(dsp.state().unwrap().presets.modified[1]); + + let ok = dsp.recall_preset(1).expect("recall_preset"); + assert!(ok); + assert_eq!(dsp.state().unwrap().presets.current_index, 1); + + // factory_reset intentionally left commented out — wipes the unit. + // let ok = dsp.factory_reset(true).expect("factory_reset"); + // assert!(ok); + } else { + println!("Skipping destructive ops (set DSP_ALLOW_DESTRUCTIVE=1 to enable)"); + } + + // ---------- Disconnect ---------- + dsp.disconnect(); + assert!(!dsp.connected(), "device should report disconnected"); +} diff --git a/src/dsp408/types.rs b/src/dsp408/types.rs index e9035df..773dcbe 100644 --- a/src/dsp408/types.rs +++ b/src/dsp408/types.rs @@ -354,13 +354,13 @@ pub struct DSPConfigState { } impl DSPConfigState { - pub fn input_state_mut(&mut self, channel: InputChannel) -> &mut InputChannelState { + pub(crate) fn input_state_mut(&mut self, channel: InputChannel) -> &mut InputChannelState { self.input_states .get_mut(&channel) .expect("Invalid input channel state") } - pub fn output_state_mut(&mut self, channel: OutputChannel) -> &mut OutputChannelState { + pub(crate) fn output_state_mut(&mut self, channel: OutputChannel) -> &mut OutputChannelState { self.output_states .get_mut(&channel) .expect("Invalid output channel state")