Skip to content

Introduce ServoManager for centralised bus servo control#162

Open
danic85 wants to merge 4 commits into
feature/bus_servo_improvementsfrom
feature/waveshare_servo_improvements
Open

Introduce ServoManager for centralised bus servo control#162
danic85 wants to merge 4 commits into
feature/bus_servo_improvementsfrom
feature/waveshare_servo_improvements

Conversation

@danic85

@danic85 danic85 commented Mar 29, 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?

Replaces the previous architecture of independent Servo instances with a centralised ServoManager + lightweight ServoState design:

  • ServoManager (new BaseModule): owns all hardware backends, manages every servo on a bus, processes per-servo move queues each loop cycle, and exposes high-level coordination APIs:
    • move_to_pose(pose_name) — queues all servos for a named pose in one call
    • group_move(moves) — queues coordinated moves for multiple servos simultaneously
    • detach_all() / exit() — cleanly releases all hardware on shutdown
  • ServoState: lightweight per-servo data holder (ID, range, speed, queue, poses). Owns no hardware; delegates all I/O to the ServoManager.
  • Shared port access: WaveshareBusServo now uses a class-level singleton per (port, baudrate, model_type) with reference counting — all ST servos on the same bus share one serial connection rather than re-opening the port per servo.
  • Backward-compatible injection: ServoManager exposes a dict-like interface (__getitem__, items(), values(), get()) so existing code such as self.servos['neck_tilt'].move_relative(pitch) works unchanged after updating the inject key from "Servo_*" to ServoManager.

❓ Why is this change needed?

The previous architecture created one independent Servo instance per physical servo, each owning its own serial port handle. This caused two key problems:

  1. Port contention: multiple WaveshareBusServo instances attempted to open the same serial port, risking conflicts on a shared RS-485 bus.
  2. No coordination: there was no single place to batch, sequence, or synchronise multi-servo motions (e.g. pose transitions, balance corrections, gait steps).

A centralised manager resolves both issues and provides a clear extension point for trajectory planning and collision avoidance in the future.

🛠️ How was this implemented?

  • bus_servo.py rewritten with ServoState (lightweight holder) and ServoManager (new BaseModule).
  • WaveshareBusServo updated with a class-level _port_singletons dict (reference-counted) so all servos on a port share one PortHandler/PacketHandler.
  • config.yml class changed from ServoServoManager.
  • Environment files (cody.yml, laptop.yml) updated: servo instances moved from instances: list to config.servos: list; personality inject changed from "Servo_*" to ServoManager.
  • All messaging subscriptions (servo:<name>:mvabs, servo:<name>:mv, servo/pose, etc.) are now registered by the single ServoManager instance rather than per-servo.
  • README updated to document the new architecture, configuration format, and coordination APIs.

🧪 How was this tested?

  • Existing unit tests updated in bus_servo_test.py and test_bus_servo.py to exercise ServoManager and ServoState directly.
  • New tests added for: queue processing, group moves, pose dispatch, dict-like interface, delegate methods (detach, attach, get_position, is_moving), shared port singleton, and reference counting.
  • Pre-existing numpy mock bug in TestRustypotBusServo fixed.
  • All 273 tests pass; 0 CodeQL alerts.

💥 Breaking changes

  • Yes (please describe)

    Environment YAML files must be updated: servo instances should move from the instances: key to config.servos:. The personality (or any module) that previously injected servos: "Servo_*" must change to servos: ServoManager. The ServoManager dict-like interface ensures Python code that accesses self.servos[name] requires no further changes.

🗂 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!


📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

@danic85

danic85 commented Mar 29, 2026

Copy link
Copy Markdown
Collaborator Author

@copilot We have a few areas now where we need to manage multiple servos, the current architecture of independent servo instances is a problem because there are requests to coordinate motion as well as port access.

The best practice for managing multiple servos, especially when you need coordinated motion and shared hardware resources (like a serial port), is to introduce a centralised ServoManager class. Here’s how this architecture should look:

ServoManager:

Owns the shared controller instance (and thus the serial port).
Maintains a registry of all servo objects (by ID or name).

Provides high-level APIs for:

  • Moving individual servos.
  • Coordinating multi-servo motions (e.g., group moves, synchronized trajectories).
  • Handling queueing, timing, and collision avoidance.

Handles all low-level communication with the hardware, ensuring only one access at a time.

Servo objects:

  • Are lightweight, stateless, and do not own the controller or port.
  • Delegate all hardware actions to the manager.
  • Store only configuration/state relevant to their identity (ID, model, logical state).

Coordination:

  • All motion commands (single or group) go through the manager, which can batch, synchronize, or sequence them as needed.
  • The manager can implement group move commands, trajectory planning, and ensure atomicity.

Messaging/Events:

  • The manager can subscribe to relevant topics and dispatch commands to servos.
  • For coordinated moves, a single message can trigger a group action.

Migration path:

  • Refactor to create a ServoManager.
  • Move controller/port logic and coordination into the manager.
  • Update Servo instances to delegate all hardware actions to the manager.
  • Update messaging/subscription logic to route through the manager where appropriate.
  • Respect current framework architecture (BaseModule, ModuleLoader, SystemLoop, config.yml files etc) and ensure this new manager follows the correct architecture.

Copilot AI commented Mar 29, 2026

Copy link
Copy Markdown

@copilot We have a few areas now where we need to manage multiple servos, the current architecture of independent servo instances is a problem because there are requests to coordinate motion as well a...

Implemented the ServoManager architecture in commit f663e49. Here's what was done:

New classes in bus_servo.py:

  • ServoState – lightweight per-servo holder (ID, range, speed, queue, poses). Owns no hardware; delegates every hardware call (move_to, get_position, detach, etc.) to the manager.
  • ServoManager (the new BaseModule) – owns all backends, processes queues each loop cycle, and provides high-level coordination APIs:
    • move_to_pose(pose_name) – queues all servos for a named pose
    • group_move(moves) – queues coordinated moves for multiple servos in one call
    • detach_all() / exit() – cleanly releases all hardware

Shared port access:
WaveshareBusServo now uses a class-level singleton (with reference counting) per (port, baudrate, model_type), so all ST servos on /dev/ttyAMA0 share one serial port handle rather than re-opening it per servo.

Config changes:

  • Environment YAMLs (cody.yml, laptop.yml) now list servos under config.servos instead of instances; one ServoManager instance manages them all.
  • Personality inject changed from "Servo_*" to ServoManager. ServoManager exposes a dict-like interface (__getitem__, items(), values(), get()) so existing code like self.servos['neck_tilt'].move_relative(pitch) works unchanged.

Copilot AI changed the title Tested waveshare backend Introduce ServoManager for centralised bus servo control Mar 29, 2026
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