LDR-Schwellen am realen Aufbau gemessen statt vom MSP430 uebernommen

Gemessen am XIAO ESP32-C3 mit dem LDR an GPIO2, je 200 Werte, Median
(Angaben nach raw_to_ldr, also in Zaehlrichtung "gross = hell"):

    abgedeckt      roh 4095 (ADC-Anschlag)  ->    0
    Raumlicht      roh 1642                 -> 2453
    angeleuchtet   roh  543                 -> 3552

Zwei Fehler in den alten Werten, die erst die Messung zeigt:

- Stufe 3 (100 %) war unerreichbar. Sie begann bei 3650, selbst eine Taschenlampe
  direkt auf dem Sensor kam nur auf 3552. Die Anzeige haette nie voll aufgedreht.
- Die Hysterese von 30 am untersten Uebergang lag unter dem Rauschen. Der Rohwert
  streut bei Raumlicht um +/-270, nach dem gleitenden Mittel bleiben rund +/-32
  (1 Sigma) -- die unterste Stufe haette geflattert.

Neu: Mitten 1200/2400/3200, Hysterese einheitlich 150. Die Aufteilung haelt den
5-%-Bereich bis 1200 (Nutzerwunsch "nachts laenger dunkel", schont die
Dunkeladaption der Beobachter). Die gestaffelten Hysteresen des alten Programms
entfallen -- die gemessene Streuung ist an allen Grenzen dieselbe.

Bekannte Grenzlage, in test_brightness festgehalten: Raumlicht (2453) liegt nur
53 Zaehler ueber HELL_HIGH und damit innerhalb der Hysterese, faellt also auf
Stufe 1 statt 2. Ob das so gewollt ist, entscheidet sich in der Sternwarte.

Am Geraet bestaetigt: Regelung laeuft stabil ohne Flattern, Abdecken schaltet
herunter, Freigeben wieder hoch, Helligkeitstelegramm wird quittiert. Die
Schwellen sind aber am Schreibtisch gemessen -- with_ldr bleibt deshalb auf
False, bis sie vor Ort bestaetigt sind. 101 Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-27 16:21:08 +02:00
parent 4f7375add3
commit e8a5b5d5e3
4 changed files with 161 additions and 52 deletions
+53 -17
View File
@@ -247,9 +247,10 @@ Mac:
## Offene Punkte ## Offene Punkte
- **LDR-Schwellen nachmessen.** Die Grenzen in `brightness.py` stammen vom alten - **LDR-Schwellen in der Sternwarte gegenprüfen.** Die Grenzen sind am realen
MSP430-Aufbau. Sobald der LDR am ESP32 hängt, die Rohwerte bei Tag und Nacht Aufbau gemessen (2026-07-27, siehe *Helligkeitsregelung*), aber am Schreibtisch.
ablesen und `HELL_LOW`/`HELL_HIGH`/`HELL_MAX` anpassen. Bis dahin läuft die 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`). Anzeige mit fester Helligkeit (`run_esp32.main()` ohne `with_ldr`).
- **Netzwerk zur echten Montierung.** Die GM4000 steht in `192.168.1.115`, das - **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 Heimnetz des ESP32 ist `192.168.178.x` — der Mac erreicht sie per VPN, der
@@ -342,23 +343,57 @@ zerlegt (Trennzeichen `:`, `*`, `0xDF`, `'` sind alle egal).
## Helligkeitsregelung ## Helligkeitsregelung
Portiert aus dem alten MSP430-Programm (`DoDimmen_V`, Werte aus `main.h`). Ein Portiert aus dem alten MSP430-Programm (`DoDimmen_V`). Ein LDR-Rohwert
LDR-Rohwert (12-Bit-ADC, 04095: je heller die Umgebung, desto höher der Wert) (12-Bit-ADC, 04095) wird über `raw_to_ldr` in die Zählrichtung „groß = hell"
wird über einen gleitenden Mittelwert (8 Werte) geglättet und auf eine gebracht, über einen gleitenden Mittelwert (8 Werte) geglättet und auf eine
Helligkeitsstufe mit Hysterese abgebildet: Helligkeitsstufe mit Hysterese abgebildet:
| Stufe | Helligkeit | erreicht ab (hoch / runter) | | Stufe | Helligkeit | erreicht ab (hoch / runter) |
|------|-----------|------------------------------| |------|-----------|------------------------------|
| 0 | 5 % | < 270 | | 0 | 5 % | < 1050 |
| 1 | 15 % | ≥ 330 / < 270 | | 1 | 15 % | ≥ 1350 / < 1050 |
| 2 | 50 % | ≥ 2650 / < 2350 | | 2 | 50 % | ≥ 2550 / < 2250 |
| 3 | 100 % | ≥ 3650 / < 3350 | | 3 | 100 % | ≥ 3350 / < 3050 |
Schwellen-Mitten `300 / 2500 / 3500`, Hysterese `±30 / ±150 / ±150`. **300 und 2500 Schwellen-Mitten `1200 / 2400 / 3200`, Hysterese einheitlich `±150`.
stammen aus dem alten Code**; die obere Grenze `HELL_MAX = 3500` für 100 % ist neu
(der alte Code hatte keine 4. Stufe) und in `brightness.py` anpassbar, sobald der **Am realen Aufbau gemessen (2026-07-27)**, XIAO ESP32-C3 mit LDR an GPIO2,
reale LDR-Wertebereich feststeht. Ein Helligkeitstelegramm wird nur bei je 200 Messwerte, Median — Werte nach `raw_to_ldr`:
**Stufenwechsel** gesendet (`ESC H 2 <ppp>`), wie im Original.
| 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 % |
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).
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.
**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
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,51 MΩ im Dunkeln) liefe das auf 160330 kΩ hinaus — dann
lägen Tag und Raumlicht aber bei ADC-Werten unter 15 und damit in der Totzone. Der
LDR überstreicht rund Faktor 4500; ein einzelner Teiler bildet davon nur etwa zwei
Dekaden ab. **1022 kΩ** wäre der Kompromiss, wenn mehr Nachtauflösung gewünscht ist.
Auf dem Mac kommt der LDR-Wert per Kommandozeile (`run_display.py --ldr <wert>`), Auf dem Mac kommt der LDR-Wert per Kommandozeile (`run_display.py --ldr <wert>`),
auf dem ESP32-C3 vom ADC an GPIO2 (`run_esp32.main(with_ldr=True)`). auf dem ESP32-C3 vom ADC an GPIO2 (`run_esp32.main(with_ldr=True)`).
@@ -371,8 +406,9 @@ wird deshalb gespiegelt: `config.LDR_INVERTED = True`, angewendet in
`brightness.raw_to_ldr`. Wer die Hardware lieber umbaut (LDR nach 3V3, R2 nach `brightness.raw_to_ldr`. Wer die Hardware lieber umbaut (LDR nach 3V3, R2 nach
GND), setzt das Flag auf `False`. GND), setzt das Flag auf `False`.
Die Schwellen selbst sind noch die alten und **müssen am realen Aufbau Die Schwellen sind am Aufbau gemessen, aber **am Schreibtisch, nicht in der
nachgemessen werden**, siehe Offene Punkte. Sternwarte**. Vor dem Echteinsatz dort bei Tag und Nacht gegenprüfen — deshalb
bleibt `with_ldr` in `run_esp32.main` vorerst auf `False`.
## Antwortleitung (RX) ## Antwortleitung (RX)
+38 -14
View File
@@ -1,11 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Helligkeitsregelung anhand eines LDR-Werts. """Helligkeitsregelung anhand eines LDR-Werts.
Portierung von DoDimmen_V aus dem alten MSP430-Programm (GrossAnzeige/main.c, Portierung von DoDimmen_V aus dem alten MSP430-Programm (GrossAnzeige/main.c):
Konstanten aus main.h): gleitender Mittelwert, 3 Helligkeitsstufen mit Hysterese, gleitender Mittelwert, Helligkeitsstufen mit Hysterese, Prozentwerte aus
Prozentwerte aus hellTafel_ub. Der Fotowiderstand (LDR) liefert einen Rohwert hellTafel_ub. Das alte Programm hatte 3 Stufen (bis 50 %), hier sind es 4 mit
(alte Hardware: 12-bit-ADC, 0..4095) -- je heller die Umgebung, desto hoeher der 100 % als oberster.
Wert und desto heller die Anzeige.
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.
Reines Python, laeuft auch unter MicroPython (dort spaeter vom ADC gespeist). Reines Python, laeuft auch unter MicroPython (dort spaeter vom ADC gespeist).
@@ -17,22 +22,41 @@ Reines Python, laeuft auch unter MicroPython (dort spaeter vom ADC gespeist).
# CLI-Teil ganz unten gebraucht und darum erst dort importiert. So laesst sich # CLI-Teil ganz unten gebraucht und darum erst dort importiert. So laesst sich
# dieses Modul auf dem ESP32 einfach importieren. # dieses Modul auf dem ESP32 einfach importieren.
# -- Werte aus dem alten Programm (main.h) -------------------------------- # -- Stufengrenzen, am realen Aufbau gemessen (2026-07-27) ----------------
HELL_LOW = 300 # Stufengrenze 0<->1 (aus dem alten Code) # Gemessen wurde am XIAO ESP32-C3 mit dem LDR an GPIO2 (200 Messwerte je Lage,
HELL_HIGH = 2500 # Stufengrenze 1<->2 (aus dem alten Code) # Median), Werte nach raw_to_ldr, also schon in Zaehlrichtung "gross = hell":
HELL_MAX = 3500 # Stufengrenze 2<->3: NEU, nicht aus dem alten Code. #
# Anpassen, sobald der reale LDR-Wertebereich bekannt ist. # abgedeckt roh 4095 (ADC-Anschlag) -> 0
HELL_HYST = 30 # Basis-Hysterese # 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 AVERAGE_WINDOW = 8 # MAXHELLARRAY: gleitender Mittelwert ueber 8 Werte
# Helligkeits-Prozente je Stufe, aus hellTafel_ub (main.h): 5/15/50/100 %. # Helligkeits-Prozente je Stufe, aus hellTafel_ub (main.h): 5/15/50/100 %.
PERCENTS = (5, 15, 50, 100) PERCENTS = (5, 15, 50, 100)
# Mittelpunkte der Stufengrenzen und deren Hysterese (Stufe i <-> i+1). # Mittelpunkte der Stufengrenzen und deren Hysterese (Stufe i <-> i+1).
# Der alte Code nutzte am unteren Uebergang +/-HYST, am oberen +/-5*HYST; das # Jetzt einheitlich: die gemessene Streuung ist an allen Grenzen dieselbe, es
# uebernehmen wir und ergaenzen die neue obere Grenze mit derselben Breite. # gibt keinen Grund mehr fuer die gestaffelten Breiten des alten Programms.
_CENTERS = (HELL_LOW, HELL_HIGH, HELL_MAX) _CENTERS = (HELL_LOW, HELL_HIGH, HELL_MAX)
_HYSTS = (HELL_HYST, HELL_HYST * 5, HELL_HYST * 5) _HYSTS = (HELL_HYST, HELL_HYST, HELL_HYST)
def raw_to_ldr(raw, inverted=None, full_scale=None): def raw_to_ldr(raw, inverted=None, full_scale=None):
+4 -3
View File
@@ -101,9 +101,10 @@ def main(host=None, port=None, with_ldr=False, fake_display=False,
host/port Montierung; Vorgabe aus config.py. Zum Testen gegen den Mock host/port Montierung; Vorgabe aus config.py. Zum Testen gegen den Mock
auf dem Mac dessen LAN-Adresse angeben. auf dem Mac dessen LAN-Adresse angeben.
with_ldr Helligkeitsregelung ueber den ADC dazuschalten. Vorgabe aus, with_ldr Helligkeitsregelung ueber den ADC dazuschalten. Die Schwellen
solange die Schwellen am realen LDR nicht nachgemessen sind sind am realen LDR gemessen (2026-07-27, siehe brightness.py),
(siehe README, Abschnitt Helligkeitsregelung). aber am Schreibtisch, nicht in der Sternwarte -- deshalb bleibt
die Vorgabe aus, bis sie vor Ort bestaetigt sind.
fake_display Anzeige nur als Hex in die REPL schreiben, statt ueber den fake_display Anzeige nur als Hex in die REPL schreiben, statt ueber den
UART zu senden -- fuer den Test ohne Pegelwandler und Anzeige. UART zu senden -- fuer den Test ohne Pegelwandler und Anzeige.
once nur ein Durchlauf, dann zurueck. once nur ein Durchlauf, dann zurueck.
+66 -18
View File
@@ -3,8 +3,13 @@
python3 -m unittest -v test_brightness python3 -m unittest -v test_brightness
Grenzen: Mitten (300, 2500, 3500) mit Hysterese (30, 150, 150) Grenzen am Geraet gemessen (2026-07-27): Mitten (1200, 2400, 3200) mit
-> hoch bei 330 / 2650 / 3650, runter bei 270 / 2350 / 3350. 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.
""" """
import unittest import unittest
@@ -19,32 +24,75 @@ class TestStufen(unittest.TestCase):
self.c = BrightnessController() self.c = BrightnessController()
def test_erststart_schaltet_von_0_hoch(self): def test_erststart_schaltet_von_0_hoch(self):
self.assertEqual(self.c._next_level(329, None), 0) self.assertEqual(self.c._next_level(1349, None), 0)
self.assertEqual(self.c._next_level(330, None), 1) self.assertEqual(self.c._next_level(1350, None), 1)
self.assertEqual(self.c._next_level(2649, None), 1) self.assertEqual(self.c._next_level(2549, None), 1)
self.assertEqual(self.c._next_level(2650, None), 2) self.assertEqual(self.c._next_level(2550, None), 2)
self.assertEqual(self.c._next_level(3649, None), 2) self.assertEqual(self.c._next_level(3349, None), 2)
self.assertEqual(self.c._next_level(3650, None), 3) # 100 % erreichbar self.assertEqual(self.c._next_level(3350, None), 3) # 100 % erreichbar
def test_hysterese_von_stufe_1(self): def test_hysterese_von_stufe_1(self):
self.assertEqual(self.c._next_level(270, 1), 1) self.assertEqual(self.c._next_level(1050, 1), 1)
self.assertEqual(self.c._next_level(269, 1), 0) self.assertEqual(self.c._next_level(1049, 1), 0)
self.assertEqual(self.c._next_level(2649, 1), 1) self.assertEqual(self.c._next_level(2549, 1), 1)
self.assertEqual(self.c._next_level(2650, 1), 2) self.assertEqual(self.c._next_level(2550, 1), 2)
def test_hysterese_von_stufe_2(self): def test_hysterese_von_stufe_2(self):
self.assertEqual(self.c._next_level(2350, 2), 2) self.assertEqual(self.c._next_level(2250, 2), 2)
self.assertEqual(self.c._next_level(2349, 2), 1) self.assertEqual(self.c._next_level(2249, 2), 1)
self.assertEqual(self.c._next_level(3649, 2), 2) self.assertEqual(self.c._next_level(3349, 2), 2)
self.assertEqual(self.c._next_level(3650, 2), 3) self.assertEqual(self.c._next_level(3350, 2), 3)
def test_hysterese_von_stufe_3(self): def test_hysterese_von_stufe_3(self):
self.assertEqual(self.c._next_level(3350, 3), 3) self.assertEqual(self.c._next_level(3050, 3), 3)
self.assertEqual(self.c._next_level(3349, 3), 2) self.assertEqual(self.c._next_level(3049, 3), 2)
def test_prozente(self): def test_prozente(self):
self.assertEqual(brightness.PERCENTS, (5, 15, 50, 100)) 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).
"""
self.assertEqual(self.c._next_level(0, None), 0) # abgedeckt -> 5 %
self.assertEqual(self.c._next_level(3552, None), 3) # Lampe -> 100 %
def test_raumlicht_liegt_dicht_an_der_grenze(self):
"""Bekannte Grenzlage, absichtlich festgehalten.
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_hysterese_ueberdeckt_das_gemessene_rauschen(self):
"""Nach dem gleitenden Mittel bleiben rund 32 Zaehler Streuung (1 Sigma).
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_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)
class TestUpdate(unittest.TestCase): class TestUpdate(unittest.TestCase):