Skip to content

Settings NVS

CJvanSoest edited this page Jun 23, 2026 · 2 revisions

Settings / NVS

All persistent state lives in NVS under namespaces declared in components/mc_domain/settings_nvs.h and components/mc_domain/identity.h (the system namespace is shared with the launcher for owner_name).

Persistent keys

lora namespace (settings_nvs.c)

Key Type Default Range / values
freq u32 869_618_000 Hz EU 868 ISM band
sf u8 8 7..12
bw u32 62 (kHz) 7, 10, 15, 20, 31, 41, 62, 125, 250, 500
cr u8 8 5..8 (4/5..4/8)
pwr i8 22 dBm -9..22 (chip limit)
sync u8 0x2B 0x00..0xFF (0x12 = public LoRa)
preamble u16 16 6..65535
fldadv_s u16 0 (off) 0, else 30s..24h — periodic flood advert
diradv_s u16 0 (off) 0, else 30s..24h — periodic direct advert
role u8 CHAT_NODE CHAT_NODE / REPEATER / ROOM_SERVER / SENSOR
path_h u8 1 1..3 (see protocol page)
country str(4) "--" ISO 3166-1 alpha-2; "--" = none (no checks)
antgain i8 0 dBi -3..15; editable only once country"--"
rxboost u8 1 (on) 1 = boosted RX (+3 dB sensitivity), 0 = power-save
gps.lat i32 Latitude ×1e6 (advert position + Nodes Dist column)
gps.lon i32 Longitude ×1e6

All of the above live in the single system NVS namespace under dotted key names (lora.freq, lora.country, …); the lora. prefix is logical, not a separate namespace.

system namespace (shared with launcher)

Key Type Purpose
owner_name str (≤32) Owner display name (shared with launcher)
last_time_s i64 Last good SNTP timestamp; restored on boot without WiFi
gps_lat_e6 i32 Saved latitude × 1e6 (MeshCore upstream scale)
gps_lon_e6 i32 Saved longitude × 1e6
gps_valid u8 1 if gps_lat_e6/gps_lon_e6 hold a real fix, 0 otherwise
gps_src u8 Source tag: 0=none, 1=manual, 2=PA1010D, 3=USB-CDC, 4=BLE, 5=HTTPS /ping
ble_enabled u8 0/1 — NimBLE companion radio on next start
ble_gps_pref u8 iPhone-app mirror of "share my position in adverts"
adv_loc_pol u8 ADVERT_LOC_NONE (0) / ADVERT_LOC_SHARE (1) — same toggle from the iPhone app, used by the ADVERT serializer
http_api_key str (64) Shared secret for the HTTPS /ping endpoint. Auto-generated 64 hex chars on first boot if missing. Rotated by Settings → Network → Regenerate API key.
ant_gain i8 Antenna gain in dBi; ERP/EIRP power-check input

http_cert namespace (per-badge HTTPS cert)

Per-badge ECDSA P-256 self-signed cert generated on first boot and persisted across reflashes. See GPS sources → HTTPS /ping for the iPhone profile install dance.

Key Type Purpose
cert_pem str (PEM) X.509 cert with SAN DNS:tanmatsu.local, DNS:tanmatsu, 10-year validity
key_pem str (PEM) ECDSA P-256 private key

Both wiped + regenerated by Settings → Network → Regenerate cert. Triggers a full HTTPS server restart, so the iPhone profile install must be repeated after a regen.

Sound preferences (in system namespace)

Key Type Default Purpose
snd_vol u8 50 Master volume 0..100 %
snd_dm u8 1 WAV slot for DM RX (0=off, 1..4 = /sd/meshcore/sounds/<n>.wav)
snd_ch u8 2 WAV slot for channel RX
snd_err u8 3 WAV slot for the error event (played on error paths and the Sounds preview)
snd_boot u8 4 WAV slot played at end of app_main init

ui keys (per-app brightness, in system namespace)

Per-app overrides for the three BSP backlight / LED globals. Applied on boot by apply_brightness() and again on every value change in the Brightness settings category, so the launcher's globals stay untouched once MeshCore is running. All three are u8, range 0..100 %, defaults chosen to match the backlog #47 trade-off (battery vs. visibility).

Key Type Default BSP call
ui.disp_bl u8 50 bsp_display_set_backlight_brightness(pct)
ui.kb_bl u8 50 bsp_input_set_backlight_brightness(pct)
ui.led_br u8 5 bsp_led_set_brightness(pct)

Edited via Settings → Brightness; the slider cycles through 5 / 10 / 25 / 50 / 75 / 100 % stops. The launcher's globals are NOT restored on exit — BadgeVMS PIE ELF apps have no clean exit hook, so the MeshCore value persists until the launcher writes its own value next time the launcher's brightness UI runs.

map keys (VIEW_MAP state, in system namespace)

Dotted map.* keys persisting the map view's centre, zoom, lock toggle and style. Loaded on boot by map_state_init / load_map_profile; centre + zoom are debounce-saved while panning. All in the single system namespace.

Key Type Default Range / values
map.lat_e6 i32 Den Haag fallback Centre latitude × 1e6
map.lon_e6 i32 Den Haag fallback Centre longitude × 1e6
map.zoom u8 8 6..17
map.lock u8 1 (on) Lock-to-GPS toggle
map.profile u8 MAP_PROFILE_CARTO map_profile_t; the loader clamps a stored-but-disabled style to the first entry of MAP_PROFILES_ENABLED[] (Carto-only by default — see Maps)

mc namespace (MeshCore-specific identity)

Key Type Purpose
adv_name str (≤32) Overrides owner_name in adverts when non-empty
region str (≤8) Region scope tag (NL, DE, …)
node_pub blob 32 Ed25519 public key
node_prv blob 32 Ed25519 private key

contacts namespace (contacts.c)

Key Type Purpose
count u8 Number of saved contacts
c<n> blob Contact n: pub key + alias + role

MAX_CONTACTS = 16; pubkey-prefix collisions are accepted but rare in practice.

C6 sync flow

The C6 also holds its own copy of LoRa config. On boot:

  1. load_lora_from_nvs reads the P4 copy into lora_cfg.
  2. lora_get_config(&c6_cfg) reads the C6 copy.
  3. If c6_cfg.frequency != 0, the C6 wins → lora_cfg = c6_cfg and we persist back to NVS.
  4. If c6_cfg.frequency == 0 (blank — typical after a fresh radio flash), we push lora_cfg to the C6 with lora_set_config(&lora_cfg).

Edits in the Settings tab call save_lora_to_nvs and lora_set_config immediately on save, so the two copies stay in sync.

LoRa presets

settings_nvs.c carries a LORA_PRESETS[] table. Selecting a preset in the Settings tab overwrites SF, BW, CR (and sync word for the MeshCore preset). Frequency, TX power, preamble and role are not touched by presets.

Default presets shipped:

Name SF BW CR Notes
MeshCore 8 62 4/6 Matches stock MeshCore default net
LongFast 11 250 4/5 Meshtastic-style fast & long-range
LongSlow 12 125 4/8 Maximum range, very slow
ShortFast 7 500 4/5 Local, high throughput

When a custom combination is active the Settings row shows (custom).

Regulatory compliance

components/mc_proto/region_limits.{h,c} carries a per-country table of the licence-exempt ISM-band rules. Picking a Country in the Settings tab drives three things: off-band / over-power warnings, the antenna-gain → ERP/EIRP calculation, and a hard duty-cycle budget enforced in radio.c.

Enforcement model

Limit Mode Where
Frequency outside any sub-band soft — Country/Frequency rows turn red, footer hint render.c
Effective power > sub-band max soft — TX power row turns red render.c (region_effective_power_dbm)
Duty cycle (rolling 1 h airtime) hard — TX blocked when budget spent radio.c (Semtech time-on-air)

The split is deliberate: a wrong frequency or a few dB over is the operator's call to make (they may have a licence, or a directional antenna), but airtime hogging actively degrades the shared mesh, so the duty-cycle cap is enforced.

The data table is a guidance helper, not legal advice. It is also an interim app-side implementation — radio-side enforcement may move into the C6 firmware once upstream tanmatsu-radio grows a compliance layer.

Data schema

typedef struct {
    float    freq_min_mhz, freq_max_mhz;
    int8_t   max_power_dbm;        // unit per containing country (ERP or EIRP)
    uint16_t duty_cycle_permille;  // 1000 = none, 100 = 10%, 10 = 1%, 1 = 0.1%
    bool     lbt_alternative;      // LBT may replace duty cycle (informational)
    uint16_t max_dwell_time_ms;    // 0 = none (FCC FHSS = 400)
    const char *label;             // e.g. "g3"
} regulatory_subband_t;

typedef struct {
    const char *country_code;      // ISO 3166-1 alpha-2
    const char *display_name;
    power_unit_t power_unit;       // POWER_UNIT_ERP | POWER_UNIT_EIRP
    const regulatory_subband_t *subbands;
    uint8_t n_subbands;
} regulatory_country_t;

ERP vs EIRP

The EU regulates in ERP (dipole reference); the US/JP/AU/NZ/etc in EIRP (isotropic). The app converts your conducted TX power to the country's unit before comparing against the limit:

ERP  = conducted_dBm + antenna_gain_dBi − 2.15
EIRP = conducted_dBm + antenna_gain_dBi

So at the SX1262's 22 dBm conducted ceiling with a 0 dBi antenna you radiate ~19.85 dBm ERP — comfortably under the EU g3 limit of 27 dBm ERP.

EU 863–870 MHz sub-bands (ERP) — EU868_SUBBANDS

Band Range (MHz) Max power Duty cycle
g 863.0–865.0 14 dBm 0.1%
g1 865.0–868.0 14 dBm 1%
g1' 868.0–868.6 14 dBm 1%
g2 868.7–869.2 14 dBm 0.1%
g3 869.4–869.65 27 dBm 10%
g4 869.7–870.0 14 dBm 1%

Used by: NL, BE, DE, AT, FR, CH, UK, IT, ES, PT, SE, NO, DK, FI, PL, CZ, IE, UA, ZA (19 countries). Default 869.618 MHz → g3.

Other regions

Constant Countries Range (MHz) Max power Duty / dwell
EU433_SUBBANDS 433 SRD 433.05–434.79 10 dBm ERP 10%
US915_SUBBANDS US, CA, MX 902–928 30 dBm EIRP none (FHSS dwell 400 ms info)
ANZ915_SUBBANDS AU, NZ 915–928 30 dBm EIRP none
JP920_SUBBANDS JP 920.5–923.5 13 dBm EIRP 10% + LBT required
KR920_SUBBANDS KR 920.0–923.0 14 dBm EIRP 10%
IN865_SUBBANDS IN 865.0–867.0 30 dBm EIRP none
RU864_SUBBANDS RU 864–865 / 868.7–869.2 14 dBm ERP 0.1%

Duty-cycle accounting

radio.c keeps 60 one-minute airtime buckets (rolling hour). Every TX path estimates on-air time with the Semtech LoRa time-on-air formula (AN1200.13) and checks dc_budget_available() before sending; region_dc_budget_ms_per_hour turns the sub-band's permille into the ms/hour budget (10% → 360 000 ms). The Settings Duty cycle (1h) row surfaces used.x% (used s / budget s) and appends BLOCKED when a send was just refused.

Sources

  • EU 863–870: ETSI EN 300 220 V3.2.1 + ERC-REC-70-03
  • US 902–928: FCC Part 15.247 / 15.249
  • Country→band mapping: Lansitec LoRaWAN frequency-plan reference
  • Cross-check only: Meshtastic src/mesh/RadioInterface.cpp (it collapses each region to a single sub-band; we keep full EU sub-band granularity)

Clone this wiki locally