# Morse-Decoder / -Geber – ESP32 + SPI-TFT

**Board:** AZ-Delivery ESP32 DevKit V4 (ESP32-WROOM-32)
Geprüft per esptool an COM3: ESP32-D0WD-V3 (rev v3.1), Dual Core 240 MHz, 4 MB Flash, USB-Seriell CP210x.
**Arduino-Board:** „ESP32 Dev Module“ (FQBN `esp32:esp32:esp32`), esp32-Core 3.2.0

## Verdrahtung

Alle GPIOs sind auf den Stiftleisten des AZ-Delivery DevKit V4 herausgeführt.
Display-Typ und Display-Pins stammen aus dem PlatformIO-Projekt
`C:\Users\lschu\Documents\PlatformIO\Projects\Test ST7789`
(`.pio\libdeps\az-delivery-devkit-v4\TFT_eSPI\User_Setup.h`, Rotation aus `src\main.cpp`).

### Display: 1,69" ST7789V2, 240x280 (SPI)

| Display-Pin | ESP32 GPIO | Bemerkung |
|---|---|---|
| VCC         | 3V3     | |
| GND         | GND     | |
| SCL (SCK)   | GPIO 14 | SPI-Takt |
| SDA (MOSI)  | GPIO 13 | SPI-Daten |
| CS          | GPIO 26 | |
| DC          | GPIO 2  | Strapping-Pin, im bestehenden Aufbau unkritisch (Flashen/Booten getestet) |
| RST         | GPIO 4  | |
| BL / BLK    | 3V3 bzw. offen | im Projekt kein BL-Pin definiert |
| MISO        | –       | nicht benötigt |

Einstellungen: Treiber ST7789(V2), 240x280, Rotation 0 (Hochformat), SPI 27 MHz, SPI-Mode 0,
Farbreihenfolge RGB, Inversion an, Zeilen-Offset 20 (wird von `Adafruit_ST7789::init(240, 280)` automatisch gesetzt).

Ausrichtung: Der Sketch setzt MADCTL nach `setRotation()` auf die TFT_eSPI-Werte (Rotation 0 → 0x00). Standard ist `MIRROR_Y 1` (→ MADCTL 0x80): an diesem Modul war 0x00 links/rechts gespiegelt und 0x40 um 180° gedreht. Der Zeilen-Offset (20 oben/unten bei 280 von 320 Zeilen) ist symmetrisch, MY verschiebt daher nichts.
Ist die Schrift gespiegelt oder steht sie auf dem Kopf, oben im Sketch umschalten:
`#define MIRROR_X 1` (links/rechts), `#define MIRROR_Y 1` (oben/unten), beide 1 = 180°.
Für Querformat `#define DISPLAY_ROTATION 1` (oder 3). Der verwendete MADCTL-Wert wird beim Start auf Serial ausgegeben.

### Bedienung / Ton

| Bauteil | ESP32 GPIO | Anschluss |
|---|---|---|
| Morsetaste (Taster)     | GPIO 27 | andere Seite an GND (interner Pull-up) |
| Funktionstaste (Taster) | GPIO 33 | andere Seite an GND (interner Pull-up) – **verlegt von 26** (jetzt Display-CS) |
| Summer (passiv)         | GPIO 25 | andere Seite an GND (bei Bedarf 100 Ω in Reihe) |
| LED Geber               | –       | deaktiviert (GPIO 2 ist DC); optional externe LED + 330 Ω an GPIO 22, dann `#define PIN_LED 22` |

### Audio-Eingang (Morse-Ton vom Tablet-Kopfhörerausgang)

```
 Klinke Spitze (L) ──||──┬───────── GPIO 34
                  1 µF   │
                 3V3 ──[10k]──┤
                 GND ──[10k]──┘
 Klinke Hülse (Masse) ───────────── GND
```

| Verbindung | von | nach |
|---|---|---|
| Koppelkondensator 1 µF | Klinke Spitze (linker Kanal) | GPIO 34 (bei Elko: Minus zur Klinke, Plus zu GPIO 34) |
| Widerstand 10 kΩ | GPIO 34 | 3V3 |
| Widerstand 10 kΩ | GPIO 34 | GND |
| Masse | Klinke Hülse (Sleeve) | GND des ESP32 |

Ring (rechter Kanal) frei lassen. Bei 4-poligen TRRS-Steckern ist der Ring nahe der Hülse das Mikrofon, also ebenfalls frei lassen.
Ablauf im Sketch: ADC mit 20 kHz, Goertzel auf 700 Hz (`/freq N`), Blöcke von 8 ms, adaptive Schwelle.
Selbsttest ohne Tablet: mit `-DAUDIO_SELFTEST=1` bzw. `#define AUDIO_SELFTEST 1` liest der ADC GPIO 32 und GPIO 32 erzeugt selbst Morse mit 700 Hz („PARIS SOS 0123456789“, abwechselnd 12/18/25 WPM). Auf dem Board getestet: wird fehlerfrei dekodiert.
Gegen Knacken und Brummen vom Tablet filtert ein Hochpass (200 Hz) vor dem Goertzel; ein Ton zählt außerdem nur, wenn die Zielfrequenz mindestens 40 % der Blockenergie ausmacht (`ton=` in der Debug-Zeile). `/dump` gibt 4096 Rohwerte am Stück aus.
Mit `/audio` gibt der Sketch laufend Pegel aus (`#A mag=… nf=… thrOn=…`), `/src key|audio|both` wählt die Quelle.

Hinweise:
* Aktiver Summer statt passivem: im Sketch `#define BUZZER_PASSIVE 0`.
* Im alten Projekt hing ein nRF24 mit CE an GPIO 27 und CSN an GPIO 12. Falls das Funkmodul noch verdrahtet ist, die Morsetaste auf einen anderen freien Pin legen (z. B. GPIO 32) oder das nRF24 abklemmen.
* Freie, unkritische Reserve-Pins: 16, 17, 21, 22, 32 (18, 19, 23 falls kein nRF24 am VSPI hängt).

## Display-Typ wählen

Oben im Sketch genau eine Zeile aktiv lassen (Standard jetzt ST7789 240x280):

```cpp
//#define DISPLAY_ILI9341    // 240x320
//#define DISPLAY_ST7735     // 1,8" 128x160  (ggf. ST7735_TAB anpassen)
#define DISPLAY_ST7789       // 240x280 (ST7789_W/H, DISPLAY_ROTATION anpassen)
```

## Bedienung

* **Morsetaste (GPIO 27):** Tasten → Mithörton, Punkte/Striche erscheinen grün, nach der Zeichenpause der Buchstabe (weiß). Tempo wird automatisch geschätzt (RX-WPM in der Kopfzeile).
* **Funktionstaste (GPIO 33):** kurz = Text löschen, lang (≥ 1 s) = Text auf dem Display als Morse wiedergeben.
* **Serieller Monitor (115200 Baud, Zeilenende „Neue Zeile“):** Text eingeben → wird gemorst (gelb angezeigt). Befehle: `/wpm 18`, `/ton 650`, `/clear`, `/help`. Ein Tastendruck bricht die Ausgabe ab.
* Unbekannte Morsefolgen werden als `*` angezeigt. Umlaute beim Senden: Ä→AE, Ö→OE, Ü→UE, ß→SS.

## Audio-Test mit Tablet

`test/morse_audio_test.py` (pyserial nötig) spielt die WAVs aus `test/wav/` (700 Hz; PARIS, SOS, HALLO LUTZ, CQ, Ziffern; je 12, 18 und 25 WPM) über ADB auf dem Tablet ab, liest COM3 mit und berechnet die Zeichenfehlerrate je WPM.
Nur Pegel messen: `py -3.10 morse_audio_test.py --noise 5`. Lautstärke einpegeln mit Dauerton: `py -3.10 morse_audio_test.py --calib --push --volume 10`. Hat das Tablet keine Standard-App für WAV, sucht das Skript selbst einen Player (`--player paket/.Activity` erzwingt einen). Erster Lauf mit Kopieren aufs Tablet: `py -3.10 morse_audio_test.py --push --volume 10`.
Die WAVs erzeugt `make_wavs.py`.

Ergebnis am 11.10.2026 (DOOGEE Tab A9, Kopfhörerausgang, Player `com.google.android.apps.youtube.music/.audiopreview.AudioPreviewPlayerActivity` per `--player`):
12, 18 und 25 WPM jeweils 0 % Zeichenfehler (15/15 Dateien fehlerfrei). Pegel am ESP32 bei Dauerton: mag ≈ 470, ton ≈ 0,9, Rohwerte ca. 1300..2600 (keine Übersteuerung).

### Langer Test (11.10.2026, 34 WAVs, ca. 25 min, `test/wav_long/`, erzeugt mit `test/make_wavs_long.py`)

Texte: Pangramm, deutscher Satz (Umlaute ersetzt), 2x QSO, Satzzeichen `. , ? / =`, 17 zufällige 5er-Gruppen; je 10/15/20/25/30 WPM,
dazu Farnsworth 20/12, Tempowechsel 15→25→12 in einer Datei, Rauschen SNR 10 dB, leise (−12 dB).
Aufruf: `py -3.10 morse_audio_test.py --set wav_long --push --volume 10 --stayon --player com.google.android.apps.youtube.music/.audiopreview.AudioPreviewPlayerActivity`
(Ergebnisse werden nach jeder Datei in `results_*.json` geschrieben; `--files a.wav,b.wav` für Wiederholungen.)
Offline-Simulation des Decoders auf den WAVs: `python3 test/sim_long.py [jitter_ms]`.

Decoder-Verbesserungen nach dem ersten Lauf: nachträgliches Teilen von Zeichen („TH“ wurde nach Pause als „6“ gelesen),
Punkt/Strich-Grenze aus dem Zeichen selbst bzw. aus den Pausen im Zeichen (Tempowechsel), adaptive Wortpause aus den
Pausen zwischen Zeichen (Farnsworth). Das Leerzeichen erscheint dadurch erst mit dem Beginn des nächsten Zeichens.
Ergebnis nachher: 20/25/30 WPM, Farnsworth, Rauschen, leise jeweils 0 % Zeichenfehler; Restfehler nur durch Störbytes
der seriellen Übertragung (64x 0xFF statt eines Zeichens) und 1 Zeichen beim harten Tempowechsel.

## Bargraph / Abstimmanzeige (unten am Display, `/bar on|off`)

Layout (240x280 Hochformat, von oben): Kopfzeile (blau, RX/TX-WPM) · Punkte/Striche · dekodierter Text
(mit Bargraph 7 Zeilen à 18 Zeichen, ohne 12 Zeilen) · Trennlinie · **Bargraph-Bereich 91 px**:
- Pegelbalken (ohne Schrift): waagerecht, 220x10 px mit Rahmen, direkt unter der Trennlinie. Zeigt max |Rohwert−dc|
  je ~63 ms. Hintergrund in dunklen Zonenfarben, Balken hell: gelb = zu leise (< 100 Counts, 0–35 % der Breite),
  grün = gut (Spitzen innerhalb 300…3800, bei dc ≈ 1957 bis ~1650 Counts, 35–85 %), rot = darüber (85–100 %).
  Weißer Peak-Hold-Strich (hält 0,5 s, fällt ~60 px/s). Clip (Rohwert ≤ 8 oder ≥ 4087): Rahmen 1 s rot.
  Serial: `lvl=` (Counts) und `clip=` in `/audio`/`/stat`.
- Textzeile: `Peak  712 Hz  +12  tiefer v` (grün = innerhalb ±15 Hz der Zielfrequenz „OK“, gelb = daneben mit Pfeil
  „hoeher ^“/„tiefer v“; ohne Ton `Peak ---  Ziel 700 Hz` grau). Das ist die Richtung, in die der Ton wandern muss.
- 25 Balken 400…1000 Hz (25-Hz-Raster, 6 px breit, 8 px Abstand, 50 px hoch), logarithmisch 0…60 dB
  (0 dB = 1 ADC-Count, ~60 dB = Vollaussteuerung); grün, Balken der Zielfrequenz (`/freq`) gelb,
  rote Peak-Hold-Marke fällt mit ~12 dB/s.
- Skala darunter: Striche und Beschriftung 400 / 700 / 1000 Hz, Mittenmarke bei 700 Hz weiß und länger.

Technik: 25 Goertzel-Filter auf denselben ADC-Daten (nach dem Hochpass), Hann-Fenster 1024 Werte (~63 ms bei
den tatsächlichen ~16,4 kHz), Spitzenfrequenz per Parabel-Interpolation, Update ~15/s, gezeichnet werden nur
geänderte Balkenteile, die Textzeile als ein Block (max. ~7 ms je Bild). Ton-Flanken bekommen Zeitstempel aus dem
Abtastzähler, deshalb stören Displayausgaben (auch Text-Scrollen, ~120 ms) das Decoder-Timing nicht.
`/audio` und `/stat` zeigen zusätzlich `pk=` (Peak-Hz), `dpk=` (Ablage zu /freq), `pkdb=`, `nfdb=` (Rauschboden),
`loopMax=`, `barMax=` (µs).

Test (`test/tune_test.py --selftest` mit `-DAUDIO_SELFTEST=1`, Rechteck auf GPIO 32 per `/stone N`): Töne
500/650/700/720/800/950 Hz → Peak 499,8/648,8/700,0/718,6/797,6/950,4 Hz (Abweichung ≤ 2,4 Hz),
Stufen-Sweep 400…1000 Hz in 25-Hz-Schritten: |Abw| mittel 0,8 Hz, max 3,1 Hz. Live-Signal ansehen:
`tune_test.py --live 30`. WAVs für einen späteren Tablet-Test: `test/make_wavs_tune.py` → `test/wav_tune/`.

## Benötigte Bibliotheken

* Adafruit GFX Library
* Adafruit ILI9341
* Adafruit ST7735 and ST7789 Library (für das ST7789 nötig – in der Arduino IDE auf dem PC noch nicht installiert)
* Adafruit BusIO (Abhängigkeit)

## Kompilieren / Flashen

```
arduino-cli compile --fqbn esp32:esp32:esp32 morse_esp32
arduino-cli upload  --fqbn esp32:esp32:esp32 -p COM3 morse_esp32
```

Oder in der Arduino IDE: Board „ESP32 Dev Module“, Port COM3, Hochladen.
Falls der Upload bei „Connecting…“ hängt: BOOT-Taste gedrückt halten, bis das Schreiben beginnt.

Test-Kompilierung (esp32-Core 3.2.0): ST7789 350 290 Bytes Flash (26 %), 23 612 Bytes RAM (7 %); ILI9341 und ST7735 kompilieren ebenfalls fehlerfrei.
