# SPDX-License-Identifier: MIT # WiFi companion on a PSRAM board: the WiFi heap and the network buffers go to # PSRAM, so WiFi and BLE fit together (ZEPHCORE_COMPANION_WIFI). Ahead of # Kconfig.zephyr, because a choice takes its first matching default and the # driver's own ESP_WIFI_HEAP default is unconditional. choice ESP_WIFI_HEAP default ESP_WIFI_HEAP_SPIRAM if ZEPHCORE_COMPANION_WIFI && ESP_SPIRAM endchoice config ESP32_WIFI_NET_ALLOC_SPIRAM default y if ZEPHCORE_COMPANION_WIFI && ESP_SPIRAM # WiFi companion on an ESP32-S3 without PSRAM: the WiFi heap is the system heap, # and Espressif's ADD_SIZE reservation (51 KB for WiFi alone) is sized for its # default 10 static / 32 dynamic RX and 32 TX buffers. Bound the buffers and size # the heap from measurement instead (72 KB, HEAP_MEM_POOL_SIZE in # wifi_companion.conf): measured 2026-09-24 on a XIAO ESP32-S3 built without # PSRAM, peak 61.6 KB under devdocs/tools/mem_load.py, and heap exhaustion under # a ~10k frames/s flood recovers (devdocs/lld/08-connectivity.md §13). Saves # ~12 KB of DRAM, spent on contacts. S3 only: C3/C6 and the classic ESP32 run # different BLE controllers with different heap needs, not measured here. config HEAP_MEM_POOL_IGNORE_MIN default y if ZEPHCORE_COMPANION_WIFI && SOC_SERIES_ESP32S3 && !ESP_SPIRAM config ESP32_WIFI_STATIC_RX_BUFFER_NUM default 6 if ZEPHCORE_COMPANION_WIFI && SOC_SERIES_ESP32S3 && !ESP_SPIRAM config ESP32_WIFI_DYNAMIC_RX_BUFFER_NUM default 16 if ZEPHCORE_COMPANION_WIFI && SOC_SERIES_ESP32S3 && !ESP_SPIRAM config ESP32_WIFI_DYNAMIC_TX_BUFFER_NUM default 16 if ZEPHCORE_COMPANION_WIFI && SOC_SERIES_ESP32S3 && !ESP_SPIRAM # Main thread stack. Measured 2026-09-24 (CONFIG_ZEPHCORE_MEM_STATS, # devdocs/tools/mem_load.py: fresh BLE pairing + bonded reconnects, protocol # golden over BLE and TCP, 8 KB sign, key export/import, flood advert, 120 # contacts added and dumped): companion peak 3008 B on nRF52 (T1000-E), 3828 B # on ESP32-S3 with WiFi. 6144 for the companion (the LoRa-E5 companion already # ran 6144); the servers keep 8192 (region CLI, uplink). A conf fragment still # overrides (stm32wl_common 6144, observer 4096). Ahead of Kconfig.zephyr so it # beats Zephyr's own default. config MAIN_STACK_SIZE default 6144 if ZEPHCORE_ROLE_COMPANION default 8192 # The air530z driver's dynamic model (patch 0005) follows ours, so one knob # sets it for every role, room servers included. Ahead of Kconfig.zephyr so # it beats the driver's own default. config GNSS_LUATOS_AIR530Z_NAV_MODE int default ZEPHCORE_GPS_NAV_MODE depends on GNSS_LUATOS_AIR530Z 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_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). It is one interface in the companion's MultiSerialInterface: USB, BLE (and TCP) clients can be connected at once, and every connected one gets every frame. 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. DT_CHOSEN_ZEPHCORE_COMPANION_UART := zephcore,companion-uart config ZEPHCORE_COMPANION_SERIAL bool "Enable plain-UART companion transport" depends on ZEPHCORE_ROLE_COMPANION && !ZEPHCORE_COMPANION_USB default y if !BT && $(dt_chosen_enabled,$(DT_CHOSEN_ZEPHCORE_COMPANION_UART)) help Run the same wired-companion stack (ZephyrCompanionUSB: frame parser, TX ring, text CLI) 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 should be dedicated to the companion (console/shell moved off or disabled), since the protocol stream and a text console cannot share one line. It is one interface in the MultiSerialInterface, beside BLE and TCP. On by default for a board without Bluetooth that names the chosen node (the LoRa-E5 mini): the UART is then its only companion link. 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. 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 225ms idle at 45ms). 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 "Companion TCP Transport" config ZEPHCORE_TRANSPORT_TCP bool "Companion over a TCP socket" default n help Serve the companion protocol over TCP (TcpCompanionTransport), as one more interface in the MultiSerialInterface beside BLE and USB: the native-Linux builds (linux_common.conf turns it on) and the WiFi companion (ZEPHCORE_COMPANION_WIFI selects it). The wire format is MeshCore's SerialWifiInterface framing: ['<'][len_LSB][len_MSB][payload] app->node and ['>']... node->app. config ZEPHCORE_TCP_PORT int "TCP listen port" depends on ZEPHCORE_TRANSPORT_TCP default 5000 range 1 65535 help Port the companion TCP transport listens on. Default 5000 matches upstream MeshCore's companion TCP_PORT and its Linux companion service, so one mobile-app TCP configuration works against every backend. config ZEPHCORE_TCP_QUEUE_SIZE int "TCP TX/RX frame queue depth" depends on ZEPHCORE_TRANSPORT_TCP default 8 range 4 64 help Frames queued between the TCP socket and the companion, each way. Smaller than the BLE queues (ZEPHCORE_BLE_QUEUE_SIZE, 24): there is one TCP client, the socket buffers on both ends absorb bursts, and the RX side waits for room instead of dropping, so a full queue pushes back through TCP flow control. 16 frames less each way is ~5.7 KB of DRAM, which the no-PSRAM WiFi boards spend on contacts. config ZEPHCORE_COMPANION_WIFI bool "WiFi companion (app over TCP on the saved network)" depends on ZEPHCORE_ROLE_COMPANION && WIFI select ZEPHCORE_TRANSPORT_TCP help Join the network saved with upstream's wifi.* CLI (set wifi.ssid, set wifi.pwd, set wifi.enabled; applied on reboot) and serve the companion protocol over TCP on ZEPHCORE_TCP_PORT, beside BLE and USB: every connected transport is served (MultiSerialInterface). Turned on by boards/common/wifi_companion.conf, which CMake adds to companion builds of boards whose zephcore.yml declares `capabilities: wifi: true` (the ones with the RAM for WiFi and BLE together). endmenu # Companion TCP Transport endif # ZEPHCORE_ROLE_COMPANION config ZEPHCORE_HOUSEKEEPING_INTERVAL_MS int "Companion housekeeping interval (ms)" depends on ZEPHCORE_ROLE_COMPANION default 5000 range 1000 60000 help Wake interval for the companion's periodic housekeeping: noise floor calibration, battery reads, UI refresh, and loop()'s own deadlines. Longer = lower power, less responsive monitoring. Companion only. It keeps a fixed periodic tick because its block contains genuine pollers of external state (contact-dump progress, BLE advertising watchdog, battery) that have no deadline to arm. Repeaters and room servers are deadline-driven instead — see ZEPHCORE_MAINTENANCE_BACKSTOP_MS. config ZEPHCORE_MAINTENANCE_BACKSTOP_MS int "Repeater / room server maintenance backstop (ms)" depends on ZEPHCORE_ROLE_REPEATER || ZEPHCORE_ROLE_ROOM_SERVER default 60000 range 15000 600000 help Hard ceiling on how long a repeater or room server will go without a maintenance pass. The event loop 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 node. 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 "Console window: no light sleep for this long (ms)" depends on PM && SOC_FAMILY_ESPRESSIF_ESP32 default 600000 range 0 3600000 help How long the SoC refuses to light-sleep after boot, and again after every press of the user button, so the USB console is 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 on boards that console over the S3's USB Serial/JTAG the USB device detaches on every sleep. Press the user button, or send `powersaving off` over LoRa, to get it back. 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 after each boot or press. 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 unavailable outside the console window (ZEPHCORE_PM_BOOT_AWAKE_MS). 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/V4-R8). 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, ThinkNode M1, Heltec T114) 0x0123 - S140 v7.3.0 (XIAO nRF52840, Wio Tracker L1, T1000-E) 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 PRINTK 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]" The lines are printk(), so they need no logging subsystem: packet_logging.conf keeps CONFIG_LOG=n and the console carries nothing else. 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_MEM_STATS bool "CLI: report thread stack and heap high-water marks" select INIT_STACKS select THREAD_STACK_INFO select THREAD_MONITOR select THREAD_NAME select SYS_HEAP_RUNTIME_STATS help Adds `get mem ` to the CLI (all roles). Each call returns one line: n = 0.. walks the threads (name, stack size, peak bytes used), then the k_heaps (address, size, current and peak allocation), then "end". For sizing stacks and heaps against measured use, e.g. after a Zephyr bump. Stack watermarks need INIT_STACKS (stacks painted at thread creation), which costs boot time, so leave this off in releases. Resolve heap addresses to symbols with nm on zephyr.elf. config ZEPHCORE_LOOP_WAKE_STATS bool "CLI: count event-loop wakes" help Count every return from the mesh thread's k_event_wait(), in total and per event bit, and report them with `get loop.wakes`. For measuring a role's wake profile (wakes per second = wakes / up), e.g. before and after a change to its scheduling. Measure on the release config with this passed by -D, so debug logging does not add wakes of its own. config ZEPHCORE_POWER_DIAG bool "CLI: CPU wake sources, sleep fraction, enabled peripherals (TEMPORARY)" depends on SOC_NRF52840 help Bench instrumentation: `get pwr ` reports which interrupts wake the CPU, how much of the time it sleeps, and which clocks and peripherals stay enabled. Build with boards/common/power_diag.conf, which turns on the user tracing hooks it counts from. 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 or declaring their own node; 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. A node with rv3028-eeprom-config has its time read and written with the chip's backup switch flag cleared before and checked after, and, when its chip is the one adopted, a read identifies it, and a checked time read just before the store shows no backup switchover, has that configuration committed to the chip's EEPROM when it differs, with backup switchover held off while the EEPROM is accessed. Compact raw-I2C reader (no Zephyr RTC subsystem). Safe to leave on: a board that declares no zephcore,rtc-i2c node 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. Applies to wake cycles where the module kept its state; the first acquisition and every wake after a full power-off use ZEPHCORE_GPS_FIRST_FIX_TIMEOUT_SEC. config ZEPHCORE_GPS_FIRST_FIX_TIMEOUT_SEC int "GPS first-acquisition timeout in seconds" default 300 range 30 1800 help Bounded window for a position fix that follows a full power-off (cold start): the first one after GPS is enabled, and every wake when the duty interval is above the `gps standby` limit, on every board. 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, whose CONFIG_GNSS_LUATOS_AIR530Z_NAV_MODE follows this value. 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. config ZEPHCORE_GPS_REAPPLY bool "Re-send GNSS module settings after every GPS power-on" depends on GNSS default y help CASIC receivers (Quectel L76K/L76KB, Air530Z) keep the sentence selection (PCAS03) and constellation set (PCAS04) in RAM only: nothing here sends PCAS00 to save them, so a supply cut returns the module to its factory GPS + BeiDou. Configuration otherwise runs once, at boot, so a board whose gps off / duty-cycle standby really removes power would lose GLONASS after its first sleep (upstream MeshCore #3201 re-sends PCAS04 on every start for the same reason). With this on, about one second after each power-on the settings that are lost with power are written again, blind, one sentence at a time with the boot sequence's settle gaps, over the GNSS UART from the main thread (never through modem_chat, whose blocking scripts are unsafe right after a power restore): PCAS03, PCAS11 (the nav mode, when set) and PCAS04 last; on generic-NMEA boards, whose module is unknown, MediaTek's PMTK353 constellations, EASY (PMTK869) and AIC (PMTK286) first. Both constellation commands restart their own chip's engine; on a module that kept its settings that is one restart per wake, taken over leaving one that lost them searching GPS-only. UBX stays boot-only: CFG-CFG saves it to the module's flash, and repeating it would wear that flash. No board list: every GNSS board gets it, and it is inert where it is not needed: receivers ignore the other families' sentences, and one that only went to standby (Wio Tracker L1's WAKEUP pin) is simply told what it already has. Costs under 130 UART bytes per wake. Turn it off in a board.conf only with a reason, e.g. a CASIC board proven to lose fixes to the navigation restart PCAS04 triggers. 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 3200 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" # Shipped on the Wio Tracker L1 companion. A Kconfig default rather than a # build.sh flag, so a local build is the same firmware as the release; not # in board.conf, which would also enable it in that board's repeater build. default y if BOARD_WIO_TRACKER_L1 && ZEPHCORE_ROLE_COMPANION 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 ~7KB flash and ~2KB RAM when enabled. config ZEPHCORE_UI_JOYSTICK bool # In-tree Zephyr board (no board Kconfig of ours to select from) default y if BOARD_WIO_TRACKER_L1 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