feat: adapt to real usage
CI / Rust CI (push) Failing after 1m3s

This commit is contained in:
2026-09-06 12:29:45 +02:00
parent e2fee36cdb
commit af2e7dc602
13 changed files with 2580 additions and 2515 deletions
+38 -28
View File
@@ -7,18 +7,16 @@ use std::{
time::Duration,
};
use strum::IntoEnumIterator;
use crate::{
common::errors::{DSPError, ValidationError},
common::errors::DSPError,
dsp408::{
mapper::{
self,
constants::{CONFIG_CHUNK_INDEX_MAX, PRESET_INDEX_MAX},
constants::{CONFIG_CHUNK_INDEX_MAX, PRESET_INDEX_MAX, delay_range},
response::map_response,
},
protocol::{commands, config::decode_config, frame::extract_frame},
types,
types::{self},
},
};
@@ -154,6 +152,13 @@ impl DSP408 {
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> {
@@ -320,7 +325,20 @@ impl DSP408 {
return Ok(false);
}
self.state_mut()?.current_config.delay_unit = unit;
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)
}
@@ -810,8 +828,12 @@ impl DSP408 {
match channel {
types::Channel::Input(input) => {
state.current_config.input_state_mut(input).peq_chain.bands
[band as usize] = peq;
state
.current_config
.input_state_mut(input)
.peq_chain
.bands
.insert(band, peq);
}
types::Channel::Output(output) => {
@@ -819,7 +841,8 @@ impl DSP408 {
.current_config
.output_state_mut(output)
.peq_chain
.bands[band as usize] = peq;
.bands
.insert(band, peq);
}
}
@@ -947,7 +970,7 @@ impl DSP408 {
}
// Command `0x38` (Set Delay)
pub fn set_channel_delay(
pub fn set_delay(
&mut self,
channel: types::Channel,
delay: f32,
@@ -1350,19 +1373,12 @@ impl DSP408 {
let state = self.state_mut()?;
let index = types::InputChannel::iter()
.position(|x| x == input_channel)
.ok_or_else(|| {
DSPError::Validation(ValidationError::Value(
"Invalid input channel".to_string(),
))
})?;
state
.current_config
.output_state_mut(output_channel)
.matrix_routes
.gains[index] = gain;
.gains
.insert(input_channel, gain);
Ok(true)
}
@@ -1398,19 +1414,13 @@ impl DSP408 {
let state = self.state_mut()?;
let index = types::DiscreteFrequency::iter()
.position(|x| x == frequency_band)
.ok_or_else(|| {
DSPError::Validation(ValidationError::Value(
"Invalid GEQ frequency band".to_string(),
))
})?;
state
.current_config
.input_state_mut(input_channel)
.geq
.gains[index] = gain;
.gains
.insert(frequency_band, gain);
Ok(true)
}
+17 -6
View File
@@ -85,10 +85,6 @@ pub fn map_response(frame: &Frame) -> Result<DeviceResponseType, ProtocolError>
// Command `0x24` (Get Config Chunk)
if cmd == 0x24 {
// Mirrors Python's `int.from_bytes(payload[:1], "little")`, which
// is `0` for an empty payload rather than an error — Python slices
// never panic. `payload.get(1..)` below mirrors the same
// forgiving-slice behavior for the remainder.
let index = payload.first().copied().unwrap_or(0);
if index > 0x1c {
@@ -163,8 +159,23 @@ pub fn map_response(frame: &Frame) -> Result<DeviceResponseType, ProtocolError>
// Command `0x40` (Get Level Meters)
if cmd == 0x40 {
fn float_to_db(v: f32) -> f32 {
40.0 * v.abs().max(1e-12).log10()
const X0: f32 = -25.0; // breakpoint, in dB
const Y0: f32 = 1.0; // raw value at the breakpoint
const K1: f32 = 0.0104; // slope below breakpoint (raw < Y0)
const K2: f32 = 0.0208; // slope above breakpoint (raw >= Y0)
const MIN_DB: f32 = -60.0;
const MAX_DB: f32 = 12.0;
fn float_to_db(raw: f32) -> f32 {
let db = if raw < Y0 {
X0 + (raw - Y0) / K1
} else {
X0 + (raw - Y0) / K2
};
// Clamping naturally absorbs noise-floor outliers (e.g. raw ≈ 0.385
// resolves to well below -60 dB here, so it correctly floors to -60).
db.clamp(MIN_DB, MAX_DB)
}
if payload.len() != 38 {
+63 -26
View File
@@ -35,12 +35,14 @@ fn read_gate(r: &mut BinaryReader) -> Result<types::Gate, DSPError> {
}
fn read_geq(r: &mut BinaryReader) -> Result<types::GraphicEQ, DSPError> {
let gains = types::DiscreteFrequency::iter()
.map(|_| {
let mut gains = std::collections::HashMap::new();
for frequency in types::DiscreteFrequency::iter() {
let raw = r.read_u16()?;
Ok(mapper::raw_to_eq_gain(raw)?)
})
.collect::<Result<Vec<_>, DSPError>>()?;
let gain = mapper::raw_to_eq_gain(raw)?;
gains.insert(frequency, gain);
}
Ok(types::GraphicEQ {
gains,
@@ -49,17 +51,20 @@ fn read_geq(r: &mut BinaryReader) -> Result<types::GraphicEQ, DSPError> {
}
fn read_peq_chain(r: &mut BinaryReader, band_count: usize) -> Result<types::PEQChain, DSPError> {
let bands = (0..band_count)
.map(|_| {
Ok(types::PEQ {
let bands = types::PEQBand::iter()
.take(band_count)
.map(|band| {
let peq = types::PEQ {
gain: mapper::raw_to_eq_gain(r.read_u16()?)?,
frequency: mapper::raw_to_frequency_hz(r.read_u16()?)?,
q: mapper::raw_to_peq_q(r.read_u8()?)?,
filter_type: mapper::raw_to_peq_filter(r.read_u8()?)?,
bypass: false,
};
Ok((band, peq))
})
})
.collect::<Result<Vec<_>, DSPError>>()?;
.collect::<Result<HashMap<_, _>, DSPError>>()?;
Ok(types::PEQChain {
bands,
@@ -67,6 +72,21 @@ fn read_peq_chain(r: &mut BinaryReader, band_count: usize) -> Result<types::PEQC
})
}
fn apply_peq_bypass(
r: &mut BinaryReader,
bands: &mut HashMap<types::PEQBand, types::PEQ>,
) -> Result<(), DSPError> {
let bitmap = r.read_u16()?;
for (index, band) in types::PEQBand::iter().enumerate() {
if let Some(peq) = bands.get_mut(&band) {
peq.bypass = bitmap & (1 << index) != 0;
}
}
Ok(())
}
fn read_crossover(r: &mut BinaryReader) -> Result<types::CrossoverFilters, DSPError> {
let hp_freq = r.read_u16()?;
let lp_freq = r.read_u16()?;
@@ -152,7 +172,13 @@ pub fn decode_config(payload: &[u8]) -> Result<types::DSPConfigState, DSPError>
inp.linked_channels = mapper::raw_to_link_channels(
mapper::channel_to_raw(types::Channel::Input(channel))?,
r.read_u8()?,
)?;
)?
.into_iter()
.filter_map(|link| match link {
types::Channel::Input(input) => Some(input),
types::Channel::Output(_) => None,
})
.collect();
}
// ============================
@@ -162,17 +188,26 @@ pub fn decode_config(payload: &[u8]) -> Result<types::DSPConfigState, DSPError>
for (channel, out) in types::OutputChannel::iter().zip(output_channels.iter_mut()) {
out.name = mapper::raw_to_channel_name(r.pop(constants::CHANNEL_NAME_LENGTH)?)?;
r.skip(1)?;
out.matrix_routes = types::MatrixRoutes {
connected: mapper::raw_to_matrix(r.read_u8()?)?,
gains: (0..4)
.map(|_| {
gains: {
r.skip(1)?;
[
types::InputChannel::InA,
types::InputChannel::InB,
types::InputChannel::InC,
types::InputChannel::InD,
]
.into_iter()
.map(|input| {
let raw = r.read_u16()?;
Ok::<f32, DSPError>(mapper::raw_to_matrix_gain(raw)?)
let gain = mapper::raw_to_matrix_gain(raw)?;
Ok::<(types::InputChannel, f32), DSPError>((input, gain))
})
.collect::<Result<Vec<_>, _>>()?,
.collect::<Result<HashMap<_, _>, _>>()?
},
};
out.crossover = read_crossover(&mut r)?;
@@ -196,7 +231,13 @@ pub fn decode_config(payload: &[u8]) -> Result<types::DSPConfigState, DSPError>
out.linked_channels = mapper::raw_to_link_channels(
mapper::channel_to_raw(types::Channel::Output(channel))?,
r.read_u8()?,
)?;
)?
.into_iter()
.filter_map(|link| match link {
types::Channel::Output(output) => Some(output),
types::Channel::Input(_) => None,
})
.collect();
}
// ============================
@@ -212,15 +253,11 @@ pub fn decode_config(payload: &[u8]) -> Result<types::DSPConfigState, DSPError>
// ============================
for inp in &mut input_channels {
apply_flag(&mut r, inp.peq_chain.bands.as_mut_slice(), |band, value| {
band.bypass = value
})?;
apply_peq_bypass(&mut r, &mut inp.peq_chain.bands)?;
}
for out in &mut output_channels {
apply_flag(&mut r, out.peq_chain.bands.as_mut_slice(), |band, value| {
band.bypass = value
})?;
apply_peq_bypass(&mut r, &mut out.peq_chain.bands)?;
}
// ============================
@@ -260,7 +297,7 @@ pub fn decode_config(payload: &[u8]) -> Result<types::DSPConfigState, DSPError>
for (index, inp) in input_channels.iter_mut().enumerate() {
let bypass = input_peq_bitmap & (1 << index) != 0;
for peq in inp.peq_chain.bands.iter_mut() {
for peq in inp.peq_chain.bands.values_mut() {
peq.bypass = bypass;
}
}
@@ -270,7 +307,7 @@ pub fn decode_config(payload: &[u8]) -> Result<types::DSPConfigState, DSPError>
for (index, out) in output_channels.iter_mut().enumerate() {
let bypass = output_peq_bitmap & (1 << index) != 0;
for peq in out.peq_chain.bands.iter_mut() {
for peq in out.peq_chain.bands.values_mut() {
peq.bypass = bypass;
}
}
+6 -6
View File
@@ -92,6 +92,7 @@ pub enum CrossoverFilter {
Bl48,
}
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum PEQBand {
B1 = 1,
@@ -291,7 +292,7 @@ pub struct Limiter {
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct GraphicEQ {
pub gains: Vec<f32>,
pub gains: HashMap<DiscreteFrequency, f32>,
pub bypass: bool,
}
@@ -323,13 +324,13 @@ pub struct Crossover {
#[derive(Debug, Clone, PartialEq, Default)]
pub struct MatrixRoutes {
pub connected: Vec<InputChannel>,
pub gains: Vec<f32>,
pub gains: HashMap<InputChannel, f32>,
}
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, PartialEq, Default)]
pub struct PEQChain {
pub bands: Vec<PEQ>,
pub bands: HashMap<PEQBand, PEQ>,
pub bypass: bool,
}
@@ -360,7 +361,7 @@ pub struct InputChannelState {
pub geq: GraphicEQ,
pub peq_chain: PEQChain,
pub crossover: CrossoverFilters,
pub linked_channels: Vec<Channel>,
pub linked_channels: Vec<InputChannel>,
pub mute: bool,
}
@@ -376,7 +377,7 @@ pub struct OutputChannelState {
pub gain: f32,
pub phase_inverted: bool,
pub delay: f32,
pub linked_channels: Vec<Channel>,
pub linked_channels: Vec<OutputChannel>,
pub mute: bool,
}
@@ -409,6 +410,5 @@ pub struct DSPState {
pub(crate) name: String,
pub(crate) flags: DeviceFlags,
pub(crate) presets: PresetBank,
pub(crate) current_config: DSPConfigState,
}
+9 -2
View File
@@ -3,6 +3,7 @@ use std::str::FromStr;
use dsp_thomann::dsp408::DSP408;
use dsp_thomann::dsp408::types;
use dsp_thomann::dsp408::types::InputChannel::InA;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let host = IpAddr::from_str("192.168.49.30")?;
@@ -22,9 +23,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
println!("DSP connected.");
dsp.set_mute(types::Channel::Input(types::InputChannel::InA), true)?;
// dsp.set_mute(types::Channel::Input(types::InputChannel::InA), true)?;
dbg!(dsp.state()?);
let r = dsp.set_peq_band(types::Channel::Input(InA), types::PEQBand::B1, 3.0, 1400.0, 2.1, types::PEQFilter::LowShelf, false)?;
dbg!(r);
// dbg!(dsp.state()?);
// let s = &dsp.state()?.current_config.input_states[&InputChannel::InA].peq_chain;
// dbg!(s)
println!("Disconnecting...");
dsp.disconnect()?;