-
Notifications
You must be signed in to change notification settings - Fork 1
Add DroneCommander module with MAVLink move-to and face-target commands #2
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
base: main
Are you sure you want to change the base?
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 |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| """ | ||
| Drone command module for translating tracked object positions into | ||
| MAVLink flight commands (move-to and face-toward). | ||
| """ | ||
|
|
||
| from .drone_command import DroneCommander | ||
| from .drone_command_worker import drone_command_worker_run |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,255 @@ | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
| Drone command interface using MAVLink. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Translates tracked-object positions (camera-relative meters) into | ||||||||||||||||||||||||||||||||||||||||
| MAVLink commands that move the drone toward a target or yaw to face it. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Coordinate frames | ||||||||||||||||||||||||||||||||||||||||
| ----------------- | ||||||||||||||||||||||||||||||||||||||||
| Camera (OAK-D): x = right, y = down, z = forward | ||||||||||||||||||||||||||||||||||||||||
| Drone body: x = forward, y = right, z = down | ||||||||||||||||||||||||||||||||||||||||
| NED (world): x = north, y = east, z = down | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| The camera is assumed to be forward-facing and aligned with the body | ||||||||||||||||||||||||||||||||||||||||
| frame, so the conversion is: | ||||||||||||||||||||||||||||||||||||||||
| body_x = camera_z (forward) | ||||||||||||||||||||||||||||||||||||||||
| body_y = camera_x (right) | ||||||||||||||||||||||||||||||||||||||||
| body_z = camera_y (down) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Body-to-NED requires the drone's current yaw heading. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| MAVLink messages used | ||||||||||||||||||||||||||||||||||||||||
| --------------------- | ||||||||||||||||||||||||||||||||||||||||
| - SET_POSITION_TARGET_LOCAL_NED (mavutil #84) | ||||||||||||||||||||||||||||||||||||||||
| Move to an offset in the local NED frame. | ||||||||||||||||||||||||||||||||||||||||
| - COMMAND_LONG MAV_CMD_CONDITION_YAW (#115) | ||||||||||||||||||||||||||||||||||||||||
| Rotate to face a heading. | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import math | ||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||
| from typing import Optional | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| from pymavlink import mavutil | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| class DroneCommander: | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
| Sends MAVLink commands to the flight controller. | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| Usage:: | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| commander = DroneCommander("tcp:localhost:5762") | ||||||||||||||||||||||||||||||||||||||||
| commander.connect() | ||||||||||||||||||||||||||||||||||||||||
| commander.move_to_target(forward=2.0, right=0.5, down=0.3) | ||||||||||||||||||||||||||||||||||||||||
| commander.face_target(forward=2.0, right=0.5) | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| def __init__( | ||||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||||
| connection_string: str, | ||||||||||||||||||||||||||||||||||||||||
| baud_rate: int = 57600, | ||||||||||||||||||||||||||||||||||||||||
| ) -> None: | ||||||||||||||||||||||||||||||||||||||||
| self._connection_string = connection_string | ||||||||||||||||||||||||||||||||||||||||
| self._baud_rate = baud_rate | ||||||||||||||||||||||||||||||||||||||||
| self._conn: Optional[mavutil.mavlink_connection] = None | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| # ------------------------------------------------------------------ | ||||||||||||||||||||||||||||||||||||||||
| # Connection | ||||||||||||||||||||||||||||||||||||||||
| # ------------------------------------------------------------------ | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| def connect(self) -> None: | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
| Establish MAVLink connection and wait for a heartbeat. | ||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||
| logger.info( | ||||||||||||||||||||||||||||||||||||||||
| "Connecting to flight controller at %s …", | ||||||||||||||||||||||||||||||||||||||||
| self._connection_string, | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| self._conn = mavutil.mavlink_connection( | ||||||||||||||||||||||||||||||||||||||||
| self._connection_string, | ||||||||||||||||||||||||||||||||||||||||
| baud=self._baud_rate, | ||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||
| self._conn.wait_heartbeat() | ||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||
| self._conn.wait_heartbeat() | |
| self._conn.wait_heartbeat(timeout=30) |
Copilot
AI
Feb 3, 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.
Inconsistent spelling: the file uses "meters" on line 4 but "metres" on lines 99 and 120. Use consistent spelling throughout. The American spelling "meters" is more common in technical documentation.
Copilot
AI
Feb 3, 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.
The comment on line 117 states "yaw_rad: Current heading in radians (0 = north, CW positive)", but the rotation matrix implementation on lines 124-125 uses standard CCW (counter-clockwise) rotation convention. The rotation matrix "north = body_x * cos_yaw - body_y * sin_yaw; east = body_x * sin_yaw + body_y * cos_yaw" rotates CCW as yaw increases. This inconsistency between documentation and implementation will cause confusion. Verify the correct convention and update either the comment or the rotation matrix accordingly.
Copilot
AI
Feb 3, 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.
Frame mismatch: the code manually rotates body coordinates to NED using body_to_ned() on line 155, then sends these NED coordinates using MAV_FRAME_BODY_OFFSET_NED on line 167. MAV_FRAME_BODY_OFFSET_NED expects body-frame coordinates as input and will perform the rotation to NED internally, causing a double rotation. Either use MAV_FRAME_LOCAL_OFFSET_NED with the NED coordinates, or use MAV_FRAME_BODY_OFFSET_NED with the body-frame coordinates (forward, right, down) directly without the body_to_ned conversion.
| mavutil.mavlink.MAV_FRAME_BODY_OFFSET_NED, # offset from current pos | |
| mavutil.mavlink.MAV_FRAME_LOCAL_OFFSET_NED, # offset from current pos in local NED |
Copilot
AI
Feb 3, 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.
The face_target method will call math.atan2(right, forward). When both forward and right are zero (target directly above/below the drone), atan2(0, 0) returns 0, which is technically correct but may not represent the intended behavior. Consider adding validation to handle the edge case where the target is at the same horizontal position as the drone, or document this behavior.
| """ | |
| if self._conn is None: | |
| raise RuntimeError("Not connected. Call connect() first.") | |
| # angle from drone nose to target (positive = clockwise) | |
| Note: | |
| If both ``forward`` and ``right`` are zero, the target is | |
| directly above or below the drone in the vertical axis and | |
| has no horizontal bearing. In this implementation, | |
| ``atan2(0, 0)`` is allowed and interpreted as a 0° yaw | |
| change (i.e. no change in heading). | |
| """ | |
| if self._conn is None: | |
| raise RuntimeError("Not connected. Call connect() first.") | |
| # Angle from drone nose to target (positive = clockwise). | |
| # Note: when forward == 0 and right == 0 (target directly above/below), | |
| # atan2(0, 0) returns 0, which we treat as "no yaw change". |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,99 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Worker that reads TrackedObject lists from the ObjectTracker output queue | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and commands the drone to move toward / face the selected target. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Target selection policy (simple): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Pick the TRACKED object closest to the camera (smallest z). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Ignore NEW objects until they become TRACKED (avoids false positives). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - If all objects are LOST, hold position and do nothing. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import List, Optional | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from modules.object_tracker.tracked_object import TrackedObject, TrackingStatus | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+12
to
+14
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import List, Optional | |
| from modules.object_tracker.tracked_object import TrackedObject, TrackingStatus | |
| from enum import Enum | |
| from typing import List, Optional, Protocol | |
| try: | |
| from modules.object_tracker.tracked_object import TrackedObject, TrackingStatus | |
| except ImportError: # Fallback when object tracker module is unavailable | |
| class TrackingStatus(Enum): | |
| NEW = "NEW" | |
| TRACKED = "TRACKED" | |
| LOST = "LOST" | |
| class TrackedObject(Protocol): | |
| object_id: int | |
| label: str | |
| x: float | |
| y: float | |
| z: float | |
| status: TrackingStatus |
Copilot
AI
Feb 3, 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.
Using a hardcoded yaw_rad=0.0 will produce incorrect position commands when the drone is not facing north. The comment on line 91 acknowledges this ("in production read from flight controller"), but the code as written will send incorrect movement commands. Consider either reading the actual yaw from the flight controller using an ATTITUDE message, or documenting this as a critical limitation that must be addressed before deployment.
Copilot
AI
Feb 3, 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.
The infinite loop lacks error handling. If any exception occurs during queue.get(), target selection, or command sending, the worker will crash without cleanup. Consider wrapping the loop body in a try-except block to handle and log exceptions, allowing the worker to continue processing or shut down gracefully.
| tracked_objects: List[TrackedObject] = input_queue.get() | |
| target = _select_target(tracked_objects) | |
| if target is None: | |
| logger.debug("No TRACKED target this frame — holding position.") | |
| continue | |
| logger.info( | |
| "Pursuing target id=%d label=%s at (%.2f, %.2f, %.2f)", | |
| target.object_id, | |
| target.label, | |
| target.x, | |
| target.y, | |
| target.z, | |
| ) | |
| # cam coords → body → MAVLink | |
| # yaw_rad=0.0 for now; in production read from flight controller | |
| commander.track_target( | |
| cam_x=target.x, | |
| cam_y=target.y, | |
| cam_z=target.z, | |
| yaw_rad=0.0, | |
| move=move, | |
| face=face, | |
| ) | |
| try: | |
| tracked_objects: List[TrackedObject] = input_queue.get() | |
| target = _select_target(tracked_objects) | |
| if target is None: | |
| logger.debug("No TRACKED target this frame — holding position.") | |
| continue | |
| logger.info( | |
| "Pursuing target id=%d label=%s at (%.2f, %.2f, %.2f)", | |
| target.object_id, | |
| target.label, | |
| target.x, | |
| target.y, | |
| target.z, | |
| ) | |
| # cam coords → body → MAVLink | |
| # yaw_rad=0.0 for now; in production read from flight controller | |
| commander.track_target( | |
| cam_x=target.x, | |
| cam_y=target.y, | |
| cam_z=target.z, | |
| yaw_rad=0.0, | |
| move=move, | |
| face=face, | |
| ) | |
| except KeyboardInterrupt: | |
| logger.info("Drone command worker received KeyboardInterrupt, shutting down.") | |
| break | |
| except Exception: | |
| logger.exception( | |
| "Unhandled exception in drone command worker loop; continuing." | |
| ) | |
| continue |
Copilot
AI
Feb 3, 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.
The worker function establishes a MAVLink connection on line 68 but provides no mechanism for graceful shutdown or connection cleanup. The infinite loop on line 72 has no exit condition. Consider adding a shutdown signal mechanism (e.g., poison pill in queue, threading.Event, or signal handler) to allow clean termination and proper resource cleanup.
Copilot
AI
Feb 3, 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.
The worker sends MAVLink commands every time it receives tracked objects from the queue, with no rate limiting. If the upstream tracker sends data at high frequency (e.g., 30 Hz from video frames), this could flood the flight controller with rapid position and yaw commands, potentially causing instability or overwhelming the autopilot's command buffer. Consider implementing rate limiting, command throttling, or a minimum movement threshold before sending new commands.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Runtime dependencies for target tracking pipeline | ||
|
|
||
| depthai>=2.24.0 | ||
| pymavlink>=2.4.40 |
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 pymavlink dependency is imported on line 33 but is not listed in requirements.txt or pyproject.toml. This will cause an ImportError when the module is loaded. Add pymavlink to the project dependencies.