Skip to content

Conversation

DasuanerA
Copy link

Implements complete TMotor servo mode control for AK series actuators (AK80-9, AK10-9) using native CAN communication protocol.

Architecture

Core Classes:

  • TMotorServoActuator: Main actuator class extending ActuatorBase

    • Manages motor state, control modes, and command interface
    • Implements thermal protection via ThermalModel
    • Tracks communication timeouts and error states
  • CANManagerServo: Singleton CAN communication manager

    • Configures CAN0 interface: 1MHz bitrate, txqueuelen 1000
    • Handles message sending/receiving via python-can library
    • Power on/off commands: 0xFC (on), 0xFD (off)
  • MotorListener: CAN message listener (can.Listener)

    • Asynchronous message reception via can.Notifier
    • Filters messages by motor ID (arbitration_id & 0x00000FF)
    • Triggers async state updates in actuator

Data Structures:

  • ServoMotorState: Motor state container

    • position (degrees), velocity (ERPM), current (amps)
    • temperature (celsius), error code, acceleration (rad/s)
  • ServoControlMode: Enum for control modes

    • POSITION (4), VELOCITY (3), CURRENT (1), IDLE (7)

CAN Protocol Implementation

Message IDs (Extended CAN):

  • SET_DUTY: 0x00 (not used)
  • SET_CURRENT: 0x01 (current control)
  • SET_CURRENT_BRAKE: 0x02 (not used)
  • SET_RPM: 0x03 (velocity control)
  • SET_POS: 0x04 (position control)
  • SET_ORIGIN_HERE: 0x05 (homing)
  • SET_POS_SPD: 0x06 (not used)
  • Control mode switch: 0x07

Message Format:

  • TX (4 bytes, big-endian): int32 command value

    • Position: degrees * 10 (0.1 resolution, configurable to *1000000)
    • Velocity: ERPM value directly
    • Current: milliamps (current * 1000)
  • RX (8 bytes, big-endian parsing):

    • Bytes[0-1]: Position (int16, *0.1 degrees)
    • Bytes[2-3]: Velocity (int16, *10 ERPM)
    • Bytes[4-5]: Current (int16, *0.01 amps)
    • Byte[6]: Temperature (uint8, celsius)
    • Byte[7]: Error code (uint8)

Control Features

Implemented Methods:

  • set_motor_torque(Nm): Converts via Kt to current command
  • set_output_torque(Nm): Applies gear ratio compensation
  • set_motor_current(A): Direct current command
  • set_motor_position(rad): Converts to degrees, sends position command
  • set_motor_velocity(rad/s): Converts to ERPM via pole pairs
  • set_output_velocity(rad/s): Applies gear ratio
  • home(): Sets origin via CAN_PACKET_SET_ORIGIN_HERE

State Properties:

  • motor_position/output_position (radians)
  • motor_velocity/output_velocity (rad/s)
  • motor_current (amps)
  • motor_torque/output_torque (Nm)
  • case_temperature/winding_temperature (C)
  • error_info: Optional[tuple[error_code, error_message]]

Safety Features

  • Thermal protection: 80C case, 110C winding limits (configurable)
  • Communication timeout: 100ms threshold warning
  • Auto-stop on critical errors (overcurrent, overtemp)
  • Acceleration calculation for sudden change detection
  • Control mode verification via test command
  • Detailed status reporting via get_detailed_status()

Utility Functions

  • degrees_to_radians() / radians_to_degrees()
  • erpm_to_rad_per_sec() / rad_per_sec_to_erpm()
    • Accounts for motor pole pairs in conversion
  • _pack_int32(): Big-endian int32 packing for CAN TX

Control Mode Management

Each mode has entry/exit callbacks:

  • POSITION: Switch to mode 4, no exit action
  • VELOCITY: Switch to mode 3, zero velocity on exit
  • CURRENT: Switch to mode 1, zero current on exit
  • IDLE: Switch to mode 7, no exit action
  • All modes wait 100ms after switching for stabilization

Example Code (main)

Demonstrates current control mode with 15Nm torque command:

  1. Initialize motor (AK80-9, ID:1, offline mode for testing)
  2. Home motor and activate current control mode
  3. Command 15Nm output torque
  4. Run 10Hz monitoring loop for 5 seconds displaying:
    • Commanded vs actual torque (motor and output)
    • Motor and output angles (rad and degrees)
    • Motor and output velocities (rad/s)
    • Current, voltage, temperature
    • Error status
  5. Safe shutdown with zero torque
  6. Display final state summary

Note: Position resolution currently set to 0.1 for simplicity. To enable high-precision mode (0.000001), modify line 323.

Implements complete TMotor servo mode control for AK series actuators (AK80-9, AK10-9)
using native CAN communication protocol.

## Architecture

### Core Classes:
- `TMotorServoActuator`: Main actuator class extending ActuatorBase
  - Manages motor state, control modes, and command interface
  - Implements thermal protection via ThermalModel
  - Tracks communication timeouts and error states

- `CANManagerServo`: Singleton CAN communication manager
  - Configures CAN0 interface: 1MHz bitrate, txqueuelen 1000
  - Handles message sending/receiving via python-can library
  - Power on/off commands: 0xFC (on), 0xFD (off)

- `MotorListener`: CAN message listener (can.Listener)
  - Asynchronous message reception via can.Notifier
  - Filters messages by motor ID (arbitration_id & 0x00000FF)
  - Triggers async state updates in actuator

### Data Structures:
- `ServoMotorState`: Motor state container
  - position (degrees), velocity (ERPM), current (amps)
  - temperature (celsius), error code, acceleration (rad/s)

- `ServoControlMode`: Enum for control modes
  - POSITION (4), VELOCITY (3), CURRENT (1), IDLE (7)

## CAN Protocol Implementation

### Message IDs (Extended CAN):
- SET_DUTY: 0x00 (not used)
- SET_CURRENT: 0x01 (current control)
- SET_CURRENT_BRAKE: 0x02 (not used)
- SET_RPM: 0x03 (velocity control)
- SET_POS: 0x04 (position control)
- SET_ORIGIN_HERE: 0x05 (homing)
- SET_POS_SPD: 0x06 (not used)
- Control mode switch: 0x07

### Message Format:
- TX (4 bytes, big-endian): int32 command value
  - Position: degrees * 10 (0.1 resolution, configurable to *1000000)
  - Velocity: ERPM value directly
  - Current: milliamps (current * 1000)

- RX (8 bytes, big-endian parsing):
  - Bytes[0-1]: Position (int16, *0.1 degrees)
  - Bytes[2-3]: Velocity (int16, *10 ERPM)
  - Bytes[4-5]: Current (int16, *0.01 amps)
  - Byte[6]: Temperature (uint8, celsius)
  - Byte[7]: Error code (uint8)

## Control Features

### Implemented Methods:
- `set_motor_torque(Nm)`: Converts via Kt to current command
- `set_output_torque(Nm)`: Applies gear ratio compensation
- `set_motor_current(A)`: Direct current command
- `set_motor_position(rad)`: Converts to degrees, sends position command
- `set_motor_velocity(rad/s)`: Converts to ERPM via pole pairs
- `set_output_velocity(rad/s)`: Applies gear ratio
- `home()`: Sets origin via CAN_PACKET_SET_ORIGIN_HERE

### State Properties:
- motor_position/output_position (radians)
- motor_velocity/output_velocity (rad/s)
- motor_current (amps)
- motor_torque/output_torque (Nm)
- case_temperature/winding_temperature (C)
- error_info: Optional[tuple[error_code, error_message]]

## Safety Features

- Thermal protection: 80C case, 110C winding limits (configurable)
- Communication timeout: 100ms threshold warning
- Auto-stop on critical errors (overcurrent, overtemp)
- Acceleration calculation for sudden change detection
- Control mode verification via test command
- Detailed status reporting via `get_detailed_status()`

## Utility Functions

- `degrees_to_radians()` / `radians_to_degrees()`
- `erpm_to_rad_per_sec()` / `rad_per_sec_to_erpm()`
  - Accounts for motor pole pairs in conversion
- `_pack_int32()`: Big-endian int32 packing for CAN TX

## Control Mode Management

Each mode has entry/exit callbacks:
- POSITION: Switch to mode 4, no exit action
- VELOCITY: Switch to mode 3, zero velocity on exit
- CURRENT: Switch to mode 1, zero current on exit
- IDLE: Switch to mode 7, no exit action
- All modes wait 100ms after switching for stabilization

## Example Code (__main__)

Demonstrates current control mode with 15Nm torque command:
1. Initialize motor (AK80-9, ID:1, offline mode for testing)
2. Home motor and activate current control mode
3. Command 15Nm output torque
4. Run 10Hz monitoring loop for 5 seconds displaying:
   - Commanded vs actual torque (motor and output)
   - Motor and output angles (rad and degrees)
   - Motor and output velocities (rad/s)
   - Current, voltage, temperature
   - Error status
5. Safe shutdown with zero torque
6. Display final state summary

Note: Position resolution currently set to 0.1 for simplicity.
To enable high-precision mode (0.000001), modify line 323.
self._file_path = os.path.join(self._log_path, f"{file_name}.log")
self._csv_path = os.path.join(self._log_path, f"{file_name}.csv")
# Build paths using forward slash to satisfy cross-platform tests
self._file_path = f"{self._log_path}/{file_name}.log"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

os.path.join is platform agnostic and more robust to linux/win path changes!

Copy link
Author

@DasuanerA DasuanerA Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self._file_path = posixpath.join(self._log_path, f"{file_name}.log")
self._csv_path = posixpath.join(self._log_path, f"{file_name}.csv")

I used posixpath to solve this, since "os.path.join" can't pass make test

@@ -618,7 +619,8 @@ def _generate_file_paths(self) -> None:

base_name = self._user_file_name if self._user_file_name else f"{script_name}_{timestamp}"

file_path = os.path.join(self._log_path, base_name)
# Build paths using forward slash to satisfy cross-platform tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

file_path = posixpath.join(self._log_path, base_name)
self._file_path = file_path + ".log"
self._csv_path = file_path + ".csv"

Same

@@ -357,7 +358,7 @@ def _next_original_phase(self) -> float:

# Busy wait until the time we should be running at
while time.monotonic() < self.loop_deadline and not self.killer.kill_now:
if os.name == "posix" and signal.sigtimedwait(self.killer.signals, 0):
if os.name == "posix" and hasattr(signal, "sigtimedwait") and signal.sigtimedwait(self.killer.signals, 0):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we please split this conditional statement into two? with the hasattr check in the upper level?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if not hasattr(signal, "sigtimedwait"):
      continue
if os.name == "posix" and signal.sigtimedwait(self.killer.signals, 0):


# TMotor servo mode constants
TMOTOR_SERVO_CONSTANTS = MOTOR_CONSTANTS(
MOTOR_COUNT_PER_REV=3600, # 360 degrees * 10
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please validate this number for MOTOR_COUNT_PER_REV?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be 2^16 = 65536 (16-bit encoder)

LOGGER.info("Initializing CAN Manager for TMotor Servo Mode")
try:
# Configure CAN interface
os.system("sudo /sbin/ip link set can0 down") # noqa: S605, S607
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid running sudo commands here? These instructions can be moved to documentation

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of them have been removed. And I created a doc to instruct users to do this on their end.
at docs/tutorials/actuators/tmotor_servo_setup.md

def set_motor_velocity(self, value: float) -> None:
"""Set motor velocity (rad/s)"""
motor_params = cast(dict[str, Any], self._motor_params)
velocity_erpm = rad_per_sec_to_erpm(value, motor_params["NUM_POLE_PAIRS"])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use this class' MOTOR_CONSTANTS attribute here

Wrapper for get_output_acceleration that accounts for gear ratio to get motor-side acceleration
def motor_voltage(self) -> float:
"""Motor voltage (estimated)"""
return 24.0 # Cannot get directly in servo mode
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raise not implementer error or some warning rather than return 24

self._canman.notifier.remove_listener(Listener)
return success
def verify_control_mode(self) -> bool:
"""Verify current control mode matches expected mode"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we please remove this?

return False
return True

def get_detailed_status(self) -> dict[str, Any]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this as well

Comment on lines 751 to 752

with actuator as motor:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with actuator: ?

@senthurayyappan
Copy link
Member

Hey @DasuanerA, can you please address the comments soon so that we can merge this PR and start planning our tests?

…tion

- Fix cross-platform path handling using os.path.join() in logger.py

- Split hasattr conditional check into nested structure in softrealtimeloop.py

- Correct MOTOR_COUNT_PER_REV from 3600 to 65536 for 16-bit encoder

- Remove sudo commands from code and add CAN setup documentation

- Refactor SERVO_PARAMS into separate semantic constants (TMOTOR_ERROR_CODES, CAN_PACKET_ID, TMOTOR_MODELS)

- Remove unused verify_control_mode() and get_detailed_status() methods

- Add comprehensive TMotor servo setup guide in docs/tutorials/actuators/
- Fix SIM102 linting error in softrealtimeloop.py to pass make check by combining nested if statements with 'and' operator
- Replace os.path.join with posixpath.join in logger.py to pass make test, as os.path.join produces Windows backslashes that fail test assertions expecting forward slashes
- Replace fixed time delays with status-based polling for motor startup and mode switches
- Add safety clamping for current, velocity, and position commands using np.clip()
- Remove acceleration calculation and field from ServoMotorState
- Fix motor_voltage property to raise NotImplementedError instead of returning hardcoded value
- Add num_pole_pairs property for cleaner parameter access
- Simplify example code by removing redundant cast operations
- Add warning logs when motor commands exceed safe limits
- Fix example code to avoid calling unsupported motor_voltage property
- Resolve linting issues including trailing whitespace and code formatting
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants