Leaf Log Auto-upload Backlog
Leaf Log Auto-upload Backlog
Status: proposed design. No uploader or logbook migration has been implemented yet.
Goal
When Leaf is in USB charging mode, automatically upload recorded IGC flights that have not already been delivered to Leaf Log. Auto-upload must not compete with a computer using the SD card through USB mass storage.
Auto-upload is enabled only when:
Settings > Leaf Labs > Leaf Logis enabled (settings.labs_leafLog).- The device has completed Leaf Log pairing and has a device bearer token.
- A saved Wi-Fi network is available.
- The SD card is mounted.
- Firmware still owns the mass-storage volume, or the USB host has explicitly ejected it.
Orphan IGC migration is intentionally out of scope. The uploader scans logbook JSON entries and does
not search /tracks for unreferenced files.
Changing what happens when the user powers Leaf on while a computer owns the mass-storage volume is
also out of scope. Preserve the current power-on behavior in this release; the ownership and UX
tradeoffs are tracked separately in
usb-mass-storage-power-on-ownership.md.
Leaf Log API contract
The source of truth is leaf-log/docs/device-api-contract.md in the Leaf Log repository.
Upload one flight with:
POST https://log.leafvario.com/api/ingestAuthorization: Bearer llk_...X-Filename: <original-name>.igcContent-Type: application/octet-stream
<raw IGC bytes>The body is a single raw IGC file, not multipart, with a maximum size of 5 MB. A successful response has this shape:
{ "flightId": "tn8t", "status": "ready", "deduped": false }Leaf Log deduplicates exact IGC bytes per account. Retrying an uncertain delivery is safe and returns
the existing flightId with deduped: true.
Current firmware behavior
- USB power boots Leaf into
PowerState::OffUSBwhen the center button was not used. TaskManager::updateWhileCharging()runs charging work every 500 ms.leaf_usbtracks USB data-host start, stop, suspend, and resume events separately from USB power.- It already provides a three-second host-enumeration grace period.
- The SD card is mounted through
SD_MMCwhile the same card is also exposed as writable USB MSC. - MSC callbacks currently call
SD_MMC.readRAW()andSD_MMC.writeRAW()without coordinating with filesystem access from firmware. - Pairing currently stores
leaf_log.pilot_id,account_email, andtokenin/profiles/profiles.json, although the token itself is already device-level. - Saved Wi-Fi credentials can already be tried through
leaf_wifi::attemptSavedNetworkConnection().
An inactivity timer is not a sufficient safety boundary. A connected host may have a mounted FAT filesystem and cached writes even when no recent MSC callback has occurred.
Recommended first version
Prefer Leaf Log for users who have explicitly enabled and paired it. On a new USB charging session, firmware initially retains the SD card and does not present the mass-storage medium while eligible pending flights are uploaded. A USB data host may enumerate Leaf during this period, but its MSC LUN reports that no medium is present.
Show a charging-page status as soon as an upload attempt starts. Example copy:
Leaf Log
Uploading 2 of 5Press button for USB driveWhile scanning it should show:
Checking Leaf Log...Press button for USB driveDuring network setup it may instead show Connecting.... Any short button press cancels auto-upload
and gives the SD card to USB mass storage. Cancellation must remain available during the logbook scan,
Wi-Fi connection, network time synchronization, and every upload.
Mass storage remains effectively unchanged for users who have not opted into Leaf Log:
- Leaf Log setting disabled: present the medium immediately.
- Leaf Log enabled but not paired: present the medium immediately.
- Paired with no eligible pending flights: present the medium after the quick eligibility scan.
- Eligible pending flights: hold the medium while uploading, unless the user cancels.
When uploads finish, are cancelled, or encounter a bounded failure, release firmware access and present the medium to the host. Network or Leaf Log failure must fail open to mass storage; a failed sync must never leave the USB drive unavailable indefinitely.
For purposes of the charging-mode uploader, once the medium has been presented, the host owns the
filesystem until explicit eject or disconnect. Neither MSC inactivity nor USB suspend releases
ownership. The Arduino USB framework supports USBMSC::onStartStop(); an explicit host eject can
safely return ownership to firmware and resume a cancelled or incomplete upload batch while Leaf
remains plugged in. A user-requested transition to normal powered-on operation retains its existing
behavior in this release and is covered by the separate power-on ownership backlog.
Charging state machine
Implement a non-blocking LeafLogSync or LeafLogUploader state machine called from
TaskManager::updateWhileCharging():
CheckingEligibility- require the Leaf Labs setting, mounted SD card, device pairing token, saved Wi-Fi, and at least one eligible pending flight.PresentingMassStorage- immediately present the medium when sync is disabled, unpaired, already complete, cancelled, or unable to proceed.ConnectingWifi- retain firmware ownership, show the cancellable Leaf Log screen, and connect only to saved station networks; never start the Leaf access point.WaitingForTime- obtain valid network time for TLS without blocking the charging loop.Uploading- stream one IGC at a time to/api/ingestwithout loading the entire file into heap.RecordingResult- atomically add the returned Leaf Log flight ID to the logbook JSON.Finishing- close SD files, disconnect Wi-Fi, present the medium if a data host is connected, and allow charging sleep again.HostOwned- perform no firmware filesystem access while the medium is presented.WaitingForEject- detect explicit host eject and return to eligibility checking for any remaining flights.Backoff- on a power-only charger, leave files unchanged after transient failures and retry later without remaining continuously awake. If a data host is waiting, present mass storage instead of holding it through a long backoff.
The state machine must expose whether it needs Leaf to stay awake. Charging sleep should require both
the existing diagnostic-network sleep condition and leafLogSync.canSleepWhileCharging().
While the Leaf Log sync screen is active, intercept any short button press before normal charging-page
behavior and set a cancellation request without immediately exposing the medium. The state machine
must first abort or finish the current bounded HTTP/storage operation, close every file, and leave any
atomic JSON replacement in a recoverable state. Only then may it call mediaPresent(true). The UI may
briefly show Opening USB drive... during this handoff. Normal charging-page button behavior resumes
after mass storage is presented.
SD card ownership
Separate USB connection state from SD ownership. A connected or suspended data host does not own the filesystem until firmware presents the medium, while presenting the medium is an irrevocable handoff until host eject or disconnect.
The ownership states should be approximately:
FirmwareReserved -> FirmwareUploading -> HostOwned | | | +-- no work ---------+ +-- explicit eject --> FirmwareReserved +-- cancel ----------+ +-- failure ---------+Configure MSC with mediaPresent(false) while ownership is FirmwareReserved or
FirmwareUploading. USB may enumerate normally, but MSC reads and writes must not reach the SD card.
After all firmware file handles and transactions are closed, mediaPresent(true) transitions to
HostOwned.
Register USBMSC::onStartStop() and treat only an explicit eject (start == false with
load_eject == true) as a host-to-firmware handoff. USB suspend and stop/spindown commands without
load_eject do not release the filesystem. After an eject-triggered upload, keep the medium ejected
until USB disconnect; the first version does not re-present it during the same USB session.
An exclusive firmware transaction API in SDCard is still useful for enforcing these charging-mode
transitions. It should prevent mediaPresent(true) while firmware files are open and prevent the
Leaf Log sync and other charging-mode work from accessing the filesystem in HostOwned. Atomics
should communicate cancellation and USB events safely between callback and task contexts. Expanding
that guarantee across the transition to normal powered-on operation belongs to the separate
power-on ownership backlog.
Do not reclaim the medium after an inactivity timeout. A mounted FAT filesystem may retain cached state and open files indefinitely, so lack of recent MSC callbacks is not an ownership release.
Eligible logbook entries
For each /logbook/*.json file, require:
- A valid logbook JSON object.
track.saved == true.track.format == "igc", or a track path ending in.igcfor compatible older entries.- A normalized track path contained under
/tracks. - An existing non-empty IGC no larger than 5 MB.
- No existing
leaf_log.flight_id. - No terminal
leaf_log.rejectedreason.
The local pilot.id does not affect upload routing. Every eligible flight on the device is uploaded
to the Leaf Log account that owns the device token. KML tracks and orphan IGC files remain
NotApplicable. A locally invalid IGC entry is not uploaded, but it must be assigned a durable
terminal rejection so the user can see that Leaf considered it and why it could not be delivered.
This includes malformed logbook data, unsafe track paths, missing track files, empty track files, and
oversized track files. Directory iteration can remain unsorted, and the uploader continues through
the backlog while uploads or local classification succeed.
Syntactically broken JSON that cannot appear in either logbook UI remains untouched and is skipped;
do not create a sidecar or rejection index for it. For parseable JSON, classify in a deterministic
order: invalid schema, no saved IGC intended, unsafe or missing track path, missing file, empty file,
then oversized file. track.saved == false and a valid KML reference are NotApplicable, not
failures. A saved track with no usable path is invalid_logbook.
Recording delivery
Keep the per-flight upload record minimal. On any valid HTTP 200 response, including deduped: true
or status: "failed", preserve the existing logbook document and add only:
"leaf_log": { "flight_id": "tn8t"}The presence of leaf_log.flight_id is the authoritative delivered marker. Leaf does not persist the
server status, dedupe result, or upload timestamp because they do not affect future device behavior.
A terminal local rejection in a parseable logbook document uses the same object with one mutually exclusive reason:
"leaf_log": { "rejected": "too_large"}Initially supported reason codes are:
invalid_logbook- parseable JSON is not a valid Leaf logbook object or lacks the fields needed to identify its saved track.unsafe_track_path- the normalized track path is not contained under/tracks.missing_track- the referenced IGC file does not exist or cannot be opened.empty_track- the referenced IGC file has zero bytes.too_large- the referenced IGC exceeds 5 MB locally, or Leaf Log returns HTTP413.invalid_igc- Leaf Log returns HTTP400for the submitted bytes.
rejected is written only for a permanent condition and suppresses automatic retries. These codes
are persisted rather than collapsed into a generic failure so both UIs can explain the result. Repair
tooling or a manual JSON edit may remove the rejection to retry that flight; a later successful upload
replaces it with flight_id.
Update the JSON using a temporary file plus backup/rename replacement, preserving unknown fields.
This should follow the existing atomic profile-writing pattern. A power loss must leave either the
old entry or the complete updated entry recoverable. Document the optional leaf_log object in the
logbook YAML schema.
Per-flight Leaf Log status
Expose the persisted result when browsing logbook entries. Derive one of four states:
NotApplicable- a valid entry does not claim a saved IGC track, such as a KML entry ortrack.saved == false; show no Leaf Log status.NotUploaded- eligible IGC with neitherflight_idnorrejected.Uploaded-leaf_log.flight_idis present.Rejected-leaf_log.rejectedis present.
When the Leaf Log Labs setting is enabled, the Leaf web app logbook entry card should show a compact
icon plus accessible phrase: Not uploaded to Leaf Log, Uploaded to Leaf Log, or
Leaf Log upload rejected. A rejected entry should also show a short reason mapped from the stable
code: Invalid logbook, Unsafe track path, Track file missing, Track file empty, Too large,
or Invalid IGC. Use a tooltip or accessible label for icons rather than relying on color alone.
Add the same state to the on-device Logbook screen in the upper-right corner, immediately to the
right of the / LOGBOOK \ title tab. Use the Leaf glyphs in leaf_8x14: an empty leaf (141) for
NotUploaded, a solid leaf (140) for Uploaded, and an exclamation leaf (143) for Rejected.
Code point 142 is reserved for a future state with explicit semantics.
NotApplicable shows no glyph. Keep a stable icon-sized area so paging between entries does not move
the title or other content.
Extend LogbookEntrySummary and LogbookStore::readSummary() to parse this state once. Reuse that
summary in the shared device logbook card and include the state/rejection reason in the web app
logbook-entry JSON response. Do not re-open the JSON separately in each UI.
Error behavior
- Network error or HTTP
5xx: leave the entry unchanged and retry with backoff. - HTTP
401: stop the batch, erase the invalid token from NVS, persistreconnect_required, and do not retry every file. - HTTP
400: recordinvalid_igc; HTTP413: recordtoo_large, so the same immutable file is not attempted during every charging session. - Invalid success JSON or a missing
flightId: treat as transient and do not mark delivered. - User cancellation: safely close network and filesystem work, then present mass storage.
- Data host connected while firmware owns the unpresented medium: continue the cancellable upload; the host waits with no MSC medium until completion, cancellation, or bounded failure.
- USB suspend after the medium is presented: retain
HostOwned; do not upload. - Explicit host eject: reacquire firmware ownership and resume eligible uploads.
When a data host is waiting for mass storage, the first transient network or server failure stops the batch and presents the medium. On a power-only charger, retry after approximately 5, 15, and 60 minutes, sleeping normally between attempts. Continue through the entire backlog while uploads are succeeding.
Network timeout defaults
Use these initial defaults:
- Saved Wi-Fi connection: 5 seconds per saved network, with the existing maximum of three networks and a 15-second total connection window.
- Network time synchronization: 8 seconds total.
- DNS/TCP/TLS connection for a Leaf Log request: 10 seconds.
- Established socket inactivity: 15 seconds without successful read or write progress.
- Total elapsed time for one IGC upload, including its server response: 60 seconds.
- Pairing start/poll HTTP calls: the same 10-second connection and 15-second inactivity limits.
Implement the total upload deadline outside HTTPClient; setConnectTimeout(10000) and
setTimeout(15000) provide the underlying connection and socket-idle bounds. Stream IGC data in
chunks and check cancellation between chunks. Normal button cancellation should hand off to mass
storage within about one second; a call already blocked in the network stack may take up to the
10-second connection or 15-second inactivity timeout. No individual blocking operation should exceed
those bounds.
Device pairing and token
Leaf uses one current Leaf Log pairing and bearer token per physical device, not one token per local pilot profile. The paired Leaf Log account owns all flights automatically uploaded from that device, regardless of the optional pilot snapshot stored in an individual logbook entry.
This matches the expected use of a Leaf unit as a primarily personal device. If it is temporarily loaned to another pilot, the resulting flight still uploads to the device owner’s account. Future Leaf Log website features may assign, share, or move flight records between accounts; that workflow is outside firmware upload routing.
Firmware changes implied by this decision:
- Do not require a pilot selection before starting Leaf Log pairing.
- Do not filter pending logbook entries by
pilot.id. - Treat a newly paired token as the device’s single current credential and replace the locally stored token when the device is paired again.
- Remove or deprecate
leaf_log.pilot_idin the profiles schema, validation, empty profile document, and Leaf Log linking UI. It is not part of authentication or upload ownership. - Keep logbook pilot snapshots unchanged for flight display and future website reassignment features.
Store the bearer token in a dedicated device NVS namespace in the first auto-upload release; do not
leave it in /profiles/profiles.json where USB mass storage can expose it. No migration is required
for experimental profile-file tokens; test devices may simply be paired again. A normal settings
reset and every equivalent reset performed by factory_interface must erase the Leaf Log token and
pairing state. Replacing or removing the SD card must not remove a valid device pairing.
Disabling Settings > Leaf Labs > Leaf Log stops automatic uploads but retains the device token and
pairing state in NVS. Re-enabling the setting resumes uploads without requiring another pairing. This
also keeps the token behavior compatible with eventually removing the experimental setting and
gating uploads only on whether a token is present.
Leaf Log has a public, unique handle and a public, non-unique displayName. Return both as an
account object when pairing succeeds, and cache them with the device pairing in NVS. Do not return or
store the account email. Leaf may display Linked to Display Name (@handle) in its web app. Treat these
fields as a cosmetic snapshot rather than authentication or upload-routing data because the user may
change either field later on Leaf Log.
The first version does not need a synchronization dashboard in the Leaf Log settings card. Keep the
card limited to actionable pairing state and controls: Not linked, Linked, or
Reconnect required, plus the appropriate pair/re-pair action. Pending-flight counts, last-success
timestamps, and rejection summaries would add SD scans and UI complexity without improving the normal
workflow. Show scan/upload progress on the charging screen; uploaded flights are visible in Leaf Log.
When linked, the Leaf web app card should also offer Unlink. Gate it with the same style of
Are you sure? confirmation used when deleting a logbook entry. On confirmation, attempt to revoke
the current token on Leaf Log, then erase the local token and pairing state even if the network call
fails. A failed server cleanup leaves the old token visible for manual revocation in Leaf Log.
Leaf Log contract changes
Update leaf-log/docs/device-api-contract.md and related Leaf Log planning documentation to state:
- One current pairing/token is stored per Leaf device, not per on-device pilot profile.
- Every IGC sent with that bearer token is owned initially by the Leaf Log account associated with the token. Local Leaf pilot IDs do not select the destination account.
- The firmware sends only the IGC and existing upload headers; no pilot identifier is required.
- Assignment, sharing, and transfer between Leaf Log accounts are later website capabilities and do not change the device ingest request.
Make additive changes to both successful response shapes. A claimed pairing poll returns the token and public account identity:
{ "status": "claimed", "token": "llk_...", "account": { "handle": "skyhawk", "displayName": "Jamie Smith" }}Every successful /api/ingest response also includes the same account object. After durably recording
the returned flightId, Leaf updates its cached handle and display name if they changed. This refreshes
the label naturally on the next upload without adding another request or connecting to Wi-Fi solely for
cosmetic metadata. If no new flight is uploaded, the cached label may remain stale until the next upload
or re-pairing; this is acceptable because the bearer token, not the label, determines ownership.
Ordinary re-pairing creates a new server token. If Leaf only overwrites its local NVS value, the old token remains valid and visible in Leaf Log’s device settings. This is not account access - device tokens can only upload flights - but a copied old token could continue adding flights, and repeated re-pairing would leave confusing duplicate device credentials.
Decision: implement a bearer-authenticated self-revoke endpoint, initially
POST /api/devices/revoke-self. During re-pairing, Leaf retains the old token in memory, completes
the new pairing, durably stores the new token, and only then calls the self-revoke endpoint using the
old token. Failure to revoke the old token does not roll back the new pairing; Leaf Log continues to
show the old credential so the user can revoke it manually. An already revoked old token returning
401 is harmless.
Self-revoke handles normal re-pairing without transmitting a permanent hardware identifier. It cannot clean up a token erased by settings reset, factory reset, corruption, or loss. Those credentials remain manually revocable on the website. Guaranteeing one live token per physical device even after local credential loss would require a stable server-visible device identifier and decisions about privacy, device transfer between accounts, and whether that identifier survives settings reset. That stronger identity model is not required for the first auto-upload implementation.
Leaf Log device-token management UI
Improve Leaf Log’s Connected Devices list so an account with multiple Leaf units and stale tokens can make an informed manual revocation decision. Each token row should show:
- The device label and active/revoked state.
- Pairing creation date and exact local time, not date alone.
- Last successful device upload date and exact local time, or
Never used. - The latest flight delivered with that token, including a link and a recognizable flight summary. Leaf Log may choose a compact track preview or a simpler summary according to the available screen width; that responsive presentation does not need to be decided in the device-upload plan.
Do not label a token Current based only on creation or last-used time; Leaf Log cannot know which
plaintext token a device currently retains after resets or failed cleanup. The richer timestamps and
latest-flight context are evidence for the user, not a server claim of current ownership.
Leaf Log currently records lastUsedAt after successful ingest but does not retain the corresponding
flight on the token. Add a nullable latest-flight association to DeviceToken, such as
lastFlightId, and update it together with lastUsedAt after every successful /api/ingest
response, including a deduplicated upload. Keep the association when the token is revoked. If the
flight is later deleted or becomes unavailable to that account, clear or omit the link safely.
Existing tokens and historical uploads may have no latest-flight association; display the available creation/last-used metadata without attempting an unreliable backfill.
Suggested implementation boundaries
src/vario/comms/leaf_log_client.*- shared base URL, CA certificate setup, pairing requests, and streaming IGC upload.src/vario/comms/leaf_log_sync.*- charging state machine, eligibility, scan, retry, and result handling.src/vario/storage/sd_card.*- exclusive firmware/MSC ownership coordination.src/vario/logbook/- eligible-entry scanning and atomic upload-result patching.src/vario/taskman.*- call the state machine and include it in the charging sleep decision.- A dedicated NVS-backed Leaf Log credential store - device pairing status and token access, including
reset handling shared with firmware settings reset and
factory_interface.
Extract the existing Leaf Log HTTPS constants and TLS setup from webserver.cpp rather than
duplicating them in the uploader.
Test matrix
- Leaf Labs setting off: no Wi-Fi attempt and no logbook access.
- No token: no Wi-Fi attempt and no upload.
- Firmware settings reset and
factory_interfacesettings reset: erase the device token and pairing state. - Existing experimental token only in
profiles.json: do not migrate it; require re-pairing. - Power-only charger, valid token, saved Wi-Fi: eligible flights upload and are marked.
- Computer connected with Leaf Log disabled or unpaired: mass storage appears immediately.
- Paired computer connection with no pending flights: mass storage appears after a quick scan.
- Paired computer connection with pending flights: medium remains absent, upload status and cancel action appear, and mass storage is presented after completion.
- Cancel during Wi-Fi, NTP, IGC streaming, and result recording: reach a safe transaction boundary, close files, then present mass storage.
- Network or server failure with a host waiting: fail open and present mass storage.
- Host eject after a cancelled or partial batch: safely resume remaining uploads.
- Suspended computer after mass storage was presented: retain host ownership and do not upload.
- MSC inactivity for any duration: retain host ownership and do not upload.
- Exact duplicate on Leaf Log:
deduped: truestill records the returnedflightId. - Leaf Log returns
status: "failed"with HTTP 200: record delivery and do not retry. - Revoked token (
401): stop batch and surface reconnect-required state. - Server
5xxor network loss with a host waiting: retain unuploaded state and present mass storage. - Server
5xxor network loss on a power-only charger: retry with 5/15/60-minute backoff. - Syntactically valid but schema-invalid logbook object: record
invalid_logbookin the entry and show the failed icon and reason. - Syntactically invalid logbook JSON: leave it untouched, create no auxiliary status file, and skip it because it cannot appear in either logbook UI.
- Unsafe track path, missing track, or empty track: record the corresponding terminal rejection, show its failed icon and reason, and continue with later entries.
- Oversized or server-rejected IGC: record
too_largeorinvalid_igcand continue with later entries. - Logbook entries containing different or missing local pilot IDs: upload all otherwise eligible entries to the account associated with the device token.
- Re-pairing: replace the locally stored device token and use the new account for future uploads.
- Re-pairing with self-revoke: save the new token before revoking the old one; cleanup failure leaves the new pairing active and the old token manually revocable.
- Disable and re-enable the Leaf Log Labs setting: retain the pairing token and resume without re-pairing.
- Unlink in the Leaf web app: require confirmation, attempt self-revocation, and erase local pairing state even when server cleanup fails.
- Leaf Log Connected Devices: show exact creation and last-used times, and the latest associated flight/track where available.
- Deduplicated device upload: update the uploading token’s last-used time and latest-flight association to the existing flight.
- Historical token without a latest-flight association: render available metadata without failure.
- Wi-Fi, NTP, connect, socket-idle, and total-upload timeout boundaries: fail predictably and preserve cancellation and mass-storage handoff.
- Web app logbook card: show accessible
Not uploaded,Uploaded, andRejectedstates. - Device logbook card: show a compact status for the same states without overlapping existing fields.
- Pairing response: cache the returned public handle and display name, and never expose account email.
- Upload after changing the Leaf Log profile: refresh the cached handle/display name from the successful ingest response without affecting token ownership.
- KML entry and orphan IGC: remain
NotApplicableand are not uploaded. - Power loss during JSON replacement: recover the original or complete updated entry.
- Large backlog: remain responsive, avoid watchdog resets, and continue while uploads succeed.
Open decisions
No product decisions are currently open for the first auto-upload implementation.