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
- **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, 04095: 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, 04095) 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 <ppp>`), 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 <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 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)
+38 -14
View File
@@ -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):
+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
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.
+66 -18
View File
@@ -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):