Inhaltsverzeichnis

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.

MeshCore — Arduino Nesso N1 (ESP32-C6)

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.

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: 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 über nesso_ui_tick() bereits integriert (siehe Fix 7).

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.

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 ✓

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: 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).

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: 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.

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

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)

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: 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.

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 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.

Schnellstart

1. Voraussetzungen

2. Zugangsdaten einrichten

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.ini ist in .gitignore und wird nicht ins Repository eingecheckt.

3. Firmware bauen und flashen

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

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 falsche Aufruf-Reihenfolge 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

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

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 des Repositories.