Skip to content

Improvements to bus servo library management#161

Open
danic85 wants to merge 3 commits into
developfrom
feature/bus_servo_improvements
Open

Improvements to bus servo library management#161
danic85 wants to merge 3 commits into
developfrom
feature/bus_servo_improvements

Conversation

@danic85

@danic85 danic85 commented Mar 26, 2026

Copy link
Copy Markdown
Collaborator

What type of change is this?

  • New module
  • Change to an existing module
  • Core improvement
  • Other (please describe)

📝 What does this change do?

Refactors the bus_servo module to support pluggable backend implementations (hardware SDKs vs simulation), migrates bus-servo configuration to degrees, and fixes several bugs identified during code review.

Key improvements include:

  • Introduces a backend abstraction (BusServoBase) with a factory and concrete backends: Waveshare, Rustypot, and Simulation
  • Fully implements the Waveshare backend: move_to_raw(), get_position_raw(), set_speed(), set_acceleration(), detach(), attach(), exit(), and calibrate_to_center() using the Waveshare SDK
  • Adds _WaveshareConnectionManager: a thread-safe shared connection pool (keyed by port, baudrate, and protocol) so all WaveshareBusServo instances on the same bus reuse one PortHandler/packet handler, with reference counting ensuring closePort() is only called when the last servo exits
  • Fixes COMM_SUCCESS missing constant in the Waveshare backend
  • Fixes all uses of self.index/self.nameself.servo_id in the Waveshare backend to match the base class
  • Fixes SC model initialisation to create a proper PortHandler and open the port/set baudrate on startup
  • Adds a default log callable to BusServoBase so backends can log errors without a caller-provided function
  • Restores calibrate_to_center() on the Servo class, delegating to the backend implementation
  • Applies per-move speed and acceleration overrides in _do_move() via set_speed()/set_acceleration() if supported by the backend
  • Fixes get_pose_value() to use proper dict lookup (self.poses.get(pose_name))
  • Fixes three incorrect move_relative(start) calls in personality.py to move(start), since start is an absolute position, not a relative delta
  • Updates bus_servo runtime code and environment YAMLs to operate in degrees rather than raw units

❓ Why is this change needed?

The previous implementation had stub methods (pass) in the Waveshare backend that silently broke servo movement, position reading, torque control, and shutdown. Several runtime errors would occur on hardware due to undefined constants (COMM_SUCCESS), wrong attribute names (self.index instead of self.servo_id), and a missing calibrate_to_center() method on the Servo class. Each WaveshareBusServo instance also opened its own serial port connection independently, which could cause interleaved writes, corrupted packets, or port-open failures when multiple servos shared the same bus. The personality.py balance logic would also overshoot servo positions by treating absolute start positions as relative deltas.

🛠️ How was this implemented?

  • waveshare_backend.py: Added _WaveshareConnectionManager class with acquire(), release(), and reset() classmethods; acquire() creates the port connection only once per (port, baudrate, protocol) key and tracks a reference count, release() decrements the count and closes the port only when it reaches zero; WaveshareBusServo.__init__ now calls acquire() instead of opening its own port, and exit() calls release(); added COMM_SUCCESS = 0 module-level constant; implemented all stub methods using the Waveshare ST/SC SDK calls; fixed get_speed() signature to match base class (unit parameter); fixed SC __init__ to import and create PortHandler; calibrate_to_center() now converts degrees to raw via degrees_to_raw() and delegates to move_to_raw()
  • bus_servo_base.py: Added self.log default (lambda msg, **kw: print(msg)) so level='error' keyword calls work without a caller-provided log function
  • bus_servo.py: Added calibrate_to_center() delegating to self.backend_servo.calibrate_to_center(); _do_move() now applies speed/acceleration overrides via set_speed()/set_acceleration() with hasattr guards; get_pose_value() uses self.poses.get(pose_name) for correct dict access
  • personality.py: Changed three move_relative(start) calls to move(start) for correct absolute positioning
  • Test files: Removed duplicate import sys from test_piservo.py and test_servo.py; updated test_bus_servo.py setUp with proper mock return values for SDK 2-tuple methods; added tearDown cleanup via _WaveshareConnectionManager.reset(); added tests verifying shared-connection reuse and ref-count port-close behaviour

🧪 How was this tested?

  • Unit tests updated and all 32 tests pass (TestWaveshareBusServo, TestRustypotBusServo, TestBusServo, TestPiServo, TestServo)
  • New tests verify that two servos on the same port share a single portHandler/packetHandler instance and that closePort() is only called after the last servo calls exit()
  • CodeQL security scan returned 0 alerts

💥 Breaking changes

  • Yes (please describe)
  • No

🗂 Related issues

✅ PR Checklist

  • Title & Description: PR title and description are clear and complete.
  • Documentation: PR links to idea in github discussion group containing complete documentation.
  • Scope & Size: PR is focused on a single issue/feature and is a reasonable size.
  • Code Quality: Code is clean, consistent, and follows the project style guide.
  • Tests: Tests have been added/updated if needed.
  • Manual Testing: Changes have been tested on the latest release of the project.
  • Self-Review: I've reviewed my own code and ensured there are no obvious issues.

🚀 Thank you for your contribution to the project!


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the bus_servo module to support pluggable backend implementations (hardware SDKs vs simulation), migrates bus-servo configuration to degrees, and updates tests/docs/environment configs to match the new architecture.

Changes:

  • Introduce a backend abstraction (BusServoBase) plus a factory and new backends (Waveshare / Rustypot / Simulation).
  • Update bus_servo runtime code and environment YAMLs to operate in degrees rather than raw units.
  • Expand/adjust unit tests and documentation to align with the new backend architecture and config format.

Reviewed changes

Copilot reviewed 23 out of 61 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
test.sh Runs unittest discovery with additional filename patterns and buffered output.
src/tests/test_module_loader.py Adds module-level dependency mocking for tests.
src/tests/test_base_module.py Adds module-level dependency mocking for tests.
src/modules/personality/tests/test_personality.py Adds module-level dependency mocking for tests.
src/modules/personality/personality.py Switches personality servo commands to move_relative() in multiple places.
src/modules/network/rtlsdr/tests/test_rtlsdr.py Adds module-level dependency mocking for tests.
src/modules/actuators/tests/servo_test.py Refactors legacy servo tests to unittest and mocks hardware dependencies.
src/modules/actuators/tests/bus_servo_test.py Updates bus-servo queue tests to mock the new backend factory + backend servo.
src/modules/actuators/servo/tests/test_servo.py Adds dependency mocking; minor test structure tweaks.
src/modules/actuators/piservo/tests/test_piservo.py Adds dependency mocking; minor test structure tweaks.
src/modules/actuators/bus_servo/tests/test_bus_servo.py Adds/expands backend-level unit tests (Waveshare/Rustypot/Simulation + factory).
src/modules/actuators/bus_servo/libraries/bus_servo_base.py Defines the abstract backend interface for bus servos.
src/modules/actuators/bus_servo/libraries/factory.py Adds BusServoFactory for selecting/instantiating backends.
src/modules/actuators/bus_servo/libraries/utils.py Adds unit conversion helpers (degrees/radians/raw linear mappings).
src/modules/actuators/bus_servo/libraries/simulation_backend.py Implements a simulation backend for development/testing without hardware.
src/modules/actuators/bus_servo/libraries/rustypot_backend.py Implements a Rustypot-based backend for supported servos.
src/modules/actuators/bus_servo/libraries/waveshare_backend.py Adds a Waveshare backend wrapper around SDK functionality (currently incomplete).
src/modules/actuators/bus_servo/libraries/waveshare/__init__.py Initializes the vendored Waveshare library package.
src/modules/actuators/bus_servo/libraries/waveshare/STservo_sdk/__init__.py Vendored Waveshare ST SDK package init.
src/modules/actuators/bus_servo/libraries/waveshare/STservo_sdk/stservo_def.py Vendored Waveshare ST SDK definitions.
src/modules/actuators/bus_servo/libraries/waveshare/STservo_sdk/protocol_packet_handler.py Vendored Waveshare ST SDK implementation.
src/modules/actuators/bus_servo/libraries/waveshare/STservo_sdk/port_handler.py Vendored Waveshare ST SDK serial port handler.
src/modules/actuators/bus_servo/libraries/waveshare/STservo_sdk/group_sync_write.py Vendored Waveshare ST SDK sync write support.
src/modules/actuators/bus_servo/libraries/waveshare/STservo_sdk/group_sync_read.py Vendored Waveshare ST SDK sync read support.
src/modules/actuators/bus_servo/libraries/waveshare/STservo_sdk/sts.py Vendored Waveshare ST SDK STS protocol implementation.
src/modules/actuators/bus_servo/libraries/waveshare/STservo_sdk/scscl.py Vendored Waveshare ST SDK SCSCL protocol implementation.
src/modules/actuators/bus_servo/libraries/waveshare/STServo_examples/write.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/STServo_examples/wheel.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/STServo_examples/sync_write.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/STServo_examples/sync_read.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/STServo_examples/sync_read_write.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/STServo_examples/reg_write.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/STServo_examples/read_write.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/STServo_examples/read.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/STServo_examples/read_all.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/STServo_examples/ping.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/STServo_examples/change_id.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/SCservo_sdk/__init__.py Vendored Waveshare SC SDK package init.
src/modules/actuators/bus_servo/libraries/waveshare/SCservo_sdk/scservo_def.py Vendored Waveshare SC SDK definitions.
src/modules/actuators/bus_servo/libraries/waveshare/SCservo_sdk/protocol_packet_handler.py Vendored Waveshare SC SDK implementation.
src/modules/actuators/bus_servo/libraries/waveshare/SCservo_sdk/port_handler.py Vendored Waveshare SC SDK serial port handler.
src/modules/actuators/bus_servo/libraries/waveshare/SCservo_sdk/packet_handler.py Vendored Waveshare SC SDK packet handler shim.
src/modules/actuators/bus_servo/libraries/waveshare/SCservo_sdk/group_sync_write.py Vendored Waveshare SC SDK sync write support.
src/modules/actuators/bus_servo/libraries/waveshare/SCservo_sdk/group_sync_read.py Vendored Waveshare SC SDK sync read support.
src/modules/actuators/bus_servo/libraries/waveshare/SCServo_examples/sync_write.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/SCServo_examples/sync_read_write.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/SCServo_examples/read_write.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/SCServo_examples/ping.py Vendored Waveshare example script.
src/modules/actuators/bus_servo/libraries/waveshare/SCServo_examples/scservo_sdk/__init__.py Vendored Waveshare example-support SDK init.
src/modules/actuators/bus_servo/libraries/waveshare/SCServo_examples/scservo_sdk/scservo_def.py Vendored Waveshare example-support SDK definitions.
src/modules/actuators/bus_servo/libraries/waveshare/SCServo_examples/scservo_sdk/protocol_packet_handler.py Vendored Waveshare example-support SDK implementation.
src/modules/actuators/bus_servo/libraries/waveshare/SCServo_examples/scservo_sdk/port_handler.py Vendored Waveshare example-support SDK serial port handler.
src/modules/actuators/bus_servo/libraries/waveshare/SCServo_examples/scservo_sdk/packet_handler.py Vendored Waveshare example-support SDK packet handler shim.
src/modules/actuators/bus_servo/libraries/waveshare/SCServo_examples/scservo_sdk/group_sync_write.py Vendored Waveshare example-support SDK sync write support.
src/modules/actuators/bus_servo/libraries/waveshare/SCServo_examples/scservo_sdk/group_sync_read.py Vendored Waveshare example-support SDK sync read support.
src/modules/actuators/bus_servo/config.yml Adds declared dependencies for bus_servo module.
src/modules/actuators/bus_servo/bus_servo.py Refactors Servo module to delegate hardware operations to selected backend.
src/modules/actuators/bus_servo/README.md Updates docs for backend architecture and degrees-based configuration.
environments/laptop.yml Adds bus_servo config using simulation backend and degrees-based ranges/poses.
environments/cody.yml Migrates bus_servo poses/ranges to degrees-based values.
README.md Updates module list/docs and adds a testing section.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +132 to +140
def handle_errors(self, comm_result, error):
"""
Handle communication errors.
:param comm_result: Communication result
:param error: Error code
"""
if comm_result != COMM_SUCCESS:
self.log("%s" % self.packetHandler.getTxRxResult(comm_result), level='error')
# log stack trace for debugging

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

handle_errors() references COMM_SUCCESS, but this module never defines or imports it, so this will raise NameError. Import COMM_SUCCESS from the appropriate SDK module (ST vs SC) or define a backend-level constant, and ensure it matches the SDK's semantics.

Copilot uses AI. Check for mistakes.
Comment thread src/modules/personality/personality.py Outdated
Comment on lines +143 to +144
self.servos['leg_l_tilt'].move_relative(self.servos['leg_l_tilt'].start)
self.servos['leg_r_tilt'].move_relative(self.servos['leg_r_tilt'].start)

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

move_relative() expects a delta, but self.servos['leg_*_tilt'].start is an absolute position. Calling move_relative(start) will overshoot by start degrees rather than returning to the start/center position. Use an absolute move (move(start)) here, or compute the delta as (start - current_position) before calling move_relative().

Copilot uses AI. Check for mistakes.
Comment thread src/modules/personality/personality.py Outdated
Comment on lines +157 to +158
self.servos['leg_l_tilt'].move_relative(self.servos['leg_l_tilt'].start)
# self.servos['leg_r_knee'].move_relative(-90)

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

move_relative(self.servos['leg_l_tilt'].start) again treats an absolute start position as a relative delta. This will drive the servo far past the intended target. Prefer move(self.servos['leg_l_tilt'].start) or compute the needed delta first.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 6
import sys
from unittest.mock import MagicMock
sys.modules['yaml'] = MagicMock()
import sys
import unittest
from unittest.mock import MagicMock, patch

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

Duplicate import sys (lines 1 and 4). Please remove the redundant import; it also makes the pre-import sys.modules[...] mocking harder to follow.

Copilot uses AI. Check for mistakes.
# self.log(f"(MOVE) Moving servo {self.identifier} from {self.pos} to position {position} for range {self.range}")
if position < self.range[0] or position > self.range[1]:
self.log(f"Position {position} out of range ({self.range[0]}-{self.range[1]})", level='error')
return

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

_do_move() receives speed and acceleration (and the queue stores per-move overrides), but the new backend call ignores both. This is a behavior regression: callers can request a speed/acceleration override but it has no effect. Consider extending the backend interface to accept these values (or call backend_servo.set_speed(...) / set acceleration) or remove these parameters from move()/the queue to avoid a misleading API.

Suggested change
return
return
# Apply per-move speed/acceleration overrides if supported by backend
effective_speed = speed if speed is not None else getattr(self, "speed", None)
if effective_speed is not None and hasattr(self.backend_servo, "set_speed"):
try:
self.backend_servo.set_speed(effective_speed)
except TypeError:
# Backend method signature may differ; ignore override in that case
pass
effective_accel = acceleration if acceleration is not None else getattr(self, "acceleration", None)
if effective_accel is not None and hasattr(self.backend_servo, "set_acceleration"):
try:
self.backend_servo.set_acceleration(effective_accel)
except TypeError:
# Backend method signature may differ; ignore override in that case
pass

Copilot uses AI. Check for mistakes.
Comment thread src/modules/actuators/bus_servo/config.yml
Comment on lines +62 to +76
def set_speed(self, value, unit='degrees'):
# Implement as needed, similar to move_to
pass

def detach(self):
# Disable torque
pass

def attach(self):
# Enable torque
pass

def exit(self):
# Cleanup
pass

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

set_speed(), detach(), attach(), and exit() are currently unimplemented (pass). These are called by bus_servo.Servo (e.g., detach() during calibration and exit() on shutdown), so leaving them empty will silently break expected behavior. Implement them via the Waveshare SDK (torque enable/disable, port close, etc.) or raise NotImplementedError to fail fast.

Copilot uses AI. Check for mistakes.
Comment on lines +243 to 245



Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

calibrate_to_center() was removed from this class, but setup_messaging() still calls it when center_on_boot is enabled. This will cause an AttributeError at runtime. Please either re-add calibrate_to_center() (likely delegating to self.backend_servo.calibrate_to_center()) or remove/replace the call site accordingly.

Suggested change
def calibrate_to_center(self):
"""
Calibrate this servo to its center position using the backend implementation.
This method is called, for example, when `center_on_boot` is enabled in setup_messaging().
"""
backend = getattr(self, "backend_servo", None)
if backend is None:
# Backend not initialized; cannot perform calibration.
self.log(
f"backend_servo is not configured for {self.identifier}; cannot calibrate to center.",
level='error',
)
return
calibrate = getattr(backend, "calibrate_to_center", None)
if callable(calibrate):
calibrate()
else:
# Backend does not support center calibration; avoid raising AttributeError.
self.log(
f"Backend for {self.identifier} does not implement calibrate_to_center(); skipping center calibration.",
level='warning',
)

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Call the method in backend implementation instead.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Done in commit 6e97bc4. Added calibrate_to_center() to the Servo class in bus_servo.py that delegates directly to self.backend_servo.calibrate_to_center(). The waveshare backend implementation was also fixed to use self.servo_id (not self.index) and properly converts the center degrees to raw units via move_to_raw().

Comment on lines +78 to +84
def move_to_raw(self, raw_value):
# Send move command using SDK
pass

def get_position_raw(self):
# Read position using SDK
pass

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

move_to_raw() and get_position_raw() are currently pass, but move_to() and get_position() depend on them. With the default backend set to waveshare, this means bus servos will not move and position reads will return None/fail. Implement these methods using the underlying Waveshare SDK (and ensure port open/baudrate setup happens in __init__).

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +94
def get_speed(self):
"""
Get the current speed of the servo.
"""
if self.model.startswith('ST'):
# Read STServo present position
sts_present_position, sts_present_speed, sts_comm_result, sts_error = self.packetHandler.ReadPosSpeed(self.index)
if not self.handle_errors(sts_comm_result, sts_error):
return sts_present_speed

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

get_speed() uses self.index, but this class (and BusServoBase) stores the id as self.servo_id; self.index is never set here, so this will raise at runtime. Also BusServoBase.get_speed is defined as get_speed(self, unit='degrees'), but this override doesn't accept unit, which can lead to TypeError if callers pass it. Align this implementation with the base interface and use self.servo_id consistently.

Copilot uses AI. Check for mistakes.
@danic85

danic85 commented Mar 26, 2026

Copy link
Copy Markdown
Collaborator Author

@copilot apply changes based on the comments in this thread except those I have specified not to fix.

Copilot AI commented Mar 26, 2026

Copy link
Copy Markdown

@copilot apply changes based on the comments in this thread except those I have specified not to fix.

Applied all changes from the review thread in commit 6e97bc4:

  • waveshare_backend.py: Added COMM_SUCCESS = 0 constant; implemented set_speed(), set_acceleration(), detach(), attach(), exit(), move_to_raw(), get_position_raw(); fixed get_speed() signature (unit param, self.servo_id); fixed all self.index/self.nameself.servo_id; fixed SC __init__ to create PortHandler and open port; refactored calibrate_to_center() to convert degrees→raw.
  • bus_servo.py: Added calibrate_to_center() delegating to backend; applied speed/acceleration overrides in _do_move(); fixed get_pose_value() to use dict lookup.
  • personality.py: Fixed three move_relative(start)move(start) calls.
  • Test files: Removed duplicate import sys from test_piservo.py and test_servo.py.

The dependency config change was skipped per your instruction.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 61 changed files in this pull request and generated 8 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +18 to +21
def __init__(self, servo_id, model, port, baudrate=1000000, range=None, range_degrees=None, **kwargs):
super().__init__(servo_id, model, port, baudrate, range, **kwargs)
self.speed = kwargs.get('speed', 300)
self.acceleration = kwargs.get('acceleration', 50)

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

__init__() accepts range_degrees, and BusServoFactory.create() passes it through, but the super().__init__(..., range, **kwargs) call drops range_degrees (base will always see range_degrees=None). If range_degrees is still part of the intended API, pass it through to BusServoBase (or remove it from the signature/factory to avoid a misleading parameter).

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +22
def move_to(self, value, unit='degrees'):
self.log(f"[SIM] Moving servo {self.servo_id} to {value} {unit}")
self.position = value
self.is_moving = True
time.sleep(0.01)
self.is_moving = False

def get_position(self, unit='degrees'):
self.log(f"[SIM] Getting position of servo {self.servo_id} in {unit}")
return self.position

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

SimulationBusServo.move_to()/get_position() ignore the unit parameter and store/return the raw value unchanged. This makes simulation behave inconsistently with other backends (e.g., calling move_to(..., unit='radians') will store radians but later get_position(unit='degrees') returns radians). Consider normalizing internal state to a single unit (e.g., degrees) and converting on input/output to match the backend contract.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +84
# Implement as needed, similar to move_to
pass

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

set_speed() is currently a no-op (pass). Since Servo._do_move() now calls backend_servo.set_speed(...) for each move, this will silently ignore speed settings on the rustypot backend. Either implement the mapping to the rustypot controller API, or explicitly raise NotImplementedError/log a warning so callers aren’t misled into thinking speed overrides are applied.

Suggested change
# Implement as needed, similar to move_to
pass
# Speed override is not currently supported for rustypot backend.
# Raising NotImplementedError avoids silently ignoring requested speeds.
raise NotImplementedError("set_speed is not supported in the rustypot backend; speed overrides are not applied.")

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +4
import sys
from unittest.mock import MagicMock
sys.modules['yaml'] = MagicMock()
sys.modules['requests'] = MagicMock()

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

Same as in test_module_loader.py: mutating sys.modules at import time affects the entire test run and can leak across modules. Consider using a scoped patch.dict(sys.modules, ...) in setUp with cleanup to keep dependency-mocking isolated per test.

Copilot uses AI. Check for mistakes.
Comment on lines 180 to +183
"""
if not self.poses:
return None
for pose in self.poses:
if pose_name in pose:
return pose[pose_name]
return None # or raise an exception if preferred
return self.poses.get(pose_name)

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

get_pose_value() now returns self.poses.get(pose_name), but self.poses appears to be a mapping of pose name → dict of {servo_name: value}. Returning the whole pose dict doesn’t match the method’s docstring (“position value”) and makes it easy to accidentally treat a dict as a numeric position. Consider returning the value for this servo (e.g., lookup self.identifier inside the pose dict) or rename/update the docstring to reflect the actual return type.

Copilot uses AI. Check for mistakes.
Comment on lines +23 to +45
if model.startswith('ST'):
from .waveshare.STservo_sdk import PortHandler, sts
self.portHandler = PortHandler(port)
self.packetHandler = sts(self.portHandler)
self.max_raw = 4095
self.min_raw = 0
self.max_deg = 360
self.min_deg = 0
elif model.startswith('SC'):
from .waveshare.SCservo_sdk import PortHandler, PacketHandler
self.portHandler = PortHandler(port)
self.packetHandler = PacketHandler(1)
self.max_raw = 1023
self.min_raw = 0
self.max_deg = 300
self.min_deg = 0
else:
raise ValueError(f"Unknown model: {model}")
# Open port and set baudrate
if not self.portHandler.openPort():
raise RuntimeError(f"Failed to open port {port} for servo {servo_id}")
if not self.portHandler.setBaudRate(baudrate):
raise RuntimeError(f"Failed to set baudrate {baudrate} for servo {servo_id}")

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

Each WaveshareBusServo instance opens its own PortHandler on the same port in __init__(). In this codebase, ModuleLoader creates one Servo instance per configured servo ID, so multiple servos will likely share the same serial port path. Opening the same serial port multiple times (and having independent is_using locks) can cause interleaved writes, corrupted packets, or port-open failures. Consider introducing a shared connection manager (singleton keyed by (port, baudrate, model/protocol)) so all servos on the same bus reuse one PortHandler/packet handler and serialize access centrally, with reference counting for closePort().

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Done in commit e1b3925. Added _WaveshareConnectionManager to waveshare_backend.py — a thread-safe class-level singleton keyed by (port, baudrate, protocol). WaveshareBusServo.__init__ calls acquire() which creates and opens the port only on the first instance for that bus, then increments a ref-count for each subsequent instance. exit() calls release() which decrements the ref-count and only calls closePort() when it reaches zero. A public reset() classmethod is provided for test isolation. Three new tests verify the shared-handler and ref-counting behaviour.

class RustypotBusServo(BusServoBase):

def __init__(self, servo_id, model, port, baudrate=1000000, range=None, range_degrees=None, **kwargs):
super().__init__(servo_id, model, port, baudrate, range, **kwargs)

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

Like WaveshareBusServo, this __init__() accepts range_degrees and the factory passes it, but the super().__init__(..., range, **kwargs) call drops range_degrees, so BusServoBase.range_degrees will always be None. Either forward it to the base class or remove the parameter to avoid a misleading API.

Suggested change
super().__init__(servo_id, model, port, baudrate, range, **kwargs)
super().__init__(servo_id, model, port, baudrate, range, range_degrees=range_degrees, **kwargs)

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +4
import sys
from unittest.mock import MagicMock
sys.modules['yaml'] = MagicMock()
sys.modules['requests'] = MagicMock()

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

These top-level sys.modules[...] = MagicMock() assignments are global for the entire test process and can leak into other tests (changing import behavior in hard-to-debug ways). Prefer scoping this to the specific tests (e.g., patch.dict(sys.modules, {...}) in setUp/addCleanup) so the mocks are reliably restored after each test module/case.

Copilot uses AI. Check for mistakes.
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.

3 participants