0a893d0684
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>
168 lines
6.1 KiB
Python
168 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Helligkeitsumschaltung anhand eines LDR-Werts.
|
|
|
|
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.
|
|
|
|
Die Werte stehen nicht hier, sondern in settings.json (siehe settings.py), damit
|
|
sie sich im Betrieb aendern lassen -- spaeter ueber MQTT.
|
|
|
|
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.
|
|
|
|
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
|
|
"""
|
|
|
|
# argparse und sys.stderr gibt es unter MicroPython nicht -- beides wird nur vom
|
|
# CLI-Teil ganz unten gebraucht und darum erst dort importiert. So laesst sich
|
|
# dieses Modul auf dem ESP32 einfach importieren.
|
|
|
|
AVERAGE_WINDOW = 8 # MAXHELLARRAY: gleitender Mittelwert ueber 8 Werte
|
|
|
|
# Die beiden Helligkeitsstufen. DUNKEL gilt unterhalb der Schwelle, HELL darueber.
|
|
DUNKEL = 0
|
|
HELL = 1
|
|
|
|
|
|
def raw_to_ldr(raw, inverted=None, full_scale=None):
|
|
"""ADC-Rohwert in die Zaehlrichtung bringen, die die Schwellen erwarten.
|
|
|
|
Die Schwellen oben stammen vom alten MSP430-Aufbau: hoher Wert = helle
|
|
Umgebung. Auf der ESP32-Platine liegt der LDR gegen Masse und der 2k gegen
|
|
3V3, dort faellt die Spannung an A0 mit steigender Helligkeit. Der Wert wird
|
|
deshalb gespiegelt, wenn config.LDR_INVERTED gesetzt ist.
|
|
"""
|
|
import config
|
|
if inverted is None:
|
|
inverted = config.LDR_INVERTED
|
|
if full_scale is None:
|
|
full_scale = config.LDR_MAX
|
|
return (full_scale - raw) if inverted else raw
|
|
|
|
|
|
class BrightnessController:
|
|
"""Schaltet anhand des LDR-Werts zwischen zwei Helligkeiten um.
|
|
|
|
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, 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
|
|
|
|
@property
|
|
def level(self):
|
|
return self._level
|
|
|
|
@property
|
|
def settings(self):
|
|
"""Die aktuell verwendeten Werte (Kopie)."""
|
|
return dict(self._werte)
|
|
|
|
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.
|
|
"""
|
|
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-Wert einspeisen. Liefert (stufe, prozent, geaendert)."""
|
|
self._buf.append(ldr)
|
|
if len(self._buf) > AVERAGE_WINDOW:
|
|
self._buf.pop(0)
|
|
avg = sum(self._buf) // len(self._buf)
|
|
|
|
level = self._next_level(avg, self._level)
|
|
changed = level != self._level
|
|
self._level = level
|
|
return level, self.percent(level), changed
|
|
|
|
|
|
def main(argv=None):
|
|
import argparse
|
|
import sys
|
|
|
|
p = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
p.add_argument("--ldr", type=int, required=True, help="LDR-Rohwert (0..4095)")
|
|
p.add_argument("--port", default=None, help="serieller Port (Vorgabe aus config.py)")
|
|
p.add_argument("--dry-run", action="store_true", help="nur berechnen/anzeigen, nicht senden")
|
|
args = p.parse_args(argv)
|
|
|
|
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
|
|
from display import open_display
|
|
d = open_display(port=args.port or config.PORT, fake=args.dry_run)
|
|
try:
|
|
d.set_brightness(percent)
|
|
except migra.MigraError as e:
|
|
print("Fehler: %s" % e, file=sys.stderr)
|
|
return 1
|
|
finally:
|
|
d.close()
|
|
print("OK")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
sys.exit(main())
|