Files
pyxis/lib/tdeck_ui/UI/LXMF/NomadNetImageLoader.cpp
Torlando 091c93aa7c feat(nomadnet): image policy setting, transfer progress UI, render fixes, media flow gate
Finishing work on the inline page-image feature (929fa2c):

- Settings: new Browser card with the page-image loading policy
  (never/manual/auto/always, upstream image_loading), persisted in NVS
  (img_load, default auto); takes effect on the next page load.
- Loader: strict sequential /media fetch with per-image terminal
  failure semantics, progress-aware deadline (base window + RTT slack,
  refreshed by per-part transfer progress), and a manual "Load images"
  action surfaced as a browser-chrome button (no title overlap).
- Mailbox: PROGRESS events carry percent + advertised total (monotonic).
- Decoder: LV_COLOR_16_SWAP byte-swap on-device for lv_draw_img_decoded
  buffers (host tests keep mathematical RGB565).
- Render: decoded images blit via lv_draw_img_decoded with a uniform
  two-axis 8.8 zoom (no upscale) sized from the placement box; coords
  carries the native source dimensions (LVGL SW reads coords size as
  src_w/src_h/stride) with the native top-left anchored to center the
  scaled image. A width-only zoom made height-constrained boxes overflow
  the content edge; a display-size coords made LVGL stride a wide
  buffer as narrow, rendering a diagonal offset.
- Progress UI: thin full-width bar under the header (overall page
  fraction) plus status line "Image N of M - % - KB".
- Fork pin: microReticulum 921b3aa -> 0487775 (media Resource response
  fix + jobs()/outbound() self-re-entrancy guard) at all six sites.
- Tests: x86 real-peer flow gains the media scenario (raw file-response
  delivery, 12/12), reference bumped to NomadNet 1.4.3 (e1e8ab8) / RNS
  1.5.4 with hash-pinned files; CI installs matching rns/nomadnet and
  the flow timeout is raised to 300 s.
- Diagnostics: T:IMG_STATE serial dump (loader entries + decode slots),
  test-hook gated and stripped from tdeck-release (audit-verified).
2026-09-21 17:01:39 +00:00

198 lines
7.0 KiB
C++

// NomadNet page-image loader implementation. See NomadNetImageLoader.h.
#include "NomadNetImageLoader.h"
#include <new>
#include <utility>
namespace UI::LXMF::NomadNet {
void ImageLoader::reset_entries() {
for (std::size_t i = 0; i < CAPACITY; ++i) {
_entries[i].data = ImageLoadEntry{};
_entries[i].state = ImageState::SKIPPED;
_entries[i].terminal_failure = false;
}
}
ImageLoader::Entry* ImageLoader::find(std::size_t index) const {
return (index < _count) ? const_cast<Entry*>(&_entries[index]) : nullptr;
}
bool ImageLoader::configure(std::size_t image_count, const ImageLoadEntry* entries,
const std::string& resolved_destination,
std::uint32_t page_generation, ImagePolicy policy,
bool loopback, std::uint32_t rtt_ms, std::uint32_t edr_bps) {
try {
_generation = page_generation;
_resolved_destination = resolved_destination;
if (image_count > CAPACITY) image_count = CAPACITY;
// Reference policy gate: ALWAYS loads; NEVER never does; MANUAL loads
// nothing until the user explicitly requests a reload (entries start
// SKIPPED); AUTO uses the RTT/EDR gate (loopback always passes).
const bool gate = (policy == ImagePolicy::ALWAYS) ||
(policy == ImagePolicy::AUTO &&
auto_gate_allows(loopback, rtt_ms, edr_bps));
for (std::size_t i = 0; i < CAPACITY; ++i) {
_entries[i].data = ImageLoadEntry{};
_entries[i].state = ImageState::SKIPPED;
_entries[i].terminal_failure = false;
}
_count = 0;
_manual_policy = (policy == ImagePolicy::MANUAL);
for (std::size_t i = 0; i < image_count; ++i) {
const ImageLoadEntry& source = entries[i];
const std::string destination = source.same_destination
? resolved_destination : source.destination_hex;
if (destination.empty()) {
// Unresolvable destination (malformed url): skip, never fetch.
continue;
}
ImageLoadEntry entry;
entry.image_index = source.image_index;
entry.same_destination = source.same_destination;
entry.destination_hex = destination;
entry.path = source.path;
entry.cache_key = image_cache_key(destination, source.path);
if (!gate) {
_entries[_count] = Entry{};
_entries[_count].data = std::move(entry);
_entries[_count].state = ImageState::SKIPPED;
} else {
_entries[_count] = Entry{};
_entries[_count].data = std::move(entry);
_entries[_count].state = ImageState::PENDING;
}
++_count;
}
_active = _count;
return true;
} catch (const std::bad_alloc&) {
reset_entries();
_count = 0;
_active = 0;
return false;
}
}
ImageAction ImageLoader::poll() {
// Strictly sequential: at most one entry may be REQUESTING, and it is
// always the first non-terminal entry in document order. The position is
// derived from the states, so no cursor can drift.
for (std::size_t i = 0; i < _count; ++i) {
if (_entries[i].state == ImageState::REQUESTING) return ImageAction::NONE;
if (_entries[i].state == ImageState::PENDING) {
_entries[i].state = ImageState::REQUESTING;
_active = i;
return ImageAction::SEND_REQUEST;
}
}
return ImageAction::NONE;
}
std::size_t ImageLoader::request_images() {
// Manual-mode reload trigger (reference load_images). Only the MANUAL
// policy admits here; other policies either auto-loaded (AUTO/ALWAYS) or
// are permanently off (NEVER). Re-admits every SKIPPED, non-terminal
// entry so the sequential loader picks them up on the next poll.
if (!_manual_policy) return 0;
std::size_t re_admitted = 0;
for (std::size_t i = 0; i < _count; ++i) {
if (_entries[i].state == ImageState::SKIPPED && !_entries[i].terminal_failure) {
_entries[i].state = ImageState::PENDING;
++re_admitted;
}
}
return re_admitted;
}
bool ImageLoader::manual_pending() const {
if (!_manual_policy || _count == 0) return false;
for (std::size_t i = 0; i < _count; ++i) {
if (_entries[i].state == ImageState::SKIPPED && !_entries[i].terminal_failure)
return true;
}
return false;
}
bool ImageLoader::finish_active(bool success) {
if (_active >= _count) return false;
if (_entries[_active].state != ImageState::REQUESTING) return false;
_entries[_active].state = success ? ImageState::LOADED : ImageState::FAILED;
if (!success) _entries[_active].terminal_failure = true;
_active = _count; // no in-flight request
return true;
}
void ImageLoader::fail_image(uint16_t image_index, bool terminal) {
for (std::size_t i = 0; i < _count; ++i) {
if (_entries[i].data.image_index == image_index) {
_entries[i].state = ImageState::FAILED;
if (terminal) _entries[i].terminal_failure = true;
if (_active == i) _active = _count; // the in-flight one died
return;
}
}
}
void ImageLoader::cancel_all() {
reset_entries();
_count = 0;
_active = 0;
}
ImageState ImageLoader::state_of(uint16_t image_index) const {
for (std::size_t i = 0; i < _count; ++i) {
if (_entries[i].data.image_index == image_index) return _entries[i].state;
}
return ImageState::SKIPPED;
}
const ImageLoadEntry* ImageLoader::active_entry() const {
if (_active >= _count) return nullptr;
if (_entries[_active].state != ImageState::REQUESTING) return nullptr;
return &_entries[_active].data;
}
std::size_t ImageLoader::active_position() const {
if (_active >= _count) return 0;
if (_entries[_active].state != ImageState::REQUESTING) return 0;
return _active + 1;
}
bool ImageLoader::has_next_after_active() const {
for (std::size_t i = _active + 1; i < _count; ++i) {
if (_entries[i].state == ImageState::PENDING) return true;
}
return false;
}
std::size_t ImageLoader::loaded_count() const {
std::size_t n = 0;
for (std::size_t i = 0; i < _count; ++i)
if (_entries[i].state == ImageState::LOADED) ++n;
return n;
}
std::size_t ImageLoader::failed_count() const {
std::size_t n = 0;
for (std::size_t i = 0; i < _count; ++i)
if (_entries[i].state == ImageState::FAILED) ++n;
return n;
}
bool ImageLoader::all_done() const {
for (std::size_t i = 0; i < _count; ++i) {
if (_entries[i].state == ImageState::PENDING ||
_entries[i].state == ImageState::REQUESTING)
return false;
}
return true;
}
std::string ImageLoader::active_destination() const {
const auto* entry = active_entry();
return entry ? entry->destination_hex : std::string();
}
} // namespace UI::LXMF::NomadNet