use std::{ collections::HashMap, fmt, io::{Read, Write}, net::{IpAddr, SocketAddr, TcpStream}, sync::{Arc, Mutex, OnceLock}, time::Duration, }; use crate::{ common::errors::DSPError, dsp408::{ default::{default_input_meters, default_output_meters}, default_dsp_state, mapper::{ self, constants::{CONFIG_CHUNK_INDEX_MAX, PRESET_INDEX_MAX, delay_range}, response::map_response, }, protocol::{commands, config::decode_config, frame::extract_frame}, types, }, }; pub const SOCKET_TIMEOUT: Duration = Duration::from_secs(20); pub const RECV_BUFSIZE: usize = 4096; static INSTANCES: OnceLock>> = OnceLock::new(); fn instances() -> &'static Mutex> { INSTANCES.get_or_init(|| Mutex::new(HashMap::new())) } pub struct DSP408 { mode: types::DSPMode, connection: types::DeviceConnection, socket: Arc>>, state: types::DSPState, input_meters: types::InputMeters, output_meters: types::OutputMeters, } impl DSP408 { pub fn new(host: IpAddr, port: u16, device_id: u8) -> Result { if matches!(host, IpAddr::V6(_)) { return Err(DSPError::InvalidHost("DSP doesn't support IPv6")); } if port == 0 { return Err(DSPError::InvalidPort("Port must be between 1 and 65535")); } if device_id == 0 || device_id > 254 { return Err(DSPError::InvalidDeviceId( "Device ID must be between 1 and 254", )); } Ok(Self { mode: types::DSPMode::Disconnected, connection: types::DeviceConnection { host, port, device_id, }, socket: Arc::new(Mutex::new(None)), state: default_dsp_state(), input_meters: default_input_meters(), output_meters: default_output_meters(), }) } pub fn new_demo() -> Self { Self { mode: types::DSPMode::Demo, connection: types::DeviceConnection { host: IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), port: 0, device_id: 1, }, socket: Arc::new(Mutex::new(None)), state: default_dsp_state(), input_meters: default_input_meters(), output_meters: default_output_meters(), } } fn send_raw(socket: &mut TcpStream, data: &[u8]) -> Result<(), DSPError> { socket.write_all(data)?; Ok(()) } fn recv_raw(socket: &mut TcpStream) -> Result, DSPError> { let mut buffer = vec![0u8; RECV_BUFSIZE]; match socket.read(&mut buffer) { Ok(0) => Err(DSPError::ConnectionClosed), Ok(size) => { buffer.truncate(size); Ok(buffer) } Err(e) if e.kind() == std::io::ErrorKind::TimedOut => Err(DSPError::Timeout), Err(e) => Err(DSPError::Io(e)), } } pub fn send_recv(&self, command: &[u8]) -> Result { let mut socket_guard = self .socket .lock() .map_err(|e| DSPError::MutexPoisoned(e.to_string()))?; let socket = socket_guard.as_mut().ok_or(DSPError::NotConnected)?; Self::send_raw(socket, command)?; let raw = Self::recv_raw(socket)?; let frame = extract_frame(&raw)?; let response = map_response(&frame)?; Ok(response) } pub fn mode(&self) -> types::DSPMode { self.mode } pub fn connected(&self) -> bool { self.mode == types::DSPMode::Connected } pub fn demo(&self) -> bool { self.mode == types::DSPMode::Demo } pub fn state(&self) -> &types::DSPState { &self.state } pub fn input_meters(&self) -> &types::InputMeters { &self.input_meters } pub fn output_meters(&self) -> &types::OutputMeters { &self.output_meters } fn state_mut(&mut self) -> &mut types::DSPState { &mut self.state } fn initialize_state(&mut self) -> Result<(), DSPError> { let name = self.get_device_name()?; let flags = self.get_device_flags()?; let current_preset = self.get_current_preset()?; let modified = self.get_preset_modification_status()?; let mut preset_names = Vec::with_capacity(PRESET_INDEX_MAX); for i in 0..PRESET_INDEX_MAX { preset_names.push(self.get_preset_name(i + 1)?); } let state = self.read_config_state()?; self.state = types::DSPState { name, flags, presets: types::PresetBank { current_index: current_preset as usize, names: preset_names, modified: modified.modified, }, current_config: state, }; Ok(()) } fn convert_delay_unit(value: f32, from: types::DelayUnit, to: types::DelayUnit) -> f32 { let (_, from_max) = delay_range(from); let (_, to_max) = delay_range(to); (value / from_max) * to_max } // ---------- Commands ---------- // Command `0x10` pub fn connect(&mut self) -> Result<(), DSPError> { if self.mode == types::DSPMode::Demo { return Ok(()); } if self.mode == types::DSPMode::Connected { return Err(DSPError::AlreadyConnected); } let key = (self.connection.host, self.connection.device_id); { let map = instances() .lock() .map_err(|e| DSPError::InternalError(e.to_string()))?; if map.contains_key(&key) { return Err(DSPError::AlreadyConnected); } } let addr = SocketAddr::new(self.connection.host, self.connection.port); let stream = TcpStream::connect_timeout(&addr, SOCKET_TIMEOUT)?; stream.set_read_timeout(Some(SOCKET_TIMEOUT))?; { let mut socket = self .socket .lock() .map_err(|e| DSPError::InternalError(e.to_string()))?; *socket = Some(stream); } let handshake = commands::build_handshake(); { let mut socket = self .socket .lock() .map_err(|e| DSPError::InternalError(e.to_string()))?; let socket = socket.as_mut().ok_or(DSPError::NotConnected)?; Self::send_raw(socket, &handshake)?; } let start = std::time::Instant::now(); while start.elapsed() < SOCKET_TIMEOUT { let raw = { let mut socket = self .socket .lock() .map_err(|e| DSPError::InternalError(e.to_string()))?; let socket = socket.as_mut().ok_or(DSPError::NotConnected)?; Self::recv_raw(socket)? }; let frame = extract_frame(&raw)?; let response = map_response(&frame)?; if matches!(response, types::DeviceResponseType::HandshakeAck(_)) { if let Err(error) = self.initialize_state() { let _ = self.disconnect(); return Err(error); } self.mode = types::DSPMode::Connected; instances() .lock() .map_err(|e| DSPError::InternalError(e.to_string()))? .insert(key, ()); return Ok(()); } } self.disconnect()?; Err(DSPError::HandshakeFailed) } // Command `0x11` pub fn disconnect(&mut self) -> Result<(), DSPError> { let key = (self.connection.host, self.connection.device_id); let mut socket_guard = self .socket .lock() .map_err(|e| DSPError::MutexPoisoned(e.to_string()))?; if let Some(mut socket) = socket_guard.take() { let _ = socket.write_all(&commands::build_disconnect()); let _ = socket.shutdown(std::net::Shutdown::Both); } instances() .lock() .map_err(|e| DSPError::MutexPoisoned(e.to_string()))? .remove(&key); if self.mode == types::DSPMode::Connected { self.mode = types::DSPMode::Disconnected; } Ok(()) } // Command `0x12` fn request_acknowledgement(&mut self) -> Result { let request = commands::build_acknowledgement(); let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => Ok(ack.success), other => Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))), } } // Command `0x13` (Device Name) fn get_device_name(&mut self) -> Result { let request = commands::build_request_device_name(); let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::DeviceInfo(info) => Ok(info.name), other => Err(DSPError::UnexpectedResponse(format!( "Expected DeviceInfo, got {:?}", other ))), } } // Command `0x14` (Current Preset Index) fn get_current_preset(&mut self) -> Result { let request = commands::build_get_current_preset(); let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::CurrentPreset(preset) => Ok(preset.index), other => Err(DSPError::UnexpectedResponse(format!( "Expected CurrentPreset, got {:?}", other ))), } } // Command `0x15` (Set Delay Unit) pub fn set_delay_unit(&mut self, unit: types::DelayUnit) -> Result { let raw_unit = mapper::delay_unit_to_raw(unit)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_set_delay_unit(raw_unit)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } // Apply the state change in both Connected and Demo. let state = self.state_mut(); let old_unit = state.current_config.delay_unit; if old_unit != unit { for input in state.current_config.input_states.values_mut() { input.delay = Self::convert_delay_unit(input.delay, old_unit, unit); } for output in state.current_config.output_states.values_mut() { output.delay = Self::convert_delay_unit(output.delay, old_unit, unit); } } state.current_config.delay_unit = unit; Ok(true) } // Command `0x20` (Recall) pub fn recall_preset(&mut self, preset_index: usize) -> Result { let raw_index = mapper::preset_index_to_raw(preset_index)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_recall(raw_index)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } let config = self.read_config_state()?; self.state_mut().current_config = config; } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => {} types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } self.state_mut().presets.current_index = preset_index; Ok(true) } // Command `0x21` (Store Preset) pub fn store_current_configuration( &mut self, preset_index: usize, force: bool, ) -> Result { let raw_index = mapper::preset_index_to_raw(preset_index)?; if preset_index == 0 && !force { return Ok(false); } match self.mode { types::DSPMode::Connected => { let request = commands::build_store_current_configuration(raw_index)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } if !self.request_acknowledgement()? { return Err(DSPError::OperationFailed( "Store Configuration failed".into(), )); } } types::DSPMode::Demo => {} types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } self.state_mut().presets.modified[preset_index] = true; Ok(true) } // Command `0x22` (Get Preset Modification Status) fn get_preset_modification_status(&mut self) -> Result { let request = commands::build_preset_modification_status(); let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::ModifiedPreset(status) => Ok(status), other => Err(DSPError::UnexpectedResponse(format!( "Expected ModifiedPreset, got {:?}", other ))), } } // Command `0x23` (Factory Reset) pub fn factory_reset(&mut self, force: bool) -> Result { if !force { return Ok(false); } match self.mode { types::DSPMode::Connected => { let request = commands::build_factory_reset(); let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } // Hardware accepted the reset. self.state = default_dsp_state(); } types::DSPMode::Demo => { self.state = default_dsp_state(); } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } Ok(true) } // Command `0x26` (Store Preset Name) pub fn set_current_preset_name(&mut self, name: &str) -> Result { let raw_name = mapper::preset_name_to_raw(name)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_store_current_preset_name(&raw_name)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } let current_index = self.state().presets.current_index; if !self.store_current_configuration(current_index, true)? { return Ok(false); } if !self.request_acknowledgement()? { return Ok(false); } } types::DSPMode::Demo => { // No hardware operations. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } // Shared authoritative state update. let current_index = self.state().presets.current_index; self.state_mut().presets.names[current_index - 1] = name.to_string(); Ok(true) } // Command `0x27 - 0x24` (Get Config Chunk) fn get_config_chunk(&mut self, index: usize) -> Result { let raw_index = mapper::config_chunk_to_raw(index)?; let request = commands::build_config_chunk(raw_index)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::ConfigChunk(config) => Ok(config), other => Err(DSPError::UnexpectedResponse(format!( "Expected ConfigChunk, got {:?}", other ))), } } // Command `0x29` (Preset Name) fn get_preset_name(&mut self, preset_index: usize) -> Result { let raw_index = mapper::preset_index_without_factory_to_raw(preset_index)?; let request = commands::build_get_preset_name(raw_index)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::PresetName(name) => Ok(name.name), other => Err(DSPError::UnexpectedResponse(format!( "Expected PresetName, got {:?}", other ))), } } fn copy_channel_state( &mut self, source: types::Channel, destination: types::Channel, ) -> Result<(), DSPError> { match (source, destination) { (types::Channel::Input(src), types::Channel::Input(dst)) => { let src_state = self .state .current_config .input_states .get(&src) .cloned() .ok_or_else(|| DSPError::InternalError("missing input channel state".into()))?; self.state .current_config .input_states .insert(dst, src_state); } (types::Channel::Output(src), types::Channel::Output(dst)) => { let src_state = self .state .current_config .output_states .get(&src) .cloned() .ok_or_else(|| { DSPError::InternalError("missing output channel state".into()) })?; self.state .current_config .output_states .insert(dst, src_state); } _ => { return Err(DSPError::InternalError( "copy_channel requires source and destination to be the same channel kind" .into(), )); } } Ok(()) } // Command `0x2A` (Copy Channel) pub fn copy_channel( &mut self, channel_1: types::Channel, channel_2: types::Channel, ) -> Result { let (raw_channel_1, raw_channel_2) = mapper::copy_channel_to_raw(channel_1, channel_2)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_copy_channel(raw_channel_1, raw_channel_2)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } self.copy_channel_state(channel_1, channel_2)?; Ok(true) } // Command `0x2C` (Device Flags) fn get_device_flags(&mut self) -> Result { let request = commands::build_get_device_flags(); let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::DeviceFlags(flags) => Ok(flags), other => Err(DSPError::UnexpectedResponse(format!( "Expected DeviceFlags, got {:?}", other ))), } } // Command `0x2D` (Unlock Device) pub fn unlock_device(&mut self, password: &str) -> Result { let raw_password = mapper::password_to_raw(password)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_unlock_device(&raw_password)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::AuthenticationResult(result) => { if !result.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected AuthenticationResult, got {:?}", other ))); } } } types::DSPMode::Demo => { // Emulate successful unlock locally. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } self.state_mut().flags.is_locked = false; Ok(true) } // Command `0x2F` (Lock Device) pub fn lock_device(&mut self, password: &str) -> Result { let raw_password = mapper::password_to_raw(password)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_lock_device(&raw_password)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // Emulate locking locally. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } self.state_mut().flags.is_locked = true; Ok(true) } // Command `0x30` (Set Compressor) pub fn set_compressor( &mut self, channel: types::OutputChannel, ratio: types::Ratio, attack: u16, release: u16, knee: u16, threshold: f32, ) -> Result { let raw_channel = mapper::output_channel_to_raw(channel)?; let raw_ratio = mapper::ratio_to_raw(ratio)?; let raw_attack = mapper::attack_ms_to_raw(attack)?; let raw_release = mapper::release_ms_to_raw(release)?; let raw_knee = mapper::knee_db_to_raw(knee)?; let raw_threshold = mapper::threshold_db_to_raw(threshold)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_set_compressor( raw_channel, raw_ratio, raw_attack, raw_release, raw_knee, raw_threshold, )?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } // Shared authoritative state update. self.state_mut() .current_config .output_state_mut(channel) .compressor = types::Compressor { threshold, ratio, attack, release, knee, }; Ok(true) } // Command `0x31` (Set Low-Pass Filter) pub fn set_low_pass( &mut self, channel: types::Channel, freq: f32, filter_type: types::CrossoverFilter, ) -> Result { let raw_channel = mapper::channel_to_raw(channel)?; let raw_freq = mapper::frequency_hz_to_raw(freq)?; let raw_filter = mapper::crossover_filter_to_raw(filter_type)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_low_pass(raw_channel, raw_freq, raw_filter)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } let state = self.state_mut(); match channel { types::Channel::Input(input) => { state .current_config .input_state_mut(input) .crossover .low_pass = types::Crossover { frequency: freq, slope: filter_type, }; } types::Channel::Output(output) => { state .current_config .output_state_mut(output) .crossover .low_pass = types::Crossover { frequency: freq, slope: filter_type, }; } } Ok(true) } // Command `0x32` (Set High-Pass Filter) pub fn set_high_pass( &mut self, channel: types::Channel, freq: f32, filter_type: types::CrossoverFilter, ) -> Result { let raw_channel = mapper::channel_to_raw(channel)?; let raw_freq = mapper::frequency_hz_to_raw(freq)?; let raw_filter = mapper::crossover_filter_to_raw(filter_type)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_high_pass(raw_channel, raw_freq, raw_filter)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } let state = self.state_mut(); match channel { types::Channel::Input(input) => { state .current_config .input_state_mut(input) .crossover .high_pass = types::Crossover { frequency: freq, slope: filter_type, }; } types::Channel::Output(output) => { state .current_config .output_state_mut(output) .crossover .high_pass = types::Crossover { frequency: freq, slope: filter_type, }; } } Ok(true) } // Command `0x33` (Set PEQ Band) #[allow(clippy::too_many_arguments)] pub fn set_peq_band( &mut self, channel: types::Channel, band: types::PEQBand, gain_db: f32, freq_hz: f32, q: f32, filter_type: types::PEQFilter, bypass: bool, ) -> Result { let raw_channel = mapper::channel_to_raw(channel)?; let raw_band = mapper::peq_band_to_raw(band, channel)?; let raw_gain = mapper::eq_gain_db_to_raw(gain_db)?; let raw_frequency = mapper::frequency_hz_to_raw(freq_hz)?; let raw_q = mapper::peq_q_to_raw(q, filter_type)?; let raw_filter = mapper::peq_filter_to_raw(filter_type)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_peq_band( raw_channel, raw_band, raw_gain, raw_frequency, raw_q, raw_filter, bypass as u8, )?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } // Shared authoritative state update (Connected + Demo). let state = self.state_mut(); let peq = types::PEQ { gain: gain_db, frequency: freq_hz, q, filter_type, bypass, }; match channel { types::Channel::Input(input) => { state .current_config .input_state_mut(input) .peq_chain .bands .insert(band, peq); } types::Channel::Output(output) => { state .current_config .output_state_mut(output) .peq_chain .bands .insert(band, peq); } } Ok(true) } // Command `0x34` (Set Gain) pub fn set_channel_gain(&mut self, channel: types::Channel, db: f32) -> Result { let raw_channel = mapper::channel_to_raw(channel)?; let raw_db = mapper::gain_db_to_raw(db)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_gain(raw_channel, raw_db)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } let state = self.state_mut(); match channel { types::Channel::Input(input) => { state.current_config.input_state_mut(input).gain = db; } types::Channel::Output(output) => { state.current_config.output_state_mut(output).gain = db; } } Ok(true) } // Command `0x35` (Set Mute) pub fn set_mute(&mut self, channel: types::Channel, muted: bool) -> Result { let raw_channel = mapper::channel_to_raw(channel)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_mute(raw_channel, muted as u8)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } let state = self.state_mut(); match channel { types::Channel::Input(input) => { state.current_config.input_state_mut(input).mute = muted; } types::Channel::Output(output) => { state.current_config.output_state_mut(output).mute = muted; } } Ok(true) } // Command `0x36` (Invert Gain) pub fn set_channel_inverse_gain( &mut self, channel: types::Channel, inverted: bool, ) -> Result { let raw_channel = mapper::channel_to_raw(channel)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_inverted_gain(raw_channel, inverted as u8)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } let state = self.state_mut(); match channel { types::Channel::Input(input) => { state.current_config.input_state_mut(input).phase_inverted = inverted; } types::Channel::Output(output) => { state.current_config.output_state_mut(output).phase_inverted = inverted; } } Ok(true) } // Command `0x38` (Set Delay) pub fn set_delay( &mut self, channel: types::Channel, delay: f32, unit: types::DelayUnit, ) -> Result { let raw_channel = mapper::channel_to_raw(channel)?; let raw_delay = mapper::delay_to_raw(delay, unit)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_set_delay(raw_channel, raw_delay)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } let state = self.state_mut(); let delay = mapper::raw_to_delay(raw_delay, unit)?; match channel { types::Channel::Input(input) => { state.current_config.input_state_mut(input).delay = delay; } types::Channel::Output(output) => { state.current_config.output_state_mut(output).delay = delay; } } Ok(true) } // Command `0x39` (Set Input Source) pub fn set_input_source(&mut self, input_source: types::InputSource) -> Result { let raw_source = mapper::input_source_to_raw(input_source.r#type)?; let raw_frequency = mapper::frequency_value_to_raw(input_source.frequency)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_set_input_source(raw_source, raw_frequency)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } self.state_mut().current_config.input_source = input_source; Ok(true) } // Command `0x3A` (Matrix Routing) pub fn set_matrix_routing( &mut self, output: types::OutputChannel, inputs: Vec, ) -> Result { let raw_output = mapper::output_channel_to_raw(output)?; let raw_inputs = mapper::matrix_to_raw(&inputs)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_matrix_routing(raw_output, raw_inputs)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } self.state_mut() .current_config .output_state_mut(output) .matrix_routes .connected = inputs; Ok(true) } // Command `0x3B` (Link Channel) pub fn link_channels( &mut self, source: types::Channel, destinations: Vec, ) -> Result { let raw_source = mapper::channel_to_raw(source)?; let bitmask = mapper::link_channels_to_raw(source, &destinations)?; // Copy source configuration to each destination first. This runs in // both Connected and Demo mode (copy_channel handles that dispatch // itself), and returns NotConnected for us if we're Disconnected. for destination in &destinations { if *destination == source { continue; } if !self.copy_channel(source, *destination)? { return Ok(false); } } match self.mode { types::DSPMode::Connected => { // Link source let request = commands::build_link_channel(raw_source, bitmask)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } // Clear destination links for destination in &destinations { if *destination == source { continue; } let raw_destination = mapper::channel_to_raw(*destination)?; let request = commands::build_link_channel(raw_destination, 0x00)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } if !self.request_acknowledgement()? { return Ok(false); } } types::DSPMode::Demo => { // Copy already applied above; nothing else to reconcile. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } let state = self.state_mut(); match source { types::Channel::Input(s) => { let input_destinations: Vec = destinations .iter() .filter_map(|destination| match destination { types::Channel::Input(input) => Some(*input), types::Channel::Output(_) => None, }) .collect(); let states = &mut state.current_config.input_states; // Remove the source and destinations from all existing links. for channel_state in states.values_mut() { channel_state .linked_channels .retain(|channel| *channel != s && !input_destinations.contains(channel)); } // Link source -> destinations. states.get_mut(&s).unwrap().linked_channels = input_destinations; } types::Channel::Output(s) => { let output_destinations: Vec = destinations .iter() .filter_map(|destination| match destination { types::Channel::Output(output) => Some(*output), types::Channel::Input(_) => None, }) .collect(); let states = &mut state.current_config.output_states; // Remove the source and destinations from all existing links. for channel_state in states.values_mut() { channel_state .linked_channels .retain(|channel| *channel != s && !output_destinations.contains(channel)); } // Link source -> destinations. states.get_mut(&s).unwrap().linked_channels = output_destinations; } } Ok(true) } // Command `0x3C` (Bypass PEQ) pub fn set_bypass_peq( &mut self, channel: types::Channel, bypass: bool, ) -> Result { let raw_channel = mapper::channel_to_raw(channel)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_bypass_peq(raw_channel, bypass as u8)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } let state = self.state_mut(); match channel { types::Channel::Input(input) => { state.current_config.input_state_mut(input).peq_chain.bypass = bypass; } types::Channel::Output(output) => { state .current_config .output_state_mut(output) .peq_chain .bypass = bypass; } } Ok(true) } // Command `0x3D` (Set Channel Name) pub fn set_channel_name( &mut self, channel: types::Channel, name: &str, ) -> Result { let raw_channel = mapper::channel_to_raw(channel)?; let raw_name = mapper::channel_name_to_raw(name)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_store_channel_name(raw_channel, &raw_name)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } let state = self.state_mut(); match channel { types::Channel::Input(input) => { state.current_config.input_state_mut(input).name = name.to_string(); } types::Channel::Output(output) => { state.current_config.output_state_mut(output).name = name.to_string(); } } Ok(true) } // Command `0x3E` (Set Gate Parameters) pub fn set_gate( &mut self, channel: types::InputChannel, threshold: f32, attack: u16, hold: u16, release: u16, ) -> Result { let raw_channel = mapper::input_channel_to_raw(channel)?; let raw_threshold = mapper::gate_threshold_db_to_raw(threshold)?; let raw_attack = mapper::attack_ms_to_raw(attack)?; let raw_hold = mapper::hold_ms_to_raw(hold)?; let raw_release = mapper::release_ms_to_raw(release)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_set_gate( raw_channel, raw_threshold, raw_attack, raw_hold, raw_release, )?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } self.state_mut() .current_config .input_state_mut(channel) .gate = types::Gate { attack, release, hold, threshold, }; Ok(true) } // Command `0x3F` (Set Output Limiter) pub fn set_limiter( &mut self, channel: types::OutputChannel, threshold: f32, attack: u16, release: u16, ) -> Result { let raw_channel = mapper::output_channel_to_raw(channel)?; let raw_threshold = mapper::threshold_db_to_raw(threshold)?; let raw_attack = mapper::attack_ms_to_raw(attack)?; let raw_release = mapper::release_ms_to_raw(release)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_set_limiter( raw_channel, raw_threshold, raw_attack, raw_release, )?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } self.state_mut() .current_config .output_state_mut(channel) .limiter = types::Limiter { threshold, attack, release, }; Ok(true) } // Command `0x40` (Get Meter Level) pub fn get_meter_levels( &mut self, ) -> Result<(types::InputMeters, types::OutputMeters), DSPError> { match self.mode { types::DSPMode::Connected => { let request = commands::build_get_meter_levels(); let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Meters(inputs, outputs) => { self.input_meters = inputs.clone(); self.output_meters = outputs.clone(); Ok((inputs, outputs)) } other => Err(DSPError::UnexpectedResponse(format!( "Expected Meters, got {:?}", other ))), } } types::DSPMode::Demo => { // Return the locally simulated meter values. Ok((self.input_meters.clone(), self.output_meters.clone())) } types::DSPMode::Disconnected => Err(DSPError::NotConnected), } } // Command `0x41` (Set Matrix Gain) pub fn set_matrix_gain( &mut self, input_channel: types::InputChannel, output_channel: types::OutputChannel, gain: f32, ) -> Result { let raw_input_channel = mapper::input_channel_to_raw(input_channel)?; let raw_output_channel = mapper::output_channel_to_raw(output_channel)?; let raw_gain = mapper::matrix_gain_db_to_raw(gain)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_matrix_gain(raw_input_channel, raw_output_channel, raw_gain)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } self.state_mut() .current_config .output_state_mut(output_channel) .matrix_routes .gains .insert(input_channel, gain); Ok(true) } // Command `0x48` (Set GEQ Band) pub fn set_geq_band( &mut self, input_channel: types::InputChannel, frequency_band: types::DiscreteFrequency, gain: f32, ) -> Result { let raw_input_channel = mapper::input_channel_to_raw(input_channel)?; let raw_frequency_band = mapper::frequency_value_to_raw(frequency_band)?; let raw_gain = mapper::eq_gain_db_to_raw(gain)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_set_geq_band(raw_input_channel, raw_frequency_band, raw_gain)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } self.state_mut() .current_config .input_state_mut(input_channel) .geq .gains .insert(frequency_band, gain); Ok(true) } // Command `0x49` (Set GEQ Bypass) pub fn set_geq_bypass( &mut self, input_channel: types::InputChannel, bypass: bool, ) -> Result { let raw_input_channel = mapper::input_channel_to_raw(input_channel)?; match self.mode { types::DSPMode::Connected => { let request = commands::build_set_geq_bypass(raw_input_channel, bypass as u8)?; let resp = self.send_recv(&request)?; match resp { types::DeviceResponseType::Acknowledgement(ack) => { if !ack.success { return Ok(false); } } other => { return Err(DSPError::UnexpectedResponse(format!( "Expected Acknowledgement, got {:?}", other ))); } } } types::DSPMode::Demo => { // No hardware request. } types::DSPMode::Disconnected => { return Err(DSPError::NotConnected); } } self.state_mut() .current_config .input_state_mut(input_channel) .geq .bypass = bypass; Ok(true) } fn read_config_state(&mut self) -> Result { let mut raw = Vec::new(); for i in 0..=CONFIG_CHUNK_INDEX_MAX { let chunk = self.get_config_chunk(i)?; raw.extend_from_slice(&chunk.data); } let config = decode_config(&raw)?; if !self.request_acknowledgement()? { return Err(DSPError::OperationFailed("DSP initialization".into())); } Ok(config) } } impl Drop for DSP408 { fn drop(&mut self) { if self.connected() { let _ = self.disconnect(); } } } impl fmt::Debug for DSP408 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("DSP408") .field("host", &self.connection.host) .field("port", &self.connection.port) .field("device_id", &self.connection.device_id) .field("connected", &self.connected()) .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(); 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().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().current_config.input_states[&input].mute); let ok = dsp.set_mute(in_ch, false).expect("unmute"); assert!(ok); assert!(!dsp.state().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().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().current_config.input_states[&input].phase_inverted); dsp.set_channel_inverse_gain(in_ch, false) .expect("revert phase"); let ok = dsp .set_delay(in_ch, 1.5, types::DelayUnit::Millisecond) .expect("set_delay"); assert!(ok); let ok = dsp .set_channel_name(in_ch, "Test InA") .expect("set_channel_name"); assert!(ok); assert_eq!( dsp.state().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().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().presets.modified[1]); let ok = dsp.recall_preset(1).expect("recall_preset"); assert!(ok); assert_eq!(dsp.state().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 ---------- assert!(dsp.disconnect().is_ok()); assert!(!dsp.connected(), "device should report disconnected"); }