Skip to content

feat(AnalyzeView): add MAVLink FTP log download for PX4 .ulg log files#13996

Draft
dakejahl wants to merge 1 commit intomavlink:masterfrom
dakejahl:jake/px4_log_download_mavftp
Draft

feat(AnalyzeView): add MAVLink FTP log download for PX4 .ulg log files#13996
dakejahl wants to merge 1 commit intomavlink:masterfrom
dakejahl:jake/px4_log_download_mavftp

Conversation

@dakejahl
Copy link
Collaborator

Feature Description

Adds MAVLink FTP-based log download for PX4 vehicles, providing faster and more
reliable log transfers than the legacy LOG_REQUEST_DATA protocol. When a PX4
vehicle reports MAV_PROTOCOL_CAPABILITY_FTP, the log download page automatically
uses MAVFTP to list and download .ulg files from /fs/microsd/log.

The legacy LOG_REQUEST protocol remains fully functional for ArduPilot and
non-FTP-capable vehicles.

Implementation Details

  • Auto-detects FTP capability on PX4 vehicles via MAV_PROTOCOL_CAPABILITY_FTP
  • Walks PX4 log directory tree via FTPManager::listDirectory (date subdirs → .ulg files)
  • Downloads selected logs sequentially via FTPManager::download with progress/rate display
  • All MAVFTP code guarded by QGC_ENABLE_MAVFTP_LOG_DOWNLOAD cmake option (ON by default)
    so the legacy protocol can be isolated or deprecated via build flag in the future
  • Explicit MavftpListState enum tracks listing phase (root vs subdirectory)
  • Proper state cleanup on vehicle switch and cancellation

Testing

  • Tested locally
  • Added/updated unit tests
  • Tested with simulator (SITL)
  • Tested with hardware

Platforms Tested

  • Linux
  • Windows
  • macOS
  • Android
  • iOS

Flight Stacks Tested

  • PX4
  • ArduPilot (legacy path unchanged)

Checklist

  • My code follows the project's coding standards
  • I have added tests that prove my feature works
  • New and existing unit tests pass locally

Known Limitations

  • eraseAll() still uses legacy LOG_ERASE MAVLink message when MAVFTP is active
    (PX4 should still honor this, but FTP-based deletion could be a follow-up)
  • Unit tests cover only the legacy path; MAVFTP test would require MockLink FTP
    directory listing support (follow-up)

By submitting this pull request, I confirm that my contribution is made under
the terms of the project's dual license (Apache 2.0 and GPL v3).

When a PX4 vehicle reports MAV_PROTOCOL_CAPABILITY_FTP, the log download
page uses MAVFTP to list and download .ulg files from /fs/microsd/log.
Legacy LOG_REQUEST protocol remains for ArduPilot and non-FTP vehicles.

MAVFTP code is guarded by QGC_ENABLE_MAVFTP_LOG_DOWNLOAD (ON by default)
so the legacy path can be isolated or deprecated in the future.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 17, 2026 08:46
@dakejahl dakejahl marked this pull request as draft February 17, 2026 08:46
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds MAVLink FTP-based log download capability for PX4 vehicles to the AnalyzeView, providing a faster and more reliable alternative to the legacy LOG_REQUEST_DATA protocol. When a PX4 vehicle reports the MAV_PROTOCOL_CAPABILITY_FTP capability, the system automatically uses MAVFTP to list and download .ulg files from /fs/microsd/log. The implementation is guarded by a QGC_ENABLE_MAVFTP_LOG_DOWNLOAD CMake option (ON by default), allowing the feature to be disabled at build time if needed.

Changes:

  • Adds MAVFTP-based log listing by walking the PX4 log directory tree (date subdirectories → .ulg files)
  • Implements sequential log downloads via FTPManager with progress tracking and download rate display
  • Maintains backward compatibility with the legacy LOG_REQUEST protocol for ArduPilot and non-FTP vehicles
  • Includes proper state management with cleanup on vehicle switch and cancellation

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/AnalyzeView/LogEntry.h Adds ftpPath field to QGCLogEntry (guarded by QGC_MAVFTP_LOG_DOWNLOAD) to store remote file path
src/AnalyzeView/LogDownloadController.h Declares MAVFTP methods, state machine enum, and state variables for tracking listing/download progress
src/AnalyzeView/LogDownloadController.cc Implements MAVFTP directory listing, file download, state management, and integrates with existing legacy code paths
src/AnalyzeView/CMakeLists.txt Conditionally defines QGC_MAVFTP_LOG_DOWNLOAD preprocessor macro based on CMake option
cmake/CustomOptions.cmake Adds QGC_ENABLE_MAVFTP_LOG_DOWNLOAD CMake option (default ON)
cmake/PrintSummary.cmake Adds build summary output for the MAVFTP log download option


const QString ftpPath = QString(kPx4LogRoot) + QStringLiteral("/") + currentDir + QStringLiteral("/") + fileName;

QGCLogEntry *logEntry = new QGCLogEntry(_mavftpLogIdCounter++, dateTime, fileSize, true, this);
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The QGCLogEntry object is created with this as the parent, which differs from the legacy log download path (line 209) where entries are created without a parent. While both approaches work with clearAndDeleteContents() calling deleteLater(), setting the parent could cause the entry to persist longer than intended if the model is cleared while the controller exists. Consider creating the entry without a parent for consistency with the legacy path.

Suggested change
QGCLogEntry *logEntry = new QGCLogEntry(_mavftpLogIdCounter++, dateTime, fileSize, true, this);
QGCLogEntry *logEntry = new QGCLogEntry(_mavftpLogIdCounter++, dateTime, fileSize, true);

Copilot uses AI. Check for mistakes.

qCDebug(LogDownloadControllerLog) << "MAVFTP: listing subdir" << path;

if (!_vehicle->ftpManager()->listDirectory(MAV_COMP_ID_AUTOPILOT1, path)) {
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

Missing null check for _vehicle before calling _vehicle->ftpManager(). If the vehicle changes while MAVFTP listing is in progress, this could cause a null pointer dereference. Add a check similar to line 880 in _downloadLogMavftp.

Suggested change
if (!_vehicle->ftpManager()->listDirectory(MAV_COMP_ID_AUTOPILOT1, path)) {
if (!_vehicle) {
qCWarning(LogDownloadControllerLog) << "MAVFTP: no active vehicle, aborting subdir listing for" << path;
_mavftpListState = MavftpIdle;
_mavftpDirsToList.clear();
_receivedAllEntries();
return;
}
auto* ftpManager = _vehicle->ftpManager();
if (!ftpManager) {
qCWarning(LogDownloadControllerLog) << "MAVFTP: no FTP manager, aborting subdir listing for" << path;
_mavftpListState = MavftpIdle;
_mavftpDirsToList.clear();
_receivedAllEntries();
return;
}
if (!ftpManager->listDirectory(MAV_COMP_ID_AUTOPILOT1, path)) {

Copilot uses AI. Check for mistakes.
if (!_ftpDownloadQueue.isEmpty()) {
_downloadLogMavftp(_ftpDownloadQueue.dequeue());
} else {
_setDownloading(false);
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The _setDownloading(false) call could occur after the vehicle has been changed to null (via _setActiveVehicle), since FTPManager signals may arrive asynchronously. While _ftpCurrentDownloadEntry is checked at line 936, _vehicle could still become null before reaching line 954. Consider adding a vehicle null check before calling _setDownloading(false), or ensure _setDownloading handles null _vehicle gracefully.

Suggested change
_setDownloading(false);
if (_vehicle) {
_setDownloading(false);
}

Copilot uses AI. Check for mistakes.
Comment on lines +767 to +774
void LogDownloadController::_mavftpListDirComplete(const QStringList &dirList, const QString &errorMsg)
{
if (!errorMsg.isEmpty()) {
qCWarning(LogDownloadControllerLog) << "MAVFTP listing error:" << errorMsg;
_mavftpListState = MavftpIdle;
_receivedAllEntries();
return;
}
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The callback _mavftpListDirComplete can be invoked asynchronously from FTPManager after the vehicle has been changed to null via _setActiveVehicle. While signals are disconnected during vehicle change (line 174), there may be a race condition where a signal is already queued. When _receivedAllEntries() is called (line 772), it calls _setListing(false) which dereferences _vehicle without a null check. Consider adding a guard check for _vehicle at the start of this method.

Copilot uses AI. Check for mistakes.
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

if(QGC_ENABLE_MAVFTP_LOG_DOWNLOAD)
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE QGC_MAVFTP_LOG_DOWNLOAD)
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

The CMake option is named QGC_ENABLE_MAVFTP_LOG_DOWNLOAD but the compile definition is QGC_MAVFTP_LOG_DOWNLOAD (missing ENABLE_). For consistency with other features in the codebase (e.g., QGC_ENABLE_BLUETOOTH at src/Comms/CMakeLists.txt:78), consider using the same name for both the option and the definition.

Suggested change
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE QGC_MAVFTP_LOG_DOWNLOAD)
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE QGC_ENABLE_MAVFTP_LOG_DOWNLOAD)

Copilot uses AI. Check for mistakes.
@@ -427,6 +474,23 @@ bool LogDownloadController::_prepareLogDownload()
void LogDownloadController::refresh()
{
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

Calling refresh() while an MAVFTP download is in progress could cause issues. The clearAndDeleteContents() call at line 476 will delete all log entries, including _ftpCurrentDownloadEntry if it's still referenced. When the download completes and _ftpDownloadComplete tries to access _ftpCurrentDownloadEntry->setStatus(), it could access a deleted object. Consider adding a guard to prevent refresh during active downloads, or ensure ongoing downloads are properly canceled before clearing the model.

Suggested change
{
{
#ifdef QGC_MAVFTP_LOG_DOWNLOAD
// Prevent clearing the model while a MAVFTP download is in progress, since
// that would delete entries that may still be referenced by the FTP logic.
if (_useMavftp && _downloading) {
qCWarning(LogDownloadControllerLog) << "refresh requested while MAVFTP download is in progress - ignoring refresh";
return;
}
#endif

Copilot uses AI. Check for mistakes.
@github-actions
Copy link
Contributor

github-actions bot commented Feb 17, 2026

Build Results

Platform Status

Platform Status Details
Linux Passed View
Windows Passed View
MacOS Passed View
Android Failed View

Some builds failed.

Pre-commit

Check Status Details
pre-commit Failed (non-blocking) View

Pre-commit hooks: 12 passed, 59 failed, 3 skipped.

Test Results

linux_gcc_64: 73 passed, 0 skipped

Total: 73 passed, 0 skipped

Code Coverage

Coverage Baseline Change
100.0% 100.0% +0.0%

Artifact Sizes

Artifact Size Δ from master
QGroundControl-aarch64.AppImage 189.44 MB
QGroundControl-installer-AMD64-ARM64.exe 75.50 MB -0.00 MB (decrease)
QGroundControl-installer-AMD64.exe 163.58 MB +0.01 MB (increase)
QGroundControl-installer-ARM64.exe 76.45 MB +0.00 MB (increase)
QGroundControl-x86_64.AppImage 177.37 MB
QGroundControl.dmg 315.74 MB

Total size increased by 0.01 MB

Updated: 2026-02-17 09:47:07 UTC • Triggered by: Android

Copy link
Collaborator Author

@dakejahl dakejahl left a comment

Choose a reason for hiding this comment

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

I need to review this more thoroughly. I tested it and it does work. Over USB I am seeing 350KB/s.


// --- MAVFTP log listing ---

static constexpr const char *kPx4LogRoot = "/fs/microsd/log";
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This needs to be something more generic that the FC can reinterpret as the root log dir

Suggested change
static constexpr const char *kPx4LogRoot = "/fs/microsd/log";
static constexpr const char *kPx4LogRoot = "@LOGS_DIR";


#ifdef QGC_MAVFTP_LOG_DOWNLOAD
if (_useMavftp) {
// TODO: Implement MAVFTP-based log erasure (FTP file removal)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe just continue to use the LOG_ERASE for mass erase. Individual erase is too slow and I don't see the use.

@julianoes
Copy link
Contributor

I would make it a separate tab with general file access rather than replacing the existing log file tab, as that would generally be good to have.

@HTRamsey
Copy link
Collaborator

Is there any value in having that cmake option or just enable it always when PX4 is detected?

@dakejahl
Copy link
Collaborator Author

I would make it a separate tab with general file access rather than replacing the existing log file tab, as that would generally be good to have.

I did think about that. I agree another tab for general file access would be super nice. My thinking was that by replacing the Log Download page with FTP is we could eventually deprecate and remove the non-FTP impl of log download in PX4.

Is there any value in having that cmake option or just enable it always when PX4 is detected?

Yeah probably not much value. And yes, this may also work for Ardupilot too and we should ask them if we should prefer it.

The primary motivating factor for this PR is to avoid link spam during log download. The LOG_DATA doesn't have target sys/comp ID fields so during bulk download all endpoints on a mavlink-router system get flooded.

@julianoes
Copy link
Contributor

@dakejahl are you aware of the fact that we are just discussing this over here?
mavlink/mavlink-devguide#668

@dakejahl
Copy link
Collaborator Author

Thanks I was not aware. Then maybe @peterbarker can comment: would Ardupilot prefer FTP over LOG_DATA as the default mechanism for log download in QGC?

@peterbarker
Copy link
Contributor

Thanks I was not aware. Then maybe @peterbarker can comment: would Ardupilot prefer FTP over LOG_DATA as the default mechanism for log download in QGC?

Transfer via the traditional mechanism is actually marginally faster on ArduPilot - 800kB/s or there-abouts vs 750kB/s on USB.

I don't think this really needs to be any form of "default". Ask the vehicle what file it should look at to get a list of logs per #668. In ArduPilot we may synthesise that listing entirely, but at the very least it will allow us to abstract out the actual path to the log files. On stm32-based boards these are in APM/LOGS on every in-tree board. But on Linux the FTP client gets free access to the root of the SD card and the logs actually appear in several different places, depending on the specific Linux board. The contents of the directory are always the same, 'though there's LASTLOG.txt in there to gum up the words for automation.

I'm pretty sure the string px4 shouldn't be appearing in the files you're modifying here; resource discovery should make that not an issue. If the resource discovery fails then the existing interface can be used.

@DonLakeFlyer
Copy link
Collaborator

Transfer via the traditional mechanism is actually marginally faster on ArduPilot - 800kB/s or there-abouts vs 750kB/s on USB.

That would indicate something is wrong somewhere wouldn't it. The FTP packets have more room for data than the LOG ones.

@peterbarker
Copy link
Contributor

peterbarker commented Feb 18, 2026 via email

@hamishwillee
Copy link
Collaborator

hamishwillee commented Feb 18, 2026

A couple of things to note from #13996 and its discussion:

  • We're trying to define @MAV_LOG as a virtual drive prefix so that the location of the files is standardized and abstracted from flight stacks.
  • Some flight stacks might store logs on file systems that don't support per-file removal. This means that deleting individual files will fail. However you can still implement the same behaviour as the current UI's LOG_ERASE by deleting a whole folder.
  • If we can't agree a standard virtual drive, then we should add a log location to COMPONENT_METADATA such that a GCS can fetch the generic location of files. This might be on a server somewhere (suggestion courtesy of @peterbarker)

PS FWIW Burst mode should be lots faster.

@peterbarker
Copy link
Contributor

peterbarker commented Feb 18, 2026 via email

@hamishwillee
Copy link
Collaborator

... I think this simply shows that the COMPONENT_METADATA thing is complicated and shouldn't block the virtual filesystem suggestion here.

IMO COMPONENT_METADATA is simpler

  • because the device says where it stores its logs and a GCS reads that and uses whatever URL it is given - HTTP or MAVFTP or whatever.
  • If you already have COMPONENT_METADATA it is much easier because you don't need to change MAVFTP at all.

But I still want to use virtual drives such as @MAVLINK_LOG because

  • COMPONENT_METADATA has been around for ever and despite obvious benefits still not adopted by ArduPilot.
  • Virtual drives in MAVFTP likely to be easier to standardize on, and in no way block COMPONENT_METADATA based approach.

@dakejahl I'd be interested on your thoughts. Would PX4 be interested in providing an abstraction to a log virtual drive?

@julianoes
Copy link
Contributor

@dakejahl I'd be interested on your thoughts. Would PX4 be interested in providing an abstraction to a log virtual drive?

I would think so.

@peterbarker
Copy link
Contributor

Transfer via the traditional mechanism is actually marginally faster on ArduPilot - 800kB/s or there-abouts vs 750kB/s on USB.

That would indicate something is wrong somewhere wouldn't it. The FTP packets have more room for data than the LOG ones.

A little more info. In addition to the above speeds (CubeRedPrimary), I get about the same log-download rates over a 100Mbit TCP connection.

OTOH, over http (using a http server written in Lua running on CubeRedPrimary):

$ wget http://172.16.3.68:8080/mnt/APM/LOGS/00000001.BIN
--2026-02-18 17:37:32--  http://172.16.3.68:8080/mnt/APM/LOGS/00000001.BIN
Connecting to 172.16.3.68:8080... connected.
HTTP request sent, awaiting response... 200 OK
Length: 38366640 (37M) [application/octet-stream]
Saving to: ‘00000001.BIN’

00000001.BIN        100%[===================>]  36.59M  4.18MB/s    in 8.8s    

2026-02-18 17:37:41 (4.15 MB/s) - ‘00000001.BIN’ saved [38366640/38366640]

$ 

I've varying reports on how far you can push that up towards the limits implied by the ethernet bitrate.

@peterbarker
Copy link
Contributor

tridge suggests that giving the client a selection of URLs to download the logs from would be a good idea. e.g. http will likely be fastest but the client may not have a route.

@dakejahl
Copy link
Collaborator Author

Virtual drive for logs makes sense. I like the idea of using a file to provide the log list and associated URLs. I'd be happy to help work on this both in PX4 and QGC. Can we put together a spec into a doc somewhere?

This is my understanding

  • GCS requests logs lists (via FTP, asking for file @LOG_LIST which is an alias for the actual location?)
  • FC sends down the file which contains logs metadata: index, datetime, URL
  • GCS FTPs from the URL (which could be onboard the FC or in the cloud somewhere)

It's that simple?

@peterbarker
Copy link
Contributor

peterbarker commented Feb 26, 2026 via email

@hamishwillee
Copy link
Collaborator

@dakejahl For the MAVFTP part, we're suggesting @MAV_LOG for the virtual directory name. I think we have broad agreement on this mavlink/mavlink-devguide#668 PR - needs prototype if you're up for it?

This doesn't suggest anything about log lists or whatever, but there is no reason this could not be standardized.

Component metadata might further specify the log location for logs that might be exposed via HTTPS - of which this could be one option.

@peterbarker
Copy link
Contributor

peterbarker commented Mar 5, 2026 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants