# 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
