Make the matrix frame rate configurable

24 fps costs about 11% of one core on a Zero 2 W, measured on the board. That
is a reasonable default for something you look at, but it is a decorative load
sharing a 512 MB machine with the forecaster, so it should be the owner's
choice. server.led_fps is clamped to 4..30.

Particle fall speed now divides by the configured rate rather than the module
constant, so rain falls at the same real-world speed whatever the frame rate,
instead of slowing down when you turn the frame rate down.
This commit is contained in:
2026-08-15 23:04:05 +01:00
parent 30944c2978
commit e05667d75b
3 changed files with 11 additions and 4 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ async def lifespan(app: FastAPI):
station.sample_once()
station.start()
if CONFIG.server.led_enabled:
display = LedDisplay(station, CONFIG.server.led_cycle_s)
display = LedDisplay(station, CONFIG.server.led_cycle_s, CONFIG.server.led_fps)
display.start()
try:
yield
+5
View File
@@ -118,6 +118,11 @@ class ServerConfig:
port: int = 8000
led_enabled: bool = True
led_cycle_s: float = 0.4
# Matrix frame rate. 24 is smooth and costs about 11% of one core on a
# Zero 2 W. 16 is still fluid and roughly a third cheaper; below about 12
# the crossfades and sub-pixel motion start to judder, which defeats the
# point. Set 0 to keep the panel enabled but static-cheap.
led_fps: float = 24.0
@dataclass
+5 -3
View File
@@ -344,7 +344,7 @@ class Precipitation(Scene):
colour = (0.80, 0.88, 1.00) if snowing else (0.20, 0.55, 1.00)
for d in self.drops:
d[1] += speed / FPS
d[1] += speed / max(s.get('_fps', FPS), 1.0)
if d[1] > N + 1:
d[0] = np.random.uniform(0, N)
d[1] = np.random.uniform(-2.0, -0.2)
@@ -491,9 +491,10 @@ class LedDisplay:
CROSSFADE = 1.3
def __init__(self, station, cycle_s: float = 0.4):
def __init__(self, station, cycle_s: float = 0.4, fps: float = FPS):
self.station = station
self.cycle_s = float(cycle_s)
self.fps = float(np.clip(fps, 4.0, 30.0))
self.enabled = True
self._stop = asyncio.Event()
self._task = None
@@ -540,6 +541,7 @@ class LedDisplay:
"forecast": series,
"health": self.station.monitor.health.overall,
"retrain": bool(self.station.monitor.retrain_requested),
"_fps": self.fps,
}
def _brightness(self, s: Dict) -> float:
@@ -575,7 +577,7 @@ class LedDisplay:
return self.alert if self._alerting else self.scenes[self._idx]
async def _run(self) -> None:
period = 1.0 / FPS
period = 1.0 / self.fps
t0 = time.monotonic()
while not self._stop.is_set():
frame_start = time.monotonic()