-
Notifications
You must be signed in to change notification settings - Fork 1
feat: pdb servo controller #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,4 +17,5 @@ python-can | |
| ultralytics | ||
| onnx | ||
| onnxruntime | ||
| odrive | ||
| odrive | ||
| smbus2 | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,97 @@ | ||||||||||||||||||
| import rclpy | ||||||||||||||||||
| from std_msgs.msg import Float32 | ||||||||||||||||||
| from smbus2 import SMBus | ||||||||||||||||||
|
|
||||||||||||||||||
| from servo_pkg.parent_config import Parent_Config | ||||||||||||||||||
|
|
||||||||||||||||||
| I2C_DEFAULT_ADDR = 0x21 | ||||||||||||||||||
|
|
||||||||||||||||||
| HW_NUM_SERVOS = 4 | ||||||||||||||||||
|
|
||||||||||||||||||
| REG_SERVO1_HIGH = 0x00 | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| def convert_from_radians(angle, servo_info): | ||||||||||||||||||
| total_range = servo_info.max - servo_info.min | ||||||||||||||||||
| return int(servo_info.min + (total_range * angle / servo_info.rom)) | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| def convert_to_radians(value, servo_info): | ||||||||||||||||||
| total_range = servo_info.max - servo_info.min | ||||||||||||||||||
| return servo_info.rom * (value - servo_info.min) / total_range | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| class PDB_Servo(Parent_Config): | ||||||||||||||||||
| def __init__(self): | ||||||||||||||||||
| super().__init__("pdb_servo") | ||||||||||||||||||
|
|
||||||||||||||||||
| # I2C parameters | ||||||||||||||||||
| self.declare_parameter("i2c_bus", 1) # /dev/i2c-1 on most SBCs | ||||||||||||||||||
| self.declare_parameter("i2c_address", I2C_DEFAULT_ADDR) | ||||||||||||||||||
|
|
||||||||||||||||||
| self.i2c_bus = self.get_parameter("i2c_bus").get_parameter_value().integer_value | ||||||||||||||||||
| self.i2c_addr = ( | ||||||||||||||||||
| self.get_parameter("i2c_address").get_parameter_value().integer_value | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| self.bus = SMBus(self.i2c_bus) | ||||||||||||||||||
|
|
||||||||||||||||||
| self.subs = {} | ||||||||||||||||||
| for port in range(HW_NUM_SERVOS): | ||||||||||||||||||
| self.subs[port] = self.create_subscription( | ||||||||||||||||||
| Float32, | ||||||||||||||||||
| f"/{self.servo_info[port].motor_name}", | ||||||||||||||||||
| lambda msg, port=port: self.set_position(msg, port), | ||||||||||||||||||
| 3, | ||||||||||||||||||
| ) | ||||||||||||||||||
| self.get_logger().info(f"Topic: /{self.servo_info[port].motor_name}") | ||||||||||||||||||
| self._last_pwm_us = {p: None for p in range(HW_NUM_SERVOS)} | ||||||||||||||||||
|
|
||||||||||||||||||
| def _write_servo_us(self, port: int, pwm_us: int): | ||||||||||||||||||
| base_reg = REG_SERVO1_HIGH + (2 * port) | ||||||||||||||||||
| high = (pwm_us >> 8) & 0xFF | ||||||||||||||||||
| low = pwm_us & 0xFF | ||||||||||||||||||
| self.bus.write_i2c_block_data(self.i2c_addr, base_reg, [high, low]) | ||||||||||||||||||
|
|
||||||||||||||||||
| def set_position(self, msg, port): | ||||||||||||||||||
| self.check_valid_servo(port) | ||||||||||||||||||
| servo_info = self.servo_info[port] | ||||||||||||||||||
|
|
||||||||||||||||||
| target_value_us = convert_from_radians(msg.data, servo_info) | ||||||||||||||||||
| current_position = ( | ||||||||||||||||||
| None | ||||||||||||||||||
| if self._last_pwm_us[port] is None | ||||||||||||||||||
| else convert_to_radians(self._last_pwm_us[port], servo_info) | ||||||||||||||||||
| ) | ||||||||||||||||||
|
|
||||||||||||||||||
| if not (servo_info.min <= target_value_us <= servo_info.max): | ||||||||||||||||||
| self.get_logger().warning( | ||||||||||||||||||
| f"Servo {port} input out of range. " f"Last angle: {current_position}" | ||||||||||||||||||
|
||||||||||||||||||
| f"Servo {port} input out of range. " f"Last angle: {current_position}" | |
| f"Servo {port} input out of range. " | |
| f"Requested angle: {msg.data} rad, " | |
| f"valid PWM range: [{servo_info.min}, {servo_info.max}] us, " | |
| f"last angle: {current_position}" |
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
main() spins the node without a try/finally, so a KeyboardInterrupt or exception can skip destroy_node() and leave the I2C bus handle open. Wrap rclpy.spin(node) in try: ... finally: to always close the SMBus and call rclpy.shutdown().
| rclpy.spin(node) | |
| node.destroy_node() | |
| rclpy.shutdown() | |
| try: | |
| rclpy.spin(node) | |
| finally: | |
| node.destroy_node() | |
| rclpy.shutdown() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The radians<->PWM conversion helpers are duplicated from
servo_pkg/USB_Servo.py(same formulas). Consider moving these conversions into a shared utility module (e.g.,servo_pkg/conversions.py) and importing from both controllers to avoid future drift if the mapping ever changes.