Skip to content
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
55c795a
Adds flight controller start method for hitl threading
honzikschenk Sep 18, 2025
8fb099e
Removes positions json path
honzikschenk Sep 18, 2025
066637e
Removes camera part of threading test
honzikschenk Sep 18, 2025
f6a9023
Actually removes camera from test
honzikschenk Sep 18, 2025
00c9ab3
Makes gps speed faster
honzikschenk Sep 18, 2025
8a005e3
Moves position frequency control to position module
honzikschenk Sep 18, 2025
647504c
Fixes linting issues
honzikschenk Sep 18, 2025
ba329d0
Fixes port for physical test
honzikschenk Sep 22, 2025
5e0df98
added movement interpolation between current and target position
4ppleSA0CE Sep 24, 2025
8a84dfc
created test for gps waypoint
4ppleSA0CE Sep 28, 2025
396c0fc
moved integration tests
4ppleSA0CE Sep 30, 2025
bad8c56
bypassed the parameter checking
4ppleSA0CE Sep 30, 2025
49f3a88
hopefully fixes some stuff
4ppleSA0CE Oct 1, 2025
ca1b21b
update waypoint mission upload
4ppleSA0CE Oct 1, 2025
4b6db3b
added comment to keep track of testing
4ppleSA0CE Oct 1, 2025
f94b178
added debugging for uploading mission pos
4ppleSA0CE Oct 1, 2025
b958ccc
added gps debugging
4ppleSA0CE Oct 1, 2025
18482e9
changed command upload
4ppleSA0CE Oct 2, 2025
f91782b
added debugging wtf is wrong with the command upload
4ppleSA0CE Oct 2, 2025
c9eb901
anothre way to add waypoints
4ppleSA0CE Oct 2, 2025
451e785
added pauses in case that was teh issue
4ppleSA0CE Oct 2, 2025
eb94383
added wait for gps, and manual injection
4ppleSA0CE Oct 2, 2025
6942b78
deleted unnecessary prints
4ppleSA0CE Oct 2, 2025
0c1525f
deleted useless code, nothing works currently
4ppleSA0CE Oct 2, 2025
6c042a0
created new script for manual testing
4ppleSA0CE Oct 2, 2025
cd56672
test
4ppleSA0CE Oct 7, 2025
f4170d9
increased the timeout duration
4ppleSA0CE Oct 7, 2025
76c690f
only test missionplanner
4ppleSA0CE Oct 7, 2025
54ed11e
uncomment
4ppleSA0CE Oct 7, 2025
72cee9b
removed unnecessary files
4ppleSA0CE Oct 8, 2025
78c7dd8
cleaned up code
4ppleSA0CE Oct 8, 2025
52c74b7
fixed linting error
4ppleSA0CE Oct 8, 2025
250ebfd
fixed linting errors
4ppleSA0CE Oct 8, 2025
ff8bcc3
added tolerance as variable, changed progress calculation matho
4ppleSA0CE Jan 21, 2026
15c0657
added tolerance as varibale, changed progress calculation math
4ppleSA0CE Jan 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/hitl/camera_emulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import cv2

IMAGE_SIZE = (720, 480)
IMAGE_FORMATS = (".png", ".jpeg", "jpg")
IMAGE_FORMATS = (".png", ".jpeg", ".jpg")
CAMERA_FPS = 30


Expand Down
4 changes: 3 additions & 1 deletion modules/hitl/hitl_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def create(
position_module: bool,
camera_module: bool,
images_path: str | None = None,
movement_speed: float = 5.0,
) -> "tuple[True, HITL] | tuple[False, None]":
"""
Factory method to create a HITL instance.
Expand All @@ -36,6 +37,7 @@ def create(
position_module: Boolean indicating if the position module is enabled.
camera_module: Boolean indicating if the camera module is enabled.
images_path: Optional path to the images directory for the camera emulator.
movement_speed: Speed of drone movement in m/s for position interpolation (default: 5.0).

Returns:
Success, HITL instance | None.
Expand All @@ -47,7 +49,7 @@ def create(
return True, HITL(cls.__create_key, drone, None, None)

if position_module:
result, position_emulator = PositionEmulator.create(drone)
result, position_emulator = PositionEmulator.create(drone, movement_speed)
if not result:
return False, None

Expand Down
138 changes: 127 additions & 11 deletions modules/hitl/position_emulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""

import time
import math
from ..mavlink import dronekit


Expand All @@ -15,24 +16,34 @@ class PositionEmulator:

@classmethod
def create(
cls, drone: dronekit.Vehicle
cls, drone: dronekit.Vehicle, movement_speed: float = 5.0
) -> "tuple[True, PositionEmulator] | tuple[False, None]":
"""
Setup position emulator.

Args:
drone: The dronekit instance to use for sending MAVLink messages.
movement_speed: Speed of drone movement in m/s, default is 5.0.

Returns:
Success, PositionEmulator instance.
"""

return True, PositionEmulator(cls.__create_key, drone)
return True, PositionEmulator(cls.__create_key, drone, movement_speed)

def __init__(self, class_private_create_key: object, drone: dronekit.Vehicle) -> None:
def __init__(
self, class_private_create_key: object, drone: dronekit.Vehicle, movement_speed: float = 5.0
) -> None:
"""
Private constructor, use create() method.
"""
assert class_private_create_key is PositionEmulator.__create_key, "Use create() method"

self.target_position = (43.43405014107003, -80.57898027451816, 373.0) # lat, lon, alt
self.current_position = (43.43405014107003, -80.57898027451816, 373.0) # lat, lon, alt
self.movement_speed = movement_speed # m/s
self.last_update_time = time.time()
self.waypoint_position = None

self.drone = drone

Expand All @@ -47,12 +58,26 @@ def set_target_position(self, latitude: float, longitude: float, altitude: float
"""
self.target_position = (latitude, longitude, altitude)

def set_waypoint_position(self, latitude: float, longitude: float, altitude: float) -> None:
"""
Manually sets a waypoint for the emulator to move towards.

Args:
latitude: Latitude in degrees.
longitude: Longitude in degrees.
altitude: Altitude in meters.
"""
self.waypoint_position = (latitude, longitude, altitude)
print(
f"HITL Position: Manual waypoint set to {latitude:.6f}, {longitude:.6f}, {altitude:.1f}m"
)

def get_target_position(self) -> tuple[float, float, float]:
"""
Gets the target position from the Ardupilot target.
Gets the target position from the Ardupilot target and stores it as waypoint.

Returns:
Target position as (latitude, longitude, altitude).
Current target position (not the waypoint).
"""
# pylint: disable=protected-access
position_target = None
Expand All @@ -69,24 +94,115 @@ def get_target_position(self) -> tuple[float, float, float]:
latitude = position_target.lat_int / 1e7
longitude = position_target.lon_int / 1e7
altitude = position_target.alt
return (latitude, longitude, altitude)

# Optionally log if no message received
# print("No POSITION_TARGET_GLOBAL_INT message received.")
self.waypoint_position = (latitude, longitude, altitude)

return self.target_position

def calculate_distance(
self, pos1: tuple[float, float, float], pos2: tuple[float, float, float]
) -> float:
"""
Calculate distance between two GPS coordinates using Haversine formula.

Args:
pos1: First position (lat, lon, alt)
pos2: Second position (lat, lon, alt)

Returns:
Distance in meters
"""
lat1, lon1, alt1 = pos1
lat2, lon2, alt2 = pos2

# Convert to radians
lat1_rad = math.radians(lat1)
lon1_rad = math.radians(lon1)
lat2_rad = math.radians(lat2)
lon2_rad = math.radians(lon2)

# Haversine formula for horizontal distance
dlat = lat2_rad - lat1_rad
dlon = lon2_rad - lon1_rad
a = (
math.sin(dlat / 2) ** 2
+ math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2) ** 2
)
c = 2 * math.asin(math.sqrt(a))

earth_radius = 6371000 # meters
horizontal_distance = earth_radius * c

vertical_distance = abs(alt2 - alt1)

return math.sqrt(horizontal_distance**2 + vertical_distance**2)

def interpolate_position(
self, start: tuple[float, float, float], end: tuple[float, float, float], progress: float
) -> tuple[float, float, float]:
"""
Interpolate between two positions.

Args:
start: Starting position (lat, lon, alt)
end: Ending position (lat, lon, alt)
progress: Progress from 0.0 to 1.0

Returns:
Interpolated position (lat, lon, alt)
"""
if progress >= 1.0:
return end
if progress <= 0.0:
return start

lat = start[0] + (end[0] - start[0]) * progress
lon = start[1] + (end[1] - start[1]) * progress
alt = start[2] + (end[2] - start[2]) * progress

return (lat, lon, alt)

def periodic(self) -> None:
"""
Periodic function.
Periodic function that handles gradual movement to waypoints.
"""
current_time = time.time()
dt = current_time - self.last_update_time
self.last_update_time = current_time

self.get_target_position()

self.target_position = self.get_target_position()
# If we have a waypoint and we're not already there
if self.waypoint_position is not None:
distance_to_waypoint = self.calculate_distance(
self.current_position, self.waypoint_position
)

# If we're close enough to the waypoint, consider it reached
if distance_to_waypoint < 1.0:
Copy link
Contributor

@andy24Dai andy24Dai Jan 8, 2026

Choose a reason for hiding this comment

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

tolerance should be a class variable

print(
f"HITL Position: Reached waypoint {self.waypoint_position[0]:.6f}, {self.waypoint_position[1]:.6f}, {self.waypoint_position[2]:.1f}m"
)
self.current_position = self.waypoint_position
self.target_position = self.waypoint_position
self.waypoint_position = None # Clear waypoint
else:
# Move towards the waypoint
distance_to_move = self.movement_speed * dt
progress = min(distance_to_move / distance_to_waypoint, 1.0)
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the progress calculation should take into account the starting position: progress = dist(start_position, current_position) / dist(start_position, waypoint_position)


self.current_position = self.interpolate_position(
self.current_position, self.waypoint_position, progress
)
self.target_position = self.current_position

# Inject the current interpolated position
self.inject_position(
self.target_position[0], self.target_position[1], self.target_position[2]
self.current_position[0], self.current_position[1], self.current_position[2]
)

time.sleep(0.1)

def inject_position(
self,
latitude: float = 43.43405014107003,
Expand Down
9 changes: 7 additions & 2 deletions modules/mavlink/flight_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,11 +211,16 @@ def create(
position_module: bool = False,
camera_module: bool = False,
images_path: str | None = None,
movement_speed: float = 5.0,
) -> "tuple[bool, FlightController | None]":
"""
address: TCP address or serial port of the drone (e.g. "tcp:127.0.0.1:14550").
baud: Baud rate for the connection (default is 57600).
mode: True to enable HITL mode, False or None to disable it.
hitl_enabled: True to enable HITL mode, False to disable it.
position_module: True to enable position emulation.
camera_module: True to enable camera emulation.
images_path: Path to images directory for camera emulation.
movement_speed: Speed of drone movement in m/s for position interpolation (default: 5.0).
Establishes connection to drone through provided address
and stores the DroneKit object.
"""
Expand All @@ -227,7 +232,7 @@ def create(
)
# Enable/disable HITL based on mode
success, hitl_instance = hitl_base.HITL.create(
drone, hitl_enabled, position_module, camera_module, images_path
drone, hitl_enabled, position_module, camera_module, images_path, movement_speed
)
if success:
if hitl_enabled and hitl_instance is not None:
Expand Down
Loading