use crate::messages::Message;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TeleopCommand {
Drivetrain {
linear: f32,
angular: f32,
},
Actuator {
names: Vec<String>,
positions: Vec<f32>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeleopInput {
pub source: String,
pub priority: u8,
pub command: TeleopCommand,
pub timestamp: u64,
pub emergency_stop: bool,
}
impl Message for TeleopInput {}
impl TeleopInput {
pub fn new(source: impl Into<String>, linear: f32, angular: f32, timestamp: u64) -> Self {
Self {
source: source.into(),
priority: 50,
command: TeleopCommand::Drivetrain { linear, angular },
timestamp,
emergency_stop: false,
}
}
pub fn new_actuator(source: impl Into<String>, names: Vec<String>, positions: Vec<f32>, timestamp: u64) -> Self {
Self {
source: source.into(),
priority: 50,
command: TeleopCommand::Actuator { names, positions },
timestamp,
emergency_stop: false,
}
}
pub fn emergency_stop(source: impl Into<String>, timestamp: u64) -> Self {
Self {
source: source.into(),
priority: 255,
command: TeleopCommand::Drivetrain {
linear: 0.0,
angular: 0.0,
},
timestamp,
emergency_stop: true,
}
}
pub fn with_priority(mut self, priority: u8) -> Self {
self.priority = priority;
self
}
pub fn age_ms(&self, current_time: u64) -> u64 {
let age_us = current_time.saturating_sub(self.timestamp);
age_us / 1000
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeleopStatus {
pub active_source: String,
pub last_command_age_ms: u64,
pub safety_engaged: bool,
pub mode: String,
pub current_linear: f32,
pub current_angular: f32,
pub current_positions: Vec<(String, f32)>,
#[serde(default)]
pub joint_names: Vec<String>,
#[serde(default)]
pub joint_limits: Vec<(String, f32, f32)>,
pub total_commands: u64,
pub active_priority: u8,
pub max_linear_vel: f32,
pub max_angular_vel: f32,
}
impl Message for TeleopStatus {}
impl TeleopStatus {
pub fn new() -> Self {
Self {
active_source: "none".to_string(),
last_command_age_ms: 0,
safety_engaged: false,
mode: "drivetrain".to_string(),
current_linear: 0.0,
current_angular: 0.0,
current_positions: Vec::new(),
joint_names: Vec::new(),
joint_limits: Vec::new(),
total_commands: 0,
active_priority: 0,
max_linear_vel: 0.0,
max_angular_vel: 0.0,
}
}
pub fn is_active(&self) -> bool {
!self.safety_engaged && self.active_source != "none"
}
}
impl Default for TeleopStatus {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_builds_a_drivetrain_command() {
let input = TeleopInput::new("gamepad", 1.0, -0.5, 123);
assert_eq!(input.source, "gamepad");
assert_eq!(input.priority, 50);
assert_eq!(input.timestamp, 123);
assert!(!input.emergency_stop);
match input.command {
TeleopCommand::Drivetrain { linear, angular } => {
assert_eq!(linear, 1.0);
assert_eq!(angular, -0.5);
}
TeleopCommand::Actuator { .. } => panic!("expected a Drivetrain command"),
}
}
#[test]
fn new_actuator_builds_an_actuator_command() {
let input = TeleopInput::new_actuator(
"dashboard",
vec!["shoulder".to_string(), "elbow".to_string()],
vec![0.1, 0.2],
456,
);
match input.command {
TeleopCommand::Actuator { names, positions } => {
assert_eq!(names, vec!["shoulder".to_string(), "elbow".to_string()]);
assert_eq!(positions, vec![0.1, 0.2]);
}
TeleopCommand::Drivetrain { .. } => panic!("expected an Actuator command"),
}
}
#[test]
fn emergency_stop_is_a_zeroed_drivetrain_command_with_max_priority() {
let input = TeleopInput::emergency_stop("safety", 789);
assert_eq!(input.priority, 255);
assert!(input.emergency_stop);
match input.command {
TeleopCommand::Drivetrain { linear, angular } => {
assert_eq!(linear, 0.0);
assert_eq!(angular, 0.0);
}
TeleopCommand::Actuator { .. } => panic!("expected a Drivetrain command"),
}
}
#[test]
fn with_priority_overrides_the_default() {
let input = TeleopInput::new("gamepad", 0.0, 0.0, 0).with_priority(10);
assert_eq!(input.priority, 10);
}
#[test]
fn age_ms_converts_microseconds_to_milliseconds() {
let input = TeleopInput::new("gamepad", 0.0, 0.0, 1_000);
assert_eq!(input.age_ms(5_000), 4);
}
#[test]
fn teleop_command_round_trips_through_json() {
let drivetrain = TeleopCommand::Drivetrain {
linear: 1.0,
angular: 2.0,
};
let json = serde_json::to_string(&drivetrain).expect("serializable");
let parsed: TeleopCommand = serde_json::from_str(&json).expect("deserializable");
match parsed {
TeleopCommand::Drivetrain { linear, angular } => {
assert_eq!(linear, 1.0);
assert_eq!(angular, 2.0);
}
TeleopCommand::Actuator { .. } => panic!("expected a Drivetrain command"),
}
let actuator = TeleopCommand::Actuator {
names: vec!["wrist".to_string()],
positions: vec![0.75],
};
let json = serde_json::to_string(&actuator).expect("serializable");
let parsed: TeleopCommand = serde_json::from_str(&json).expect("deserializable");
match parsed {
TeleopCommand::Actuator { names, positions } => {
assert_eq!(names, vec!["wrist".to_string()]);
assert_eq!(positions, vec![0.75]);
}
TeleopCommand::Drivetrain { .. } => panic!("expected an Actuator command"),
}
}
#[test]
fn status_default_is_drivetrain_mode_with_no_positions() {
let status = TeleopStatus::new();
assert_eq!(status.mode, "drivetrain");
assert!(status.current_positions.is_empty());
assert_eq!(status.active_source, "none");
assert!(!status.is_active());
}
}