# SPDX-License-Identifier: MIT source "$(ZEPHYR_BASE)/Kconfig.zephyr" # DT-driven PSRAM auto-enable for ESP32 boards (see Kconfig.psram). rsource "Kconfig.psram" module = ZEPHCORE_MAIN module-str = zephcore_main source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" module = ZEPHCORE_BLE module-str = zephcore_ble source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" module = ZEPHCORE_LORA module-str = zephcore_lora source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" module = ZEPHCORE_BOARD module-str = zephcore_board source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" module = ZEPHCORE_DATASTORE module-str = zephcore_datastore source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" module = ZEPHCORE_GPS module-str = zephcore_gps source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" module = ZEPHCORE_SENSORS module-str = zephcore_sensors source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" module = ZEPHCORE_USB module-str = zephcore_usb source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" module = ZEPHCORE_UI_ACTIONS module-str = zephcore_ui_actions source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" module = ZEPHCORE_WIFI_OTA module-str = zephcore_wifi_ota source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" module = ZEPHCORE_OBSERVER module-str = zephcore_observer source "$(ZEPHYR_BASE)/subsys/logging/Kconfig.template.log_config" menu "ZephCore" config ZEPHCORE_QSPI_RDID_PROBE bool "QSPI flash bring-up probe" help Five seconds after boot, report whether the QSPI flash driver came up. If it did not, print the init errno the driver discards and re-read the JEDEC id by bit-banging the pins as single-lane SPI, which distinguishes a driver-side fault from a part that is not answering. Skipped entirely when the flash is working, so it is safe to leave enabled. Bring-up diagnostic. config ZEPHCORE_RESET_ON_FATAL_ERROR bool "Reboot on fatal error" default y help Cold-reboot instead of halting when a fatal error occurs (CPU exception, stack overflow, k_panic, kernel oops). Production default; turn it off (boards/common/debug.conf does) so a debugger can inspect the halted core. Implemented via a k_sys_fatal_error_handler() override in helpers/fatal_reboot.c — Zephyr has no built-in Kconfig for this. menu "Device Role" choice ZEPHCORE_ROLE prompt "Device Role" default ZEPHCORE_ROLE_COMPANION help Select the device role. Companion devices connect to mobile apps via BLE. Repeaters forward packets and are configured via USB serial CLI. config ZEPHCORE_ROLE_COMPANION bool "Companion Device" help BLE companion device that connects to ZephCore mobile app. Includes BLE stack, contact management, offline queue, channels. config ZEPHCORE_ROLE_REPEATER bool "Repeater" help LoRa mesh repeater with USB serial CLI interface. No BLE stack. Has ACL authentication, region filtering, neighbor tracking, and full CLI command set. config ZEPHCORE_ROLE_OBSERVER bool "Observer (listen-only, WiFi+MQTT)" help ESP32-only listen-only node. Receives LoRa packets without forwarding. Connects to WiFi (STA mode) and publishes received packets to an MQTT broker in meshcoretomqtt-compatible format. All parameters (WiFi SSID/PSK, MQTT host/port/TLS/user/pass, IATA location code) are configured at runtime via serial CLI and stored in LittleFS. No BLE, no advertising, no routing. config ZEPHCORE_ROLE_ROOM_SERVER bool "Room Server (shared BBS, USB serial CLI)" help Store-and-forward shared message room (a "BBS"). Clients log in with an admin or guest password and post messages; the server pushes each new post to all other logged-in clients and tracks a per-client sync cursor. No BLE — configured via USB serial CLI. Reuses the repeater's ACL, region filtering and CLI command set. endchoice config ZEPHCORE_COMPANION_USB bool "Enable USB CDC companion transport" depends on USB_DEVICE_STACK_NEXT default y if ZEPHCORE_ROLE_COMPANION help Compile and init the USB CDC-ACM companion protocol stack (ZephyrCompanionUSB / ZephyrUSBCDC) independently of CONFIG_LOG. The full ~60-opcode binary protocol is available over USB even in production builds (CONFIG_LOG=n). USB and BLE arbitrate the active interface first-come-first-served: CMD_APP_START claims it for USB only if no BLE session is already active (and is ignored while BLE is live); on USB unplug the interface is handed back to BLE or released to idle. Enabled by default for all companion builds on platforms that already have CONFIG_USB_DEVICE_STACK_NEXT=y (all nRF52840 boards). On platforms without USB hardware (MG24, ESP32 classic, C3/C6) the depends forces this to n automatically — no conf change needed. For ESP32-S3 boards, USB_DEVICE_STACK_NEXT must be enabled first via boards/common/esp32s3_usb.conf before this becomes active. CMakeLists.txt adds that conf automatically to companion builds on the S3 boards whose board.overlay declares the USB OTG CDC-ACM node, so it is on by default there too — but not on repeater/observer/room-server or debug builds, which keep the port for the console. config ZEPHCORE_COMPANION_SERIAL bool "Enable plain-UART companion transport" depends on ZEPHCORE_ROLE_COMPANION && !ZEPHCORE_COMPANION_USB help Run the same wired-companion stack (ZephyrCompanionUSB: frame parser, TX ring, text CLI, BLE-arbitrated interface) over a plain UART instead of a native-USB CDC-ACM endpoint. For boards whose USB-C is wired to a USB-UART bridge rather than the SoC's native USB (e.g. Heltec V3 / CP2102), or that have no USB device controller at all. The board points the transport at its UART with the `zephcore,companion-uart` chosen node (e.g. = &uart0); that UART must be dedicated to the companion (console/shell moved off or disabled), since the protocol stream and a text console cannot share one line. Coexists with BLE exactly like the USB backend — same first-come arbitration. Distinct from the legacy SerialCompanionTransport (no-BLE STM32WL path): this one keeps BLE and adds the text CLI. if ZEPHCORE_ROLE_REPEATER || ZEPHCORE_ROLE_ROOM_SERVER config ZEPHCORE_REPEATER_UPLINK bool "Enable repeater WiFi+MQTT uplink" depends on ZEPHCORE_ROLE_REPEATER && SOC_FAMILY_ESPRESSIF_ESP32 default n help Adds observer-style WiFi station and MQTT packet publishing to repeater builds. Configuration is stored in LittleFS and applied on reboot. config ZEPHCORE_MAX_CLIENTS int "Maximum ACL clients" default 32 help Maximum clients in the Access Control List. Each client stores pubkey, permissions, and shared secret. config ZEPHCORE_MAX_NEIGHBOURS int "Maximum tracked neighbors" default 50 help Maximum number of neighboring repeaters to track. Set to 0 to disable neighbor tracking. Default 50 matches upstream Arduino MeshCore repeater variants. config ZEPHCORE_MAX_REGION_ENTRIES int "Maximum region entries" default 32 help Maximum entries in the region map for flood filtering. Regions control which transport codes are allowed to flood. config ZEPHCORE_ADMIN_PASSWORD string "Default admin password" default "password" help Default password for admin access to the repeater. Should be changed on first configuration! config ZEPHCORE_GUEST_PASSWORD string "Default guest password" default "" help Default password for guest access. An empty string means different things per role, matching Arduino MeshCore: REPEATER - open guest access. A blank submitted password logs in as PERM_ACL_GUEST, which cannot run CLI commands or read the access list; it gets login plus status/telemetry. ROOM SERVER - guest access disabled, so a room is never accidentally left open. Arduino reaches the same place by defaulting the room server's guest password to ROOM_PASSWORD instead of leaving it empty. Set a non-empty value to require a password on either role. config ZEPHCORE_MAX_UNSYNCED_POSTS int "Room server: max buffered posts" default 32 depends on ZEPHCORE_ROLE_ROOM_SERVER help Size of the room server's circular post buffer. When full, the oldest unsynced post is overwritten. Matches upstream MeshCore (32). endif # ZEPHCORE_ROLE_REPEATER || ZEPHCORE_ROLE_ROOM_SERVER config ZEPHCORE_TIMESYNC_QUORUM int "Mesh time sync: consensus quorum" default 6 range 3 32 help Minimum eligible advert senders required before a mesh time-sync consensus may step the clock (a strict majority of them must also agree). Each quorum unit costs a local attacker one more physical radio sustained for tenure-hours, so lowering this trades eclipse resistance for coverage on small meshes (e.g. 7 repeaters = exactly 6 potential voters). Floor 3 matches the bootstrap quorum — below that the interval intersection degenerates. Deliberately a build-time knob, not a runtime pref: this is a security parameter. config ZEPHCORE_TIMESYNC_TABLE_SIZE int "Mesh time sync: sample table slots" default 16 if ZEPHCORE_ROLE_COMPANION default 32 range 8 64 help Per-sender advert sample slots (24 bytes each) for the mesh time-sync consensus. 32 fits dense neighborhoods; companions default to 16 (RAM-bound role). Must be at least the quorum for normal-mode stepping to be reachable. endmenu # Device Role if ZEPHCORE_ROLE_COMPANION config ZEPHCORE_MAX_CONTACTS int "Maximum number of contacts" default 350 help Maximum number of contacts that can be stored. config ZEPHCORE_MAX_CHANNELS int "Maximum number of channels" default 40 help Maximum number of group channels that can be stored. config ZEPHCORE_OFFLINE_QUEUE_SIZE int "Offline message queue size" default 256 help Size of the offline message queue for incoming messages. Arduino MeshCore uses 256 for both T1000-E and Wio Tracker. config ZEPHCORE_ACK_TABLE_SIZE int "ACK tracking table size" default 16 help Number of pending ACKs that can be tracked. Sizing this below the number of messages that can be in flight at once costs delivery confirmations: a peer working through a backlog can answer seconds later, and an entry evicted before its ACK arrives leaves the message unconfirmed, prompting a retry that adds further load. Each entry costs 16 bytes. config ZEPHCORE_MAX_CONNECTIONS int "Maximum server connections" default 16 help Maximum number of simultaneous room server connections with keep-alive. config ZEPHCORE_ADVERT_PATH_TABLE_SIZE int "Advert path table size" default 16 help Size of recently-heard advert path table. menu "BLE Configuration" config ZEPHCORE_BLE_PASSKEY int "BLE pairing passkey (6 digits)" default 123456 range 0 999999 help Fixed 6-digit passkey for BLE MITM pairing. config ZEPHCORE_BLE_QUEUE_SIZE int "BLE TX/RX message queue depth" default 24 range 4 64 help Number of frames that can be queued for BLE TX/RX. Larger = better buffering for bursty traffic, more RAM. 24 absorbs the app's bursty CMD_GET_CHANNEL pipelining during channel sync (it fires up to MAX_CHANNELS requests back-to-back); at 12 the recv queue overran and dropped requests, stalling sync. config ZEPHCORE_BLE_CONN_MIN_INTERVAL int "BLE minimum connection interval (units: 1.25ms)" default 12 range 6 3200 help Minimum BLE connection interval in 1.25ms units. 12 = 15ms. Matches Arduino default. Lower = more responsive, higher power. config ZEPHCORE_BLE_CONN_MAX_INTERVAL int "BLE maximum connection interval (units: 1.25ms)" default 36 range 6 3200 help Maximum BLE connection interval in 1.25ms units. 36 = 45ms. Matches Arduino default. Higher = lower power when idle. config ZEPHCORE_BLE_CONN_LATENCY int "BLE connection latency (skip events)" default 4 range 0 500 help Skip up to N connection events when idle to save power. 4 = skip 4 events (effective interval up to 375ms idle). config ZEPHCORE_BLE_CONN_TIMEOUT int "BLE supervision timeout (units: 10ms)" default 500 range 10 3200 help Supervision timeout in 10ms units. 500 = 5000ms. Connection drops if no data for this duration. config ZEPHCORE_BLE_ADV_SLOW_INTERVAL int "BLE advertising interval (units: 0.625ms)" default 338 range 244 3200 help Advertising interval in 0.625ms units. 338 = 211.25ms (Apple-compliant, used after 60s fast window). config ZEPHCORE_BLE_DFU bool "Buttonless BLE DFU (legacy Nordic/Adafruit OTA jump)" depends on SOC_SERIES_NRF52 default y help Register the legacy Nordic DFU service (00001530/00001531) in the running companion app so a paired phone can trigger the jump into the Adafruit bootloader's BLE OTA mode without pressing a button. On a write of 0x01 to the control point the device sets the bootloader GPREGRET magic (0xA8) and resets; the bootloader then advertises for DFU and the phone's DFU tool reconnects to flash. nRF52 only. endmenu # BLE Configuration menu "Linux Companion Transport (native_sim)" config ZEPHCORE_TRANSPORT_TCP bool "Use TCP socket transport instead of BLE NUS" default n help Replaces the ZephyrBLE adapter with LinuxTCPTransport, exposing the companion NUS protocol over a TCP socket on Linux SBC builds (BOARD=native_sim). Auto-enabled by boards/linux_native/linux_common.conf. The wire format is MeshCore's SerialWifiInterface framing: ['<'][len_LSB][len_MSB][NUS payload] app->node and ['>']... node->app. config ZEPHCORE_LINUX_TCP_PORT int "TCP listen port" depends on ZEPHCORE_TRANSPORT_TCP default 5000 range 1 65535 help Port the companion TCP transport binds to. Default 5000 matches the upstream MeshCore Linux companion service so a single mobile app TCP configuration works against both backends. endmenu # Linux Companion Transport endif # ZEPHCORE_ROLE_COMPANION config ZEPHCORE_HOUSEKEEPING_INTERVAL_MS int "Periodic housekeeping interval (ms)" depends on ZEPHCORE_ROLE_COMPANION || ZEPHCORE_ROLE_ROOM_SERVER default 5000 range 1000 60000 help Wake interval for noise floor calibration, battery reads, and UI refresh. Longer = lower power, less responsive monitoring. Companion and room server only. Both still run a fixed periodic tick: the companion's block contains genuine pollers (contact-dump progress, BLE advertising watchdog) that have no deadline to arm, and the room server is paced by its own 500 ms push timer regardless. Repeaters are deadline-driven instead — see ZEPHCORE_MAINTENANCE_BACKSTOP_MS. config ZEPHCORE_MAINTENANCE_BACKSTOP_MS int "Repeater maintenance backstop (ms)" depends on ZEPHCORE_ROLE_REPEATER default 60000 range 15000 600000 help Hard ceiling on how long a repeater will go without a maintenance pass. main_repeater.cpp arms a one-shot wake at the soonest deadline reported by msUntilNextMaintenance(), then clamps it to this value. The clamp is UNCONDITIONAL, so this is a safety net against a deadline that is missed or mis-reported — not a value that should ever bind in normal operation. MUST stay above the radio measurement interval ("set probe.interval", default 15 s; ZEPHCORE_NOISE_FLOOR_INTERVAL_MS when probing is off) — the shortest recurring deadline on an idle repeater. Set at or below it and this silently becomes the wake period, reproducing the old fixed 5 s tick and making the deadline scheduling a no-op. That is exactly the bug the original 5000 default shipped with. Lowering it costs wakes without improving responsiveness — real work is already scheduled at its own deadline. Raising it widens the window in which a scheduling bug goes unnoticed. config ZEPHCORE_NOISE_FLOOR_INTERVAL_MS int "Default radio measurement interval (ms)" default 15000 range 1000 120000 help Fallback cadence for periodic radio measurements — the noise-floor RSSI sample and the CAD probe that consumes it. This is only the DEFAULT. At runtime the "probe.interval" pref governs both (see set probe.interval); this value applies when that pref is 0, i.e. CAD probing is switched off but the floor sampler still has to run. This used to be implicit: the sampler ran once per housekeeping tick, so it inherited that 5 s period. The sampler lives in shared radio code, so roles that still tick periodically (companion, room server) keep 5000 — raising it there would change their RF tracking for no power gain, since their housekeeping timer wakes them at 5 s regardless. Repeaters use 15 s, matching the default probe.interval pref (set in initNodePrefs(), helpers/NodePrefs.h) — the only other recurring radio deadline — so the two do not interleave into separate wakes any more often than they have to. On a repeater this is one of the two shortest recurring deadlines, so it bounds how long the SoC can sleep between wakes. Raising it stretches EMA warmup (8 samples) and the every-16th-sample unguarded bypass by the same factor: at 15000 warmup is ~2 min and the bypass ~4 min apart; at 30000, ~4 min and ~8 min. Past 15000 the CAD probe becomes the binding deadline instead, so raise both or neither. config ZEPHCORE_PM_BOOT_AWAKE_MS int "Block light sleep for this long after boot (ms)" depends on PM && SOC_FAMILY_ESPRESSIF_ESP32 default 600000 range 0 3600000 help How long after boot the SoC refuses to light-sleep, so the USB console is guaranteed reachable for a configuration session. This exists because nothing arms a UART wake source on ESP32 — see helpers/pm_esp32_console.c. Once the window closes, characters typed at a sleeping node are DROPPED and the CLI stops answering over USB until the board resets. In practice connecting a terminal usually does reset it (the USB bridge drives EN from DTR), which re-arms the window. 0 disables the window entirely — the node may sleep immediately after boot, and the USB console becomes unusable except in the instants it happens to be awake. Do not set 0 on a board you still need to configure. Raising it costs only the extra awake time at the start of each boot. config ZEPHCORE_PM_CONSOLE_UART_NUM int "Console UART index flushed before light sleep" depends on PM && SOC_FAMILY_ESPRESSIF_ESP32 default 0 range 0 2 help Hardware UART index (not a DT node) passed to esp_rom_uart_tx_wait_idle() before entering light sleep, so a line in the TX FIFO is not cut off mid-character by the clock stopping. 0 is correct for the boards whose console really is uart0: Heltec V3 (via a CP2102 bridge) and anything on the ROM-default pads. On Heltec V4, V4.3 and Wireless Tracker V2 the console is usb_serial (their board.overlay chooses it, and the uart0 reroute in boards/common/esp32s3_usb.overlay is companion-only), so this flush targets a peripheral that is not their console and does nothing. That is harmless but not a fix: a USJ console has no wake source either way, so on those boards the USB CLI is simply unavailable once ZEPHCORE_PM_BOOT_AWAKE_MS expires. Change it only if a board's console is moved to another hardware UART — a wrong index does not fail the build, it just silently flushes the wrong peripheral and lets the console truncate again. menu "Radio Configuration" choice ZEPHCORE_RADIO_TYPE prompt "LoRa Radio Type" default ZEPHCORE_RADIO_NATIVE help Select the LoRa radio driver. config ZEPHCORE_RADIO_NATIVE bool "Zephyr native LoRa driver (SX126x, SX127x, LLCC68, STM32WL)" help Uses Zephyr's built-in LoRa driver. Supports all radios that Zephyr has device tree bindings for: SX126x: SX1261, SX1262, SX1268 SX127x: SX1272, SX1276, SX1278 LLCC68: Semtech LLCC68 STM32WL: STM32WL SUBGHZ radio (SX126x-based) Used by: RAK4631, Wio Tracker L1, all XIAO + Wio-SX1262 boards config ZEPHCORE_RADIO_LR1110 bool "LR1110/LR1120/LR1121 (custom driver)" select LORA select SPI help Semtech LR11xx via custom ZephCore driver (not upstream Zephyr). Uses SPI mutex for thread safety + BUSY stuck recovery. TCXO, RF switch, and PA config are in the device tree. For any board with an LR1110, LR1120, or LR1121 radio. config ZEPHCORE_RADIO_LR2021 bool "LR2021 (custom driver)" select LORA select SPI help Semtech LR2021 (LoRa Plus, 4th-gen) via custom ZephCore driver. Supports sub-GHz + 2.4 GHz ISM + NTN/SATCOM. TCXO, RF switch, and PA config are in the device tree. config ZEPHCORE_RADIO_SX127X bool "SX127x (SX1272/SX1276/SX1278) via Zephyr loramac-node driver" help Uses Zephyr's loramac-node-based LoRa driver for SX127x radios. Supports SX1272, SX1276, and SX1278 chips via the standard Zephyr LoRa API (lora_config, lora_send_async, lora_recv_async). Chip-specific features not available via standard API are stubbed: - Instantaneous RSSI (hwGetCurrentRSSI returns -80 dBm sentinel) - Preamble detection (always false — TX won't abort mid-preamble) - RX boost (no-op — SX127x has no dedicated boost register) - AGC reset (no-op — loramac-node manages AGC internally) - BUSY pin (false — SX127x has no BUSY signal) Used by: TTGO LoRa32, Heltec LoRa32 V1/V2, and any board with an SX1272/SX1276/SX1278 radio. endchoice config ZEPHCORE_DEFAULT_TX_POWER_DBM int "Default TX power (dBm)" default 22 range -9 22 help Default SX1262 TX power in dBm for fresh devices (no saved prefs). Boards with external PA should set this lower to avoid overdriving. Example: Station G2 with 20dB PA gain uses 15 dBm (→ 35 dBm output). config ZEPHCORE_MAX_TX_POWER_DBM int "Maximum TX power (dBm)" default 22 range -9 22 help Hard cap on SX1262 TX power. The radio adapter clamps any value above this, protecting external PAs from damage. Example: Station G2 caps at 19 dBm to stay below PA P1dB (35 dBm). config ZEPHCORE_SX126X_HELTEC_REG_PATCH bool "Apply undocumented SX126x register 0x8B5 RX improvement (external PA/FEM)" depends on ZEPHCORE_RADIO_NATIVE default n help Sets the LSB of undocumented register 0x8B5 after the first lora_config(). Described by Heltec engineer @Quency-D in MeshCore PR#1398 — consistently improves RX reception on boards with GC1109 or KCT8103L PA (Heltec V4/V4.3). Upstream MeshCore also enables it for the RAK3401's SKY66122 FEM, so it is not Heltec-specific despite the symbol name. Enable in board.conf for boards with an external PA/FEM in the RX path. endmenu # Radio Configuration menu "Board Configuration" config ZEPHCORE_BOARD_NAME string "Board name for identification" default "Zephyr Device" help Human-readable board name returned by the 'board' CLI command and reported in device info. Should match Arduino variant names for consistency (e.g., "RAK 4631", "Wio Tracker L1", "T1000-E"). config ZEPHCORE_SD_FWID hex "SoftDevice firmware ID for DFU packages" default 0x00B6 depends on SOC_SERIES_NRF52 help SoftDevice firmware ID used by adafruit-nrfutil when generating DFU zip packages (--sd-req parameter). Must match the SoftDevice version flashed with the Adafruit bootloader on the target device. Only applicable to nRF52 boards with Adafruit bootloader. Common values: 0x00B6 - S140 v6.1.1 (RAK4631, Wio Tracker, T1000-E) 0x0123 - S140 v7.3.0 (XIAO nRF52840, Ikoka Nano) config ZEPHCORE_VBAT_MV_MULTIPLIER int "Battery voltage ADC multiplier (fallback)" default 7200 help Fallback multiplier if vbat-mv-multiplier is not defined in devicetree zephyr,user node. Prefer defining in devicetree alongside ADC channel: zephyr,user { io-channels = <&adc 7>; vbat-mv-multiplier = <7200>; }; Formula: mv = (raw * multiplier) / 4096 endmenu menu "Debug & Logging" config ZEPHCORE_PACKET_LOGGING bool "Enable mesh packet logging" default n depends on LOG help Log all RX and TX mesh packets in Arduino-compatible format. Output format matches Arduino MESH_PACKET_LOGGING exactly: "HH:MM:SS - D/M/YYYY U: RX, len=N (type=N, route=D/F, payload_len=N) SNR=N RSSI=N score=N time=N hash=XXXX [XX -> XX]" config ZEPHCORE_PACKET_LOGGING_ONLY bool "Quiet mode - only packet logs, no debug spam" default n depends on ZEPHCORE_PACKET_LOGGING help When enabled, suppresses all other log messages and only outputs packet logging (RAW, RX, TX lines). Uses printk() to bypass log level filtering. endmenu menu "LoRa Power Saving" config ZEPHCORE_LORA_RX_DUTY_CYCLE bool "Enable LoRa RX duty cycle power saving" default n help Enable RX duty cycling (chip-autonomous sniff mode) for power saving on battery-powered devices. This sets the boot default; it can be toggled at runtime via CLI "set rxduty on/off". Window timing is computed per SF/BW/preamble from primary-source constraints (SX1261/2 datasheet sec 13.1.7, AN1200.36): the deaf time per cycle (sleep + TCXO/PLL wake transition) never exceeds the budget needed to guarantee one full detection window inside the sender's preamble, and the post-detect timer budget (2*rxPeriod + sleepPeriod) always covers preamble + header. With a 32-symbol sender preamble (current MeshCore firmware at SF<=8) this yields ~40-55% radio-off time with zero loss against updated senders. Presets whose preamble is too short for a guaranteed catch (16 symbols at SF>=9) automatically fall back to continuous RX — no fake duty cycling. Caveat for mixed meshes: nodes still on pre-preamble-32 firmware (16 symbols at SF<=8) are only caught ~50% worst-phase. Keep this off on infrastructure until the local mesh has converted. Floods are usually re-heard via updated repeaters; direct messages from legacy nodes are the real exposure. Duty cycle mode also resets AGC state on entry via Calibrate(ALL), pins the boosted RX gain into the warm-start retention list (datasheet sec 9.6, mandatory for SetRxDutyCycle), and runs a parked-RX watchdog that re-arms the cycle after a false preamble detect (inspect via CLI "get dc.restarts"). LR1110/LR20xx use the same chip-level SetRxDutyCycle MODE_RX (identical preamble-detect-and-extend mechanism) sized by the same adapter math; the LR1110's earlier "broken" status was a window- sizing bug, not a chip defect. Both are wired but HW-unverified — confirm no packet loss on a live board before trusting in production. LR1110 has no infinite-park failure mode (its DC uses a bounded 2*rx+sleep timeout), so it re-arms via the normal RX-timeout path rather than the SX126x watchdog. config ZEPHCORE_LORA_DC_MIN_SYMBOLS int "RX duty cycle: preamble symbols required for detection" range 4 16 default 8 help Minimum LoRa preamble symbols that must land inside one open RX window for detection to be considered guaranteed. 8 is Semtech's own figure for sniff mode (AN1200.36 sec 4); their time-synced LoRaWAN stacks budget 6. Raise to 10-12 to trade radio-off time for extra detection margin on very noisy sites. SF5/6 adds +4 internally. Lowering below 8 risks missed packets at low SNR. config ZEPHCORE_LORA_DC_MARGIN_PCT int "RX duty cycle: sleep-budget safety margin (percent)" range 0 40 default 5 help Percentage the per-cycle deaf-time budget is derated before the sleep period is programmed. The theoretical catch budget assumes the sleep clock and wake transition are exact, but the chip's sleep timer runs on an internal RC oscillator (RC64k on SX126x, RTC on LR11xx) that drifts several percent over temperature, and the wake- transition estimate is a datasheet figure the datasheet itself calls "not accurate and may vary". Either overshoot silently pushes real deaf time past the budget and drops the fraction of packets whose preamble phase aligns with the window edge — signal-strength- independent loss that presents as random DC packet drops. This margin trades a little radio-off time for that robustness and is shared by every radio (SX126x / LR11xx / LR20xx). Set to 0 to restore the old zero-margin behaviour. Lowered from 15% to 5% on 2026-08-21. What justifies it: the clock half of the error is bounded, not open-ended. RC64k is recalibrated by Calibrate(ALL) (SX126X_CALIBRATE_RC64K is bit 0 of 0x7F) whenever agcMaintenance() sees the chip temperature move AGC_RECAL_TEMP_DELTA_C (5 C), so the drift this must absorb is what accrues inside a 5 C window, not across the whole operating range since boot. What it costs, at SF7/BW62.5/P=32 on a 5 ms-TCXO board: absolute slack against the catch budget falls from 4608 us (15%) to 1536 us (5%), and the tolerated sleep-timer error falls from 23.5% to 6.8%. Off-time goes 30.9% -> 34.9%. Note the tolerance always exceeds the margin percentage, because the derate applies to the whole budget while the clock error acts only on the sleep portion. The part NOT bounded by the temperature recalibration is the wake-transition estimate in hwWakeupTimeUs() -- a programmed TCXO delay (deterministic) plus ~1 ms of context restore and PLL lock that the datasheet itself calls "not accurate and may vary". At 5% that term has 1536 us of headroom minus whatever RC64k drift consumes. If a board shows unexplained duty-cycle packet loss, raise this first -- the loss is signal-strength-independent, so RSSI will look fine and only a PDR comparison against rxduty 0 will show it. config ZEPHCORE_LORA_RETENTION_DEBUG bool "SX126x: verify the warm-start retention list on air" depends on ZEPHCORE_RADIO_NATIVE help Read back the SX126x register retention list (0x029F) and the Rx gain register (0x08AC) and report whether they survived, at chip init and after every duty-cycle packet. This exists to answer one question: does installing the retention list once at init actually hold? The list is what restores Rx gain across the chip-internal sleep->RX wakes of SetRxDutyCycle, which the host never sees. If it does not hold, every window after the first sleep listens at power-saving gain -- a silent 3 dB sensitivity loss with nothing in the log, which is precisely the failure this check makes visible. The check runs after RX_DONE and before sx126x_restart_rx() rewrites the gain, so it observes the value the packet was actually received with rather than the one we are about to reapply. Costs two SPI register reads per received packet. Debug builds only; enabled by boards/common/debug.conf. endmenu config ZEPHCORE_RTC_AUTODISCOVER bool "Auto-discover a hardware I2C RTC at boot" default y depends on I2C help Probe I2C RTC chips declared with the "zephcore,rtc-i2c" binding (DS3231 / PCF8563 / RV3028 / RX8130CE). Boards with a battery/cap- backed RTC opt in by including boards/common/rtc-i2c.dtsi; if a chip holds a valid time it is restored at boot — shown tagged "L" until the next external sync. On every GPS/app/CLI sync the time is written back so it survives power-off. Compact raw-I2C reader (no Zephyr RTC subsystem). Safe to leave on: a board with no rtc-i2c.dtsi include has no RTC nodes and compiles to nothing. menu "GPS Configuration" config ZEPHCORE_GPS_POLL_INTERVAL_SEC int "GPS poll interval in seconds" default 300 range 10 86400 help How often to wake GPS and acquire a fix. Default 300 seconds (5 minutes). config ZEPHCORE_GPS_FIX_TIMEOUT_SEC int "GPS fix acquisition timeout in seconds" default 120 range 10 300 help Maximum time to wait for GPS fix before giving up. GPS will be powered off after this timeout. Default 120s — 30s was too aggressive for Air530Z / MAX-7Q warm starts in marginal sky conditions; the acquire loop would time out before the 3-consecutive-good-fix gate could promote. Only applies to wake cycles after the first successful fix since enable. config ZEPHCORE_GPS_FIRST_FIX_TIMEOUT_SEC int "GPS first-acquisition timeout in seconds" default 300 range 30 1800 help Bounded window for the FIRST position fix after GPS is enabled (cold start). Longer than the normal fix timeout because a cold start may need to download almanac data. If no fix is acquired in this window the node drops into the normal duty cycle anyway, so the GPS can never stay powered indefinitely with no sky view. Default 300s (5 minutes). Companion mode only; repeaters use a fixed 5-minute time-sync window. config ZEPHCORE_REPEATER_GPS_INTERVAL_SEC int "Repeater GPS duty interval in seconds (boot default)" default 172800 range 0 604800 help Default GPS standby interval for repeaters/room servers: how often the GPS wakes for a time-sync fix. Default 172800s (48 hours). 0 = always-on (never sleeps). Applied at boot until the operator overrides it with "set gps duty " (persisted to flash). Companions default to ZEPHCORE_GPS_POLL_INTERVAL_SEC (300s) instead. config ZEPHCORE_GPS_NMEA_DUMP bool "Log every NMEA sentence received from the GNSS module" depends on GNSS help Print each sentence the GNSS driver parses, plus everything else the module emits, to the console. For diagnosing a receiver that reports no satellites: it distinguishes a module that is talking normally while it searches the sky from one that is silent, at the wrong baud, or has had its NMEA output disabled — cases that are otherwise indistinguishable from the application. Verbose: at 1 Hz with multi-GNSS this is a continuous stream, and on a repeater it shares the console with the CLI. Diagnostic builds only. config ZEPHCORE_GPS_NAV_MODE int "GNSS navigation dynamic model (CASIC $PCAS11)" range -1 7 default 1 if ZEPHCORE_ROLE_REPEATER || ZEPHCORE_ROLE_ROOM_SERVER default 3 help Dynamic model sent to CASIC-family GNSS modules (Quectel L76K/L76KB, Air530Z) on boards using the generic-NMEA driver: 0 = portable, 1 = stationary, 2 = pedestrian, 3 = automotive, 4 = sea, 5-7 = airborne. -1 sends nothing. Defaults to stationary for fixed roles and automotive otherwise. Worth setting rather than leaving alone: the model is stored IN THE MODULE and survives reflashing the host, so a slot module that previously lived in another device can arrive stuck in an airborne model that quietly degrades fixes on a fixed site. Boards that always carry a CASIC part use the air530z driver instead — see CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE for those. config ZEPHCORE_GPS_SAT_DIAG bool "GPS per-constellation satellite diagnostics" # Must depend on GNSS, not just select GNSS_SATELLITES: on a board with # no GNSS at all (native Linux, xiao_nrf52840) GNSS is n, and y-selecting # a symbol whose dependencies are unmet is a Kconfig error that aborts the # build — not a warning. The repeater default below would otherwise turn # this on for every GNSS-less repeater target. depends on GNSS select GNSS_SATELLITES default y if ZEPHCORE_ROLE_REPEATER help Tally tracked satellites per constellation from GSV sentences and report them in "get gps" as sys=G/R/E/B/? (GPS / GLONASS / Galileo / BeiDou / other). This is the ground truth for whether the boot-time multi-constellation configuration (PMTK353 / UBX-CFG-GNSS) actually took: a module still in its GPS-only default emits only $GPGSV, so every counter but G stays 0. Satellite count alone cannot prove this — only the talker IDs can. Costs RAM: CONFIG_GNSS_SATELLITES adds 512 bytes to the driver's UART RX buffer plus a per-satellite array (~770 bytes total, measured on RAK3401). Default on for repeaters, which have the headroom; off for companions, which are RAM-bound. Enable it explicitly for a companion diagnostic build. Independent of "set gps diag", which is a runtime toggle and always available — this option only adds the per-constellation evidence to its report. endmenu menu "WiFi OTA Update" config ZEPHCORE_WIFI_OTA bool "WiFi OTA firmware update" depends on ZEPHCORE_ROLE_REPEATER help Enable WiFi AP + HTTP server for OTA firmware updates. Auto-enabled for ESP32 repeater builds via wifi_ota.conf. When "start ota" is issued, starts WiFi AP "ZephCore-OTA" and HTTP server. User uploads firmware via browser at http://192.168.100.1/update. Requires MCUboot (--sysbuild). if ZEPHCORE_WIFI_OTA config ZEPHCORE_OTA_AP_SSID string "WiFi AP SSID for OTA" default "ZephCore-OTA" help SSID of the WiFi access point started for OTA updates. Open network (no password), matching Arduino behavior. config ZEPHCORE_OTA_AP_IP string "WiFi AP static IP address" default "192.168.100.1" help Static IP address assigned to the WiFi AP interface. DHCP server hands out addresses starting at .10. endif # ZEPHCORE_WIFI_OTA endmenu menu "UI Configuration" config ZEPHCORE_UI_BUTTONS bool "Enable button input" default y depends on INPUT help Enable button/joystick input with long-press, double-click, and triple-click detection using Zephyr's native input subsystem. config ZEPHCORE_UI_MULTI_TAP bool "Enable multi-tap input filter" default y depends on ZEPHCORE_UI_BUTTONS help Enable the multi-tap input filter that counts rapid taps within a configurable window and emits different key codes based on tap count. Supports 1-4 tap levels via devicetree. Single-tap has ~400ms latency (waits for window to expire). config ZEPHCORE_UI_BUZZER bool "Enable buzzer" default y if $(dt_nodelabel_enabled,buzzer) select PWM help Enable PWM buzzer with RTTTL melody support. Auto-enabled when a "buzzer" nodelabel exists in devicetree. Pulls in CONFIG_PWM automatically. DT_CHOSEN_ZEPHCORE_DISPLAY := zephyr,display config ZEPHCORE_UI_DISPLAY bool "Enable display (OLED, LCD, e-ink)" default y if $(dt_chosen_enabled,$(DT_CHOSEN_ZEPHCORE_DISPLAY)) select DISPLAY select CHARACTER_FRAMEBUFFER help Enable page-based UI on any Zephyr-supported display. Auto-enabled when "zephyr,display" chosen node exists in DT. Auto-detects display via chosen node or legacy nodelabels (sh1106, ssd1306). Resolution and font are queried at runtime. Supports OLED (SSD1306, SH1106), LCD, e-ink, etc. config ZEPHCORE_UI_DISPLAY_AUTO_OFF_MS int "Display auto-off timeout (ms)" default 10000 depends on ZEPHCORE_UI_DISPLAY help Time in milliseconds before the display turns off after the last user interaction. Set to 0 to disable. config ZEPHCORE_UI_TIME_SOURCE_FRESH_HOURS int "Clock source tag freshness window (hours)" default 12 help The top-bar clock shows a one-character source tag: G (GPS), A (app), N (network/SNTP). If the most recent sync from one of those sources is older than this many hours, the tag falls back to L (local) — the RTC is free-running on its own oscillator. Set to 0 to keep the last external source tag indefinitely (no freshness expiry). Drift on a 50 ppm crystal is ~4 s/day, so this is a trust/freshness signal, not an accuracy limit. config ZEPHCORE_AUTO_SHUTDOWN_MILLIVOLTS int "Low-battery auto-shutdown threshold (mV, 0 = disabled)" default 3300 if SOC_SERIES_NRF52 default 0 help Companion only. When the battery falls below this voltage the device shows a brief "Low Battery / Shutting Down" notice (3 s on OLED; e-paper persists without delay) and enters System OFF to protect the cell from over-discharge. The check piggybacks the existing periodic housekeeping tick (no extra polling) and is skipped while externally powered (USB/charger) via MainBoard::isExternalPowered(). 3300 mV is ~10% SoC on the default LiPo curve (battery_curve.c) — a protective floor that still leaves headroom for the under-load voltage sag during a TX burst. Requires CONFIG_POWEROFF to actually power down. This is only the boot DEFAULT: the value is copied into prefs and is tunable at runtime over the USB CLI ("set autoshutdown " / "get autoshutdown", 0 = off or 1-5000 mV), persisted to flash. Defaults ON (3300) for nRF52 only: that is where isExternalPowered() reads VBUS, so the "plugged in" gate actually works. ESP32 / MG24 / nRF54L stay at 0 (off) — isExternalPowered() is a stub there, so a USB/mains-powered node could otherwise self-shutdown on a low ADC reading. Set explicitly per board to enable once VBUS detect exists. config ZEPHCORE_UI_CONFIRM_WINDOW_MS int "Double-press confirmation window (ms)" default 500 depends on ZEPHCORE_UI_DISPLAY help Maximum time between the two KEY_ENTER presses that confirm destructive actions (shutdown, DFU, offgrid toggle). The default 500 ms suits boards where ENTER is a direct short tap. Boards with only one button that emit ENTER via a zephyr,input-longpress filter (T-Echo, RAK4631 Pocket) need a longer window — two 1 s holds plus the release gap do not fit in 500 ms. Set to 3000 on such boards. config ZEPHCORE_DISPLAY_INSET int "Display inset margin (pixels)" default 0 depends on ZEPHCORE_UI_DISPLAY help Shrink the usable drawing area by this many pixels on each side. mc_display_width/height() report the reduced size and all draw primitives are offset by this amount. Useful on panels (e.g. LilyGo T-Echo SSD1681) where the outermost rows/columns show hardware artefacts — a small inset hides them behind a clean background margin. Set to 0 on boards without edge artefacts. config ZEPHCORE_DISPLAY_EPD_FULL_REFRESH_INTERVAL int "E-paper full-refresh interval (partial refreshes)" default 16 depends on ZEPHCORE_UI_DISPLAY help On e-paper panels, partial refreshes never fully clear the previous image, so ghosting accumulates where text changes most (status bar name/time/battery). After this many partial refreshes, perform one full refresh to clear the panel. A full refresh is the ~2 s black/white flash, so keep this large enough that it isn't constant but small enough that ghosting never becomes unreadable. Set to 0 to disable periodic full refreshes (partial-only — accumulates ghosting). Ignored on non-EPD displays (OLED/TFT never ghost). config ZEPHCORE_DISPLAY_LARGE_FONT bool "Use a larger built-in CFB font" default n depends on ZEPHCORE_UI_DISPLAY && CHARACTER_FRAMEBUFFER_USE_DEFAULT_FONTS help Select the smallest Zephyr built-in CFB font whose height is >= 16 pixels (typically the 10x16 font). Intended for larger e-paper panels (T-Echo, ThinkNode M1) where the default 6x8 glyph is too small to read comfortably. The ui_pages.c layout auto-centers its content rows using the active font height, so no per-page changes are needed. Requires Zephyr's default CFB fonts to be compiled in (CONFIG_CHARACTER_FRAMEBUFFER_USE_DEFAULT_FONTS). config ZEPHCORE_DISPLAY_MONO_TFT bool "Monochrome TFT wrapper (MONO01 => RGB565)" default y if DT_HAS_ZEPHCORE_MONO_TFT_ENABLED depends on DT_HAS_ZEPHCORE_MONO_TFT_ENABLED help Wraps an RGB TFT (e.g. ST7789V) as a PIXEL_FORMAT_MONO01 display. CFB allocates x*y/8 bytes (~4 KB for 240×135) instead of ~64 KB RGB565, making a colour TFT viable on RAM-constrained devices like the nRF52840. On display_write(), converts 1bpp row-major MONO01 to RGB565 row by row. Auto-enabled when a "zephcore,mono-tft" device exists in the devicetree. config ZEPHCORE_EASTER_EGG_DOOM bool "Enable Doom raycaster easter egg" default n depends on ZEPHCORE_UI_DISPLAY && ZEPHCORE_UI_BUTTONS help Hidden easter egg: Wolf3D-style raycaster on the OLED. Button UI: triple-press ENTER on the Messages page. Joystick UI: open the Tools menu and select "Doom". Press BACK/ESC to exit. Adds ~5KB flash and ~1.7KB RAM when enabled. config ZEPHCORE_UI_JOYSTICK bool help Set by board-level Kconfig when the hardware has a 5-way joystick or D-pad. Not user-selectable; set by the board configuration. Follows the same pattern as ZEPHCORE_UI_BUTTONS and ZEPHCORE_UI_DISPLAY. config ZEPHCORE_UI_KEYBOARD bool help Set by board-level Kconfig when the hardware has a full keyboard (e.g. ThinkNode M9's STC8H matrix MCU on I2C). Not user-selectable. A keyboard is a superset of a joystick — it supplies arrows, enter and escape — so it drives the same full companion UI rather than a separate one. Selecting this makes ZEPHCORE_UI_DESIGN_JOYSTICK available on a board that has no 5-way stick. Selected by ThinkNode M9. The STC8H driver is helpers/input/ stc8h_keypad.c; the keypad's arrow-key codes are still unknown, so the driver logs any code it does not recognise (see its header). The key-space reservation in helpers/ui-joystick/joystick_defs.h is the groundwork that makes them droppable in later. config ZEPHCORE_UI_DESIGN_BUTTON bool "Button-based page UI" default y if (ZEPHCORE_UI_BUTTONS || ZEPHCORE_UI_DISPLAY || ZEPHCORE_UI_BUZZER) && !ZEPHCORE_UI_DESIGN_JOYSTICK depends on (ZEPHCORE_UI_BUTTONS || ZEPHCORE_UI_DISPLAY || ZEPHCORE_UI_BUZZER) && !ZEPHCORE_UI_DESIGN_JOYSTICK help Compile the default page-based button UI (helpers/ui-button/). Auto-selected when any UI hardware is present and the joystick UI design is not active. Disable to supply a custom ui_task.h implementation without touching this Kconfig. config ZEPHCORE_UI_DESIGN_JOYSTICK bool "Full companion UI (joystick or keyboard driven)" default y if ZEPHCORE_ROLE_COMPANION && ZEPHCORE_UI_DISPLAY && ZEPHCORE_UI_BUTTONS && (ZEPHCORE_UI_JOYSTICK || ZEPHCORE_UI_KEYBOARD) depends on ZEPHCORE_ROLE_COMPANION && ZEPHCORE_UI_DISPLAY && ZEPHCORE_UI_BUTTONS && (ZEPHCORE_UI_JOYSTICK || ZEPHCORE_UI_KEYBOARD) help Full companion UI (helpers/ui-joystick/). Provides GPS, contacts, channels, snake game, repeater admin, and all other companion screens. Replaces ZEPHCORE_UI_DESIGN_BUTTON for companion builds on boards with joystick or keyboard hardware. Named "joystick" for the hardware that first drove it; the screens themselves only consume the abstract key codes in joystick_defs.h, so any input device supplying arrows/enter/escape can drive them. endmenu endmenu