Skip to content

Generalize OS-environment detection; make storage failures non-fatal; add build provenance - #383

Open
3rdIteration wants to merge 13 commits into
devfrom
generalized-platform-detection
Open

Generalize OS-environment detection; make storage failures non-fatal; add build provenance#383
3rdIteration wants to merge 13 commits into
devfrom
generalized-platform-detection

Conversation

@3rdIteration

Copy link
Copy Markdown
Owner

Why

The app decided "am I on SeedSigner OS?" solely from the hostname
(Settings.HOSTNAME == "seedsigner-os"), wired into ~18 sites. That is fragile —
the same board can run either SeedSigner OS or a development OS (e.g. a
Raspberry Pi) — and it crashed the app on the Luckfox Pico: the wrong hostname
dropped the app into desktop mode, which ran mkdir('/microsd') on a full rootfs
and raised OSError [Errno 28], escaping main() (exit 1).

What

Generalized OS-environment detection (models/settings.py) — a new
OS_ENVIRONMENT (seedsigner_os / dev_board / desktop), distinct from the board
(RUNTIME_PROFILE):

  1. positive marker file /etc/seedsigner-os-release (shipped by seedsigner-os),
  2. HOSTNAME == "seedsigner-os" fallback (back-compat with existing images),
  3. /home/pi → dev board (Raspberry Pi OS),
  4. otherwise desktop.

New helpers Settings.is_seedsigner_os() / is_dev_board() / is_desktop()
replace every raw hostname / /home/pi / is_desktop_mode() check
(settings.py, hardware/microsd.py, view.py, settings_views.py,
gpg_views.py, smartcard_views.py). Also fixes the missing lc_lafrite entry
in the platform-name maps.

Storage / command failures are handled, never crash — the boot-time
interstitials in Controller.start() run before the main loop's exception
handling, so a failure there escaped main(). They are now wrapped and routed to
the normal error flow. MicroSD.get_microsd_dir(), screensaver.load_boot_logo_image()
and the microSD FIFO thread are hardened against OSError.

numpy preload guard — the background time_import('numpy') /
pivideostream preload (Pi-camera-only; numpy can't build on the Luckfox uClibc
toolchain) no longer aborts the whole preload thread. QR scanning already works
without numpy (pyzbar decodes PIL images directly).

Build provenance in System Infohelpers/seedsigner_os.get_os_release() and
is_running_from_microsd(); the System Info view shows the app version, the
seedsigner-os and app branch/commit/date, and "Internal" vs "Running from MicroSD".

Tests

  • tests/test_os_environment.py — the four detection branches.
  • tests/test_custom_logo.py — boot-logo survives a storage error.
  • Updated test_flows_view.py / test_flows_settings.py to patch OS_ENVIRONMENT
    instead of HOSTNAME for OS-mode simulation.

Companion

The /etc/seedsigner-os-release marker is produced by the seedsigner-os build
(branch merge-luckfox-pico-build). To build a Luckfox image with these app
changes, run build-luckfox.yml there with seedsigner_branch=generalized-platform-detection.

🤖 Generated with Claude Code

3rdIteration and others added 13 commits July 24, 2026 09:53
… add build provenance

The app previously decided "am I on SeedSigner OS?" solely from the hostname
(HOSTNAME == "seedsigner-os"), wired into ~18 sites. That is fragile — the same
board can run either SeedSigner OS or a development OS — and it crashed the app
on Luckfox (wrong hostname -> desktop mode -> mkdir('/microsd') on a full rootfs
-> OSError [Errno 28], escaping main()).

- Add a generalized OS-environment detector in models/settings.py, distinct from
  the board (RUNTIME_PROFILE): a positive marker file /etc/seedsigner-os-release
  (shipped by seedsigner-os), with a hostname fallback and a /home/pi dev-board
  check. New helpers Settings.is_seedsigner_os()/is_dev_board()/is_desktop().
- Route all Axis-B sites (settings.py, hardware/microsd.py, view.py,
  settings_views.py, gpg_views.py, smartcard_views.py) through the helpers.
  microsd.py is_desktop_mode() now delegates to Settings.is_desktop(). Fix the
  missing lc_lafrite entry in the platform-name maps.
- Make storage/command failures non-fatal: wrap the boot-time interstitials in
  controller.start() so they route into handle_exception instead of escaping
  main(); harden MicroSD.get_microsd_dir() mkdir and screensaver.load_boot_logo_image()
  against OSError; guard the microSD FIFO-thread I/O.
- Guard the numpy/pivideostream background preload so a missing optional module
  (numpy can't build on the Luckfox uClibc toolchain) doesn't abort the thread.
- Add helpers/seedsigner_os.get_os_release() and is_running_from_microsd(); show
  app version, seedsigner-os and app branch/commit/date, and Internal vs
  Running-from-MicroSD in the System Info view.
- Tests: test_os_environment.py for the four detection branches; a boot-logo
  storage-error resilience test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The restart-command and microSD-removal tests previously simulated SeedSigner OS
by setting Settings.HOSTNAME. Detection is now based on the computed
Settings.OS_ENVIRONMENT, so patch that instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oot.yml, add build-luckfox.yml

The existing "Build" workflow only builds the Buildroot (Pi/La Frite) images via
docker compose; running it with target=luckfox failed because opt/build.sh has no
such target. Rename it to "Build Buildroot" to make that explicit.

Add "Build Luckfox", which reuses the seedsigner-os build-luckfox reusable
workflow to build a Luckfox image from the current app branch (defaults to the
RV1103 Mini / SD_CARD combo). Runs on workflow_dispatch (pick hardware/boot/refs)
and push to main; the ~1-2h build intentionally does not run on every PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Mini ties the ST7789 LCD CS to GND (no CE wiring), so it needs the driver's
SPI_NO_CS / SPI mode 3 path (SCK idles high, avoiding boot-time SCK noise
corrupting the command stream). The ST7789 driver already implements this when
the display config has cs="disabled" (with a Luckfox EINVAL fallback that keeps
mode 3), but FOX_22's io_config entry lacked the key, so it ran the default
kernel-managed mode-0 path and the display stayed blank. Pro Max (FOX_40) wires
CS to CE0 and works in mode 0, so it is left unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…oards

Adds a REBOOT_LOADER option to PowerOptionsView, shown only when RUNTIME_PROFILE
is a Luckfox Pico board (luckfox_22/40/pi). Selecting it reboots the device into
Rockchip rockusb Loader mode so it can be re-flashed over USB (SocToolKit /
rkdeveloptool) without the BOOT button. It issues the reboot(2) RESTART2 syscall
with "loader" directly (busybox reboot ignores the mode arg), mirroring the
rk-reboot helper. Adds RebootToLoaderView + RebootToLoaderScreen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
LargeButtonScreen only lays out 2 or 4 buttons (and its arrow-nav only handles
the 4-button case), so the Luckfox 3-button Power menu (Restart / Power off /
Reboot to flash mode) raised "LargeButtonScreen only supports 2 or 4 buttons"
the moment Settings -> Power opened.

Select the screen class by option count: keep LargeButtonScreen for the 2-button
Pi/desktop menu (unchanged) and use the scrollable ButtonListScreen when the
Luckfox reboot-to-loader option makes it 3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The FOX_22 (Luckfox Pico Mini) display block carried "cs": "disabled" left over
from the abandoned Mini display debugging. It broke
test_io_config_profiles.py::test_existing_profiles_have_no_cs_field_by_default,
which asserts no shipped profile declares a 'cs' field by default — failing the
entire test and desktop CI matrices.

Removing it is also the correct config: the Mini uses kernel-managed CE CS, and
the field never fixed the blank screen. The SPI_NO_CS / Mode-3 / EINVAL-fallback
support remains and stays covered by the synthetic-mapping ST7789/ST7735 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On Luckfox the SDK auto-mounts the card at /mnt/sdcard (not /mnt/microsd) and
NAND/eMMC builds have no card, so settings never persisted. Add a resolver that
treats both /mnt/microsd and /mnt/sdcard as the SeedSigner-OS data dir (a mounted
card wins; else the first existing dir; else /mnt/microsd). Route
MicroSD.get_microsd_dir(), MicroSD.is_inserted and Settings.SETTINGS_FILENAME
through it, and makedirs the target before writing settings.

Effect:
- Luckfox SD_CARD: settings persist on the card at /mnt/sdcard.
- Luckfox NAND/eMMC (no card): /mnt/sdcard is an empty mountpoint on the writable
  UBIFS rootfs, so settings persist there across reboots without a partition.
- Raspberry Pi / La Frite: unchanged. Their root is stateless (wipes on reboot),
  so persistence still requires a physical card; /mnt/sdcard does not exist there,
  so the resolver returns /mnt/microsd and real-card detection / persistent-
  settings gating are byte-for-byte preserved.

Adds tests/test_microsd_resolver.py covering both platform behaviors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…gnal

Adds a button-free recovery gesture on Luckfox: holding KEY3 for ~5s on the Home
screen reboots into rockusb Loader mode (same as the Power menu "Reboot to flash
mode"); a short KEY3 tap still selects.

- buttons.py: add is_pressed(key) — raw held-state read (no debounce/edge
  consumption) for hold polling.
- screen.py: LargeButtonScreen gains optional long_press_key/ret_code/ms; the
  _run() loop discriminates tap-vs-hold outside the render lock. MainMenuScreen
  enables KEY3→RET_CODE__REBOOT_TO_LOADER only on Luckfox profiles.
- view.py: MainMenuView routes RET_CODE__REBOOT_TO_LOADER → RebootToLoaderView,
  and on SeedSigner OS writes /tmp/seedsigner-ready (boot-watchdog signal) and
  resets the U-Boot bootcount once per boot (best-effort fw_setenv).

Desktop/other boards are unchanged (long_press_key stays None; the ready signal
is gated to is_seedsigner_os()).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Luckfox U-Boot boot-failover counter is now memory-backed
(CONFIG_SYS_BOOTCOUNT_ADDR = GRF OS_REG scratch register 0xFF020218),
not env-backed, because this Rockchip U-Boot 2017.09 fork has no
CONFIG_BOOTCOUNT_LIMIT/_ENV Kconfig symbols. On reaching Home, clear the
counter with `devmem 0xFF020218 32 0` instead of `fw_setenv bootcount 0`
— this targets the real counter and writes no flash (the old fw_setenv
wrote the mtd0 env on every Home visit and was ignored by the backend).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ct tests

Two B11 (Tools_View refactor) regressions that only surfaced on hardware:

1. SEEDKEEPER_DIC_TYPE NameError (view secrets / load descriptors, all
   platforms): the pysatochip try-block in smartcard_views imported three
   modules that don't exist (pysatochip.satochip, pysatochip.exception,
   pysatochip.satochip_protocol_helper), and `except ImportError: pass`
   swallowed the whole block, so the valid JCconstants names never bound.
   Fix the import paths (UnexpectedSW12Error lives in
   pysatochip.CardConnector; format_sw_error is the local
   seedsigner.helpers.iso7816 helper), move logger above the block, and
   log any future ImportError instead of swallowing it.

2. card_get_ndef AttributeError: the KeycardSatochipConnector adapter
   exposed no NDEF surface. Add card_get_ndef / card_get_ndef_v2 /
   card_set_ndef backed by the Keycard's native NDEF data slot
   (GET DATA / STORE DATA), returning pysatochip-shaped tuples and
   surfacing the card's real SW on failure. (The SeedKeeper-side
   AttributeError was the seedsigner-os per-toolchain pysatochip version
   split, fixed OS-side by unifying on 0.6a.)

Tests — this class of bug was invisible to the suite because
sys.modules['pysatochip'] = MagicMock() auto-creates any name:
- tests/test_pysatochip_contract.py: real-pysatochip contract checks in a
  pristine subprocess (constants bind in smartcard_views, connector has
  the NDEF methods the views call, view-style header decode), plus the
  adapter NDEF surface; skipped when real pysatochip isn't installed.
- Keycard adapter NDEF unit tests (mock card): roundtrip + real-SW paths.
- Hardware tests: SeedKeeper card_get_ndef/card_set_ndef roundtrip and
  list-secrets decoded with the real JCconstants dicts (mirroring
  ToolsSeedkeeperViewSecretsView); Keycard NDEF slot roundtrip.

Full suite: 849 passed, 188 skipped, 1 xfailed; the 5 l10n failures are
pre-existing on this machine (uncompiled translations).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…i & Lafrite"

The Luckfox workflow could only pass hardware/boot/app-ref through to the
reusable seedsigner-os build, so there was no way to request a dev image
or any of the Luckfox-specific toggles from this repo. Add the full
manual-dispatch surface, matching what the Pi/La Frite build already
offers:

- build_variant (non-dev | dev) — the hardened air-gapped image vs the
  serial-console + adb debug image
- usb_mode (auto | host | gadget | otg), debug_network (auto | on | off),
  disable_uart2_console_debug (auto | true | false) — all default to auto,
  which follows build_variant
- ALL option for hardware_type / boot_medium (builds every valid combo)
- app-repo and release_tag, mirroring the Pi build's app-repo /
  upload-release inputs
- job permission raised to contents: write so release_tag can upload

Push-to-main behaviour is unchanged: a single non-dev Pico Mini / SD_CARD
build.

Also rename the Buildroot workflow to "Build Pi & Lafrite" (display name
and run-name only; the filename stays so existing links and history are
unaffected).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The workflow was renamed to "Build Pi & Lafrite" but could not build a
La Frite image: `lafrite` was absent from the target input's description
and from the "all" matrix expansion, so it was only reachable by typing
the undocumented value by hand.

Add lafrite to the "all" expansion and document it in the input. Note in
the description that upstream ships only lafrite-smartcard and
lafrite-smartcard-dev configs, so La Frite requires smartcard enabled
(the workflow's default).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant