use super::driver::{GimbalAngles, Storm32Gimbal};
use super::error::{Result, Storm32Error};
use super::frame::{RCCommand, RCMessage};
use tracing::debug;
impl Storm32Gimbal {
pub fn set_angles(&mut self, pitch: f32, roll: f32, yaw: f32) -> Result<()> {
Self::validate_angle(pitch, "Pitch")?;
Self::validate_angle(roll, "Roll")?;
Self::validate_angle(yaw, "Yaw")?;
let payload = RCMessage::create_set_angle_payload(pitch, roll, yaw);
let message = RCMessage::new(RCCommand::SetAngle, payload);
self.send_rc_command(&message)?;
self.current_status.sp = GimbalAngles { pitch, roll, yaw };
Ok(())
}
pub fn set_angles_rc(&mut self, pitch: u16, roll: u16, yaw: u16) -> Result<()> {
let payload = RCMessage::create_set_pitch_roll_yaw_payload(pitch, roll, yaw);
let message = RCMessage::new(RCCommand::SetPitchRollYaw, payload);
self.send_rc_command(&message)?;
Ok(())
}
pub fn set_pitch(&mut self, value: u16) -> Result<()> {
Self::validate_rc_value(value, "Pitch")?;
let payload = RCMessage::create_single_axis_payload(value);
let message = RCMessage::new(RCCommand::SetPitch, payload);
self.send_rc_command(&message)?;
Ok(())
}
pub fn set_roll(&mut self, value: u16) -> Result<()> {
Self::validate_rc_value(value, "Roll")?;
let payload = RCMessage::create_single_axis_payload(value);
let message = RCMessage::new(RCCommand::SetRoll, payload);
self.send_rc_command(&message)?;
Ok(())
}
pub fn set_yaw(&mut self, value: u16) -> Result<()> {
Self::validate_rc_value(value, "Yaw")?;
let payload = RCMessage::create_single_axis_payload(value);
let message = RCMessage::new(RCCommand::SetYaw, payload);
self.send_rc_command(&message)?;
Ok(())
}
pub fn recenter_pitch(&mut self) -> Result<()> {
self.set_pitch(0)
}
pub fn recenter_roll(&mut self) -> Result<()> {
self.set_roll(0)
}
pub fn recenter_yaw(&mut self) -> Result<()> {
self.set_yaw(0)
}
pub fn update_status(&mut self) -> Result<()> {
let bitmask = 0x0020u16;
let payload = RCMessage::create_get_data_fields_payload(bitmask);
let message = RCMessage::new(RCCommand::GetDataFields, payload);
let response = self.send_rc_command(&message)?;
if response.is_empty() {
debug!("GetDataFields returned empty response (possibly unsupported)");
return Ok(());
}
self.current_status.pv = parse_imu1_angles_payload(&response)?;
Ok(())
}
pub fn set_pan_mode(&mut self, mode: u8) -> Result<()> {
Self::validate_pan_mode(mode)?;
let payload = RCMessage::create_set_pan_mode_payload(mode);
let message = RCMessage::new(RCCommand::SetPanMode, payload);
self.send_rc_command(&message)?;
Ok(())
}
pub fn set_standby(&mut self, enabled: bool) -> Result<()> {
let state = if enabled { 1u8 } else { 0u8 };
let payload = RCMessage::create_set_standby_payload(state);
let message = RCMessage::new(RCCommand::SetStandby, payload);
self.send_rc_command(&message)?;
Ok(())
}
pub fn get_version(&mut self) -> Result<String> {
let message = RCMessage::new(RCCommand::GetVersion, Vec::new());
let response = self.send_rc_command(&message)?;
if response.len() < 6 {
return Err(Storm32Error::ProtocolError(
"Version response too short".to_string(),
));
}
let firmware_version = u16::from_le_bytes([response[0], response[1]]);
let layout_version = u16::from_le_bytes([response[2], response[3]]);
let capabilities = u16::from_le_bytes([response[4], response[5]]);
Ok(format!(
"Firmware: {}.{}, Layout: {}.{}, Capabilities: 0x{:04X}",
firmware_version >> 8,
firmware_version & 0xFF,
layout_version >> 8,
layout_version & 0xFF,
capabilities
))
}
pub fn get_version_string(&mut self) -> Result<String> {
let message = RCMessage::new(RCCommand::GetVersionStr, Vec::new());
let response = self.send_rc_command(&message)?;
if response.len() < 48 {
return Err(Storm32Error::ProtocolError(
"Version string response too short".to_string(),
));
}
let version_str = String::from_utf8_lossy(&response[0..16])
.trim_end_matches('\0')
.to_string();
let name_str = String::from_utf8_lossy(&response[16..32])
.trim_end_matches('\0')
.to_string();
let board_str = String::from_utf8_lossy(&response[32..48])
.trim_end_matches('\0')
.to_string();
Ok(format!(
"Version: '{}', Name: '{}', Board: '{}'",
version_str, name_str, board_str
))
}
#[allow(dead_code)]
pub fn debug_send_raw(&mut self, command: RCCommand, payload: Vec<u8>) -> Result<Vec<u8>> {
let message = RCMessage::new(command, payload);
self.send_rc_command(&message)
}
}
fn parse_imu1_angles_payload(response: &[u8]) -> Result<GimbalAngles> {
if response.len() < 8 {
return Err(Storm32Error::ProtocolError(format!(
"IMU1ANGLES payload too short: got {} bytes, need 8 \
(bitmask + pitch + roll + yaw)",
response.len()
)));
}
let pitch_raw = i16::from_le_bytes([response[2], response[3]]);
let roll_raw = i16::from_le_bytes([response[4], response[5]]);
let yaw_raw = i16::from_le_bytes([response[6], response[7]]);
Ok(GimbalAngles {
pitch: f32::from(pitch_raw) / 100.0,
roll: f32::from(roll_raw) / 100.0,
yaw: f32::from(yaw_raw) / 100.0,
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn build_imu1_payload(pitch_deg: f32, roll_deg: f32, yaw_deg: f32) -> Vec<u8> {
let mut payload = Vec::with_capacity(8);
payload.extend_from_slice(&0x0020u16.to_le_bytes());
payload.extend_from_slice(&((pitch_deg * 100.0) as i16).to_le_bytes());
payload.extend_from_slice(&((roll_deg * 100.0) as i16).to_le_bytes());
payload.extend_from_slice(&((yaw_deg * 100.0) as i16).to_le_bytes());
payload
}
#[test]
fn parse_imu1_angles_round_trips_three_axes() {
let payload = build_imu1_payload(10.0, -5.0, 42.0);
let angles = parse_imu1_angles_payload(&payload).unwrap();
assert!((angles.pitch - 10.0).abs() < 0.01);
assert!((angles.roll - -5.0).abs() < 0.01);
assert!((angles.yaw - 42.0).abs() < 0.01);
}
#[test]
fn parse_imu1_angles_handles_negative_yaw() {
let payload = build_imu1_payload(0.0, 0.0, -179.99);
let angles = parse_imu1_angles_payload(&payload).unwrap();
assert!((angles.yaw - -179.99).abs() < 0.01);
}
#[test]
fn parse_imu1_angles_rejects_short_payload() {
let short = vec![0u8; 7];
let err = parse_imu1_angles_payload(&short).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("IMU1ANGLES"), "unexpected error: {msg}");
}
}