Skip to content

feat: add Nordic nRF54L15-DK variant (Zephyr + BLE + LoRa)#10193

Merged
thebentern merged 39 commits into
meshtastic:developfrom
cvaldess:feature/nrf54l15-port
May 16, 2026
Merged

feat: add Nordic nRF54L15-DK variant (Zephyr + BLE + LoRa)#10193
thebentern merged 39 commits into
meshtastic:developfrom
cvaldess:feature/nrf54l15-port

Conversation

@cvaldess

@cvaldess cvaldess commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds community hardware variant for the Nordic nRF54L15-DK (PCA10156) with an external EBYTE E22-900M30S (SX1262, 30 dBm, 868/915 MHz) LoRa module.
  • First Meshtastic port running on the Zephyr RTOS — all other Nordic targets use the nRF5 SoftDevice Arduino stack. The new code lives under src/platform/nrf54l15/ and does not touch the existing src/platform/nrf52/ tree.
  • Bluetooth LE peripheral implemented directly on the Zephyr BT host stack (NRF54L15Bluetooth.*): Meshtastic GATT service, legacy connectable advertising, just-works pairing, MTU exchange up to 247, iOS-friendly connection parameters.
  • LittleFS-backed InternalFileSystem on SPIM20 for config/NodeDB persistence.
  • Build env: nrf54l15dk. Preset region is EU_868 at 869.5875 MHz / SFNarrow (easy to override via user prefs).

Hardware

Component Model Notes
MCU board Nordic nRF54L15-DK (PCA10156) Zephyr, Cortex-M33
LoRa EBYTE E22-900M30S SX1262 + 30 dBm PA, 868/915 MHz

E22 pins on the DK's J2 header (all E22 pins sit in the P2 HP-domain, 3.0 V; P1 at 1.8 V is below the SX1262 VIH threshold) — full table + reserved-pin map in variants/nrf54l15/nrf54l15dk/README.md.

Note: The E22-900M30S does not wire DIO2 to TXEN internally. A solder/wire bridge between DIO2 and TXEN on the module is required; without it the module will not transmit. SX126X_DIO2_AS_RF_SWITCH then lets the SX1262 drive the PA automatically.

Files changed

  • src/platform/nrf54l15/ — new platform layer (Arduino shims over Zephyr, SPI/Wire/Stream, InternalFileSystem, NRF54L15Bluetooth, main entry point).
  • variants/nrf54l15/nrf54l15dk/ — variant config (PlatformIO env, DT overlay, pin map, wiring README).
  • zephyr/prj.conf + zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay — Zephyr project + board-level config (BT, GPIO, SPI, RTT logging, heap/stack budgets).
  • boards/nrf54l15dk.json — PlatformIO board definition (the Seeed platform only ships the XIAO variants).
  • extra_scripts/nrf54l15_linker.py — post-script that parses build.ninja and runs the Zephyr two-pass linker-script generation directly, working around a PlatformIO + old-Ninja issue where the second pass never fires on its own.
  • variants/rp2350/rp2350.ini — exclude platform/nrf54l15/ from the RP2350 build_src_filter.
  • Shared source (src/main.*, src/FSCommon.*, src/RedirectablePrint.cpp, src/mesh/{Channels,NodeDB,RadioLibInterface,MeshService,PhoneAPI}.cpp, src/mesh/RadioLibInterface.h, src/modules/AdminModule.cpp) — small guards/helpers so the Zephyr build compiles alongside the Arduino targets. Behavior on existing boards is unchanged.
  • .gitignore — add flash.jlink and rtt_*.txt (nRF J-Link / RTT debug artifacts).

Hardware model

HW_VENDOR maps to meshtastic_HardwareModel_PRIVATE_HW until a dedicated protobuf enum value is assigned upstream. The variant declares custom_meshtastic_hw_model = 132 so the enum can be wired through the protobufs repo after merge.

Notes on scope

  • No changes to existing board variants or to the nRF52 platform tree.
  • Shared-source changes are minimal and wrapped in #ifdef guards so other targets are unaffected; easy to review.
  • The iOS app previously required a specific PREPARE_COUNT / MTU configuration on the Zephyr side — that's included here.
  • UART0 is held at reset polarity by the DK's interface MCU, so Meshtastic logs go out over SEGGER RTT (channel 1 in immediate mode) rather than USB serial.

Test plan

  • Firmware builds cleanly (pio run -e nrf54l15dk) against current develop.
  • iOS companion app pairs + connects over BLE, full config round-trip, ATT MTU negotiates to 247.
  • LoRa TX/RX end-to-end with a canonical T-Beam on the same Meshtastic channel at 868 MHz.
  • NodeDB updates propagate both directions; traceroute completes.
  • Reset causes + boot logs stream over RTT channel 1.
  • Any additional hardware / CI validation requested during review.

🤖 Generated with Claude Code

@github-actions github-actions Bot added needs-review Needs human review hardware-support Hardware related: new devices or modules, problems specific to hardware labels Apr 17, 2026
@cvaldess

Copy link
Copy Markdown
Contributor Author

Heads-up for reviewers — one Zephyr framework patch was applied locally and is intentionally not in this PR.

On my bench the BLE host's TX processor (tx_processortx_work) gets starved on sys_work_q during an active connection: the kick TX log fires but tx_processor: TX process start never runs until the connection ends. The ATT_EXCHANGE_MTU_RSP therefore never goes out, iOS hits its 5 s supervision timeout, and we disconnect with reason 0x13.

The workaround is a one-line change in framework-zephyr/subsys/bluetooth/host/hci_core.c inside bt_tx_irq_raise():

#if defined(CONFIG_BT_RECV_WORKQ_BT)
    k_work_submit_to_queue(&bt_workq, &tx_work);
#else
    k_work_submit(&tx_work);
#endif

i.e. submit tx_work to the dedicated BT workqueue instead of sys_work_q. With this patch BLE is rock-solid (iOS pairing + full config stream + ATT MTU=247 + mesh TX/RX validated E2E).

I'm deliberately not shipping this as a framework fork in the PR — it belongs upstream in Zephyr / nrfxlib, not in the Meshtastic repo. If CI (or another reviewer's hardware) can't reproduce stable BLE, this is almost certainly why. Happy to file the upstream Zephyr issue if useful; flagging it here so it's not a surprise.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Zephyr-based Meshtastic platform/variant for the Nordic nRF54L15-DK, including a BLE peripheral implementation, LittleFS-backed persistence, and build-system wiring to integrate the new target without impacting existing Arduino-based platforms.

Changes:

  • Introduces src/platform/nrf54l15/ Zephyr “Arduino shim” layer plus a Zephyr BT-host GATT peripheral (NRF54L15Bluetooth).
  • Adds the nRF54L15-DK variant + PlatformIO environment/board definition and Zephyr Kconfig/DTS overlay configuration.
  • Makes small cross-platform guards/adjustments in shared code to compile and run with the new Zephyr target.

Reviewed changes

Copilot reviewed 44 out of 45 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
zephyr/prj.conf Adds Zephyr Kconfig enabling C++17, RTT logging, LittleFS, and detailed BLE host/controller tuning for iOS compatibility.
zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay Board-level DTS overlay to disable unused peripherals and free SPI resources for the SX1262 wiring.
variants/rp2350/rp2350.ini Excludes the new platform/nrf54l15/ source tree from RP2350 builds.
variants/nrf54l15/nrf54l15dk/variant.h Defines the nRF54L15-DK pin map and radio configuration macros for the E22 (SX1262) module.
variants/nrf54l15/nrf54l15dk/variant.cpp Adds the variant init hook (currently minimal/no-op).
variants/nrf54l15/nrf54l15dk/platformio.ini Adds the nrf54l15dk PlatformIO environment configuration.
variants/nrf54l15/nrf54l15dk/nrf54l15dk.overlay Provides an additional DTS overlay under variants/ (currently conflicting with the board overlay).
variants/nrf54l15/nrf54l15dk/README.md Documents wiring, required DIO2→TXEN bridge, build/flash, and RTT monitoring steps.
variants/nrf54l15/nrf54l15.ini Adds the nRF54L15 Zephyr base PlatformIO configuration, build flags, and src filters.
src/platform/nrf54l15/utility/bonding.h Adds a stub header to satisfy bonding-related include chains when building under Zephyr.
src/platform/nrf54l15/nrf54l15_main.cpp Adds the Zephyr main() entry point with crash capture/logging plumbing.
src/platform/nrf54l15/nrf54l15_arduino.cpp Implements Arduino API shims (timing/GPIO/SPI/String/Print/etc.) atop Zephyr primitives.
src/platform/nrf54l15/main-nrf54l15.cpp Adds nRF54L15 platform hooks for setup/loop, power, BLE enable/disable, and deep sleep stubs.
src/platform/nrf54l15/bluefruit.h Adds a Bluefruit SDK stub to satisfy nRF52-specific include paths.
src/platform/nrf54l15/architecture.h Defines ARCH_NRF54L15 and feature flags / HW_VENDOR mapping for the new platform.
src/platform/nrf54l15/Wire.h Adds an Arduino TwoWire stub (compile-only at present).
src/platform/nrf54l15/WProgram.h Adds legacy Arduino header shim redirecting to Arduino.h.
src/platform/nrf54l15/Tone.h Adds a tone/noTone shim header redirecting to Arduino.h.
src/platform/nrf54l15/Stream.h Adds a Stream shim header redirecting to Arduino.h.
src/platform/nrf54l15/SPI.h Adds an Arduino SPI shim header for Zephyr-backed SPI transfers.
src/platform/nrf54l15/Print.h Adds a Print shim header redirecting to Arduino.h.
src/platform/nrf54l15/Nrf52SaadcLock.h Adds a stub to satisfy nRF52 SAADC lock include chains.
src/platform/nrf54l15/NRF54L15Bluetooth.h Declares the Zephyr BLE backend implementing the project’s Bluetooth API interface.
src/platform/nrf54l15/NRF54L15Bluetooth.cpp Implements the Meshtastic BLE GATT service/peripheral using Zephyr BT host APIs + watchdog logic.
src/platform/nrf54l15/NRF52Bluetooth.h Adds a stub NRF52Bluetooth header for Zephyr builds.
src/platform/nrf54l15/InternalFileSystem.h Declares Zephyr LittleFS-backed InternalFileSystem compatible with existing FS abstractions.
src/platform/nrf54l15/InternalFileSystem.cpp Implements mount/open/read/write/dir traversal and recursive delete using Zephyr FS APIs.
src/platform/nrf54l15/IPAddress.h Adds a stub IPAddress type for code paths expecting Arduino networking types.
src/platform/nrf54l15/Arduino.h Adds the core Arduino compatibility layer used throughout Meshtastic and 3rd-party Arduino libs.
src/modules/AdminModule.cpp Adds guards/helpers around GPIO output config handling and nRF54L15 BLE status plumbing.
src/mesh/RadioLibInterface.h Clears the static instance on destruction; adds a TX_DONE missed-IRQ polling hook declaration.
src/mesh/RadioLibInterface.cpp Adds missed TX_DONE IRQ polling to recover from dropped IRQ events.
src/mesh/PhoneAPI.cpp Skips filesystem manifest enumeration on NRF54L15-DK due to an FS recursion abort on this target.
src/mesh/NodeDB.cpp Adds nRF54L15 device-id derivation; adds additional LoRa USERPREFS overrides and “always-apply” logic after load.
src/mesh/MeshService.cpp Avoids abort() on to-phone queue enqueue failures by releasing to pool and returning.
src/mesh/Channels.cpp Applies compile-time LoRa USERPREFS overrides when building default LoRa config.
src/main.h Adds nRF54L15 Bluetooth externs and updates platform hook declarations (including a new rp2040Loop declaration).
src/main.cpp Adds nRF54L15 setup/loop calls and adds an RP2040 loop hook call; includes InputBroker conditionally.
src/RedirectablePrint.cpp Adds BLE-log routing via nrf54l15Bluetooth when building for ARCH_NRF54L15.
src/FSCommon.h Wires FSCommon to use the new Zephyr InternalFS implementation on ARCH_NRF54L15.
src/FSCommon.cpp Enables recursive LittleFS directory deletion for ARCH_NRF54L15.
platformio.ini Registers a post-build script to work around Zephyr’s two-pass link script generation for nRF54L15.
extra_scripts/nrf54l15_linker.py Implements the post-build linker.cmd generation workaround by parsing build.ninja and invoking gcc -E.
boards/nrf54l15dk.json Adds a PlatformIO board definition for the nRF54L15-DK.
.gitignore Ignores J-Link/RTT debug artifacts.

Comment thread variants/nrf54l15/nrf54l15dk/README.md Outdated
Comment thread src/main.cpp Outdated
Comment thread zephyr/prj.conf
Comment thread src/platform/nrf54l15/nrf54l15_main.cpp
Comment thread src/platform/nrf54l15/nrf54l15_main.cpp
Comment thread variants/nrf54l15/nrf54l15dk/nrf54l15dk.overlay Outdated
Comment thread src/platform/nrf54l15/nrf54l15_arduino.cpp
Comment thread variants/nrf54l15/nrf54l15dk/README.md Outdated
Comment thread variants/nrf54l15/nrf54l15dk/variant.h
Comment thread src/platform/nrf54l15/SPI.h Outdated
cvaldess added a commit to cvaldess/protobufs that referenced this pull request Apr 17, 2026
Reserves enum value 132 for the Nordic nRF54L15-DK community firmware
port. The port is tracked in meshtastic/firmware#10193 and currently
uses HardwareModel_PRIVATE_HW as a placeholder until this PR merges
and the regenerated protobufs propagate back into the firmware tree.

Board: Nordic nRF54L15-DK (PCA10156), Zephyr RTOS, external EBYTE
E22-900M30S (SX1262) LoRa module. Firmware variant: nrf54l15dk.
@cvaldess

Copy link
Copy Markdown
Contributor Author

Companion PR to reserve the HardwareModel enum value has been opened: meshtastic/protobufs#896 (NRF54L15_DK = 132). Once that merges and the regenerated protobufs propagate back into src/mesh/generated/, HW_VENDOR in src/platform/nrf54l15/architecture.h can flip from PRIVATE_HW to NRF54L15_DK in a small follow-up.

@cvaldess cvaldess force-pushed the feature/nrf54l15-port branch from 0712a36 to 4b64236 Compare April 17, 2026 23:26
@cvaldess

Copy link
Copy Markdown
Contributor Author

Force-pushed 4b64236 with CI fixes:

  • Build failures (60 jobs)platform/nrf54l15/ was getting picked up by every other platform's build_src_filter. Added -<platform/nrf54l15/> exclusion to esp32-common.ini, nrf52.ini, rp2040.ini, stm32.ini, and portduino.ini. (rp2350.ini was already done.)
  • Trunk clang-format / black / markdownlint / prettier — ran the configured formatters on every flagged file.
  • bandit B602 / semgrep subprocess-shell-true — in extra_scripts/nrf54l15_linker.py, the gcc_cmd comes verbatim from our own build.ninja and contains Windows-style paths with spaces that can't be safely argv-split (shlex.split mangles them, build breaks). Kept shell=True with inline # nosec B602 + # nosemgrep suppressions and a comment explaining why.

check-label failure — a maintainer will need to apply one of the required labels (hardware-support or enhancement look right). External contributors can't set labels themselves.

@cvaldess cvaldess force-pushed the feature/nrf54l15-port branch from 4b64236 to b337f86 Compare April 17, 2026 23:45
@cvaldess

Copy link
Copy Markdown
Contributor Author

Force-pushed b337f86 addressing all Copilot review comments. Each item below maps to one of the unresolved threads.

Real bugs:

  • src/main.cpp — removed the stray rp2040Loop() call (function isn't defined anywhere; the stub declaration in main.h was never implemented).
  • src/platform/nrf54l15/nrf54l15_arduino.cpp — added _spi00() nullptr guards in SPIClass::transfer16() and SPIClass::transferBytes() to match the existing transfer(uint8_t) behavior.
  • src/platform/nrf54l15/nrf54l15_main.cpp — renamed the misused saved_crash.sp = esf->basic.xpsr to separate psp (captured via mrs psp on handler entry) and xpsr (from the exception frame). Print block updated accordingly. The handler comment now clarifies it saves to .noinit RAM before k_fatal_halt resets the SoC.
  • src/platform/nrf54l15/NRF54L15Bluetooth.cppBluetoothPhoneAPI::onNowHasData and BleDeferredThread::runOnce now take a bt_conn_ref/unref around any notify on active_conn to close the race with disconnected_cb on another thread. The pendingToRadioBuf/pendingToRadioLen/pendingToRadio trio is now protected by K_MUTEX_DEFINE(pendingToRadioMutex), with the mutex held only for the memcpy and dropped before handleToRadio() runs.

Documentation / config inconsistencies:

  • variants/nrf54l15/nrf54l15dk/README.md — rewritten to describe the actual P2 (HP-domain) wiring through SPIM00 on the J2 header, matching variant.h and the Zephyr board overlay. The old Spanish README was from the early P1/SPIM20 prototype phase (that path failed because P1 runs at 1.8 V, below the SX1262's 2.31 V VIH).
  • variants/nrf54l15/nrf54l15dk/nrf54l15dk.overlaydeleted (stale SPIM20/P1 config from the same prototype phase; unused by the build). variant.cpp's comment now points at zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay which is the active overlay.
  • src/platform/nrf54l15/SPI.h — header comment updated (says SPIM00, references the correct overlay path).
  • zephyr/prj.conf — added CONFIG_BT_EXT_ADV=n explicitly; previously the comment block explained why legacy advertising is required on the nRF54L15 SW-LL but the value was only coming from Kconfig defaults.

Tested on hardware after push: clean boot, SX1262 init OK, iOS pairs + streams config + mesh TX/RX still working end-to-end.

@vidplace7 vidplace7 requested a review from Jorropo April 18, 2026 01:11
Comment thread src/platform/nrf54l15/architecture.h Outdated
@cvaldess cvaldess force-pushed the feature/nrf54l15-port branch from b337f86 to e1d77ce Compare April 18, 2026 10:28
@cvaldess

Copy link
Copy Markdown
Contributor Author

Force-pushed e1d77ce addressing @fifieldt's review comment on src/platform/nrf54l15/architecture.h.

Expanded the feature-flags comment block into two paragraphs covering (1) what the HAS_* macros do in practice and why defaulting them to 0 on this bare DK is deliberate, and (2) why HAS_* flags should be preferred over #ifdef ARCH_X sprinkled across shared code when absorbing platform divergence. The BLE/SoftDevice context that used to live in this block is folded into the second paragraph.

No behavior change — comment-only edit on a single commit. Amended + force-pushed to keep the PR as one squashed commit, matching the earlier rounds.

cvaldess and others added 4 commits April 20, 2026 12:43
Adds a community hardware variant for the Nordic nRF54L15-DK (PCA10156)
with an external EBYTE E22-900M30S (SX1262) LoRa module. First Meshtastic
port running on the Zephyr RTOS; all other Nordic targets use the nRF5
SoftDevice stack.

Scope
-----
- New Zephyr-based platform layer under src/platform/nrf54l15/ providing
  Arduino-compatible shims (Arduino.h, SPI, Wire, Print, Stream) over the
  Zephyr APIs plus a LittleFS-backed InternalFileSystem on SPIM20.
- Bluetooth LE peripheral (NRF54L15Bluetooth.*) built on the Zephyr BT
  host stack, exposing the Meshtastic GATT service with legacy
  connectable advertising, just-works pairing, dynamic MTU exchange
  (up to 247 bytes), and iOS connection-parameter tweaks.
- Variant directory variants/nrf54l15/nrf54l15dk/ with pin map for the
  E22 module on connector J1, PlatformIO env (nrf54l15dk), Zephyr
  DT overlay and a wiring README.
- Zephyr project config (zephyr/prj.conf + board overlay) tuned for
  BT + LoRa: 16 KB main stack, 4 KB BT RX thread, RTT logging in
  immediate mode, newlib-nano heap sized to leave room for the GATT
  pools while still allowing ATT MTU=247.
- extra_scripts/nrf54l15_linker.py works around a PlatformIO + old Ninja
  issue where Zephyr's two-pass linker script generation does not run
  automatically; the post-script parses build.ninja and invokes the
  gcc -E step directly before the final link.
- boards/nrf54l15dk.json board definition (PlatformIO needs it for the
  DK; the Seeed platform only ships the XIAO variants).
- variants/rp2350/rp2350.ini excludes platform/nrf54l15/ from RP2350
  build_src_filter so the shared platform tree does not leak between
  targets.
- .gitignore: add nRF J-Link / RTT debug artifacts (flash.jlink,
  rtt_*.txt).

Shared source changes
---------------------
- src/main.{cpp,h}, src/RedirectablePrint.cpp, src/FSCommon.{cpp,h},
  src/mesh/{Channels,NodeDB,RadioLibInterface,MeshService,PhoneAPI}.cpp,
  src/mesh/RadioLibInterface.h, src/modules/AdminModule.cpp: add small
  guards / helpers so the Zephyr build compiles alongside the Arduino
  targets. Behavior on existing boards is unchanged.

Hardware model
--------------
HW_VENDOR maps to meshtastic_HardwareModel_PRIVATE_HW until a dedicated
protobuf enum value is assigned upstream. The variant declares
custom_meshtastic_hw_model = 132 so the maintainers can wire the new
enum value through the protobufs repo after merge.

Hardware note
-------------
The E22-900M30S does not connect its DIO2 pin to TXEN internally — a
wire/solder bridge between DIO2 and TXEN on the module is required for
TX to work. Details and full pin map are in the variant README.

Validation
----------
Built clean against develop. On real hardware (April 2026) the port
passes end-to-end: iOS companion app pairs and connects, configuration
round-trip works, LoRa TX/RX reaches a canonical tbeam on the same mesh
channel, NodeDB updates propagate both ways, and traceroute completes.
Zephyr LittleFS on nrf54l15 supports fs_rename natively, so route it
through the same atomic path as ESP32. The previous copyFile+remove
fallback truncated the destination before copying, leaving 0-byte files
if interrupted mid-write.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LittleFS on the default 9-block (36KB) storage_partition ran out of
space during copy-on-write of config.proto, causing fs_write to return
ENOSPC and pb_encode to surface "io error" when saving configuration
via the mobile app.

Reclaim slot1_partition (the MCUboot secondary slot — unused since we
flash directly via J-Link) and grow storage_partition to span
0xb6000..0x165000 (~175 blocks).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NodeDB rewrites LoRa config from USERPREFS_LORACONFIG_* on every boot,
which prevented reconfiguration via the BLE/serial app. Drop the
variant-level defaults; users configure region and modem preset through
the app like every other variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cvaldess cvaldess force-pushed the feature/nrf54l15-port branch from 1bdda74 to f5210fa Compare April 20, 2026 10:43
cvaldess and others added 5 commits April 25, 2026 18:32
- Add MESH_PERM_READ/MESH_PERM_WRITE macros (READ_AUTHEN/WRITE_AUTHEN)
  on all mesh service characteristics so clients must complete passkey
  exchange before accessing fromNum/fromRadio/toRadio/logRadio.
- Wire FIXED_PIN mode to bt_passkey_set() so the device advertises a
  known PIN (config.bluetooth.fixed_pin); RANDOM_PIN keeps default
  per-pairing random passkey.
- Reduce BleDeferredThread HARD_WATCHDOG_MS from 3min to 1min.
- prj.conf: CONFIG_BT_SMP_ENFORCE_MITM=y, CONFIG_BT_FIXED_PASSKEY=y,
  CONFIG_BT_SMP_SC_PAIR_ONLY=n (legacy fallback for clients that abort
  SC pairing with reason 0x01 within 150ms).
The `Merge branch 'develop'` left two ~RadioLibInterface() declarations
in src/mesh/RadioLibInterface.h: the inline version added upstream by
PR meshtastic#10254 (which independently applied the same UAF guard this PR was
carrying) and the out-of-line version this PR introduced. GCC rejects
the duplicate, breaking every platform build. Drop the out-of-line
declaration + definition; keep upstream's inline form.

Also silence the 13 cppcheck low warnings introduced by the new
nrf54l15 Arduino shim — Arduino's `String`/`SPISettings` API contract
relies on implicit single-arg constructors used pervasively by
existing Meshtastic code, so suppress `noExplicitConstructor` inline
with a comment instead of breaking the API. The few mechanical wins
(`const tmp[2]`, `const uint32_t *sp`) are applied directly.
cvaldess and others added 3 commits May 13, 2026 14:42
bt_pub_key_gen() runs the ECC P256 key generation on bt_long_wq.  At default
prio=10 (preemptible) and stack=1400 it gets starved by Meshtastic app
threads at boot — sc_public_key stays NULL for minutes, smp_public_key()
defers with SMP_FLAG_PKEY_SEND, and every SC pairing attempt stalls right
after the public-key exchange.  iOS shows "Connecting…" forever with no
PIN prompt; bleak/CLI fails the first CCC notify write with
"Protocol Error 0x05: Insufficient Authentication".

Set CONFIG_BT_LONG_WQ_PRIO=0 (highest preemptible, ties with main) and
CONFIG_BT_LONG_WQ_STACK_SIZE=4096 (margin for the P256M driver frames).

Validated E2E with iOS Meshtastic app: bt_smp_pkey_ready fires within ~40 s
of boot, 20 SC Passkey Entry rounds complete with matching pcnf/cfm,
encrypt 0x01 / sec_level 0x04 (Authenticated MITM), bonded=1.
@cvaldess

Copy link
Copy Markdown
Contributor Author

Thanks @thebentern for the develop merge in 23d1fbd3f — that pulled in the latest upstream cleanly on top of 0f7b3f04e (the BLE pairing fix from yesterday).

Current status:

  • MERGEABLE + APPROVED
  • All 4 workflows on this HEAD are sitting in action_required (CI, Pull Request, Check PR Labels, Semgrep Differential Scan) — could a maintainer kick them when convenient?

Quick note on 0f7b3f04e: that commit raises CONFIG_BT_LONG_WQ_PRIO from 10 to 0 and bumps the long-workqueue stack to 4096 in zephyr/prj.conf. Without it, bt_pub_key_gen (ECC P256) gets starved on the long workqueue at boot, sc_public_key stays NULL, and every SC pairing attempt stalls right after the public-key exchange — no PIN prompt on iOS, and bleak/CLI fails the first CCC notify write with Protocol Error 0x05: Insufficient Authentication. Validated end-to-end on hardware: bt_smp_pkey_ready now fires ~40 s post-boot, 20 SC Passkey Entry rounds complete with matching pcnf/cfm, encrypt 0x01 / sec_level 0x04 (Authenticated + MITM), bonded=1.

@cvaldess cvaldess requested a review from thebentern May 13, 2026 23:25
cvaldess added 6 commits May 14, 2026 14:09
Adds the Arduino TwoWire layer for the nRF54L15-DK so Meshtastic's
sensor drivers can talk to external I2C devices over the hardware
TWIM30 peripheral.

Bus binding:
- &uart30 disabled in the board overlay (peripheral instance 30 is
  shared between UARTE30 / TWIM30 / SPIM30 — pick one). Console stays
  on RTT via CONFIG_RTT_CONSOLE.
- New i2c30_default / i2c30_sleep pinctrl with SDA=P0.03 / SCL=P0.04.
  External 4.7 kOhm pull-ups required on both lines.
- &i2c30 enabled at I2C_BITRATE_FAST (400 kHz).
- button_3 (SW3, P0.04) deleted from DTS so the pad can be claimed by
  i2c30 pinctrl; SW3 is still wired to the pad on the DK, do not press
  it during I2C use or it will short SCL to GND.

Arduino layer:
- src/platform/nrf54l15/Wire.cpp resolves the DT node at compile time
  via DEVICE_DT_GET(DT_NODELABEL(i2c30)) and dispatches Arduino's
  beginTransmission / write / endTransmission / requestFrom to
  i2c_write / i2c_write_read / i2c_read. Buffer is sized to 256 bytes
  for forward compatibility with the SE050 secure element on the
  custom PCB.
- Wire.h drops the prior compile-only stubs and exposes the real
  TwoWire surface.
- Arduino.h: BitOrder becomes an enum (not #define) so Adafruit_BusIO's
  `typedef BitOrder BusIOBitOrder;` compiles.

Variant + build flags:
- nrf54l15.ini flips HAS_WIRE / HAS_SENSOR / HAS_TELEMETRY from 0 to 1
  and cherry-picks the sensor libs Meshtastic needs (BusIO, Sensor,
  BMP280, BME280, INA219/226/260/3221, SHT4X). The full
  environmental_base group is avoided because it pulls
  Adafruit_SSD1306 / Adafruit_GFX which rely on Arduino pin macros the
  Zephyr shim does not implement.
- nrf54l15dk variant.h defines PIN_WIRE_SDA / PIN_WIRE_SCL for parity
  with the Arduino convention used by other variants. The actual bus
  wiring is fixed by the overlay pinctrl above.

Validated 2026-05-14/15 on the DK with BMP280 @ 0x76 (temperature +
barometric pressure) and INA3221 @ 0x42 (rail voltage / current);
EnvironmentTelemetry / PowerTelemetry packets transmit successfully
over LoRa.

Footprint cost on nrf54l15dk: +45 KB flash, +1.7 KB RAM.
installDefaultConfig() now respects two new compile-time prefs:

  USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL
  USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED

The mobile apps enforce a 30 min floor on environment_update_interval
in the settings UI, which makes short-interval bring-up testing of new
sensor hardware painful — you have to wait half an hour for the first
LoRa packet to confirm wiring + driver. With these prefs baked into
the variant, the firmware can ship a freshly-flashed device that
broadcasts on a shorter cadence (e.g. 900 s) the moment storage_partition
is empty.

Both prefs are gated on #ifdef so the behavior is unchanged for any
variant that does not opt in. Documented in userPrefs.jsonc with the
existing telemetry-interval pref block.
CONFIG_BT_MAX_PAIRED defaults to 1, so once the first peer (e.g. an
iOS phone) has paired and bonded, every subsequent pairing attempt
from a different MAC fails inside bt_keys_get_addr() with no free
key slot — the host returns BT_SECURITY_ERR_KEY_DOES_NOT_EXIST and
the second peer never gets past SMP.

Raise the slot count to 4 so the device can simultaneously hold an
iOS phone, a Windows host, a Linux host, and one spare bond. Add
BT_KEYS_OVERWRITE_OLDEST so that once the table fills, the LRU peer
is evicted on the next pairing rather than rejecting the new peer.
This matches the behavior other Meshtastic ports already provide
(nRF52 uses CONFIG_BT_PERIPHERAL_PRIO_CONN with similar semantics).

Discovered while bringing up the Python CLI on Windows alongside
the existing iOS bond.
cppcheck on every CI target (esp32s3, rp2040, rp2350, nrf52840, ...) was
failing the build with two `uninitMemberVar` warnings on TwoWire's
constructor: `txBuf` and `rxBuf` (256-byte arrays) were not initialized.
Even though the buffers are only read after txLen/rxLen is set, leaving
them uninitialized is a footgun if any future caller bypasses the
len-set step. Use C++11 value-initialization in the member initializer
list — costs ~512 B of memset at boot, gains a clean cppcheck pass and
defensive-against-future-changes semantics.

Also reformat Wire.{cpp,h} with the project's `.trunk/configs/.clang-format`
config so the Trunk Check Runner passes — clang-format moved the
`<errno.h>` include before the Zephyr-namespaced ones in Wire.cpp and
collapsed two inline overloads to single lines in Wire.h.
Comment thread src/modules/AdminModule.cpp Outdated
@cvaldess

Copy link
Copy Markdown
Contributor Author

Thanks for setting up meshtastic/zephyr @thebentern — really appreciate the offer.

Quick update so the fork doesn't end up as dead weight: the upstream fix already exists and was merged into Zephyr mainline back in late 2025 — zephyrproject-rtos/zephyr#97913 "Bluetooth: Host: Run tx_processor on its own thread" (commit 04b8dbae4, 2025-11-26). It adds CONFIG_BT_TX_PROCESSOR_THREAD which creates a dedicated bt_tx_processor_workq — semantically equivalent to my one-line workaround. Included in Zephyr v4.4.0 (released 2026-04-14). Not in v4.3.0.

The reason the patch is still showing up locally is that the PIO package this PR builds against — Seeed-Studio/platform-seeedboardsframework-zephyr@~3.40201.251021 — pins Zephyr v4.2.1, which predates the fix. The PIO registry already publishes v4.4.0 as framework-zephyr@3.40400.260428, but Seeed hasn't bumped their platform package to point at it yet.

So the actual target for the patch isn't zephyrproject-rtos/zephyr (where it's already fixed) — it's the framework-zephyr PlatformIO package consumed by Seeed-Studio/platform-seeedboards. Two clean paths off the local patch:

  1. Wait for Seeed to bump their platform to Zephyr ≥4.4, then drop the local edit and add CONFIG_BT_TX_PROCESSOR_THREAD=y to zephyr/prj.conf. Lowest risk; we just need them to cut the bump.
  2. Force the framework bump in nrf54l15.ini with platform_packages = framework-zephyr@~3.40400. Works, but the v4.4 migration guide touches several Kconfigs we use (bt_conn_le_info.intervalinterval_us, BT_FIXED_PASSKEY deprecation, BT_SIGNING deprecation, plus the LL split rebasing), so it would need a separate validation pass on hardware.

Happy to ping Seeed about (1) on this PR's behalf if useful, or to spike (2) on a follow-up branch. In either case, the meshtastic/zephyr fork doesn't strictly need a branch for this patch — but it's a great fallback for the next time we hit something that hasn't already landed upstream (the Windows-post-MTU LL bug is a likelier candidate; still tracking that one separately via zephyr#109217 + DevZone 360037).

Also just pushed 1336fbd — clean merge of upstream/develop to keep the branch current. No code changes, just the develop sync.

…nces

OUTPUT_GPIO_PIN is never defined and modules/GpioOutputModule.h doesn't
exist in the codebase; all #ifdef OUTPUT_GPIO_PIN branches were dead code
introduced by the nRF54L15-DK variant commit. Strips the include, the
output_gpio_enabled OFF→ON/ON→OFF transition logic in handleSetConfig(),
and the digitalRead() reflection in handleGetConfig().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@cvaldess cvaldess requested a review from thebentern May 16, 2026 10:48
@thebentern thebentern merged commit 4a1ff18 into meshtastic:develop May 16, 2026
70 checks passed
cvaldess added a commit to cvaldess/firmware that referenced this pull request May 20, 2026
SHTXXSensor (the SHT4x driver) includes <SHTSensor.h>, gated by
__has_include(<SHTSensor.h>). That header ships in Sensirion/arduino-sht.
Adafruit_SHT4X ships Adafruit_SHT4X.h and has no consumer anywhere in
src/, so the SHT40 driver was silently excluded from the build -- the
nRF54L15 variant could not read an SHT4x sensor as committed in meshtastic#10193.

Replace the dead Adafruit_SHT4X libdep with arduino-sht v1.2.6.
Validated on nRF54L15-DK: SHT40-AD1B @0x44, 24.3h soak, 0 reboots,
temperature/humidity telemetry stable end-to-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
thebentern added a commit that referenced this pull request May 21, 2026
SHTXXSensor (the SHT4x driver) includes <SHTSensor.h>, gated by
__has_include(<SHTSensor.h>). That header ships in Sensirion/arduino-sht.
Adafruit_SHT4X ships Adafruit_SHT4X.h and has no consumer anywhere in
src/, so the SHT40 driver was silently excluded from the build -- the
nRF54L15 variant could not read an SHT4x sensor as committed in #10193.

Replace the dead Adafruit_SHT4X libdep with arduino-sht v1.2.6.
Validated on nRF54L15-DK: SHT40-AD1B @0x44, 24.3h soak, 0 reboots,
temperature/humidity telemetry stable end-to-end.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
@kartben

kartben commented May 25, 2026

Copy link
Copy Markdown

@cvaldess would be curious to hear you thoughts if you end up playing with https://github.com/kartben/meshtastic-zephyr :)
BTW there is already an Arduino Core for Zephyr which would probably be a better fit than your "custom" shim layer: https://github.com/zephyrproject-rtos/ArduinoCore-zephyr

@cvaldess

Copy link
Copy Markdown
Contributor Author

@
Hey @kartben — thanks for the pointers, both are squarely on my radar 🙏

Quick context on why I went with the in-tree shim for this PR rather than pulling in ArduinoCore-zephyr:

  1. Keep the PR reviewable. The port already touched ~50 files / +4.6k LOC just for the variant itself. Adding a brand-new framework dependency (and the PlatformIO platform/builder plumbing it would have needed) would have pushed it well past whats reasonable to land as a single PR.
  2. Symmetry with the existing nrf52840 port. Meshtastic core code is written against the Adafruit nRF52 Arduino core surface (NRF52Bluetooth.h, bluefruit.h, InternalFileSystem, etc.). The shim deliberately mimics that surface so the core compiles unchanged on nRF54L15 — i.e. its a translation layer between the existing nrf52 expectations and Zephyr, not a generic Arduino layer. ArduinoCore-zephyr would have required intrusive changes in the core to switch APIs.
  3. PlatformIO story. The whole build is PIO-driven. ArduinoCore-zephyrs PIO integration isnt something I had a known path for at the time; the shim let me stay on the existing framework=zephyr PIO platform from Seeed and ship.
  4. Minimal surface, minimal mystery. Only the subset Meshtastic actually uses is wrapped, which kept the bug surface small while I was chasing the real fires (BLE host TX starvation, SPIM00 freq constraints, storage_partition sizing, etc.).

That said — none of this is a religious choice. Once ArduinoCore-zephyrs PIO integration is solid and the shim starts feeling like the long pole, swapping it out is the obvious next step, and Id happily take a follow-up PR in that direction. Ill definitely take your meshtastic-zephyr fork for a spin — going Zephyr-native end-to-end is the cleaner endgame for this MCU family, no question.
@

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

hardware-support Hardware related: new devices or modules, problems specific to hardware needs-review Needs human review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants