Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,24 @@ Log timestamps come from the modification time in the listing when the vehicle s

Downloads are staged in a temporary directory and only moved next to the finished logs once the transferred size matches the listing, so a partial file is never mistaken for a complete one.

`.BIN` files are not accepted by review.px4.io, so on ArduPilot leave `upload_enabled = false` or point `remote_server` somewhere that understands dataflash logs.

### Upgrading from a pre-FTP logloader
Older versions identified logs by the timestamp `LOG_ENTRY` reported, which MAVLink FTP cannot reproduce. On first start the existing `logs` table is renamed to `logs_legacy` and its rows are matched against the FTP listing by size, so logs already downloaded and uploaded are not fetched or uploaded a second time. Nothing is deleted; `logs_legacy` stays behind for inspection.

### Configuration
| Key | Default | Description |
| --- | --- | --- |
| `connection_url` | `udp://:14551` | MAVSDK connection string |
| `local_server` | `http://127.0.0.1:5006` | Local upload target |
| `remote_server` | `https://review.px4.io` | Remote upload target |
| `email` | `""` | Email attached to remote uploads |
| `remote_api_key` | `""` | Per-account API key for authenticated Flight Review (`Authorization: Bearer` + `X-API-Key`). Empty = upload without API key headers (open servers). Generate under /account |
| `upload_enabled` | `false` | Upload to the remote server |
| `public_logs` | `false` | Mark remote uploads public |
| `remote_log_directory` | `""` | Override the vehicle log directory |
| `ftp_use_burst` | `true` | Use FTP burst reads |

### Build
Install dependencies
```
Expand Down
7 changes: 7 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ email = ""
upload_enabled = false
public_logs = false

# Per-account API key for authenticated Flight Review (ARK).
# Generate at https://review.arkelectron.com/account (shown once; format fr_...).
# When set: sent as Authorization: Bearer and X-API-Key on POST /upload.
# When empty: remote uploads are still attempted without API key headers
# (works for open servers; ARK Flight Review will reject with 403).
remote_api_key = ""

# Vehicle log directory. Empty probes "@MAV_LOG" (the virtual log directory from
# the MAVLink FTP specification), then "/fs/microsd/log" (PX4) and "/APM/LOGS"
# (ArduPilot). Set this when the logs live somewhere else.
Expand Down
12 changes: 10 additions & 2 deletions src/LogLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ LogLoader::LogLoader(const LogLoader::Settings& settings)
.db_path = _settings.application_directory + "local_server.db",
.upload_enabled = true, // Always upload to local server
.public_logs = true, // Public required true for searching using Web UI
.api_key = "", // Local Flight Review is typically open on the companion
};

// Setup remote server interface
Expand All @@ -58,6 +59,7 @@ LogLoader::LogLoader(const LogLoader::Settings& settings)
.db_path = _settings.application_directory + "remote_server.db",
.upload_enabled = settings.upload_enabled,
.public_logs = settings.public_logs,
.api_key = settings.remote_api_key,
};

_local_server = std::make_shared<ServerInterface>(local_server_settings);
Expand Down Expand Up @@ -405,8 +407,14 @@ void LogLoader::upload_pending_logs(std::shared_ptr<ServerInterface> server)
if (result.success) {
LOG("Log upload SUCCESS: " << result.message);

} else if (result.status_code == 400) {
LOG("Log upload failed (" << result.status_code << "): " << result.message);
} else if (result.status_code == 400 || result.status_code == 401 || result.status_code == 403) {
LOG("Log upload rejected (" << result.status_code << "): " << result.message);

// Account/auth policy applies to every log on this server — stop the batch.
if (result.status_code == 401 || result.status_code == 403) {
LOG("Stopping remote uploads for this cycle (server requires login/approval)");
return;
}

} else if (result.status_code == 503) {
// Server down (e.g. local flight-review not running). Already logged once
Expand Down
2 changes: 2 additions & 0 deletions src/LogLoader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class LogLoader
std::string email;
std::string local_server;
std::string remote_server;
// API key for remote_server (ARK Flight Review account key). Empty = none.
std::string remote_api_key;
std::string mavsdk_connection_url;
std::string application_directory;
bool upload_enabled;
Expand Down
72 changes: 60 additions & 12 deletions src/ServerInterface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,11 @@ ServerInterface::UploadResult ServerInterface::upload_log(const std::string& uui
}

} else if (result.status_code == 400) {
// Permanent failure - add to blacklist
add_to_blacklist(uuid, "HTTP 400: Bad Request");
// The server will not take this log whoever asks - do not keep retrying it.
// 401/403 are not blacklisted: they say the account is not authorized yet,
// which is fixed by setting remote_api_key, and every pending log would
// otherwise be thrown away one per cycle while the key is missing.
add_to_blacklist(uuid, "HTTP 400: " + result.message);
}

return result;
Expand Down Expand Up @@ -445,25 +448,64 @@ ServerInterface::UploadResult ServerInterface::upload(const std::string& filepat
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
items.push_back({"filearg", content, filepath, "application/octet-stream"});

LOG("Uploading " << fs::path(filepath).filename().string() << " to " << _settings.server_url);
// API-key headers only when a non-empty key is configured. Never send
// Authorization/X-API-Key with an empty value.
httplib::Headers headers;
const bool use_api_key = !_settings.api_key.empty();

if (use_api_key) {
// ARK Flight Review (flight_review api_key.py):
// Authorization: Bearer <key>
// X-API-Key: <key>
// (Query ?api_key= also works server-side; form fields cannot — auth
// runs in prepare() before the body is available.)
headers.emplace("Authorization", "Bearer " + _settings.api_key);
headers.emplace("X-API-Key", _settings.api_key);
}

LOG("Uploading " << fs::path(filepath).filename().string() << " to " << _settings.server_url
<< (use_api_key ? " (with API key)" : ""));

// Post multi-part form
httplib::Result res;

if (_protocol == Protocol::Https) {
httplib::SSLClient cli(_settings.server_url);
res = cli.Post("/upload", items);
cli.set_connection_timeout(30, 0);
cli.set_read_timeout(120, 0);
res = use_api_key ? cli.Post("/upload", headers, items)
: cli.Post("/upload", items);

} else {
httplib::Client cli(_settings.server_url);
res = cli.Post("/upload", items);
cli.set_connection_timeout(30, 0);
cli.set_read_timeout(120, 0);
res = use_api_key ? cli.Post("/upload", headers, items)
: cli.Post("/upload", items);
}

if (res && res->status == 302) {
return {true, 302, "Success: " + _settings.server_url + res->get_header_value("Location")};

} else if (res && res->status == 400) {
return {false, 400, "Bad Request - Will not retry"};
} else if (res && (res->status == 400 || res->status == 401 || res->status == 403)) {
// Permanent client/auth errors: do not spin on the same log forever.
// review.arkelectron.com returns 403 when the account is not logged in /
// not approved for automated uploads.
std::string detail = res->body;

// Collapse HTML / whitespace for a one-line log message.
for (char& c : detail) {
if (c == '\n' || c == '\r' || c == '\t') {
c = ' ';
}
}

if (detail.size() > 200) {
detail.resize(200);
detail += "...";
}

return {false, res->status, detail.empty() ? "Rejected by server (will not retry)" : detail};

} else {
return {false, res ? res->status : 0, "Will retry later"};
Expand All @@ -483,18 +525,24 @@ bool ServerInterface::server_reachable()

if (_protocol == Protocol::Https) {
httplib::SSLClient cli(_settings.server_url);
cli.set_connection_timeout(2, 0);
cli.set_read_timeout(2, 0);
cli.set_connection_timeout(5, 0);
cli.set_read_timeout(5, 0);
// Flight Review often redirects "/" (302). We only need proof the host answers.
cli.set_follow_location(false);
res = cli.Get("/");

} else {
httplib::Client cli(_settings.server_url);
cli.set_connection_timeout(2, 0);
cli.set_read_timeout(2, 0);
cli.set_connection_timeout(5, 0);
cli.set_read_timeout(5, 0);
cli.set_follow_location(false);
res = cli.Get("/");
}

const bool success = res && res->status == 200;
// Any HTTP response means the server is up. Flight Review's home page returns
// 302, so requiring status == 200 falsely marked review.arkelectron.com dead.
// Connection / TLS failures leave res empty.
const bool success = static_cast<bool>(res);

if (!success) {
_unreachable_until = now + kUnreachableCooldown;
Expand Down
6 changes: 6 additions & 0 deletions src/ServerInterface.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ class ServerInterface
std::string db_path; // Path to this server's database
bool upload_enabled {};
bool public_logs {};
// Per-account API key for authenticated Flight Review instances
// (e.g. review.arkelectron.com). Sent as:
// Authorization: Bearer <key>
// X-API-Key: <key>
// Matches ARK flight_review api_key.py. Leave empty for open servers.
std::string api_key;
};

struct UploadResult {
Expand Down
22 changes: 22 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ int main(int argc, char** argv)
.email = config["email"].value_or(""),
.local_server = config["local_server"].value_or("http://127.0.0.1:5006"),
.remote_server = config["remote_server"].value_or("https://logs.px4.io"),
.remote_api_key = config["remote_api_key"].value_or(""),
.mavsdk_connection_url = config["connection_url"].value_or("0.0.0"),
.application_directory = config["application_directory"].value_or(data_dir.string() + "/"),
.upload_enabled = config["upload_enabled"].value_or(false),
Expand All @@ -65,6 +66,27 @@ int main(int argc, char** argv)
.ftp_use_burst = config["ftp_use_burst"].value_or(true)
};

// Trim whitespace-only keys so they count as "not set" (no empty auth headers).
auto trim = [](std::string s) {
const auto start = s.find_first_not_of(" \t\r\n");

if (start == std::string::npos) {
return std::string{};
}

const auto end = s.find_last_not_of(" \t\r\n");
return s.substr(start, end - start + 1);
};
settings.remote_api_key = trim(std::move(settings.remote_api_key));

// Still attempt remote uploads without a key (open servers like logs.px4.io).
// Authenticated ARK Flight Review will 403 until remote_api_key is set; we
// never send empty Authorization / X-API-Key headers (see ServerInterface).
if (settings.upload_enabled && settings.remote_api_key.empty()) {
LOG("upload_enabled is true but remote_api_key is empty — remote uploads will "
"proceed without an API key (open servers only; ARK Flight Review needs a key)");
}

_log_loader = std::make_shared<LogLoader>(settings);

bool connected = false;
Expand Down