pub mod calibration;
pub mod messages;
use crate::calibration::{CalibrationData, Units};
use crate::messages::{JointPositions, MAX_SERVOS};
use cu_linux_resources::LinuxSerialPort;
use cu29::cubridge::{
BridgeChannel, BridgeChannelConfig, BridgeChannelInfo, BridgeChannelSet, CuBridge,
};
use cu29::prelude::*;
use cu29::resources;
use heapless::Vec as HeaplessVec;
use std::io::{self, Read, Write};
const HEADER: [u8; 2] = [0xFF, 0xFF];
const MAX_PACKET_SIZE: usize = 32;
const MAX_STATUS_PACKET_SIZE: usize = 64;
#[allow(dead_code)]
const BROADCAST_ID: u8 = 0xFE;
#[allow(dead_code)]
mod instr {
pub const PING: u8 = 0x01;
pub const READ: u8 = 0x02;
pub const WRITE: u8 = 0x03;
pub const REG_WRITE: u8 = 0x04;
pub const ACTION: u8 = 0x05;
pub const SYNC_WRITE: u8 = 0x83;
pub const RESET: u8 = 0x06;
}
#[allow(dead_code)]
mod reg {
pub const MODEL_NUMBER: u8 = 3; pub const ID: u8 = 5; pub const BAUD_RATE: u8 = 6; pub const MIN_ANGLE_LIMIT: u8 = 9; pub const MAX_ANGLE_LIMIT: u8 = 11;
pub const TORQUE_ENABLE: u8 = 40; pub const GOAL_POSITION: u8 = 42; pub const GOAL_TIME: u8 = 44; pub const GOAL_SPEED: u8 = 46; pub const PRESENT_POSITION: u8 = 56; pub const PRESENT_SPEED: u8 = 58; pub const PRESENT_LOAD: u8 = 60; pub const PRESENT_VOLTAGE: u8 = 62; pub const PRESENT_TEMPERATURE: u8 = 63; pub const MOVING: u8 = 66; }
#[inline]
fn compute_checksum(data: &[u8]) -> u8 {
let mut sum: u8 = 0;
for &b in data {
sum = sum.wrapping_add(b);
}
!sum
}
rx_channels! {
positions => JointPositions
}
tx_channels! {
goal_positions => JointPositions
}
resources!({
serial => Owned<LinuxSerialPort>,
});
#[derive(Reflect)]
#[reflect(from_reflect = false)]
pub struct FeetechBridge {
#[reflect(ignore)]
port: LinuxSerialPort,
ids: [u8; MAX_SERVOS],
num_servos: u8,
has_writers: bool,
cached_positions: [u16; MAX_SERVOS],
#[reflect(ignore)]
units: Units,
centers: [f32; MAX_SERVOS],
#[reflect(ignore)]
ticks_per_rev: u32,
#[reflect(ignore)]
half_ranges: [f32; MAX_SERVOS],
}
impl Freezable for FeetechBridge {
}
impl FeetechBridge {
fn send_packet(&mut self, id: u8, instruction: u8, params: &[u8]) -> io::Result<()> {
let length = (params.len() + 2) as u8;
let packet_size = 2 + 1 + 1 + 1 + params.len() + 1; if packet_size > MAX_PACKET_SIZE {
return Err(io::Error::other("Feetech: packet too large"));
}
let mut packet = [0u8; MAX_PACKET_SIZE];
packet[0..2].copy_from_slice(&HEADER);
packet[2] = id;
packet[3] = length;
packet[4] = instruction;
packet[5..5 + params.len()].copy_from_slice(params);
let checksum = compute_checksum(&packet[2..5 + params.len()]);
packet[5 + params.len()] = checksum;
self.port.write_all(&packet[..packet_size])?;
self.port.flush()?;
Ok(())
}
fn read_status_packet(
&mut self,
) -> io::Result<(u8, u8, HeaplessVec<u8, MAX_STATUS_PACKET_SIZE>)> {
let mut header = [0u8; 4];
self.port.read_exact(&mut header)?;
if header[0] != 0xFF || header[1] != 0xFF {
return Err(io::Error::other("Feetech: invalid response header"));
}
let id = header[2];
let length = header[3] as usize;
if length < 2 {
return Err(io::Error::other("Feetech: response length too short"));
}
if length > MAX_STATUS_PACKET_SIZE {
return Err(io::Error::other("Feetech: response packet too large"));
}
let mut remaining = [0u8; MAX_STATUS_PACKET_SIZE];
self.port.read_exact(&mut remaining[..length])?;
let received_checksum = remaining[length - 1];
let mut checksum_sum: u8 = id;
checksum_sum = checksum_sum.wrapping_add(length as u8);
for &b in &remaining[..length - 1] {
checksum_sum = checksum_sum.wrapping_add(b);
}
let expected_checksum = !checksum_sum;
if received_checksum != expected_checksum {
return Err(io::Error::other("Feetech: checksum mismatch"));
}
let error_byte = remaining[0];
let data_len = length - 2; let mut data = HeaplessVec::new();
data.extend_from_slice(&remaining[1..1 + data_len])
.map_err(|_| io::Error::other("Feetech: failed to create data vector"))?;
Ok((id, error_byte, data))
}
#[allow(dead_code)]
pub fn ping(&mut self, id: u8) -> CuResult<()> {
self.send_packet(id, instr::PING, &[])
.map_err(|e| CuError::new_with_cause("Feetech: ping write failed", e))?;
let (resp_id, error, _) = self
.read_status_packet()
.map_err(|e| CuError::new_with_cause("Feetech: ping read failed", e))?;
if error != 0 {
return Err(
format!("Feetech: servo {} returned error 0x{:02X}", resp_id, error).into(),
);
}
if resp_id != id {
return Err(format!("Feetech: ping expected ID {} but got {}", id, resp_id).into());
}
Ok(())
}
fn read_register(
&mut self,
id: u8,
address: u8,
count: u8,
) -> io::Result<HeaplessVec<u8, MAX_STATUS_PACKET_SIZE>> {
self.send_packet(id, instr::READ, &[address, count])?;
let (_id, _error, data) = self.read_status_packet()?;
Ok(data)
}
#[allow(dead_code)]
fn write_register(&mut self, id: u8, address: u8, data: &[u8]) -> io::Result<()> {
if 1 + data.len() > MAX_PACKET_SIZE - 5 {
return Err(io::Error::other("Feetech: write data too large"));
}
let mut params = [0u8; MAX_PACKET_SIZE - 5];
params[0] = address;
params[1..1 + data.len()].copy_from_slice(data);
self.send_packet(id, instr::WRITE, ¶ms[..1 + data.len()])?;
let _ = self.read_status_packet()?;
Ok(())
}
fn read_present_position(&mut self, id: u8) -> CuResult<u16> {
let data = self
.read_register(id, reg::PRESENT_POSITION, 2)
.map_err(|e| {
CuError::new_with_cause(
&format!("Feetech: failed to read position from servo {}", id),
e,
)
})?;
if data.len() < 2 {
return Err(format!(
"Feetech: short read for position from servo {} (got {} bytes)",
id,
data.len()
)
.into());
}
Ok(u16::from_le_bytes([data[0], data[1]]))
}
fn read_all_positions(&mut self) -> CuResult<()> {
for i in 0..self.num_servos as usize {
match self.read_present_position(self.ids[i]) {
Ok(raw) => self.cached_positions[i] = raw,
Err(e) => {
debug!(
"Feetech: failed to read servo {} (ID {}): {}",
i, self.ids[i], e
);
}
}
}
Ok(())
}
fn sync_write_positions(&mut self, positions: &JointPositions) -> CuResult<()> {
let vals = positions.as_slice();
let n = (self.num_servos as usize).min(vals.len());
if n == 0 {
return Ok(());
}
let data_len_per_servo: u8 = 2; let params_size = 2 + n * 3;
if params_size > MAX_PACKET_SIZE - 5 {
return Err(CuError::from("Feetech: sync-write params too large"));
}
let mut params = [0u8; MAX_PACKET_SIZE - 5];
params[0] = reg::GOAL_POSITION; params[1] = data_len_per_servo;
let mut offset = 2;
for (i, val) in vals.iter().enumerate().take(n) {
let param = self.param_for_slot(i);
let raw = self.units.to_raw(*val, self.centers[i], param);
params[offset] = self.ids[i]; params[offset + 1] = (raw & 0xFF) as u8; params[offset + 2] = (raw >> 8) as u8; offset += 3;
}
self.send_packet(BROADCAST_ID, instr::SYNC_WRITE, ¶ms[..params_size])
.map_err(|e| CuError::new_with_cause("Feetech: sync-write failed", e))?;
Ok(())
}
#[allow(dead_code)]
pub fn set_torque(&mut self, id: u8, enable: bool) -> io::Result<()> {
self.write_register(id, reg::TORQUE_ENABLE, &[enable as u8])
}
#[inline]
fn param_for_slot(&self, i: usize) -> f32 {
if self.units == Units::Normalize {
let hr = self.half_ranges[i];
if hr > 0.0 { hr } else { 1.0 } } else {
self.ticks_per_rev as f32
}
}
fn enable_all_torque(&mut self) -> CuResult<()> {
for i in 0..self.num_servos as usize {
self.set_torque(self.ids[i], true).map_err(|e| {
CuError::new_with_cause(
&format!("Feetech: failed to enable torque on servo {}", self.ids[i]),
e,
)
})?;
}
Ok(())
}
}
impl CuBridge for FeetechBridge {
type Tx = TxChannels;
type Rx = RxChannels;
type Resources<'r> = Resources;
fn new(
config: Option<&ComponentConfig>,
tx_channels: &[BridgeChannelConfig<<Self::Tx as BridgeChannelSet>::Id>],
_rx_channels: &[BridgeChannelConfig<<Self::Rx as BridgeChannelSet>::Id>],
resources: Self::Resources<'_>,
) -> CuResult<Self>
where
Self: Sized,
{
let cfg = config.ok_or("FeetechBridge requires a config block with servo IDs")?;
let mut ids = [0u8; MAX_SERVOS];
let mut num_servos: u8 = 0;
for (i, id_slot) in ids.iter_mut().enumerate().take(MAX_SERVOS) {
let key = format!("servo{}", i);
match cfg.get::<u8>(&key)? {
Some(id) => {
*id_slot = id;
num_servos = (i + 1) as u8;
}
None if i == 0 => {
return Err(
"FeetechBridge: you must configure at least one servo ID (\"servo0\")"
.into(),
);
}
None => break, }
}
let units = match cfg.get::<String>("units")? {
Some(s) => s.parse().map_err(|_| {
CuError::from(format!(
"FeetechBridge: unknown units \"{s}\". Use \"raw\", \"deg\", \"rad\", or \"normalize\"."
))
})?,
None => Units::Raw,
};
let mut centers = [0.0f32; MAX_SERVOS];
let mut half_ranges = [0.0f32; MAX_SERVOS];
if units != Units::Raw {
let cal_path = cfg
.get::<String>("calibration_file")?
.ok_or("FeetechBridge: \"calibration_file\" is required when units != raw")?;
let cal = CalibrationData::load(std::path::Path::new(&cal_path)).map_err(|e| {
CuError::new_with_cause(
&format!("FeetechBridge: failed to load calibration from \"{cal_path}\""),
e,
)
})?;
for i in 0..num_servos as usize {
centers[i] = cal.center_for(ids[i]).ok_or_else(|| {
CuError::from(format!(
"FeetechBridge: no calibration entry for servo ID {} in \"{cal_path}\"",
ids[i]
))
})?;
if units == Units::Normalize {
half_ranges[i] = cal.half_range_for(ids[i]).ok_or_else(|| {
CuError::from(format!(
"FeetechBridge: no calibration entry for servo ID {} in \"{cal_path}\" (normalize)",
ids[i]
))
})?;
}
}
}
let ticks_per_rev = cfg.get::<u32>("ticks_per_rev")?.unwrap_or(4096);
let port = resources.serial.0;
let has_writers = !tx_channels.is_empty();
Ok(FeetechBridge {
port,
ids,
num_servos,
has_writers,
cached_positions: [0u16; MAX_SERVOS],
units,
centers,
ticks_per_rev,
half_ranges,
})
}
fn start(&mut self, _ctx: &CuContext) -> CuResult<()> {
if self.has_writers {
self.enable_all_torque()?;
debug!(
"FeetechBridge: enabled torque on {} servos",
self.num_servos
);
} else {
debug!(
"FeetechBridge: read-only mode, torque left disabled on {} servos",
self.num_servos
);
}
Ok(())
}
fn send<'a, Payload>(
&mut self,
_ctx: &CuContext,
channel: &'static BridgeChannel<<Self::Tx as BridgeChannelSet>::Id, Payload>,
msg: &CuMsg<Payload>,
) -> CuResult<()>
where
Payload: CuMsgPayload + 'a,
{
match channel.id() {
TxId::GoalPositions => {
let goal_msg: &CuMsg<JointPositions> = msg.downcast_ref()?;
if let Some(positions) = goal_msg.payload() {
self.sync_write_positions(positions)?;
}
}
}
Ok(())
}
fn receive<'a, Payload>(
&mut self,
ctx: &CuContext,
channel: &'static BridgeChannel<<Self::Rx as BridgeChannelSet>::Id, Payload>,
msg: &mut CuMsg<Payload>,
) -> CuResult<()>
where
Payload: CuMsgPayload + 'a,
{
self.read_all_positions()?;
msg.tov = Tov::Time(ctx.now());
match channel.id() {
RxId::Positions => {
let mut payload = JointPositions::new();
payload.fill_from_iter(
self.cached_positions[..self.num_servos as usize]
.iter()
.enumerate()
.map(|(i, &raw)| {
self.units
.from_raw(raw, self.centers[i], self.param_for_slot(i))
}),
);
let pos_msg: &mut CuMsg<JointPositions> = msg.downcast_mut()?;
pos_msg.set_payload(payload);
}
}
Ok(())
}
fn stop(&mut self, _ctx: &CuContext) -> CuResult<()> {
for i in 0..self.num_servos as usize {
if let Err(e) = self.set_torque(self.ids[i], false) {
debug!(
"FeetechBridge: failed to disable torque on servo {}: {}",
self.ids[i],
e.to_string()
);
}
}
debug!(
"FeetechBridge: disabled torque on {} servos",
self.num_servos
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn checksum_matches_known_values() {
let body = [0x01u8, 0x04, 0x03, 0x28, 0x01];
assert_eq!(compute_checksum(&body), 0xCE);
}
#[test]
fn joint_positions_from_slice() {
let mut p = JointPositions::new();
p.fill_from_iter([0.0f32, 32768.0, 65535.0]);
assert_eq!(p.as_slice(), &[0.0, 32768.0, 65535.0]);
}
#[test]
fn units_raw_roundtrip() {
use crate::calibration::Units;
let u = Units::Raw;
let tpr = 4096.0;
assert_eq!(u.from_raw(2048, 0.0, tpr), 2048.0);
assert_eq!(u.to_raw(2048.0, 0.0, tpr), 2048);
}
#[test]
fn units_deg_roundtrip() {
use crate::calibration::{DEFAULT_TICKS_PER_REV, Units};
let u = Units::Deg;
let center = 2048.0;
assert!((u.from_raw(2048, center, DEFAULT_TICKS_PER_REV as f32)).abs() < 1e-6);
assert_eq!(u.to_raw(0.0, center, DEFAULT_TICKS_PER_REV as f32), 2048);
}
#[test]
fn units_rad_roundtrip() {
use crate::calibration::{DEFAULT_TICKS_PER_REV, Units};
let u = Units::Rad;
let center = 2048.0;
let rad = u.from_raw(3072, center, DEFAULT_TICKS_PER_REV as f32);
let back = u.to_raw(rad, center, DEFAULT_TICKS_PER_REV as f32);
assert_eq!(back, 3072);
}
#[test]
fn units_normalize_roundtrip() {
use crate::calibration::Units;
let u = Units::Normalize;
let center = 2048.0;
let half_range = 1024.0; assert!((u.from_raw(2048, center, half_range)).abs() < 1e-6);
assert!((u.from_raw(1024, center, half_range) + 1.0).abs() < 1e-6);
assert!((u.from_raw(3072, center, half_range) - 1.0).abs() < 1e-6);
assert_eq!(u.to_raw(0.0, center, half_range), 2048);
assert_eq!(u.to_raw(-1.0, center, half_range), 1024);
assert_eq!(u.to_raw(1.0, center, half_range), 3072);
}
}