use embedded_hal::delay::DelayNs;
use embedded_hal::digital::OutputPin;
use heapless::{FnvIndexMap, String};
use crate::config::{MechanicalConstraints, MotorConfig, SystemConfig};
use crate::error::{ConfigError, Error, Result};
use crate::motor::state::Idle;
use crate::motor::{StepperMotor, StepperMotorBuilder};
use crate::trajectory::TrajectoryRegistry;
pub struct MotorSystem {
config: SystemConfig,
registry: TrajectoryRegistry,
registered_motors: FnvIndexMap<String<32>, MechanicalConstraints, 8>,
}
impl MotorSystem {
pub fn from_config(config: SystemConfig) -> Self {
let registry = TrajectoryRegistry::from_config(&config);
Self {
config,
registry,
registered_motors: FnvIndexMap::new(),
}
}
pub fn config(&self) -> &SystemConfig {
&self.config
}
pub fn trajectories(&self) -> &TrajectoryRegistry {
&self.registry
}
pub fn motor_config(&self, name: &str) -> Option<&MotorConfig> {
self.config.motor(name)
}
pub fn constraints(&self, name: &str) -> Option<MechanicalConstraints> {
self.config
.motor(name)
.map(MechanicalConstraints::from_config)
}
pub fn has_motor(&self, name: &str) -> bool {
self.config.motor(name).is_some()
}
pub fn motor_names(&self) -> impl Iterator<Item = &str> {
self.config.motor_names()
}
pub fn register_motor<STEP, DIR, DELAY>(
&mut self,
name: &str,
step_pin: STEP,
dir_pin: DIR,
delay: DELAY,
) -> Result<StepperMotor<STEP, DIR, DELAY, Idle>>
where
STEP: OutputPin,
DIR: OutputPin,
DELAY: DelayNs,
{
let motor_config = self.config.motor(name).ok_or_else(|| {
Error::Config(ConfigError::MotorNotFound(
String::try_from(name).unwrap_or_default(),
))
})?;
let constraints = MechanicalConstraints::from_config(motor_config);
let motor_name: String<32> = String::try_from(name).unwrap_or_default();
let _ = self.registered_motors.insert(motor_name, constraints);
StepperMotorBuilder::new()
.step_pin(step_pin)
.dir_pin(dir_pin)
.delay(delay)
.from_motor_config(motor_config)
.build()
}
pub fn build_motor<STEP, DIR, DELAY>(
&self,
name: &str,
step_pin: STEP,
dir_pin: DIR,
delay: DELAY,
) -> Result<StepperMotor<STEP, DIR, DELAY, Idle>>
where
STEP: OutputPin,
DIR: OutputPin,
DELAY: DelayNs,
{
let motor_config = self.config.motor(name).ok_or_else(|| {
Error::Config(ConfigError::MotorNotFound(
String::try_from(name).unwrap_or_default(),
))
})?;
StepperMotorBuilder::new()
.step_pin(step_pin)
.dir_pin(dir_pin)
.delay(delay)
.from_motor_config(motor_config)
.build()
}
pub fn is_registered(&self, name: &str) -> bool {
self.registered_motors
.iter()
.any(|(k, _)| k.as_str() == name)
}
pub fn registered_count(&self) -> usize {
self.registered_motors.len()
}
pub fn registered_constraints(&self, name: &str) -> Option<&MechanicalConstraints> {
self.registered_motors
.iter()
.find(|(k, _)| k.as_str() == name)
.map(|(_, v)| v)
}
pub fn trajectory(&self, name: &str) -> Result<&crate::config::TrajectoryConfig> {
self.registry.get_or_error(name)
}
pub fn trajectories_for_motor<'a>(
&'a self,
motor_name: &'a str,
) -> impl Iterator<Item = &'a str> + 'a {
self.registry
.iter()
.filter(move |(_, traj)| traj.motor.as_str() == motor_name)
.map(|(name, _)| name)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn test_config() -> SystemConfig {
let toml = r#"
[motors.x_axis]
name = "X Axis"
steps_per_revolution = 200
microsteps = 16
max_velocity_deg_per_sec = 360.0
max_acceleration_deg_per_sec2 = 720.0
[motors.y_axis]
name = "Y Axis"
steps_per_revolution = 400
microsteps = 8
max_velocity_deg_per_sec = 180.0
max_acceleration_deg_per_sec2 = 360.0
[trajectories.home_x]
motor = "x_axis"
target_degrees = 0.0
velocity_percent = 50
[trajectories.home_y]
motor = "y_axis"
target_degrees = 0.0
velocity_percent = 50
"#;
toml::from_str(toml).unwrap()
}
#[test]
fn test_motor_system_creation() {
let config = test_config();
let system = MotorSystem::from_config(config);
assert!(system.has_motor("x_axis"));
assert!(system.has_motor("y_axis"));
assert!(!system.has_motor("z_axis"));
}
#[test]
fn test_motor_names() {
let config = test_config();
let system = MotorSystem::from_config(config);
let names: Vec<_> = system.motor_names().collect();
assert!(names.contains(&"x_axis"));
assert!(names.contains(&"y_axis"));
}
#[test]
fn test_constraints_lookup() {
let config = test_config();
let system = MotorSystem::from_config(config);
let constraints = system.constraints("x_axis").unwrap();
assert_eq!(constraints.steps_per_revolution, 3200);
let constraints = system.constraints("y_axis").unwrap();
assert_eq!(constraints.steps_per_revolution, 3200);
}
#[test]
fn test_trajectories_for_motor() {
let config = test_config();
let system = MotorSystem::from_config(config);
let x_trajectories: Vec<_> = system.trajectories_for_motor("x_axis").collect();
assert!(x_trajectories.contains(&"home_x"));
assert!(!x_trajectories.contains(&"home_y"));
let y_trajectories: Vec<_> = system.trajectories_for_motor("y_axis").collect();
assert!(y_trajectories.contains(&"home_y"));
assert!(!y_trajectories.contains(&"home_x"));
}
#[test]
fn test_trajectory_lookup() {
let config = test_config();
let system = MotorSystem::from_config(config);
let traj = system.trajectory("home_x");
assert!(traj.is_ok());
assert_eq!(traj.unwrap().motor.as_str(), "x_axis");
let traj = system.trajectory("nonexistent");
assert!(traj.is_err());
}
}