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:
+81
-68
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user