use serde::{Deserialize, Serialize};
use std::time::SystemTime;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PanMode {
Off = 0,
HoldHoldPan = 1,
HoldHoldHold = 2,
PanPanPan = 3,
PanHoldHold = 4,
PanHoldPan = 5,
}
impl PanMode {
pub fn from_u8(value: u8) -> Option<Self> {
match value {
0 => Some(PanMode::Off),
1 => Some(PanMode::HoldHoldPan),
2 => Some(PanMode::HoldHoldHold),
3 => Some(PanMode::PanPanPan),
4 => Some(PanMode::PanHoldHold),
5 => Some(PanMode::PanHoldPan),
_ => None,
}
}
pub fn to_name(&self) -> &'static str {
match self {
PanMode::Off => "OFF",
PanMode::HoldHoldPan => "HOLDHOLDPAN",
PanMode::HoldHoldHold => "HOLDHOLDHOLD",
PanMode::PanPanPan => "PANPANPAN",
PanMode::PanHoldHold => "PANHOLDHOLD",
PanMode::PanHoldPan => "PANHOLDPAN",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ControlSource {
UnixSocket(u32),
Mavlink(u8),
Cli,
}
impl ControlSource {
pub fn priority(&self) -> u8 {
match self {
ControlSource::Mavlink(1) => 100,
ControlSource::Mavlink(_) => 50,
ControlSource::UnixSocket(_) => 20,
ControlSource::Cli => 10,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum CommandMode {
Position,
Rate,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PrimaryControl {
pub primary_sysid: u8,
pub primary_compid: u8,
pub secondary_sysid: u8,
pub secondary_compid: u8,
}
impl PrimaryControl {
pub fn is_allowed(&self, sysid: u8, compid: u8) -> bool {
if self.primary_sysid == 0 && self.primary_compid == 0 {
return true;
}
self.primary_sysid == sysid && self.primary_compid == compid
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GimbalCommand {
pub source: ControlSource,
pub mode: CommandMode,
pub yaw: Option<f32>,
pub pitch: Option<f32>,
pub roll: Option<f32>,
pub timestamp: SystemTime,
}
impl GimbalCommand {
pub fn new(
source: ControlSource,
mode: CommandMode,
yaw: Option<f32>,
pitch: Option<f32>,
roll: Option<f32>,
) -> Self {
Self {
source,
mode,
yaw,
pitch,
roll,
timestamp: SystemTime::now(),
}
}
pub fn is_stale(&self) -> bool {
if let Ok(elapsed) = self.timestamp.elapsed() {
elapsed.as_secs() > 5
} else {
true
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GimbalState {
pub yaw: f32,
pub pitch: f32,
pub roll: f32,
pub measured_at: Option<SystemTime>,
pub pan_mode: PanMode,
pub standby: bool,
pub firmware_version: Option<String>,
pub last_update: SystemTime,
}
impl Default for GimbalState {
fn default() -> Self {
Self {
yaw: 0.0,
pitch: 0.0,
roll: 0.0,
measured_at: None,
pan_mode: PanMode::Off,
standby: false,
firmware_version: None,
last_update: SystemTime::now(),
}
}
}
impl GimbalState {
pub fn update_measured_angles(&mut self, yaw: f32, pitch: f32, roll: f32) {
self.yaw = yaw;
self.pitch = pitch;
self.roll = roll;
let now = SystemTime::now();
self.measured_at = Some(now);
self.last_update = now;
}
pub fn update_pan_mode(&mut self, mode: PanMode) {
self.pan_mode = mode;
self.last_update = SystemTime::now();
}
pub fn update_standby(&mut self, standby: bool) {
self.standby = standby;
self.last_update = SystemTime::now();
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")]
pub enum IpcCommand {
Set {
pitch: f32,
roll: f32,
yaw: f32,
},
Center,
Status,
Version,
PanMode {
mode: u8,
},
Standby {
enabled: bool,
},
CalibrateYaw,
SetYawOffset {
deg: f32,
},
Selftest,
Help,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "lowercase")]
pub enum IpcResponse {
Ok {
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<serde_json::Value>,
},
Error {
message: String,
},
}
impl IpcResponse {
pub fn ok() -> Self {
IpcResponse::Ok { data: None }
}
pub fn ok_with_data(data: serde_json::Value) -> Self {
IpcResponse::Ok { data: Some(data) }
}
pub fn error(message: impl Into<String>) -> Self {
IpcResponse::Error {
message: message.into(),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn deserialize_set_command() {
let cmd: IpcCommand =
serde_json::from_str(r#"{"cmd":"set","pitch":10.0,"roll":-2.5,"yaw":42.0}"#).unwrap();
match cmd {
IpcCommand::Set { pitch, roll, yaw } => {
assert!((pitch - 10.0).abs() < 1e-6);
assert!((roll + 2.5).abs() < 1e-6);
assert!((yaw - 42.0).abs() < 1e-6);
}
other => panic!("expected Set, got {other:?}"),
}
}
#[test]
fn deserialize_pan_mode_uses_snake_case_discriminator() {
let cmd: IpcCommand = serde_json::from_str(r#"{"cmd":"pan_mode","mode":3}"#).unwrap();
match cmd {
IpcCommand::PanMode { mode } => assert_eq!(mode, 3),
other => panic!("expected PanMode, got {other:?}"),
}
}
#[test]
fn deserialize_standby_uses_enabled_field() {
let cmd: IpcCommand = serde_json::from_str(r#"{"cmd":"standby","enabled":true}"#).unwrap();
match cmd {
IpcCommand::Standby { enabled } => assert!(enabled),
other => panic!("expected Standby, got {other:?}"),
}
}
#[test]
fn old_kebab_pan_mode_is_rejected() {
let result: Result<IpcCommand, _> = serde_json::from_str(r#"{"cmd":"pan-mode","mode":3}"#);
assert!(
result.is_err(),
"kebab-case pan-mode must not parse (got {result:?})"
);
}
#[test]
fn old_enable_field_is_rejected() {
let result: Result<IpcCommand, _> =
serde_json::from_str(r#"{"cmd":"standby","enable":true}"#);
assert!(
result.is_err(),
"old `enable` field must not parse (got {result:?})"
);
}
#[test]
fn serialize_pan_mode_round_trips_to_snake_case() {
let cmd = IpcCommand::PanMode { mode: 2 };
let s = serde_json::to_string(&cmd).unwrap();
assert!(
s.contains(r#""cmd":"pan_mode""#),
"expected snake_case discriminator, got {s}"
);
assert!(s.contains(r#""mode":2"#), "got {s}");
}
#[test]
fn serialize_standby_round_trips_to_enabled_field() {
let cmd = IpcCommand::Standby { enabled: false };
let s = serde_json::to_string(&cmd).unwrap();
assert!(s.contains(r#""cmd":"standby""#), "got {s}");
assert!(
s.contains(r#""enabled":false"#),
"expected `enabled`, got {s}"
);
}
#[test]
fn deserialize_calibrate_yaw() {
let cmd: IpcCommand = serde_json::from_str(r#"{"cmd":"calibrate_yaw"}"#).unwrap();
assert!(matches!(cmd, IpcCommand::CalibrateYaw));
}
#[test]
fn deserialize_set_yaw_offset() {
let cmd: IpcCommand =
serde_json::from_str(r#"{"cmd":"set_yaw_offset","deg":-4.5}"#).unwrap();
match cmd {
IpcCommand::SetYawOffset { deg } => assert!((deg - -4.5).abs() < 1e-6),
other => panic!("expected SetYawOffset, got {other:?}"),
}
}
#[test]
fn serialize_set_yaw_offset_uses_snake_case() {
let cmd = IpcCommand::SetYawOffset { deg: 1.25 };
let s = serde_json::to_string(&cmd).unwrap();
assert!(
s.contains(r#""cmd":"set_yaw_offset""#),
"expected snake_case discriminator, got {s}"
);
assert!(s.contains(r#""deg":1.25"#), "got {s}");
}
#[test]
fn deserialize_selftest() {
let cmd: IpcCommand = serde_json::from_str(r#"{"cmd":"selftest"}"#).unwrap();
assert!(matches!(cmd, IpcCommand::Selftest));
}
}