Files
firmware/zephyr/prj.conf
Carlos Valdes 4a1ff18f57 feat: add Nordic nRF54L15-DK variant (Zephyr + BLE + LoRa) (#10193)
* feat: add Nordic nRF54L15-DK variant (Zephyr + BLE + LoRa)

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.

* fix(nrf54l15): use atomic fs_rename instead of copy fallback

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>

* fix(nrf54l15): expand storage_partition from 36KB to 700KB

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>

* fix(nrf54l15): drop USERPREFS_LORACONFIG_* so LoRa config stays mutable

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>

* fix(nrf54l15): enforce MITM passkey pairing on GATT service

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

* fix(nrf54l15): resolve develop-merge conflict + cppcheck warnings

The `Merge branch 'develop'` left two ~RadioLibInterface() declarations
in src/mesh/RadioLibInterface.h: the inline version added upstream by
PR #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.

* fmt: fix Trunk Check lint issues on nrf54l15-port

- extra_scripts/nrf54l15_linker.py: move regular imports above
  Import("env") to silence E402, add trunk-ignore-all(F821) for the
  PIO/SCons SConstruct injection (matches esp32_pre.py / nrf52_extra.py
  convention)
- src/platform/nrf54l15/NRF54L15Bluetooth.cpp: clang-format 16.0.3
- boards/nrf54l15dk.json + variants/nrf54l15/nrf54l15dk/README.md:
  prettier 3.8.3 (also resolves markdownlint MD060 on README tables)

No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(nrf54l15): address Copilot review comments + correct clang-format style

Six review threads from the 2026-04-30 Copilot review:

- src/platform/nrf54l15/nrf54l15_main.cpp: validate PSP against the nRF54L15
  SRAM range (0x20000000..0x20040000) and 4-byte alignment before walking the
  faulting thread's stack, and clamp the walk so it never reads past the end
  of RAM. Prevents a second fault inside the fatal handler when PSP is
  corrupted (common in real faults).

- src/platform/nrf54l15/nrf54l15_arduino.cpp: gate the bring-up printk traces
  in digitalWrite/digitalRead (CS/NRESET toggle log, BUSY-before-NRESET
  snapshot, BUSY periodic timeline) behind a new -DNRF54L15_GPIO_DEBUG flag
  that is off by default. The "dev NOT READY" message stays unconditional —
  it indicates a genuine hardware/DTS misconfig.

- src/modules/AdminModule.cpp: don't mutate config.device.output_gpio_enabled
  from handleGetConfig(). Reflect the live pin state in the response payload
  only — a getter must not write back to disk-persisted state.

- src/platform/nrf54l15/InternalFileSystem.h: derive totalBytes() from
  FIXED_PARTITION_SIZE(storage_partition) at compile time so it tracks the
  DK overlay's ~700 KB partition instead of the stale 36 KB hard-coded value.
  Updated the file header comment accordingly.

- extra_scripts/nrf54l15_linker.py: make _extract_gcc_command() handle the
  POSIX Ninja COMMAND format (no `cmd.exe /C "..."` wrapper) in addition to
  the Windows form, so the script doesn't hard-fail on Linux/macOS hosts.

- src/platform/nrf54l15/NRF54L15Bluetooth.cpp: clamp NO_PIN to RANDOM_PIN
  with a one-shot LOG_WARN. The mesh GATT permissions are declared with
  BT_GATT_PERM_*_AUTHEN and prj.conf sets CONFIG_BT_SMP_ENFORCE_MITM=y, so
  NO_PIN with no auth callbacks would leave every characteristic returning
  BT_ATT_ERR_AUTHENTICATION. Falling back to RANDOM_PIN keeps the link
  usable instead of silently broken. Also re-formatted this file with the
  project's .trunk/configs/.clang-format (Linux braces, 4-space indent,
  130-col) — the previous lint-fix commit a2aca3234 accidentally used the
  default LLVM style here, which CI's clang-format would have rejected.

Build verified: pio run -e nrf54l15dk passes, RAM 47.4%, Flash 28.6%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(nrf54l15): address remaining Copilot review threads

Round 2/3 review fixes — bugs first, then docs/portability:

BLE concurrency (NRF54L15Bluetooth.cpp):
- onNowHasData / sendLog / BleDeferredThread / shutdown: acquire
  active_conn under ble_mutex via new acquire_active_conn() helper so
  disconnected_cb can't free the conn between the null check and
  bt_conn_ref/bt_gatt_notify (use-after-unref).
- write_toradio: reject writes that exceed MAX_TO_FROM_RADIO_SIZE with
  ATT_ERR_INVALID_ATTRIBUTE_LEN instead of returning success and silently
  dropping the payload (would hide failed config writes from the phone).
- start_advertising: truncate the device name to fit the 31-byte legacy
  scan-response limit and switch to BT_DATA_NAME_SHORTENED so
  bt_le_adv_start() doesn't reject the payload when the name approaches
  CONFIG_BT_DEVICE_NAME_MAX=32.

Linker / portability:
- main.h: drop the rp2040Loop() forward declaration that had no
  definition and no callers — would surface as a link error if any RP2040
  build added a call to the symbol.
- nrf54l15_arduino.cpp: transfer16() now uses static __aligned(4) DMA
  buffers (matching transfer()), removing the EasyDMA-reachability hazard
  of caller-stack buffers on this part.

Filesystem (InternalFileSystem.h):
- usedBytes(): return real usage from fs_statvfs() instead of 0 so OTA
  / range-test free-space guards work.
- rewindDirectory(): close the dir before reopening — Zephyr fs_dir_t has
  no rewind, and re-fs_opendir on an open handle leaks LittleFS state.

Crash handler (nrf54l15_main.cpp):
- After the stack walk, busy-wait 50 ms to flush RTT/printk and call
  sys_reboot(SYS_REBOOT_COLD) directly so the saved_crash record is
  actually reported on the next boot. Default Zephyr config has
  RESET_ON_FATAL_ERROR=n, so the previous k_fatal_halt() spun forever.

Generalization / config:
- PhoneAPI.cpp: replace the NRF54L15_DK ifdef with a
  MESHTASTIC_EXCLUDE_FILES_MANIFEST capability flag (defined in the
  nrf54l15dk env) so future variants can opt in/out without touching
  shared code.
- variants/nrf54l15/nrf54l15.ini: parameterize libdeps include paths via
  ${PIOENV} so additional nRF54L15 envs sharing nrf54l15_base don't break.
- prj.conf: drop the stale "36 KB storage_partition" comments — the DK
  overlay reclaims slot1 to ~700 KB and runtime size comes from
  FIXED_PARTITION_SIZE.
- nrf54l15dk overlay: remove the zephyr,console / zephyr,shell-uart
  chosen entries that conflicted with CONFIG_UART_CONSOLE=n + RTT
  console; keep uart30 enabled so swapping the console is one Kconfig
  flip away.

Build: nrf54l15dk SUCCESS (flash 28.6%, RAM 47.4%); wiznet_5500_evb_pico2
SUCCESS (verifies the shared main.h change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(nrf54l15dk): use PRIVATE_HW (255) for custom_meshtastic_hw_model

Per @thebentern's review: the nRF54L15-DK is a development kit, not a
canonical Meshtastic SKU, so it falls under HardwareModel::PRIVATE_HW
(255) — the same enum value already used at runtime via HW_VENDOR. The
placeholder 132 is removed; no dedicated enum number will be assigned
for DK boards. Slug stays NRF54L15_DK as a human-readable identifier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(nrf54l15): unstarve bt_long_wq so SC pairing completes

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.

* feat(nrf54l15): hardware I2C bus via TWIM30 + sensor telemetry

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.

* feat(nodedb): honor USERPREFS for environment telemetry on first boot

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.

* fix(nrf54l15): allow multiple bonded BLE peers

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.

* fix(nrf54l15): zero-initialize TwoWire buffers + clang-format Wire

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.

* fix(AdminModule): remove dead OUTPUT_GPIO_PIN/GpioOutputModule references

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>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-05-16 06:16:11 -05:00

300 lines
17 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Zephyr project configuration for nRF54L15 Meshtastic port
#
# NOTE: this prj.conf is shared by ALL Zephyr PlatformIO environments
# in this project. Keep it compatible with any future Zephyr targets.
# ── C++ support (required by Meshtastic) ──────────────────────────────────────
CONFIG_CPP=y
CONFIG_STD_CPP17=y
# Full libstdc++ — provides <functional>, <list>, <string>, <vector>, etc.
# Works with either newlib or picolibc (Zephyr auto-selects based on board).
CONFIG_REQUIRES_FULL_LIBCPP=y
# Disable C++ exceptions — not needed by Meshtastic and saves RAM/ROM
CONFIG_CPP_EXCEPTIONS=n
# ── Peripheral subsystems ─────────────────────────────────────────────────────
CONFIG_SPI=y
CONFIG_I2C=y
CONFIG_GPIO=y
# sys_reboot() used by BLE zombie-connection watchdog (BleDeferredThread).
# nRF54L15 SW-LL occasionally drops the BLE link without forwarding
# LE Disconnection Complete to the host; cold reboot is the only reliable
# recovery path.
CONFIG_REBOOT=y
# ── ATT Prepare/Execute Write (LONG WRITE) ───────────────────────────────────
# iOS CoreBluetooth automatically fragments writes >MTU-3 via ATT Prepare Write
# (opcode 0x16). Default CONFIG_BT_ATT_PREPARE_COUNT=0 makes Zephyr reject with
# "Request Not Supported" (0x06), which iOS surfaces as a write error →
# disconnect. With MTU=65 any ToRadio write >62 bytes triggers this path.
# Enabling this allocates N prep_pool buffers (each BT_ATT_BUF_SIZE = 65 bytes)
# and reassembles fragments into a single write_toradio() call on execute.
# 4 × 65 = 260 B max assembled value — enough for typical iOS NodeInfo/admin
# writes after config stream completes.
CONFIG_BT_ATT_PREPARE_COUNT=4
# ── Filesystem — LittleFS on storage_partition (RRAM) ───────────────────────
# Size is set by the board overlay (the nRF54L15-DK overlay reclaims slot1 to
# expand storage_partition to ~700 KB). Capacity is reported at runtime via
# FIXED_PARTITION_SIZE(storage_partition) in InternalFileSystem::totalBytes().
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_FILE_SYSTEM=y
CONFIG_FILE_SYSTEM_LITTLEFS=y
CONFIG_FILE_SYSTEM_MKFS=y
# Disable SPI NOR flash driver — MX25R64 node deleted from DTS, SPIM00 used
# exclusively by RadioLib (SX1262). Without this, the spi_nor driver claims
# SPIM00 at boot and tries to read MX25R64 ID (gets garbage since the chip is
# not wired), producing "Device id a8 a8 a8 does not match config c2 28 17".
CONFIG_SPI_NOR=n
# Disable runtime PM — keeps SPI initialization path simple; avoids any
# interaction between PM auto-suspend/resume cycles and the SPIM00 clock
# request mechanism (CONFIG_CLOCK_CONTROL_NRF_HSFLL_GLOBAL).
CONFIG_PM_DEVICE_RUNTIME=n
# Suppress Zephyr FS subsystem's internal error/warning logs (ENOENT on
# missing files and EEXIST on duplicate mkdir are expected and handled).
CONFIG_FS_LOG_LEVEL_OFF=y
# ── Console / logging ─────────────────────────────────────────────────────────
# Use SEGGER RTT for console — does not require COM3 (CDC UART), reads via SWD
CONFIG_UART_CONSOLE=n
CONFIG_USE_SEGGER_RTT=y
CONFIG_RTT_CONSOLE=y
CONFIG_LOG=y
CONFIG_LOG_BACKEND_RTT=y
CONFIG_LOG_DEFAULT_LEVEL=2
# Immediate mode: log writes go directly to RTT backend without a separate thread.
# Deferred mode requires the log thread to run (lowest priority — never gets CPU
# in heavy setup()/loop() workloads), leaving the RTT buffer empty indefinitely.
CONFIG_LOG_MODE_IMMEDIATE=y
# Force RTT control block re-init on every boot — prevents stale/corrupted CB after crash
CONFIG_SEGGER_RTT_INIT_MODE_ALWAYS=y
# Use RTT channel 1 for the LOG backend, channel 0 (Terminal) for direct printk.
# Sharing channel 0 forces LOG_PRINTK=y (deferred) to avoid corruption.
CONFIG_LOG_BACKEND_RTT_BUFFER=1
# Buffer sizes shrunk from 24576 → 4096 to free ~40 KB of BSS for newlib heap.
# At 24576 the BSS pushed _end up so far that newlib heap was only ~25 KB,
# and BUF_ACL_RX_SIZE=152 + BLE/PhoneAPI lazy init ran out of malloc space.
# 4 KB still gives several seconds of log retention before host attaches.
CONFIG_LOG_BACKEND_RTT_BUFFER_SIZE=4096
# Overwrite oldest data if buffer fills — never stalls
CONFIG_LOG_BACKEND_RTT_MODE_BLOCK=n
CONFIG_LOG_BACKEND_RTT_MODE_OVERWRITE=y
CONFIG_LOG_BACKEND_RTT_OUTPUT_BUFFER_SIZE=256
# ── LFXO clock source — use RC oscillator to avoid ~2s crystal stabilization
# disrupting the GRTC timer and hanging k_sleep
CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y
# ── Stack sizes — Meshtastic setup() is heavy (RadioLib, NodeDB, printf) ──────
# bt_enable() called from nrf54l15Setup() needs >8KB.
# Phase 7: CONFIG_BT_SETTINGS=y causes bt_set_name() → settings_save_one() →
# settings_file_save() → LittleFS I/O. The I/O chain needs ~3 KB of stack
# headroom beyond what the BT init alone requires. Increase main stack to 24KB
# and system workqueue to 8KB to cover both the cooperative-OSThread call path
# (which runs on the main thread) and any async flash work items.
CONFIG_MAIN_STACK_SIZE=24576
CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=8192
# Log processing thread stack — default 768 overflows when processing RTT fault dump
CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=2048
# ── Fault/exception diagnostics — identify ~2000ms crash ─────────────────────
CONFIG_FAULT_DUMP=2
CONFIG_EXCEPTION_DEBUG=y
CONFIG_STACK_SENTINEL=y
# Thread names + extra exception info — fault dumps then identify the failing
# thread (otherwise "Current thread: 0x... (unknown)") and include r4-r11 + psp
# so the custom k_sys_fatal_error_handler can walk the stack to show the caller
# chain. Cheap (~32 B/thread for names, no perf hit) and very useful when a
# crash recurs in the field.
CONFIG_THREAD_NAME=y
CONFIG_EXTRA_EXCEPTION_INFO=y
# SEGGER RTT buffer — keep modest (4 KB) to leave RAM for newlib heap.
CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=4096
# Report reset reason from previous crash
CONFIG_HWINFO=y
# ── Bluetooth ─────────────────────────────────────────────────────────────────
# Zephyr BT host + LL SW controller (MPSL) — peripheral role only
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
# SMP / LE Encryption enabled so the Meshtastic app pairs with a PIN before
# accessing the GATT service.
CONFIG_BT_SMP=y
# Enforce MITM so clients must complete passkey exchange — without this Just
# Works pairings complete silently without prompting the user for a PIN.
CONFIG_BT_SMP_ENFORCE_MITM=y
# Fixed-passkey path so the device (no display) can advertise a known PIN via
# bt_passkey_set() when config.bluetooth.mode == FIXED_PIN.
CONFIG_BT_FIXED_PASSKEY=y
# Allow legacy pairing as fallback. SC_PAIR_ONLY=y has been observed to cause
# some clients to abort pairing with reason 0x01 within 150 ms of the pairing
# request, before any PIN dialog appears. Accepting legacy lets the same
# clients fall through to Passkey Entry successfully.
CONFIG_BT_SMP_SC_PAIR_ONLY=n
# BT_LL_SW_SPLIT is auto-selected from DT (zephyr,bt-hci-ll-sw-split node in nRF54L15 DTS)
# Do NOT set CONFIG_BT_CTLR=y (deprecated — radio silently non-functional)
# Dynamic device name so bt_set_name() can embed the node short ID at runtime
CONFIG_BT_DEVICE_NAME_DYNAMIC=y
CONFIG_BT_DEVICE_NAME_MAX=32
# Only need one simultaneous central connection
CONFIG_BT_MAX_CONN=1
# BT subsystem logging — INF for connection/service diagnostics
CONFIG_BT_LOG_LEVEL_INF=y
# BT thread stacks — defaults are too small for nRF54L15 SW-LL init.
# prio_recv_thread overflows at 2048; bump all BT stacks to safe sizes.
# BT RX thread runs ALL our GATT write callbacks (rx_work_handler → hci_acl →
# bt_conn_recv → bt_l2cap_recv → bt_att_recv → write_toradio_cb →
# PhoneAPI::handleStartConfig → getFiles("/", 10) recursion + nanopb encode).
# 4096 overflows on "Client wants config" → abort() / kernel panic (reason 4).
CONFIG_BT_RX_STACK_SIZE=4096
CONFIG_BT_HCI_TX_STACK_SIZE=1024
# bt_long_wq runs bt_pub_key_gen (ECC P256 keygen) on this thread.
# Defaults (prio=10, stack=1400) leave it starved by Meshtastic app threads
# at boot: pub_key gen never completes, so smp_public_key() defers
# indefinitely waiting for sc_public_key, and every SC pairing attempt
# stalls right after exchanging public keys (no PIN prompt on iOS, every
# AUTHEN-gated char rejects with ATT error 0x05).
# Prio 0 = highest preemptible, ties with main; stack 4096 clears P256M
# driver frames with margin.
CONFIG_BT_LONG_WQ_PRIO=0
CONFIG_BT_LONG_WQ_STACK_SIZE=4096
# Use legacy advertising (bt_le_adv_start / HCI 0x2006 path).
# With CONFIG_BT_EXT_ADV=y, bt_le_adv_start() is internally translated to the
# extended HCI path with LEGACY-bit (0x2036), which produces non-connectable PDUs
# on the nRF54L15 SW-LL. With CONFIG_BT_EXT_ADV=n the host uses pure legacy HCI
# commands (0x2006/0x2008/0x200a) — the same path Nordic uses in all nRF54L15
# NCS examples (peripheral_uart, peripheral_lbs), which is iOS-compatible and
# avoids the LE Remove Advertising Set (0x203c) controller timeout crash.
CONFIG_BT_EXT_ADV=n
# ── Phase 7: BT bond persistence ──────────────────────────────────────────────
# CONFIG_BT_SETTINGS enables the BT host settings integration: the stack
# automatically calls settings_save_subtree("bt/keys") after pairing, and
# settings_load() on boot restores previously bonded peers.
#
# Backend: SETTINGS_FILE stores all key-value pairs in a single flat file in
# LittleFS. No new partition needed — the existing storage_partition (mounted
# at /lfs, size set by the board overlay) is used. File path: /lfs/bt_settings.
#
# Ordering guarantee: LittleFS is mounted by fsInit() BEFORE nrf54l15Setup()
# calls nrf54l15_bt_preinit(), so the file backend is always available when
# settings_load() is called after bt_enable().
CONFIG_BT_SETTINGS=y
CONFIG_SETTINGS=y
CONFIG_SETTINGS_FILE=y
CONFIG_SETTINGS_FILE_PATH="/lfs/bt_settings"
# BT_MAX_PAIRED default is 1 — first bond (e.g. iOS) blocks every subsequent
# peer's SMP pairing request with "Unable to get keys" because there is no free
# bt_keys slot to allocate. Raise to 4 so the device can simultaneously hold
# iOS, Windows, Linux, and one spare bond. Add OVERWRITE_OLDEST so that when
# the table fills, the LRU peer is evicted instead of rejecting the new pair.
CONFIG_BT_MAX_PAIRED=4
CONFIG_BT_KEYS_OVERWRITE_OLDEST=y
# Disable GATT database caching and Service Changed characteristic.
# CONFIG_BT_GATT_CACHING (default y with BT_SETTINGS) marks every new client as
# "not change-aware" and returns ATT_ERR_DB_OUT_OF_SYNC (0x12) on every GATT
# request until the client reads the DB-hash characteristic. The Meshtastic app
# does not implement GATT caching and silently aborts service discovery on 0x12,
# causing the connection to stall with zero GATT activity.
# CONFIG_BT_GATT_SERVICE_CHANGED (default y) adds the Generic Attribute Profile
# service; disabling it is required before BT_GATT_CACHING can be disabled.
CONFIG_BT_GATT_SERVICE_CHANGED=n
CONFIG_BT_GATT_CACHING=n
# Disable automatic PHY update (1M→2M) after connection.
# The nRF54L15 SW-LL fails the LL_PHY_REQ/RSP exchange and disconnects
# exactly 1.786s after connection — before any ATT/GATT operations.
CONFIG_BT_AUTO_PHY_UPDATE=n
# ATT/GATT/L2CAP debug logging — see exactly what happens after connection
CONFIG_BT_ATT_LOG_LEVEL_DBG=y
CONFIG_BT_GATT_LOG_LEVEL_DBG=y
CONFIG_BT_SMP_LOG_LEVEL_DBG=y
# L2CAP DBG: shows recv on fixed ATT channel — confirms whether iOS sends any data
CONFIG_BT_L2CAP_LOG_LEVEL_DBG=y
# Keep bt_conn at INF — DBG floods RTT buffer every ~150µs (tx_processor loop),
# overwriting all ATT/GATT messages before they can be read.
# Connection events (connected/disconnected) are logged at INF level.
CONFIG_BT_CONN_LOG_LEVEL_INF=y
# Keep HCI logs at INF to save RAM (log thread processing buffers, etc.).
# (Earlier DBG was used to diagnose the hci_acl → L2CAP stall — fix applied.)
CONFIG_BT_HCI_CORE_LOG_LEVEL_INF=y
CONFIG_BT_HCI_DRIVER_LOG_LEVEL_INF=y
# Fix: ACL packets reach hci_acl() but never reach bt_l2cap_recv().
# Root cause: bt_conn_recv() calls bt_conn_tx_notify(conn, true) which submits
# tx_complete_work to k_sys_work_q and blocks on k_work_flush(). The BT rx
# workqueue (bt_workq) is stuck in k_work_flush waiting for the system
# workqueue, which is busy with LittleFS I/O / other work → dead stall until
# iOS supervision timeout fires (5s) and disconnects with reason 0x13.
# Solution: dedicate a separate workqueue for TX notify processing so it is
# independent from the system workqueue.
CONFIG_BT_CONN_TX_NOTIFY_WQ=y
# Dedicated workqueue only runs tx_notify_process() (iterates tx_complete list,
# calls short callbacks). Default 8192 is overkill and eats malloc heap needed
# by PowerFSM init → realloc() returns NULL → bus fault during FSM::add_transition.
CONFIG_BT_CONN_TX_NOTIFY_WQ_STACK_SIZE=2048
# ── ATT/L2CAP MTU — larger payloads for Meshtastic packets ───────────────────
# TX side: controller sends up to L2CAP_TX_MTU bytes per ATT operation.
# RX side: server ATT MTU is min(BT_L2CAP_TX_MTU, BT_BUF_ACL_RX_SIZE - 4).
# Both set to 247 / 251 → ATT MTU = 247 in each direction, matching Zephyr's
# samples/bluetooth/mtu_update reference. This means typical iOS ToRadio
# writes (NodeInfo, channel settings, common admin packets) fit in a single
# ATT_WRITE_REQ and avoid the ATT Prepare/Execute Write path entirely.
# CONFIG_BT_ATT_PREPARE_COUNT=4 (above) still backstops oversized writes.
#
# Heap dependency: bumping BUF_ACL_RX_SIZE > default (~69) grows the BT host
# net_buf pools in BSS, which proportionally shrinks the newlib heap arena
# (MAX_HEAP_SIZE = SRAM_SIZE - (_end - SRAM_BASE), so any BSS growth steals
# from the heap directly). Empirically the lazy BLE init path
# (setBluetoothEnable → startDisabled → bt_set_name → settings_save →
# LittleFS) needs ~12 KB of newlib heap to run without bad_alloc. At
# BUF_ACL_RX_SIZE=251 with the previous 24 KB RTT buffers (LOG_BACKEND_RTT +
# SEGGER_RTT_BUFFER_SIZE_UP), the heap collapsed to ~4 KB free at
# transition time → `new char[]` in RedirectablePrint::log returned NULL →
# libstdc++ called abort() from main thread. Shrinking both RTT buffers to
# 4 KB (above) frees ~40 KB of BSS for the heap and resolves it.
#
# DLE stays off (BT_DATA_LEN_UPDATE=n below): the LLCP remote table at
# ull_llcp_remote.c:878 is guarded by #ifdef CONFIG_BT_CTLR_DATA_LENGTH, so
# the controller answers iOS's LL_LENGTH_REQ with LL_UNKNOWN_RSP and falls
# back to 27-byte LL PDUs. The host reassembles LL PDUs into L2CAP frames
# up to BT_BUF_ACL_RX_SIZE before dispatching to ATT.
CONFIG_BT_L2CAP_TX_MTU=247
# Server ATT MTU = BUF_ACL_RX_SIZE - 4 = 247 (matches L2CAP_TX_MTU)
CONFIG_BT_BUF_ACL_RX_SIZE=251
# ── Fix: LL Feature Exchange collision (ROOT CAUSE of iOS GATT hang) ─────────
# On connection, Zephyr host calls bt_hci_le_read_remote_features() because
# BT_CTLR_PER_INIT_FEAT_XCHG=y makes can_initiate_feature_exchange() return
# true for peripheral role. This makes the controller send LL_PER_INIT_FEAT_XCHG
# to iOS right after connecting.
# iOS (as central) simultaneously sends LL_FEATURE_REQ to the peripheral.
# The nRF54L15 SW-LL mishandles this COLLISION: iOS waits for LL_FEATURE_RSP
# to its LL_FEATURE_REQ, never gets it, and stalls — sending zero L2CAP bytes.
# BT_CTLR_PER_INIT_FEAT_XCHG=n: host does NOT send HCI_LE_Read_Remote_Features
# as peripheral → no LL_PER_INIT_FEAT_XCHG sent → no collision → iOS feature
# exchange completes → iOS proceeds to L2CAP/ATT.
CONFIG_BT_CTLR_PER_INIT_FEAT_XCHG=n
# Fix: LL Connection Parameter Request handling.
# BT_CTLR_CONN_PARAM_REQ=n was ineffective: the LLCP remote decode table in
# ull_llcp_remote.c hardcodes PDU_DATA_LLCTRL_TYPE_CONN_PARAM_REQ → PROC_CONN_PARAM_REQ
# regardless of Kconfig. With =n the handler is compiled out → controller asserts /
# enters broken state when iOS sends LL_CONN_PARAM_REQ (which is optional from Central).
# Fix: =y so the procedure is actually handled. To avoid host/peripheral vs Central
# collision at 5 s (deferred_work → send_conn_le_param_update), disable auto-update
# below so the host never initiates HCI_LE_Connection_Update.
CONFIG_BT_CTLR_CONN_PARAM_REQ=y
# Prevent Zephyr host from initiating connection parameter update 5 s after connect.
# With CONN_PARAM_REQ=y, if iOS (Central) already issued LL_CONN_PARAM_REQ and the
# SW-LL is mid-procedure, a simultaneous host-initiated HCI_LE_Connection_Update
# creates an LL collision. Disabling the auto-update avoids the collision entirely.
CONFIG_BT_GAP_AUTO_UPDATE_CONN_PARAMS=n
# Disable optional LL procedures (belt-and-suspenders while debugging):
# BT_PHY_UPDATE=n + BT_CTLR_PHY_2M=n: no PHY update procedure → iOS doesn't attempt LL_PHY_REQ
# BT_DATA_LEN_UPDATE=n: no DLE → controller sends LL_UNKNOWN_RSP to LL_LENGTH_REQ
CONFIG_BT_CTLR_PHY_2M=n
CONFIG_BT_PHY_UPDATE=n
CONFIG_BT_DATA_LEN_UPDATE=n