mirror of
https://github.com/apache/nuttx-apps.git
synced 2026-08-09 06:25:16 +00:00
system/nxstore: address review findings and fix hardware-found bugs.
Review/hardening findings from the companion nxpkg PRs: - Own framebuffer/input device paths instead of borrowing CONFIG_EXAMPLES_LVGLDEMO_FBDEVPATH/INPUT_DEVPATH from an unrelated example app: new CONFIG_SYSTEM_NXSTORE_FBDEVPATH/INPUT_DEVPATH Kconfig string options (defaulting to /dev/fb0 and /dev/input0). - g_index moves from a static struct to a heap-allocated FAR struct pkg_index_s * (pkg_zalloc()), with a "Not enough memory to load the catalog." UI fallback if the allocation fails. - nxstore_launch() now loads the specific installed version's manifest via pkg_metadata_load_manifest_path() instead of combining a rollback-selected version with whatever the current catalog entry happens to describe for that name - those can disagree after a rollback if the catalog has since moved on. Also passes through the installed manifest's launch_args/launch_argc (previously always argv[1] = NULL). - title_text buffer sized PKG_NAME_MAX + PKG_VERSION_MAX + 5 instead of a fixed 96, fixing a real compiler truncation warning. - README.txt moved to the companion NuttX documentation PR rather than shipping user-facing documentation as an in-tree README. Bugs found bringing this up on real hardware: - LV_EVENT_LONG_PRESSED could fire for a touch that was actually driving a scroll of the app list: if a drag starts slowly enough that the long-press timer (400ms) elapses before the finger crosses the scroll-lock distance, LVGL hasn't committed the gesture to scrolling yet and still delivers the long-press event, silently uninstalling whatever card the touch happened to land on. Ignore the long-press if this input device is currently attributed to scrolling any object (lv_indev_get_scroll_obj()). - Tapping an installed-app card to launch it, while a different install was in progress elsewhere in the list (or another app was already running), was allowed through unconditionally - install_worker() auto-launches its own package once done, and letting a second launch through independently meant whichever one finished last silently overwrote g_running, leaving the other process alive, unsupervised, and drawing into the same shared framebuffer with no way to close it from this UI again. Both the direct-launch and fresh-install paths in install_btn_event_cb() now refuse (with a toast) if g_running.active or g_active.manifest indicate another app is already running or about to be. - nxstore_is_installed() only checked the package *name*, not which version was actually on disk - an older installed version showed as plain "Installed" identically to a current one, and tapping it silently launched the stale payload with no update indication or action. Add nxstore_is_up_to_date() (compares the installed version against the catalog's latest manifest) and a third status_bar color (in addition to not-installed/up-to-date) plus a "Update available - tap to update" subtitle for the mismatch case; tapping such a card now goes through the install path (which fetches and auto-launches the newly installed version) instead of the direct-launch path. - lv_indev_set_scroll_limit() was set to 255 (copied from examples/ lvgldemo/lvgldemo.c, whose own touch-drift mitigation this file reused), requiring a nearly-full-screen drag before a touch was even recognized as a scroll gesture. Unlike that demo, this screen's app list is scrolled constantly, and a threshold that large read as broken/laggy scrolling rather than drift protection. Lowered to 20, enough to reject typical touch-driver jitter while still recognizing a real scroll almost immediately; momentum stays off (scroll_throw 0), which is what the dual-launch/scroll-lock fixes above actually depend on, not the gesture-start threshold. Also adds nxstore_load_icon(): best-effort loads and caches a package's optional icon (manifest->icon) as a raw RGB565 image LVGL can render with no decoder (this board has no PNG/JPEG decode capability), falling back to the existing colored-circle-plus-glyph rendering on any failure (no icon set, download failed, corrupt/ oversized file) so a bad icon never blocks a package from being listed or installed. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: aviralgarg05 <gargaviral99@gmail.com>
This commit is contained in:
parent
778bc0e64c
commit
3d8bbb8630
3 changed files with 412 additions and 182 deletions
|
|
@ -8,6 +8,9 @@ config SYSTEM_NXSTORE
|
|||
default n
|
||||
depends on GRAPHICS_LVGL && SYSTEM_NXPKG
|
||||
select NETUTILS_CJSON
|
||||
select SCHED_WAITPID
|
||||
select LV_FONT_MONTSERRAT_12
|
||||
select LV_FONT_MONTSERRAT_20
|
||||
---help---
|
||||
Enable the Nxstore LVGL graphical application store frontend.
|
||||
Lists packages from the local nxpkg repository, installs the
|
||||
|
|
@ -27,4 +30,12 @@ config SYSTEM_NXSTORE_STACKSIZE
|
|||
int "nxstore stack size"
|
||||
default 8192
|
||||
|
||||
config SYSTEM_NXSTORE_FBDEVPATH
|
||||
string "Framebuffer device path"
|
||||
default "/dev/fb0"
|
||||
|
||||
config SYSTEM_NXSTORE_INPUT_DEVPATH
|
||||
string "Input device path"
|
||||
default "/dev/input0"
|
||||
|
||||
endif
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
nxstore - an LVGL touchscreen app store for NuttX
|
||||
=================================================
|
||||
|
||||
nxstore is a graphical front-end for system/nxpkg. It lists the
|
||||
packages available in a nxpkg repository, installs the one you tap via
|
||||
nxpkg, launches it, and supervises it with an on-screen "Close"
|
||||
control. It is the touchscreen equivalent of driving the nxpkg CLI by
|
||||
hand; both consume the same repository, so the repository/server setup
|
||||
(layout, how to serve it, populating it with tools/export_pkg_repo.py,
|
||||
pointing the board at it) is documented once, in system/nxpkg's
|
||||
README.txt, and is not repeated here.
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
Kconfig: SYSTEM_NXSTORE depends on GRAPHICS_LVGL and SYSTEM_NXPKG and
|
||||
selects NETUTILS_CJSON. It needs a working framebuffer (/dev/fb0) and
|
||||
a touch input device (/dev/input0) - the same graphics/input stack
|
||||
examples/lvgldemo uses. NETUTILS_CJSON is used to parse the
|
||||
repository index and package manifests.
|
||||
|
||||
|
||||
On-device lifecycle
|
||||
===================
|
||||
|
||||
1. Browse - the app list is built from the repository index nxpkg
|
||||
fetched. Installed packages show a launch action;
|
||||
not-yet-installed ones show an install action.
|
||||
2. Install - tapping an uninstalled package runs the nxpkg install
|
||||
flow (download -> sha256 verify -> transactional
|
||||
install) on a background worker thread, with progress
|
||||
shown as a toast.
|
||||
3. Launch - tapping an installed package spawns its payload
|
||||
(posix_spawn) and switches to the "running" screen: a
|
||||
thin bar across the top NXSTORE_BAR_HEIGHT pixels (see
|
||||
include/system/nxstore_chrome.h) carrying the app name
|
||||
and a Close button, with the rest of the framebuffer
|
||||
left to the running app.
|
||||
4. Close - see "Closing a running app" below.
|
||||
|
||||
Launched apps are given NXSTORE_LAUNCH_STACKSIZE (32 KiB) of stack -
|
||||
posix_spawn's default (CONFIG_POSIX_SPAWN_DEFAULT_STACKSIZE, 2 KiB) is
|
||||
far too small for a real LVGL/framebuffer app, and overflowing it does
|
||||
not fail loudly (it corrupts adjacent heap, surfacing later as a crash
|
||||
nowhere near the real cause).
|
||||
|
||||
|
||||
Closing a running app: the cooperative-SIGTERM contract
|
||||
=======================================================
|
||||
|
||||
This is the one thing an app author MUST get right to be launchable
|
||||
from nxstore.
|
||||
|
||||
The Close button sends the running app SIGTERM and waits for it to
|
||||
reap itself. It does NOT (and cannot safely) force-kill the task:
|
||||
|
||||
- On a build with CONFIG_SIG_DEFAULT disabled (the default), NuttX
|
||||
has no default action for SIGTERM at all. Delivery does nothing
|
||||
unless the app installs its own handler with sigaction().
|
||||
- An earlier nxstore fell back to task_delete() when SIGTERM went
|
||||
unreaped. On real hardware that force-kill landed while the app
|
||||
was mid framebuffer/heap access and hung the *entire board* (not
|
||||
just the task), requiring a physical power cycle. That fallback
|
||||
was removed: there is no safe way to force-terminate an arbitrary
|
||||
task from the outside here.
|
||||
|
||||
So an nxstore-launchable app must cooperate:
|
||||
|
||||
- Install an async-signal-safe SIGTERM handler that only sets a
|
||||
volatile sig_atomic_t flag.
|
||||
- Poll that flag from its own main loop and exit cleanly (freeing
|
||||
the framebuffer, restoring input state, etc.) when it is set.
|
||||
|
||||
Apps whose main loop can never block indefinitely (they run to
|
||||
completion on their own, or fail fast at startup) do not strictly need
|
||||
a handler, but any app that renders in a long-lived loop does. See
|
||||
these in-tree examples for the exact pattern:
|
||||
|
||||
- games/NXDoom (i_install_quit_signal / i_poll_quit_signal)
|
||||
- examples/calculator
|
||||
- games/cgol, games/brickmatch
|
||||
|
||||
If SIGTERM is not reaped within the timeout, nxstore keeps the running
|
||||
screen up rather than returning to the app list - switching back while
|
||||
the app might still be alive and drawing into the framebuffer would
|
||||
just reproduce the original hang, hidden.
|
||||
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
SYSTEM_NXSTORE_PROGNAME program/registration name (default nxstore)
|
||||
SYSTEM_NXSTORE_PRIORITY task priority (default 100)
|
||||
SYSTEM_NXSTORE_STACKSIZE nxstore's own stack (default 8192)
|
||||
|
||||
NXSTORE_LAUNCH_STACKSIZE (the stack given to launched apps) and
|
||||
NXSTORE_BAR_HEIGHT (the reserved top bar) are compile-time constants in
|
||||
the source / include/system/nxstore_chrome.h rather than Kconfig
|
||||
options.
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
#include <nuttx/config.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <spawn.h>
|
||||
|
|
@ -34,6 +35,7 @@
|
|||
#include <stdatomic.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <syslog.h>
|
||||
#include <time.h>
|
||||
|
|
@ -41,6 +43,8 @@
|
|||
|
||||
#include <lvgl/lvgl.h>
|
||||
|
||||
#include <system/nxstore_chrome.h>
|
||||
|
||||
#include "../nxpkg/pkg.h"
|
||||
|
||||
/****************************************************************************
|
||||
|
|
@ -80,6 +84,14 @@
|
|||
#define NXSTORE_COLOR_WARNING 0xf5a623 /* In progress / slow */
|
||||
#define NXSTORE_COLOR_ERROR 0xf0554c /* Failed */
|
||||
|
||||
/* Sanity bound on a package icon's declared width/height (see
|
||||
* nxstore_load_icon()) - purely a defense against a garbage or
|
||||
* malicious icon file driving an oversized allocation/render, not a
|
||||
* real design constraint (icons are shown well under this size).
|
||||
*/
|
||||
|
||||
#define NXSTORE_ICON_MAX_DIM 128
|
||||
|
||||
/****************************************************************************
|
||||
* Private Types
|
||||
****************************************************************************/
|
||||
|
|
@ -144,7 +156,16 @@ struct running_app_s
|
|||
****************************************************************************/
|
||||
|
||||
static lv_obj_t *g_list;
|
||||
static struct pkg_index_s g_index;
|
||||
|
||||
/* Heap-allocated (nxstore_main()'s first-run allocation in
|
||||
* build_app_store_ui()) rather than a plain static struct - each manifest
|
||||
* slot is ~1.7KB and PKG_INDEX_MAX has grown past what's comfortable to
|
||||
* carve out of internal DRAM as a fixed BSS array (see the comment on
|
||||
* PKG_INDEX_MAX in pkg.h). calloc()/pkg_zalloc() on this board's tasks
|
||||
* draws from the PSRAM-backed user heap instead.
|
||||
*/
|
||||
|
||||
static FAR struct pkg_index_s *g_index;
|
||||
static struct install_ctx_s g_active;
|
||||
|
||||
/* g_main_scr is the normal app-list screen (built once in
|
||||
|
|
@ -164,7 +185,7 @@ static struct running_app_s g_running;
|
|||
static void nxstore_toast(bool is_error, FAR const char *fmt, ...);
|
||||
static lv_obj_t *nxstore_progress_bar_start(lv_obj_t *card);
|
||||
static void nxstore_progress_bar_stop(lv_obj_t *bar);
|
||||
static void nxstore_enter_running_screen(FAR const char *name, pid_t pid);
|
||||
static void nxstore_enter_running_screen(FAR const char *name);
|
||||
static void close_running_app_event_cb(lv_event_t *e);
|
||||
static void nxstore_poll_running_app(void);
|
||||
static void build_run_screen(void);
|
||||
|
|
@ -344,21 +365,36 @@ static void nxstore_progress_bar_stop(lv_obj_t *bar)
|
|||
* Name: nxstore_enter_running_screen
|
||||
*
|
||||
* Description:
|
||||
* Switches to the supervisor screen and records the spawned child so
|
||||
* nxstore_poll_running_app()/close_running_app_event_cb() can reap or
|
||||
* force-close it. Must only be called from the LVGL/main thread -
|
||||
* lv_screen_load() is not thread-safe.
|
||||
* Switches to the supervisor screen and forces that switch to actually
|
||||
* reach the physical framebuffer before returning. Deliberately does
|
||||
* NOT record a pid or set g_running.active - callers that can control
|
||||
* spawn order (the direct-launch path in install_btn_event_cb()) must
|
||||
* call this *before* nxstore_launch(), then set g_running.pid/active
|
||||
* themselves only once the spawn actually succeeds. Getting this
|
||||
* backwards (spawn first, switch screens after) leaves a real window,
|
||||
* observed on hardware, where the newly-spawned app starts drawing
|
||||
* into the framebuffer immediately while this task's own pending
|
||||
* screen switch only reaches the display whenever its LVGL loop next
|
||||
* happens to run - which could be starved indefinitely by a busy-looping
|
||||
* child, leaving the old app-list screen visibly stuck underneath/around
|
||||
* whatever the child draws for as long as it keeps running.
|
||||
*
|
||||
* The async install-then-launch path (nxstore_poll_active_install(),
|
||||
* reacting to a background thread that already called nxstore_launch()
|
||||
* itself) can't avoid that ordering - LVGL calls aren't safe off the
|
||||
* main thread, so the screen switch can only happen after the worker
|
||||
* reports done - but forcing the flush here still shrinks that window
|
||||
* to one polling tick instead of leaving it open indefinitely.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static void nxstore_enter_running_screen(FAR const char *name, pid_t pid)
|
||||
static void nxstore_enter_running_screen(FAR const char *name)
|
||||
{
|
||||
g_running.pid = pid;
|
||||
g_running.active = true;
|
||||
snprintf(g_running.name, sizeof(g_running.name), "%s", name);
|
||||
|
||||
lv_label_set_text(g_run_label, g_running.name);
|
||||
lv_screen_load(g_run_scr);
|
||||
lv_refr_now(NULL);
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
|
|
@ -552,7 +588,7 @@ static void build_run_screen(void)
|
|||
lv_obj_clear_flag(g_run_scr, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
bar = lv_obj_create(g_run_scr);
|
||||
lv_obj_set_size(bar, lv_pct(100), 36);
|
||||
lv_obj_set_size(bar, lv_pct(100), NXSTORE_BAR_HEIGHT);
|
||||
lv_obj_align(bar, LV_ALIGN_TOP_MID, 0, 0);
|
||||
lv_obj_set_style_bg_color(bar, lv_color_hex(NXSTORE_COLOR_HEADER_BG), 0);
|
||||
lv_obj_set_style_radius(bar, 0, 0);
|
||||
|
|
@ -717,6 +753,45 @@ static bool nxstore_is_installed(FAR const struct pkg_manifest_s *manifest)
|
|||
return found;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: nxstore_is_up_to_date
|
||||
*
|
||||
* Description:
|
||||
* nxstore_is_installed() only checks the package *name*, not which
|
||||
* version is actually on disk. If an older version is installed and
|
||||
* the catalog's latest manifest for that name is newer, that made a
|
||||
* card read as plain "Installed" with no way to tell the two apart -
|
||||
* tapping it launched whatever old version was actually on disk, with
|
||||
* no update indication or action at all. Returns true only when the
|
||||
* installed version string exactly matches manifest->version.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static bool nxstore_is_up_to_date(FAR const struct pkg_manifest_s *manifest)
|
||||
{
|
||||
FAR struct pkg_installed_db_s *db;
|
||||
FAR struct pkg_installed_entry_s *entry;
|
||||
bool up_to_date;
|
||||
|
||||
db = pkg_zalloc(sizeof(*db));
|
||||
if (db == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pkg_metadata_load_installed(db) < 0)
|
||||
{
|
||||
pkg_free(db);
|
||||
return false;
|
||||
}
|
||||
|
||||
entry = pkg_metadata_find_installed(db, manifest->name);
|
||||
up_to_date = entry != NULL &&
|
||||
strcmp(entry->current, manifest->version) == 0;
|
||||
pkg_free(db);
|
||||
return up_to_date;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: install_worker
|
||||
*
|
||||
|
|
@ -870,26 +945,23 @@ static void nxstore_poll_active_install(void)
|
|||
}
|
||||
|
||||
/* A fresh successful install started out with the "not installed"
|
||||
* blue/download icon (set at populate_app_list() time) - flip it to
|
||||
* the green/play "installed" affordance now that it's true, instead
|
||||
* of leaving a stale icon until the next reboot repopulates the list.
|
||||
* blue status bar (set at populate_app_list() time) - flip it to the
|
||||
* green "installed" affordance now that it's true, instead of leaving
|
||||
* it stale until the next reboot repopulates the list. The icon
|
||||
* itself (child 0) is never state-colored - see populate_app_list()'s
|
||||
* own comment on why a real app icon can't double as that indicator -
|
||||
* status_bar (child 1) is the only thing that needs flipping here.
|
||||
*/
|
||||
|
||||
if (g_active.state == INSTALL_STATE_DONE_OK && g_active.btn != NULL)
|
||||
{
|
||||
lv_obj_t *icon = lv_obj_get_child(g_active.btn, 0);
|
||||
lv_obj_t *icon_label = icon != NULL ? lv_obj_get_child(icon, 0) : NULL;
|
||||
lv_obj_t *status_bar = lv_obj_get_child(g_active.btn, 1);
|
||||
|
||||
if (icon != NULL)
|
||||
if (status_bar != NULL)
|
||||
{
|
||||
lv_obj_set_style_bg_color(icon,
|
||||
lv_obj_set_style_bg_color(status_bar,
|
||||
lv_color_hex(NXSTORE_COLOR_SUCCESS), 0);
|
||||
}
|
||||
|
||||
if (icon_label != NULL)
|
||||
{
|
||||
lv_label_set_text(icon_label, LV_SYMBOL_PLAY);
|
||||
}
|
||||
}
|
||||
|
||||
if (g_active.btn != NULL)
|
||||
|
|
@ -907,8 +979,9 @@ static void nxstore_poll_active_install(void)
|
|||
* about to be hidden and never actually seen.
|
||||
*/
|
||||
|
||||
nxstore_enter_running_screen(g_active.manifest->name,
|
||||
g_active.launched_pid);
|
||||
g_running.pid = g_active.launched_pid;
|
||||
g_running.active = true;
|
||||
nxstore_enter_running_screen(g_active.manifest->name);
|
||||
break;
|
||||
|
||||
case INSTALL_STATE_INSTALL_FAILED:
|
||||
|
|
@ -969,6 +1042,21 @@ static void uninstall_btn_event_cb(lv_event_t *e)
|
|||
return;
|
||||
}
|
||||
|
||||
/* LV_EVENT_LONG_PRESSED can fire for a touch that is actually driving
|
||||
* a scroll of the enclosing list: if the drag starts slowly enough
|
||||
* that the long-press timer (400ms) elapses before the finger has
|
||||
* moved past the scroll-lock distance, LVGL hasn't committed the
|
||||
* gesture to scrolling yet and still delivers the long-press event.
|
||||
* This is what reads to a user as "it just happens when scrolling",
|
||||
* not a deliberate long-press. Ignore the event if this input
|
||||
* device is currently attributed to scrolling any object.
|
||||
*/
|
||||
|
||||
if (lv_indev_get_scroll_obj(lv_indev_active()) != NULL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
card = lv_event_get_target(e);
|
||||
|
||||
/* Reuses the same LV_STATE_DISABLED reentrancy guard as
|
||||
|
|
@ -995,29 +1083,24 @@ static void uninstall_btn_event_cb(lv_event_t *e)
|
|||
{
|
||||
if (ret == EXIT_SUCCESS)
|
||||
{
|
||||
lv_obj_t *icon;
|
||||
lv_obj_t *icon_label;
|
||||
lv_obj_t *status_bar;
|
||||
|
||||
snprintf(text, sizeof(text), "Removed - tap to reinstall");
|
||||
|
||||
/* Icon reverts to the "not installed" affordance now that
|
||||
* it genuinely isn't.
|
||||
/* status_bar (child 1) reverts to the "not installed"
|
||||
* affordance now that it genuinely isn't - the icon itself
|
||||
* (child 0) is never state-colored, same reasoning as the
|
||||
* install success path above.
|
||||
*/
|
||||
|
||||
icon = lv_obj_get_child(card, 0);
|
||||
icon_label = icon != NULL ? lv_obj_get_child(icon, 0) : NULL;
|
||||
status_bar = lv_obj_get_child(card, 1);
|
||||
|
||||
if (icon != NULL)
|
||||
if (status_bar != NULL)
|
||||
{
|
||||
lv_obj_set_style_bg_color(icon,
|
||||
lv_obj_set_style_bg_color(status_bar,
|
||||
lv_color_hex(NXSTORE_COLOR_ACCENT),
|
||||
0);
|
||||
}
|
||||
|
||||
if (icon_label != NULL)
|
||||
{
|
||||
lv_label_set_text(icon_label, LV_SYMBOL_DOWNLOAD);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -1038,10 +1121,13 @@ static void uninstall_btn_event_cb(lv_event_t *e)
|
|||
* Name: install_btn_event_cb
|
||||
*
|
||||
* Description:
|
||||
* Tapped list entry: launch directly if already installed, otherwise
|
||||
* kick off an async install+launch and show a progress bar while it
|
||||
* runs. Long-press (uninstall_btn_event_cb) removes an installed
|
||||
* entry.
|
||||
* Tapped list entry: launch directly if already installed *and*
|
||||
* up-to-date, otherwise kick off an async install+launch (this
|
||||
* fetches whatever version the catalog currently advertises and
|
||||
* launches it once done, which for an already-installed-but-outdated
|
||||
* package is exactly the "update" action) and show a progress bar
|
||||
* while it runs. Long-press (uninstall_btn_event_cb) removes an
|
||||
* installed entry.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
|
|
@ -1061,21 +1147,37 @@ static void install_btn_event_cb(lv_event_t *e)
|
|||
card = lv_event_get_target(e);
|
||||
subtitle = nxstore_card_subtitle(card);
|
||||
|
||||
if (nxstore_is_installed(manifest))
|
||||
if (nxstore_is_installed(manifest) && nxstore_is_up_to_date(manifest))
|
||||
{
|
||||
char orig[192];
|
||||
char text[256];
|
||||
pid_t pid = 0;
|
||||
int ret;
|
||||
|
||||
/* This doesn't touch g_active (the single install-worker slot) at
|
||||
* all, so it's safe to run even while an unrelated install is in
|
||||
* progress elsewhere in the list. What it does need is its own
|
||||
* reentrancy guard: a rapid double-tap on this exact button, while
|
||||
* the first tap's blocking nxstore_launch() call is still
|
||||
* resolving, must not spawn the target process twice.
|
||||
/* This board's display is a single shared framebuffer that only
|
||||
* one app can own at a time, and g_running is nxstore's only
|
||||
* record of who that is. A rapid double-tap on this exact button
|
||||
* is guarded below, but that alone isn't enough: this path used
|
||||
* to run unconditionally even while another app was already
|
||||
* running (g_running.active), or while an unrelated install
|
||||
* elsewhere in the list was about to auto-launch its own package
|
||||
* once done (install_worker() -> nxstore_launch() runs on its own
|
||||
* thread and g_active.manifest stays non-NULL for the whole
|
||||
* install+launch+settle window - see nxstore_poll_active_install()
|
||||
* - so checking it here closes the same race as checking
|
||||
* g_running.active, without needing to know which state the
|
||||
* install is currently in). Letting either through overwrote
|
||||
* g_running with whichever launch finished last, leaving the
|
||||
* other process alive, unsupervised, and drawing into the same
|
||||
* framebuffer with no way to close it from this UI again.
|
||||
*/
|
||||
|
||||
if (g_running.active || g_active.manifest != NULL)
|
||||
{
|
||||
nxstore_toast(true, "Close the running app first");
|
||||
return;
|
||||
}
|
||||
|
||||
if (lv_obj_has_state(card, LV_STATE_DISABLED))
|
||||
{
|
||||
return;
|
||||
|
|
@ -1091,6 +1193,14 @@ static void install_btn_event_cb(lv_event_t *e)
|
|||
lv_timer_handler();
|
||||
}
|
||||
|
||||
/* Switch to (and force-flush) the running screen *before* spawning
|
||||
* - see nxstore_enter_running_screen()'s own comment for why doing
|
||||
* it the other way around left the old app-list screen visibly
|
||||
* stuck under/around whatever the newly-launched app draws.
|
||||
*/
|
||||
|
||||
nxstore_enter_running_screen(manifest->name);
|
||||
|
||||
ret = nxstore_launch(manifest, &pid);
|
||||
if (subtitle != NULL)
|
||||
{
|
||||
|
|
@ -1101,14 +1211,15 @@ static void install_btn_event_cb(lv_event_t *e)
|
|||
|
||||
if (ret == 0)
|
||||
{
|
||||
/* No "launched" toast here, same reasoning as the install
|
||||
* path - the screen switch itself is the confirmation.
|
||||
*/
|
||||
|
||||
nxstore_enter_running_screen(manifest->name, pid);
|
||||
g_running.pid = pid;
|
||||
g_running.active = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
g_running.active = false;
|
||||
lv_screen_load(g_main_scr);
|
||||
lv_refr_now(NULL);
|
||||
|
||||
if (subtitle != NULL)
|
||||
{
|
||||
snprintf(text, sizeof(text),
|
||||
|
|
@ -1124,10 +1235,20 @@ static void install_btn_event_cb(lv_event_t *e)
|
|||
|
||||
if (g_active.manifest != NULL)
|
||||
{
|
||||
/* Only one *install* can run at a time (single worker slot);
|
||||
* launching an already-installed app above isn't gated by this.
|
||||
/* Only one *install* can run at a time (single worker slot). */
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_running.active)
|
||||
{
|
||||
/* This install will auto-launch its package once done (see
|
||||
* install_worker()) - starting it while another app already owns
|
||||
* the framebuffer would just reproduce the dual-launch race
|
||||
* guarded against above, once this install finishes.
|
||||
*/
|
||||
|
||||
nxstore_toast(true, "Close the running app first");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1179,6 +1300,127 @@ static void install_btn_event_cb(lv_event_t *e)
|
|||
pthread_attr_destroy(&attr);
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: nxstore_load_icon
|
||||
*
|
||||
* Description:
|
||||
* Best-effort load of a package's optional icon (manifest->icon) into
|
||||
* an lv_image_dsc_t, downloading and caching it under PKG_ROOT_DIR
|
||||
* first if it isn't already cached. The icon file format is a raw,
|
||||
* uncompressed image matching lv_image_header_t's own 12-byte layout
|
||||
* (magic/cf/flags, then w/h, then stride/reserved, all little-endian)
|
||||
* followed immediately by RGB565 pixel data - deliberately the
|
||||
* simplest thing LVGL can render directly with no decoder, since this
|
||||
* board has no PNG/JPEG decode capability wired up. tools/
|
||||
* export_pkg_repo.py's --icon option writes this exact format.
|
||||
*
|
||||
* Every failure mode (no icon set, download failed, corrupt/oversized
|
||||
* file) just returns NULL - callers fall back to the plain colored
|
||||
* circle + symbol glyph, so a bad icon never blocks the app from being
|
||||
* listed or installed.
|
||||
*
|
||||
* On success, both the returned descriptor and its data pointer are
|
||||
* heap-allocated and intentionally never freed - the descriptor is
|
||||
* handed straight to a permanent lv_image row widget (which keeps a
|
||||
* pointer to it, not a copy) that lives for nxstore's whole runtime,
|
||||
* so there's no point it's ever safe to free at. Must be heap, not a
|
||||
* caller's stack local: it has to outlive the populate_app_list() loop
|
||||
* iteration that creates it.
|
||||
*
|
||||
****************************************************************************/
|
||||
|
||||
static FAR lv_image_dsc_t *
|
||||
nxstore_load_icon(FAR const struct pkg_manifest_s *manifest)
|
||||
{
|
||||
char cache_path[PATH_MAX];
|
||||
char source[PATH_MAX];
|
||||
FAR lv_image_dsc_t *dsc;
|
||||
FAR uint8_t *buf;
|
||||
struct stat st;
|
||||
int fd;
|
||||
ssize_t nread;
|
||||
uint16_t w;
|
||||
uint16_t h;
|
||||
uint16_t stride;
|
||||
|
||||
if (manifest->icon[0] == '\0')
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
snprintf(cache_path, sizeof(cache_path), PKG_ROOT_DIR "/icons/%s.bin",
|
||||
manifest->name);
|
||||
|
||||
if (stat(cache_path, &st) < 0)
|
||||
{
|
||||
mkdir(PKG_ROOT_DIR "/icons", 0755);
|
||||
|
||||
if (pkg_resolve_icon_source(source, sizeof(source), manifest) < 0 ||
|
||||
pkg_acquire_source(source, cache_path, NULL) < 0 ||
|
||||
stat(cache_path, &st) < 0)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (st.st_size <= 12 || st.st_size > 64 * 1024)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
buf = pkg_malloc((size_t)st.st_size);
|
||||
if (buf == NULL)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
fd = open(cache_path, O_RDONLY);
|
||||
if (fd < 0)
|
||||
{
|
||||
pkg_free(buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
nread = read(fd, buf, (size_t)st.st_size);
|
||||
close(fd);
|
||||
|
||||
if (nread != st.st_size || buf[0] != LV_IMAGE_HEADER_MAGIC)
|
||||
{
|
||||
pkg_free(buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
w = (uint16_t)(buf[4] | (buf[5] << 8));
|
||||
h = (uint16_t)(buf[6] | (buf[7] << 8));
|
||||
stride = (uint16_t)(buf[8] | (buf[9] << 8));
|
||||
|
||||
if (w == 0 || h == 0 || w > NXSTORE_ICON_MAX_DIM ||
|
||||
h > NXSTORE_ICON_MAX_DIM || stride != w * 2 ||
|
||||
12 + (size_t)stride * h > (size_t)nread)
|
||||
{
|
||||
pkg_free(buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
dsc = pkg_zalloc(sizeof(*dsc));
|
||||
if (dsc == NULL)
|
||||
{
|
||||
pkg_free(buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
dsc->header.magic = buf[0];
|
||||
dsc->header.cf = buf[1];
|
||||
dsc->header.flags = (uint16_t)(buf[2] | (buf[3] << 8));
|
||||
dsc->header.w = w;
|
||||
dsc->header.h = h;
|
||||
dsc->header.stride = stride;
|
||||
dsc->data_size = (uint32_t)stride * h;
|
||||
dsc->data = buf + 12;
|
||||
|
||||
return dsc;
|
||||
}
|
||||
|
||||
/****************************************************************************
|
||||
* Name: populate_app_list
|
||||
*
|
||||
|
|
@ -1193,20 +1435,23 @@ static void populate_app_list(void)
|
|||
size_t seen_count = 0;
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < g_index.count; i++)
|
||||
for (i = 0; i < g_index->count; i++)
|
||||
{
|
||||
FAR const struct pkg_manifest_s *manifest;
|
||||
bool installed;
|
||||
bool up_to_date;
|
||||
bool dup = false;
|
||||
size_t j;
|
||||
lv_obj_t *card;
|
||||
lv_obj_t *icon;
|
||||
lv_obj_t *icon_label;
|
||||
lv_obj_t *status_bar;
|
||||
lv_obj_t *text_col;
|
||||
lv_obj_t *title_label;
|
||||
lv_obj_t *subtitle_label;
|
||||
lv_obj_t *chevron;
|
||||
char title_text[96];
|
||||
FAR lv_image_dsc_t *icon_dsc;
|
||||
char title_text[PKG_NAME_MAX + PKG_VERSION_MAX + 5];
|
||||
char subtitle_text[192];
|
||||
|
||||
/* The index can list multiple versions of the same package as
|
||||
|
|
@ -1221,7 +1466,7 @@ static void populate_app_list(void)
|
|||
|
||||
for (j = 0; j < seen_count; j++)
|
||||
{
|
||||
if (strcmp(seen_names[j], g_index.manifests[i].name) == 0)
|
||||
if (strcmp(seen_names[j], g_index->manifests[i].name) == 0)
|
||||
{
|
||||
dup = true;
|
||||
break;
|
||||
|
|
@ -1234,17 +1479,19 @@ static void populate_app_list(void)
|
|||
}
|
||||
|
||||
snprintf(seen_names[seen_count], sizeof(seen_names[seen_count]), "%s",
|
||||
g_index.manifests[i].name);
|
||||
g_index->manifests[i].name);
|
||||
seen_count++;
|
||||
|
||||
manifest = pkg_metadata_find_latest(&g_index,
|
||||
g_index.manifests[i].name);
|
||||
manifest = pkg_metadata_find_latest(g_index,
|
||||
g_index->manifests[i].name);
|
||||
if (manifest == NULL)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
installed = nxstore_is_installed(manifest);
|
||||
up_to_date = !installed || nxstore_is_up_to_date(manifest);
|
||||
icon_dsc = nxstore_load_icon(manifest);
|
||||
|
||||
/* Card: one flex row [icon circle][title+subtitle column][chevron],
|
||||
* styled as a distinct surface (rounded corners, subtle border)
|
||||
|
|
@ -1284,27 +1531,68 @@ static void populate_app_list(void)
|
|||
LV_FLEX_ALIGN_CENTER);
|
||||
lv_obj_clear_flag(card, LV_OBJ_FLAG_SCROLLABLE);
|
||||
|
||||
/* Icon circle: color + symbol double as the installed/not-installed
|
||||
* indicator, so status is legible at a glance without reading text.
|
||||
/* Icon circle: always shows the package's own art (icon_dsc) when
|
||||
* available, on a neutral background - install state used to be
|
||||
* conveyed by coloring this same circle, but that only worked for
|
||||
* the plain-glyph fallback; a real (opaque, same-size) icon image
|
||||
* just covered the ring color entirely, silently losing the only
|
||||
* install-state indicator in the row. status_bar below is a
|
||||
* dedicated indicator instead, so state stays legible regardless
|
||||
* of whether a row has real art or just the fallback glyph.
|
||||
*/
|
||||
|
||||
icon = lv_obj_create(card);
|
||||
lv_obj_set_size(icon, 44, 44);
|
||||
lv_obj_set_style_radius(icon, LV_RADIUS_CIRCLE, 0);
|
||||
lv_obj_set_style_bg_color(icon,
|
||||
lv_color_hex(installed
|
||||
? NXSTORE_COLOR_SUCCESS
|
||||
: NXSTORE_COLOR_ACCENT), 0);
|
||||
lv_color_hex(NXSTORE_COLOR_CARD_BORDER), 0);
|
||||
lv_obj_set_style_border_width(icon, 0, 0);
|
||||
lv_obj_set_style_pad_all(icon, 0, 0);
|
||||
lv_obj_set_style_clip_corner(icon, true, 0);
|
||||
lv_obj_clear_flag(icon, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_clear_flag(icon, LV_OBJ_FLAG_CLICKABLE);
|
||||
|
||||
icon_label = lv_label_create(icon);
|
||||
lv_label_set_text(icon_label,
|
||||
installed ? LV_SYMBOL_PLAY : LV_SYMBOL_DOWNLOAD);
|
||||
lv_obj_set_style_text_color(icon_label, lv_color_hex(0xffffff), 0);
|
||||
lv_obj_center(icon_label);
|
||||
if (icon_dsc != NULL)
|
||||
{
|
||||
lv_obj_t *icon_img = lv_image_create(icon);
|
||||
|
||||
lv_image_set_src(icon_img, icon_dsc);
|
||||
lv_image_set_scale(icon_img, 200);
|
||||
lv_obj_center(icon_img);
|
||||
}
|
||||
else
|
||||
{
|
||||
icon_label = lv_label_create(icon);
|
||||
lv_label_set_text(icon_label, LV_SYMBOL_FILE);
|
||||
lv_obj_set_style_text_color(icon_label,
|
||||
lv_color_hex(NXSTORE_COLOR_TEXT_MUTED),
|
||||
0);
|
||||
lv_obj_center(icon_label);
|
||||
}
|
||||
|
||||
/* Status bar: a small vertical stripe next to the icon, the only
|
||||
* thing that carries installed/not-installed state now - legible
|
||||
* at a glance without depending on the icon itself ever being
|
||||
* colorable (a real app icon's artwork is whatever it is). A
|
||||
* third color distinguishes "installed, but an older version than
|
||||
* the catalog's latest" from a fully up-to-date install - without
|
||||
* it, an outdated install looked identical to a current one and
|
||||
* gave no hint that tapping would still launch the old payload.
|
||||
*/
|
||||
|
||||
status_bar = lv_obj_create(card);
|
||||
lv_obj_set_size(status_bar, 6, 36);
|
||||
lv_obj_set_style_radius(status_bar, 3, 0);
|
||||
lv_obj_set_style_bg_color(status_bar,
|
||||
lv_color_hex(!installed
|
||||
? NXSTORE_COLOR_ACCENT
|
||||
: up_to_date
|
||||
? NXSTORE_COLOR_SUCCESS
|
||||
: NXSTORE_COLOR_WARNING), 0);
|
||||
lv_obj_set_style_border_width(status_bar, 0, 0);
|
||||
lv_obj_set_style_pad_all(status_bar, 0, 0);
|
||||
lv_obj_clear_flag(status_bar, LV_OBJ_FLAG_SCROLLABLE);
|
||||
lv_obj_clear_flag(status_bar, LV_OBJ_FLAG_CLICKABLE);
|
||||
|
||||
/* Text column: title never gets overwritten by install/launch
|
||||
* status (unlike the previous single-label design) - only the
|
||||
|
|
@ -1332,7 +1620,17 @@ static void populate_app_list(void)
|
|||
lv_color_hex(NXSTORE_COLOR_TEXT), 0);
|
||||
|
||||
subtitle_label = lv_label_create(text_col);
|
||||
if (manifest->description[0] != '\0')
|
||||
if (installed && !up_to_date)
|
||||
{
|
||||
/* Takes priority over the static description - an available
|
||||
* update is actionable state the user needs to see, and a
|
||||
* fixed description text would never convey it.
|
||||
*/
|
||||
|
||||
snprintf(subtitle_text, sizeof(subtitle_text),
|
||||
"Update available - tap to update");
|
||||
}
|
||||
else if (manifest->description[0] != '\0')
|
||||
{
|
||||
snprintf(subtitle_text, sizeof(subtitle_text), "%s",
|
||||
manifest->description);
|
||||
|
|
@ -1386,6 +1684,19 @@ static void build_app_store_ui(FAR const char *repo_url)
|
|||
g_main_scr = scr;
|
||||
lv_obj_set_style_bg_color(scr, lv_color_hex(NXSTORE_COLOR_BG), 0);
|
||||
|
||||
g_index = pkg_zalloc(sizeof(*g_index));
|
||||
if (g_index == NULL)
|
||||
{
|
||||
status = lv_label_create(scr);
|
||||
lv_obj_add_flag(status, LV_OBJ_FLAG_IGNORE_LAYOUT);
|
||||
lv_label_set_text(status, "Out of memory building catalog.");
|
||||
lv_obj_set_style_text_align(status, LV_TEXT_ALIGN_CENTER, 0);
|
||||
lv_obj_center(status);
|
||||
lv_obj_set_style_text_color(status, lv_color_hex(NXSTORE_COLOR_ERROR),
|
||||
0);
|
||||
return;
|
||||
}
|
||||
|
||||
/* The indev-level scroll_limit/scroll_throw settings in main() only
|
||||
* raise the bar for a scroll gesture to *start* - they don't stop one
|
||||
* from happening once that bar is cleared. Without also locking the
|
||||
|
|
@ -1572,7 +1883,7 @@ static void build_app_store_ui(FAR const char *repo_url)
|
|||
}
|
||||
}
|
||||
|
||||
ret = pkg_metadata_load_index(&g_index);
|
||||
ret = pkg_metadata_load_index(g_index);
|
||||
if (ret < 0)
|
||||
{
|
||||
status = lv_label_create(scr);
|
||||
|
|
@ -1587,7 +1898,7 @@ static void build_app_store_ui(FAR const char *repo_url)
|
|||
return;
|
||||
}
|
||||
|
||||
if (g_index.count == 0)
|
||||
if (g_index->count == 0)
|
||||
{
|
||||
/* The index parsed fine but has zero usable entries (empty
|
||||
* catalog, or every entry was for a different arch/board and got
|
||||
|
|
@ -1646,18 +1957,26 @@ int main(int argc, FAR char *argv[])
|
|||
return 1;
|
||||
}
|
||||
|
||||
/* Same touch-drift mitigation as examples/lvgldemo/lvgldemo.c: require
|
||||
* a large drag before scroll starts and kill momentum, so this board's
|
||||
* noisy touch driver can't turn a tap into an accidental scroll. The
|
||||
* list itself already clears LV_OBJ_FLAG_SCROLLABLE, but that's a
|
||||
* single-widget workaround - this covers the indev (and therefore the
|
||||
* whole screen, including anything added outside the list) the same
|
||||
* way the reference app does.
|
||||
/* Touch-drift mitigation: require a modest drag before scroll starts,
|
||||
* so this board's noisy touch driver can't turn a tap into an
|
||||
* accidental scroll. examples/lvgldemo/lvgldemo.c uses 255 for this
|
||||
* same setting, which this file originally copied verbatim - but
|
||||
* unlike that demo, this screen's list is something a user actually
|
||||
* scrolls constantly, and requiring a drag that large before anything
|
||||
* visibly responds reads as broken/laggy scrolling, not drift
|
||||
* protection. 20px is enough to reject typical touch-driver jitter
|
||||
* (single-digit pixels) while still recognizing a real scroll gesture
|
||||
* almost immediately. Momentum stays off (scroll_throw 0) - see
|
||||
* nxstore_enter_running_screen()'s and populate_app_list()'s own
|
||||
* comments on why a coasting scroll misaligning row positions caused
|
||||
* real "tapped one entry, a different one installed" reports; that
|
||||
* risk is about *momentum* continuing after release, not about the
|
||||
* gesture-start threshold, so it's independent of this value.
|
||||
*/
|
||||
|
||||
if (result.indev != NULL)
|
||||
{
|
||||
lv_indev_set_scroll_limit(result.indev, 255);
|
||||
lv_indev_set_scroll_limit(result.indev, 20);
|
||||
lv_indev_set_scroll_throw(result.indev, 0);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue