kcan 0.1.4

CAN controller primitives for actuator and motor control.
Documentation
use std::time::{Duration, Instant};

use crate::bus::{CanBus, CanFrame, CanId};
use crate::error::{Error, Result};
use crate::protocol::{
    DEFAULT_CONTROL_RATE_HZ, DEFAULT_MOTOR_ID, DirectCommand, FeedbackFilter, MitCommand,
    MitHelperCommand, MotorFeedback, MotorModel, OriginMode, clamp_erpm, degrees_to_radians,
    direct_command_frame, erpm_to_rad_s, mit_command_frame, mit_helper_frame,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum HelperPolicy {
    Strict,
    FcFd,
    Legacy,
}

#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MotorConfig {
    pub motor_id: u8,
    pub model: MotorModel,
    pub feedback_id: Option<CanId>,
    pub helper_policy: HelperPolicy,
    pub allow_legacy_feedback_ids: bool,
    pub control_rate_hz: f64,
    pub mit_velocity_kd: Option<f64>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefreshStats {
    pub iterations: u64,
    pub feedback_frames: u64,
    pub missed_feedback: u64,
    pub expected_period: Duration,
    pub elapsed: Duration,
}

impl RefreshStats {
    pub const fn new(expected_period: Duration) -> Self {
        Self {
            iterations: 0,
            feedback_frames: 0,
            missed_feedback: 0,
            expected_period,
            elapsed: Duration::ZERO,
        }
    }

    pub fn effective_hz(&self) -> Option<f64> {
        if self.iterations == 0 || self.elapsed.is_zero() {
            return None;
        }
        Some(self.iterations as f64 / self.elapsed.as_secs_f64())
    }

    pub fn feedback_ratio(&self) -> Option<f64> {
        if self.iterations == 0 {
            return None;
        }
        Some(self.feedback_frames as f64 / self.iterations as f64)
    }
}

impl MotorConfig {
    pub fn feedback_filter(&self) -> FeedbackFilter {
        let mut filter = FeedbackFilter::for_motor(self.model, self.motor_id);
        if let Some(feedback_id) = self.feedback_id {
            filter = filter.with_explicit_id(feedback_id);
        }
        if self.allow_legacy_feedback_ids {
            filter = filter.with_legacy_ids(self.motor_id);
        }
        filter
    }

    pub fn refresh_period(&self) -> Result<Duration> {
        if self.control_rate_hz <= 0.0 || !self.control_rate_hz.is_finite() {
            return Err(Error::InvalidRefreshRate(self.control_rate_hz));
        }
        Ok(Duration::from_secs_f64(1.0 / self.control_rate_hz))
    }

    pub fn velocity_kd(&self) -> f64 {
        self.mit_velocity_kd
            .unwrap_or_else(|| self.model.spec().mit_velocity_kd)
    }
}

impl Default for MotorConfig {
    fn default() -> Self {
        Self {
            motor_id: DEFAULT_MOTOR_ID,
            model: MotorModel::Ak60_6,
            feedback_id: None,
            helper_policy: HelperPolicy::FcFd,
            allow_legacy_feedback_ids: false,
            control_rate_hz: DEFAULT_CONTROL_RATE_HZ,
            mit_velocity_kd: None,
        }
    }
}

#[derive(Debug)]
pub struct MotorController<B> {
    bus: B,
    config: MotorConfig,
    feedback_filter: FeedbackFilter,
    last_feedback: Option<MotorFeedback>,
    active_command: Option<MitCommand>,
    mit_enabled: bool,
}

impl<B: CanBus> MotorController<B> {
    pub fn new(bus: B, config: MotorConfig) -> Result<Self> {
        config.refresh_period()?;
        Ok(Self {
            feedback_filter: config.feedback_filter(),
            bus,
            config,
            last_feedback: None,
            active_command: None,
            mit_enabled: false,
        })
    }

    pub fn config(&self) -> &MotorConfig {
        &self.config
    }

    pub fn bus(&self) -> &B {
        &self.bus
    }

    pub fn bus_mut(&mut self) -> &mut B {
        &mut self.bus
    }

    pub fn into_inner(self) -> B {
        self.bus
    }

    pub fn last_feedback(&self) -> Option<&MotorFeedback> {
        self.last_feedback.as_ref()
    }

    pub const fn mit_enabled(&self) -> bool {
        self.mit_enabled
    }

    pub fn active_command(&self) -> Option<MitCommand> {
        self.active_command
    }

    pub fn mit_frame(&self, command: MitCommand) -> Result<CanFrame> {
        mit_command_frame(self.config.model, self.config.motor_id, command)
    }

    pub fn helper_frame(&self, helper: MitHelperCommand) -> Result<CanFrame> {
        mit_helper_frame(self.config.model, self.config.motor_id, helper)
    }

    pub fn direct_frame(&self, command: DirectCommand) -> Result<CanFrame> {
        direct_command_frame(self.config.motor_id, command)
    }

    pub fn enable_mit_mode(&mut self) -> Result<()> {
        match self.config.helper_policy {
            HelperPolicy::Strict => {}
            HelperPolicy::FcFd => self.send_helper(MitHelperCommand::Enable)?,
            HelperPolicy::Legacy => {
                self.send_helper(MitHelperCommand::Enable)?;
                self.send_helper(MitHelperCommand::EnableLegacy)?;
            }
        }

        self.send_mit_frame(MitCommand::neutral())?;
        self.mit_enabled = true;
        Ok(())
    }

    pub fn disable_mit_mode(&mut self) -> Result<()> {
        for _ in 0..3 {
            self.send_mit_frame(MitCommand::neutral())?;
        }

        match self.config.helper_policy {
            HelperPolicy::Strict => {}
            HelperPolicy::FcFd => self.send_helper(MitHelperCommand::Disable)?,
            HelperPolicy::Legacy => {
                self.send_helper(MitHelperCommand::Disable)?;
                self.send_helper(MitHelperCommand::DisableLegacy)?;
            }
        }

        self.active_command = None;
        self.mit_enabled = false;
        Ok(())
    }

    pub fn zero_position(&mut self) -> Result<()> {
        self.send_helper(MitHelperCommand::ZeroPosition)?;
        self.last_feedback = None;
        Ok(())
    }

    pub fn set_mit_mode(&mut self, mut command: MitCommand) -> Result<()> {
        if !self.mit_enabled {
            self.enable_mit_mode()?;
        }

        let spec = self.config.model.spec();
        let requested_erpm = crate::protocol::rad_s_to_erpm(command.velocity_rad_s, spec);
        let clamped_erpm = clamp_erpm(requested_erpm, spec);
        command.velocity_rad_s = erpm_to_rad_s(clamped_erpm, spec);

        self.send_mit_frame(command)?;
        self.active_command = Some(command);
        Ok(())
    }

    pub fn set_velocity_erpm(&mut self, velocity_erpm: i32) -> Result<()> {
        if velocity_erpm == 0 {
            return self.stop();
        }

        let spec = self.config.model.spec();
        let clamped_erpm = clamp_erpm(velocity_erpm, spec);
        self.set_mit_mode(MitCommand {
            position_rad: 0.0,
            velocity_rad_s: erpm_to_rad_s(clamped_erpm, spec),
            kp: 0.0,
            kd: self.config.velocity_kd(),
            torque_nm: 0.0,
        })
    }

    pub fn set_position_degrees(&mut self, position_degrees: f64) -> Result<()> {
        let spec = self.config.model.spec();
        self.set_mit_mode(MitCommand {
            position_rad: degrees_to_radians(position_degrees),
            velocity_rad_s: 0.0,
            kp: spec.mit_position_kp,
            kd: spec.mit_position_kd,
            torque_nm: 0.0,
        })
    }

    pub fn set_torque_nm(&mut self, torque_nm: f64) -> Result<()> {
        self.set_mit_mode(MitCommand {
            position_rad: 0.0,
            velocity_rad_s: 0.0,
            kp: 0.0,
            kd: 0.0,
            torque_nm,
        })
    }

    pub fn send_direct_command(&mut self, command: DirectCommand) -> Result<()> {
        let frame = self.direct_frame(command)?;
        self.bus.send(&frame)
    }

    pub fn set_duty_cycle(&mut self, duty: f64) -> Result<()> {
        self.send_direct_command(DirectCommand::DutyCycle(duty))
    }

    pub fn set_current_amps(&mut self, current_amps: f64) -> Result<()> {
        self.send_direct_command(DirectCommand::Current(current_amps))
    }

    pub fn set_brake_current_amps(&mut self, current_amps: f64) -> Result<()> {
        self.send_direct_command(DirectCommand::BrakeCurrent(current_amps))
    }

    pub fn set_direct_velocity_erpm(&mut self, velocity_erpm: i32) -> Result<()> {
        self.send_direct_command(DirectCommand::Velocity(velocity_erpm))
    }

    pub fn set_direct_position_degrees(&mut self, position_degrees: f64) -> Result<()> {
        self.send_direct_command(DirectCommand::Position(position_degrees))
    }

    pub fn set_origin(&mut self, origin_mode: OriginMode) -> Result<()> {
        self.send_direct_command(DirectCommand::SetOrigin(origin_mode))
    }

    pub fn send_keepalive(&mut self) -> Result<()> {
        let command = self.active_command.unwrap_or_else(MitCommand::neutral);
        self.send_mit_frame(command)
    }

    pub fn refresh_once(&mut self, feedback_timeout: Duration) -> Result<Option<MotorFeedback>> {
        self.send_keepalive()?;
        self.receive_feedback(feedback_timeout)
    }

    pub fn refresh_for(
        &mut self,
        duration: Duration,
        feedback_timeout: Duration,
    ) -> Result<RefreshStats> {
        let period = self.config.refresh_period()?;
        let started = Instant::now();
        let mut stats = RefreshStats::new(period);

        while started.elapsed() < duration {
            let loop_started = Instant::now();
            match self.refresh_once(feedback_timeout)? {
                Some(_) => stats.feedback_frames += 1,
                None => stats.missed_feedback += 1,
            }
            stats.iterations += 1;

            let loop_elapsed = loop_started.elapsed();
            if loop_elapsed < period {
                std::thread::sleep(period - loop_elapsed);
            }
        }

        stats.elapsed = started.elapsed();
        Ok(stats)
    }

    pub fn receive_feedback(&mut self, timeout: Duration) -> Result<Option<MotorFeedback>> {
        let deadline = Instant::now() + timeout;

        loop {
            let now = Instant::now();
            if now >= deadline {
                return Ok(None);
            }

            let Some(frame) = self.bus.receive(deadline.saturating_duration_since(now))? else {
                return Ok(None);
            };

            if !self.feedback_filter.matches(&frame) {
                continue;
            }

            let feedback = self.config.model.parse_feedback(frame.data())?;
            self.last_feedback = Some(feedback.clone());
            return Ok(Some(feedback));
        }
    }

    pub fn check_communication(&mut self, timeout: Duration) -> Result<bool> {
        self.enable_mit_mode()?;
        self.send_keepalive()?;
        Ok(self.receive_feedback(timeout)?.is_some())
    }

    pub fn stop(&mut self) -> Result<()> {
        for _ in 0..4 {
            self.send_mit_frame(MitCommand::neutral())?;
        }

        self.disable_mit_mode()
    }

    fn send_mit_frame(&mut self, command: MitCommand) -> Result<()> {
        let frame = self.mit_frame(command)?;
        self.bus.send(&frame)
    }

    fn send_helper(&mut self, helper: MitHelperCommand) -> Result<()> {
        let frame = self.helper_frame(helper)?;
        self.bus.send(&frame)
    }
}