diff --git a/ashvale/api.py b/ashvale/api.py index 32f3a82..7f9ba39 100644 --- a/ashvale/api.py +++ b/ashvale/api.py @@ -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 diff --git a/ashvale/config.py b/ashvale/config.py index 0e9a41f..a3e9d18 100644 --- a/ashvale/config.py +++ b/ashvale/config.py @@ -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 diff --git a/ashvale/led.py b/ashvale/led.py index 953a6d2..5e59b35 100644 --- a/ashvale/led.py +++ b/ashvale/led.py @@ -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()