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:
@@ -14,3 +14,8 @@ Hardware/
|
||||
|
||||
# WLAN-Zugangsdaten des ESP32 (Vorlage: wifi_config_example.py)
|
||||
wifi_config.py
|
||||
|
||||
# Laufzeit-Einstellungen (Helligkeiten, Schwelle -- spaeter per MQTT verstellt).
|
||||
# Gehoert dem Geraet, nicht dem Repo; die Vorgaben stehen in settings.DEFAULTS.
|
||||
settings.json
|
||||
test_settings_tmp.json
|
||||
@@ -25,9 +25,9 @@ python3 run_display.py --mock --move --ldr 2000 # zusätzlich mit Helligkeitsre
|
||||
python3 mount_mock.py --move # Mock separat starten
|
||||
|
||||
# Helligkeit aus einem LDR-Wert setzen:
|
||||
python3 brightness.py --ldr 1500 # LDR-Rohwert -> Stufe/Prozent -> senden
|
||||
python3 brightness.py --ldr 1500 # LDR-Rohwert -> Helligkeit -> senden
|
||||
|
||||
python3 -m unittest test_migra test_lx200 test_brightness test_display test_esp32
|
||||
python3 -m unittest discover -p 'test_*.py' # alle Tests
|
||||
python3 probe.py ruler # Geometrie der Anzeige ausmessen
|
||||
```
|
||||
|
||||
@@ -56,9 +56,10 @@ beiden Startprogrammen.
|
||||
| `ticks.py` | Timeouts, die unter CPython und MicroPython gleich funktionieren. | ✓ |
|
||||
| `display.py` | Treiber: Telegramm senden, Antwort auswerten. | ✓ |
|
||||
| `coords.py` | Koordinaten einlesen und auf die Zeilenbreite formatieren. | ✓ |
|
||||
| `config.py` | Port/Pins, Adresse, Geometrie, Helligkeit, WLAN — alles an einer Stelle. | ✓ |
|
||||
| `config.py` | Port/Pins, Adresse, Geometrie, WLAN — fest, ändert sich nur beim Deployen. | ✓ |
|
||||
| `settings.py` | Zur Laufzeit änderbare Werte (Helligkeiten, Schwelle) in `settings.json`. | ✓ |
|
||||
| `displayloop.py` | Die Poll-Schleife: abfragen, formatieren, anzeigen, Fehler abfangen. | ✓ |
|
||||
| `brightness.py` | Helligkeitsregelung: LDR-Wert → Stufe/Prozent. | ✓ |
|
||||
| `brightness.py` | Helligkeit: LDR-Wert → dunkel/hell, Schwelle aus `settings.json`. | ✓ |
|
||||
| `mount_client.py` | TCP-Client zur GM4000 (LX200). | ✓ |
|
||||
| `lx200.py` | LX200-Kommandorahmen. | ✓ |
|
||||
| `wifi.py` | WLAN-Anbindung des ESP32 (verbinden, Verbindung halten). | ✓ |
|
||||
@@ -69,7 +70,7 @@ beiden Startprogrammen.
|
||||
| `mount_mock.py` | Simulator der Montierung (LX200) fürs Testen ohne Teleskop. | |
|
||||
| `probe.py` | Testmuster zum Ausmessen von Zeilenbreite und Zeilenposition. | |
|
||||
| `deploy.sh` | Kopiert die ESP32-Dateien per `mpremote` auf das Gerät. | |
|
||||
| `test_migra.py` / `test_lx200.py` / `test_brightness.py` / `test_display.py` / `test_esp32.py` | Tests. | |
|
||||
| `test_*.py` | Tests (migra, lx200, brightness, settings, display, esp32). | |
|
||||
|
||||
## Protokoll (Kurzfassung)
|
||||
|
||||
@@ -247,11 +248,14 @@ Mac:
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- **LDR-Schwellen in der Sternwarte gegenprüfen.** Die Grenzen sind am realen
|
||||
Aufbau gemessen (2026-07-27, siehe *Helligkeitsregelung*), aber am Schreibtisch.
|
||||
Vor Ort bei Tag und Nacht die Rohwerte ablesen und `HELL_LOW`/`HELL_HIGH`/
|
||||
`HELL_MAX` bestätigen — besonders die Grenzlage bei 2400. Bis dahin läuft die
|
||||
Anzeige mit fester Helligkeit (`run_esp32.main()` ohne `with_ldr`).
|
||||
- **Umschaltschwelle in der Sternwarte gegenprüfen.** Die Schwelle (1800) ist am
|
||||
realen Aufbau gemessen (2026-07-27, siehe *Helligkeitsregelung*), aber am
|
||||
Schreibtisch. Vor Ort bei Tag und Nacht die Rohwerte ablesen und die Schwelle in
|
||||
`settings.json` bestätigen. Bis dahin läuft die Anzeige mit fester Helligkeit
|
||||
(`run_esp32.main()` ohne `with_ldr`).
|
||||
- **MQTT-Anbindung.** Die Einstellungen liegen bereits zur Laufzeit änderbar in
|
||||
`settings.json` (`settings.update(...)`, geprüft auf Typ und Bereich); der
|
||||
MQTT-Client, der sie von außen setzt, fehlt noch.
|
||||
- **Netzwerk zur echten Montierung.** Die GM4000 steht in `192.168.1.115`, das
|
||||
Heimnetz des ESP32 ist `192.168.178.x` — der Mac erreicht sie per VPN, der
|
||||
ESP32 so nicht. Muss geklärt werden, bevor es an die echte Montierung geht.
|
||||
@@ -343,51 +347,75 @@ zerlegt (Trennzeichen `:`, `*`, `0xDF`, `'` sind alle egal).
|
||||
|
||||
## Helligkeitsregelung
|
||||
|
||||
Portiert aus dem alten MSP430-Programm (`DoDimmen_V`). Ein LDR-Rohwert
|
||||
(12-Bit-ADC, 0–4095) wird über `raw_to_ldr` in die Zählrichtung „groß = hell"
|
||||
gebracht, über einen gleitenden Mittelwert (8 Werte) geglättet und auf eine
|
||||
Helligkeitsstufe mit Hysterese abgebildet:
|
||||
**Zwei Helligkeiten, ein Umschaltpunkt.** Unterhalb der Schwelle läuft die Anzeige
|
||||
mit `dunkel_prozent`, oberhalb mit `hell_prozent`. Die vier abgestuften Helligkeiten
|
||||
des alten MSP430-Programms (5/15/50/100 %) sind bewusst aufgegeben — für die
|
||||
Sternwarte genügt der Wechsel Tag/Nacht.
|
||||
|
||||
| Stufe | Helligkeit | erreicht ab (hoch / runter) |
|
||||
|------|-----------|------------------------------|
|
||||
| 0 | 5 % | < 1050 |
|
||||
| 1 | 15 % | ≥ 1350 / < 1050 |
|
||||
| 2 | 50 % | ≥ 2550 / < 2250 |
|
||||
| 3 | 100 % | ≥ 3350 / < 3050 |
|
||||
Ein LDR-Rohwert (12-Bit-ADC, 0–4095) wird über `raw_to_ldr` in die Zählrichtung
|
||||
„groß = hell" gebracht, über einen gleitenden Mittelwert (8 Werte) geglättet und
|
||||
gegen die Schwelle gehalten:
|
||||
|
||||
Schwellen-Mitten `1200 / 2400 / 3200`, Hysterese einheitlich `±150`.
|
||||
| Bereich | Helligkeit | Umschaltpunkt |
|
||||
|---|---|---|
|
||||
| unter der Schwelle | `dunkel_prozent` = 5 % | herunter ab < 1650 |
|
||||
| über der Schwelle | `hell_prozent` = 50 % | hoch ab ≥ 1950 |
|
||||
|
||||
Schwelle `1800`, Hysterese `±150`. Beim allerersten Messwert entscheidet die
|
||||
blanke Schwelle (1800) statt der Kante — sonst startete die Anzeige im Bereich
|
||||
dazwischen fälschlich dunkel und wachte erst später auf.
|
||||
|
||||
### Die Werte stehen in `settings.json`, nicht im Code
|
||||
|
||||
`config.py` ist Programmcode und ändert sich nur beim Deployen. Die Helligkeiten
|
||||
und die Schwelle sollen dagegen im Betrieb verstellbar sein — **später über MQTT**.
|
||||
Sie liegen deshalb als JSON auf dem Gerät und werden über `settings.py` gelesen und
|
||||
geschrieben:
|
||||
|
||||
```python
|
||||
import settings
|
||||
settings.load() # mit Vorgaben aufgefüllt
|
||||
settings.update({"hell_prozent": 60}) # ändern und speichern
|
||||
```
|
||||
|
||||
Ein laufender `BrightnessController` übernimmt Änderungen mit `reload()`.
|
||||
|
||||
Jeder Wert wird beim Laden **und** beim Schreiben geprüft (Typ und Bereich:
|
||||
Prozente 0–100, Schwelle 0–4095). Das ist der Grund, warum die Prüfung überhaupt
|
||||
existiert: Sobald MQTT dazukommt, kommen die Werte von außen, und ein
|
||||
Helligkeitswert von 500 % oder eine Schwelle von −1 würde die Anzeige sonst
|
||||
unbrauchbar machen. Fehlt die Datei oder ist sie beschädigt, gelten die Vorgaben
|
||||
aus `settings.DEFAULTS` — ein Gerät in der Sternwarte soll sich nicht an einer
|
||||
kaputten Konfigurationsdatei aufhängen. Ein einzelner unbrauchbarer Wert verwirft
|
||||
nur diesen, nicht die ganze Datei.
|
||||
|
||||
`settings.json` wird von `deploy.sh` **nicht** mitkopiert und steht in `.gitignore`:
|
||||
Es ist Laufzeitzustand des Geräts, kein Quellcode. Würde es mitkopiert, wären die
|
||||
eingestellten Werte nach jedem Deployen weg.
|
||||
|
||||
### Die Schwelle stammt aus einer Messung
|
||||
|
||||
**Am realen Aufbau gemessen (2026-07-27)**, XIAO ESP32-C3 mit LDR an GPIO2,
|
||||
je 200 Messwerte, Median — Werte nach `raw_to_ldr`:
|
||||
|
||||
| Lichtsituation | roh | nach `raw_to_ldr` | Stufe |
|
||||
|---|---|---|---|
|
||||
| abgedeckt | 4095 (ADC-Anschlag) | 0 | 0 → 5 % |
|
||||
| Raumlicht | 1642 | 2453 | 1 → 15 % |
|
||||
| angeleuchtet (Taschenlampe) | 543 | 3552 | 3 → 100 % |
|
||||
| abgedeckt | 4095 (ADC-Anschlag) | 0 | dunkel → 5 % |
|
||||
| Raumlicht | 1642 | 2453 | hell → 50 % |
|
||||
| angeleuchtet (Taschenlampe) | 543 | 3552 | hell → 50 % |
|
||||
|
||||
Die alten Werte `300 / 2500 / 3500` stammten vom MSP430-Aufbau und passten nicht:
|
||||
Stufe 3 begann bei 3650 und war **unerreichbar** — selbst eine Taschenlampe direkt
|
||||
auf dem Sensor kam nur auf 3552. Die Aufteilung ist jetzt so gewählt, dass der
|
||||
5-%-Bereich bis 1200 reicht („nachts länger dunkel", schont die Dunkeladaption).
|
||||
Die Schwelle 1800 liegt mit Abstand zwischen dem Dunkelwert und dem Raumlicht.
|
||||
|
||||
Auch die **Hysterese war zu klein**: Der Rohwert streut bei Raumlicht um ±270,
|
||||
nach der Mittelung bleiben rund ±32 (1σ). Die früheren `±30` am untersten Übergang
|
||||
lagen darunter — die unterste Stufe hätte geflattert. Jetzt einheitlich ±150 (≈4,7σ)
|
||||
und damit weit unter dem Stufenabstand von 800.
|
||||
|
||||
Ein Helligkeitstelegramm wird nur bei **Stufenwechsel** gesendet (`ESC H 2 <ppp>`),
|
||||
wie im Original. Am Gerät bestätigt: Abdecken schaltet herunter, Freigeben wieder
|
||||
hoch, ohne Flattern.
|
||||
|
||||
> **Bekannte Grenzlage:** Der gemessene Raumlichtwert 2453 liegt nur 53 Zähler über
|
||||
> `HELL_HIGH` (2400) und damit innerhalb der Hysterese — Raumlicht landet deshalb auf
|
||||
> Stufe 1 (15 %), nicht auf 50 %. Ob das so gewollt ist, entscheidet sich erst an den
|
||||
> Lichtverhältnissen in der Sternwarte. `test_brightness.py` hält das Verhalten fest.
|
||||
**Die Hysterese ist nicht optional.** Der Rohwert streut bei Raumlicht um ±270,
|
||||
nach der Mittelung bleiben rund ±32 (1σ). Ohne Hysterese würde die Anzeige am
|
||||
Umschaltpunkt zwischen den beiden Helligkeiten hin- und herspringen; ±150 liegt
|
||||
sicher darüber (≈4,7σ). Ein Helligkeitstelegramm wird ohnehin nur bei
|
||||
**Stufenwechsel** gesendet (`ESC H 2 <ppp>`), wie im Original.
|
||||
|
||||
**Im Dunkeln steht der ADC am Anschlag** (4095). Der Teiler treibt A0 über den
|
||||
Messbereich, „Dämmerung" und „stockdunkel" sind daher nicht unterscheidbar. Für die
|
||||
Regelung unkritisch — nachts ist ohnehin die dunkelste Stufe gewollt. Eine feinere
|
||||
Messbereich, „Dämmerung" und „stockdunkel" sind daher nicht unterscheidbar. Bei
|
||||
nur zwei Helligkeiten ist das ohne Belang — unterschieden werden muss nur hell
|
||||
gegen dunkel, und dafür reicht die Auflösung weit. Eine feinere
|
||||
Nachtabstufung ginge nur über den Vorwiderstand: sättigungsfrei wäre
|
||||
`R2 ≥ R_LDR(dunkel) × (3,3 V − U_max)/U_max`, also grob `R2 ≥ 0,32 × R_LDR(dunkel)`.
|
||||
Bei einem typischen LDR (0,5–1 MΩ im Dunkeln) liefe das auf 160–330 kΩ hinaus — dann
|
||||
|
||||
+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
|
||||
|
||||
@@ -23,10 +23,16 @@ PORT="${1:-auto}"
|
||||
# sys.argv, aber nur in main() -- die Messfunktionen ruft man am REPL direkt auf:
|
||||
# import probe, display
|
||||
# d = display.open_display(); probe.pitch(d, 6); d.close()
|
||||
#
|
||||
# settings.json steht bewusst NICHT in dieser Liste: Das ist Laufzeitzustand auf
|
||||
# dem Geraet (Helligkeiten, Schwelle -- spaeter per MQTT verstellt). Wuerde
|
||||
# deploy.sh sie mitkopieren, waeren diese Einstellungen nach jedem Deployen weg.
|
||||
# Fehlt die Datei, gelten die Vorgaben aus settings.DEFAULTS.
|
||||
FILES="
|
||||
ticks.py
|
||||
migra.py
|
||||
config.py
|
||||
settings.py
|
||||
transport.py
|
||||
display.py
|
||||
coords.py
|
||||
|
||||
+2
-1
@@ -64,7 +64,8 @@ def poll_loop(display, client, interval=None, once=False,
|
||||
level, percent, changed = dimmer.update(raw)
|
||||
if changed:
|
||||
display.set_brightness(percent)
|
||||
log("LDR %d -> Helligkeit Stufe %d = %d %%" % (raw, level, percent))
|
||||
log("LDR %d -> %s = %d %%"
|
||||
% (raw, "HELL" if level else "DUNKEL", percent))
|
||||
|
||||
ra_raw, dec_raw = client.get_coordinates()
|
||||
ra_line, dec_line = format_coordinates(ra_raw, dec_raw)
|
||||
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
"""Einstellungen, die sich im Betrieb aendern lassen -- als JSON auf dem Geraet.
|
||||
|
||||
config.py ist Programmcode: Pinbelegung, Protokoll, Geometrie. Das aendert sich
|
||||
nur beim Deployen. Werte, die im laufenden Betrieb verstellt werden sollen --
|
||||
spaeter ueber MQTT -- gehoeren dagegen hierher. Sie liegen als JSON im
|
||||
Dateisystem (settings.json) und lassen sich lesen und zurueckschreiben, ohne das
|
||||
Programm neu zu spielen.
|
||||
|
||||
import settings
|
||||
w = settings.load() # mit Vorgaben aufgefuellt
|
||||
settings.update({"hell_prozent": 60}) # aendern und speichern
|
||||
|
||||
Fehlt die Datei oder ist sie beschaedigt, gelten die Vorgaben aus DEFAULTS --
|
||||
die Anzeige laeuft dann weiter, statt beim Start auszufallen. Das ist Absicht:
|
||||
ein Geraet in der Sternwarte soll sich nicht an einer kaputten Konfigurationsdatei
|
||||
aufhaengen.
|
||||
|
||||
Alle Werte werden beim Laden UND beim Schreiben geprueft (siehe _pruefe). Das ist
|
||||
wichtig, sobald MQTT dazukommt: Von dort kommen fremde Eingaben, und ein
|
||||
Helligkeitswert von 500 % oder eine Schwelle von -1 wuerde die Anzeige sonst in
|
||||
einen unbrauchbaren Zustand bringen.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
PFAD = "settings.json"
|
||||
|
||||
# Vorgaben. Die Helligkeiten und die Schwelle stammen aus der LDR-Messung vom
|
||||
# 2026-07-27 (siehe README, Abschnitt Helligkeitsregelung): abgedeckt ergab 0,
|
||||
# Raumlicht 2453, angeleuchtet 3552 -- 1800 liegt mit Abstand zwischen dunkel
|
||||
# und Raumlicht. Die Hysterese deckt das gemessene ADC-Rauschen ab (~32 nach
|
||||
# der Mittelung, 1 Sigma).
|
||||
DEFAULTS = {
|
||||
"dunkel_prozent": 5, # Helligkeit unterhalb der Schwelle
|
||||
"hell_prozent": 50, # Helligkeit oberhalb der Schwelle
|
||||
"schwelle": 1800, # LDR-Wert (nach raw_to_ldr) fuer den Umschaltpunkt
|
||||
"hysterese": 150, # Breite um die Schwelle, gegen Flattern
|
||||
}
|
||||
|
||||
# Erlaubte Bereiche: (minimum, maximum). Die Prozente gehen an ESC H, das nur
|
||||
# 0..100 kennt; die LDR-Werte kommen aus einem 12-Bit-ADC.
|
||||
_GRENZEN = {
|
||||
"dunkel_prozent": (0, 100),
|
||||
"hell_prozent": (0, 100),
|
||||
"schwelle": (0, 4095),
|
||||
"hysterese": (0, 2000),
|
||||
}
|
||||
|
||||
|
||||
class SettingsError(ValueError):
|
||||
"""Ein Wert liegt ausserhalb des erlaubten Bereichs oder ist unbekannt."""
|
||||
|
||||
|
||||
def _pruefe(werte):
|
||||
"""Werte auf Typ und Bereich pruefen. Liefert ein geprueftes neues dict."""
|
||||
geprueft = {}
|
||||
for name, wert in werte.items():
|
||||
if name not in DEFAULTS:
|
||||
raise SettingsError("unbekannte Einstellung %r" % (name,))
|
||||
# bool ist in Python ein int -- hier aber sicher ein Irrtum.
|
||||
if isinstance(wert, bool) or not isinstance(wert, int):
|
||||
raise SettingsError("%s: %r ist keine ganze Zahl" % (name, wert))
|
||||
low, high = _GRENZEN[name]
|
||||
if not low <= wert <= high:
|
||||
raise SettingsError("%s: %d liegt ausserhalb %d..%d"
|
||||
% (name, wert, low, high))
|
||||
geprueft[name] = wert
|
||||
return geprueft
|
||||
|
||||
|
||||
def load(pfad=None, log=None):
|
||||
"""Einstellungen lesen, fehlende Werte aus DEFAULTS ergaenzen.
|
||||
|
||||
Wirft nicht: Fehlt die Datei, ist sie unlesbar oder steht Unsinn darin, gibt
|
||||
es die Vorgaben. Ein einzelner unbrauchbarer Wert verwirft nur diesen, nicht
|
||||
die ganze Datei.
|
||||
"""
|
||||
pfad = PFAD if pfad is None else pfad
|
||||
werte = dict(DEFAULTS)
|
||||
try:
|
||||
with open(pfad) as f:
|
||||
gelesen = json.load(f)
|
||||
except (OSError, ValueError):
|
||||
return werte # keine Datei oder kaputtes JSON
|
||||
if not isinstance(gelesen, dict):
|
||||
return werte
|
||||
|
||||
for name, wert in gelesen.items():
|
||||
try:
|
||||
werte.update(_pruefe({name: wert}))
|
||||
except SettingsError as e:
|
||||
if log:
|
||||
log("settings: %s -- Vorgabe %r bleibt"
|
||||
% (e, DEFAULTS.get(name)))
|
||||
return werte
|
||||
|
||||
|
||||
def save(werte, pfad=None):
|
||||
"""Gepruefte Einstellungen schreiben. Liefert die gespeicherten Werte."""
|
||||
pfad = PFAD if pfad is None else pfad
|
||||
vollstaendig = dict(DEFAULTS)
|
||||
vollstaendig.update(_pruefe(werte))
|
||||
with open(pfad, "w") as f:
|
||||
json.dump(vollstaendig, f)
|
||||
return vollstaendig
|
||||
|
||||
|
||||
def update(aenderungen, pfad=None, log=None):
|
||||
"""Einzelne Werte aendern und speichern -- der Weg fuer MQTT.
|
||||
|
||||
Erst pruefen, dann mit dem Bestand mischen, dann schreiben. Ist etwas
|
||||
ungueltig, bleibt die Datei unveraendert.
|
||||
"""
|
||||
geprueft = _pruefe(aenderungen)
|
||||
werte = load(pfad, log=log)
|
||||
werte.update(geprueft)
|
||||
return save(werte, pfad)
|
||||
+122
-90
@@ -1,127 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests fuer die Helligkeitsregelung (verallgemeinert aus DoDimmen_V).
|
||||
"""Tests fuer die Helligkeitsumschaltung (zwei Stufen, ein Umschaltpunkt).
|
||||
|
||||
python3 -m unittest -v test_brightness
|
||||
|
||||
Grenzen am Geraet gemessen (2026-07-27): Mitten (1200, 2400, 3200) mit
|
||||
einheitlicher Hysterese 150 -> hoch bei 1350 / 2550 / 3350,
|
||||
runter bei 1050 / 2250 / 3050.
|
||||
|
||||
Die Zahlen stehen bewusst ausgeschrieben statt aus brightness.* gerechnet:
|
||||
So faellt eine versehentliche Aenderung der Schwellen im Test auf, statt
|
||||
stillschweigend mitzuwandern.
|
||||
Die Tests geben ihre Werte selbst vor, statt settings.json zu lesen -- so
|
||||
haengen sie nicht daran, was gerade auf einem Geraet konfiguriert ist.
|
||||
Vorgabewerte: Schwelle 1800 +/- 150 -> hoch ab 1950, runter unter 1650.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
import brightness
|
||||
from brightness import BrightnessController
|
||||
from brightness import BrightnessController, DUNKEL, HELL
|
||||
|
||||
WERTE = {"dunkel_prozent": 5, "hell_prozent": 50,
|
||||
"schwelle": 1800, "hysterese": 150}
|
||||
|
||||
|
||||
class TestStufen(unittest.TestCase):
|
||||
def controller(**abweichend):
|
||||
werte = dict(WERTE)
|
||||
werte.update(abweichend)
|
||||
return BrightnessController(werte=werte)
|
||||
|
||||
|
||||
class TestUmschalten(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.c = BrightnessController()
|
||||
self.c = controller()
|
||||
|
||||
def test_erststart_schaltet_von_0_hoch(self):
|
||||
self.assertEqual(self.c._next_level(1349, None), 0)
|
||||
self.assertEqual(self.c._next_level(1350, None), 1)
|
||||
self.assertEqual(self.c._next_level(2549, None), 1)
|
||||
self.assertEqual(self.c._next_level(2550, None), 2)
|
||||
self.assertEqual(self.c._next_level(3349, None), 2)
|
||||
self.assertEqual(self.c._next_level(3350, None), 3) # 100 % erreichbar
|
||||
def test_erststart_entscheidet_an_der_blanken_schwelle(self):
|
||||
"""Ohne bisherige Stufe zaehlt die Schwelle selbst, nicht ihre Kante.
|
||||
|
||||
def test_hysterese_von_stufe_1(self):
|
||||
self.assertEqual(self.c._next_level(1050, 1), 1)
|
||||
self.assertEqual(self.c._next_level(1049, 1), 0)
|
||||
self.assertEqual(self.c._next_level(2549, 1), 1)
|
||||
self.assertEqual(self.c._next_level(2550, 1), 2)
|
||||
|
||||
def test_hysterese_von_stufe_2(self):
|
||||
self.assertEqual(self.c._next_level(2250, 2), 2)
|
||||
self.assertEqual(self.c._next_level(2249, 2), 1)
|
||||
self.assertEqual(self.c._next_level(3349, 2), 2)
|
||||
self.assertEqual(self.c._next_level(3350, 2), 3)
|
||||
|
||||
def test_hysterese_von_stufe_3(self):
|
||||
self.assertEqual(self.c._next_level(3050, 3), 3)
|
||||
self.assertEqual(self.c._next_level(3049, 3), 2)
|
||||
|
||||
def test_prozente(self):
|
||||
self.assertEqual(brightness.PERCENTS, (5, 15, 50, 100))
|
||||
|
||||
def test_gemessene_lagen_ergeben_die_gewollten_stufen(self):
|
||||
"""Die am Geraet gemessenen Lichtlagen, in Stufen uebersetzt.
|
||||
|
||||
Das ist der eigentliche Zweck der Kalibrierung: abgedeckt muss auf die
|
||||
dunkelste Stufe fallen, angeleuchtet auf die hellste. Beides ging mit
|
||||
den alten MSP430-Schwellen daneben (100 % war unerreichbar).
|
||||
Sonst startete die Anzeige im Bereich zwischen Schwelle und Kante
|
||||
faelschlich dunkel und wachte erst spaeter auf.
|
||||
"""
|
||||
self.assertEqual(self.c._next_level(0, None), 0) # abgedeckt -> 5 %
|
||||
self.assertEqual(self.c._next_level(3552, None), 3) # Lampe -> 100 %
|
||||
self.assertEqual(self.c._next_level(1799, None), DUNKEL)
|
||||
self.assertEqual(self.c._next_level(1800, None), HELL)
|
||||
|
||||
def test_raumlicht_liegt_dicht_an_der_grenze(self):
|
||||
"""Bekannte Grenzlage, absichtlich festgehalten.
|
||||
def test_hochschalten_erst_ueber_der_oberen_kante(self):
|
||||
self.assertEqual(self.c._next_level(1949, DUNKEL), DUNKEL)
|
||||
self.assertEqual(self.c._next_level(1950, DUNKEL), HELL)
|
||||
|
||||
Der gemessene Raumlichtwert 2453 liegt nur 53 Zaehler ueber HELL_HIGH
|
||||
(2400) und damit unter der Hysterese: Hochgeschaltet wird erst ab 2550,
|
||||
also bleibt Raumlicht auf Stufe 1 (15 %). Eine Stufengrenze so dicht an
|
||||
einem realen Betriebspunkt ist keine gute Stelle -- welche Stufe dort
|
||||
gewollt ist, entscheidet sich erst an den Lichtverhaeltnissen in der
|
||||
Sternwarte. Bis dahin haelt dieser Test das Verhalten schriftlich fest,
|
||||
damit es niemanden ueberrascht.
|
||||
"""
|
||||
self.assertEqual(self.c._next_level(2453, None), 1)
|
||||
self.assertLess(abs(2453 - brightness.HELL_HIGH), brightness.HELL_HYST)
|
||||
def test_herunterschalten_erst_unter_der_unteren_kante(self):
|
||||
self.assertEqual(self.c._next_level(1650, HELL), HELL)
|
||||
self.assertEqual(self.c._next_level(1649, HELL), DUNKEL)
|
||||
|
||||
def test_hysterese_ueberdeckt_das_gemessene_rauschen(self):
|
||||
"""Nach dem gleitenden Mittel bleiben rund 32 Zaehler Streuung (1 Sigma).
|
||||
def test_im_hysteresefenster_bleibt_die_stufe_stehen(self):
|
||||
"""Der Kern der Sache: dazwischen aendert sich nichts, egal von wo."""
|
||||
for wert in (1650, 1800, 1949):
|
||||
self.assertEqual(self.c._next_level(wert, DUNKEL), DUNKEL)
|
||||
self.assertEqual(self.c._next_level(wert, HELL), HELL)
|
||||
|
||||
Die Hysterese muss deutlich darueber liegen, sonst springt die Stufe an
|
||||
einer Grenze hin und her -- genau das waere mit den alten 30 passiert.
|
||||
"""
|
||||
self.assertGreaterEqual(brightness.HELL_HYST, 100)
|
||||
def test_prozente_kommen_aus_den_einstellungen(self):
|
||||
c = controller(dunkel_prozent=7, hell_prozent=80)
|
||||
self.assertEqual(c.percent(DUNKEL), 7)
|
||||
self.assertEqual(c.percent(HELL), 80)
|
||||
|
||||
def test_kein_stufensprung_bei_rauschen_um_eine_grenze(self):
|
||||
"""Werte, die um eine Stufengrenze streuen, duerfen die Stufe halten."""
|
||||
c = BrightnessController()
|
||||
for _ in range(brightness.AVERAGE_WINDOW):
|
||||
c.update(1200) # genau auf der Grenze einschwingen
|
||||
stufe_vorher = c.level
|
||||
for delta in (+60, -60, +55, -50, +45, -60, +60, -55):
|
||||
stufe, _, changed = c.update(1200 + delta)
|
||||
self.assertFalse(changed, "Rauschen um die Grenze hat umgeschaltet")
|
||||
self.assertEqual(stufe, stufe_vorher)
|
||||
def test_gemessene_lichtlagen(self):
|
||||
"""Die am Geraet gemessenen Lagen (2026-07-27) in Stufen uebersetzt."""
|
||||
self.assertEqual(self.c._next_level(0, None), DUNKEL) # abgedeckt
|
||||
self.assertEqual(self.c._next_level(2453, None), HELL) # Raumlicht
|
||||
self.assertEqual(self.c._next_level(3552, None), HELL) # angeleuchtet
|
||||
|
||||
|
||||
class TestUpdate(unittest.TestCase):
|
||||
|
||||
def test_erste_aenderung_meldet_changed(self):
|
||||
c = BrightnessController()
|
||||
self.assertEqual(c.update(100), (0, 5, True))
|
||||
def test_erste_meldung_gilt_als_aenderung(self):
|
||||
c = controller()
|
||||
level, prozent, changed = c.update(100)
|
||||
self.assertEqual((level, prozent), (DUNKEL, 5))
|
||||
self.assertTrue(changed)
|
||||
|
||||
def test_gleicher_wert_keine_aenderung(self):
|
||||
c = BrightnessController()
|
||||
def test_gleicher_wert_meldet_keine_aenderung(self):
|
||||
c = controller()
|
||||
c.update(100)
|
||||
level, percent, changed = c.update(100)
|
||||
_, prozent, changed = c.update(100)
|
||||
self.assertFalse(changed)
|
||||
self.assertEqual(percent, 5)
|
||||
self.assertEqual(prozent, 5)
|
||||
|
||||
def test_dunkel_zu_hell_erreicht_100(self):
|
||||
c = BrightnessController()
|
||||
c.update(100)
|
||||
level = percent = None
|
||||
for _ in range(brightness.AVERAGE_WINDOW + 2):
|
||||
level, percent, _ = c.update(4000)
|
||||
self.assertEqual(level, 3)
|
||||
self.assertEqual(percent, 100)
|
||||
def test_dunkel_nach_hell_und_zurueck(self):
|
||||
c = controller()
|
||||
c.update(0)
|
||||
self.assertEqual(c.level, DUNKEL)
|
||||
for _ in range(brightness.AVERAGE_WINDOW):
|
||||
c.update(3500)
|
||||
self.assertEqual(c.level, HELL)
|
||||
self.assertEqual(c.percent(c.level), 50)
|
||||
for _ in range(brightness.AVERAGE_WINDOW):
|
||||
c.update(0)
|
||||
self.assertEqual(c.level, DUNKEL)
|
||||
|
||||
def test_gleitender_mittelwert_fenster(self):
|
||||
c = BrightnessController()
|
||||
def test_rauschen_um_die_schwelle_schaltet_nicht(self):
|
||||
"""Gemessen wurden rund +/-32 Streuung nach der Mittelung.
|
||||
|
||||
Genau dagegen ist die Hysterese da -- ohne sie wuerde die Anzeige an der
|
||||
Schwelle zwischen den beiden Helligkeiten springen.
|
||||
"""
|
||||
c = controller()
|
||||
for _ in range(brightness.AVERAGE_WINDOW):
|
||||
c.update(1800)
|
||||
stufe = c.level
|
||||
for delta in (+60, -60, +55, -50, +45, -60, +60, -55):
|
||||
neu, _, changed = c.update(1800 + delta)
|
||||
self.assertFalse(changed, "Rauschen hat umgeschaltet")
|
||||
self.assertEqual(neu, stufe)
|
||||
|
||||
def test_gleitender_mittelwert_begrenzt_das_fenster(self):
|
||||
c = controller()
|
||||
for _ in range(20):
|
||||
c.update(1000)
|
||||
self.assertLessEqual(len(c._buf), brightness.AVERAGE_WINDOW)
|
||||
|
||||
def test_mittelwert_daempft_einen_ausreisser(self):
|
||||
"""Ein einzelner heller Messwert darf die Anzeige nicht aufblitzen lassen."""
|
||||
c = controller()
|
||||
for _ in range(brightness.AVERAGE_WINDOW):
|
||||
c.update(0)
|
||||
_, _, changed = c.update(4095)
|
||||
self.assertFalse(changed)
|
||||
self.assertEqual(c.level, DUNKEL)
|
||||
|
||||
|
||||
class TestReload(unittest.TestCase):
|
||||
"""Geaenderte Einstellungen uebernehmen -- der Weg, den MQTT spaeter geht."""
|
||||
|
||||
def test_reload_liest_die_datei_neu(self):
|
||||
import os
|
||||
import settings
|
||||
pfad = "test_settings_tmp.json"
|
||||
try:
|
||||
settings.save({"schwelle": 1000, "hell_prozent": 90}, pfad)
|
||||
c = BrightnessController(pfad=pfad)
|
||||
self.assertEqual(c.settings["schwelle"], 1000)
|
||||
self.assertEqual(c.percent(HELL), 90)
|
||||
|
||||
settings.save({"schwelle": 3000, "hell_prozent": 20}, pfad)
|
||||
c.reload()
|
||||
self.assertEqual(c.settings["schwelle"], 3000)
|
||||
self.assertEqual(c.percent(HELL), 20)
|
||||
finally:
|
||||
try:
|
||||
os.remove(pfad)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
class TestRawToLdr(unittest.TestCase):
|
||||
|
||||
def test_invertiert_spiegelt_den_rohwert(self):
|
||||
self.assertEqual(brightness.raw_to_ldr(4095, inverted=True, full_scale=4095), 0)
|
||||
self.assertEqual(brightness.raw_to_ldr(0, inverted=True, full_scale=4095), 4095)
|
||||
|
||||
def test_nicht_invertiert_laesst_den_rohwert_stehen(self):
|
||||
self.assertEqual(brightness.raw_to_ldr(1234, inverted=False, full_scale=4095), 1234)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
||||
+11
-7
@@ -328,15 +328,19 @@ class TestLdrKennlinie(unittest.TestCase):
|
||||
self.assertEqual(brightness.raw_to_ldr(0),
|
||||
config.LDR_MAX if config.LDR_INVERTED else 0)
|
||||
|
||||
def test_helle_umgebung_ergibt_hohe_stufe(self):
|
||||
def test_helle_umgebung_ergibt_die_helle_stufe(self):
|
||||
"""Viel Licht -> niedrige ADC-Spannung -> nach Spiegelung hoher Wert."""
|
||||
import brightness
|
||||
# Feste Werte statt settings.json: der Test soll nicht davon abhaengen,
|
||||
# was auf einem Geraet gerade konfiguriert ist.
|
||||
werte = {"dunkel_prozent": 5, "hell_prozent": 50,
|
||||
"schwelle": 1800, "hysterese": 150}
|
||||
hell = brightness.raw_to_ldr(100, inverted=True, full_scale=4095)
|
||||
dunkel = brightness.raw_to_ldr(4000, inverted=True, full_scale=4095)
|
||||
c = brightness.BrightnessController()
|
||||
self.assertEqual(c.update(hell)[0], len(brightness.PERCENTS) - 1)
|
||||
c = brightness.BrightnessController()
|
||||
self.assertEqual(c.update(dunkel)[0], 0)
|
||||
c = brightness.BrightnessController(werte=werte)
|
||||
self.assertEqual(c.update(hell)[0], brightness.HELL)
|
||||
c = brightness.BrightnessController(werte=werte)
|
||||
self.assertEqual(c.update(dunkel)[0], brightness.DUNKEL)
|
||||
|
||||
|
||||
class TestLdrLeser(unittest.TestCase):
|
||||
@@ -434,8 +438,8 @@ class TestWifi(unittest.TestCase):
|
||||
|
||||
# Die Dateien, die auf dem Geraet landen -- muss zu deploy.sh passen.
|
||||
DEPLOYED = (
|
||||
"ticks.py", "migra.py", "config.py", "transport.py", "display.py",
|
||||
"coords.py", "lx200.py", "mount_client.py", "brightness.py",
|
||||
"ticks.py", "migra.py", "config.py", "settings.py", "transport.py",
|
||||
"display.py", "coords.py", "lx200.py", "mount_client.py", "brightness.py",
|
||||
"displayloop.py", "wifi.py", "run_esp32.py", "probe.py",
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests fuer die zur Laufzeit aenderbaren Einstellungen (settings.json).
|
||||
|
||||
python3 -m unittest -v test_settings
|
||||
|
||||
Schwerpunkt ist die Pruefung der Werte: Sobald MQTT dazukommt, kommen sie von
|
||||
aussen. Ein Helligkeitswert von 500 % oder eine kaputte Datei darf die Anzeige
|
||||
nicht lahmlegen.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import settings
|
||||
|
||||
|
||||
class Tempdatei(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.pfad = "test_settings_tmp.json"
|
||||
self._weg()
|
||||
|
||||
def tearDown(self):
|
||||
self._weg()
|
||||
|
||||
def _weg(self):
|
||||
try:
|
||||
os.remove(self.pfad)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _schreibe_roh(self, inhalt):
|
||||
with open(self.pfad, "w") as f:
|
||||
f.write(inhalt)
|
||||
|
||||
|
||||
class TestLaden(Tempdatei):
|
||||
|
||||
def test_ohne_datei_gelten_die_vorgaben(self):
|
||||
self.assertEqual(settings.load(self.pfad), settings.DEFAULTS)
|
||||
|
||||
def test_fehlende_werte_werden_ergaenzt(self):
|
||||
self._schreibe_roh(json.dumps({"schwelle": 1234}))
|
||||
w = settings.load(self.pfad)
|
||||
self.assertEqual(w["schwelle"], 1234)
|
||||
self.assertEqual(w["hell_prozent"], settings.DEFAULTS["hell_prozent"])
|
||||
|
||||
def test_kaputtes_json_faellt_auf_die_vorgaben_zurueck(self):
|
||||
"""Ein Geraet in der Sternwarte darf daran nicht haengenbleiben."""
|
||||
self._schreibe_roh("{das ist kein json")
|
||||
self.assertEqual(settings.load(self.pfad), settings.DEFAULTS)
|
||||
|
||||
def test_json_das_kein_objekt_ist(self):
|
||||
self._schreibe_roh("[1, 2, 3]")
|
||||
self.assertEqual(settings.load(self.pfad), settings.DEFAULTS)
|
||||
|
||||
def test_einzelner_unbrauchbarer_wert_verwirft_nur_diesen(self):
|
||||
self._schreibe_roh(json.dumps({"schwelle": 99999, "hell_prozent": 42}))
|
||||
meldungen = []
|
||||
w = settings.load(self.pfad, log=meldungen.append)
|
||||
self.assertEqual(w["schwelle"], settings.DEFAULTS["schwelle"]) # verworfen
|
||||
self.assertEqual(w["hell_prozent"], 42) # uebernommen
|
||||
self.assertTrue(any("schwelle" in m for m in meldungen))
|
||||
|
||||
def test_unbekannter_schluessel_wird_ignoriert(self):
|
||||
self._schreibe_roh(json.dumps({"voellig_unbekannt": 1}))
|
||||
w = settings.load(self.pfad)
|
||||
self.assertEqual(w, settings.DEFAULTS)
|
||||
self.assertNotIn("voellig_unbekannt", w)
|
||||
|
||||
|
||||
class TestSpeichern(Tempdatei):
|
||||
|
||||
def test_speichern_und_wieder_lesen(self):
|
||||
settings.save({"schwelle": 2000, "dunkel_prozent": 3}, self.pfad)
|
||||
w = settings.load(self.pfad)
|
||||
self.assertEqual(w["schwelle"], 2000)
|
||||
self.assertEqual(w["dunkel_prozent"], 3)
|
||||
|
||||
def test_gespeichert_wird_immer_vollstaendig(self):
|
||||
settings.save({"schwelle": 2000}, self.pfad)
|
||||
with open(self.pfad) as f:
|
||||
roh = json.load(f)
|
||||
self.assertEqual(set(roh), set(settings.DEFAULTS))
|
||||
|
||||
def test_update_aendert_nur_das_angegebene(self):
|
||||
settings.save({"schwelle": 1000, "hell_prozent": 30}, self.pfad)
|
||||
settings.update({"hell_prozent": 70}, self.pfad)
|
||||
w = settings.load(self.pfad)
|
||||
self.assertEqual(w["hell_prozent"], 70)
|
||||
self.assertEqual(w["schwelle"], 1000) # unveraendert
|
||||
|
||||
def test_ungueltiges_update_laesst_die_datei_unberuehrt(self):
|
||||
settings.save({"schwelle": 1000}, self.pfad)
|
||||
with self.assertRaises(settings.SettingsError):
|
||||
settings.update({"hell_prozent": 500}, self.pfad)
|
||||
self.assertEqual(settings.load(self.pfad)["schwelle"], 1000)
|
||||
|
||||
|
||||
class TestPruefung(unittest.TestCase):
|
||||
"""Die Grenzen, an denen spaeter MQTT-Eingaben haengenbleiben sollen."""
|
||||
|
||||
def test_prozent_ueber_100_wird_abgelehnt(self):
|
||||
with self.assertRaises(settings.SettingsError):
|
||||
settings.save({"hell_prozent": 101}, "/dev/null")
|
||||
|
||||
def test_negative_werte_werden_abgelehnt(self):
|
||||
with self.assertRaises(settings.SettingsError):
|
||||
settings.save({"schwelle": -1}, "/dev/null")
|
||||
|
||||
def test_schwelle_ueber_adc_bereich_wird_abgelehnt(self):
|
||||
with self.assertRaises(settings.SettingsError):
|
||||
settings.save({"schwelle": 4096}, "/dev/null")
|
||||
|
||||
def test_unbekannter_name_wird_abgelehnt(self):
|
||||
with self.assertRaises(settings.SettingsError):
|
||||
settings.save({"gibtsnicht": 1}, "/dev/null")
|
||||
|
||||
def test_nichtganzzahlige_werte_werden_abgelehnt(self):
|
||||
for wert in (1.5, "50", None, True):
|
||||
with self.assertRaises(settings.SettingsError):
|
||||
settings.save({"hell_prozent": wert}, "/dev/null")
|
||||
|
||||
def test_die_raender_sind_erlaubt(self):
|
||||
geprueft = settings._pruefe({"hell_prozent": 100, "dunkel_prozent": 0,
|
||||
"schwelle": 4095, "hysterese": 0})
|
||||
self.assertEqual(geprueft["hell_prozent"], 100)
|
||||
self.assertEqual(geprueft["schwelle"], 4095)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
Reference in New Issue
Block a user