HINWEIS: Befindet sich in der Testphase!
HINWEIS: team-nessoN1-meshcore muss aus Gesundheitlichen Gründen Pausieren. Wer mag kann Ihn wegen Details der Projektübergabe unter E-Mail — team-nessoN1-meshcore kontaktieren.
Autor: team-nessoN1-meshcore — team-nessoN1-meshcore@posteo.de
Stand: April 2026
Diese Seite beschreibt 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.
| 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 |
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.
| 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.
Der ESP32-C6 hat keinen VSPI-Bus: SPIClass lora_spi(FSPI).
| 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 |
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.
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!
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: 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. Auf dem Nesso N1 hängen KEY1 und KEY2 über I²C-Expander 0 (Adresse 0x43, Pins P0/P1). UITask hat damit keinen Wakeup-Mechanismus — das Display bleibt nach Timeout-Ablauf permanent schwarz.
Fix:
; 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 unterschiedliche Makronamen verwenden.
Hinweis Akku-Betrieb: Sinnvoller Wert z.B.-D SCREEN_TIMEOUT=30000. Der Button-Wakeup ist übernesso_ui_tick()bereits integriert (siehe Fix 7).
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.
Ursache: setRotation(1) wurde in begin() nach _gfx→width() / _gfx→height() aufgerufen. M5GFX tauscht die Dimensionen erst nach setRotation() intern um. Zusätzlich trat das Problem auf, wenn display.begin() in main.cpp vor radio_init() aufgerufen wurde: Der idempotente Guard beim zweiten begin()-Aufruf hat setRotation(1) dann gar nicht mehr ausgeführt.
Fix in NessoDisplayDriver.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 ✓
Datum: April 2026
Betroffene Datei: NessoN1Board.cpp
Symptom: ERROR: radio init failed: -2, Serial zeigt [loraReset] TIMEOUT!.
Ursache: examples/simple_repeater/main.cpp ruft unter #ifdef DISPLAY_CLASS in setup() explizit board.begin() auf — vor radio_init(). 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():
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;
Vollständige Lösung: In main.cpp board.begin() und display.begin() aus setup() entfernen — beide werden intern durch radio_init() erledigt (siehe main_cpp_setup_hinweis.txt).
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: main.cpp ruft display.begin() vor radio_init() auf. 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 → Duplikat.
Fix: Der Board-Guard (Fix 3) verhindert den zweiten lora_spi.begin()-Aufruf. Die drei [E]-Zeilen sind funktional harmlos, aber ein sichtbarer Indikator für die unvollständig behobene Aufruf-Reihenfolge.
Datum: April 2026
Betroffene Datei: NessoN1Board.cpp
Symptom: Log meldet BUSY direkt nach NRST HIGH: LOW (unerwartet) obwohl Reset korrekt funktioniert und std_init erfolgreich ist.
Ursache: 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.
Fix:
_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)
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: 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.
Fix — drei Ebenen:
1. Neues Interface UIActions.h — entkoppelt UITask von MyMesh:
class UIActions { public: virtual void uiGetNeighborList(char* buf, int bufSize) = 0; virtual void uiSendDiscover() = 0; };
2. MyMesh implementiert UIActions — in MyMesh.h:
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):
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.
Datum: April 2026
Betroffene Dateien: NessoDisplayDriver.cpp, NessoDisplayDriver.h, platformio.ini
Symptom: KEY1 und KEY2 werden trotz korrekter Flanken-Erkennung nicht oder unzuverlässig registriert. Im Serial-Log:
[E][Wire.cpp:131] setPins(): bus already initialized. change pins only when not. [E][esp32-hal-cpu.c:123] addApbChangeCallback(): duplicate func=...
Ursache: 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 → Bus-Kollisionen. Dokumentiert im Arduino Forum (November 2025).
Fix: M5Unified übernimmt Button-Handling und Display-Init koordiniert. M5.begin() initialisiert Wire, M5GFX und GPIO-Expander in der richtigen Reihenfolge.
// 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
; 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.
cd <projektwurzel> cp variants/arduino_nesso_n1/credentials.ini.example credentials.ini
credentials.ini anpassen:
[credentials] build_flags_repeater = -D ADMIN_PASSWORD='"dein_passwort"' build_flags_wifi = -D WIFI_SSID='"dein_wlan"' -D WIFI_PWD='"dein_passwort"'
credentials.iniist in.gitignoreund wird nicht ins Repository eingecheckt.
Repeater mit Display (empfohlen als Einstieg):
pio run -e NessoN1_repeater_display --target upload pio device monitor -b 115200
Repeater ohne Display:
pio run -e NessoN1_repeater --target upload
Companion Radio — BLE:
pio run -e NessoN1_companion_ble --target upload
Companion Radio — WiFi/TCP:
pio run -e NessoN1_companion_wifi --target upload
[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 falsche Aufruf-Reihenfolge hat — sie sind harmlos.
| 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 |
| Funktion | Expander-Pin |
|---|---|
| Taste A (KEY1) | P0 |
| Taste B (KEY2) | P1 |
| LNA Enable | P5 |
| Antennen-Switch | P6 |
| LoRa Reset (NRST) | P7 |
| Funktion | Expander-Pin |
|---|---|
| LCD Reset | P1 |
| LCD Backlight | P6 |
| 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 |
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
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
| # | 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 |
MIT-Lizenz, identisch mit MeshCore. Vollständiger Text im Wurzelverzeichnis des Repositories.
Diese Anleitung / Übersetzung wurde nach bestem Wissen erstellt, erhebt aber nicht den Anspruch auf Vollständigkeit und Richtigkeit der Angaben und dient lediglich als Hilfestellung.
Diese Seite steht in keinerlei Verbindung zum MeshCore Projekt https://meshcore.io.
Erstellt 2025 für die deutschsprachige MeshCore Community •
Impressum