Helligkeitsregelung (LDR -> Stufe/Prozent) ergaenzt
- brightness.py: BrightnessController + CLI, portiert aus DoDimmen_V/main.h (gleitender Mittelwert, Hysterese). Schwellen 300/2500 aus altem Code, Prozente 5/15/50/100 - verallgemeinert auf 4 Stufen: 100% jetzt erreichbar (alter Code max 50%); neue obere Grenze HELL_MAX=3500 (anpassbar), Rest wie im Original - run_display: --ldr bindet die Regelung in die Hauptschleife ein (sendet nur bei Stufenwechsel) - test_brightness.py: Schwellen, Hysterese, Mittelung - README dokumentiert die Regelung; .gitignore: Hardware/ Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,3 +8,6 @@ venv/
|
|||||||
|
|
||||||
# Editor-/OS-Kram
|
# Editor-/OS-Kram
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
#Hardware-Sachern nicht ist Git
|
||||||
|
Hardware/
|
||||||
@@ -21,9 +21,13 @@ python3 show_coords.py 12h34m56s +45d12m30s --dry-run # nur Telegramm als Hex
|
|||||||
python3 run_display.py # Montierung aus config.py pollen
|
python3 run_display.py # Montierung aus config.py pollen
|
||||||
python3 run_display.py --host 10.0.0.42 # andere IP
|
python3 run_display.py --host 10.0.0.42 # andere IP
|
||||||
python3 run_display.py --mock --move # ohne Teleskop: lokaler Mock, RA läuft
|
python3 run_display.py --mock --move # ohne Teleskop: lokaler Mock, RA läuft
|
||||||
|
python3 run_display.py --mock --move --ldr 2000 # zusätzlich mit Helligkeitsregelung
|
||||||
python3 mount_mock.py --move # Mock separat starten
|
python3 mount_mock.py --move # Mock separat starten
|
||||||
|
|
||||||
python3 -m unittest test_migra test_lx200 # alle Tests
|
# Helligkeit aus einem LDR-Wert setzen:
|
||||||
|
python3 brightness.py --ldr 1500 # LDR-Rohwert -> Stufe/Prozent -> senden
|
||||||
|
|
||||||
|
python3 -m unittest test_migra test_lx200 test_brightness # alle Tests
|
||||||
python3 probe.py ruler # Geometrie der Anzeige ausmessen
|
python3 probe.py ruler # Geometrie der Anzeige ausmessen
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -38,9 +42,13 @@ Einzige Abhängigkeit auf dem Mac ist `pyserial`.
|
|||||||
| `display.py` | Treiber: Telegramm senden, Antwort auswerten. |
|
| `display.py` | Treiber: Telegramm senden, Antwort auswerten. |
|
||||||
| `coords.py` | Koordinaten einlesen und auf die Zeilenbreite formatieren. |
|
| `coords.py` | Koordinaten einlesen und auf die Zeilenbreite formatieren. |
|
||||||
| `config.py` | Port, Adresse, Geometrie, Helligkeit — alles an einer Stelle. |
|
| `config.py` | Port, Adresse, Geometrie, Helligkeit — alles an einer Stelle. |
|
||||||
| `show_coords.py` | Das Testprogramm. |
|
| `show_coords.py` | Das Testprogramm (Koordinaten von Hand). |
|
||||||
|
| `brightness.py` | Helligkeitsregelung: LDR-Wert → Stufe/Prozent (portabel + CLI). |
|
||||||
|
| `run_display.py` | Hauptschleife Phase 2: Montierung pollen, anzeigen, Helligkeit regeln. |
|
||||||
|
| `mount_client.py` / `mount_mock.py` | TCP-Client zur GM4000 bzw. Simulator (LX200). |
|
||||||
|
| `lx200.py` | LX200-Kommandorahmen. |
|
||||||
| `probe.py` | Testmuster zum Ausmessen von Zeilenbreite und Zeilenposition. |
|
| `probe.py` | Testmuster zum Ausmessen von Zeilenbreite und Zeilenposition. |
|
||||||
| `test_migra.py` | Tests, u. a. gegen die Beispieltelegramme aus dem Handbuch. |
|
| `test_migra.py` / `test_lx200.py` / `test_brightness.py` | Tests. |
|
||||||
|
|
||||||
## Protokoll (Kurzfassung)
|
## Protokoll (Kurzfassung)
|
||||||
|
|
||||||
@@ -119,6 +127,30 @@ diese Zelle überschreibt.
|
|||||||
`CHARS_PER_LINE` aufgefüllt werden, sonst bricht das Füll-Leerzeichen um und
|
`CHARS_PER_LINE` aufgefüllt werden, sonst bricht das Füll-Leerzeichen um und
|
||||||
beschädigt die andere Zeile.
|
beschädigt die andere Zeile.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
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 |
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Der LDR-Wert kommt derzeit per Kommandozeile (`--ldr`); auf dem ESP32-C3 liefert
|
||||||
|
ihn später der ADC. `run_display.py --ldr <wert>` bindet die Regelung in die
|
||||||
|
Hauptschleife ein (`BrightnessController`).
|
||||||
|
|
||||||
## Antwortleitung (RX)
|
## Antwortleitung (RX)
|
||||||
|
|
||||||
Der Rückkanal ist verdrahtet und verifiziert (2026-07-16): Anzeige Pin 3 (TxD) →
|
Der Rückkanal ist verdrahtet und verifiziert (2026-07-16): Anzeige Pin 3 (TxD) →
|
||||||
|
|||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
#!/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.
|
||||||
|
|
||||||
|
Reines Python, laeuft auch unter MicroPython (dort spaeter vom ADC gespeist).
|
||||||
|
|
||||||
|
python3 brightness.py --ldr 1500 # Stufe/Prozent berechnen und senden
|
||||||
|
python3 brightness.py --ldr 200 --dry-run
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# -- 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
|
||||||
|
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.
|
||||||
|
_CENTERS = (HELL_LOW, HELL_HIGH, HELL_MAX)
|
||||||
|
_HYSTS = (HELL_HYST, HELL_HYST * 5, HELL_HYST * 5)
|
||||||
|
|
||||||
|
|
||||||
|
class BrightnessController:
|
||||||
|
"""Rechnet aus LDR-Rohwerten die Anzeigehelligkeit -- wie DoDimmen_V.
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._buf = []
|
||||||
|
self._level = None # entspricht oldHellStufe_ub = 0xFF (>=2-Zweig)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def level(self):
|
||||||
|
return self._level
|
||||||
|
|
||||||
|
def _next_level(self, val, old):
|
||||||
|
"""Stufe mit Hysterese bestimmen (verallgemeinert auf beliebig viele Stufen).
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
def update(self, ldr):
|
||||||
|
"""Einen LDR-Rohwert einspeisen. Liefert (stufe, prozent, geaendert)."""
|
||||||
|
self._buf.append(ldr)
|
||||||
|
if len(self._buf) > AVERAGE_WINDOW:
|
||||||
|
self._buf.pop(0)
|
||||||
|
avg = sum(self._buf) // len(self._buf)
|
||||||
|
|
||||||
|
level = self._next_level(avg, self._level)
|
||||||
|
changed = level != self._level
|
||||||
|
self._level = level
|
||||||
|
return level, PERCENTS[level], changed
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None):
|
||||||
|
p = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
p.add_argument("--ldr", type=int, required=True, help="LDR-Rohwert (0..4095)")
|
||||||
|
p.add_argument("--port", default=None, help="serieller Port (Vorgabe aus config.py)")
|
||||||
|
p.add_argument("--dry-run", action="store_true", help="nur berechnen/anzeigen, nicht senden")
|
||||||
|
args = p.parse_args(argv)
|
||||||
|
|
||||||
|
level, percent, _ = BrightnessController().update(args.ldr)
|
||||||
|
print("LDR %d -> Stufe %d -> %d %%" % (args.ldr, level, percent))
|
||||||
|
|
||||||
|
import config
|
||||||
|
import migra
|
||||||
|
from display import open_display
|
||||||
|
d = open_display(port=args.port or config.PORT, fake=args.dry_run)
|
||||||
|
try:
|
||||||
|
d.set_brightness(percent)
|
||||||
|
except migra.MigraError as e:
|
||||||
|
print("Fehler: %s" % e, file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
finally:
|
||||||
|
d.close()
|
||||||
|
print("OK")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -18,6 +18,7 @@ import time
|
|||||||
import config
|
import config
|
||||||
import coords
|
import coords
|
||||||
import migra
|
import migra
|
||||||
|
from brightness import BrightnessController
|
||||||
from display import open_display
|
from display import open_display
|
||||||
from mount_client import MountClient, MountError
|
from mount_client import MountClient, MountError
|
||||||
|
|
||||||
@@ -40,6 +41,8 @@ def main(argv=None):
|
|||||||
help="nur mit --mock: RA laeuft mit der Zeit")
|
help="nur mit --mock: RA laeuft mit der Zeit")
|
||||||
p.add_argument("--dry-run", action="store_true",
|
p.add_argument("--dry-run", action="store_true",
|
||||||
help="Anzeige nur als Hex mitschreiben (keine serielle Hardware)")
|
help="Anzeige nur als Hex mitschreiben (keine serielle Hardware)")
|
||||||
|
p.add_argument("--ldr", type=int, default=None,
|
||||||
|
help="LDR-Rohwert fuer die Helligkeitsregelung (spaeter vom ADC)")
|
||||||
args = p.parse_args(argv)
|
args = p.parse_args(argv)
|
||||||
|
|
||||||
host, port = args.host, args.port
|
host, port = args.host, args.port
|
||||||
@@ -55,11 +58,24 @@ def main(argv=None):
|
|||||||
client = MountClient(host, port, timeout=config.MOUNT_TIMEOUT)
|
client = MountClient(host, port, timeout=config.MOUNT_TIMEOUT)
|
||||||
last_shown = None
|
last_shown = None
|
||||||
|
|
||||||
|
# Helligkeitsregelung: derzeit ein fester LDR-Wert von der Kommandozeile;
|
||||||
|
# spaeter (ESP32) wird hier pro Durchlauf der ADC gelesen.
|
||||||
|
dimmer = BrightnessController() if args.ldr is not None else None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
if not client.connected:
|
if not client.connected:
|
||||||
client.connect()
|
client.connect()
|
||||||
|
|
||||||
|
# Helligkeit nachfuehren (nur senden, wenn sich die Stufe aendert)
|
||||||
|
if dimmer is not None:
|
||||||
|
level, percent, changed = dimmer.update(args.ldr)
|
||||||
|
if changed:
|
||||||
|
d.set_brightness(percent)
|
||||||
|
print("LDR %d -> Helligkeit Stufe %d = %d %%"
|
||||||
|
% (args.ldr, level, percent))
|
||||||
|
|
||||||
ra_raw, dec_raw = client.get_coordinates()
|
ra_raw, dec_raw = client.get_coordinates()
|
||||||
ra_line, dec_line = _format(ra_raw, dec_raw)
|
ra_line, dec_line = _format(ra_raw, dec_raw)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Tests fuer die Helligkeitsregelung (verallgemeinert aus DoDimmen_V).
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import brightness
|
||||||
|
from brightness import BrightnessController
|
||||||
|
|
||||||
|
|
||||||
|
class TestStufen(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def test_prozente(self):
|
||||||
|
self.assertEqual(brightness.PERCENTS, (5, 15, 50, 100))
|
||||||
|
|
||||||
|
|
||||||
|
class TestUpdate(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_erste_aenderung_meldet_changed(self):
|
||||||
|
c = BrightnessController()
|
||||||
|
self.assertEqual(c.update(100), (0, 5, True))
|
||||||
|
|
||||||
|
def test_gleicher_wert_keine_aenderung(self):
|
||||||
|
c = BrightnessController()
|
||||||
|
c.update(100)
|
||||||
|
level, percent, changed = c.update(100)
|
||||||
|
self.assertFalse(changed)
|
||||||
|
self.assertEqual(percent, 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_gleitender_mittelwert_fenster(self):
|
||||||
|
c = BrightnessController()
|
||||||
|
for _ in range(20):
|
||||||
|
c.update(1000)
|
||||||
|
self.assertLessEqual(len(c._buf), brightness.AVERAGE_WINDOW)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main(verbosity=2)
|
||||||
Reference in New Issue
Block a user