From e8a5b5d5e3869345335b139d046db91e5fb457ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reinhard=20X=2E=20F=C3=BCrst?= Date: Mon, 27 Jul 2026 16:21:08 +0200 Subject: [PATCH] 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) --- README.md | 70 ++++++++++++++++++++++++++++---------- brightness.py | 52 ++++++++++++++++++++-------- run_esp32.py | 7 ++-- test_brightness.py | 84 ++++++++++++++++++++++++++++++++++++---------- 4 files changed, 161 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 00e4e51..a1b6e3f 100644 --- a/README.md +++ b/README.md @@ -247,9 +247,10 @@ Mac: ## Offene Punkte -- **LDR-Schwellen nachmessen.** Die Grenzen in `brightness.py` stammen vom alten - MSP430-Aufbau. Sobald der LDR am ESP32 hängt, die Rohwerte bei Tag und Nacht - ablesen und `HELL_LOW`/`HELL_HIGH`/`HELL_MAX` anpassen. Bis dahin läuft die +- **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`). - **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 @@ -342,23 +343,57 @@ zerlegt (Trennzeichen `:`, `*`, `0xDF`, `'` sind alle egal). ## Helligkeitsregelung -Portiert aus dem alten MSP430-Programm (`DoDimmen_V`, Werte aus `main.h`). Ein -LDR-Rohwert (12-Bit-ADC, 0–4095: je heller die Umgebung, desto höher der Wert) -wird über einen gleitenden Mittelwert (8 Werte) geglättet und auf eine +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: | Stufe | Helligkeit | erreicht ab (hoch / runter) | |------|-----------|------------------------------| -| 0 | 5 % | < 270 | -| 1 | 15 % | ≥ 330 / < 270 | -| 2 | 50 % | ≥ 2650 / < 2350 | -| 3 | 100 % | ≥ 3650 / < 3350 | +| 0 | 5 % | < 1050 | +| 1 | 15 % | ≥ 1350 / < 1050 | +| 2 | 50 % | ≥ 2550 / < 2250 | +| 3 | 100 % | ≥ 3350 / < 3050 | -Schwellen-Mitten `300 / 2500 / 3500`, Hysterese `±30 / ±150 / ±150`. **300 und 2500 -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 -reale LDR-Wertebereich feststeht. Ein Helligkeitstelegramm wird nur bei -**Stufenwechsel** gesendet (`ESC H 2 `), wie im Original. +Schwellen-Mitten `1200 / 2400 / 3200`, Hysterese einheitlich `±150`. + +**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 % | + +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 `), +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,5–1 MΩ im Dunkeln) liefe das auf 160–330 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. **10–22 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 `), 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 GND), setzt das Flag auf `False`. -Die Schwellen selbst sind noch die alten und **müssen am realen Aufbau -nachgemessen werden**, siehe Offene Punkte. +Die Schwellen sind am Aufbau gemessen, aber **am Schreibtisch, nicht in der +Sternwarte**. Vor dem Echteinsatz dort bei Tag und Nacht gegenprüfen — deshalb +bleibt `with_ldr` in `run_esp32.main` vorerst auf `False`. ## Antwortleitung (RX) diff --git a/brightness.py b/brightness.py index 203863a..d518744 100644 --- a/brightness.py +++ b/brightness.py @@ -1,11 +1,16 @@ #!/usr/bin/env python3 """Helligkeitsregelung anhand eines LDR-Werts. -Portierung von DoDimmen_V aus dem alten MSP430-Programm (GrossAnzeige/main.c, -Konstanten aus main.h): gleitender Mittelwert, 3 Helligkeitsstufen mit Hysterese, -Prozentwerte aus hellTafel_ub. Der Fotowiderstand (LDR) liefert einen Rohwert -(alte Hardware: 12-bit-ADC, 0..4095) -- je heller die Umgebung, desto hoeher der -Wert und desto heller die Anzeige. +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. + +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). @@ -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 # dieses Modul auf dem ESP32 einfach importieren. -# -- Werte aus dem alten Programm (main.h) -------------------------------- -HELL_LOW = 300 # Stufengrenze 0<->1 (aus dem alten Code) -HELL_HIGH = 2500 # Stufengrenze 1<->2 (aus dem alten Code) -HELL_MAX = 3500 # Stufengrenze 2<->3: NEU, nicht aus dem alten Code. - # Anpassen, sobald der reale LDR-Wertebereich bekannt ist. -HELL_HYST = 30 # Basis-Hysterese +# -- 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). -# Der alte Code nutzte am unteren Uebergang +/-HYST, am oberen +/-5*HYST; das -# uebernehmen wir und ergaenzen die neue obere Grenze mit derselben Breite. +# 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 * 5, HELL_HYST * 5) +_HYSTS = (HELL_HYST, HELL_HYST, HELL_HYST) def raw_to_ldr(raw, inverted=None, full_scale=None): diff --git a/run_esp32.py b/run_esp32.py index 47026cd..b1638eb 100644 --- a/run_esp32.py +++ b/run_esp32.py @@ -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 auf dem Mac dessen LAN-Adresse angeben. - with_ldr Helligkeitsregelung ueber den ADC dazuschalten. Vorgabe aus, - solange die Schwellen am realen LDR nicht nachgemessen sind - (siehe README, Abschnitt Helligkeitsregelung). + with_ldr Helligkeitsregelung ueber den ADC dazuschalten. Die Schwellen + sind am realen LDR gemessen (2026-07-27, siehe brightness.py), + 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 UART zu senden -- fuer den Test ohne Pegelwandler und Anzeige. once nur ein Durchlauf, dann zurueck. diff --git a/test_brightness.py b/test_brightness.py index ca3b590..2aed7b3 100644 --- a/test_brightness.py +++ b/test_brightness.py @@ -3,8 +3,13 @@ python3 -m unittest -v test_brightness -Grenzen: Mitten (300, 2500, 3500) mit Hysterese (30, 150, 150) - -> hoch bei 330 / 2650 / 3650, runter bei 270 / 2350 / 3350. +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. """ import unittest @@ -19,32 +24,75 @@ class TestStufen(unittest.TestCase): self.c = BrightnessController() def test_erststart_schaltet_von_0_hoch(self): - self.assertEqual(self.c._next_level(329, None), 0) - self.assertEqual(self.c._next_level(330, None), 1) - self.assertEqual(self.c._next_level(2649, None), 1) - self.assertEqual(self.c._next_level(2650, None), 2) - self.assertEqual(self.c._next_level(3649, None), 2) - self.assertEqual(self.c._next_level(3650, None), 3) # 100 % erreichbar + 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_hysterese_von_stufe_1(self): - self.assertEqual(self.c._next_level(270, 1), 1) - self.assertEqual(self.c._next_level(269, 1), 0) - self.assertEqual(self.c._next_level(2649, 1), 1) - self.assertEqual(self.c._next_level(2650, 1), 2) + 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(2350, 2), 2) - self.assertEqual(self.c._next_level(2349, 2), 1) - self.assertEqual(self.c._next_level(3649, 2), 2) - self.assertEqual(self.c._next_level(3650, 2), 3) + 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(3350, 3), 3) - self.assertEqual(self.c._next_level(3349, 3), 2) + 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). + """ + 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):