Debian policy reserves /usr/local/ for the local administrator;
packages may not install files there. dh_usrlocal was correctly
rejecting the staged tree built from remote-ui/square/files/.
Move:
remote-ui/square/files/usr/local/sbin/firstboot.sh
→ remote-ui/square/files/usr/lib/secubox/firstboot.sh
Update ExecStart in secubox-firstboot.service to match. The new
location is namespaced under /usr/lib/secubox/ consistent with
the sister helpers find-usb-serial and leasewatch.sh shipped by
secubox-system.
Bump 1.0.2 → 1.0.3. Local dpkg-buildpackage now produces a valid
.deb; `dpkg-deb -c` confirms:
- /usr/lib/secubox/firstboot.sh (0755)
- /etc/systemd/system/secubox-firstboot.service (with new ExecStart)
- no entries under /usr/local/
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(round): real glyphs for the 6 root-menu icons
The radial dashboard's ROOT menu (DEVICES, SECUBOX, LOCAL, NETWORK,
SECURITY, EXIT) was rendering with the letter-in-circle placeholders
emitted by assets/icons/generate_menu_icons.py — never replaced with
actual designs, so each slice on the Pi screen showed a coloured disk
with a single capital letter (D/S/L/N/S/E) over the slice label.
Replace those 12 PNGs (22 + 48 each) with proper PIL-drawn vector
glyphs (white #e8e6d9, transparent background, 4x supersampled AA):
devices — tablet + phone overlay with home dot
secubox — isometric cube outline (brand cube)
local — house silhouette with door
network — globe with meridians
security — shield with embedded checkmark
exit — door frame + arrow pointing out
New generator script assets/icons/generate_root_icons.py is committed
alongside the PNGs so the design can be tweaked and re-emitted; the
existing generate_menu_icons.py still owns the remaining ~40
sub-menu icons (which remain placeholders for now — a separate
follow-up will design those properly).
* fix(round): use common/ brand-icon glyphs (recolored to text-primary) for the 6 root menu slices
Replaces the letter-in-circle placeholders shipped for DEVICES, SECUBOX,
LOCAL, NETWORK, SECURITY, EXIT with the corresponding SecuBox brand
icons from remote-ui/common/assets/icons/ — the same glyphs the fallback
connected dashboard already uses on its rotating cube. To keep contrast
on every slice color, the brand-icon alpha mask is preserved but the
fill is normalized to text-primary (#e8e6d9), matching the existing
label color in radial_renderer.
Mapping mirrors the fallback dashboard's cube order so the menu reads
as a logical continuation of the connected state:
DEVICES → auth (CPU)
SECUBOX → wall (Memory / Firewall)
LOCAL → boot (Disk)
NETWORK → mind (Load / Brain)
SECURITY → root (Temp / Root)
EXIT → mesh (Network)
Drops the hand-drawn glyph generator (generate_root_icons.py) from the
previous commit — superseded by this approach.
* fix(round): fallback_manager finds the 6 brand icons (auth/wall/boot/mind/root/mesh)
The build script installs remote-ui/common/ at /var/www/common/ so the
real brand icons (auth-48.png … mesh-48.png) land at
/var/www/common/assets/icons/ on the image. fallback_manager.py however
only searched /usr/lib/secubox-eye/assets/icons/ (which holds the
round/-side menu placeholders) and so couldn't load any of the six
canonical module icons — the fallback radar's central icon stack came
out either blank or substituted with a placeholder.
Two-pronged fix:
1. fallback_manager.ICON_PATHS gains /var/www/common/assets/icons as the
third lookup path, before the local round/assets/icons fallback. First
existing file wins per icon name; the round-side dir keeps owning any
icon name not present in common/.
2. build-eye-remote-image.sh now also copies common/assets/icons/*.png
into /usr/lib/secubox-eye/assets/icons/ (with cp -n so the round/
placeholders aren't overwritten when names collide — none do today
anyway). Pure defense-in-depth so any consumer resolving icons via
either of the two canonical paths finds them.
Issue reported: on the rpiz boot, the offline fallback radar showed
five of the six module icons correctly but one slot was wrong/empty.
---------
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
These two edits were on fix/round-real-root-icons (commit a5b312f7) but
got wiped by my earlier force-push to fix/round-armv6-boot-services.
Re-applying identically so v4 picks them up.
fallback_manager.py: add /var/www/common/assets/icons as the third
lookup path so the 6 brand icons (auth/wall/boot/mind/root/mesh) ship
via common/ are found. Without this, fallback_manager only sees the
round/-side placeholder set and can't render the canonical module
icons on the fallback radar.
build-eye-remote-image.sh: also copy common/assets/icons/*.png into
/usr/lib/secubox-eye/assets/icons/ (cp -n so placeholders aren't
overwritten when names collide — none do). Pure defense-in-depth.
PR #175 commented out four ln -sf lines (two for secubox-eye-gadget.service
storage-only mode, two for secubox-eye-agent.service Pydantic-v2/SIGILL
crash loop). A force-push for the dwc2 fix earlier today inadvertently
overwrote that branch HEAD, dropping the four edits. Re-applying them
identically so the branch's intent — only secubox-otg-gadget.service
active at boot — is restored.
Combined with the dwc2 modprobe fix already on the branch, the rpiz now
boots into:
- secubox-otg-gadget.service modprobes dwc2, libcomposite, usb_f_*
- waits for /sys/class/udc to populate (poll, 5s)
- creates composite ECM+ACM gadget on the UDC
- MOCHAbin enumerates "Linux Foundation Multifunction Composite Gadget"
The previous build had two gadget services enabled at boot:
- secubox-eye-gadget.service: ExecStartPre=modprobe dwc2 + libcomposite
- secubox-otg-gadget.service: ExecStartPre=modprobe libcomposite + usb_f_*
(NO dwc2 — relied on eye-gadget loading it first)
When PR #175 disabled secubox-eye-gadget at boot (to stop the UDC conflict),
nobody loaded dwc2 anymore. systemd-modules-load reads /etc/modules but the
order of dwc2 load vs the otg-gadget service start window was tight — and
on the rpiz the journal showed no successful gadget enumeration after the
PR #175 image deployed.
Defensive fix in two places:
1. secubox-otg-gadget.service: prepend ExecStartPre=/sbin/modprobe dwc2.
Drop the ConditionPathIsDirectory=/sys/class/udc gate so the service
actually runs and can load dwc2 if it isn't loaded yet (the path is a
sysfs dir that exists when its parent /sys/class is mounted — it was
harmless, but removing it makes the chain self-bootstrapping).
2. secubox-otg-gadget.sh check_prerequisites(): also modprobe dwc2 (in
case the service is invoked manually by an operator), then poll
/sys/class/udc for up to 5s waiting for the BCM USB controller to
bind asynchronously. Diagnostic message updated to point at the two
places to check (dtoverlay + /etc/modules) if no UDC ever shows up.
Replaces the brittle .link rename approach with a package-owned `eye-br0` bridge so multiple Pi RNDIS gadgets coexist at L2. Verified live on 192.168.1.200 in #155 / PR thread. Ref #155.
Port the radar concentric layout from remote-ui/round/agent/display/fallback/
into a stateless painter under secubox_common.painters.radar_concentric.
Both round and square dashboards now consume it; the square's main loop
drives a monotonic-clock phase so its left half rotates the same way the
round's deployed fallback_manager does.
What landed
-----------
- `secubox_common/painters/radar_concentric.py`
- `paint(img, center, modules, metrics, radii=None, phase=0.0, ...)`
- phase × 2π = sweep angle radians, clockwise from 12 o'clock.
- Module → wheel-angle map decoupled from list order via
DEFAULT_NAME_TO_ANGLE so AUTH/WALL/ROOT/MESH/MIND/BOOT sit at the
rainbow position matching their colour regardless of MODULES order.
- DEFAULT_RADII = [214, 188, 162, 136, 110, 84] matches the deployed
fallback_manager.py geometry.
- draw_hub=False lets the converged dashboard composite its own
central button + pod cluster on top.
- `DashboardCanvas.paint_radar_concentric` — thin wrapper, single source
of truth verified by a test that asserts canvas-vs-painter byte-equal.
- `RoundDashboard.layout(metrics, phase=0.0)` and
`SquareDashboard.layout(metrics, phase=0.0)` — phase=0 preserves the
static still-frame contract for tests / callers that don't drive
animation. RING_RADII bumped to the deployed [214..84] geometry on
both classes so the converged left-half look matches the round.
- `secubox_eye_square_kiosk.__main__` drives
`phase = (time.monotonic() * RADAR_RPM / 60) % 1` at 12 RPM, matching
fallback_manager._sweep_speed.
Tests: 8 new in secubox_common/tests/test_radar_concentric.py — phase
differentiation, period-1 wrap, default-radii fallback, length-mismatch
guard, hub toggle, sweep_accent range, canvas-helper byte-equal.
Total green: 118 / 118 (36 secubox_common + 78 square kiosk + 4 round).
Out of scope for this commit (deferred)
---------------------------------------
- `fallback_manager.py` migration to the painter (#138 acceptance lists it).
fallback_manager uses its own brighter MODULES list and stateful
ConnectionState machinery; converting it requires a colour-palette
decision the user has not yet signed off on. Will land in a follow-up
fixup after hardware confirms the square radar visual is what we want.
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fixup(common): pod_size=48 to match deployed icon sizes (ref #135)
Module icons were falling back to first-letter placeholders because
SquareDashboard/RoundDashboard called paint_pod_cluster(pod_size=40)
while load_module_icon only ships sizes 22/48/96/128. Bump pod_size
to 48 and radius from 70 to 78 so the pod inner edge (54) stays clear
of the central button (44).
Confirmed on Pi 4B square hardware: pods now render with full icons.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fixup(square): ship remote-ui/common/assets/icons to /var/www/common/assets (ref #135)
The square build script shipped remote-ui/common/python/ but skipped
remote-ui/common/assets/. secubox_common.icons.load_module_icon resolves
icons at /var/www/common/assets/icons/ — when that path is missing on the
deployed image, every module pod falls back to a first-letter placeholder.
The round build script already does this at line 925; bringing square to
parity. Fails the build (exit 2) if no *-48.png icons land — same
shape as the existing python-package guard right above.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fixup(square): install secubox-otg-gadget.sh to /usr/local/sbin (ref #135)
The square build script enables secubox-otg-gadget.service but never copies
the composer script to /usr/local/sbin/. On boot the service ExecStarts a
missing file → fails silently → the Pi 4B's USB-C bus stays unconfigured →
MOCHAbin/host see no enumeration (zero dmesg events, xhci setup timeouts).
The round build copies it at build-eye-remote-image.sh:618; bringing square
to parity. The script itself lives in remote-ui/common/shell/, already
shared between the two images.
Hardware-confirmed: Pi 4B booted and rendered the dashboard but MOCHAbin
got no USB events whatsoever on either a hub-deep port or a direct port,
across cable swaps. Root cause was the missing composer, not cabling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(round): remove dead ifupdown config + secubox sudo + clarify gadget IP comment (closes#139)
Three small cleanups against the round image, all triggered by the
follow-up to PR #137's OTG bench:
1. Drop `remote-ui/round/files/etc/network/interfaces.d/usb0` — the
stanza configured `usb0` via `ifupdown`, but `ifupdown` is not in
the apt list, `networking.service` is inactive, and the actual IP
binding happens in `secubox-otg-gadget.sh` (which configures `usb1`
programmatically with `ip addr add`). Keeping the file on disk
misleads diagnostics — it cost a round trip during the 2026-05-15
bench when I assumed `usb0`/10.55.0.2 must be where the IP would
land. Deleting it.
2. Add `sudo` to the `secubox` user's group list in
`build-eye-remote-image.sh`. Without it, ACM serial console
recovery is impossible — the user can log in but can't fix
anything. The round previously needed `pi/raspberry` (which the
build script does not provision) as a fallback; now `secubox` can
recover networking from the only path that survives a broken IP
link.
3. Rewrite the misleading comment block in `secubox-otg-gadget.sh`
about which kernel interface name belongs to which gadget function.
The legacy comment claimed `usb1 = ECM` and tied the binding choice
to "Linux uses cdc_ether"; in reality both RNDIS and ECM share the
same host_addr so the host can reach 10.55.0.2 via either function
(verified on Pop!_OS 22.04 host: ping succeeded with 10.55.0.1
bound on the host's RNDIS leg). Comment now states the actual
contract: bind on `usb1` (the second-registered network function),
independent of which transport carries the packets.
Test plan
---------
- [ ] Rebuild the round image and flash to Pi Zero W 1st gen.
- [ ] Verify boot still reaches OFFLINE-mode radar on the HyperPixel.
- [ ] `id secubox` on the booted device shows `sudo` in groups.
- [ ] `ls /etc/network/interfaces.d/usb0` returns "no such file".
- [ ] `systemctl is-active secubox-otg-gadget` returns `active`.
- [ ] Host ping 10.55.0.2 succeeds (regression check — the comment
rewrite is documentation-only, must not change behaviour).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(round): also drop inline heredoc that recreates dead ifupdown stanza (ref #139)
The previous commit deleted remote-ui/round/files/etc/network/interfaces.d/usb0
but missed that build-eye-remote-image.sh writes the same file inline at
build time via heredoc. Without removing that block too, the dead config
keeps shipping to the rootfs.
Verified on the just-rebuilt image: file still present after first
attempt at the fix. With both source-tree deletion AND the heredoc gone,
the config never lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(common): seed secubox_common package skeleton (ref #135)
* fixup(common): complete SPDX header (ref #135)
Code review on T2 flagged the truncated 1-line SPDX header (project
canon is 4 lines: SPDX + Copyright + License + LICENCE.md ref). The
plan's code blocks propagated the short form; restoring the full
header now so it doesn't ripple through T3-T18+.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(common): theme.py — palette constants + DEFAULT_FONT loader (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fixup(common): remove dead ImageFont import from test_theme (ref #135)
Code review flagged: test_theme.py imported PIL.ImageFont but never
referenced it directly — fonts come back through theme.load_default_font().
F401 noise; clean up before downstream lint runs catch it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(common): modules.py — canonical Hamiltonian module table (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fixup(common): MIND extract divides load_avg by os.cpu_count() (ref #135)
Code review on T4 flagged the hardcoded /4.0 divisor: on Pi Zero W
(single-core, round/ target), load_avg=1.0 would render the MIND
arc at only 25% fill even though the CPU is fully saturated.
Reading os.cpu_count() at import time makes the formula self-adapt
per device — round/ (1 core), square/ Pi 4B/400 (4 cores) — without
any per-platform branching.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(common): icons.py — module icon loader with multi-path resolution (closes round/ ICONS_DIR bug, ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(common): canvas.py — DashboardCanvas base + paint_background (ref #135)
* feat(common): canvas — paint_rainbow_ring (HSV hue rotation) (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fixup(common): paint_rainbow_ring erase colour + dead test assert (ref #135)
Code review on T7 flagged two plan defects:
1. The inner-disc erase was hardcoded to (0,0,0,255), but both
RoundDashboard (T17) and SquareDashboard (T11) composite against
COSMOS_BLACK (8,8,8) — gaps between primitives (r≈22-50,
r≈90-125) would show a dark-ring artefact. Add a `background`
parameter defaulting to theme.COSMOS_BLACK so callers blend
correctly.
2. test_paint_rainbow_ring_pixels_in_band_are_colored had an
always-false guard `if 240 + 250 < 480` (490 < 480 = False), so
the outer-radius assertion was dead code. Use radius 238 instead
(x=478 in-bounds) and drop the guard — the outside pixel stays
at the fixture's initial (0,0,0) because rainbow_ring never
touches it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(common): canvas — paint_concentric_arcs (per-module ring fill) (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fixup(common): saturate concentric_arcs test metrics for CPU-count portability (ref #135)
T8 commit ea7c2781 left test_paint_concentric_arcs_six_rings_present
brittle: MIND's load_avg_1=4.0 only saturates the arc on a 4-core
machine. On the 20-core dev host _CPU_COUNT=20 makes the MIND ring
fill 20% (4/20), leaving the 3 o'clock sample on the dark track and
failing the colour-distance assertion. Push every metric to 999 so
each ring clamps to 100% regardless of os.cpu_count().
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fixup(common): type-annotate paint_concentric_arcs modules param (ref #135)
Code review on T8 flagged the `modules` parameter as the only
unannotated arg on `paint_concentric_arcs` — every other arg
(`img`, `center`, `metrics`, `radii`) carries an annotation, so this
one creates an inconsistent surface and gives no IDE assist. Use
`Iterable[Module]` (accurate — zip() only needs an iterable) and
import Module from secubox_common.modules. Required before T9 adds
more callers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(common): canvas — paint_pod_cluster (icon or letter on coloured disc) (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fixup(common): exercise paint_pod_cluster icon-paste path + drop dead asserts (ref #135)
Code review on T9 flagged two test-quality gaps:
1. Both T9 tests used pod_size 20 / 30, which have no matching icon
files (sizes on disk: 22, 48, 96, 128), so load_module_icon
always returned None and the icon-paste code path went untested.
Adds test_paint_pod_cluster_uses_icon_when_available at
pod_size=48 (real file present) — exercises img.paste(icon, ...)
on every commit. Calls icons._cache_clear() first to avoid stale
None cache entries from earlier tests in the session.
2. test_paint_pod_cluster_six_coloured_circles computed dr/dg/db
colour distances but never asserted on them — dead code that
misled the contract. Drop the computation; the assertion stays
the same (loose non-black check, justified because the letter
fallback may paint the centre pixel white).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(common): canvas — paint_central_button + paint_alert_ribbon (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fixup(common): paint_alert_ribbon honest fill + docstring (ref #135)
Code review on T10 flagged that the docstring claimed
"semi-transparent fill" but ImageDraw.rectangle on an RGBA image
just overwrites pixels with the raw tuple — no compositing happens.
The kiosk's framebuffer blit then calls .convert("RGB") which drops
the alpha channel entirely, so the visible result was always a
solid black strip regardless of the alpha=200 hint.
Switch fill to (0, 0, 0, 255), rewrite the docstring to describe
what the code actually does, and add an inline comment explaining
why alpha is pinned to 255 (so future authors don't try to "dim"
the ribbon by lowering it).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(square): SquareDashboard via secubox_common; drop ring_dashboard.py (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fixup(square): smoke test uses SquareDashboard, drops RingDashboard tap test (ref #135)
T11 deleted ring_dashboard.py but test_kiosk_smoke.py still imported
RingDashboard, breaking collection. Rewrite the compose-frame test to
drive SquareDashboard (which already composes the 800x480 frame
internally) and drop the on_module_tap test — that routing moves
into __main__.py at T15 and will be covered there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fixup(square): drop dead tm var, clarify paint_background omission (ref #135)
Code review on T11 flagged two housekeeping items:
1. test_kiosk_smoke.py kept TransportManager assignment from the old
pre-fixup smoke test, but the new SquareDashboard path doesn't
need it — F841 dead-var noise. Drop both the import and the
unused assignment.
2. SquareDashboard.layout() skips paint_background because Image.new()
already fills with the same colour. Add a one-line comment so
future readers don't wonder why a base-class primitive went
unused.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(square): pointer_input — mouse + touchpad via python-evdev (ref #135)
* fixup(square): pointer_input motion-event contract + log level (ref #135)
Code review on T12 flagged five items:
1. _handle_evdev_event was silent on REL/ABS but _drain_test_queue
emitted "motion" events — tests asserted motion presence which
only worked through the test path. Real Pi 4B + USB mouse would
never see motion events. Align both paths: emit one motion event
per axis update from the real path too. T15 ignores motion events,
so no downstream breakage.
2. log.warning("pointer devices found: ...") fires on every clean
boot, polluting the log. Switch to log.info for the populated
case and keep log.warning only when zero devices are found (the
genuinely unexpected condition).
3. except (OSError, PermissionError) — PermissionError is a subclass
of OSError, redundant. Drop it.
4. import os, fcntl was inside _discover_devices (PEP 8 E401 +
non-top-level). Hoist both to module top.
5. _last_motion = 0.0 default keeps the cursor hidden until first
motion — intentional but non-obvious; add a one-line comment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(square): cursor.py — 12×16 arrow sprite for pointer overlay (ref #135)
* fixup(square): cursor sprite uses theme.GOLD_HERMETIC, real test asserts (ref #135)
Code review on T13 flagged four items:
1. Off-canvas guard used hardcoded 12/16 magic numbers; refactor to
_W/_H constants so the guard stays coupled to the polygon shape.
2. _OUTLINE comment claimed GOLD_HERMETIC but duplicated the hex —
import theme.GOLD_HERMETIC so palette changes propagate.
3. test_cursor_clamped_to_image_bounds had a comment but no assert —
silent green even if draw_cursor were deleted. Add an assertion
that at least one pixel in the clipped 5×5 corner is non-black.
4. test_cursor_negative_coords_dont_crash had no assert either —
pin the invariant that the canvas is untouched on no-op return.
Also added the sys.path bootstrap line to the test file so the
secubox_common import resolves on dev hosts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(square): theme + modules_table become re-export shims for secubox_common (ref #135)
theme.py becomes a thin re-export of secubox_common.theme — keeps
`from .theme import COSMOS_BLACK` etc. working in tabs/* without
sweeping import rewrites in this PR. Also exposes DEFAULT_FONT =
load_default_font(12) at module level for legacy module-level imports.
modules_table.py becomes a re-export of secubox_common.modules. The
legacy in-package Module had `radius` and `unit` fields:
- `radius` was form-factor-specific layout (round Pi Zero W used
214..149, square uses 200..125 via SquareDashboard.RING_RADII).
Layout properly lives on the dashboard subclass now, not the
Module dataclass.
- `unit` was defined but never read in production code.
The legacy test_modules_table.py asserted the old [214,201,...] radii
plus extractor behaviour that is fully duplicated in test_modules.py
under secubox_common. Removed as part of the same commit.
Full square/ kiosk suite still 73 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(square): __main__ wires SquareDashboard + PointerInput + cursor overlay (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fixup(square): __main__ drop dead touch_input plumbing (ref #135)
T15's commit kept find_touch_devices()/read_events() in the loop with
a `pass` body — the implementer recognised it as duplicate input but
preserved it to keep the call shape. PointerInput already discovers
every device exposing BTN_LEFT or BTN_TOUCH (T12), which covers USB
mouse, USB touchpad, AND the 7" DSI touchscreen in one path. Calling
read_events on the same fds in parallel would consume events from
the shared device stream without dispatching them.
Drop the empty loop and the four unused imports (TouchEvent, classify,
find_touch_devices, read_events). Leave touch_input.py on disk — it
still has tests and might be useful for a different scenario, but
the kiosk main loop no longer uses it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(square): cross-form-factor secubox_common API stability test (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(round): RoundDashboard via secubox_common (ref #135)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(round): fb_dashboard.draw_dashboard delegates to RoundDashboard (ref #135)
* build(images): ship secubox_common to /var/www/common/python/ on both images (ref #135)
* build(systemd): PYTHONPATH=/var/www/common/python for both kiosks (ref #135)
---------
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(remote-ui/square): 4 bugs caught at Pi 4B hardware bench (closes#133, ref #127)
Phase 3 (#132) merged with 82/82 pytest green and two-stage subagent
review on every task — but the kiosk crashed at hardware boot because
the review loop had no real /dev/fb0. All four fixes validated live
on a Pi 4B + official 7" DSI panel this session by hand-patching the
uSD, then ported here.
(1) /run/secubox not recreated on reboot
Add remote-ui/square/files/etc/tmpfiles.d/secubox-eye-square.conf
creating /run/secubox + /var/log/secubox at boot under the
secubox-eye-square user. /run is tmpfs and the build-time mkdir
didn't persist, so secubox-eye-square-helper.service crashed on
Path.mkdir() at startup, which cascaded into the kiosk's
HelperClient timeout-loop.
(2) fonts-dejavu-core missing from chroot apt-install
(3) draw.text() calls lacked font= argument
Pillow on Bookworm (9.4) falls back to a latin-1 bitmap font when
no font is passed. ring_dashboard.py draws "○ NOMINAL" (U+25CB)
which raised UnicodeEncodeError. Fix on both ends: install
fonts-dejavu-core in the chroot, expose theme.DEFAULT_FONT loaded
from /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf, and pass
font=theme.DEFAULT_FONT to every draw.text() in the kiosk
(25 call sites across 6 files — confirmed by AST walk).
(4) framebuffer.py hardcoded 32bpp BGRA but vc4drmfb is 16bpp RGB565
The Pi 4B 7" DSI exposes /dev/fb0 as DRM_FORMAT_RGB565
(16bpp, R in top 5 bits). framebuffer.py wrote 800*480*4=1.5MB
of BGRA bytes into a 768KB fb. mmap silently truncated, dd ...
of=/dev/fb0 confirmed smem_len=768KB by erroring "No space left
on device" at exactly that offset. Pillow's RGB→RGB565 raw
packers were removed in Pillow >=9.4 (tested 9.4 and 10.2,
same behaviour), so we pack via numpy:
pixels = ((R & 0xF8) << 8) | ((G & 0xFC) << 3) | (B >> 3)
pixels.astype("<u2").tobytes()
Auto-detect bpp from /sys/class/graphics/<dev>/bits_per_pixel;
for 32bpp paths, FBIOGET_VSCREENINFO ioctl picks the right
Pillow raw mode (BGRA / RGBA / ARGB / ABGR). EYE_SQUARE_FB_MODE
env var overrides for diagnostics. Adds python3-numpy to the
chroot apt-install list.
Tests
- test_framebuffer.py rewritten: drops bpp= kwarg (now auto-detected),
adds tests for env override, sysfs bpp=16 detection picking RGB565,
numpy pack of pure red → 0xF800 (R in top 5 bits, LE bytes), and
pure black → 0x0000.
- New test_theme.py: palette tuple shape + DEFAULT_FONT loads +
Unicode glyph (U+25CB) rendering smoke.
- 68/68 kiosk tests green (was 61 + 7 new). Helper untouched.
Hardware validation (Task 23 / #127)
Same uSD with these patches renders correctly on Pi 4B + 7" DSI:
black background, six distinct ring colors (AUTH orange, WALL gold,
BOOT brown, MIND blue, ROOT teal, MESH blue), Unicode dot in tab
labels, frame-budget headroom (numpy pack ~5-10ms on Pi 4B).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(remote-ui/square): also auto-detect fb size, center-pad for HDMI on Pi 400 (ref #133)
Caught at Pi 400 + HDMI bench. The Pi 400 doesn't ship with the official
7" DSI panel, so /dev/fb0 is exposed at the HDMI monitor's native
resolution (e.g. 1920x1080) instead of 800x480. The kiosk hardcodes
800x480 and writes 800-wide rows; against a 1920-wide fb the writes
get sliced and tiled, producing rainbow stripes across the top.
Fix: in addition to the bpp + byte-order detection from the first
commit, read /sys/class/graphics/<dev>/virtual_size at __init__.
Mmap to actual fb size (not logical). Center-pad the kiosk's 800x480
canvas into a black canvas of the real fb size inside blit() before
encoding. Kiosk drawing code stays unchanged — same 800x480 design,
just letterboxed on larger displays.
API change
- FrameBuffer signature is now (path, logical_width=800, logical_height=480)
instead of (path, width=800, height=480). width/height still exist as
attributes but now hold the ACTUAL fb dimensions, with logical_width /
logical_height holding the kiosk's design canvas.
Tests
- New autouse fixture _isolate_sysfs monkeypatches _read_sysfs_size to
return the fallback. Otherwise tests on a host with a real /dev/fb0
would pick up the laptop's display dimensions and mismatch the tmp
fb file size.
- New tests:
* test_fb_size_detection_uses_sysfs_when_available (1920x1080 fake fb)
* test_blit_pads_into_larger_fb (red square center-painted into 1920x1080)
* test_blit_no_pad_when_fb_equals_logical (fast path)
- 71/71 kiosk tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add mode_api.py: HTTP API for manual gadget mode switching (port 8081)
- GET /api/status: Current gadget mode and functions
- POST /api/switch/<mode>: Switch to composite/network/storage/silent/hid
- GET /health: Health check for dashboard
- GET /metrics: System metrics (CPU, RAM, disk, temp, load, uptime)
- Web UI control panel with mode buttons
- Integrated into admin dashboard at /eye-remote/
- Uses port 8081 to avoid conflict with CrowdSec on 8080
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Auto-mode controller now detects current gadget mode from configfs
- Probes network connectivity BEFORE attempting mode switches
- Stays in COMPOSITE mode when network already works (no unnecessary switching)
- Display manager uses PID file locking to prevent multiple instances
- Gadget-setup.sh uses lock file and cooldown to prevent switch race conditions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- RNDIS was not responding to incoming connections
- CDC-ECM interface works correctly for bidirectional traffic
- Updated udev rules to use CDC-ECM as primary, disable RNDIS
- Fixed subnet mask from /24 to /30 for point-to-point link
- Added deploy-auto-mode.sh for deploying auto-mode system
- Added test-auto-mode.sh for testing all gadget modes
The Pi Zero W composite gadget exposes both RNDIS and CDC-ECM,
but CDC-ECM provides better compatibility with Linux hosts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add robust import handling with try/except blocks in main.py
- Create stub implementations for missing modules:
- system.py: WifiManager, BluetoothManager, DisplayController
- secubox.py: DeviceManager, FleetAggregator
- web.py: WebServer
- Add mode-specific renderers in display/renderers.py:
- DashboardRenderer: 3D cube + metric rings
- LocalRenderer: Local mode display
- FlashRenderer: Flash messages
- GatewayRenderer: Fleet overview
- RenderContext: Data container for renderers
- Update display/__init__.py with graceful fallbacks
- Imports now succeed even when optional dependencies (aiohttp) are missing
The agent can now be imported without crashing when some components
are unavailable. This allows the service to start and provide
basic functionality while degraded.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Detailed prompt for GPT/Gemini/DALL-E to generate
v2.2.1 product status infographic with hardware,
software, features, and connectivity checklists.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Detailed prompt for GPT/Gemini/DALL-E to generate
colored cyberpunk-style roadmap infographic with:
- SecuBox color palette
- Version timeline (v2.2.1 → v3.0.0)
- Feature breakdown per version
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Prompt for GPT/Gemini/Claude to create well-formatted
Eye Remote GitHub issues with proper context.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add secubox-fallback-display.service (3D cube + rainbow rings)
- Enable fallback-display instead of broken eye-agent
- Add libopenjp2-7, libtiff6 PIL dependencies
- Bump version to 2.2.1
The fallback_manager.py provides a stable working dashboard with:
- Connection states: OFFLINE/CONNECTING/ONLINE/COMMUNICATING
- Real metrics from MOCHAbin via direct API calls
- 3D rotating cube and rainbow ring visualizations
The eye-agent has import errors (relative imports, missing classes)
and needs further work to be functional.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Problem:
- hyperpixel2r-init.service had Restart=on-failure which caused
repeated restart attempts during boot
- This triggered a kernel panic in bcm2835_handle_irq due to
GPIO/DPI timing conflicts
Solution:
- Remove Restart=on-failure from hyperpixel2r-init.service
- LCD init is a one-shot operation that should not retry
- Add Before= ordering to ensure init completes before dashboard
- Configure pigpiod with -l flag (local socket only)
The fix allows the HyperPixel 2.1 Round display to initialize
correctly on Pi Zero W with Bookworm.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add secubox-metoblizer package (centralized log aggregator)
- Streamlit: double-buffer caching, power management UI, autostart
- Streamlit UI: consolidate Components into Status tab
- GitHub Actions: replace espressobin-v7 with mochabin for live USB builds
- Various UI improvements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- vr-radar.js: Full radar sweep animation for virtual round display
- True hexagonal icon layout (60° spacing)
- Rainbow colored rings (red→orange→yellow→green→blue→violet)
- Rainbow radar sweep line with blue trail
- Colored circles around each icon matching module color
- Icons illuminate when radar passes
- Real-time metrics from hub API
- vr-styles.css: Pod highlighting styles for radar animation
Matches the real HyperPixel 2.1 Round gadget display exactly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add connections, peak_connections, connections_percent to /api/v1/system/metrics
- Track current TCP connections via /proc/net/tcp and /proc/net/tcp6
- Persist peak_connections to file for resilience across restarts
- Pre-calculate connections_percent (current/peak * 100) for MIND ring
- Update README.md and WIKI.md with new API response documentation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Pi Zero composite gadget creates both RNDIS (usb0) and CDC-ECM (usb1)
interfaces with same MAC prefix. This caused duplicate routes for
10.55.0.0/24 network, breaking connectivity to Eye Remote.
Fix:
- RNDIS (usb0): Configure with IP 10.55.0.1/24, bring up
- CDC-ECM (usb1): Disable (ip link down) to prevent route conflict
- Add serial console symlink /dev/eye-remote-console
Ref #52
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Added /api/v1/system/metrics endpoint to eye-remote API
- Returns host metrics (CPU, mem, disk, load) for remote-ui dashboard
- Pi Zero can now seamlessly fetch MOCHAbin metrics
- Reverted secubox_client.py to use original /api/v1/system/metrics path
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
VHost Matrix Sync (scripts/vhost-matrix-sync.sh):
- Extracts vhost→backend mappings from HAProxy config
- Syncs to mitmproxy routes.json (using 10.100.0.1 for LXC bridge)
- Syncs to health prober vhost-matrix.json
- Commands: sync, status, reload
Eye Remote Round UI fix:
- Changed METRICS_ENDPOINT from /api/v1/system/metrics
to /api/v1/eye-remote/metrics/public
- Pi Zero will now fetch MOCHAbin host metrics, not local metrics
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Root causes identified and fixed:
- "martian source" packet drops due to rp_filter=1 on USB interfaces
- Dual interface conflict: usb0 (RNDIS) and usb1 (CDC-ECM) both getting
same IP 10.55.0.2, causing routing loops on Pi Zero
MOCHAbin fixes:
- Add sysctl.d/99-secubox-usb.conf to disable rp_filter globally
- Update udev rules to disable rp_filter per-interface on add
- Add bind/change events for USB re-plug detection
- Update secubox-eye-network.sh with find_interface() and rp_filter disable
Pi Zero fixes:
- Add usb1-disable config to install_zerow.sh to prevent routing conflict
Tested: Ping/TCP connectivity restored, metrics flowing via API.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add /api/v1/eye-remote/auto-pair POST endpoint for device pairing
- Add /api/v1/eye-remote/paired-devices GET endpoint for listing devices
- Add /api/v1/eye-remote/pizero/metrics public endpoint for dashboard
- Integrate pizero-metrics-api into install_zerow.sh builder (v1.9.0)
- Metrics API auto-installed on new Pi Zero SD cards
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
VMs with multiple interfaces (NAT + host-only) now correctly configure
all interfaces with DHCP instead of creating a br-lan bridge that
conflicts with VBox's host-only DHCP server.
Changes:
- Add dedicated x64-vm case in get_interface_config()
- Set profile="vm" for VMs to trigger single mode (no bridge)
- In generate_netplan(), detect x64-vm board and configure ALL
physical interfaces with DHCP instead of WAN-only
- VM profile forces mode="single" in main() to skip bridge creation
This fixes host-only network access for VirtualBox VMs while
preserving router/bridge functionality for baremetal deployments.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace hard drive icon with classic 3.5" floppy disk icon for the
DISK metric on Eye Remote display. More recognizable and nostalgic.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Compact all metric symbols to fit within the 30px targeting glow:
- CPU: 20px chip with shorter pins
- MEM: 36x16px RAM stick
- DISK: 24px disk with spindle
- LOAD: 28px gauge arc
- TEMP: 30px thermometer
- WiFi: 22px signal arcs
Symbols only appear when ONLINE and radar targets module.
Offline mode (local metrics) unchanged - shows module icons.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When the radar sweep targets a module icon, temporarily replace
the module icon with a metric symbol representing its function:
- AUTH → CPU chip with pins
- WALL → RAM stick with chips
- BOOT → Hard drive cylinder
- MIND → Gauge with dynamic needle
- ROOT → Thermometer with level
- MESH → WiFi signal arcs
Symbols are drawn using PIL primitives for crisp rendering.
Original module icon returns when radar moves away.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Clean up display by removing module label/value text overlays.
Only icons and rings are shown now for a cleaner look.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Switch from HTTP:8000 to HTTPS:443 (nginx reverse proxy)
- Add SSL context for self-signed certificate bypass
- Handle ESPRESSObin field names (memory_used_percent, disk_used_percent)
- Add synchronous initial fetch on OFFLINE→ONLINE transition
- Fix race condition: don't overwrite API values with local metrics
- Add /api/v1/eye-remote/api/system/info as primary endpoint
- Add ESPRESSObin main network IP (192.168.255.250) as fallback
- Network: Pi Zero W (10.55.0.2) ← OTG USB → ESPRESSObin (10.55.0.1)
This fixes the issue where Pi Zero W display showed local metrics
instead of remote ESPRESSObin metrics when connected via OTG.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The 'validate' field name in ImportRequest shadows Pydantic's BaseModel
validate method, causing NameError on Pydantic 1.x. Renamed to 'do_validate'.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Eye Remote Interactive UI enhancements:
- TTY mode: Serial terminal display from /dev/ttyGS0
- Flash mode: Progress bar with speed/ETA for USB transfers
- Auth mode: QR code generation for backup authentication
- Mode detection via /etc/secubox/gadget-mode
Hub service VM compatibility fix:
- Changed from Unix socket to TCP port 8001
- Updated nginx configs for TCP proxy
- Fixes 502 errors in VirtualBox VMs
Also includes:
- FAQ/Troubleshooting wiki page with GitHub issue links
- Kiosk launcher --no-sandbox fix for VMs
- Profile Generator GUI mockup
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add CORS headers to nginx secubox-proxy.conf for cross-origin API requests
- Fix login.html endpoints: /auth/login -> /login
- Upgrade Python deps in build scripts: pydantic>=2.0, fastapi>=0.100, uvicorn>=0.25
- Add pip upgrade in secubox-core postinst for Debian bookworm compatibility
- Fix display/__init__.py to import existing modules only
Fixes authentication and API issues in VBox and ebin builds.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Each colored arc now points toward its corresponding icon
- Fixed green/purple and red/yellow position mapping
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>