# 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 <n>` 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 <n>` 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 to a time within 2000-2099 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 <sec>" (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<n>/R<n>/E<n>/B<n>/?<n>
	  (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 <mv>" /
	  "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
