mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-06-30 14:10:33 +00:00
Compare commits
37 Commits
25e1e89ed1
...
ec1a4641e0
| Author | SHA1 | Date | |
|---|---|---|---|
| ec1a4641e0 | |||
| 6d12af8624 | |||
| 8996847745 | |||
| f4acd5a9ca | |||
| 387fabb4f3 | |||
| 5546169971 | |||
| c9539f8ba6 | |||
| b57891941b | |||
| 39769c318b | |||
| eeb455b673 | |||
| 4aa36f104e | |||
| 626e68904f | |||
| 4e03649954 | |||
| d2ec163912 | |||
| c214c946c2 | |||
| 50a687654c | |||
| 75991ec5b0 | |||
| 238304f351 | |||
| 4c176f8be2 | |||
| 153023b390 | |||
| 26e7f45285 | |||
| b15497d668 | |||
| 8922b2f59e | |||
| f3fba94bc4 | |||
| f1e2375e57 | |||
| 1cc3f8ee8e | |||
| ea7c278126 | |||
| 43a994fde0 | |||
| 74c5076b60 | |||
| c31c61bf3b | |||
| 8809104760 | |||
| 50e4c05a67 | |||
| 33fb2a20b0 | |||
| 0f8498a37a | |||
| 847af3e563 | |||
| 7b0f5306f5 | |||
| f552411f96 |
|
|
@ -28,6 +28,8 @@ HELPER_SOCK = os.environ.get(
|
|||
TARGET_FPS = 30
|
||||
PROBE_INTERVAL_S = 30
|
||||
METRICS_INTERVAL_S = 2
|
||||
# Radar sweep rotation speed (matches the deployed round fallback radar).
|
||||
RADAR_RPM = 12.0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
|
@ -89,8 +91,10 @@ def main() -> int:
|
|||
if ev.kind == "tap":
|
||||
_dispatch_tap(ev.x, ev.y, panel, dashboard)
|
||||
|
||||
# Render.
|
||||
full = dashboard.layout(metrics)
|
||||
# Render. phase advances the radar sweep angle — monotonic
|
||||
# so frame-to-frame motion is smooth across system clock jumps.
|
||||
phase = (time.monotonic() * RADAR_RPM / 60.0) % 1.0
|
||||
full = dashboard.layout(metrics, phase=phase)
|
||||
if pointer.cursor_visible:
|
||||
draw_cursor(full, *pointer.cursor_xy)
|
||||
fb.blit(full)
|
||||
|
|
|
|||
|
|
@ -22,27 +22,34 @@ class SquareDashboard(DashboardCanvas):
|
|||
DASHBOARD_REGION_SIZE = (480, 480)
|
||||
PANEL_REGION_SIZE = (320, 480)
|
||||
CENTER = (240, 240)
|
||||
RING_RADII = [200, 185, 170, 155, 140, 125]
|
||||
# Same radii as RoundDashboard so the left half is visually identical
|
||||
# to the deployed Pi Zero W radar.
|
||||
RING_RADII = [214, 188, 162, 136, 110, 84]
|
||||
|
||||
def __init__(self, right_panel):
|
||||
self.right_panel = right_panel
|
||||
|
||||
def layout(self, metrics: dict) -> Image.Image:
|
||||
# Image.new() with COSMOS_BLACK+(255,) is equivalent to calling
|
||||
# paint_background on a fresh canvas; skip the redundant fill.
|
||||
def layout(self, metrics: dict, phase: float = 0.0) -> Image.Image:
|
||||
"""Render one frame at animation `phase` (0..1).
|
||||
|
||||
Phase rotates the radar sweep on the left half; the right panel
|
||||
is static. Pass `phase=0.0` for a still frame.
|
||||
"""
|
||||
img = Image.new("RGBA", self.SIZE, theme.COSMOS_BLACK + (255,))
|
||||
|
||||
# Left dashboard region.
|
||||
# Left dashboard region — phase-aware radar.
|
||||
dash = Image.new("RGBA", self.DASHBOARD_REGION_SIZE,
|
||||
theme.COSMOS_BLACK + (255,))
|
||||
self.paint_rainbow_ring(dash, self.CENTER, 235, 220)
|
||||
self.paint_concentric_arcs(dash, self.CENTER, MODULES, metrics,
|
||||
self.RING_RADII)
|
||||
self.paint_pod_cluster(dash, MODULES, self.CENTER, radius=70, pod_size=40)
|
||||
self.paint_radar_concentric(
|
||||
dash, self.CENTER, MODULES, metrics,
|
||||
radii=self.RING_RADII, phase=phase, draw_hub=True,
|
||||
)
|
||||
# pod_size=48 matches deployed icon sizes (22/48/96/128).
|
||||
self.paint_pod_cluster(dash, MODULES, self.CENTER, radius=78, pod_size=48)
|
||||
self.paint_central_button(dash, self.CENTER, size=44)
|
||||
img.paste(dash, (0, 0))
|
||||
|
||||
# Right panel.
|
||||
# Right panel (static).
|
||||
panel = Image.new("RGBA", self.PANEL_REGION_SIZE,
|
||||
theme.COSMOS_BLACK + (255,))
|
||||
self.right_panel.draw(panel)
|
||||
|
|
|
|||
|
|
@ -146,6 +146,25 @@ class DashboardCanvas:
|
|||
draw.text((cx - lw // 2, cy + size + 4),
|
||||
label, fill=theme.TEXT_PRIMARY + (255,), font=font)
|
||||
|
||||
def paint_radar_concentric(self, img: Image.Image,
|
||||
center: tuple[int, int],
|
||||
modules: Iterable[Module],
|
||||
metrics: dict,
|
||||
radii: list[int] | None = None,
|
||||
phase: float = 0.0,
|
||||
draw_hub: bool = True,
|
||||
name_to_angle: dict[str, int] | None = None
|
||||
) -> None:
|
||||
"""Phase-aware concentric radar (ring backgrounds + tube arcs +
|
||||
rotating sweep + sweep head + optional hub). Delegates to
|
||||
`secubox_common.painters.radar_concentric.paint`. Drives the
|
||||
OFFLINE-state visual on both round/ and the square's left half.
|
||||
"""
|
||||
from .painters import radar_concentric as _radar
|
||||
_radar.paint(img, center, modules, metrics,
|
||||
radii=radii, phase=phase, draw_hub=draw_hub,
|
||||
name_to_angle=name_to_angle)
|
||||
|
||||
def paint_alert_ribbon(self, img: Image.Image, region_y: int,
|
||||
text: str, severity: str) -> None:
|
||||
"""Bottom strip: solid dark fill + coloured severity text.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
# Source-Disclosed License — All rights reserved except as expressly granted.
|
||||
# See LICENCE-CMSD-1.0.md for terms.
|
||||
"""Phase-aware painter modules for animated dashboard variants."""
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
# Source-Disclosed License — All rights reserved except as expressly granted.
|
||||
# See LICENCE-CMSD-1.0.md for terms.
|
||||
"""Concentric radar painter — port of fallback_manager._draw_offline_radar().
|
||||
|
||||
Stateless: given a `phase` in [0, 1) it draws the full radar frame
|
||||
(ring backgrounds, balanced tube arcs, rotating segment-coloured sweep,
|
||||
sweep head, centre hub) onto `img` centred on `center`.
|
||||
|
||||
`phase * 2π` is the sweep angle in radians, measured clockwise from
|
||||
12 o'clock. Caller is responsible for advancing phase across frames
|
||||
(typically `(time.monotonic() * rpm / 60) % 1`).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import colorsys
|
||||
import math
|
||||
from collections.abc import Iterable
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from ..modules import Module
|
||||
|
||||
# Default radii match the deployed fallback_manager.py layout (6 rings).
|
||||
DEFAULT_RADII: list[int] = [214, 188, 162, 136, 110, 84]
|
||||
|
||||
# Stroke width for ring backgrounds + tube arcs.
|
||||
RING_WIDTH: int = 20
|
||||
|
||||
# Inner hub radius — masks the centre so it can host icons / metrics text.
|
||||
HUB_RADIUS: int = 85
|
||||
|
||||
# Mapping module NAME → PIL arc centre angle so each tube arc sits at the
|
||||
# position on the wheel that matches its colour. PIL convention: 0=3 o'clock,
|
||||
# angles increase counter-clockwise. Decoupling angle from list order means
|
||||
# the painter is invariant to MODULES iteration order — callers can pass
|
||||
# secubox_common.MODULES (AUTH-first) or fallback_manager's local list
|
||||
# (BOOT-first) and get the same wheel layout.
|
||||
DEFAULT_NAME_TO_ANGLE: dict[str, int] = {
|
||||
"AUTH": 0, # red-orange (3 o'clock)
|
||||
"WALL": 60, # orange-yellow
|
||||
"ROOT": 120, # green
|
||||
"MESH": 180, # blue
|
||||
"MIND": 240, # purple
|
||||
"BOOT": 300, # deep red — closes the wheel between purple and red
|
||||
}
|
||||
|
||||
|
||||
def paint(
|
||||
img: Image.Image,
|
||||
center: tuple[int, int],
|
||||
modules: Iterable[Module],
|
||||
metrics: dict,
|
||||
radii: list[int] | None = None,
|
||||
phase: float = 0.0,
|
||||
ring_width: int = RING_WIDTH,
|
||||
hub_radius: int = HUB_RADIUS,
|
||||
draw_hub: bool = True,
|
||||
name_to_angle: dict[str, int] | None = None,
|
||||
) -> None:
|
||||
"""Paint one radar frame at `phase`.
|
||||
|
||||
Args:
|
||||
img: target RGBA image; must be large enough that
|
||||
`radii[0] + 8` fits within ``min(img.size) / 2``.
|
||||
center: (cx, cy) — the radar centre in image coordinates.
|
||||
modules: ordered iterable of Module — drawn outermost-first.
|
||||
metrics: dict passed to `Module.extract` for each ring's fill ratio.
|
||||
radii: per-ring radius, outermost-first. Default matches the
|
||||
deployed Pi Zero W fallback radar.
|
||||
phase: 0..1 maps to 0..2π sweep angle (clockwise from 12 o'clock).
|
||||
ring_width: stroke width for ring backgrounds and tube arcs.
|
||||
hub_radius: radius of the dark centre disc.
|
||||
draw_hub: when False, callers can composite their own centre
|
||||
(e.g. the converged dashboard's central button + pod cluster).
|
||||
"""
|
||||
if radii is None:
|
||||
radii = DEFAULT_RADII
|
||||
if name_to_angle is None:
|
||||
name_to_angle = DEFAULT_NAME_TO_ANGLE
|
||||
modules = list(modules)
|
||||
if len(modules) != len(radii):
|
||||
raise ValueError(
|
||||
f"modules ({len(modules)}) and radii ({len(radii)}) length mismatch"
|
||||
)
|
||||
|
||||
draw = ImageDraw.Draw(img)
|
||||
cx, cy = center
|
||||
sweep_rad = (phase % 1.0) * 2.0 * math.pi
|
||||
|
||||
_paint_ring_backgrounds(draw, cx, cy, radii, ring_width)
|
||||
_paint_tube_arcs(draw, cx, cy, modules, metrics, radii, ring_width,
|
||||
name_to_angle)
|
||||
_paint_sweep(draw, cx, cy, modules, metrics, radii, sweep_rad)
|
||||
_paint_sweep_head(draw, cx, cy, modules[0].colour, radii[0], sweep_rad)
|
||||
if draw_hub:
|
||||
draw.ellipse(
|
||||
(cx - hub_radius, cy - hub_radius, cx + hub_radius, cy + hub_radius),
|
||||
fill=(12, 12, 22, 255),
|
||||
)
|
||||
|
||||
|
||||
def _paint_ring_backgrounds(
|
||||
draw: ImageDraw.ImageDraw,
|
||||
cx: int,
|
||||
cy: int,
|
||||
radii: list[int],
|
||||
ring_width: int,
|
||||
) -> None:
|
||||
for r in radii:
|
||||
draw.ellipse(
|
||||
(cx - r, cy - r, cx + r, cy + r),
|
||||
outline=(20, 20, 28, 255),
|
||||
width=ring_width,
|
||||
)
|
||||
|
||||
|
||||
def _paint_tube_arcs(
|
||||
draw: ImageDraw.ImageDraw,
|
||||
cx: int,
|
||||
cy: int,
|
||||
modules: list[Module],
|
||||
metrics: dict,
|
||||
radii: list[int],
|
||||
ring_width: int,
|
||||
name_to_angle: dict[str, int],
|
||||
) -> None:
|
||||
# PIL arc degrees: 0=3 o'clock, counter-clockwise. Each module's arc
|
||||
# sits at the wheel position matching its colour, looked up by name
|
||||
# — independent of caller's iteration order.
|
||||
for m, r in zip(modules, radii):
|
||||
pct = m.extract(metrics)
|
||||
if pct <= 0.0:
|
||||
continue
|
||||
# Fall back to evenly-spaced positions if name unknown — keeps the
|
||||
# painter robust against custom module sets without bringing in a
|
||||
# hard dependency on the canonical six.
|
||||
center_deg = name_to_angle.get(m.name, 0)
|
||||
half = (pct * 360.0) / 2.0
|
||||
start = center_deg + half
|
||||
end = center_deg - half
|
||||
|
||||
# Outer dark edge (color × 1/3).
|
||||
dark = (m.colour[0] // 3, m.colour[1] // 3, m.colour[2] // 3, 255)
|
||||
draw.arc(
|
||||
(cx - r - 2, cy - r - 2, cx + r + 2, cy + r + 2),
|
||||
end, start, fill=dark, width=ring_width - 2,
|
||||
)
|
||||
# Main color band.
|
||||
draw.arc(
|
||||
(cx - r, cy - r, cx + r, cy + r),
|
||||
end, start, fill=m.colour + (255,), width=ring_width - 6,
|
||||
)
|
||||
# Inner light tube highlight.
|
||||
light = (
|
||||
min(255, m.colour[0] + 80),
|
||||
min(255, m.colour[1] + 80),
|
||||
min(255, m.colour[2] + 80),
|
||||
255,
|
||||
)
|
||||
draw.arc(
|
||||
(cx - r + 2, cy - r + 2, cx + r - 2, cy + r - 2),
|
||||
end, start, fill=light, width=4,
|
||||
)
|
||||
|
||||
|
||||
def _paint_sweep(
|
||||
draw: ImageDraw.ImageDraw,
|
||||
cx: int,
|
||||
cy: int,
|
||||
modules: list[Module],
|
||||
metrics: dict,
|
||||
radii: list[int],
|
||||
sweep_rad: float,
|
||||
) -> None:
|
||||
# Per-ring sweep segments coloured by that ring's metric.
|
||||
max_r = radii[0] + 8
|
||||
min_r = radii[-1] - 8
|
||||
|
||||
for idx, m in enumerate(modules):
|
||||
r = radii[idx]
|
||||
# Segment bounds — each ring owns the slab between its midpoint
|
||||
# to the previous ring and its midpoint to the next ring.
|
||||
r_outer = max_r if idx == 0 else (radii[idx - 1] + r) // 2
|
||||
r_inner = min_r if idx == len(modules) - 1 else (r + radii[idx + 1]) // 2
|
||||
|
||||
pct = m.extract(metrics)
|
||||
intensity = 0.5 + pct * 0.5
|
||||
|
||||
# Fading trail behind the sweep line.
|
||||
for i in range(15):
|
||||
offset = -0.15 * (i / 15.0)
|
||||
a = sweep_rad + offset
|
||||
fade = 1.0 - (i / 15.0)
|
||||
x1 = cx + r_inner * math.sin(a)
|
||||
y1 = cy - r_inner * math.cos(a)
|
||||
x2 = cx + r_outer * math.sin(a)
|
||||
y2 = cy - r_outer * math.cos(a)
|
||||
seg = (
|
||||
int(m.colour[0] * fade * intensity),
|
||||
int(m.colour[1] * fade * intensity),
|
||||
int(m.colour[2] * fade * intensity),
|
||||
255,
|
||||
)
|
||||
draw.line([(x1, y1), (x2, y2)], fill=seg, width=2)
|
||||
|
||||
# Main bright leading edge for this ring.
|
||||
x1 = cx + r_inner * math.sin(sweep_rad)
|
||||
y1 = cy - r_inner * math.cos(sweep_rad)
|
||||
x2 = cx + r_outer * math.sin(sweep_rad)
|
||||
y2 = cy - r_outer * math.cos(sweep_rad)
|
||||
bright = (
|
||||
min(255, m.colour[0] + 60),
|
||||
min(255, m.colour[1] + 60),
|
||||
min(255, m.colour[2] + 60),
|
||||
255,
|
||||
)
|
||||
draw.line([(x1, y1), (x2, y2)], fill=bright, width=3)
|
||||
|
||||
|
||||
def _paint_sweep_head(
|
||||
draw: ImageDraw.ImageDraw,
|
||||
cx: int,
|
||||
cy: int,
|
||||
head_color: tuple[int, int, int],
|
||||
outer_r: int,
|
||||
sweep_rad: float,
|
||||
) -> None:
|
||||
hx = cx + outer_r * math.sin(sweep_rad)
|
||||
hy = cy - outer_r * math.cos(sweep_rad)
|
||||
draw.ellipse(
|
||||
(hx - 4, hy - 4, hx + 4, hy + 4),
|
||||
fill=head_color + (255,),
|
||||
)
|
||||
|
||||
|
||||
# Convenience for callers that want a quick HSV-rotating accent colour
|
||||
# matching the current sweep position (used by the original radar variant
|
||||
# but kept here for downstream painters that want it).
|
||||
def sweep_accent(phase: float) -> tuple[int, int, int]:
|
||||
hue = phase % 1.0
|
||||
r, g, b = colorsys.hsv_to_rgb(hue, 0.9, 0.8)
|
||||
return (int(r * 255), int(g * 255), int(b * 255))
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
|
||||
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
|
||||
# Source-Disclosed License — All rights reserved except as expressly granted.
|
||||
# See LICENCE-CMSD-1.0.md for terms.
|
||||
"""Tests for secubox_common.painters.radar_concentric."""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from secubox_common.modules import MODULES
|
||||
from secubox_common.painters import radar_concentric
|
||||
|
||||
|
||||
METRICS = {
|
||||
"cpu_percent": 60.0,
|
||||
"mem_percent": 45.0,
|
||||
"disk_percent": 30.0,
|
||||
"load_avg_1": 0.8,
|
||||
"cpu_temp": 55.0,
|
||||
"wifi_rssi": -55,
|
||||
}
|
||||
|
||||
|
||||
def _frame() -> Image.Image:
|
||||
return Image.new("RGBA", (480, 480), (0, 0, 0, 255))
|
||||
|
||||
|
||||
def test_paint_modifies_canvas():
|
||||
"""Painting at any phase must change the canvas from solid black."""
|
||||
img = _frame()
|
||||
radar_concentric.paint(img, (240, 240), MODULES, METRICS, phase=0.0)
|
||||
# At least one pixel outside the inner hub must have changed.
|
||||
sample = img.getpixel((240, 30)) # near outer ring
|
||||
assert sample != (0, 0, 0, 255), \
|
||||
"outer ring area should have radar pixels painted"
|
||||
|
||||
|
||||
def test_paint_differs_across_phases():
|
||||
"""phase=0.0 and phase=0.5 must produce different sweep positions."""
|
||||
a = _frame()
|
||||
b = _frame()
|
||||
radar_concentric.paint(a, (240, 240), MODULES, METRICS, phase=0.0)
|
||||
radar_concentric.paint(b, (240, 240), MODULES, METRICS, phase=0.5)
|
||||
assert a.tobytes() != b.tobytes(), \
|
||||
"frames at phase 0.0 vs 0.5 should differ (sweep rotated 180°)"
|
||||
|
||||
|
||||
def test_paint_wraps_at_phase_one():
|
||||
"""phase=0.0 and phase=1.0 produce the same frame (period = 1)."""
|
||||
a = _frame()
|
||||
b = _frame()
|
||||
radar_concentric.paint(a, (240, 240), MODULES, METRICS, phase=0.0)
|
||||
radar_concentric.paint(b, (240, 240), MODULES, METRICS, phase=1.0)
|
||||
assert a.tobytes() == b.tobytes(), \
|
||||
"phase=1.0 must wrap back to phase=0.0"
|
||||
|
||||
|
||||
def test_paint_uses_default_radii_when_none():
|
||||
"""radii=None must fall back to DEFAULT_RADII (6 entries)."""
|
||||
img = _frame()
|
||||
# 6 modules + 6 default radii must match.
|
||||
radar_concentric.paint(img, (240, 240), MODULES, METRICS, radii=None)
|
||||
sample = img.getpixel((240, 30))
|
||||
assert sample != (0, 0, 0, 255)
|
||||
|
||||
|
||||
def test_paint_rejects_mismatched_radii():
|
||||
"""A radii list of wrong length must raise."""
|
||||
img = _frame()
|
||||
with pytest.raises(ValueError, match="length mismatch"):
|
||||
radar_concentric.paint(
|
||||
img, (240, 240), MODULES, METRICS, radii=[200, 180, 160]
|
||||
)
|
||||
|
||||
|
||||
def test_paint_hub_toggle():
|
||||
"""draw_hub=False must leave the centre area unfilled by the painter
|
||||
(so callers can composite their own central button + icons)."""
|
||||
a = _frame()
|
||||
b = _frame()
|
||||
radar_concentric.paint(a, (240, 240), MODULES, METRICS,
|
||||
phase=0.0, draw_hub=True)
|
||||
radar_concentric.paint(b, (240, 240), MODULES, METRICS,
|
||||
phase=0.0, draw_hub=False)
|
||||
# Sample a point well inside the hub (radius ~85). With draw_hub=True
|
||||
# the colour is the hub fill (12, 12, 22, 255). With False it remains
|
||||
# the canvas background.
|
||||
assert a.getpixel((240, 240)) == (12, 12, 22, 255)
|
||||
assert b.getpixel((240, 240)) == (0, 0, 0, 255)
|
||||
|
||||
|
||||
def test_sweep_accent_in_rgb_range():
|
||||
"""sweep_accent returns valid RGB tuples for any phase."""
|
||||
for p in [0.0, 0.25, 0.5, 0.75, 0.999]:
|
||||
r, g, b = radar_concentric.sweep_accent(p)
|
||||
for chan in (r, g, b):
|
||||
assert 0 <= chan <= 255
|
||||
|
||||
|
||||
def test_canvas_method_delegates():
|
||||
"""DashboardCanvas.paint_radar_concentric must produce the same frame
|
||||
as calling the painter directly — proves the canvas helper is a
|
||||
thin wrapper, not divergent code."""
|
||||
from secubox_common.canvas import DashboardCanvas
|
||||
|
||||
direct = _frame()
|
||||
via_canvas = _frame()
|
||||
radar_concentric.paint(direct, (240, 240), MODULES, METRICS, phase=0.25)
|
||||
DashboardCanvas().paint_radar_concentric(
|
||||
via_canvas, (240, 240), MODULES, METRICS, phase=0.25,
|
||||
)
|
||||
assert direct.tobytes() == via_canvas.tobytes(), \
|
||||
"canvas helper should delegate verbatim to painters.radar_concentric.paint"
|
||||
|
|
@ -15,22 +15,32 @@ from secubox_common.modules import MODULES
|
|||
class RoundDashboard(DashboardCanvas):
|
||||
SIZE = (480, 480)
|
||||
CENTER = (240, 240)
|
||||
RING_RADII = [200, 185, 170, 155, 140, 125]
|
||||
# Match the deployed fallback_manager.py radar geometry so the round
|
||||
# rendering stays visually identical after migration.
|
||||
RING_RADII = [214, 188, 162, 136, 110, 84]
|
||||
|
||||
def layout(self, metrics: dict) -> Image.Image:
|
||||
def layout(self, metrics: dict, phase: float = 0.0) -> Image.Image:
|
||||
"""Render one frame at the given animation `phase` (0..1).
|
||||
|
||||
Phase advances the rotating sweep; callers pass `phase=0.0` for
|
||||
a static still frame, or `(time.monotonic() * rpm / 60) % 1` for
|
||||
an animated loop.
|
||||
"""
|
||||
img = Image.new("RGBA", self.SIZE, theme.COSMOS_BLACK + (255,))
|
||||
self.paint_rainbow_ring(img, self.CENTER, 235, 220)
|
||||
self.paint_concentric_arcs(img, self.CENTER, MODULES, metrics,
|
||||
self.RING_RADII)
|
||||
self.paint_pod_cluster(img, MODULES, self.CENTER, radius=70, pod_size=40)
|
||||
self.paint_radar_concentric(
|
||||
img, self.CENTER, MODULES, metrics,
|
||||
radii=self.RING_RADII, phase=phase, draw_hub=True,
|
||||
)
|
||||
# Pods sit on top of the hub; pod_size=48 matches deployed icon
|
||||
# sizes (22/48/96/128). radius=78 keeps the pod inner edge (54)
|
||||
# clear of the central button (44).
|
||||
self.paint_pod_cluster(img, MODULES, self.CENTER, radius=78, pod_size=48)
|
||||
self.paint_central_button(img, self.CENTER, size=44)
|
||||
return img
|
||||
|
||||
# Round-only additional view modes (called by fb_dashboard.py's main
|
||||
# loop when the user long-presses center → radial menu → terminal/flash/auth).
|
||||
def layout_terminal(self, term_state) -> Image.Image:
|
||||
# Delegates to the existing draw_terminal() helper for now;
|
||||
# extracted into a method to give the main loop a class-based API.
|
||||
from fb_dashboard import draw_terminal
|
||||
return draw_terminal(term_state)
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,17 @@ log "Installing config files (systemd, udev, apparmor, firstboot)..."
|
|||
cp -r "$REPO_ROOT/remote-ui/square/files/." "$ROOT_MNT/"
|
||||
chmod +x "$ROOT_MNT/usr/local/sbin/firstboot.sh"
|
||||
|
||||
# Install the shared OTG gadget composer (round does this at line 618 of
|
||||
# build-eye-remote-image.sh). secubox-otg-gadget.service ExecStarts this path;
|
||||
# without it the gadget never composes and the Pi 4B's USB-C bus stays silent
|
||||
# → MOCHAbin/host enumeration fails (no descriptor events, xhci timeouts).
|
||||
log "Installing OTG gadget composer at /usr/local/sbin/secubox-otg-gadget.sh..."
|
||||
cp "$REPO_ROOT/remote-ui/common/shell/secubox-otg-gadget.sh" \
|
||||
"$ROOT_MNT/usr/local/sbin/secubox-otg-gadget.sh"
|
||||
chmod +x "$ROOT_MNT/usr/local/sbin/secubox-otg-gadget.sh"
|
||||
test -x "$ROOT_MNT/usr/local/sbin/secubox-otg-gadget.sh" || \
|
||||
{ err "secubox-otg-gadget.sh not executable on rootfs"; exit 2; }
|
||||
|
||||
# Ship the shared secubox_common package.
|
||||
log "Embedding remote-ui/common/python at /var/www/common/python/..."
|
||||
mkdir -p "$ROOT_MNT/var/www/common/python"
|
||||
|
|
@ -110,6 +121,17 @@ cp -r "$REPO_ROOT/remote-ui/common/python/." "$ROOT_MNT/var/www/common/python/"
|
|||
test -d "$ROOT_MNT/var/www/common/python/secubox_common" || \
|
||||
{ err "secubox_common not in /var/www/common/python — common/ source incomplete"; exit 2; }
|
||||
|
||||
# Ship the shared icon assets — secubox_common.icons.load_module_icon
|
||||
# resolves at /var/www/common/assets/icons/ first. Without these the pod
|
||||
# cluster falls back to first-letter placeholders.
|
||||
log "Embedding remote-ui/common/assets at /var/www/common/assets/..."
|
||||
mkdir -p "$ROOT_MNT/var/www/common/assets"
|
||||
cp -r "$REPO_ROOT/remote-ui/common/assets/." "$ROOT_MNT/var/www/common/assets/"
|
||||
ICON_COUNT=$(ls "$ROOT_MNT/var/www/common/assets/icons"/*-48.png 2>/dev/null | wc -l)
|
||||
test "$ICON_COUNT" -gt 0 || \
|
||||
{ err "no *-48.png icons in /var/www/common/assets/icons — common/ assets incomplete"; exit 2; }
|
||||
log " → $ICON_COUNT module icons at size 48 shipped"
|
||||
|
||||
log "Installing Python packages..."
|
||||
mkdir -p "$ROOT_MNT/usr/lib/python3/dist-packages"
|
||||
cp -r "$REPO_ROOT/packages/secubox-eye-square/helper/eye_square_helper" \
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user