feat: wire safe opt-in map downloads

This commit is contained in:
torlando-agent[bot]
2026-08-07 01:34:30 +00:00
parent 8e0b603e11
commit f55b54eec0
16 changed files with 251 additions and 9 deletions
+48
View File
@@ -0,0 +1,48 @@
// Copyright (c) 2026 Pyxis contributors
// SPDX-License-Identifier: MIT
#ifndef HARDWARE_TDECK_MAP_TILE_CA_H
#define HARDWARE_TDECK_MAP_TILE_CA_H
namespace Hardware {
namespace TDeck {
// ISRG Root X1, valid through 2035-06-04. Public trust anchor for the
// default OpenStreetMap endpoint; this is not a credential.
static const char MAP_TILE_ISRG_ROOT_X1[] = R"CERT(-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
)CERT";
} // namespace TDeck
} // namespace Hardware
#endif
@@ -81,7 +81,7 @@ MapTileUrlResult MapTileDownloader::canonicalUrl(const char* endpoint, const Til
}
MapTileEnqueueResult MapTileDownloader::enqueue(const TileKey& key, std::uint32_t generation) {
if (!policy_.enabled) return MapTileEnqueueResult::DISABLED;
if (!policy_.enabled) return MapTileEnqueueResult::POLICY_DISABLED;
if (!validKey(key)) return MapTileEnqueueResult::INVALID_KEY;
if (active_ && sameKey(current_.key, key)) return MapTileEnqueueResult::DUPLICATE;
for (std::size_t i = 0U; i < queue_count_; ++i) {
@@ -94,6 +94,22 @@ MapTileEnqueueResult MapTileDownloader::enqueue(const TileKey& key, std::uint32_
return MapTileEnqueueResult::ACCEPTED;
}
void MapTileDownloader::setEnabled(bool enabled) {
if (policy_.enabled == enabled) return;
policy_.enabled = enabled;
if (enabled) return;
if (active_) finish(MapTileResultCode::CANCELED, store_open_);
while (queue_count_ != 0U) {
current_ = queue_[0];
for (std::size_t index = 1U; index < queue_count_; ++index) {
queue_[index - 1U] = queue_[index];
}
--queue_count_;
received_ = 0U;
publish(MapTileResultCode::CANCELED);
}
}
void MapTileDownloader::publish(MapTileResultCode code) {
MapTileDownloadResult result = {current_.key, current_.generation, code, received_};
if (result_count_ == RESULT_CAPACITY) {
@@ -78,7 +78,7 @@ private:
MapTileStore& store_;
};
enum class MapTileEnqueueResult : std::uint8_t { ACCEPTED, DISABLED, INVALID_KEY, DUPLICATE, QUEUE_FULL };
enum class MapTileEnqueueResult : std::uint8_t { ACCEPTED, POLICY_DISABLED, INVALID_KEY, DUPLICATE, QUEUE_FULL };
enum class MapTileUrlResult : std::uint8_t { OK, INVALID_ARGUMENT, INVALID_KEY, TOO_LONG };
enum class MapTilePumpResult : std::uint8_t { IDLE, PROGRESSED };
enum class MapTileResultCode : std::uint8_t {
@@ -119,6 +119,8 @@ public:
static MapTileUrlResult canonicalUrl(const char* endpoint, const TileKey& key,
char* output, std::size_t capacity);
MapTileEnqueueResult enqueue(const TileKey& key, std::uint32_t generation);
/** Worker-owner only: disabling aborts active and queued work immediately. */
void setEnabled(bool enabled);
std::size_t cancelGeneration(std::uint32_t generation);
MapTilePumpResult pump();
bool takeResult(MapTileDownloadResult& result);
+92 -3
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT
#include "MapScreen.h"
#include "Hardware/TDeck/MapTileCa.h"
#ifdef ARDUINO
@@ -26,6 +27,17 @@ constexpr std::size_t TILE_PIXEL_COUNT = 256U * 256U;
constexpr std::uint32_t STORE_BYTE_QUOTA = 64U * 1024U * 1024U;
constexpr std::uint16_t STORE_ENTRY_CAPACITY = 128U;
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "dev"
#endif
Hardware::TDeck::MapTileDownloadConfig makeDownloadConfig() {
Hardware::TDeck::MapTileDownloadConfig config;
config.ca_certificate = Hardware::TDeck::MAP_TILE_ISRG_ROOT_X1;
config.firmware_version = FIRMWARE_VERSION;
return config;
}
lv_obj_t* createToolbarButton(lv_obj_t* parent, const char* text,
lv_event_cb_t callback, void* context,
lv_coord_t width) {
@@ -59,7 +71,12 @@ MapScreen::MapScreen(lv_obj_t* parent)
presenter_(), storage_(),
store_config_{STORE_ENTRY_CAPACITY, STORE_BYTE_QUOTA,
MAX_COMPRESSED_TILE_BYTES},
store_(storage_, store_config_), compressed_staging_(nullptr),
store_(storage_, store_config_), download_store_(store_),
download_transport_(), download_clock_(), download_policy_(),
download_config_(makeDownloadConfig()),
downloader_(download_store_, download_transport_, download_clock_,
download_policy_, download_config_),
downloads_enabled_(false), compressed_staging_(nullptr),
state_mutex_(nullptr), worker_task_(nullptr), stop_requested_(false),
worker_exited_(true), worker_started_(false), store_initialized_(false),
requests_released_(false),
@@ -295,6 +312,61 @@ void MapScreen::workerLoop() {
}
Pyxis::MapTileLoadResult MapScreen::loadTile(
const Pyxis::MapTileRequest& request) {
const Pyxis::MapTileLoadResult cached = readTile(request);
if (cached != Pyxis::MapTileLoadResult::MISS) return cached;
const Pyxis::MapTileLoadResult downloaded = downloadTile(request);
return downloaded == Pyxis::MapTileLoadResult::READY
? readTile(request) : downloaded;
}
Pyxis::MapTileLoadResult MapScreen::downloadTile(
const Pyxis::MapTileRequest& request) {
const bool enabled = downloads_enabled_.load(std::memory_order_acquire);
downloader_.setEnabled(enabled);
if (!enabled) return Pyxis::MapTileLoadResult::MISS;
Hardware::TDeck::MapTileDownloadResult ignored{};
while (downloader_.takeResult(ignored)) {}
const Hardware::TDeck::MapTileEnqueueResult queued =
downloader_.enqueue(request.key, request.frame_epoch);
if (queued != Hardware::TDeck::MapTileEnqueueResult::ACCEPTED) {
return Pyxis::MapTileLoadResult::DOWNLOAD_FAILED;
}
while (downloader_.isBusy()) {
if (stop_requested_.load(std::memory_order_acquire) ||
!downloads_enabled_.load(std::memory_order_acquire)) {
downloader_.setEnabled(false);
return Pyxis::MapTileLoadResult::DOWNLOAD_FAILED;
}
bool stale = false;
if (lockState(pdMS_TO_TICKS(20))) {
stale = presenter_.generation() != request.generation ||
presenter_.frameEpoch() != request.frame_epoch;
unlockState();
}
if (stale) {
(void)downloader_.cancelGeneration(request.frame_epoch);
}
(void)downloader_.pump();
if (downloader_.isBusy()) vTaskDelay(pdMS_TO_TICKS(1));
}
Hardware::TDeck::MapTileDownloadResult result{};
while (downloader_.takeResult(result)) {
if (result.generation == request.frame_epoch &&
result.key.zoom == request.key.zoom && result.key.x == request.key.x &&
result.key.y == request.key.y) {
return result.code == Hardware::TDeck::MapTileResultCode::SUCCESS
? Pyxis::MapTileLoadResult::READY
: Pyxis::MapTileLoadResult::DOWNLOAD_FAILED;
}
}
return Pyxis::MapTileLoadResult::DOWNLOAD_FAILED;
}
Pyxis::MapTileLoadResult MapScreen::readTile(
const Pyxis::MapTileRequest& request) {
if (!store_initialized_) {
return Pyxis::MapTileLoadResult::STORAGE_UNAVAILABLE;
@@ -340,8 +412,22 @@ Pyxis::MapTileLoadResult MapScreen::loadTile(
unsigned char* rgb = nullptr;
unsigned width = 0U;
unsigned height = 0U;
const unsigned decode_error = lodepng_decode24(
&rgb, &width, &height, compressed_staging_, total);
LodePNGState decode_state;
lodepng_state_init(&decode_state);
// Bound the largest legal 256x256 source scanline stream (RGBA16 plus
// filter bytes/interlace overhead) independently of compressed size.
decode_state.decoder.zlibsettings.max_output_size = 528U * 1024U;
unsigned decode_error = lodepng_inspect(
&width, &height, &decode_state, compressed_staging_, total);
if (decode_error != 0U || width != 256U || height != 256U) {
lodepng_state_cleanup(&decode_state);
return Pyxis::MapTileLoadResult::INVALID_PNG;
}
decode_state.info_raw.colortype = LCT_RGB;
decode_state.info_raw.bitdepth = 8U;
decode_error = lodepng_decode(
&rgb, &width, &height, &decode_state, compressed_staging_, total);
lodepng_state_cleanup(&decode_state);
if (decode_error != 0U || rgb == nullptr || width != 256U || height != 256U) {
if (rgb) lv_mem_free(rgb);
return Pyxis::MapTileLoadResult::INVALID_PNG;
@@ -464,6 +550,9 @@ void MapScreen::setStatusFor(Pyxis::MapTileLoadResult result) {
case Pyxis::MapTileLoadResult::TOO_LARGE:
lv_label_set_text(status_label_, "Tile too large");
break;
case Pyxis::MapTileLoadResult::DOWNLOAD_FAILED:
lv_label_set_text(status_label_, "Download failed");
break;
case Pyxis::MapTileLoadResult::IO_ERROR:
lv_label_set_text(status_label_, "Tile I/O error");
break;
+14
View File
@@ -18,6 +18,8 @@
#include "Hardware/TDeck/MapTileStore.h"
#include "Hardware/TDeck/MapTileStoreSD.h"
#include "Hardware/TDeck/MapTileDownloader.h"
#include "Hardware/TDeck/MapTileHttpArduino.h"
namespace UI {
namespace LXMF {
@@ -46,6 +48,9 @@ public:
// These methods never call LVGL and are invoked before LVGL_LOCK.
void serviceIo();
void updateModel(const Pyxis::MapView::Request& request);
void setDownloadEnabled(bool enabled) {
downloads_enabled_.store(enabled, std::memory_order_release);
}
// These methods only mutate the pre-created object pool and are invoked
// while UIManager owns LVGL_LOCK.
@@ -75,6 +80,13 @@ private:
Hardware::TDeck::MapTileStoreSD storage_;
Hardware::TDeck::TileStoreConfig store_config_;
Hardware::TDeck::MapTileStore store_;
Hardware::TDeck::MapTileStoreDownloadAdapter download_store_;
Hardware::TDeck::MapTileHttpArduino download_transport_;
Hardware::TDeck::MapTileMillisClock download_clock_;
Hardware::TDeck::MapTileDownloadPolicy download_policy_;
Hardware::TDeck::MapTileDownloadConfig download_config_;
Hardware::TDeck::MapTileDownloader downloader_;
std::atomic<bool> downloads_enabled_;
std::uint8_t* compressed_staging_;
SemaphoreHandle_t state_mutex_;
TaskHandle_t worker_task_;
@@ -93,6 +105,8 @@ private:
static void workerEntry(void* context);
void workerLoop();
Pyxis::MapTileLoadResult loadTile(const Pyxis::MapTileRequest& request);
Pyxis::MapTileLoadResult readTile(const Pyxis::MapTileRequest& request);
Pyxis::MapTileLoadResult downloadTile(const Pyxis::MapTileRequest& request);
bool startWorker();
void stopWorker();
bool lockState(TickType_t ticks = portMAX_DELAY);
@@ -314,6 +314,7 @@ MapTileSlot::State MapScreenPresenter::stateFor(MapTileLoadResult result) {
return MapTileSlot::STORAGE_UNAVAILABLE;
case MapTileLoadResult::INVALID_PNG: return MapTileSlot::INVALID_PNG;
case MapTileLoadResult::TOO_LARGE: return MapTileSlot::TOO_LARGE;
case MapTileLoadResult::DOWNLOAD_FAILED: return MapTileSlot::IO_ERROR;
case MapTileLoadResult::IO_ERROR: return MapTileSlot::IO_ERROR;
}
return MapTileSlot::IO_ERROR;
@@ -18,6 +18,7 @@ enum class MapTileLoadResult : std::uint8_t {
STORAGE_UNAVAILABLE,
INVALID_PNG,
TOO_LARGE,
DOWNLOAD_FAILED,
IO_ERROR
};
+36
View File
@@ -34,6 +34,7 @@ static const char* KEY_ANNOUNCE_INT = "announce";
static const char* KEY_SYNC_INT = "sync_int";
static const char* KEY_GPS_SYNC = "gps_sync";
static const char* KEY_TRANSPORT_ENABLED = "transport";
static const char* KEY_MAP_DOWNLOAD = "map_dl";
// Notification settings
static const char* KEY_NOTIF_SND = "notif_snd";
static const char* KEY_NOTIF_VOL = "notif_vol";
@@ -71,6 +72,7 @@ SettingsScreen::SettingsScreen(lv_obj_t* parent)
_ta_announce_interval(nullptr), _ta_sync_interval(nullptr), _switch_gps_sync(nullptr),
_switch_transport_enabled(nullptr), _transport_warning_modal(nullptr),
_transport_modal_group(nullptr), _transport_enable_confirmed(false),
_switch_map_download(nullptr),
_btn_propagation_nodes(nullptr), _switch_prop_fallback(nullptr), _switch_prop_only(nullptr),
_gps(nullptr) {
LVGL_LOCK();
@@ -867,6 +869,27 @@ void SettingsScreen::create_advanced_section(lv_obj_t* parent) {
lv_obj_align(_switch_gps_sync, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_switch_gps_sync, Theme::border(), LV_PART_MAIN);
lv_obj_set_style_bg_color(_switch_gps_sync, Theme::primary(), LV_PART_INDICATOR | LV_STATE_CHECKED);
// Optional network tile downloads are explicit opt-in and visible-tile only.
lv_obj_t* map_download_row = lv_obj_create(parent);
lv_obj_set_width(map_download_row, LV_PCT(100));
lv_obj_set_height(map_download_row, 28);
lv_obj_set_style_bg_opa(map_download_row, LV_OPA_TRANSP, 0);
lv_obj_set_style_border_width(map_download_row, 0, 0);
lv_obj_set_style_pad_all(map_download_row, 0, 0);
lv_obj_clear_flag(map_download_row, LV_OBJ_FLAG_SCROLLABLE);
lv_obj_t* map_download_label = lv_label_create(map_download_row);
lv_label_set_text(map_download_label, "Download map tiles:");
lv_obj_align(map_download_label, LV_ALIGN_LEFT_MID, 0, 0);
lv_obj_set_style_text_color(map_download_label, Theme::textTertiary(), 0);
lv_obj_set_style_text_font(map_download_label, &lv_font_montserrat_14, 0);
_switch_map_download = lv_switch_create(map_download_row);
lv_obj_set_size(_switch_map_download, 40, 20);
lv_obj_align(_switch_map_download, LV_ALIGN_RIGHT_MID, 0, 0);
lv_obj_set_style_bg_color(_switch_map_download, Theme::border(), LV_PART_MAIN);
lv_obj_set_style_bg_color(_switch_map_download, Theme::primary(),
LV_PART_INDICATOR | LV_STATE_CHECKED);
if (grp) lv_group_add_obj(grp, _switch_map_download);
}
void SettingsScreen::create_transport_mode_section(lv_obj_t* parent) {
@@ -1030,6 +1053,7 @@ void SettingsScreen::load_settings() {
_settings.sync_interval = prefs.getUInt(KEY_SYNC_INT, 14400); // Default 14400s = 4 hours
_settings.gps_time_sync = prefs.getBool(KEY_GPS_SYNC, true);
_settings.transport_enabled = prefs.getBool(KEY_TRANSPORT_ENABLED, false);
_settings.map_download_enabled = prefs.getBool(KEY_MAP_DOWNLOAD, false);
// Notification settings
_settings.notification_sound = prefs.getBool(KEY_NOTIF_SND, true);
@@ -1080,6 +1104,7 @@ void SettingsScreen::save_settings() {
prefs.putUInt(KEY_SYNC_INT, _settings.sync_interval);
prefs.putBool(KEY_GPS_SYNC, _settings.gps_time_sync);
prefs.putBool(KEY_TRANSPORT_ENABLED, _settings.transport_enabled);
prefs.putBool(KEY_MAP_DOWNLOAD, _settings.map_download_enabled);
// Notification settings
prefs.putBool(KEY_NOTIF_SND, _settings.notification_sound);
@@ -1190,6 +1215,13 @@ void SettingsScreen::update_ui_from_settings() {
}
_transport_enable_confirmed = false;
}
if (_switch_map_download) {
if (_settings.map_download_enabled) {
lv_obj_add_state(_switch_map_download, LV_STATE_CHECKED);
} else {
lv_obj_clear_state(_switch_map_download, LV_STATE_CHECKED);
}
}
// Interface settings
if (_switch_tcp_enabled) {
@@ -1328,6 +1360,10 @@ void SettingsScreen::update_settings_from_ui() {
if (_switch_transport_enabled) {
_settings.transport_enabled = lv_obj_has_state(_switch_transport_enabled, LV_STATE_CHECKED);
}
if (_switch_map_download) {
_settings.map_download_enabled =
lv_obj_has_state(_switch_map_download, LV_STATE_CHECKED);
}
// Interface settings
if (_switch_tcp_enabled) {
+3
View File
@@ -56,6 +56,7 @@ struct AppSettings {
uint32_t sync_interval; // seconds (0 = disabled; UI shows hours; default 14400 = 4h)
bool gps_time_sync;
bool transport_enabled; // Route traffic for other nodes; default off, requires reboot
bool map_download_enabled; // Explicit opt-in; visible tiles only
// Propagation
bool prop_auto_select; // Auto-select best propagation node
@@ -85,6 +86,7 @@ struct AppSettings {
sync_interval(14400),
gps_time_sync(true),
transport_enabled(false),
map_download_enabled(false),
prop_auto_select(true),
prop_selected_node(""),
prop_fallback_enabled(true),
@@ -293,6 +295,7 @@ private:
lv_obj_t* _ta_announce_interval;
lv_obj_t* _ta_sync_interval;
lv_obj_t* _switch_gps_sync;
lv_obj_t* _switch_map_download;
// Dangerous transport-mode section (must remain last in Settings)
lv_obj_t* _switch_transport_enabled;
+4
View File
@@ -1178,6 +1178,10 @@ void UIManager::set_gps(TinyGPSPlus* gps) {
}
}
void UIManager::set_map_download_enabled(bool enabled) {
if (_map_screen) _map_screen->setDownloadEnabled(enabled);
}
Telemetry::LocationConsentResult UIManager::start_location_sharing(
const Bytes& peer_hash,
const Telemetry::ShareStartOptions& options) {
+1
View File
@@ -197,6 +197,7 @@ public:
* @param gps TinyGPSPlus instance
*/
void set_gps(TinyGPSPlus* gps);
void set_map_download_enabled(bool enabled);
// Location sharing is always explicit and peer-scoped. No session exists
// until the UI calls start_location_sharing().
+5
View File
@@ -700,6 +700,7 @@ void load_app_settings() {
app_settings.sync_interval = prefs.getULong("sync_int", 14400); // Default 14400s = 4 hours
app_settings.gps_time_sync = prefs.getBool("gps_sync", true);
app_settings.transport_enabled = prefs.getBool("transport", false);
app_settings.map_download_enabled = prefs.getBool("map_dl", false);
// Propagation
app_settings.prop_auto_select = prefs.getBool("prop_auto", true);
@@ -1343,6 +1344,7 @@ void setup_ui_manager() {
while (1) delay(1000);
}
update_radio_activity_source();
ui_manager->set_map_download_enabled(app_settings.map_download_enabled);
// Set initial RNS connection status (check all interfaces)
{
@@ -1415,6 +1417,9 @@ void setup_ui_manager() {
// Set save callback (update app_settings and apply)
settings->set_save_callback([](const UI::LXMF::AppSettings& new_settings) {
if (ui_manager) {
ui_manager->set_map_download_enabled(new_settings.map_download_enabled);
}
UI::LXMF::RouterLock router_lock(0);
if (!router_lock.acquired()) {
WARNING("Router busy; settings application deferred by user retry");
@@ -49,7 +49,9 @@ def test_fixed_pool_and_cache_contracts():
def test_worker_predecodes_and_render_path_has_no_io():
source = text(UI / "MapScreen.cpp")
assert "lodepng_decode24" in source
assert "lodepng_decode(" in source
assert "lodepng_inspect" in source
assert "max_output_size" in source
assert "beginGet" in source and "readGetChunk" in source
assert 'lv_img_set_src(tile_images_[index], &tile_descriptors_[index])' in source
assert 'lv_img_set_src(tile_images_[index], "' not in source
@@ -5,6 +5,8 @@ CORE_H = ROOT / "lib/tdeck_ui/Hardware/TDeck/MapTileDownloader.h"
CORE_CPP = ROOT / "lib/tdeck_ui/Hardware/TDeck/MapTileDownloader.cpp"
ADAPTER_H = ROOT / "lib/tdeck_ui/Hardware/TDeck/MapTileHttpArduino.h"
ADAPTER_CPP = ROOT / "lib/tdeck_ui/Hardware/TDeck/MapTileHttpArduino.cpp"
MAP_SCREEN = ROOT / "lib/tdeck_ui/UI/LXMF/MapScreen.cpp"
SETTINGS = ROOT / "lib/tdeck_ui/UI/LXMF/SettingsScreen.cpp"
def test_portable_core_is_bounded_and_allocation_free():
@@ -29,3 +31,14 @@ def test_https_adapter_verifies_peer_with_explicit_ca_and_has_no_credentials():
assert "setTimeout" in source
for forbidden in ("Authorization", "Cookie", "username", "password", "SD.begin", "format(", "LittleFS"):
assert forbidden not in source
def test_downloader_is_explicitly_opt_in_and_wired_only_for_visible_misses():
screen = MAP_SCREEN.read_text()
settings = SETTINGS.read_text()
assert "downloadTile(request)" in screen
assert "downloader_.enqueue(request.key, request.frame_epoch)" in screen
assert "presenter_.frameEpoch() != request.frame_epoch" in screen
assert 'KEY_MAP_DOWNLOAD = "map_dl"' in settings
assert "prefs.getBool(KEY_MAP_DOWNLOAD, false)" in settings
assert "Download map tiles:" in settings
+9 -2
View File
@@ -114,7 +114,7 @@ void runUntilIdle(MapTileDownloader& d, FakeClock& clock, int limit = 40) {
MapTileDownloadResult take(MapTileDownloader& d) { MapTileDownloadResult r; CHECK(d.takeResult(r)); return r; }
void testDisabledByDefault() { beginTest(); FakeStore s; FakeTransport t; FakeClock c; MapTileDownloadPolicy p; MapTileDownloader d(s,t,c,p,config());
CHECK(!p.enabled); CHECK(d.enqueue(key(),7U)==MapTileEnqueueResult::DISABLED); CHECK(d.queuedCount()==0U); CHECK(t.starts==0); }
CHECK(!p.enabled); CHECK(d.enqueue(key(),7U)==MapTileEnqueueResult::POLICY_DISABLED); CHECK(d.queuedCount()==0U); CHECK(t.starts==0); }
void testCanonicalUrlAndBounds() { beginTest(); char out[MapTileDownloader::URL_CAPACITY];
CHECK(MapTileDownloader::canonicalUrl("https://tile.openstreetmap.org/",TileKey{22U,4194303U,4194303U},out,sizeof(out))==MapTileUrlResult::OK);
CHECK(std::string(out)=="https://tile.openstreetmap.org/22/4194303/4194303.png");
@@ -160,9 +160,16 @@ void testDestructorAbortsOwnedResources() { beginTest(); FakeStore s; FakeTransp
{ MapTileDownloader d(s,t,c,enabled(),config()); CHECK(d.enqueue(key(),1U)==MapTileEnqueueResult::ACCEPTED); CHECK(d.pump()==MapTilePumpResult::PROGRESSED); CHECK(d.pump()==MapTilePumpResult::PROGRESSED); CHECK(d.pump()==MapTilePumpResult::PROGRESSED); CHECK(s.open); }
CHECK(s.aborts==1); CHECK(t.closes==1); CHECK(!s.open);
}
void testRuntimeDisableCancelsAllWork() { beginTest(); FakeStore s; FakeTransport t; t.body=bytes(5000U); FakeClock c; MapTileDownloader d(s,t,c,enabled(),config());
CHECK(d.enqueue(key(1U),7U)==MapTileEnqueueResult::ACCEPTED); CHECK(d.enqueue(key(2U),8U)==MapTileEnqueueResult::ACCEPTED);
CHECK(d.pump()==MapTilePumpResult::PROGRESSED); CHECK(d.pump()==MapTilePumpResult::PROGRESSED); CHECK(d.pump()==MapTilePumpResult::PROGRESSED); CHECK(s.open);
d.setEnabled(false); runUntilIdle(d,c); CHECK(s.aborts==1); CHECK(t.closes==1); CHECK(d.queuedCount()==0U);
CHECK(take(d).code==MapTileResultCode::CANCELED); CHECK(take(d).code==MapTileResultCode::CANCELED);
CHECK(d.enqueue(key(),9U)==MapTileEnqueueResult::POLICY_DISABLED);
}
void testSdDisappearanceAndMailboxBound() { beginTest(); FakeStore s; FakeTransport t; t.body=bytes(5000U); FakeClock c; MapTileDownloader d(s,t,c,enabled(),config()); CHECK(d.enqueue(key(),1U)==MapTileEnqueueResult::ACCEPTED); CHECK(d.pump()==MapTilePumpResult::PROGRESSED); CHECK(d.pump()==MapTilePumpResult::PROGRESSED); s.available=false; runUntilIdle(d,c); CHECK(take(d).code==MapTileResultCode::STORE_UNAVAILABLE);
FakeStore s2; FakeTransport t2; FakeClock c2; MapTileDownloader d2(s2,t2,c2,enabled(),config()); for(std::uint32_t i=0;i<6U;++i) CHECK(d2.enqueue(key(i),i)==MapTileEnqueueResult::ACCEPTED); for(std::uint32_t i=0;i<6U;++i) CHECK(d2.cancelGeneration(i)==1U); CHECK(d2.resultCount()==MapTileDownloader::RESULT_CAPACITY); CHECK(d2.droppedResultCount()==0U); }
void testStress() { beginTest(); FakeStore s; FakeTransport t; FakeClock c; MapTileDownloader d(s,t,c,enabled(),config());
for(std::uint32_t i=0;i<100000U;++i){ TileKey k=key(i&3U); const std::uint32_t g=i&7U; MapTileEnqueueResult r=d.enqueue(k,g); CHECK(r==MapTileEnqueueResult::ACCEPTED||r==MapTileEnqueueResult::DUPLICATE||r==MapTileEnqueueResult::QUEUE_FULL); if((i&3U)==0U)d.cancelGeneration(g); MapTileDownloadResult ignored; while(d.takeResult(ignored)){} } CHECK(d.queuedCount()<=MapTileDownloader::QUEUE_CAPACITY); }
}
int main(){ testDisabledByDefault(); testCanonicalUrlAndBounds(); testDedupeAndQueueFullNoEviction(); testSuccessExactChunksAndPublicContract(); testStatusAndContentTypeFailures(); testLengthOverUnderAndChunkOverCap(); testTransportAndStoreFailuresAbort(); testCancellationAtStagesAndGenerationIsolation(); testTimeoutRollbackAndSaturation(); testDestructorAbortsOwnedResources(); testSdDisappearanceAndMailboxBound(); testStress(); std::cout<<"map tile downloader: "<<tests_run<<" tests passed\n"; }
int main(){ testDisabledByDefault(); testCanonicalUrlAndBounds(); testDedupeAndQueueFullNoEviction(); testSuccessExactChunksAndPublicContract(); testStatusAndContentTypeFailures(); testLengthOverUnderAndChunkOverCap(); testTransportAndStoreFailuresAbort(); testCancellationAtStagesAndGenerationIsolation(); testTimeoutRollbackAndSaturation(); testDestructorAbortsOwnedResources(); testRuntimeDisableCancelsAllWork(); testSdDisappearanceAndMailboxBound(); testStress(); std::cout<<"map tile downloader: "<<tests_run<<" tests passed\n"; }
+1 -1
View File
@@ -29,4 +29,4 @@ def test_bounded_map_tile_downloader(tmp_path: Path, sanitize: bool) -> None:
env["UBSAN_OPTIONS"] = "halt_on_error=1:print_stacktrace=1"
ran = subprocess.run([str(binary)], capture_output=True, text=True, timeout=60, env=env)
assert ran.returncode == 0, ran.stdout + ran.stderr
assert ran.stdout == "map tile downloader: 12 tests passed\n"
assert ran.stdout == "map tile downloader: 13 tests passed\n"