Files
paparazzi/sw/tools/opti_dist/dist.py
T
Christophe De Wagter cf7c8b3797
Issues due date / Add labels to issues (push) Has been cancelled
Doxygen / build (push) Has been cancelled
MAVLab Course2026 Updates (p1) (#3632)
* updated distance measurement script.
- automatically shows which ids are available
- filter out big jumps
- plot_summary.py shows distance over time with recording regions

* Adding config file for radiomaster pocket joystick ble/usb (#108)

* added radiomaster tx16s xml (#123)

Co-authored-by: Wiebe van der Knaap <wkvanderknaap@tudelft.nl>

* Fix joystick device argument parsed as single token in control panel sessions (#118)

The `-d 0` joystick device flag was passed as a single `flag` attribute,
causing the joystick program to receive it as one token instead of two
separate arguments. This prevented the device number from being recognized,
breaking joystick input in the Simulation - Gazebo and Flight UDP sessions.

Fixed by splitting into `<arg flag="-d" constant="0"/>`.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* fixed names of variables and resolution bugs, added documentation (#113)

Co-authored-by: macoman <macoman@student.tudelft.nl>

* Update Gazebo Models: Gate, Plants, Logo

* Added some (math) tests (#114)

* added a test for paparazzi's math librarie's int sqrt function and int quaternion normalization function
* Keep essential tests

Reduced the number of tests planned from 9 to 6 and removed tests for int32_sqrt.

---------

Co-authored-by: LSSchef <l.s.scheffer@student.tudelft.nl>
Co-authored-by: AniketBehura <aniketbehura1023@gmail.com>
Co-authored-by: diaa <D.abbasi@student.tudelft.nl>

* Feat: readme update for submodule installation (#115)

* feat: readme update for submodule installation

* Rename README to README.md

---------

Co-authored-by: Christophe De Wagter <dewagter@gmail.com>

---------

Co-authored-by: robinferede <robinferede@tudelft.nl>
Co-authored-by: Robin Euger <robin.euger@gmail.com>
Co-authored-by: Wiebe van der Knaap <wkvanderknaap@tudelft.nl>
Co-authored-by: EAbbenhuis <113993394+EAbbenhuis@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Mihai Coman <127535163+miki133@users.noreply.github.com>
Co-authored-by: macoman <macoman@student.tudelft.nl>
Co-authored-by: Swayam Kuckreja <110131770+swayamkuckreja@users.noreply.github.com>
Co-authored-by: LSSchef <l.s.scheffer@student.tudelft.nl>
Co-authored-by: AniketBehura <aniketbehura1023@gmail.com>
Co-authored-by: diaa <D.abbasi@student.tudelft.nl>
Co-authored-by: Douwe-Rijs <Douwe@standofl.nl>
2026-04-09 15:37:28 +02:00

194 lines
5.9 KiB
Python
Executable File

#!/usr/bin/env python3
import time
from datetime import datetime
import sys
import os
import math
import socket
import struct
import argparse
import matplotlib.pyplot as plt
PPRZ_HOME = os.getenv("PAPARAZZI_HOME", os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../../')))
sys.path.append(PPRZ_HOME + "/sw/ground_segment/python/natnet3.x")
from NatNetClient import NatNetClient
def discover_motive(multicast="239.255.42.99", data_port=1511, timeout=3):
"""Discover a Motive server by listening for NatNet multicast data."""
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.settimeout(timeout)
sock.bind(('', data_port))
# Join the NatNet multicast group
mreq = struct.pack('4sL', socket.inet_aton(multicast), socket.INADDR_ANY)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
try:
_, addr = sock.recvfrom(4096)
sock.close()
return addr[0]
except socket.timeout:
sock.close()
return None
# Global state
recording = False
pos_x, pos_y, pos_z = 0.0, 0.0, 0.0
track_id = None
frame_count = 0
seen_ids = set()
def on_press(event):
global recording
if event.key == 'r':
recording = not recording
if event.key == 'q':
plt.close()
def receive_rigid_body_frame(rigid_body_data, stamp):
global pos_x, pos_y, pos_z, frame_count, seen_ids
frame_count += 1
for rb in rigid_body_data.rigid_body_list:
seen_ids.add(rb.id_num)
if track_id is not None and rb.id_num != track_id:
continue
pos_x = rb.pos[0]
pos_y = rb.pos[1]
pos_z = rb.pos[2]
def main(args):
global track_id
# Discover or use provided server IP
if args.server:
server_ip = args.server
print(f"Using provided server IP: {server_ip}")
else:
print("Discovering Motive server on the network...")
server_ip = discover_motive()
if server_ip is None:
print("ERROR: No Motive server found. Check that Motive is running and streaming.")
print("You can also specify the IP manually with --server <ip>")
return
print(f"Found Motive server at {server_ip}")
client = NatNetClient()
client.server_ip_address = server_ip
client.local_ip_address = "0.0.0.0"
client.set_print_level(0)
client.rigid_body_list_listener = receive_rigid_body_frame
client.run()
print("Waiting for rigid body data...")
time.sleep(3)
if frame_count == 0:
print("ERROR: No data received. Check that Motive is streaming.")
client.shutdown()
return
print(f"Receiving data: {frame_count} frames, rigid body IDs: {sorted(seen_ids)}")
# Pick which rigid body to track
if len(seen_ids) == 1:
track_id = list(seen_ids)[0]
print(f"Auto-selected ID {track_id}")
else:
while True:
choice = input(f"Enter rigid body ID to track {sorted(seen_ids)}: ").strip()
try:
choice = int(choice)
if choice in seen_ids:
track_id = choice
break
print(f"ID {choice} not available.")
except ValueError:
print("Please enter a valid integer.")
print(f"Tracking ID {track_id}. Press r to record, q to quit.")
# Output file with timestamp and rigid body ID
if args.outputfile:
output_path = args.outputfile
else:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = f"dist_rb{track_id}_{timestamp}.csv"
file = open(output_path, 'w')
file.write('time, distance, x, y, z, recording\n')
print(f"Writing data to {output_path}")
# Setup plot
fig = plt.figure()
plt.axis([-6, 6, -6, 6])
fig.canvas.mpl_connect('key_press_event', on_press)
old_x = pos_x
old_z = pos_z
distance = 0
glitch_count = 0
start_time = time.time()
pre_time = time.time()
freq_count = 0
freq_time = time.time()
data_freq = 0.0
while plt.fignum_exists(fig.number):
h = math.hypot(pos_x - old_x, pos_z - old_z)
if h > 1.0:
# Glitch: jump too large, ignore but update position
glitch_count += 1
old_x = pos_x
old_z = pos_z
elif h > 0.10:
if recording:
distance += h
old_x = pos_x
old_z = pos_z
# Track data frequency
freq_count += 1
now = time.time()
if now - freq_time >= 1.0:
data_freq = freq_count / (now - freq_time)
freq_count = 0
freq_time = now
if now - pre_time > 0.1:
current_time = now - start_time
print(f'distance: {distance:.2f} m; time: {current_time:.2f} s; freq: {data_freq:.0f} Hz; recording: {recording}')
pre_time = now
data = f'{current_time}, {distance}, {pos_x}, {pos_y}, {pos_z}, {recording}\n'
file.write(data)
rec_str = "RECORDING" if recording else "NOT RECORDING"
plt.title(f'Distance: {distance:.2f} m | {data_freq:.0f} Hz | {rec_str}\nPress r to record, q to quit')
if recording:
plt.plot(pos_z, pos_x, 'ro')
else:
plt.plot(pos_z, pos_x, 'bo')
plt.pause(0.001)
client.shutdown()
file.close()
print(f"Total distance: {distance:.2f} m ({glitch_count} glitches ignored)")
print(f"Data saved to {output_path}")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Distance counter using Optitrack NatNet stream")
parser.add_argument('--server', default=None, help="Motive server IP (auto-discovers if not set)")
parser.add_argument('--outputfile', type=str, default=None, help="CSV output file (default: dist_rb<id>_<timestamp>.csv)")
args = parser.parse_args()
main(args)