From 55afae6af1fe52484ca24d4e161c2d516242e3c0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Sep 2025 10:57:26 +0000 Subject: [PATCH 1/3] Initial plan From d592df5c71fa84c32de6f7b2fcadb0ce17bf5a7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:03:06 +0000 Subject: [PATCH 2/3] Add MAVLink log collection script and integrate into postflight Co-authored-by: nielmistry <10933146+nielmistry@users.noreply.github.com> --- ansible/drones_postflight.yml | 17 ++++ ansible/scripts/get_mavlogs.py | 166 +++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 ansible/scripts/get_mavlogs.py diff --git a/ansible/drones_postflight.yml b/ansible/drones_postflight.yml index 3be7a7b..7028fca 100644 --- a/ansible/drones_postflight.yml +++ b/ansible/drones_postflight.yml @@ -37,6 +37,23 @@ remote_user: lis gather_facts: true tasks: + - name: Copy MAVLink log collection script to drone + ansible.builtin.template: + src: scripts/get_mavlogs.py + dest: /tmp/get_mavlogs.py + mode: '0755' + + - name: Download MAVLink logs from flight controller + ansible.builtin.shell: python3 /tmp/get_mavlogs.py + register: mavlink_log_result + failed_when: false # Don't fail the whole playbook if MAVLink logs unavailable + changed_when: false + + - name: Log MAVLink collection result + ansible.builtin.debug: + msg: "MAVLink log collection: {{ mavlink_log_result.stdout_lines | default(['No output']) }}" + when: mavlink_log_result is defined + - name: Pull latest log contents from each drone into its hostname subfolder ansible.posix.synchronize: mode: pull diff --git a/ansible/scripts/get_mavlogs.py b/ansible/scripts/get_mavlogs.py new file mode 100644 index 0000000..990d319 --- /dev/null +++ b/ansible/scripts/get_mavlogs.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +""" +MAVLink log collection script for swarm-nxt drones. +Uses pymavlink to download flight logs from the flight controller. +""" + +from pymavlink import mavutil +from loguru import logger +import os +import sys +import time +from datetime import datetime +import signal +import threading + +# Configure logging +os.makedirs("{{ drone_base_path }}/logs/scripts/get_mavlogs", exist_ok=True) +logger.add(f"{{ drone_base_path }}/logs/scripts/get_mavlogs/{datetime.now().strftime('%Y%m%d_%H%M%S')}.log") + +# Global shutdown event +shutdown_event = threading.Event() + +def signal_handler(signum, frame): + """Handle shutdown signals gracefully""" + logger.info(f"Received signal {signum}, initiating shutdown") + shutdown_event.set() + +def get_mavlink_logs(connection_string, log_dir): + """ + Download available MAVLink logs from flight controller + + Args: + connection_string (str): MAVLink connection string (e.g., "/dev/ttyTHS1:921600") + log_dir (str): Directory to save downloaded logs + + Returns: + bool: True if successful, False otherwise + """ + try: + logger.info(f"Connecting to flight controller at {connection_string}") + conn = mavutil.mavlink_connection(connection_string) + + # Wait for heartbeat to confirm connection + logger.info("Waiting for heartbeat...") + conn.wait_heartbeat(timeout=10) + logger.info(f"Connected to system {conn.target_system}, component {conn.target_component}") + + # Request log list + logger.info("Requesting log list...") + conn.mav.log_request_list_send( + conn.target_system, + conn.target_component, + 0, # start + 0xffff # end (all logs) + ) + + # Collect log entries + log_entries = [] + start_time = time.time() + timeout = 10 # seconds + + while time.time() - start_time < timeout and not shutdown_event.is_set(): + msg = conn.recv_match(type=['LOG_ENTRY'], timeout=1) + if msg: + if msg.get_type() == 'LOG_ENTRY': + log_entries.append(msg) + logger.info(f"Found log {msg.id}: {msg.num_logs} total logs, size: {msg.size} bytes") + + if not log_entries: + logger.warning("No logs found on flight controller") + return True # Not an error, just no logs available + + logger.info(f"Found {len(log_entries)} log entries") + + # Download each log + for log_entry in log_entries: + if shutdown_event.is_set(): + break + + log_id = log_entry.id + log_size = log_entry.size + + if log_size == 0: + logger.warning(f"Skipping log {log_id} (size is 0)") + continue + + logger.info(f"Downloading log {log_id} ({log_size} bytes)...") + + # Create output filename + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + log_filename = os.path.join(log_dir, f"mavlink_log_{log_id}_{timestamp}.bin") + + # Request log data + conn.mav.log_request_data_send( + conn.target_system, + conn.target_component, + log_id, + 0, # start offset + log_size # count (entire log) + ) + + # Collect log data + log_data = bytearray() + bytes_received = 0 + data_timeout = 30 # seconds + data_start_time = time.time() + + while bytes_received < log_size and not shutdown_event.is_set(): + if time.time() - data_start_time > data_timeout: + logger.error(f"Timeout waiting for log data for log {log_id}") + break + + msg = conn.recv_match(type=['LOG_DATA'], timeout=2) + if msg and msg.get_type() == 'LOG_DATA' and msg.id == log_id: + # Append the data + data_chunk = msg.data[:msg.count] # Only use the valid bytes + log_data.extend(data_chunk) + bytes_received += msg.count + + if bytes_received % 1024 == 0: # Log progress every KB + logger.debug(f"Received {bytes_received}/{log_size} bytes for log {log_id}") + + if bytes_received == log_size: + # Save log to file + with open(log_filename, 'wb') as f: + f.write(log_data) + logger.info(f"Successfully downloaded log {log_id} to {log_filename}") + else: + logger.error(f"Incomplete download for log {log_id}: {bytes_received}/{log_size} bytes") + + conn.close() + return True + + except Exception as e: + logger.error(f"Error downloading MAVLink logs: {e}") + return False + +def main(): + """Main function""" + # Set up signal handlers + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # Configuration + connection_string = "{{ drone_fcu_url }}" + log_dir = "{{ drone_base_path }}/logs/latest/mavlink" + + # Create log directory + os.makedirs(log_dir, exist_ok=True) + + logger.info("Starting MAVLink log collection") + logger.info(f"Connection: {connection_string}") + logger.info(f"Log directory: {log_dir}") + + # Download logs + success = get_mavlink_logs(connection_string, log_dir) + + if success: + logger.info("MAVLink log collection completed successfully") + sys.exit(0) + else: + logger.error("MAVLink log collection failed") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file From 300fce87d3892c4c2c1d0bcd939d4de51ac449d7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Sep 2025 11:05:26 +0000 Subject: [PATCH 3/3] Add documentation for MAVLink log collection and postflight procedures Co-authored-by: nielmistry <10933146+nielmistry@users.noreply.github.com> --- docs/flying.md | 24 +++++++++++++++++++++++ docs/software-common-tasks.md | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/docs/flying.md b/docs/flying.md index 541da17..72a6961 100644 --- a/docs/flying.md +++ b/docs/flying.md @@ -30,3 +30,27 @@ This will update apt repositories, pull the latest version of ros packages, and "file": "/swarm-nxt/demos/drones_update.cast" } ``` + +## Post-Flight Procedures + +### Log Collection + +After completing flight operations, collect all flight data and logs using: + +```bash +cd /path/to/swarmnxtrepo/ansible +ansible-playbook -i inventory.ini drones_postflight.yml -K +``` + +This will automatically: +- Download MAVLink logs from each drone's flight controller +- Collect ROS bag files and system logs from all drones +- Consolidate logs into timestamped directories on the host computer +- Create convenient symlinks for accessing the latest flight data + +Collected data includes: +- **ROS bag files**: High-level flight data, sensor readings, commands +- **MAVLink logs**: Low-level flight controller data, parameters, system status +- **System logs**: Service status, error messages, performance metrics + +For more details on log analysis and data handling, see [Data Logging with ROS Bag](software-common-tasks.md#data-logging-with-ros-bag). diff --git a/docs/software-common-tasks.md b/docs/software-common-tasks.md index fac980e..b72e394 100644 --- a/docs/software-common-tasks.md +++ b/docs/software-common-tasks.md @@ -207,6 +207,42 @@ ros2 run plotjuggler plotjuggler --buffer-size 100000 # Then: File -> Load Data -> Load ROS2 bag ``` +### Postflight Log Collection + +The SwarmNXT system includes automated postflight log collection that gathers all relevant flight data from drones into a centralized location. This includes both ROS bag data and MAVLink flight controller logs. + +#### MAVLink Log Collection + +In addition to ROS bag data, the system automatically downloads MAVLink logs directly from each drone's flight controller using pymavlink. These logs contain: +- Low-level flight controller data +- Parameter changes +- System status and error messages +- Detailed flight performance metrics + +MAVLink logs are saved to: +- **Location**: `${drone_base_path}/logs/latest/mavlink/` on each drone +- **Format**: Binary `.bin` files (standard ArduPilot format) +- **Naming**: `mavlink_log_{id}_{timestamp}.bin` + +#### Running Postflight Collection + +```bash +# Collect logs from all configured drones +cd /path/to/swarmnxt/ansible +ansible-playbook -i inventory.ini drones_postflight.yml -K +``` + +This will: +1. Download MAVLink logs from each drone's flight controller +2. Collect all log files from each drone +3. Consolidate them into timestamped directories on the host computer +4. Create symlinks for easy access to latest logs + +Collected logs are stored in: +- **Host location**: `${host_base_path}/consolidated_logs/latest/` +- **Structure**: `{hostname}/` subdirectories for each drone +- **Contents**: ROS bags, MAVLink logs, and system logs + ## Check EKF Tracking To check EKF tracking, perform the following steps: