Skip to content

BTClock settings reference

All settings live under the NVS namespace settings and are exposed via GET /api/settings and PATCH /api/settings. Changes via PATCH persist to NVS and take effect without reboot unless the "Notes" column says otherwise. A PATCH that writes a reboot-only field still persists the value immediately; the response is {"rebootRequired": true} so the WebUI can prompt the user.

Source of truth for the list is components/settings/include/settings/schema.hpp (67 scalar fields), plus a handful of special-case keys (dnd, screens, actCurrencies, timePerScreen, invertedColor, txPower) handled directly in components/settings/settings_api.cpp::ApplyPatch().

The WebUI mirrors kFields into a generated TypeScript module via scripts/generate-settings-meta.py (in the WebUI repo). Whenever a field is added, removed, or its boot_only flag flips here, run pnpm generate:settings-meta in the WebUI checkout to regenerate src/lib/types/settings.generated.ts. A unit test in the WebUI (settings.generated.spec.ts) cross-checks "(restart required)" labels against this metadata so a stale label fails CI before it reaches a device.

Defaults in the table below come from components/settings/include/settings/schema.hpp's FieldSpec::default_* fields, ported from the old firmware's btclock_v3_fci/src/lib/system/defaults.hpp. A fresh install (NVS wiped, or a key never PATCHed) surfaces these values.

JSON type mapping

Schema type JSON shape NVS storage
bool true / false NVS u8 (0/1)
uint JSON number (>= 0) NVS u32
int JSON number NVS i32
uchar JSON number (0..255) NVS u8
string JSON string NVS string
hex RGB JSON number (24-bit integer) NVS u32, e.g. 0xFF8800 is sent as 16746496
time-of-day split into dnd.startHour / dnd.startMinute etc. four NVS u32 keys
CSV emitted as a JSON array, persisted as a comma-joined string NVS string

Unknown top-level keys in a PATCH body are silently ignored (matches the old firmware). Validation failures return 400 Bad Request with a structured {"error": "<token>"} body. The WebUI's parseSettingsError helper splits the token on : and uses the left half to identify the offending field (so it can pop the relevant CollapseCard open and scroll the input into view) and the right half to localise the reason. The known vocabulary today:

error token Meaning Where it fires
json Body is not valid JSON. Top-level parser.
not_object Body parsed but the root is not a JSON object. Top-level parser.
bad body Content-Length: 0 or > 16 KiB. HTTP wrapper before parse.
<key>:bad_type Known key with the wrong JSON type (e.g. "true" for a bool field, "30" for a uint). Generic kFields walk.
<key>:bad_length String exceeds NVS / hardware bound (e.g. miningPoolUser too long). Per-key validators.
<key>:bad_hex String fails per-key hex validation (e.g. nostrZapPubkey not 64 hex chars). Per-key validators.
<key>:bad_scheme URL string with a forbidden scheme (e.g. nostrRelay not wss:// / ws://). Per-key validators.
<key>:unknown Enum-typed string with an unknown value (fontName, miningPoolName). Per-key validators.
range:<key> Numeric or numeric-bounded string out of [min,max]. Note the inverted shape — the field name is on the right of the colon, not the left. The WebUI's parseSettingsError handles both forms. Generic kFields walk for kUint / kUChar.
screens:partial_order Array contains some entries with order and some without. PATCH must be all-or-nothing. screens[] validator.
screens:bad_entry Entry is not a JSON object, or is missing id. screens[] validator.
screens:unknown_id screens[i].id does not match a defined screen. screens[] validator.
screens:dup_id / screens:dup_order Two entries share an id or order value. screens[] validator.
screens:order_range order outside [0, screens.length). screens[] validator.
screens:incomplete Reorder PATCH did not list every defined screen. screens[] validator.
currency:not_string actCurrencies[] entry is not a string. actCurrencies[] validator.
dnd:range startHour/startMinute/endHour/endMinute outside [0,23]/[0,59]. DND nested-object validator.

The PATCH 200 body is empty when no boot_only field was touched, or {"rebootRequired": true} when at least one was. The WebUI uses this to surface a "restart required" banner without firing a reboot itself.

Grouping rationale

Settings are grouped by the subsystem that owns them rather than by schema type, so the reader can find "where does the Bitaxe host go?" without scanning all booleans. Each group ends with any special-case derived keys the WebUI uses to render that section.


Display

Key Type Default Description Notes
fontName string (enum) "antonio" Font family used by label / digit / small-chars / unit screen roles. Values come from availableFonts in GET (antonio, oswald, inter, sourceSerif, merriweather, bitter, atkinson). Rebound live via the on_font_changed hook, which marks the screen dirty for a repaint. PATCH validates against the catalogue. Devices upgrading from a build that stored the retired "dejavu" value fall back to "antonio" at boot via the validation walk.
invertedColor bool false White-on-black when true, black-on-white when false (default — matches the natural EPD polarity, where un-driven pixels are white). PATCH side-writes fgColor / bgColor to keep EPD polarity consistent. Applied live: EpdSetGlobalInverted + ScreenManager::MarkDirty.
fgColor uint (hex RGB) 0xFFFF EPD foreground colour. Auto-updated when invertedColor is PATCHed; schema does not expose it as a PATCH key, but GET emits it.
bgColor uint (hex RGB) 0x0000 EPD background colour. Same auto-update rule as fgColor.
fullRefreshMin uint 60 Minutes between mandatory full-refresh cycles on the EPDs. Range 0..1440. Honored by ScreenManager::LoadScreenRenderPrefs.
refrScrnChange bool false Force a full EPD refresh on every screen change. Honored by ScreenManager::LoadScreenRenderPrefs.
verticalDesc bool true Rotate label panels 90° CCW so text reads along the panel's long axis. Honored by PaintSlotIntoFb for kLabel / kLabelSplit slots (ports v3 splitText's verticalDesc branch). Applied live via ScreenManager::LoadScreenRenderPrefs.
mcapBigChar bool true Use big-character layout for market-cap and supply screens. Honored by ScreenManager::LoadScreenRenderPrefs.
supplyPercent bool false Render bitcoin supply as a percentage of 21M rather than absolute BTC. Honored by ScreenManager::LoadScreenRenderPrefs.
useBlkCountdown bool true Halving screen: count in blocks remaining, not days. Honored by ScreenManager::LoadScreenRenderPrefs.
useMscwTime bool true Show the sats-per-dollar ("Moscow Time") flip layout. Honored by ScreenManager::LoadScreenRenderPrefs.
useSatsSymbol bool false Use the sats-glyph font for price suffixes. Honored by ScreenManager::LoadScreenRenderPrefs.
blockFeeDec bool true Show decimal sats/vB on the fee-rate screen when the data source reports a precise value. Honored in ScreenManager rendering path.
suffixPrice bool false Use k/M suffixes on the price screen. Honored by ScreenManager::ReadRenderPrefs and threaded through RenderBtcPriceScreen / BuildBtcPrice.
suffixShareDot bool false When true, the decimal-point dot shares the digit panel before it instead of taking its own panel — frees one panel for an extra digit before or after the separator. When false (default), the dot occupies its own panel. Honored by LayoutBtcPriceSuffixStrings via ScreenManager::ReadRenderPrefs. Folds the dot into the preceding digit cell so K/M-suffix layouts get one more digit of width.
mowMode bool false "Mow mode" price formatting — renders the BTC/fiat price in millions of fiat per BTC ($X.XM) instead of the raw integer. Named after Samson Mow, who popularised quoting Bitcoin in millions. Honored by ScreenManager::ReadRenderPrefs and LayoutBtcPriceSuffixStrings.
hideLeadZero bool false Clock screen: drop the leading zero on single-digit hours ("07:00" → "7:00"). Minute leading zero is always preserved. Honored by ComputeClockLayout via ScreenManager::ReadRenderPrefs. Applied live: on_settings_patched calls ScreenManager::MarkDirty. NVS key truncated to hideLeadZero (15-char cap; JSON key matches).

Display-related special keys:

Key Type Description
timePerScreen uint (minutes) PATCH-only alias. Writes timerSeconds = timePerScreen * 60. Sanity capped at 3600 s.
timerSeconds uint (seconds) GET-only (read from NVS key timerSeconds, default 1800). Auto-rotate period.
screens array of {id, name, enabled, order} Per-screen visibility + rotation order. PATCH accepts either visibility-only toggles or a full reorder. Partial reorders are rejected. screen<id>Visible NVS keys default to true.
screenOrder string (CSV of ids) Persisted rotation order. Written indirectly by PATCHing screens[].order.
currentScreen uint Last-shown screen id. Used for restore-on-boot; not PATCH-settable.

Network

Key Type Default Description Notes
hostnamePrefix string "btclock" Hostname prefix used for mDNS + DHCP. The MAC suffix is appended. Reboot required.
mdnsEnabled bool true Advertise _http._tcp and _btclock._tcp over mDNS. Reboot required. Honored in main/app/mdns_service.cpp.
wifiRebootMin uint 10 Minutes of continuous STA disconnect before a soft reboot. 0 disables. Range 0..120. Live — the wifi_guard tick re-reads on every fire.
wpTimeout uint 900 (15 min) WiFiManager captive-portal timeout (seconds). Range 0..3600. Reboot required. After this many seconds in AP-provisioning mode the device reboots so the next boot retries STA — gated on wifiConfigured being true (set once the user submits creds), so a never-provisioned device sits in the portal indefinitely.
txPower int device default WiFi TX power in quarter-dBm. -1..78 valid; 80 is a sentinel that clears the override. Live. Applied via esp_wifi_set_max_tx_power. Re-applied at boot in init_network.cpp between esp_wifi_start and Connect, mirroring the same IsValidWifiTxPower range gate. Queried back on /api/status via esp_wifi_get_max_tx_power.
wifiConfigured bool false Set by the provisioning flow once STA credentials are captured. Not PATCH-settable in practice — internal flag.

Data sources

Key Type Default Description Notes
dataSource uchar (enum) 0 Selects the BTC-price / block / fee data source. 0=BTClock WS, 1=third-party custom endpoint, 2=Nostr. Range 0..3. Reboot required.
mempoolInstance string "mempool.space" Base URL of the mempool.space-compatible instance. Reboot required.
mempoolSecure bool true Use HTTPS/WSS when talking to mempoolInstance. Reboot required.
ceEndpoint string "ws-staging.btclock.dev" Custom-endpoint host when dataSource=1. Reboot required.
ceDisableSSL bool false Disable TLS for the custom endpoint. Reboot required.
minSecPriceUpd uint 30 Minimum seconds between price updates applied to the EPD. Range 1..3600. Live. Throttles the EPD price-write path (kBtcPrice / kMoscowTime / kMarketCap) inside ScreenManager::ShouldRender so a chatty WS price stream can't burn the e-paper. Nav events and force-fulls always paint. 0 disables.
actCurrencies CSV string "USD,EUR,JPY" Comma-joined ISO-4217 codes that appear in the ticker / moscow-time rotation. GET emits a filtered array; PATCH accepts an array. Codes outside availableCurrencies are silently dropped.

Light & LEDs

Key Type Default Description Notes
ledBrightness uint (0..255) 128 NeoPixel master brightness. Range 0..255. Stored under NVS namespace led/brightness by the LED controller (separate namespace). The settings-namespace value mirrors the WebUI PATCH write; consumers read the led namespace at boot.
blockFlashColor uint (hex RGB) 0xE04300 RGB colour pulsed on LEDs when a new block arrives. Range 0..0xFFFFFF. See led namespace note above.
disableLeds bool false Master mute flag for the NeoPixel strip. Honored by led_controller.cpp.
ledFlashOnUpd bool false Flash LEDs on new-block / price-update events. Honored by led_controller.cpp.
ledFlashOnZap bool true Flash LEDs on incoming Nostr zap receipt. Live — zap listener rechecks per receipt.
ledTestOnPower bool true Run the rainbow LED self-test at boot. Honored by InitBootLeds: when false, posts kSetIdle instead of the rainbow kSetBoot. Reboot required.
flAlwaysOn bool true Frontlight always on, overriding ambient-off. Rev B only. Read at boot in init_hardware.cpp; live PATCH applied via on_frontlight_changed.
flDisable bool false Master mute for the frontlight. Rev B only. Read at boot in init_hardware.cpp; live PATCH applied via on_frontlight_changed.
flEffectDelay uint 15 Fade-effect step delay (ms). Range 0..1000. Rev B only. Read at boot in init_hardware.cpp; live PATCH applied via on_frontlight_changed.
flFlashOnUpd bool true Flash frontlight on update events. Rev B only. Read at boot in init_hardware.cpp; live PATCH applied via on_frontlight_changed.
flFlashOnZap bool true Flash frontlight on Nostr zap receipt. Live — zap listener rechecks per receipt.
flMaxBrightness uint 2048 (kDefaultMaxDuty) Frontlight PWM duty at 100 percent. Range 0..65535. Rev B only. Read at boot in init_hardware.cpp; live PATCH applied via on_frontlight_changed.
flOffWhenDark bool true Turn frontlight off when ambient lux is very low. Rev B only. Hysteresis: enters dark at 1.0 lux, exits at 2.0 lux. Read at boot in init_hardware.cpp; live PATCH applied via on_frontlight_changed.
luxLightToggle uint 128 (kDefaultLuxThreshold) Ambient-lux threshold that flips the frontlight between "room bright" and "room dim". Range 0..65535. Rev B only. Read at boot in init_hardware.cpp; live PATCH applied via on_frontlight_changed. 0 disables the auto-off feature (matches v3).
inverseButtons bool false Swap the button-1/button-4 polarity. Honored by ButtonReader::SetInverted — physical pin i is delivered as logical ButtonId(N-1-i). Read once at boot from sources.cpp; reboot required.

Do Not Disturb

GET emits a nested dnd object; PATCH takes the same shape.

Nested key Type Default Description Notes
dnd.enabled bool false Manual "force on now" flag. Live.
dnd.dndTimeEnabled bool false Enable scheduled DND. Live, mirrored into the DND singleton by on_dnd_changed.
dnd.startHour uint (0..23) 22 Schedule start hour. Live.
dnd.startMinute uint (0..59) 0 Schedule start minute. Live.
dnd.endHour uint (0..23) 7 Schedule end hour. Live.
dnd.endMinute uint (0..59) 0 Schedule end minute. Live. All four hour/minute values must be present in the same PATCH or the block is skipped.

NVS keys: dndEnabled, dndTimeEnabled, dndStartHour, dndStartMin, dndEndHour, dndEndMin. Each is also listed individually in the top-level GET response (so a GET returns both the flat keys and the nested dnd block).


Time zone

Key Type Default Description Notes
tzString string (IANA) "Europe/Amsterdam" IANA zone name (e.g. "Europe/Amsterdam"). Live: PATCH triggers timezone::SetTimezoneByName which calls setenv("TZ", ...) + tzset().

Both gmtOffset and the tzOffset (minutes) PATCH alias were removed in v4. The firmware drives the clock from POSIX TZ strings via setenv("TZ", ...) + tzset(), and the old offset pref was never read back. Legacy clients that still send gmtOffset / tzOffset get a silent no-op (the rest of the PATCH body still applies).


Mining pool

Key Type Default Description Notes
miningPoolStats bool false Master toggle — when false no pool data source runs. Live at data-source restart.
miningPoolName string (enum) "noderunners" Pool backend to use. Values come from availablePools in GET: ocean, noderunners, satoshi_radio, braiins, public_pool, local_public_pool, gobrrr_pool, ckpool, eu_ckpool, nerdminers_org, nerdminer_io, foundry_usa, viabtc. PATCH validates against the catalogue. v4 default flipped from v3's "ocean" so a fresh device works without per-user credentials (paired with poolGlobalStats=true).
miningPoolUser string "38Qkkei3SuF1Eo45BaYmRHUneRD54yyTFy" Primary identifier for the active pool. Semantics are per-pool. Public-info pools (Ocean payout address, Braiins username, CKPool worker, NerdMiner BTC address): emitted plaintext in GET. Secret-key pools (viabtc, foundry_usa): the value is an API key — GET suppresses the raw string and emits miningPoolUserSet (bool) instead, same protocol as httpAuthPass. See docs/WEBUI_MINING_POOL_FIELDS.md for the per-pool labels the WebUI should render.
poolWorker string "" Optional secondary identifier scoped under miningPoolUser. Used by foundry_usa for the subaccount path segment; reserved for braiins / ckpool worker name (currently unused by their parsers). Always plaintext in GET.
poolGlobalStats bool true Noderunners / Satoshi Radio: show the pool-wide aggregate rather than the user-specific view. Honored by mining_pool_selector.cpp. v4 default flipped from v3's false so the new default noderunners pool renders without per-user credentials.
localPoolHost string "umbrel.local:2019" Host:port for local_public_pool. Reboot required.
poolPollSec uint 60 HTTPS poll cadence for the pool data source (seconds). Range 10..3600. Live — PoolDataSource::poll_interval_ms() re-reads NVS each tick so a PATCH lands on the next poll without reboot. Bounds leave room to throttle past the legacy 60 s default for pools that publish per-minute stats already, without overwhelming free public endpoints.
poolLogosUrl string "https://git.btclock.dev/btclock/mining-pool-logos/raw/branch/main" Base URL for fetching mining-pool logos. Consumed by pool_logo_fetcher.cpp::LogosBaseUrl() — the on-demand fetcher pulls per-pool PNGs and caches them on LittleFS, so the firmware no longer ships every logo bitmap in flash. Trailing slashes are trimmed so concatenation can't yield a //.

Bitaxe

Key Type Default Description Notes
bitaxeEnabled bool false Poll a Bitaxe miner for hashrate / best difficulty. Live at data-source restart.
bitaxeHostname string "bitaxe1" LAN hostname or IP of the Bitaxe. Consumed by components/bitaxe/src/bitaxe_source.cpp.
bitaxePollSec uint 10 LAN poll cadence (seconds). Range 5..300. Live — BitaxeSource::Run() re-reads NVS each tick so a PATCH lands on the next poll without reboot. Lower bound keeps the AxeOS HTTP server from being hammered while still allowing fast updates during bring-up.

Nostr / zap

Key Type Default Description Notes
nostrRelay string "wss://relay.primal.net" Relay URL for the primary data feed when dataSource=2. Also reused by the zap listener. Reboot required. PATCH validates the scheme: only wss:// or ws:// are accepted (or an empty string to disable). Bare hostnames (relay.example.com) and https:// URLs return 400 {"error":"nostrRelay:bad_scheme"} — the underlying esp_websocket_client parser would otherwise silently reject them at boot with Error parse uri.
nostrPubKey string (64-hex) "642317135fd4c4205323b9dea8af3270657e62d51dc31a657c0ec8aab31c6288" Author pubkey the data-source listener follows. Reboot required. Validated: empty or exactly 64 lowercase-hex chars.
nostrZapNotify bool false Listen for NIP-57 zap receipts and pop the zap screen. Live (zap listener rechecks).
nostrZapPubkey string (64-hex) "b5127a08cf33616274800a4387881a9f98e04b9c37116e92de5250498635c422" Pubkey whose zaps trigger the zap screen. Live. Validated: empty or 64-hex.
scrnRestoreZap bool true After a zap-screen pop, restore the screen that was active before. Live.
stealFocus bool false When a new block arrives, jump the display to the block-height screen so the fresh height appears without waiting for rotation. Live — event_loop.cpp reads NVS per new-block event so a PATCH lands without reboot. Overlay-aware: debug / custom / zap overlays block the steal via BlockEventPolicy::ShouldSteal.

HTTP auth / OTA

Key Type Default Description Notes
httpAuthEnabled bool false Require HTTP basic auth on control-API endpoints. Reboot required.
httpAuthUser string "btclock" HTTP basic-auth username. Reboot required. Default lives in auth_gate.cpp::kDefaultUser.
httpAuthPass string "" HTTP basic-auth password. Reboot required. GET suppresses the value and emits httpAuthPassSet (bool) instead.
otaEnabled bool true Enable the OTA firmware-upload endpoints. Reboot required.
otaPass string "" Password gating the OTA firmware upload. Reboot required. GET suppresses the value and emits otaPassSet (bool) instead.
gitReleaseUrl string "https://git.btclock.dev/api/v1/repos/btclock/btclock_v3/releases/latest" Base URL the auto-update endpoint pulls releases from. Live — consumed by OtaManager::Init and /api/firmware/auto_update.

Diagnostics

Key Type Default Description Notes
enableDebugLog bool false Enable verbose debug logging. Reboot required. Honored in InitStorage immediately after NVS comes up: when true, esp_log_level_set("*", ESP_LOG_DEBUG) raises every subsystem's log level for the rest of the boot. The pre-NVS banner lines stay at INFO.
timerActive bool true Rotation timer running. Not typically PATCHed (use /api/action/pause); the pref exists for boot restore.
blockHeight int 0 Cached latest block height. Internal; persisted so the block screen can paint before WS reconnect.

Read-only fields (GET only — not PATCH-settable)

These appear in the GET /api/settings response but are not writable via PATCH. They're derived from the running firmware and the device context.

Key Type Source Description
hostname string Wifi STA Resolved <hostnamePrefix>-<mac> hostname.
ip string Wifi STA Current STA IP, empty in AP mode.
hwRev string compile-time Human-readable hardware revision (e.g. "Rev B").
fsRev string compile-time LittleFS bundle version string.
gitRev string compile-time Git SHA of the firmware. Omitted when unknown.
gitTag string compile-time Git tag, when the build was tagged. Omitted when empty.
lastBuildTime int compile-time Firmware build time as Unix seconds (UTC). Omitted when the compiler emitted an unparseable __DATE__.
numScreens int board config Number of EPD panels on this board (3 on Rev A / Rev B, 7 on V8). Emitted identically by GET /api/status so the WebUI's custom-text maxlength agrees with both endpoints. Not related to the screen-rotation slot count.
screens[] array of objects kScreenKinds + NVS Per-screen rotation catalogue, see the Display section.
availableFonts[] string array kAvailableFonts Font ids the WebUI picker shows.
availablePools[] string array AvailablePoolNames() Mining-pool ids the WebUI picker shows.
availableCurrencies[] string array kAvailableCurrencies ISO codes the price feed can serve.
hasFrontlight bool board capability Whether this board has a PCA9685 frontlight (Rev B only today).
hasLightLevel bool board capability Whether a BH1750 ambient sensor is populated (Rev B only today).
lightLevel number (lux) BH1750 Current lux reading. Only emitted when hasLightLevel is true.
httpAuthPassSet bool NVS presence true iff httpAuthPass is non-empty.
otaPassSet bool NVS presence true iff otaPass is non-empty.
miningPoolUserSet bool NVS presence Emitted only when the active miningPoolName is a secret-key pool (viabtc, foundry_usa); true iff miningPoolUser is non-empty. For public-info pools the raw miningPoolUser rides plaintext and this companion is omitted.
timerRunning bool runtime Rotation-timer live state.
invertedColor bool NVS Default false (black-on-white) when the key is absent.

Two additional GET-only bodies are surfaced by sibling endpoints that the WebUI calls alongside /api/settings:

  • GET /api/system/status — returns txPower (queried live via esp_wifi_get_max_tx_power), rssi, espFreeHeap, PSRAM totals, fsUsedBytes, fsTotalBytes, uptime.
  • GET /api/status — the compact render / rotation snapshot.

Honoured-status guarantee

Every schema key listed above has a read site outside components/settings/ — i.e. PATCHing it actually changes device behaviour, not just NVS contents. The "Notes" column on each row points to the consumer (ScreenManager::ReadRenderPrefs, on_frontlight_changed, init_hardware.cpp, etc.) so the wiring is auditable from this page.

Two exceptions, intentional and documented in their rows:

  • httpAuthPass / otaPass — secrets are never echoed; presence is surfaced via the …Set companion booleans.
  • wifiConfigured — internal provisioning-flow flag, set by the AP flow rather than by user PATCH.