Webseiten-Werkzeuge


weitere_hardware:nesso_n1

Dies ist eine alte Version des Dokuments!


HINWEIS: Befindet sich in der Testphase!

# MeshCore — Arduino Nesso N1 (ESP32-C6)

Author: team-nessoN1-meshcore — team-nessoN1-meshcore@posteo.de Stand: April 2026

Dieses Verzeichnis enthält die Hardware-Abstraktionsschicht (HAL) für den Arduino Nesso N1 als MeshCore-Zielplattform. Das Board wurde gemeinsam von Arduino und M5Stack entwickelt und kombiniert einen ESP32-C6 Mikrocontroller mit einem SX1262 LoRa-Transceiver, Wi-Fi 6, Bluetooth 5.3, Thread/Zigbee und einem 1,14″-Touchscreen in einem kompakten, batteriebetriebenen Gehäuse.

## Aktueller Status April 2026:

Die Migration läuft (Repeater, Companion). Umfangreiche Tests stehen an. Das eigentliche Produkt Nesso N1 ist jung und hat einige Fehler in der Doku. Der Source wurden noch nicht veröffentlicht, weil Alpha Versionstatus.

## Hardware-Überblick

Komponente Details
MCU Espressif ESP32-C6 (RISC-V, 160 MHz)
Flash 16 MB
RAM 512 KB
LoRa Semtech SX1262, 850–960 MHz, bis +22 dBm
Antennenschalter / LNA Extern, gesteuert über I²C-GPIO-Expander
I²C-GPIO-Expander PI4IOE5V6408 (2× Instanzen, Adressen 0x43 und 0x44)
Display ST7789P3, 1,14″, 240×135 px, Touchscreen (FT6336U)
IMU BMI270 (6-Achsen)
Akku 250 mAh LiPo, USB-C Laden
Konnektivität Wi-Fi 6, BT 5.3, Thread/Zigbee (802.15.4), LoRa

## Besonderheiten dieser Portierung

### SX1262 Reset und RF-Switch über I²C-Expander

Anders als bei den meisten anderen MeshCore-Zielplattformen sind beim Nesso N1 die LoRa-Steuerleitungen nicht direkt mit GPIO-Pins des ESP32-C6 verbunden, sondern laufen über einen PI4IOE5V6408 I²C-GPIO-Expander (Adresse 0x43):

Signal Expander-Pin Funktion
SX_NRST P7 Hardware-Reset des SX1262
SX_ANT_SW P6 Antennen-RF-Schalter
SX_LNA_EN P5 Low-Noise-Amplifier
KEY1 P0 Taste A (Input)
KEY2 P1 Taste B (Input)

Konsequenz: RadioLib's `setRfSwitchTable()` funktioniert nicht (nur direkte GPIOs). `NessoN1Board` implementiert stattdessen das `RfSwitchCallback`-Interface, das `CustomSX1262Wrapper` bei jedem TX/RX-Übergang aufruft.

### Geteilter SPI-Bus (SX1262 + ST7789)

Signal GPIO
MOSI G21
MISO G22
SCK G20
LoRa CS (NSS) G23 (Software-CS)
LCD CS G17 (Software-CS)
LCD DC G16

`lora_spi.begin()` wird ohne NSS-Argument aufgerufen. Mit NSS würde Arduino einen APB-Hardware-CS-Callback für GPIO23 registrieren; M5GFX tut dasselbe für GPIO17 — zwei Callbacks auf demselben Bus führen zu `addApbChangeCallback: duplicate` und SPI-Korruption.

### FSPI statt Default-SPI

Der ESP32-C6 hat keinen VSPI-Bus: `SPIClass lora_spi(FSPI)`.

### USB-CDC und BLE schließen sich aus

Firmware `ARDUINO_USB_CDC_ON_BOOT` Debug-Output
Repeater `1` USB-Serial aktiv
Companion BLE `0` kein USB-Serial
Companion WiFi `0` kein USB-Serial

## Display-Architektur

### Auflösung und UITask-Layoutprüfung

Das ST7789P3 hat 240×135 Pixel (Landscape, `setRotation(1)`). Alle UITask-Standardlayouts passen ohne Überlauf:

Element Breite x (zentriert) Rechte Kante Passt
MeshCore-Logo (64×36 px XBM) 64 px 88 152
Versionszeile (setTextSize 2) 156 px 42 198
Node-Name „NessoN1 Repeater„ 192 px 24 216
Frequenzzeile „869.6 SF8 BW62.5“ 204 px 18 222

Alle vier Elemente belegen zusammen ~100 px von 135 px Höhe — kein vertikaler Überlauf.

### Pflichtsequenz der Initialisierung

``` radio_init() in target.cpp:

1. board.begin()           SPI2, Wire, Expander, loraReset()
2. board.onRfRx()          RF-Switch für Kalibrierung
3. radio.std_init(nullptr) RadioLib — nullptr: kein zweites spi->begin()
4. display.begin()         M5GFX Lazy Construction — zwingend nach std_init!

```

## Bekannte Probleme und ihre Fixes

### Fix 1 — Display schwarz nach wenigen Sekunden

Datum: April 2026 Betroffene Datei: `platformio.ini`, `[env:NessoN1_repeater_display]`

Symptom: Display zeigt Boot-Screen (~4 Sek.) und Home-Screen (~10 Sek.), dann dauerhaft schwarz. Kein Wakeup durch Tastendruck.

Ursache — präzise: `UITask.h` (MeshCore, `examples/simple_repeater/UITask.h`) definiert den Makro `SCREEN_TIMEOUT` (Standardwert: 10 000 ms). Nach Ablauf wird intern `display.turnOff()` aufgerufen. Der Wakeup-Pfad in UITask prüft `PIN_USER_BTN` — einen direkten GPIO-Pin der bei einem Tastendruck HIGH wird. Auf dem Nesso N1 hängen KEY1 und KEY2 über I²C-Expander 0 (Adresse 0x43, Pins P0/P1). Ein direkter GPIO als `PIN_USER_BTN` steht nicht zur Verfügung. UITask hat damit keinen Wakeup-Mechanismus — das Display bleibt nach Timeout-Ablauf permanent schwarz.

Fix: ```ini ; platformio.ini — [env:NessoN1_repeater_display]: -D SCREEN_TIMEOUT=0 -D DISPLAY_TIMEOUT=0 ``` Beide Flags auf 0 deaktivieren den Timeout vollständig. `DISPLAY_TIMEOUT` wird zusätzlich gesetzt weil verschiedene UITask-Versionen im MeshCore-Repository unterschiedliche Makronamen verwenden.

Hinweis Akku-Betrieb: Sinnvoller Wert z.B. `-D SCREEN_TIMEOUT=30000`. Der Button-Wakeup ist über `nesso_ui_tick()` bereits integriert (siehe Fix 7).

### Fix 7 — Tasten ohne Funktion im Repeater-Display-Modus

Datum: April 2026 Betroffene Dateien: `simple_repeater/main.cpp`, `simple_repeater/UITask.h`, `simple_repeater/UITask.cpp`, `simple_repeater/MyMesh.h`

Symptom: KEY1 und KEY2 reagieren scheinbar nicht. Im Serial-Log erscheint `[ui] KEY1 erkannt` — die Taste wird erkannt, aber es passiert nichts am Display.

Ursache — präzise: `nesso_ui_tick()` gibt bei erkannter Taste `-1` (KEY1) oder `-2` (KEY2) zurück, wenn das Display bereits an ist. In `main.cpp` war die Auswertung dieser Werte vollständig auskommentiert. Zusätzlich hatte `UITask` keine navigierbaren Screens und keine Verbindung zu `MyMesh`, um Nachbardaten oder einen Discover-Request abrufen zu können.

Fix — drei Ebenen:

1. Neues Interface `UIActions.h` — entkoppelt UITask von MyMesh: ```cpp class UIActions { public:

virtual void uiGetNeighborList(char* buf, int bufSize) = 0;
virtual void uiSendDiscover() = 0;

}; ```

2. `MyMesh` implementiert `UIActions` — in `MyMesh.h`: ```cpp class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public UIActions {

...
void uiGetNeighborList(char* buf, int bufSize) override { formatNeighborsReply(buf); }
void uiSendDiscover() override { sendNodeDiscoverReq(); }

}; ```

3. `UITask` bekommt 3 Screens + Navigation, verbunden via `setActions()`: ``` SCREEN_HOME — Node-Name, Frequenz, SF/BW/CR SCREEN_NEIGHBOURS — Nachbarliste (HEX-ID, Alter, SNR) SCREEN_POLL_SENT — Feedback „Poll gesendet“ für 2 Sek. ```

4. `main.cpp` wertet `btn` aus (vorher auskommentiert): ```cpp if (btn == -1) ui_task.nextScreen(); KEY1: vorwärts / Poll if (btn == -2) ui_task.prevScreen(); KEY2: zurück ```

Bedienung nach dem Patch:

Taste Display AUS Display AN (Home) Display AN (Nachbarn)
——-————-——————-———————–
KEY1 Wakeup → Nachbarn-Screen → Poll senden
KEY2 Wakeup bleibt Home → Home-Screen

Nach dem Poll zeigt der Screen 2 Sekunden „Poll gesendet“, dann wechselt er automatisch zurück zur (aktualisierten) Nachbarliste.

### Fix 8 — Tasten nicht erkannt: Wire-Konflikt zwischen M5GFX und Expander-Treiber

Datum: April 2026 Betroffene Dateien: `NessoDisplayDriver.cpp`, `NessoDisplayDriver.h`, `platformio.ini`

Symptom: KEY1 und KEY2 werden trotz korrekter Flanken-Erkennung und Boot-Sperre nicht oder unzuverlässig registriert. Im Serial-Log erscheinen: ``` [E][Wire.cpp:131] setPins(): bus already initialized. change pins only when not. [E][esp32-hal-cpu.c:123] addApbChangeCallback(): duplicate func=… ```

Ursache — präzise: M5GFX initialisiert den I²C-Bus intern über `WireInternal.begin(SDA, SCL)`. Der eigene `PI4IOE5V6408`-Treiber in `NessoN1Board` initialisiert `Wire` ebenfalls direkt. Beide greifen auf denselben Bus (SDA=GPIO10, SCL=GPIO8) zu — mit unterschiedlichen `TwoWire`-Instanzen. Das führt zu Bus-Kollisionen und instabilen Expander-Reads. Dokumentiert im Arduino-Forum (November 2025): https://forum.arduino.cc/t/cant-use-buttons-and-graphics-at-the-same-time/1415099

Fix: `M5Unified` übernimmt Button-Handling und Display-Init koordiniert. `M5.begin()` initialisiert Wire, M5GFX und GPIO-Expander in der richtigen Reihenfolge — kein Wire-Konflikt mehr.

```cpp NessoDisplayDriver.cpp — begin(): M5.begin(cfg); statt: _gfx→begin() _gfx = &M5.Display; statt: static M5GFX gfx_instance checkButtons(): M5.update(); if (M5.BtnA.wasPressed()) return 1; KEY1 if (M5.BtnB.wasPressed()) return 2; KEY2 ```

```ini ; platformio.ini — nesso_n1_base: lib_deps =

m5stack/M5GFX
m5stack/M5Unified   ; NEU: Fix 8

```

Entfernt: `_prevBtnState`, `_btnReadyAt`, `_lastBtnCheck` (manueller Debounce entfällt — M5Unified macht Flanken-Erkennung intern).

### Fix 2 — Display im Portrait-Modus (135×240 statt 240×135)

Datum: April 2026 Betroffene Datei: `NessoDisplayDriver.cpp`

Symptom: Serial meldet `M5GFX meldet Display: 135 x 240 px`. UITask-Texte werden mit negativem x-Offset abgeschnitten und sind nicht lesbar (Versionszeile bei x=−11, Node-Name bei x=−29).

Ursache — präzise: `setRotation(1)` wurde in `begin()` nach `_gfx→width()` / `_gfx→height()` aufgerufen. M5GFX tauscht die Dimensionen erst nach `setRotation()` intern um. Die Abfrage vor `setRotation()` lieferte daher immer die Portrait-Werte 135×240. Zusätzlich trat das Problem auf wenn `display.begin()` in `main.cpp` vor `radio_init()` aufgerufen wurde: Der idempotente Guard beim zweiten `begin()`-Aufruf aus `radio_init()` hat `setRotation(1)` dann gar nicht mehr ausgeführt — der Controller lief weiter im Portrait-Modus obwohl `DisplayDriver(240, 135)` im Konstruktor richtig gesetzt war.

Fix in `NessoDisplayDriver.cpp`: ```cpp setRotation ZUERST — M5GFX tauscht Dimensionen erst danach: _gfx→setRotation(1); Jetzt korrekte Werte: Serial.printf(„[display] M5GFX bereit: %d x %d px\n“, _gfx→width(), _gfx→height()); Ausgabe: 240 x 135 px ✓ ``` — ### Fix 3 — `radio init failed: -2` durch doppelten board.begin()-Aufruf Datum: April 2026 Betroffene Datei: `NessoN1Board.cpp` Symptom: `ERROR: radio init failed: -2`, Serial zeigt `[loraReset] TIMEOUT!`. Ursache — präzise: `examples/simple_repeater/main.cpp` (MeshCore-eigener Code) ruft unter `#ifdef DISPLAY_CLASS` in `setup()` explizit `board.begin()` auf — vor `radio_init()`. Das generische MeshCore-Muster funktioniert auf anderen Boards problemlos, weil dort Board-Init und Radio-Init unabhängig sind. Auf dem Nesso N1 führt `board.begin()` intern `loraReset()` aus: der SX1262 bootet, BUSY geht HIGH. Anschließend ruft `radio_init()` erneut `board.begin()` auf → zweites `loraReset()` → SX1262 bootet erneut, BUSY wieder HIGH → `radio.std_init()` trifft Chip mitten im Bootvorgang → `RADIOLIB_ERR_CHIP_NOT_FOUND` = `-2`. Fix in `NessoN1Board::begin()`: ```cpp static bool s_boardBeginCalled = false; if (s_boardBeginCalled) { Serial.println(„[board] begin() bereits initialisiert — Guard greift“); return; loraReset() wird NICHT nochmals ausgeführt } s_boardBeginCalled = true; ``` Der Guard stellt sicher dass `loraReset()` und `lora_spi.begin()` genau einmal laufen, unabhängig von der Aufruf-Reihenfolge in `main.cpp`.

Vollständige Lösung: In `main.cpp` `board.begin()` und `display.begin()` aus `setup()` entfernen — beide werden intern durch `radio_init()` erledigt. Da `main.cpp` MeshCore-eigener Code ist, ist der Guard die robustere Variante ohne Repository-Fork. Hinweis: `main_cpp_setup_hinweis.txt`.

### Fix 4 — APB-Callback-Duplikat und Wire.setPins-Warnung

Datum: April 2026 Betroffene Dateien: `NessoN1Board.cpp`, `NessoDisplayDriver.cpp`

Symptom: ``` [E][Wire.cpp:131] setPins(): bus already initialized. [E][esp32-hal-cpu.c:123] addApbChangeCallback(): duplicate func=… [E][esp32-hal-cpu.c:146] removeApbChangeCallback(): not found func=… ```

Ursache — präzise: `main.cpp` ruft `display.begin()` vor `radio_init()` auf. Beim ersten `display.begin()`-Aufruf konstruiert `NessoDisplayDriver` M5GFX als static-local. M5GFX ruft intern `spi_bus_initialize(SPI2_HOST)` und `Wire.begin()` auf. Wenn anschließend `radio_init()` → `board.begin()` → `lora_spi.begin()` läuft, versucht der ESP-IDF SPI-Treiber denselben APB-Callback erneut zu registrieren → `duplicate`. `Wire.begin()` auf bereits initialisierten Bus → `setPins warning`.

Fix: Der Board-Guard (Fix 3) verhindert den zweiten `lora_spi.begin()`-Aufruf. `BUSY` bleibt LOW, `std_init` läuft durch. Die drei `[E]`-Zeilen erscheinen trotzdem solange `main.cpp` die falsche Reihenfolge hat — sie sind funktional harmlos (System funktioniert korrekt), aber ein sichtbarer Indikator für die unvollständig behobene Aufruf-Reihenfolge.

### Fix 5 — BUSY-Puls-Timing nach loraReset()

Datum: April 2026 Betroffene Datei: `NessoN1Board.cpp`

Symptom: Log meldet `BUSY direkt nach NRST HIGH: LOW (unerwartet — evtl. kein Reset angekommen)` obwohl Reset korrekt funktioniert und `std_init` erfolgreich ist.

Ursache — präzise: Der `delay(20)` nach NRST HIGH war zu lang. Der SX1262 auf dem Nesso N1 hat einen BUSY-HIGH-Puls von unter 1 ms nach dem Reset — der Chip hatte BUSY längst wieder LOW gezogen wenn `digitalRead()` nach 20 ms lief. Es handelte sich um keinen echten Fehler, sondern um ein zu konservatives Timing kombiniert mit einer zu alarmistischen Logmeldung.

Fix: ```cpp _exp0.digitalWrite(NESSO_EXP0_SX_NRST, true); delay(2); war: delay(20) — 2 ms reicht, BUSY-Puls auf Nesso N1 < 1 ms ``` Logmeldung geändert zu: ``` [loraReset] BUSY 2 ms nach NRST HIGH: LOW (Chip hat BUSY-Puls bereits abgeschlossen — normal auf Nesso N1) ``` — ## Verzeichnisstruktur ``` variants/arduino_nesso_n1/ ├── NessoN1Board.h — Pin-Konstanten, PI4IOE5V6408-Treiber, Board-Klasse ├── NessoN1Board.cpp — begin() mit Guard (Fix 3), loraReset() (Fix 5) ├── NessoDisplayDriver.h — DisplayDriver-Erweiterung, Lazy Construction ├── NessoDisplayDriver.cpp — begin() mit setRotation-Fix (Fix 2) ├── target.h — Externe Deklarationen, nesso_check_buttons() ├── target.cpp — radio_init() Pflichtsequenz, globale Objekte ├── platformio.ini — incl. SCREEN_TIMEOUT=0 (Fix 1) ├── credentials.ini.example — Vorlage für lokale Zugangsdaten └── main_cpp_setup_hinweis.txt — Hinweis zur korrekten main.cpp-Struktur (Fix 3/4) src/helpers/ui/ └── DisplayDriver.h — Abstraktes Interface (Pure-Virtuals) src/helpers/radiolib/ ├── RfSwitchCallback.h — Abstraktes Interface für externen RF-Switch └── CustomSX1262Wrapper.h — SX1262-Wrapper mit RF-Switch-Callback ``` — ## Schnellstart ### 1. Voraussetzungen - [PlatformIO](https://platformio.org/) (CLI oder VS Code Extension) - Arduino Nesso N1 per USB-C verbunden ### 2. Zugangsdaten einrichten ```bash cd <projektwurzel> cp variants/arduino_nesso_n1/credentials.ini.example credentials.ini ``` `credentials.ini` anpassen: ```ini [credentials] build_flags_repeater = -D ADMIN_PASSWORD='„dein_passwort“' build_flags_wifi = -D WIFI_SSID='„dein_wlan“' -D WIFI_PWD='„dein_passwort“' ``` > `credentials.ini` ist in `.gitignore` und wird nicht ins Repository eingecheckt. ### 3. Firmware bauen und flashen Repeater mit Display (empfohlen als Einstieg): ```bash pio run -e NessoN1_repeater_display –target upload pio device monitor -b 115200 ``` Repeater ohne Display: ```bash pio run -e NessoN1_repeater –target upload ``` Companion Radio — BLE: ```bash pio run -e NessoN1_companion_ble –target upload ``` Companion Radio — WiFi/TCP: ```bash pio run -e NessoN1_companion_wifi –target upload ``` ### 4. Erwartete Serial-Ausgabe beim Boot (Repeater mit Display) ``` [board] begin() gestartet [board] I2C-Scan: [board] 0x43 ← Expander 0 (LoRa/Keys) GEFUNDEN [board] 0x44 ← Expander 1 (Display) GEFUNDEN [loraReset] BUSY 2 ms nach NRST HIGH: LOW (normal auf Nesso N1) [loraReset] BUSY=LOW nach 0 ms — SX1262 bereit [board] begin() abgeschlossen [display] M5GFX bereit: 240 x 135 px (nach setRotation(1)) [display] ST7789 bereit, Backlight ein [init] === radio_init() START === [init] 3/8: board.begin() [board] begin() bereits initialisiert — Guard greift [radio] std_init OK [init] === radio_init() abgeschlossen — Radio bereit === Repeater ID: … ``` Drei `[E]`-Zeilen (Wire.setPins, APB-Duplikat) erscheinen noch solange `main.cpp` die Reihenfolge aus Fix 3/4 hat — sie sind harmlos. — ## Pin-Referenz ### Direkte ESP32-C6 GPIOs | Funktion | GPIO | |—|—| | LoRa MOSI | G21 | | LoRa MISO | G22 | | LoRa SCK | G20 | | LoRa CS (NSS) | G23 | | LoRa BUSY | G19 | | LoRa IRQ (DIO1) | G15 | | LCD CS | G17 | | LCD DC | G16 | | I²C SDA | G10 | | I²C SCL | G8 | | Touch INT | G3 | ### Über I²C-Expander 0 (0x43) | Funktion | Expander-Pin | |—|—| | Taste A (KEY1) | P0 | | Taste B (KEY2) | P1 | | LNA Enable | P5 | | Antennen-Switch | P6 | | LoRa Reset (NRST) | P7 | ### Über I²C-Expander 1 (0x44) | Funktion | Expander-Pin | |—|—| | LCD Reset | P1 | | LCD Backlight | P6 | — ## RF-Switch-Schaltlogik | Modus | ANT_SW | LNA_EN | Beschreibung | |—|—|—|—| | TX | HIGH | LOW | Sende-Pfad aktiv, LNA geschützt | | RX | HIGH | HIGH | Empfangs-Pfad mit LNA aktiv | | IDLE | LOW | LOW | HF-Pfad getrennt, Stromsparmodus | — ## Architektur ``` target.cpp └─ radio_init() ├─ board.begin() SPI2, Wire, I²C-Scan, Expander, loraReset() ├─ rtc_clock.begin(Wire) ├─ board.onRfRx() RF-Switch für Kalibrierung ├─ radio.std_init(nullptr) kein zweites spi→begin() ├─ radio_driver.setRfSwitchCallback(&board) └─ display.begin() M5GFX Lazy Construction — nach std_init NessoDisplayDriver ├─ begin() static M5GFX; setRotation(1) vor width()/height() └─ checkButtons() Polling KEY1/KEY2 via I²C-Expander 0 (50ms Debounce) NessoN1Board (RfSwitchCallback) ├─ begin() Guard (einmaliger Aufruf) + loraReset() ├─ onRfTx() ANT_SW=HIGH, LNA_EN=LOW ├─ onRfRx() ANT_SW=HIGH, LNA_EN=HIGH └─ onRfIdle() ANT_SW=LOW, LNA_EN=LOW ``` — ## Offene Punkte | # | Thema | Status | |—|—|—| | 1 | `main.cpp`: `board.begin()`/`display.begin()` vor `radio_init()` entfernen | Workaround aktiv (Guard) | | 2 | KEY1/KEY2 als UITask Screen-Wakeup einhängen | offen | | 3 | Touch FT6336U (I²C 0x38) integrieren | offen | | 4 | IMU BMI270 (I²C 0x68) für Auto-Rotation | offen | | 5 | Akku-Monitor (I²C 0x49) für Display-Anzeige | offen | — ## Lizenz MIT-Lizenz, identisch mit MeshCore. Vollständiger Text im Wurzelverzeichnis.

weitere_hardware/nesso_n1.1776528408.txt.gz · Zuletzt geändert: von mellinux