NOTE: Currently in the testing phase!
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.
| 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 |
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.
| 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).
The ESP32-C6 has no VSPI bus: SPIClass lora_spi(FSPI).
| 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 |
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.
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!
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).
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.
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.
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
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).
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.
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)
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)
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"'
bash pio run -e NessoN1_repeater_display --target upload && pio device monitor -b 115200
[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 ===
| 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 |
KEY1=P0, KEY2=P1, LNA Enable=P5, Antenna Switch=P6, LoRa Reset (NRST)=P7
LCD Reset=P1, LCD Backlight=P6
| 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 |
| # | 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 |
MIT License, identical to MeshCore. Full text in the repository root.
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.
Erstellt 2025 für die deutschsprachige MeshCore Community •
Impressum