use crate::device::{Device, Error, Result, WhoAmI};
use core::slice;
use core::time::Duration;
use low_level::{
CMD_CONTROLLED_MOTOR_SPEED, CMD_COUNTERS, CMD_ENCODER_TICKS, CMD_FIRMWARE_FEATURES,
CMD_MOTOR_SHUTDOWN_TIMEOUT, CMD_PID_I_ACC, CMD_PID_K_D, CMD_PID_K_I, CMD_PID_K_P,
CMD_PWM_FREQUENCY, CMD_RAW_ENCODER_TICKS, CMD_RAW_MOTOR_SPEED, CMD_STATUS,
};
#[cfg(feature = "float")]
#[allow(unused)] use micromath::F32Ext as _;
pub const REQUIRED_FIRMWARE_VERSION: (u8, u8, u8) = (1, 2, 0);
pub trait Controller: Device {
fn check_firmware_version(&mut self) -> Result<(u8, u8, u8), Self::I2cError> {
if !matches!(self.who_am_i()?, WhoAmI::Controller) {
return Err(Error::UnknownFirmwareFound);
};
let version = self.firmware_version()?;
if REQUIRED_FIRMWARE_VERSION.0 != version.0
|| REQUIRED_FIRMWARE_VERSION.1 > version.1
|| (REQUIRED_FIRMWARE_VERSION.1 == version.1 && REQUIRED_FIRMWARE_VERSION.2 > version.2)
{
Err(Error::InvalidVersion(version))
} else {
Ok(version)
}
}
fn set_pwm_frequency(&mut self, frequency_hz: u32) -> Result<(), Self::I2cError> {
if frequency_hz > 100_000 {
return Err(Error::InvalidFrequency(frequency_hz));
}
self.write((u32::from(CMD_PWM_FREQUENCY) + (frequency_hz << 8)).to_le_bytes())
}
fn get_pwm_frequency(&mut self) -> Result<u32, Self::I2cError> {
let mut data = [0; 4];
self.write_read([CMD_PWM_FREQUENCY], &mut data[..3])?;
Ok(u32::from_le_bytes(data))
}
fn set_raw_pid_coefficients(
&mut self,
k_p: i32,
k_i: i32,
k_d: i32,
) -> Result<(), Self::I2cError> {
let mut buf = [0; 5];
for (command, data) in [(CMD_PID_K_P, k_p), (CMD_PID_K_I, k_i), (CMD_PID_K_D, k_d)] {
buf[0] = command;
buf[1..5].copy_from_slice(&data.to_le_bytes());
self.write(buf)?;
}
Ok(())
}
fn get_raw_pid_coefficients(&mut self) -> Result<(i32, i32, i32), Self::I2cError> {
let mut result = [0i32; 3];
let mut buf = [0; 4];
for (command, data) in [CMD_PID_K_P, CMD_PID_K_I, CMD_PID_K_D]
.into_iter()
.zip(result.iter_mut())
{
self.write_read([command], &mut buf)?;
*data = i32::from_le_bytes(buf);
}
Ok(result.into())
}
fn get_raw_pid_i_accumulator(&mut self) -> Result<(i32, i32), Self::I2cError> {
let mut buf = [0; 8];
self.write_read([CMD_PID_I_ACC], &mut buf)?;
Ok((
i32::from_le_bytes(buf[..4].try_into().unwrap()),
i32::from_le_bytes(buf[4..].try_into().unwrap()),
))
}
#[cfg(feature = "float")]
fn set_pid_coefficients(&mut self, k_p: f32, k_i: f32, k_d: f32) -> Result<(), Self::I2cError> {
self.set_raw_pid_coefficients(to_i32(k_p), to_i32(k_i), to_i32(k_d))
}
#[cfg(feature = "float")]
fn get_pid_coefficients(&mut self) -> Result<(f32, f32, f32), Self::I2cError> {
let (k_p, k_i, k_d) = self.get_raw_pid_coefficients()?;
Ok((to_f32(k_p), to_f32(k_i), to_f32(k_d)))
}
#[cfg(feature = "float")]
fn get_pid_i_accumulator(&mut self) -> Result<(f32, f32), Self::I2cError> {
let (left, right) = self.get_raw_pid_i_accumulator()?;
Ok((to_f32(left), to_f32(right)))
}
fn set_motor_shutdown_timeout(&mut self, delay: Duration) -> Result<(), Self::I2cError> {
let timeout = delay.as_millis();
if timeout > 10_000 {
return Err(Error::InvalidDuration(delay));
}
let timeout = u8::try_from((timeout + 50) / 100).unwrap();
self.write([CMD_MOTOR_SHUTDOWN_TIMEOUT, timeout])
}
fn get_motor_shutdown_timeout(&mut self) -> Result<Duration, Self::I2cError> {
let mut delay = 0;
self.write_read([CMD_MOTOR_SHUTDOWN_TIMEOUT], slice::from_mut(&mut delay))?;
Ok(Duration::from_millis(u64::from(delay) * 100))
}
#[expect(clippy::cast_sign_loss)]
fn set_raw_motor_speed(
&mut self,
left: Option<i8>,
right: Option<i8>,
) -> Result<(), Self::I2cError> {
let (left, right) = (left.map(|v| v as u8), right.map(|v| v as u8));
if left == Some(0x80) || right == Some(0x80) {
return Err(Error::InvalidRawSpeed);
}
self.write([
CMD_RAW_MOTOR_SPEED,
left.unwrap_or(0x80),
right.unwrap_or(0x80),
])
}
#[expect(clippy::cast_possible_wrap)]
fn get_raw_motor_speed(&mut self) -> Result<Option<(i8, i8)>, Self::I2cError> {
let mut buf = [0; 2];
self.write_read([CMD_RAW_MOTOR_SPEED], &mut buf)?;
match buf {
[0x80, 0x80] => Ok(None),
[0x80, _] | [_, 0x80] => Err(Error::InvalidRawSpeed),
[left, right] => Ok(Some((left as i8, right as i8))),
}
}
fn standby(&mut self) -> Result<(), Self::I2cError> {
self.set_raw_motor_speed(None, None)
}
fn set_motor_speed(&mut self, left: i16, right: i16) -> Result<(), Self::I2cError> {
let mut buf = [CMD_CONTROLLED_MOTOR_SPEED, 0, 0, 0, 0];
buf[1..3].copy_from_slice(&left.to_le_bytes());
buf[3..5].copy_from_slice(&right.to_le_bytes());
self.write(buf)
}
fn get_motor_speed(&mut self) -> Result<Option<(i16, i16)>, Self::I2cError> {
let mut buf = [0; 4];
self.write_read([CMD_CONTROLLED_MOTOR_SPEED], &mut buf)?;
Ok((buf != [0x80, 0x00, 0x80, 0x00]).then(|| {
(
i16::from_le_bytes(buf[0..2].try_into().unwrap()),
i16::from_le_bytes(buf[2..4].try_into().unwrap()),
)
}))
}
#[deprecate_until::deprecate_until(
remove = ">= 2.x",
note = "use `Controller::get_relative_encoder_ticks()` instead"
)]
fn get_encoder_ticks(&mut self) -> Result<(i16, i16), Self::I2cError> {
let mut buf = [0; 4];
self.write_read([CMD_ENCODER_TICKS], &mut buf)?;
Ok((
i16::from_le_bytes(buf[0..2].try_into().unwrap()),
i16::from_le_bytes(buf[2..4].try_into().unwrap()),
))
}
fn get_raw_encoder_ticks(&mut self) -> Result<(u16, u16), Self::I2cError> {
let mut buf = [0; 4];
self.write_read([CMD_RAW_ENCODER_TICKS], &mut buf)?;
Ok((
u16::from_le_bytes(buf[0..2].try_into().unwrap()),
u16::from_le_bytes(buf[2..4].try_into().unwrap()),
))
}
fn new_relative(&mut self) -> Result<RelativeEncoders, Self::I2cError> {
let (latest_left, latest_right) = self.get_raw_encoder_ticks()?;
Ok(RelativeEncoders {
latest_left,
latest_right,
})
}
#[expect(clippy::cast_possible_wrap)]
fn get_relative_encoder_ticks(
&mut self,
relative: &mut RelativeEncoders,
) -> Result<(i16, i16), Self::I2cError> {
let (new_left, new_right) = self.get_raw_encoder_ticks()?;
let (left, right) = (
new_left.wrapping_sub(relative.latest_left) as i16,
new_right.wrapping_sub(relative.latest_right) as i16,
);
*relative = RelativeEncoders {
latest_left: new_left,
latest_right: new_right,
};
Ok((left, right))
}
fn get_status(&mut self) -> Result<Status, Self::I2cError> {
let mut status = 0;
self.write_read([CMD_STATUS], slice::from_mut(&mut status))?;
Ok(Status(status))
}
fn get_counters(&mut self) -> Result<Counters, Self::I2cError> {
let mut counters = Counters::default();
self.write_read([CMD_COUNTERS], counters.as_mut())?;
Ok(counters)
}
fn firmware_features(&mut self) -> Result<FirmwareFeatures, Self::I2cError> {
let mut features = 0;
self.write_read([CMD_FIRMWARE_FEATURES], slice::from_mut(&mut features))?;
Ok(FirmwareFeatures(features))
}
}
impl<D: Device> Controller for D {}
#[derive(Clone, Copy)]
pub struct Status(u8);
impl Status {
#[must_use]
pub fn is_moving(self) -> bool {
self.0 & 1 != 0
}
#[must_use]
pub fn is_controlled(self) -> bool {
self.0 & 2 != 0
}
}
#[derive(Clone, Copy)]
pub struct FirmwareFeatures(u8);
impl FirmwareFeatures {
#[must_use]
pub fn has_bootloader_support(self) -> bool {
self.0 & 1 != 0
}
}
#[repr(C)]
#[derive(Clone, Default)]
pub struct Counters {
pub btf: u8,
pub unknown_command: u8,
pub incorrect_processing: u8,
pub emergency_stops: u8,
}
impl AsMut<[u8; 4]> for Counters {
fn as_mut(&mut self) -> &mut [u8; 4] {
unsafe { (&raw mut *self).cast::<[u8; 4]>().as_mut().unwrap() }
}
}
pub struct RelativeEncoders {
latest_left: u16,
latest_right: u16,
}
pub const PID_FRACTIONAL_BITS: usize = 8;
#[cfg(feature = "float")]
#[expect(clippy::cast_precision_loss)]
fn to_f32(v: i32) -> f32 {
v as f32 / (1 << PID_FRACTIONAL_BITS) as f32
}
#[cfg(feature = "float")]
#[expect(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
fn to_i32(v: f32) -> i32 {
(v * (1 << PID_FRACTIONAL_BITS) as f32).round() as i32
}
pub(crate) mod low_level {
pub const CMD_PWM_FREQUENCY: u8 = 0x10;
pub const CMD_PID_K_P: u8 = 0x20;
pub const CMD_PID_K_I: u8 = 0x21;
pub const CMD_PID_K_D: u8 = 0x22;
pub const CMD_PID_I_ACC: u8 = 0x26;
pub const CMD_MOTOR_SHUTDOWN_TIMEOUT: u8 = 0x28;
pub const CMD_RAW_MOTOR_SPEED: u8 = 0x30;
pub const CMD_CONTROLLED_MOTOR_SPEED: u8 = 0x31;
pub const CMD_ENCODER_TICKS: u8 = 0x32;
pub const CMD_RAW_ENCODER_TICKS: u8 = 0x33;
pub const CMD_STATUS: u8 = 0x36;
pub const CMD_COUNTERS: u8 = 0x38;
pub const CMD_FIRMWARE_FEATURES: u8 = 0xfe;
}