Webseiten-Werkzeuge


weitere_hardware:nesso_n1

Dies ist eine alte Version des Dokuments!


Inhaltsverzeichnis

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

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.


English version

NOTE: Currently in the testing phase!

MeshCore — Arduino Nesso N1 (ESP32-C6)

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

This directory contains the Hardware Abstraction Layer (HAL) for the Arduino Nesso N1 as a MeshCore target platform. The board was co-developed by Arduino and M5Stack, combining an ESP32-C6 microcontroller with an SX1262 LoRa transceiver, Wi-Fi 6, Bluetooth 5.3, Thread/Zigbee, and a 1.14\u2033 touchscreen in a compact, battery-powered enclosure.


Hardware Overview

Component Details
MCU Espressif ESP32-C6 (RISC-V, 160 MHz)
Flash 16 MB
RAM 512 KB
LoRa Semtech SX1262, 850\u2013960 MHz, up to +22 dBm
RF Switch / LNA External, controlled via I\u00b2C GPIO expander
I\u00b2C GPIO Expander PI4IOE5V6408 (2\u00d7 instances, addresses 0x43 and 0x44)
Display ST7789P3, 1.14\u2033, 240\u00d7135 px, touchscreen (FT6336U)
IMU BMI270 (6-axis)
Battery 250 mAh LiPo, USB-C charging
Connectivity Wi-Fi 6, BT 5.3, Thread/Zigbee (802.15.4), LoRa

Key Porting Decisions

SX1262 Reset and RF Switch via I\u00b2C Expander

Unlike most other MeshCore targets, the LoRa control lines on the Nesso N1 are not connected directly to ESP32-C6 GPIO pins. They are routed through a PI4IOE5V6408 I\u00b2C GPIO expander (address 0x43):

Signal Expander Pin Function
SX_NRST P7 SX1262 hardware reset
SX_ANT_SW P6 Antenna RF switch
SX_LNA_EN P5 Low-noise amplifier enable
KEY1 P0 Button A (input)
KEY2 P1 Button B (input)

RadioLib's setRfSwitchTable() (direct GPIO only) cannot be used. NessoN1Board implements the RfSwitchCallback interface instead.

Shared 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() is called without the NSS argument to avoid registering a duplicate APB hardware CS-callback (see Fix 4).

FSPI instead of default SPI

The ESP32-C6 has no VSPI bus: SPIClass lora_spi(FSPI).

USB-CDC and BLE are mutually exclusive

Firmware ARDUINO_USB_CDC_ON_BOOT Debug output
Repeater 1 USB-Serial active
Companion BLE 0 no USB-Serial
Companion WiFi 0 no USB-Serial

Display Architecture

Resolution and UITask Layout Verification

The ST7789P3 has 240\u00d7135 pixels (Landscape, setRotation(1)). All UITask default layouts fit without overflow:

Element Width x (centred) Right edge Fits
MeshCore logo (64\u00d736 px XBM) 64 px 88 152 \u2705
Version line (setTextSize 2) 156 px 42 198 \u2705
Node name \„NessoN1 Repeater\“ 192 px 24 216 \u2705
Frequency line \„869.6 SF8 BW62.5\“ 204 px 18 222 \u2705

All four elements together occupy ~100 px of 135 px height \u2014 no vertical overflow.

Mandatory Initialisation Order

radio_init() in target.cpp:
  1. board.begin()           SPI2, Wire, expanders, loraReset()
  2. board.onRfRx()          RF switch active for calibration
  3. radio.std_init(nullptr) RadioLib \u2014 nullptr: no second spi->begin()
  4. display.begin()         M5GFX lazy construction \u2014 must follow std_init!

Known Issues and Their Fixes

Fix 1 \u2014 Display Goes Black After a Few Seconds

Date: April 2026 Affected file: platformio.ini, [env:NessoN1_repeater_display]

Symptom: Display shows boot screen (~4 s) and home screen (~10 s), then stays permanently black. Button presses have no effect.

Root cause: UITask.h (MeshCore, examples/simple_repeater/UITask.h) defines SCREEN_TIMEOUT (default: 10 000 ms). After expiry it calls display.turnOff(). The wakeup path in UITask polls PIN_USER_BTN \u2014 a direct GPIO pin. On the Nesso N1, KEY1 and KEY2 are connected via I\u00b2C expander 0 (address 0x43, pins P0/P1) and are not direct GPIOs. UITask therefore has no wakeup path \u2014 once the timeout fires, the display stays off permanently.

Fix:

ini
; platformio.ini \u2014 [env:NessoN1_repeater_display]:
-D SCREEN_TIMEOUT=0
-D DISPLAY_TIMEOUT=0

Both flags set to 0 disable the automatic timeout entirely. DISPLAY_TIMEOUT is also set because different UITask versions use different macro names.

Note for battery operation: Use e.g. -D SCREEN_TIMEOUT=30000. Button wakeup is already integrated via nesso_ui_tick() (see Fix 7).


Fix 7 \u2014 Buttons Non-Functional in Repeater Display Mode

Date: April 2026 Affected files: simple_repeater/main.cpp, simple_repeater/UITask.h, simple_repeater/UITask.cpp, simple_repeater/MyMesh.h

Symptom: KEY1 and KEY2 appear to do nothing. Serial log shows [ui] KEY1 erkannt \u2014 the keypress is detected, but the display does not change.

Root cause: nesso_ui_tick() correctly returns -1 (KEY1) or -2 (KEY2) when the display is already on. In main.cpp, evaluation of these return values was entirely commented out. Additionally, UITask had no navigable screens and no connection to MyMesh to fetch neighbour data or send a discover request.

Fix \u2014 three levels:

1. New interface UIActions.h \u2014 decouples UITask from MyMesh:

cpp
class UIActions {
public:
  virtual void uiGetNeighborList(char* buf, int bufSize) = 0;
  virtual void uiSendDiscover() = 0;
};

2. MyMesh implements UIActions \u2014 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 gains 3 screens + navigation, connected via setActions():

SCREEN_HOME       \u2014 node name, frequency, SF/BW/CR
SCREEN_NEIGHBOURS \u2014 neighbour list (hex ID, age, SNR)
SCREEN_POLL_SENT  \u2014 "Poll sent" feedback for 2 seconds

4. main.cpp evaluates btn (previously commented out):

cpp
if (btn == -1) ui_task.nextScreen();   // KEY1: forward / poll
if (btn == -2) ui_task.prevScreen();   // KEY2: back

Button behaviour after patch:

Button Display OFF Display ON (Home) Display ON (Neighbours)
——–————-——————-————————-
KEY1 Wakeup \u2192 Neighbours screen \u2192 Send poll
KEY2 Wakeup stays Home \u2192 Home screen

After sending a poll, the screen shows „Poll sent“ for 2 seconds, then automatically returns to the (refreshed) neighbour list.


Fix 8 \u2014 Buttons Not Recognised: Wire Conflict Between M5GFX and Expander Driver

Date: April 2026 Affected files: NessoDisplayDriver.cpp, NessoDisplayDriver.h, platformio.ini

Symptom: KEY1 and KEY2 are not reliably registered despite correct edge detection and boot lock. Serial log shows:

[E][Wire.cpp:131] setPins(): bus already initialized. change pins only when not.
[E][esp32-hal-cpu.c:123] addApbChangeCallback(): duplicate func=...

Root cause: M5GFX initialises the I\u00b2C bus internally via WireInternal.begin(SDA, SCL). The custom PI4IOE5V6408 driver in NessoN1Board also initialises Wire directly. Both access the same bus (SDA=GPIO10, SCL=GPIO8) with different TwoWire instances, causing bus collisions and unstable expander reads. Documented in the Arduino Forum (November 2025): https://forum.arduino.cc/t/cant-use-buttons-and-graphics-at-the-same-time/1415099

Fix: M5Unified takes over button handling and display initialisation in a coordinated way. M5.begin() initialises Wire, M5GFX and the GPIO expander in the correct order \u2014 no more Wire conflict.

cpp
// NessoDisplayDriver.cpp \u2014 begin():
M5.begin(cfg);              // instead of: _gfx->begin()
_gfx = &M5.Display;        // instead of: static M5GFX gfx_instance

// checkButtons():
M5.update();
if (M5.BtnA.wasPressed()) return 1;  // KEY1
if (M5.BtnB.wasPressed()) return 2;  // KEY2
ini
; platformio.ini \u2014 nesso_n1_base:
lib_deps =
  m5stack/M5GFX
  m5stack/M5Unified   ; NEW: Fix 8

Removed: _prevBtnState, _btnReadyAt, _lastBtnCheck \u2014 manual debounce is no longer needed; M5Unified handles edge detection internally.


Fix 2 \u2014 Display in Portrait Mode (135\u00d7240 Instead of 240\u00d7135)

Date: April 2026 Affected file: NessoDisplayDriver.cpp

Symptom: Serial reports M5GFX reports display: 135 x 240 px. UITask text is clipped with a negative x-offset and is not readable.

Root cause: setRotation(1) was called after querying width() and height(). M5GFX swaps dimensions only after setRotation(), so the pre-rotation query always returned 135\u00d7240. The problem also occurred when display.begin() ran before radio_init() from main.cpp: the idempotent guard on the second begin() call skipped setRotation(1) entirely.

Fix in NessoDisplayDriver.cpp:

cpp
_gfx->setRotation(1);  // FIRST \u2014 M5GFX swaps dimensions only after this
Serial.printf("[display] M5GFX ready: %d x %d px\n",
    _gfx->width(), _gfx->height());  // now correctly 240 x 135

Fix 3 \u2014 ''radio init failed: -2'' from Double board.begin() Call

Date: April 2026 Affected file: NessoN1Board.cpp

Symptom: ERROR: radio init failed: -2, serial shows [loraReset] TIMEOUT!.

Root cause: examples/simple_repeater/main.cpp calls board.begin() in setup() under #ifdef DISPLAY_CLASS \u2014 before radio_init(). On the Nesso N1, board.begin() calls loraReset(): SX1262 boots, BUSY goes HIGH. Then radio_init() calls board.begin() again \u2014 second loraReset() \u2014 SX1262 boots again, BUSY HIGH again \u2014 radio.std_init() hits the chip during boot \u2014 RADIOLIB_ERR_CHIP_NOT_FOUND = -2.

Fix in NessoN1Board::begin():

cpp
static bool s_boardBeginCalled = false;
if (s_boardBeginCalled) {
    Serial.println("[board] begin() already initialised \u2014 guard active");
    return;   // loraReset() is NOT called again
}
s_boardBeginCalled = true;

Complete solution: Remove board.begin() and display.begin() from setup() in main.cpp (see main_cpp_setup_hinweis.txt).


Fix 4 \u2014 APB Callback Duplicate and Wire.setPins Warning

Date: April 2026 Affected files: 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=...

Root cause: main.cpp calls display.begin() before radio_init(). M5GFX (constructed on first display.begin()) calls spi_bus_initialize(SPI2_HOST) and Wire.begin(). When radio_init() \u2192 board.begin() \u2192 lora_spi.begin() runs afterwards, the ESP-IDF SPI driver tries to register the same APB callback again \u2192 duplicate.

Fix: The board guard (Fix 3) prevents the second lora_spi.begin() call. The three [E] lines are functionally harmless but remain visible while main.cpp retains the wrong call order.


Fix 5 \u2014 BUSY Pulse Timing After loraReset()

Date: April 2026 Affected file: NessoN1Board.cpp

Symptom: Log reports BUSY immediately after NRST HIGH: LOW (unexpected) even though std_init succeeds.

Root cause: delay(20) after NRST HIGH was too long. The SX1262 on the Nesso N1 produces a BUSY-HIGH pulse of less than 1 ms after reset \u2014 already LOW by the time digitalRead() ran after 20 ms. No hardware fault, just overly conservative timing.

Fix: delay(20) \u2192 delay(2), updated log message:

[loraReset] BUSY 2 ms after NRST HIGH: LOW (pulse complete \u2014 normal on Nesso N1)

Directory Structure

variants/arduino_nesso_n1/
\u251c\u2500\u2500 NessoN1Board.h             \u2014 pin constants, PI4IOE5V6408 driver, board class
\u251c\u2500\u2500 NessoN1Board.cpp           \u2014 begin() with guard (Fix 3), loraReset() (Fix 5)
\u251c\u2500\u2500 NessoDisplayDriver.h       \u2014 DisplayDriver extension, lazy construction
\u251c\u2500\u2500 NessoDisplayDriver.cpp     \u2014 begin() with setRotation fix (Fix 2)
\u251c\u2500\u2500 target.h                   \u2014 extern declarations, nesso_check_buttons()
\u251c\u2500\u2500 target.cpp                 \u2014 radio_init() mandatory sequence, global objects
\u251c\u2500\u2500 platformio.ini             \u2014 incl. SCREEN_TIMEOUT=0 (Fix 1)
\u251c\u2500\u2500 credentials.ini.example    \u2014 template for local credentials
\u2514\u2500\u2500 main_cpp_setup_hinweis.txt \u2014 correct main.cpp structure (Fix 3/4)

Quick Start

1. Prerequisites

2. Set up credentials

bash
cd <project root>
cp variants/arduino_nesso_n1/credentials.ini.example credentials.ini

Edit credentials.ini:

ini
[credentials]
build_flags_repeater =
  -D ADMIN_PASSWORD='"your_password"'
build_flags_wifi =
  -D WIFI_SSID='"your_network"'
  -D WIFI_PWD='"your_password"'

3. Build and flash

bash
pio run -e NessoN1_repeater_display --target upload && pio device monitor -b 115200

4. Expected serial output (repeater with display)

[loraReset] BUSY 2 ms after NRST HIGH: LOW (normal on Nesso N1)
[loraReset] BUSY=LOW after 0 ms \u2014 SX1262 ready
[display] M5GFX ready: 240 x 135 px (after setRotation(1))
[display] ST7789 ready, backlight on
[board] begin() already initialised \u2014 guard active
[radio] std_init OK
[init] === radio_init() complete \u2014 radio ready ===

Pin Reference

Direct ESP32-C6 GPIOs

Function 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\u00b2C SDA G10 I\u00b2C SCL G8 Touch INT G3

Via I\u00b2C Expander 0 (0x43)

KEY1=P0, KEY2=P1, LNA Enable=P5, Antenna Switch=P6, LoRa Reset (NRST)=P7

Via I\u00b2C Expander 1 (0x44)

LCD Reset=P1, LCD Backlight=P6


RF Switch Logic

Mode ANT_SW LNA_EN Description
TX HIGH LOW TX path active, LNA protected
RX HIGH HIGH RX path active with LNA
IDLE LOW LOW RF path disconnected, low power

Open Items

# Topic Status
1 Remove board.begin()/display.begin() from main.cpp setup() workaround active (guard)
2 Hook KEY1/KEY2 into UITask screen wakeup open
3 Touch FT6336U (I\u00b2C 0x38) open
4 IMU BMI270 (I\u00b2C 0x68) for auto-rotation open
5 Battery monitor (I\u00b2C 0x49) open

License

MIT License, identical to MeshCore. Full text in the repository root.

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