Skip to content

fix: log download truncation and lossy-link stalls, add cancellation and protocol tests - #24

Merged
Rouniy merged 8 commits into
Rouniy:masterfrom
Poholos:fix/log-download-cancel-tests
Aug 30, 2026
Merged

fix: log download truncation and lossy-link stalls, add cancellation and protocol tests#24
Rouniy merged 8 commits into
Rouniy:masterfrom
Poholos:fix/log-download-cancel-tests

Conversation

@userepo

@userepo userepo commented Aug 28, 2026

Copy link
Copy Markdown

Hardens the MAVLink dataflash log download end to end, ported and extended from
the protocol test work in userepo/MissionPlanner (upstream PR ArduPilot#3764).

Bug fixes

  • Silent download truncation. LogDownloadTracker trusted the first short
    packet as the log end, so a stale short retransmit of an earlier block locked in
    a too-small TotalLength and SetLength silently truncated the file. A short
    packet is now only trusted at the highest offset seen, and only frontier-near
    packets can raise that bar — so a corrupt far-offset packet can neither poison
    end detection nor lengthen the returned file.
  • Phantom log end from a corrupt short packet (found by review on the
    upstream PR). A packet both short and far cleared the highest-offset bar
    trivially and ended the download at a phantom length — below the true end it
    silently truncated the returned file. Rejecting far end packets outright is
    not an option either: packet loss stalls the trusted frontier at the first
    dropped block, so the genuine end of a large log always arrives far past it
    (that variant passes every unit test and never finishes the lossy SITL run —
    each recovered gap forces a full re-stream). A far end packet is now a
    deferred candidate, discarded if any more data arrives and accepted once the
    stream goes quiet: a corrupt packet is followed by more stream, the real end
    is not.
  • Lossy links effectively unusable. The repair phase served one missing range
    per ~3 s silence window. At 5% packet loss a 2.3 MB log leaves ~1,290 scattered
    single-block gaps — roughly 65 minutes projected; the SITL run did not finish in
    10. Repair requests now chain the moment the current one is satisfied (gated on
    actual coverage progress, so duplicated packets cannot multiply requests), with a
    500 ms repair-phase silence fallback. The same scenario now completes in ~77 s
    in a single streaming pass, byte-identical (~3 s of that is the silence window
    confirming the deferred end above).
  • Silence tolerance. The retry budget is time-based (retryLimit x LogRetryDelayMs, ~30 s), so the short repair windows no longer shrink the
    total silence a fading radio is allowed before the download aborts. Data
    beyond the known log end is ignored.

Cancellation

GetLog takes a CancellationToken; the download window gets a Cancel button
(enabled only while a download actually runs), a per-batch token source, and a
cancel that also covers the copy/KML tail — LOG_REQUEST_END and temp-file
cleanup already run on that path.

Download window layout

The six toolbar items need ~830 px in one row; the horizontal StackPanel
painted "Erase All…" (and the new Cancel) past the 540 px default width. The
toolbar now wraps (WrapPanel with ItemSpacing/LineSpacing, per the sibling
views) and the window gains MinWidth/MinHeight. A LayoutOverflowTests
theory guards every toolbar child inside the window at 420 and 540 px.

Testing

  • 15 fake-vehicle protocol tests (GetLogProtocolTests): ordering, exact
    block-multiple logs, loss recovery, stray short retransmits, corrupt short-far
    packets (including the silent-truncation case), a far end marker past a
    stalled frontier, duplicate storms, the silence time budget, beyond-end data,
    oversized counts, empty logs, cancellation, and timeout — plus tracker unit
    tests. The truncation, corrupt-packet, duplicate storm, and budget tests all
    fail against the previous code.

  • A manual SITL end-to-end harness with a 5%-loss proxy
    (MissionPlannerTests/Avalonia/MissionPlanner.SitlTests, registered as retained
    tooling in PROJECT_ARTIFACT_AUDIT.tsv; checkpoint recorded in
    Porting/STATUS.md). Reference results, 2.3 MB log against real ArduCopter SITL:

    scenario before after
    clean link 0.44 s, byte-identical 0.44 s, byte-identical + cancel verified
    5% LOG_DATA loss did not complete in 10 min 77 s, byte-identical, one streaming pass

Full suite: 1519/1531 — the failures are the pre-existing platform/environment
set, unchanged from master.

…col tests

- LogDownloadTracker: only trust a short packet as the log end at the
  highest offset seen - a stale short retransmit of an earlier block
  used to set a too-small total and silently truncate the download
- GetLog: CancellationToken support; LOG_REQUEST_END and temp-file
  cleanup already cover the cancel path
- LogDownload UI: Cancel button with a per-batch CancellationTokenSource
- port the fake-vehicle protocol suite (ordering, loss recovery, stray
  retransmits, cancel, timeout) with a LogRetryDelayMs test seam
Six toolbar items need ~830px in a single row; the horizontal StackPanel
painted "Erase All" (and the new Cancel) past the 540px default width.
WrapPanel per the joystick-window fix, MinWidth 420, and a layout guard
asserting every toolbar child stays inside the window at 420 and 540.
Ported from the upstream fork: log list, timed full download with oracle
byte-compare, cancellation and link-reuse checks, plus a lossy proxy
dropping 5% of LOG_DATA. First run found the repair phase serving one
gap per 3 s silence window - a lossy 2.3 MB download projected ~65 min.
Track the outstanding request's end and issue the next missing-range
request the moment it is satisfied; a new LogRepairDelayMs (500 ms)
covers repair responses that were themselves lost. Lossy-link SITL run
drops from not finishing to 72.9 s, byte-identical; clean link unchanged.
…end inference

- silence tolerance is a time budget (retryLimit x LogRetryDelayMs), so short
  repair windows no longer shrink it from 30 s to 5 s
- repair chaining requires actual coverage progress; stale or duplicated
  packets cannot multiply requests
- only frontier-near packets raise the end-inference bar, so a corrupt far
  offset cannot poison it; data beyond the known total is ignored
- shared IssueNextRequest/SilenceWindowMs helpers; completion checks reuse
  the computed coverage instead of re-walking the range list per packet
- four new discriminating protocol/tracker tests; SITL clean and lossy
  runs re-verified byte-identical (0.45 s / 73.7 s)
- claim IsBusy before the picker awaits so batches cannot interleave
- Cancel enables only while a download runs (new IsDownloading), not
  during refresh/erase where it had nothing to cancel
- a cancel landing during the copy/KML tail now cancels instead of
  reporting success
- toolbar spacing via ItemSpacing/LineSpacing like the sibling views
@userepo

userepo commented Aug 29, 2026

Copy link
Copy Markdown
Author

Hi @Rouniy — would you be interested in this change set? It fixes a silent
truncation bug and makes downloads over lossy links usable, with a protocol test
suite to lock the behavior in. I've submitted the same fixes to upstream
MissionPlanner as ArduPilot#3764, but this branch is adapted to
your LogDownloadTracker rework rather than a straight port.

Separately, I've been rewriting the dataflash log core in Rust
(https://github.com/userepo/MissionPlanner/tree/rust/dflog-core): parser +
format/units metadata, C FFI consumed by DFLogBuffer behind a managed
fallback, a Python wheel, and a CLI — roughly 4× faster than the managed parser
in my benchmarks. Happy to open a PR bringing it to MissionPlanner10 as well if
that fits your roadmap.

A LOG_DATA packet that is both short and at a corrupt far offset cleared
the end-inference bar trivially: only frontier-near packets raise the
bar, so end >= bar held and the download ended at a phantom length.
Past the true end that stalls the repair phase into a timeout; below it
the phantom silently truncated the returned file.

Rejecting far end packets outright is not an option: packet loss stalls
the frontier at the first dropped block, so the genuine end of a large
log always sits far past the trust window, and every recovered gap then
forces the vehicle to re-stream the whole log (that variant never
finished the lossy SITL run). Instead the tracker records a deferred
candidate, discarded when more stream arrives, and GetLog promotes it
once a silence window expires: a corrupt packet is followed by more
stream, the real end is not.

Found via review of the equivalent change on the upstream PR
(ArduPilot#3764).

SITL: clean unchanged (0.44 s byte-identical), 5% LOG_DATA loss 77.3 s
byte-identical in one streaming pass - the earlier 72.9 s reference plus
one silence window confirming the end. Harness README reference results
and the STATUS.md checkpoint updated to match.
@Rouniy
Rouniy merged commit 0ef8376 into Rouniy:master Aug 30, 2026
@Rouniy

Rouniy commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Reviewed and merged. Thank you for the careful port and the protocol coverage.

Why we accepted it:

  • it fixes a real silent-truncation path in DataFlash downloads;
  • it turns lossy-link repair from one gap per silence window into progress-gated chained repair;
  • it adds end-to-end cancellation, bounded silence handling, narrow-window layout coverage, 15 fake-vehicle protocol tests, tracker tests, and a retained SITL harness.

Integrated exact contributor head 6257c6c53e5c6cef8069e7dd397e09e946634d21 through merge commit 0ef83765432b04db2ed3af70aa87d8081fdcfd52.

During review we added follow-up fee3c2dc3:

  • native file/folder picker failures now release IsBusy and report an actionable status instead of leaving the log window wedged;
  • cancellation arriving during the final synchronous KML export is rechecked after export returns, so the batch cannot report false success;
  • the empty-batch path also releases the busy gate.

Local verification on the integrated tree:

  • complete Release suite: 1531/1531 passed;
  • MissionPlanner.slnx: 0 warnings, 0 errors;
  • standalone SITL harness: 0 warnings, 0 errors;
  • lossy proxy source parses;
  • all six porting/inventory gates pass: 1623 native rows, 0 blockers, 708/708 pinned source paths, and clean WinForms/project/binary/key audits.

The contributor PR workflows were held as action_required because this came from an external fork, so we verified locally before integration. Master CI/package and CodeQL runs are now executing on final pushed head 02e8f38f45d681ec48ef580bdf85ed11f7d571bb.

@userepo
userepo deleted the fix/log-download-cancel-tests branch August 30, 2026 18:07
@userepo

userepo commented Aug 30, 2026

Copy link
Copy Markdown
Author

Thanks for reviewing and merging the log download PR, @Rouniy!

Following up on the offer I mentioned there, in case it got lost in the review
noise: I've been rewriting Mission Planner's dataflash log core in Rust
(https://github.com/userepo/MissionPlanner/tree/rust/dflog-core), and I'd be
happy to bring it to MissionPlanner10 as a PR. What it would look like in this
repo:

  • Vendored Rust workspace (parser core + a small C ABI library), built from
    source during the normal build: no checked-in binaries, and it slots into
    your existing four-RID CI/packaging as another native library alongside
    SimpleBLE/SkiaSharp.
  • DFLogBuffer gets a native fast path with the managed code as an automatic
    fallback (no Rust toolchain installed = today's behavior, unchanged), behind
    a settings toggle.
  • First consumers: LogBrowse columnar reads (~30x faster than the managed
    path in my benchmarks; a 250 MB log's column extraction drops from seconds
    to ~0.1 s) and the FFT/spectrogram ISBD path, verified bit-identical against
    the managed parser.

The parser is parity-tested against a multi-vehicle corpus, fuzzed, and
byte-identical on text conversion. If you want to kick the tires standalone
first, there's a release with a CLI and Python wheel here:
https://github.com/userepo/MissionPlanner/releases/tag/dflog-v0.7.1

Interested? If yes I'd open it as a small stack of reviewable PRs (build
plumbing first, then the fast paths, then consumers) rather than one big drop.
No hard feelings if it's out of scope for the port right now.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants