diff --git a/.gitignore b/.gitignore index 5a4ff3e..31ca500 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ venv/ # Editor-/OS-Kram .DS_Store + +#Hardware-Sachern nicht ist Git +Hardware/ \ No newline at end of file diff --git a/README.md b/README.md index 2738469..72eac56 100644 --- a/README.md +++ b/README.md @@ -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 --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 --ldr 2000 # zusätzlich mit Helligkeitsregelung 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 ``` @@ -38,9 +42,13 @@ Einzige Abhängigkeit auf dem Mac ist `pyserial`. | `display.py` | Treiber: Telegramm senden, Antwort auswerten. | | `coords.py` | Koordinaten einlesen und auf die Zeilenbreite formatieren. | | `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. | -| `test_migra.py` | Tests, u. a. gegen die Beispieltelegramme aus dem Handbuch. | +| `test_migra.py` / `test_lx200.py` / `test_brightness.py` | Tests. | ## Protokoll (Kurzfassung) @@ -119,6 +127,30 @@ diese Zelle überschreibt. `CHARS_PER_LINE` aufgefüllt werden, sonst bricht das Füll-Leerzeichen um und 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 `), 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 ` bindet die Regelung in die +Hauptschleife ein (`BrightnessController`). + ## Antwortleitung (RX) Der Rückkanal ist verdrahtet und verifiziert (2026-07-16): Anzeige Pin 3 (TxD) → diff --git a/brightness.py b/brightness.py new file mode 100644 index 0000000..758a214 --- /dev/null +++ b/brightness.py @@ -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()) diff --git a/run_display.py b/run_display.py index 3acd8de..eb37ef5 100644 --- a/run_display.py +++ b/run_display.py @@ -18,6 +18,7 @@ import time import config import coords import migra +from brightness import BrightnessController from display import open_display from mount_client import MountClient, MountError @@ -40,6 +41,8 @@ def main(argv=None): help="nur mit --mock: RA laeuft mit der Zeit") p.add_argument("--dry-run", action="store_true", 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) host, port = args.host, args.port @@ -55,11 +58,24 @@ def main(argv=None): client = MountClient(host, port, timeout=config.MOUNT_TIMEOUT) 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: while True: try: if not client.connected: 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_line, dec_line = _format(ra_raw, dec_raw) diff --git a/test_brightness.py b/test_brightness.py new file mode 100644 index 0000000..ca3b590 --- /dev/null +++ b/test_brightness.py @@ -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)