Helligkeit: zwei Werte und eine Schwelle, zur Laufzeit aenderbar

Die vier abgestuften Helligkeiten des alten MSP430-Programms (5/15/50/100 %)
entfallen. Fuer die Sternwarte genuegt der Wechsel Tag/Nacht: unterhalb der
Schwelle laeuft die Anzeige mit dunkel_prozent, oberhalb mit hell_prozent.

Die Werte stehen nicht mehr im Code, sondern in settings.json auf dem Geraet --
Vorbereitung fuer die spaetere MQTT-Anbindung:

    settings.load()                        # mit Vorgaben aufgefuellt
    settings.update({"hell_prozent": 60})  # aendern und speichern

Vorgaben: dunkel 5 %, hell 50 %, Schwelle 1800, Hysterese 150. Die Schwelle liegt
mit Abstand zwischen den gemessenen Lagen (abgedeckt 0, Raumlicht 2453).

Beibehalten, weil die Messung vom 27.07. sie verlangt: der gleitende Mittelwert
und eine Hysterese um die Schwelle. Der ADC rauscht mit +/-32 (nach Mittelung,
1 Sigma) -- ohne Hysterese wuerde die Anzeige am Umschaltpunkt springen.

settings.py prueft jeden Wert beim Laden UND beim Schreiben auf Typ und Bereich.
Das ist der eigentliche Zweck: Mit MQTT kommen die Werte von aussen, und ein
Helligkeitswert von 500 % oder eine Schwelle von -1 wuerde die Anzeige sonst
unbrauchbar machen. Eine fehlende oder beschaedigte Datei faellt auf die Vorgaben
zurueck, ein einzelner kaputter Wert verwirft nur diesen -- ein Geraet in der
Sternwarte soll sich nicht an seiner Konfigurationsdatei aufhaengen.

settings.json wird von deploy.sh bewusst NICHT mitkopiert und steht in
.gitignore: Es ist Laufzeitzustand, kein Quellcode. Sonst waeren die
eingestellten Werte nach jedem Deployen weg.

Am Geraet bestaetigt: schreiben, wiederlesen, Controller uebernimmt die Werte,
beide Helligkeiten auf der Anzeige sichtbar. 119 Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-27 16:34:47 +02:00
parent e8a5b5d5e3
commit 0a893d0684
9 changed files with 549 additions and 210 deletions
+81 -68
View File
@@ -1,20 +1,29 @@
#!/usr/bin/env python3
"""Helligkeitsregelung anhand eines LDR-Werts.
"""Helligkeitsumschaltung anhand eines LDR-Werts.
Portierung von DoDimmen_V aus dem alten MSP430-Programm (GrossAnzeige/main.c):
gleitender Mittelwert, Helligkeitsstufen mit Hysterese, Prozentwerte aus
hellTafel_ub. Das alte Programm hatte 3 Stufen (bis 50 %), hier sind es 4 mit
100 % als oberster.
Zwei Helligkeiten, ein Umschaltpunkt: unterhalb der Schwelle laeuft die Anzeige
mit "dunkel", oberhalb mit "hell". Die vier abgestuften Helligkeiten des alten
MSP430-Programms (DoDimmen_V, 5/15/50/100 %) sind bewusst aufgegeben -- fuer die
Sternwarte genuegt der Wechsel Tag/Nacht.
Der Fotowiderstand (LDR) liefert einen Rohwert (12-bit-ADC, 0..4095). Die
Schwellen rechnen in der Richtung "grosser Wert = helle Umgebung"; auf der
ESP32-Platine ist die Kennlinie umgekehrt, deshalb laeuft der Rohwert zuerst
durch raw_to_ldr(). Die Grenzen sind am realen Aufbau gemessen (siehe unten),
nicht mehr vom MSP430 uebernommen.
Die Werte stehen nicht hier, sondern in settings.json (siehe settings.py), damit
sie sich im Betrieb aendern lassen -- spaeter ueber MQTT.
Reines Python, laeuft auch unter MicroPython (dort spaeter vom ADC gespeist).
Zwei Dinge aus der alten Regelung bleiben, weil die Messung am Geraet sie
verlangt (2026-07-27):
- **gleitender Mittelwert** ueber AVERAGE_WINDOW Werte: der ADC des ESP32-C3
rauscht kraeftig (Rohwert +/-270 bei Raumlicht).
- **Hysterese** um die Schwelle: nach der Mittelung bleiben rund +/-32
(1 Sigma). Ohne Hysterese wuerde die Anzeige am Umschaltpunkt zwischen den
beiden Helligkeiten hin- und herspringen.
python3 brightness.py --ldr 1500 # Stufe/Prozent berechnen und senden
Der Fotowiderstand liefert einen Rohwert (12-bit-ADC, 0..4095). Gerechnet wird in
der Richtung "grosser Wert = helle Umgebung"; auf der ESP32-Platine ist die
Kennlinie umgekehrt, deshalb laeuft der Rohwert zuerst durch raw_to_ldr().
Reines Python, laeuft auch unter MicroPython.
python3 brightness.py --ldr 1500 # Helligkeit berechnen und senden
python3 brightness.py --ldr 200 --dry-run
"""
@@ -22,41 +31,11 @@ Reines Python, laeuft auch unter MicroPython (dort spaeter vom ADC gespeist).
# CLI-Teil ganz unten gebraucht und darum erst dort importiert. So laesst sich
# dieses Modul auf dem ESP32 einfach importieren.
# -- Stufengrenzen, am realen Aufbau gemessen (2026-07-27) ----------------
# Gemessen wurde am XIAO ESP32-C3 mit dem LDR an GPIO2 (200 Messwerte je Lage,
# Median), Werte nach raw_to_ldr, also schon in Zaehlrichtung "gross = hell":
#
# abgedeckt roh 4095 (ADC-Anschlag) -> 0
# Raumlicht roh 1642 -> 2453
# angeleuchtet roh 543 -> 3552
#
# Die alten Grenzen (300/2500/3500) stammten vom MSP430-Aufbau und passten
# nicht: Stufe 3 begann bei 3650 und war damit unerreichbar -- selbst eine
# Taschenlampe direkt auf dem Sensor kam nur auf 3552.
#
# Aufteilung nach Nutzerwunsch "nachts laenger dunkel": der 5-%-Bereich reicht
# bis 1200, damit schwaches Licht die Anzeige noch nicht aufhellt und die
# Dunkeladaption der Beobachter geschont wird.
HELL_LOW = 1200 # Stufengrenze 0<->1 (5 % bis hierher)
HELL_HIGH = 2400 # Stufengrenze 1<->2
HELL_MAX = 3200 # Stufengrenze 2<->3 (100 % ab hier, jetzt erreichbar)
# Hysterese: Der Rohwert streut bei Raumlicht um +/-270; nach dem gleitenden
# Mittel ueber AVERAGE_WINDOW bleiben rund +/-32 (1 Sigma). 150 liegt sicher
# darueber (~4,7 Sigma) und zugleich weit unter dem Stufenabstand von 800.
# Frueher standen hier 30 fuer den untersten Uebergang (und 5*30 fuer die
# oberen) -- 30 lag unter dem Rauschen, die unterste Stufe haette geflattert.
HELL_HYST = 150
AVERAGE_WINDOW = 8 # MAXHELLARRAY: gleitender Mittelwert ueber 8 Werte
# Helligkeits-Prozente je Stufe, aus hellTafel_ub (main.h): 5/15/50/100 %.
PERCENTS = (5, 15, 50, 100)
# Mittelpunkte der Stufengrenzen und deren Hysterese (Stufe i <-> i+1).
# Jetzt einheitlich: die gemessene Streuung ist an allen Grenzen dieselbe, es
# gibt keinen Grund mehr fuer die gestaffelten Breiten des alten Programms.
_CENTERS = (HELL_LOW, HELL_HIGH, HELL_MAX)
_HYSTS = (HELL_HYST, HELL_HYST, HELL_HYST)
# Die beiden Helligkeitsstufen. DUNKEL gilt unterhalb der Schwelle, HELL darueber.
DUNKEL = 0
HELL = 1
def raw_to_ldr(raw, inverted=None, full_scale=None):
@@ -76,39 +55,68 @@ def raw_to_ldr(raw, inverted=None, full_scale=None):
class BrightnessController:
"""Rechnet aus LDR-Rohwerten die Anzeigehelligkeit -- wie DoDimmen_V.
"""Schaltet anhand des LDR-Werts zwischen zwei Helligkeiten um.
update() gibt die neue Helligkeit in Prozent nur zurueck, wenn sich die Stufe
geaendert hat (sonst None). So wird -- wie im alten Programm -- nur bei
tatsaechlicher Aenderung ein Helligkeitstelegramm gesendet.
update() meldet zurueck, ob sich die Stufe geaendert hat -- nur dann wird ein
Helligkeitstelegramm gesendet, wie schon im alten Programm.
Die Werte kommen aus settings.json und werden beim Anlegen einmal gelesen.
Wer sie im Betrieb aendert (spaeter ueber MQTT), ruft danach reload() auf.
"""
def __init__(self):
def __init__(self, werte=None, pfad=None, log=None):
import settings
self._settings = settings
self._pfad = pfad
self._werte = settings.load(pfad, log=log) if werte is None else dict(werte)
self._buf = []
self._level = None # entspricht oldHellStufe_ub = 0xFF (>=2-Zweig)
self._level = None
@property
def level(self):
return self._level
def _next_level(self, val, old):
"""Stufe mit Hysterese bestimmen (verallgemeinert auf beliebig viele Stufen).
@property
def settings(self):
"""Die aktuell verwendeten Werte (Kopie)."""
return dict(self._werte)
Hochschalten, solange der Wert die obere Kante (Mitte + Hysterese) der
naechsten Grenze ueberschreitet; herunterschalten, solange er die untere
Kante (Mitte - Hysterese) unterschreitet. Beim ersten Aufruf (old=None)
wird von Stufe 0 aus hochgeschaltet -- die Anzeige startet also gedimmt
und hellt sich passend zum Messwert auf.
def reload(self, log=None):
"""Einstellungen neu aus der Datei lesen.
Die Stufe bleibt stehen; ob sie zu den neuen Werten noch passt, klaert
der naechste update() -- inklusive Hysterese, also ohne Sprung.
"""
level = 0 if old is None else old
while level < len(PERCENTS) - 1 and val >= _CENTERS[level] + _HYSTS[level]:
level += 1
while level > 0 and val < _CENTERS[level - 1] - _HYSTS[level - 1]:
level -= 1
return level
self._werte = self._settings.load(self._pfad, log=log)
return self.settings
def percent(self, level):
"""Prozentwert zu einer Stufe."""
return self._werte["hell_prozent" if level == HELL else "dunkel_prozent"]
def _next_level(self, val, old):
"""Stufe mit Hysterese bestimmen.
Umgeschaltet wird erst, wenn der Wert die Schwelle um die Hysterese
ueberschreitet -- nach oben auf HELL, nach unten auf DUNKEL. Dazwischen
bleibt es bei der bisherigen Stufe; genau das verhindert das Flattern.
Beim ersten Aufruf (old=None) gibt es keine bisherige Stufe. Dann
entscheidet die blanke Schwelle, damit die Anzeige sofort richtig
startet, statt erst nach der halben Hysterese aufzuwachen.
"""
schwelle = self._werte["schwelle"]
hyst = self._werte["hysterese"]
if old is None:
return HELL if val >= schwelle else DUNKEL
if val >= schwelle + hyst:
return HELL
if val < schwelle - hyst:
return DUNKEL
return old
def update(self, ldr):
"""Einen LDR-Rohwert einspeisen. Liefert (stufe, prozent, geaendert)."""
"""Einen LDR-Wert einspeisen. Liefert (stufe, prozent, geaendert)."""
self._buf.append(ldr)
if len(self._buf) > AVERAGE_WINDOW:
self._buf.pop(0)
@@ -117,7 +125,7 @@ class BrightnessController:
level = self._next_level(avg, self._level)
changed = level != self._level
self._level = level
return level, PERCENTS[level], changed
return level, self.percent(level), changed
def main(argv=None):
@@ -131,8 +139,13 @@ def main(argv=None):
p.add_argument("--dry-run", action="store_true", help="nur berechnen/anzeigen, nicht senden")
args = p.parse_args(argv)
level, percent, _ = BrightnessController().update(args.ldr)
print("LDR %d -> Stufe %d -> %d %%" % (args.ldr, level, percent))
c = BrightnessController()
level, percent, _ = c.update(args.ldr)
w = c.settings
print("Schwelle %d (+/-%d) -> dunkel %d %%, hell %d %%"
% (w["schwelle"], w["hysterese"], w["dunkel_prozent"], w["hell_prozent"]))
print("LDR %d -> %s -> %d %%"
% (args.ldr, "HELL" if level == HELL else "DUNKEL", percent))
import config
import migra