Joystick labelling, a clock guard, and throttle logging

Three things the hardware offers that the code ignored.

The joystick has never had a line of code. Left records a dry label, right a
wet one, middle cycles the LED scene, and a full-panel flash acknowledges the
press because a headless box gives no other sign and a button you cannot tell
worked gets pressed twice. Precipitation is the weakest head in the bank and
strong labels are its binding constraint: this station has 80 of them against
thousands of proxy ones, entirely because the only label control lives in a web
page, and a web page is not where anyone is standing when it starts raining.

The board has no RTC, so a power cut without a network gives a clock somewhere
in 1970 on the next boot. Solar elevation, the diurnal harmonics and a sample's
position on the 5-minute grid then all lie with complete confidence, and unlike
a gap in the record the damage cannot be identified afterwards. train() now
refuses a clock below 2025 or one that has stepped behind the newest stored
row, and logs the refusal rather than training on fiction.

Undervoltage and thermal capping both shift the SoC temperature, which is the
regressor in the self-heating compensation, so a weak power supply presents as
an unexplained temperature bias rather than as anything resembling a power
problem. get_throttled is now sampled hourly and logged when set.

Measured and deliberately not done: colour features. r, g and b are logged and
74% of rows carry usable colour, but adding blue/red, green/red and saturation
made MAE 1.50% worse and helped in only 13 of 72 cases. Three more regressors
on a 33-feature model whose longest horizon trains on 13 independent pairs is
straightforwardly overfitting. That also prompted a sweep of the RLS prior and
forgetting factor in both directions; delta = 100 with lambda = 0.9985 is a
local optimum on both axes, so neither moved.
This commit is contained in:
2026-08-19 19:45:47 +01:00
parent f1647788c8
commit 3dd45f7ebf
6 changed files with 264 additions and 2 deletions
+46 -1
View File
@@ -26,9 +26,10 @@ from __future__ import annotations
import logging
import math
import subprocess
import time
from pathlib import Path
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional, Tuple
import numpy as np
@@ -42,6 +43,37 @@ TCS3400_CONTROL = 0x8F
TCS3400_CDATA = 0x94
def read_throttled() -> Optional[Dict[str, Any]]:
"""Raspberry Pi undervoltage and throttling flags, or None if all clear.
Bit 0 is undervoltage now, 16 is undervoltage since boot, 2 is arm
frequency capped, 3 is thermal throttling. A capped or browning-out board
runs its SoC at a different temperature, and the SoC temperature is the
regressor in the self-heating compensation, so the visible symptom is a
temperature bias with no apparent cause.
"""
try:
out = subprocess.run(["vcgencmd", "get_throttled"], capture_output=True,
text=True, timeout=5).stdout.strip()
except (OSError, subprocess.SubprocessError):
return None
if "=" not in out:
return None
try:
bits = int(out.split("=", 1)[1], 0)
except ValueError:
return None
if bits == 0:
return None
now = {0: "undervoltage", 1: "arm_capped", 2: "throttled", 3: "soft_temp_limit"}
ever = {16: "undervoltage_since_boot", 17: "arm_capped_since_boot",
18: "throttled_since_boot", 19: "soft_temp_limit_since_boot"}
active = [name for bit, name in now.items() if bits & (1 << bit)]
historic = [name for bit, name in ever.items() if bits & (1 << bit)]
return {"raw": hex(bits), "active": active, "since_boot": historic,
"severity": "warn" if active else "info"}
def read_cpu_temperature() -> float:
"""Core temperature in C. This is the single most important nuisance
variable on a Sense HAT: the HTS221 and LPS25HB sit millimetres above a
@@ -387,6 +419,19 @@ class SenseBoard:
"simulated": False,
}
def stick_events(self) -> List[Tuple[str, str]]:
"""Pending joystick events as (direction, action), oldest first.
Non-blocking, and returns [] when nothing has happened. The library
buffers events, so polling slowly loses none of them.
"""
if self.sense is None:
return []
try:
return [(e.direction, e.action) for e in self.sense.stick.get_events()]
except Exception:
return []
# --------------------------------------------------------------- LED
def clear(self, *args):