mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-08-28 23:00:28 +00:00
feat: wire safe opt-in map downloads
This commit is contained in:
@@ -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-----
|
||||
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
|
||||
-----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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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().
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"; }
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user