532 lines
13 KiB
Rust
532 lines
13 KiB
Rust
use crate::{
|
|
common::errors::ProtocolError,
|
|
dsp408::{
|
|
protocol::{
|
|
constants::{
|
|
METERS_HIGH_STEP,
|
|
METERS_LOW_STEP,
|
|
METERS_SPLIT,
|
|
METERS_SPLIT_LEVEL_DB,
|
|
},
|
|
frame::Frame,
|
|
},
|
|
types::{
|
|
Acknowledgement,
|
|
AuthenticationResult,
|
|
ConfigChunk,
|
|
CurrentPreset,
|
|
DeviceFlags,
|
|
DeviceInfo,
|
|
DeviceResponseType,
|
|
HandshakeAck,
|
|
Meters,
|
|
ModifiedPreset,
|
|
NotImplemented,
|
|
PresetName,
|
|
UnknownResponse,
|
|
},
|
|
},
|
|
};
|
|
|
|
use super::encoding::{decode_ascii, decode_float16};
|
|
|
|
pub fn map_response(frame: &Frame) -> Result<DeviceResponseType, ProtocolError> {
|
|
let cmd = frame.command;
|
|
let payload = &frame.payload;
|
|
|
|
// Command `0x01`
|
|
if cmd == 0x01 {
|
|
return Ok(DeviceResponseType::Acknowledgement(Acknowledgement {
|
|
success: true,
|
|
}));
|
|
}
|
|
|
|
// Command `0x02`
|
|
if cmd == 0x02 {
|
|
return Ok(DeviceResponseType::NotImplemented(NotImplemented));
|
|
}
|
|
|
|
// Command `0x10` (Start)
|
|
if cmd == 0x10 {
|
|
if payload.len() != 1 {
|
|
return Err(ProtocolError::new(format!(
|
|
"Invalid Start response payload length: {}",
|
|
payload.len()
|
|
)));
|
|
}
|
|
|
|
return Ok(DeviceResponseType::HandshakeAck(HandshakeAck {
|
|
ack: payload.first().copied(),
|
|
}));
|
|
}
|
|
|
|
// Command `0x13` (Device Name)
|
|
if cmd == 0x13 {
|
|
if payload.is_empty() {
|
|
return Err(ProtocolError::new("Missing device name payload"));
|
|
}
|
|
|
|
let name = decode_ascii(payload, false)
|
|
.map_err(|e| ProtocolError::new(e.0))?;
|
|
|
|
return Ok(DeviceResponseType::DeviceInfo(DeviceInfo { name }));
|
|
}
|
|
|
|
// Command `0x14` (Current Preset Index)
|
|
if cmd == 0x14 {
|
|
if payload.len() != 1 {
|
|
return Err(ProtocolError::new(format!(
|
|
"Invalid Current Preset payload length: {}",
|
|
payload.len()
|
|
)));
|
|
}
|
|
|
|
return Ok(DeviceResponseType::CurrentPreset(CurrentPreset {
|
|
index: payload[0],
|
|
}));
|
|
}
|
|
|
|
// Command `0x22` (Get Preset Modification Status)
|
|
if cmd == 0x22 {
|
|
if payload.len() != 20 {
|
|
return Err(ProtocolError::new(format!(
|
|
"Invalid Modified Preset payload length: {}",
|
|
payload.len()
|
|
)));
|
|
}
|
|
|
|
let flags: Vec<bool> = payload.iter().map(|&b| b == 0xFF).collect();
|
|
|
|
return Ok(DeviceResponseType::ModifiedPreset(ModifiedPreset {
|
|
modified: flags,
|
|
}));
|
|
}
|
|
|
|
// 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 {
|
|
return Err(ProtocolError::new(format!(
|
|
"Invalid Config Chunk Index: {}",
|
|
index
|
|
)));
|
|
}
|
|
|
|
let data = payload.get(1..).unwrap_or(&[]).to_vec();
|
|
|
|
return Ok(DeviceResponseType::ConfigChunk(ConfigChunk {
|
|
sub_index: index,
|
|
data,
|
|
}));
|
|
}
|
|
|
|
// Command `0x29` (Preset Name)
|
|
if cmd == 0x29 {
|
|
if payload.is_empty() {
|
|
return Err(ProtocolError::new("Missing preset index"));
|
|
}
|
|
|
|
let name = decode_ascii(&payload[1..], false)
|
|
.map_err(|e| ProtocolError::new(e.0))?;
|
|
|
|
return Ok(DeviceResponseType::PresetName(PresetName {
|
|
index: payload[0],
|
|
name,
|
|
}));
|
|
}
|
|
|
|
// Command `0x2C` (Device Flags)
|
|
if cmd == 0x2C {
|
|
if payload.len() < 6 {
|
|
return Err(ProtocolError::new(format!(
|
|
"Invalid Device Flags payload length: {}",
|
|
payload.len()
|
|
)));
|
|
}
|
|
|
|
if payload[5] != 0x00 && payload[5] != 0x01 {
|
|
return Err(ProtocolError::new(format!(
|
|
"Invalid lock flag: {:#04x}",
|
|
payload[5]
|
|
)));
|
|
}
|
|
|
|
return Ok(DeviceResponseType::DeviceFlags(DeviceFlags {
|
|
is_locked: payload[5] == 0x01,
|
|
}));
|
|
}
|
|
|
|
// Command `0x2D` (Unlock Device)
|
|
if cmd == 0x2D {
|
|
if payload.len() < 2 {
|
|
return Err(ProtocolError::new("Missing authentication result"));
|
|
}
|
|
|
|
if payload[1] != 0x00 && payload[1] != 0x01 {
|
|
return Err(ProtocolError::new(format!(
|
|
"Invalid authentication flag: {:#04x}",
|
|
payload[1]
|
|
)));
|
|
}
|
|
|
|
return Ok(DeviceResponseType::AuthenticationResult(AuthenticationResult {
|
|
success: payload[1] == 0x01,
|
|
}));
|
|
}
|
|
|
|
// Command `0x40` (Get Level Meters)
|
|
if cmd == 0x40 {
|
|
if payload.len() != 38 {
|
|
return Err(ProtocolError::new("Not enough bytes for meter levels"));
|
|
}
|
|
|
|
let floats: Vec<f32> = (0..36)
|
|
.step_by(3)
|
|
.map(|i| decode_float16(payload[i], payload[i + 1]).unwrap_or_default())
|
|
.collect();
|
|
|
|
let meters: Vec<f32> = floats
|
|
.into_iter()
|
|
.map(|f| {
|
|
if f < METERS_SPLIT {
|
|
METERS_SPLIT_LEVEL_DB + (f - METERS_SPLIT) / METERS_LOW_STEP
|
|
} else {
|
|
METERS_SPLIT_LEVEL_DB + (f - METERS_SPLIT) / METERS_HIGH_STEP
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
return Ok(DeviceResponseType::Meters(Meters { levels: meters }));
|
|
}
|
|
|
|
Ok(DeviceResponseType::UnknownResponse(UnknownResponse {
|
|
cmd: format!("{:#04x}", cmd),
|
|
raw: payload.clone(),
|
|
}))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn frame(command: u8, payload: Vec<u8>) -> Frame {
|
|
Frame { command, payload }
|
|
}
|
|
|
|
#[test]
|
|
fn acknowledgement() {
|
|
let result = map_response(&frame(0x01, vec![])).unwrap();
|
|
|
|
assert_eq!(
|
|
result,
|
|
DeviceResponseType::Acknowledgement(Acknowledgement {
|
|
success: true
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn not_implemented() {
|
|
let result = map_response(&frame(0x02, vec![])).unwrap();
|
|
|
|
assert_eq!(
|
|
result,
|
|
DeviceResponseType::NotImplemented(NotImplemented)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn handshake_ack() {
|
|
let result = map_response(&frame(0x10, vec![0x01])).unwrap();
|
|
|
|
assert_eq!(
|
|
result,
|
|
DeviceResponseType::HandshakeAck(HandshakeAck {
|
|
ack: Some(1)
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn handshake_ack_invalid_payload_length() {
|
|
let result = map_response(&frame(0x10, vec![1, 2]));
|
|
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn device_name() {
|
|
let result = map_response(&frame(0x13, b"TestDevice".to_vec()))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
result,
|
|
DeviceResponseType::DeviceInfo(DeviceInfo {
|
|
name: "TestDevice".into()
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn device_name_missing_payload() {
|
|
assert!(
|
|
map_response(&frame(0x13, vec![]))
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn device_name_invalid_ascii() {
|
|
assert!(
|
|
map_response(&frame(0x13, vec![0xff]))
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn current_preset() {
|
|
let result = map_response(&frame(0x14, vec![5]))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
result,
|
|
DeviceResponseType::CurrentPreset(CurrentPreset {
|
|
index: 5
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn current_preset_wrong_length_errors() {
|
|
assert!(
|
|
map_response(&frame(0x14, vec![1, 2]))
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn modified_preset() {
|
|
let payload = vec![0xff; 20];
|
|
|
|
let result = map_response(&frame(0x22, payload))
|
|
.unwrap();
|
|
|
|
match result {
|
|
DeviceResponseType::ModifiedPreset(p) => {
|
|
assert_eq!(p.modified.len(), 20);
|
|
assert!(p.modified.iter().all(|v| *v));
|
|
}
|
|
other => panic!("unexpected {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn modified_preset_invalid_length() {
|
|
assert!(
|
|
map_response(&frame(0x22, vec![0; 19]))
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn config_chunk() {
|
|
let result = map_response(&frame(
|
|
0x24,
|
|
vec![3, 0xaa, 0xbb],
|
|
))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
result,
|
|
DeviceResponseType::ConfigChunk(ConfigChunk {
|
|
sub_index: 3,
|
|
data: vec![0xaa, 0xbb],
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn config_chunk_empty_payload_defaults_to_zero() {
|
|
let result = map_response(&frame(0x24, vec![]))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
result,
|
|
DeviceResponseType::ConfigChunk(ConfigChunk {
|
|
sub_index: 0,
|
|
data: vec![],
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn config_chunk_invalid_index() {
|
|
assert!(
|
|
map_response(&frame(0x24, vec![0x1d]))
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn preset_name() {
|
|
let result = map_response(&frame(
|
|
0x29,
|
|
vec![2, b'A', b'm', b'p'],
|
|
))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
result,
|
|
DeviceResponseType::PresetName(PresetName {
|
|
index: 2,
|
|
name: "Amp".into(),
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn preset_name_missing_index() {
|
|
assert!(
|
|
map_response(&frame(0x29, vec![]))
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn preset_name_invalid_ascii() {
|
|
assert!(
|
|
map_response(&frame(0x29, vec![1, 0xff]))
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn device_flags_locked() {
|
|
let mut payload = vec![0; 6];
|
|
payload[5] = 1;
|
|
|
|
let result = map_response(&frame(0x2c, payload))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
result,
|
|
DeviceResponseType::DeviceFlags(DeviceFlags {
|
|
is_locked: true
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn device_flags_invalid_length() {
|
|
assert!(
|
|
map_response(&frame(0x2c, vec![0; 5]))
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn device_flags_invalid_lock_flag() {
|
|
let mut payload = vec![0; 6];
|
|
payload[5] = 2;
|
|
|
|
assert!(
|
|
map_response(&frame(0x2c, payload))
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn authentication_success() {
|
|
let result = map_response(&frame(
|
|
0x2d,
|
|
vec![0, 1],
|
|
))
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
result,
|
|
DeviceResponseType::AuthenticationResult(
|
|
AuthenticationResult {
|
|
success: true
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn authentication_missing_result() {
|
|
assert!(
|
|
map_response(&frame(0x2d, vec![0]))
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn authentication_invalid_flag() {
|
|
assert!(
|
|
map_response(&frame(0x2d, vec![0, 2]))
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn meters_invalid_length() {
|
|
assert!(
|
|
map_response(&frame(0x40, vec![0; 10]))
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn meters_decode() {
|
|
// 38 bytes required.
|
|
// First 36 bytes are meter values, remaining 2 bytes ignored.
|
|
let mut payload = vec![0; 38];
|
|
|
|
// half float 1.0 = 0x3c00
|
|
for i in (0..36).step_by(3) {
|
|
payload[i] = 0x00;
|
|
payload[i + 1] = 0x3c;
|
|
}
|
|
|
|
let result = map_response(&frame(0x40, payload))
|
|
.unwrap();
|
|
|
|
match result {
|
|
DeviceResponseType::Meters(m) => {
|
|
assert_eq!(m.levels.len(), 12);
|
|
|
|
for level in m.levels {
|
|
assert!(level.is_finite());
|
|
}
|
|
}
|
|
other => panic!("unexpected {:?}", other),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_command_falls_through() {
|
|
let result = map_response(&frame(
|
|
0x99,
|
|
vec![0xaa],
|
|
))
|
|
.unwrap();
|
|
|
|
match result {
|
|
DeviceResponseType::UnknownResponse(r) => {
|
|
assert_eq!(r.cmd, "0x99");
|
|
assert_eq!(r.raw, vec![0xaa]);
|
|
}
|
|
other => panic!(
|
|
"expected UnknownResponse, got {:?}",
|
|
other
|
|
),
|
|
}
|
|
}
|
|
}
|