Commit Graph
100 Commits
Author SHA1 Message Date
Jorropo 9e529da460 replace screens with unique_ptr (#11163) 2026-07-25 11:51:13 +00:00
Andrew YongandBen Meadors b71c1adb26 stm32wl: add hardware RTC support (rak3172) (#10961)
* stm32wl: add hardware RTC support infrastructure

Wires the STM32WL chip's internal RTC (running off the LSE 32.768kHz
crystal) into meshtastic's existing time-of-day framework
(perhapsSetRTC()/readFromRTC()), following the same pattern already
used for I2C RTC chips (RV3028, PCF8563/85063, RX8130CE).

LSE is started and polled manually before ever calling into the
STM32RTC library, with our own bounded timeout - the library's own
internal LSE startup path has no bounded fallback and hangs forever
via Error_Handler() if the crystal never locks, so this is required
for a board with a missing/faulty crystal to boot normally rather
than hang.

Gated behind a new HAS_LSE variant flag (currently unset everywhere,
so this is inert until a variant opts in - see follow-up commit).

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

* gps: qualify RTC.h includes to avoid case-insensitive filesystem collision with STM32RTC

The stm32duino STM32RTC library (added to lib_deps in a follow-up
commit) ships its own src/rtc.h. On case-insensitive filesystems
(the macOS default), an unqualified #include "RTC.h"/<RTC.h> from
any file outside src/gps/ resolves to the library's rtc.h instead of
src/gps/RTC.h, since PlatformIO's LDF puts lib_deps include paths
ahead of the project's own -Isrc/gps.

Qualify every include as gps/RTC.h so it can't collide with any
same-named header a future dependency might ship, regardless of
filesystem case sensitivity. Purely mechanical, no behavior change.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

* stm32wl(rak3172): enable hardware RTC support

Opts rak3172 into the HAS_LSE infrastructure added previously: sets
STM32WL_LSE_DRIVE to a conservative default and pulls in the
STM32RTC library. rak3172 has ~63KB flash headroom going in;
build-verified at 76.7% flash usage after this change (up from a
73.8% baseline), well within budget.

wio-e5 is not opted in here despite sharing the same STM32WLE5 chip
- it's already at 96.8% flash usage today (GPS + I2C sensor support
compiled in, unlike rak3172), leaving too little headroom to safely
add STM32RTC without first trimming something else.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

* stm32wl: add docstrings for LSE/RTC setup functions

Addresses CodeRabbit's docstring coverage check on PR #10961.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

* stm32wl: address CodeRabbit nitpicks on PR #10961

- Brace the single-statement HAS_LSE branch in perhapsSetRTC() to
  match the sibling readFromRTC() branch's style.
- Quote the RTC.h include in PhoneAPI.cpp for consistency with every
  other qualified include site.

Signed-off-by: Andrew Yong <me@ndoo.sg>
Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>

---------

Signed-off-by: Andrew Yong <me@ndoo.sg>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-07-16 19:00:45 -05:00
e64d20548c feat(eth): port mesh/http to RP2350 + W5500 (HTTP/80 + HTTPS/443) (#10573)
* add watchdog to rp2xx0

* feat(eth-api): phase 0 skeleton — HTTP API server on TCP/80 (503 only)

First step of porting mesh/http/ to RP2350 + W5500 (today ESP32-only).
Phase 0 stands up the listener over the existing arduino-libraries/Ethernet
stack (no Mongoose — its built-in TCP/IP would conflict with EthernetServer
and break OTA/MQTT/NTP). Skeleton parses request line, logs it, replies 503.
Real handlers (/api/v1/{info,fromradio,toradio}) come in later phases.

- New mesh/eth/ethApiServer.{h,cpp} gated by HAS_ETHERNET_API
- Wired into ethClient init+loop in parallel with existing ethHttpOTA (port 4244)
- Enabled in both wiznet_5500_evb_pico2_e22p and pico2_w5500_e22 variants
- Build impact on wiznet: +~9 KB flash (63.0% used), <1 KB RAM

* feat(eth-api): phase 1 — implement /api/v1/{fromradio,toradio} with PhoneAPI

Replace phase 0 skeleton with the real handlers that bridge the HTTP transport
to PhoneAPI, mirroring mesh/http/ContentHandler (ESP32) semantics.

- EthHttpAPI : public PhoneAPI (api_type=TYPE_HTTP, checkIsConnected=true),
  outside the MESHTASTIC_EXCLUDE_WEBSERVER gate so it builds on RP2350.
- Minimal HTTP parser: method/path/query/Content-Length, no allocations beyond
  Arduino String, capped at 32 header lines x 256 bytes (anti-DoS).
- OPTIONS preflight -> 204 with CORS + X-Protobuf-Schema.
- GET /api/v1/fromradio[?all=true]: stream-write up to 64 protobufs from
  webAPI.getFromRadio(), Connection: close framing (HTTP/1.0 style).
- PUT /api/v1/toradio: read Content-Length bytes (<=512), call
  webAPI.handleToRadio(), echo body back.
- 404 default for unknown paths, 405 for wrong method, 400 for bad body.

Validated e2e against wiznet @ 192.168.1.143:
- OPTIONS /api/v1/fromradio -> 204 + correct CORS headers
- PUT ToRadio{want_config_id=1} (2 bytes) -> 200 + echo
- GET /api/v1/fromradio?all=true -> 200 + 3476 bytes (config+channels+nodeinfo)
- GET /api/v1/nonexistent -> 404 unknown endpoint
- OTA HTTP on port 4244 untouched and still responsive

Build impact on wiznet_5500_evb_pico2_e22p: +2 KB flash (63.1%), +1.8 KB RAM
(17.6%) vs phase 0.

* fix(eth-api): move accept loop to dedicated OSThread (20ms tick)

The API server was being polled from the Ethernet client Periodic which runs
every 5s. Measured impact before this fix on wiznet @ 192.168.1.143:

  req 1: ttfb=6.458s total=6.509s    (matches the 5s tick + handler overhead)
  req 2: connection refused          (W5500 sockets exhausted)
  req 3: connection refused

After: same hardware, same network, back-to-back requests:

  req 2: ttfb=0.287s
  req 3: ttfb=0.015s
  req 4: ttfb=0.022s
  req 5: ttfb=0.020s

The web client (meshtastic/web served from localhost) was visibly stalling
mid-handshake — it had pulled 109 nodes + 19 messages but channels and device
info never arrived. With the periodic-only polling, every request takes 5s
and the W5500's 4 hardware sockets fill up under the burst.

EthApiServerThread mirrors the WebServerThread pattern from ESP32
mesh/http/WebServer.cpp: adaptive interval — 20ms when there's recent
traffic, 100ms after 5s of idle, 500ms after 30s. Auto-registers with the
OSThread scheduler on construction.

ethHttpOTA still ticks from the periodic; left unchanged because OTA is one
large transfer that tolerates the latency, and minimizing scope here to one
behaviour change at a time.

* refactor(eth-api): extract handlers to shared module via IStreamReadWrite

Phase 2.0 of the TLS port — separate transport from request handling so
the upcoming HTTPS server can reuse the exact same parser + routing logic.

- New ethApiHandlers.{h,cpp}: Request, parser, CORS helpers, fromradio/toradio
  handlers, EthHttpAPI : PhoneAPI subclass. All driven by a single
  IStreamReadWrite interface that inherits Print (so client.print(...) keeps
  working transparently).
- ethApiServer.cpp slimmed to ~90 LOC: now just the EthernetServer(80) +
  OSThread + an EthernetClientStream adapter that forwards reads/writes to
  the underlying EthernetClient. No behaviour change.

Validated on wiznet @ 192.168.1.143: 5 curl requests TTFB 18-110ms (same
as pre-refactor), protobuf round-trip PUT 200 + GET ?all=true 3499 bytes.
Flash impact: +200 bytes (63.2%); RAM unchanged (17.6%).

* feat(eth-tls): ECDSA P-256 self-signed cert generation for HTTPS API

mbedTLS-based cert generation module that produces a SAN=IP self-signed
ECDSA P-256 server certificate, persisted under LittleFS so subsequent
boots reuse the same key. Generation runs once on a dedicated OSThread
so the ECDSA keygen path (~430 ms) never blocks the Periodic stack or
the Ethernet reconnect loop.

  - mbedTLS 3.6.2 sources compiled in via scripts/add_mbedtls_sources.py
    (BuildSources of pico-sdk/lib/mbedtls/library/*.c, all 108 files).
    MBEDTLS_USER_CONFIG_FILE injected as CPPDEFINES tuple in the script —
    build_flags shell escape mangles the embedded quotes on Windows.
  - src/mbedtls_user_config.h undefs MBEDTLS_HAVE_TIME, HAVE_TIME_DATE,
    TIMING_C, NET_C, FS_IO, PSA_ITS_FILE_C, PSA_CRYPTO_STORAGE_C, and
    defines MBEDTLS_NO_PLATFORM_ENTROPY (entropy_poll.c uses a raw
    platform check, not gated by a flag).
  - src/mesh/eth/ethCert.{h,cpp}: ECDSA P-256 keypair + cert with
    SAN(IP=current) using pico-sdk get_rand_64() directly as f_rng,
    bypassing mbedtls_entropy. DER buffers heap-allocated to keep the
    OSThread stack within budget. Cert + key + ip persisted under
    /prefs/eth_*.der so subsequent boots reuse the same identity.
  - Gated by HAS_ETHERNET_TLS_API. Standalone phase: generation only;
    HTTPS server (TCP/443) wired up in the follow-up commit.

* feat(eth-tls-api): phase 2.2 — HTTPS server on TCP/443 reusing handlers

Brings up an mbedTLS server on port 443 that defers to the ECDSA P-256
self-signed cert produced by [[ethCert]]. Reuses the HTTP request /
response handlers from [[ethApiHandlers]] via the IStreamReadWrite
interface — there is exactly one code path for routing / CORS / PhoneAPI
integration regardless of whether the transport is plain TCP or TLS.

EthTlsApiServerThread (OSThread):
  - Phase A: poll isEthCertReady() every 500 ms while the cert worker
    runs. Once true, parse cert chain + key, build ssl_config (TLS server,
    stream transport, default preset, VERIFY_NONE since we are the cert
    issuer), install own cert, run ssl_setup, bind tlsServer on 443.
  - Phase B: standard adaptive accept loop (20 / 100 / 500 ms tick),
    identical to the plain-HTTP server.

Per-connection flow:
  1. session_reset on the static ssl context (1 in-flight session — multi-
     session pool is Phase 3 if needed)
  2. set_bio routes mbedtls I/O to two C callbacks (netSend / netRecv)
     that bridge to the live EthernetClient via the void* ctx
  3. handshake loop (sync, blocking, with 10 s recv timeout) — mbedTLS
     errors are logged with mbedtls_strerror
  4. wrap (ssl, client) in MbedTlsStream → handleApiClient(stream)
  5. close_notify + client.stop

Stack budget continues the Phase 2.1-bis discipline: every large buffer
(ssl context, cert chain, pk_key, ssl_config) lives in BSS as a static
global. The OSThread stack only holds the per-connection EthernetClient
adapter and small mbedtls return codes.

Validated on wiznet 192.168.1.143:
  - cert load-from-FS path: 13 ms (regen skipped on second boot)
  - cert gen path: 290 ms (first boot only)
  - ssl_setup chain: parse cert, parse key, ssl_config_defaults,
    conf_own_cert, ssl_setup all return 0
  - end-to-end: curl -k https://192.168.1.143/api/v1/fromradio → 200 OK
    with application/x-protobuf, CORS, X-Protobuf-Schema headers, server
    initiates close_notify cleanly

Footprint vs 2.0:
  - flash 70.5% → 87.3% (+220 KB for mbedtls_ssl + x509 server-side code)
  - RAM static 17.6% → 19.8% (+11 KB BSS for ssl_context + cert chain)
  - heap at runtime adds ~32 KB per active session (mbedtls in/out
    record buffers, default MBEDTLS_SSL_*_CONTENT_LEN=16384)

Next: validate from Firefox direct (https://192.168.1.143 → warning
accept → JSON visible), then from client.meshtastic.org hosted to
confirm the mixed-content block is gone.

* fix(eth-tls): add KeyUsage + EKU serverAuth and cap TLS 1.2 for browser compat

Two browser-compat fixes that surfaced in Firefox validation:

1. Cert v1 only had Basic Constraints + SAN. NSS / Firefox refuse to
   treat a cert as a TLS server cert without an Extended Key Usage
   extension naming id-kp-serverAuth since 2023 — the error surfaces as
   a non-overridable 'Secure Connection Failed' with no 'Accept the
   Risk' path. Add KeyUsage(digitalSignature + keyEncipherment, critical)
   and ExtendedKeyUsage(serverAuth, critical). Bump cert/key/ip file
   paths to '_v2' so live boards regenerate on next start instead of
   loading a v1 cert that the browser silently refuses.

2. pico-sdk mbedtls defines MBEDTLS_SSL_PROTO_TLS1_3 in its default
   config, but the server-side 1.3 plumbing in this vendored build is
   incomplete: Firefox and openssl-3.5's s_client default to 1.3 and
   the handshake dies 4 ms in with MBEDTLS_ERR_ERROR_GENERIC_ERROR
   (-0x0001). curl/SChannel happened to default to 1.2 so it masked the
   issue earlier. Cap min/max_tls_version to TLS 1.2 — clients downgrade
   transparently and we keep the ECDHE-ECDSA + AES-GCM / CHACHA20-POLY1305
   suites that already work end-to-end.

Validated on wiznet 192.168.1.143:
  - cert v2 dumps with the 4 extensions visible (openssl x509 -text):
    BasicConstraints CA:FALSE, KeyUsage(critical) digitalSignature +
    keyEncipherment, ExtendedKeyUsage(critical) serverAuth, SAN IP.
  - openssl s_client (no version flag): downgrades to TLS 1.2, handshake
    completes, verify_return=18 (self-signed, expected).
  - Firefox: warning self-signed -> Advanced -> Accept Risk -> handshake
    OK in 632 ms with CHACHA20-POLY1305, request reaches handleApiClient
    and returns the protobuf body.

* perf(eth-api): HTTP/1.1 keep-alive — one handshake per session, not per request

Before this change /fromradio used 'Connection: close' framing (HTTP/1.0
style with no Content-Length), forcing each poll to redo the full TLS
handshake. client.meshtastic.org needs ~80 sequential /fromradio polls
during initial sync (one per packet from the config-replay state
machine: MyInfo, channels, every Config_*, every ModuleConfig_*, every
NodeInfo), so the user-visible load time was ~50 s of pure handshake
overhead (80 requests * ~625 ms ECDSA P-256 each).

Three coordinated changes:

1. handleApiClient() now loops on the same connection until the peer
   closes or parseRequest hits its 3 s idle timeout. requestsServed
   counter keeps the 'bad/timeout request' debug log from firing on
   the natural idle close after a keep-alive sequence.

2. handleFromRadio() buffers all packets into a std::vector before
   writing, so it can emit a real Content-Length and 'Connection:
   keep-alive'. Buffer is dynamic — common 1-packet response only
   allocates ~256 B; ?all=true keeps the 64-packet cap which tops out
   around 16 KB. handleToRadio + sendPreflight switched to keep-alive
   too (they already had real Content-Length). sendError keeps close —
   errors are terminal.

3. ethTlsApiServer netRecv RECV_TIMEOUT_MS dropped from 10 s to 3 s so
   mbedtls_ssl_read can't outlast the handler's idle deadline (a quiet
   browser leaving the socket open would otherwise wedge the OSThread
   for 10 s past the natural close).

Measured on wiznet 192.168.1.143 against client.meshtastic.org:
  - one TLS handshake (641 ms, CHACHA20-POLY1305)
  - ~80 requests pipelined over the same session
  - full config + 40 NodeInfos in ~6-8 s (vs ~50 s before)
  - per-request latency post-handshake: ~10-25 ms
  - curl -kv with two URLs: 'Reusing existing https: connection',
    server returns 'Connection: keep-alive' explicitly.

* fix(eth-tls): watchdog + Chrome compat — handshake stability across browsers

Phase 3 keep-alive shipped a working Firefox flow but client.meshtastic.org
loops + Chrome 'Test Connection' both rebooted the board. Four distinct
issues; collectively they kept the OSThread inside serveClient() too long
or spin-looping without yielding, and pico-sdk mbedtls' TLS 1.3 code path
choked on Chrome's modern ClientHello.

1. Watchdog reset during keep-alive idle. Once a client drained the
   replay queue and entered 3 s poll mode, the OSThread sat inside
   netRecv()'s busy-wait waiting for the next request. Two consecutive
   3 s waits plus prior handler time crossed the 8 s RP2350 hardware
   watchdog. Pet the watchdog inside netRecv()'s poll loop (every 2 ms)
   so a quiet client can never starve the watchdog. Same fix in the
   ethApiHandlers per-request yield path.

2. Cap session at 64 requests + yield() between. Defense-in-depth: a
   pathological client can't monopolize serveClient indefinitely; after
   the cap it just re-handshakes (~625 ms), still vastly cheaper than
   the per-request handshake we had before keep-alive.

3. TLS 1.3 code compiled out of mbedtls entirely
   (#undef MBEDTLS_SSL_PROTO_TLS1_3 in mbedtls_user_config). Capping
   max_tls_version=TLS1_2 at runtime is enough for Firefox / openssl
   (they downgrade cleanly), but Chrome's ClientHello carries TLS 1.3
   extensions — post-quantum key shares, Encrypted ClientHello,
   etc. — that the vendored mbedtls 1.3 parser crashes on before the
   downgrade decision happens. Removing the 1.3 sources sidesteps the
   parser; ServerHello just announces TLS 1.2 and Chrome accepts.

4. netSend infinite WANT_WRITE spin. When W5500's TX buffer momentarily
   filled mid-handshake (Chrome draining slower than Firefox during
   ServerKeyExchange), EthernetClient::write() returned 0, our netSend
   returned MBEDTLS_ERR_SSL_WANT_WRITE without delay, mbedtls retried
   immediately, repeat at ~180k iter/sec until ... well, until the
   board's other threads got nothing done. Log signature: ret=-0x6880
   tight-looping in the handshake iter trace. Rewrite netSend to block
   with delay(2) + watchdog_update() and a 3 s timeout — same shape as
   netRecv. Return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY on disconnect
   (was incorrectly returning WANT_WRITE).

Also added granular per-iter handshake logging gated on first 20 iters
+ every 50th after that, so any future regression localizes itself in
COM9 without RTT JLink.

Validated on wiznet 192.168.1.143:
  - Firefox: client.meshtastic.org full sync + idle poll stable (no
    reset during the previously-crashing 'replay drain complete' phase)
  - Chrome: 'Test Connection' accepts the cert prompt and connects
  - Edge: same as Chrome
  - openssl s_client default + tls1_2 forced: both negotiate TLS 1.2
    with ECDHE-ECDSA + AES-GCM / CHACHA20-POLY1305, verify=18 (self-
    signed, expected)

* chore(eth-tls): drop verbose debug logs from cert + TLS init paths

The cert pipeline + TLS context init had step-by-step LOG_INFOs and
ubiquitous Serial.flush() that were essential while diagnosing the
Phase 2.1-bis stack overflow, the Chrome handshake crash, and the
keep-alive watchdog reset. Once those bugs were fixed the logs just
clutter COM9 on every boot.

Kept on hand:
  - cert: 'loaded from FS', 'generating ...', 'generated N B in T ms',
    'persisted to LittleFS', plus all LOG_ERROR / LOG_WARN paths
  - tls: 'server listening on TCP port 443', 'client connected from',
    'handshake OK in N ms ciphersuite=', 'handshake failed -0xXXXX (...)',
    plus all init LOG_ERRORs

Dropped:
  - cert: 'step 1/8 pk_setup' through 'step 8/8 copy key DER', 'thread
    woke', 'pipeline OK' (now silent on success)
  - tls: 'cert is ready, initializing', 'parsing cert chain', 'parsing
    key', 'ssl_config_defaults', 'conf_own_cert', 'ssl_setup',
    'server worker scheduled', and the per-iter handshake trace
  - obsolete 'Optional mbedtls debug bridge' commented-out stub
  - all Serial.flush() that were added defensively for the
    debug-the-crash phase

Bin shrinks ~40 KB (logs + format strings). Validated on wiznet
192.168.1.143: HTTP 200 round-trip works post-flash, no regression.

* fix(eth): harden API/TLS build guards (review #10573)

- ethApiHandlers.{h,cpp}: gate on (HAS_ETHERNET_API || HAS_ETHERNET_TLS_API)
  so the shared handleApiClient/IStreamReadWrite still compile for an
  HTTPS-only variant (the TLS server depends on them).
- ethTlsApiServer.{cpp,h}, ethCert.{cpp,h}: require ARCH_RP2040 (they use Pico
  SDK get_rand_64()/watchdog_update()); the ethClient.cpp TLS include + call
  site are tightened to match, so enabling the flag on a non-RP2040 Ethernet
  target is a clean no-op instead of a cryptic build break.
- clang-format (16) the touched eth files to satisfy Trunk Check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(eth): address review — listener/cert recovery + keep-alive framing

- Rebind HTTP/80 and HTTPS/443 after a W5500 chip reset. reconnectETH() now
  tears the API/TLS servers down (deInitEthApiServer/deInitEthTlsApiServer) so
  the restart path recreates them; the singleton guards previously left both
  bound to dead sockets until reboot. The workers are kept (only the listener
  is dropped) so nothing is deleted from another thread's runOnce.

- Stop the keep-alive loop after a handler error. /fromradio and /toradio
  return whether the connection may stay open; a 400/405/408 response
  (Connection: close) now ends the loop so unread body bytes can't be parsed
  as the next request.

- Validate the cached cert/key before trusting it (parse both DERs), and clear
  the IP commit-marker before rewriting the pair and write it last, so a reset
  mid-persist regenerates instead of handing the TLS server a truncated or
  mismatched pair (which would hard-disable HTTPS).

- Track a cert generation counter so the TLS server reloads and rebinds when a
  DHCP lease change regenerates the cert for a new IP (the SAN must follow, or
  browsers reject the new address). The cert worker is no longer one-shot.

- Silence the SCons F821/E402 lint on add_mbedtls_sources.py to match the other
  extra_scripts.

Validated on a W5500 board: a forced chip reset rebinds 80 and 443 without an
MCU reboot; a truncated cached cert regenerates and HTTPS recovers; a simulated
lease change reissues the cert with the new IP in the SAN.

* fix(eth): clear cert readiness on regen failure + verify cached pair

Follow-up to the review:
- Reset ready_/certIp_ when ensureCertForIp() fails, so isReady() no longer
  reports true with empty material — otherwise a later TLS reload fails
  initTlsContext() and stays disabled instead of retrying on the next poll.
- certKeyParse() now also checks the cached cert and key form a matching pair
  (mbedtls_pk_check_pair), rejecting an independently-parseable but mismatched
  cert/key (e.g. if the IP commit-marker survived a failed clear).

Validated on a W5500 board: a valid cached pair still loads from FS, and a
regenerated cert reloads cleanly.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 05:51:14 -05:00
Clive BlackledgeandBen Meadors 98fa4a67db [feature] Free a little memory used by bluetooth if wifi is enabled or bluetooth is disabled (#10398)
* esp32: release BTDM heap when Bluetooth inactive

Release ESP-IDF BTDM memory after config load when Bluetooth is disabled or WiFi is enabled, recovering heap on ESP32 targets where BLE won’t be used for this boot.

* Address BT memory release review comments

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-06-25 17:05:27 -05:00
07a87a8254 security: runtime-toggleable MESHTASTIC_LOCKDOWN hardening for nRF52 (#10349)
* security: add MESHTASTIC_LOCKDOWN hardened build option

Meshtastic nodes ship with secrets on flash (channel PSKs, the device
private key, admin keys, wifi PSK) and over-the-wire access to admin
APIs that can re-key the mesh. Lose the device, at a border crossing,
in a raid, off a backpack, and an attacker reads everything in 30s
with a USB cable. There's no at-rest encryption, no client auth, the
screen leaks contents, and SWD is wide open. This adds an opt-in
hardened build for users who care.

-DMESHTASTIC_LOCKDOWN=1 on nRF52 (CC310) turns on:

  DEBUG_MUTE                         silence USB/serial logs
  MESHTASTIC_ENCRYPTED_STORAGE       AES-128-CTR + HMAC-SHA256 on
                                     LocalConfig / channels / NodeDB.
                                     Passphrase-gated DEK, TTL/boot
                                     unlock token, failed-attempt
                                     backoff (within-boot, wall-clock,
                                     persisted bootsSinceFail).
  MESHTASTIC_PHONEAPI_ACCESS_CONTROL per-connection auth gate. Secrets
                                     emitted as empty proto structs
                                     to unauthenticated clients.
  MESHTASTIC_ENABLE_APPROTECT        one-way UICR APPROTECT, reset
                                     applied same boot. Recoverable
                                     only via \`nrfjprog --recover\`,
                                     which also wipes the DEK.
  LockdownDisplay                    screen shows "LOCKED" when locked
                                     or idle 30s. OLED only; InkHUD /
                                     niche / device-ui not yet wired.

Wire format is the LockdownAuth / LockdownStatus pair from
meshtastic/protobufs#911 (AdminMessage tag 104, FromRadio tag 18).

Access-control state is a file-scope 6-slot table in PhoneAPI.cpp
keyed by \`this\`, not class members. Adding *any* per-instance field
to PhoneAPI breaks USB-CDC enumeration on the current nRF52 Adafruit
framework, one volatile bool was enough. Out-of-line side-steps it.

lockdown_auth is handled synchronously in PhoneAPI::handleToRadioPacket
rather than routed through the mesh Router into AdminModule. Two
reasons: the passphrase never travels through a routed MeshPacket
queue, and per-connection authorization runs while \`this\` is still on
the call stack. The previous async-via-router design lost connection
identity (g_currentContext was null by the time AdminModule processed
the auth), so per-connection unlock never actually took effect on the
originating client.

Non-nRF52: #warning, only DEBUG_MUTE activates. tools/lockdown_provision.py
drives provision / unlock / lock-now / watch over USB.

Display privacy is a screen-lock latch separate from storage-lock
state: shouldRedactDisplay() is true when storage is locked OR the
latch is set. Screen::setOn(false) sets the latch when the stock idle
timeout powers the display off (reusing config.display.screen_on_secs,
no second timer); it is cleared only when a client authenticates with
the passphrase. A device idling on the mesh keeps routing but hides
its screen until re-auth; button input wakes the backlight to the
LOCKED frame, not content. The earlier lockdown-specific 30s idle
timer is removed — it duplicated PowerFSM idle detection and showed a
misleading LOCKED screen on a merely-idle device.

Unlock-token TTL fix: a token carrying both a boot-count and a
wall-clock TTL is no longer destroyed when the RTC is invalid at cold
boot. The boot count is independently verifiable without a clock, so
the token falls back to boot-count enforcement instead of being
deleted. A token is only hard-rejected when its wall-clock TTL can be
evaluated and is found expired.

NodeDB::reloadFromDisk() after unlock is deferred to the main loop via
lockdownReloadPending rather than run inline on the transport callback
stack — the reload is too heavy for the BLE/serial task stack and was
resetting the device immediately after a successful unlock.

The screen-lock latch also swallows local input events in
InputBroker::handleInputEvent while it (or storage-locked) is set.
Without that, a blind operator could drive on-device menus, fire
canned messages, or change settings through the joystick/buttons even
though the screen content was hidden. PowerFSM is still triggered
first so the backlight wakes to the LOCKED frame; the event is dropped
before reaching the UI observers.

The screen-lock latch is initialised to true at boot, so even a
token-auto-unlocked cold boot comes up redacted. Otherwise an attacker
holding a screen-locked device could power-cycle it (the RAM latch
resets) and recover a content screen. After any boot, the operator
must authenticate from a client to reveal screen content.

MyNodeInfo.device_id is also redacted for unauthenticated clients —
it is a stable hardware identifier useful to an attacker for
fingerprinting / correlating the device across observations. The
public mesh fields (my_node_num, owner short/long name, public key,
hw model) are left as-is because they are already broadcast on-mesh.

ModuleConfig.mqtt is also redacted for unauthenticated clients —
MQTTConfig carries broker username, password, server address, and
root_topic. The empty MQTTConfig is emitted via the same zero-init
pattern as the other gated sections.

Uptime-based session limit (MESHTASTIC_LOCKDOWN_SESSION_DEFAULT_SECONDS)
caps how long a single auto-unlocked session can hold storage open,
measured in firmware millis() since unlock. 0 = unlimited (existing
token-only behavior, suitable for tower/infra nodes); non-zero arms a
timer on every passphrase unlock and on every token-auto-unlock that
inherits the value, since the cap is persisted in the token (token
format bumped to v2: adds sessionMaxSeconds, body 56→60 bytes).

On expiry the device revokes per-connection auth, re-engages the
screen-lock latch, and reboots WITHOUT deleting the token. Next boot
auto-unlocks via the boot count (decrementing it) and arms a fresh
session window. Hard exposure ceiling: bootsRemaining * sessionMaxSeconds.
Explicit user Lock Now still deletes the token (passphrase required to
recover); only session expiry preserves it.

Why uptime, not wall-clock: getValidTime() is fed by GPS/RTC/client
time pushes — all manipulable by an attacker with the device (GPS
spoof to roll the clock back, pull the RTC backup cell, Faraday-cage
the whole thing). millis() comes off the Cortex-M's internal cycle
counter, sealed inside the chip; the only way to reset it is a reboot,
which costs a boot from the on-flash token counter. APPROTECT remains
the load-bearing defense against forging higher boot counts via SWD.

A future LockdownAuth.max_session_seconds proto field will let the
client set this per-token; until that lands the build-time
MESHTASTIC_LOCKDOWN_SESSION_DEFAULT_SECONDS macro is the only source.

Session expiry now decrements the on-flash boot count in place and
re-arms the uptime timer WITHOUT rebooting, while budget remains.
Mesh routing keeps running across session boundaries; the device only
reboots when bootsRemaining reaches zero (rollback budget exhausted),
at which point it hard-locks and forces passphrase re-entry.

Each session boundary still: revokes per-connection admin auth so
clients must re-authenticate to see content, re-engages the screen
lock latch, and emits LockdownStatus{LOCKED, needs_auth, boots=N}
so connected clients see the decremented count and know to re-auth.
Storage stays unlocked (DEK in RAM) for continuity.

The boot count's role as the rollback ledger is unchanged — it
decrements monotonically once per session boundary, whether the
session ends in a reboot or an in-place roll. Attacker who power-
cycles to dodge the session timer still pays a boot via the existing
readAndConsumeToken decrement-at-load path. APPROTECT remains the
only defense against forging higher counts.

Net effect for an unattended/tower node with bootsRemaining=50,
sessionSeconds=3600: 50 hours of continuous mesh service, one
reboot at the end, vs. the previous design's 50 reboots over the
same period. Same exposure ceiling, far better uptime.

LockdownAuth.max_session_seconds (proto tag 5) is now consumed: when
non-zero the client value wins; 0 falls back to the firmware-side
MESHTASTIC_LOCKDOWN_SESSION_DEFAULT_SECONDS, matching the boots_remaining
sentinel convention. Protobufs submodule pin bumped to develop tip
which contains meshtastic/protobufs#916 (merged).

* security: drop dead is_managed allowlist for set_config(security).private_key

The 'isLockdownSecurityCmd' allowlist in handleReceivedProtobuf dates
from the pre-LockdownAuth design when the passphrase was smuggled
through SecurityConfig.private_key. With lockdown_auth handled
synchronously in PhoneAPI::handleToRadioPacket before any admin message
reaches the Router, this allowlist now serves no legitimate purpose
and lets an unauthenticated local client mutate security settings on
a managed device by setting private_key.size>=1 — including
potentially disabling is_managed itself.

Remove the allowlist. Managed-mode local admin now requires a
PhoneAPI connection that has already authenticated via lockdown_auth
(or, on the pki_encrypted branch below, a valid PKC admin key).

Resolves Copilot review feedback on src/modules/AdminModule.cpp:105.

* security: protect lockdown-status drain slot from concurrent writers

g_pendingLockdownStatus / g_hasPendingLockdownStatus are written from
multiple call sites (PhoneAPI::handleLockdownAuthInline on the BLE/USB
transport callback, AdminModule on the Router thread, main loop session
expiry) and read in getFromRadio() on whichever transport is draining
FromRadio. The struct read/write was unprotected, so a writer could
corrupt the slot mid-encode. Same pattern as nodeInfoMutex — wrap
both the queue path and the drain in a small lock. Drain re-checks
the bool under the lock to handle the case where another reader
grabbed the slot first.

Resolves Copilot review feedback on src/mesh/PhoneAPI.cpp:1560.

* security: derive readAndDecrypt size cap from caller buffer, not a hardcoded 64 KB

The MAX_PROTO_FILE_SIZE = 65536 + OVERHEAD ceiling was an absolute
constant chosen against a since-outdated assumption that 'meshtastic
proto files are well under 64 KB'. On variants where MAX_NUM_NODES
pushes the serialised NodeDatabase past 64 KB the legitimate file gets
rejected at load and the device treats its own real config as corrupt.

The caller already knows the maximum plaintext it expects (outBufSize).
Cap the ciphertext at outBufSize + OVERHEAD instead — this is the tightest
sound bound (anything larger could not possibly decode into the caller's
buffer), still defends against OOM / integer overflow, and scales with
the platform's actual NodeDB size rather than an arbitrary constant.

Resolves Copilot review feedback on src/security/EncryptedStorage.cpp:1327.

* docs: fix stale 'passphrase delivery via AdminModule' references in configuration.h

The lockdown overview comment block was written when passphrase delivery
ran through AdminModule's handleReceivedProtobuf. With the synchronous
refactor that path now lives in PhoneAPI::handleLockdownAuthInline,
called before the admin message reaches the Router. Update both the
nRF52 feature list and the non-nRF52 degraded-mode rationale to point
at the current code path.

Resolves Copilot review feedback on src/configuration.h:578 (and :604).

* docs: refresh unlock-token format doc to match v2 layout

The header comment for the UTOK file still described v1 (version 0x01,
no session_max_seconds, 71 bytes) even after the in-flight bump to
TOKEN_VERSION=0x02 and TOKEN_TOTAL_SIZE=75. The inline body-size
breakdown comment was also wrong (claimed 39 bytes and mismatched the
real NONCE_SIZE/AES_KEY_SIZE constants). Rewrite both to match the
actual on-flash layout and note how v1 tokens are handled on upgrade
(rejected via the version byte; passphrase re-entry mints a v2).

Resolves Copilot review feedback on src/security/EncryptedStorage.h:50.

* docs: correct session-limit comment re: token-auto-unlock behavior

The s_sessionMaxMs comment block claimed 'token-auto-unlocked
sessions have no session timer (the session feature is a
passphrase-unlock-only knob)'. Stale: readAndConsumeToken() now
persists sessionMaxSeconds in the token file and re-calls
setSession() from the token-load path, so token-auto-unlocked
sessions DO inherit the same cap (and consumeSessionBoot() re-arms
in place between sessions on a single boot). Update the comment to
match.

Resolves Copilot review feedback on src/security/EncryptedStorage.cpp:72.

* docs: clarify input-swallow gate re: screen-lock latch vs storage state

The previous comment said input is swallowed 'until a client authenticates
and unlockScreen() clears the latch (or storage is unlocked)'. The
parenthetical was misleading: storage being unlocked is not in itself
enough to clear the latch — the latch persists across the
storage-unlocked-but-screen-locked steady state, and only an explicit
unlockScreen() (called from a successful passphrase auth path) clears
it. Reword so the only-passphrase-clears-the-latch invariant is
explicit and local input is named as something that does NOT clear it.

Resolves Copilot review feedback on src/input/InputBroker.cpp:134.

* docs: fix reloadFromDisk() trigger comment in NodeDB.h

The header still claimed reloadFromDisk() is called by AdminModule
after a successful passphrase op. With the synchronous PhoneAPI
refactor the actual trigger is PhoneAPI::handleLockdownAuthInline
setting lockdownReloadPending, with main.cpp's loop() dispatching
the heavy reload on the main thread (the transport callback stack
isn't large enough). Update the comment to point at the real path
and explain why the deferral exists.

Resolves Copilot review feedback on src/mesh/NodeDB.h:393.

* style: clang-format lockdown sources

Apply trunk clang-format (16.0.3) to satisfy the format check.

* style: black-format lockdown_provision.py

Satisfy the trunk black formatter check.

* security: drop unused v1 EncryptedStorage formats and migration

This storage layer has never shipped, so there are no v1 DEK files,
v1 unlock tokens, or v1 backoff records anywhere to stay compatible
with. Remove the dead compatibility machinery:

- legacy init() (FICR-only KEK, no passphrase) — had no callers
- deriveKEKv1() / loadDEKv1() and the v1->v2 DEK migration paths in
  provisionPassphrase() and unlockWithPassphrase()
- the 5-byte v1 backoff file format

Also drop the now-pointless version byte from the on-disk MENC, MDEK,
and UTOK formats. Each is identified by its 4-byte magic (and, for the
keyed formats, its HMAC); with only one version that will ever exist,
the version field added nothing. Sizes shrink by one byte each
(overhead 54->53, DEK 66->65, token 75->74).

Rename the surviving helpers to drop the _v2 suffix (deriveKEK,
loadDEK, saveDEK, KEK_DOMAIN). No behavioral change for provisioning,
unlock, token consumption, or session handling.

Verified with an nRF52 lockdown build (rak4631).

* fix(lockdown): harden auth-table and lockdown_auth handler (audit)

Audit findings addressed:

C3 — `~PhoneAPI()` now clears its auth slot unconditionally. The previous
slot-clear in `close()` was gated on `state != STATE_SEND_NOTHING`, so a
PhoneAPI that never reached config (or that already closed) left
`slot.who` pointing at freed memory; a future PhoneAPI heap-allocated at
the same address would inherit the prior session's authorization through
`findOrAllocSlot`.

C4 — All access to `g_authSlots`, `g_authEpoch`, and `g_currentContext` is
now serialised through `g_authSlotsMutex`. Previously these were touched
without locking from BLE/USB/TCP/Router tasks, so two parallel slot scans
could hand out the same slot and mid-update reads could observe
authorized=true alongside a stale epoch. Granularity is fine — every
critical section is a short linear scan over six entries, and getFromRadio
(which calls `getAdminAuthorized()` per redaction check) tolerates the
brief blocking.

A4 / H1 — `lock_now` now requires the originating connection to be
already authorized. Previously any unauthenticated client (BLE/USB/TCP)
could submit `lockdown_auth { lock_now=true }` and force a reboot,
which was a trivial local-presence DoS — an attacker near the radio
could brick-loop it indefinitely. The original "panic button without
auth" property is dropped; panic now requires the operator to have
passphrase-unlocked the connection.

H2 — Empty-passphrase `lockdown_auth` (with `lock_now=false`) used to
silently return success. The client received no feedback distinguishing
that case from a real success, and an attacker could probe lockdown
state for free. Now emits UNLOCK_FAILED with no backoff increment
(empty-passphrase is more likely a client bug than an attack, but the
honest signal still lets the client correct itself).

H14 — `la.boots_remaining > 255` previously truncated silently
(256 → 0 → mapped to TOKEN_DEFAULT_BOOTS=50; 257 → 1). Honest clients
could not detect the misbehavior. Now rejected explicitly with
UNLOCK_FAILED.

L1 — The `to == nodeDB->getNodeNum()` allowance in the unauth ToRadio
gate now also requires `getNodeNum() != 0`. During the locked-default
boot path `getNodeNum()` returns 0, so a packet with `to=0` could
otherwise satisfy the equality and bypass the gate.

L2 — Comment added on `g_authEpoch` wrap. Practically unreachable
(2^32 lockNow events on one boot), but worth recording the behavior.

M17 — `findOrAllocSlot_LH` now evicts the first unauthorized stale slot
when the table is full of non-nullptr entries, rather than failing
closed. Authorized slots are never evicted — they represent live
operator sessions. Fail-closed (with LOG_WARN) only when every slot
holds a different live authorized PhoneAPI, which would require seven
simultaneous authed connections.

M18 — `s_screenLocked` is now `std::atomic<bool>` with relaxed ordering.
Plain bool happened to work on single-core Cortex-M4 today but breaks
silently if lockdown ports to ESP32 / RP2040, or under LTO whole-
program elision.

Verified with an nRF52 lockdown build (rak4631).

* fix(lockdown): gate every admin op on per-connection auth + storage unlock

Audit findings addressed:

H6 — Unauthenticated local clients could previously set_config / set_module_config /
set_channel etc. on a lockdown device whenever is_managed was unset.
The previous gate inside AdminModule's is_managed branch consulted
PhoneAPI::isLocalAdminAuthorized(), which reads a global g_currentContext
set during synchronous PhoneAPI dispatch — but AdminModule runs on the
Router task, by which time the dispatch task has exited and the global is
unrelated to the originating connection. The check was both broken (always
false on Router, so even authed clients were rejected) and unsafe (when it
did fire, the wrong connection could be authorized).

The fix relocates the gate to PhoneAPI::handleToRadioPacket, where dispatch
is synchronous and getAdminAuthorized() can be trusted. The admin payload
is already decoded there to extract lockdown_auth; extend the same branch
so that any non-lockdown_auth admin variant from an unauthorized connection
is dropped before ever reaching the Router queue.

H7 — Same root cause: get_config_request / get_module_config_request /
get_channel_request handlers returned full security/network/mqtt content
to unauthorized local clients. With the H6 gate in PhoneAPI, these
requests never reach AdminModule, so handleGetConfig / handleGetModuleConfig
/ handleGetChannel are only callable from authorized connections.

H9 — Remote admin (PKC-authorized peers, mesh-relayed admin) bypassed
lockdown entirely. If admin_keys were baked in via USERPREFS or set on a
prior unlocked boot, a remote attacker could drive factory_reset /
set_config against a locked device before the operator ever unlocked it.
Added an EncryptedStorage::isUnlocked() early-return at the top of
AdminModule::handleReceivedProtobuf. The local lockdown_auth path is
unaffected because PhoneAPI handles it synchronously before AdminModule
runs.

H10 — Removed g_currentContext, the ContextGuard, authorizeLocalAdmin(),
and isLocalAdminAuthorized() entirely. The audit's race (Router-thread
reads a pointer set by an unrelated parallel dispatch and authorizes the
wrong PhoneAPI) and the always-false-on-Router behavior both disappear
with the code that produced them. The PKC-admin auto-authorize path is
gone — PKC admin and the per-connection lockdown auth are now
independent: clients using PKC admin from a local app must also send
lockdown_auth to unlock the redacted FromRadio stream.

Cleaned up AdminModule's is_managed branch: under lockdown the
PhoneAPI-layer gate has already done its job, so no additional check
is needed; without lockdown the legacy is_managed-blocks-plain-admin
semantics are preserved.

Verified with an nRF52 lockdown build (rak4631).

* fix(lockdown): hold radio silent until storage is unlocked

Audit finding H8: while locked, the device beaconed nodeinfo and
telemetry on the public LongFast default PSK and routed incoming default-
channel packets through the locked router. The locked-default boot path
in NodeDB::loadFromDisk installs config via installDefaultConfig, which
honours USERPREFS_CONFIG_LORA_REGION (the common shape for managed
deployments) and synthesises the default LongFast channel. So a locked
device on managed firmware came up TX-enabled on a well-known PSK
before any operator interaction.

Force config.lora.region = UNSET in the locked-boot block.
RadioLibInterface gates both TX (startSend) and RX (readData) on
region != UNSET — locked devices no longer initialise the SX12xx for
either direction. Also set tx_enabled = false for any code path that
checks the flag directly without consulting region.

reloadFromDisk() restores the persisted lora config once the operator
unlocks. Note: until the audit's M8 (radio re-init after reload, the
upcoming commit 5 in this remediation series) lands, an unlocked
device may need to reboot before its radio fully comes up under the
real config; this is no worse than the pre-fix state, where the radio
was already running on the wrong (default) config and any real config
change required an explicit reconfigure or reboot anyway.

Verified with an nRF52 lockdown build (rak4631).

* fix(lockdown): per-connection status queue, redaction expansion, log/banner mute (audit)

M14 — Replaces the single file-scope LockdownStatus slot with a per-
PhoneAPI table keyed by PhoneAPI*, parallel to the auth-slot table and
sharing g_authSlotsMutex. Previously a status produced for connection
A (UNLOCKED with the active TTL, or UNLOCK_FAILED with a backoff)
could be drained by connection B before A read it, leaking A's auth
state to B. queueLockdownStatus is now a per-instance method writing
to this->slot. A new static broadcastLockdownStatus exists for the
main-loop session-expiry callers that have no PhoneAPI* in hand —
those want every connected client to learn about the session roll,
which is the only legitimate broadcast use case. hasPendingLockdownStatus
is a const helper for the FromRadio available()/drain check.

M13 — buildStatus_LH (the single point where lock_reason crosses into
the on-wire LockdownStatus) collapses any token_* reason to a generic
"locked" before emission. The specific reasons (token_hmac_fail,
token_wrong_size, token_bad_magic, token_boots_zero, token_expired,
token_dek_fail, token_missing) still go to local logs, but no longer
tell an unauthenticated client that the firmware noticed their
tampering / rollback / corrupt-file attempt.

M15 — Extended the STATE_SEND_MY_INFO redaction (previously device_id
only) to also wipe pio_env and min_app_version for unauth clients —
both are pure build-fingerprint vectors that tell an attacker which
known issues to probe. Kept my_node_num (broadcast on the mesh anyway)
and nodedb_count (clients need it post-unlock to decide whether to
pull the node DB). Added equivalent redaction for STATE_SEND_METADATA:
the whole DeviceMetadata struct is wiped for unauth clients
(firmware_version, device_state_version, hw_model, hw_model_string,
has_bluetooth/has_wifi/has_ethernet, role, position_flags,
excluded_modules). Clients re-fetch after authenticating.

M16 — LoRa config is now whitelisted for unauth clients to the set
that is intrinsically observable on the air anyway: region,
modem_preset, use_preset, channel_num, hop_limit. Operator-private
knobs (ignore_incoming, override_duty_cycle, override_frequency,
sx126x_rx_boosted_gain, tx_power, ignore_mqtt, fem_lna_mode,
config_ok_to_mqtt) are zeroed. The whitelist is built as a fresh
LoRaConfig stack copy rather than masked in place to avoid touching
the persisted struct.

M12 — Skip the DEBUG_MUTE "we are muted, FYI" banner under
MESHTASTIC_LOCKDOWN. The banner spilled APP_VERSION / APP_ENV /
APP_REPO over USB CDC even with all other logging suppressed, which
defeats the muting in lockdown builds and gives a USB-attached
attacker a free firmware-fingerprint primitive.

L9 — Removed the numeric backoff value from the LOG_WARN unlock-
failed message. The client receives backoff_seconds via the
UNLOCK_FAILED status; printing it again to USB serial under
non-DEBUG_MUTE builds (i.e. MESHTASTIC_LOCKDOWN_DEBUG dev builds)
was the only place it appeared in logs.

Verified with an nRF52 lockdown build (rak4631).

* fix(lockdown): atomic post-unlock reload with corruption surface (audit)

Closes M6, M7, M8, M9 from the lockdown security audit.

M6 — handleLockdownAuthInline no longer flips the connection to
authorized or emits UNLOCKED on the cold-unlock path (the first
successful passphrase verify after a locked boot). The client keeps
seeing LOCKED until reloadFromDisk has actually populated config /
channelFile / nodeDatabase with the operator's real values. Without
this, the window between the auth call and the main-loop reload
exposed two race-friendly bugs: (a) the client could read the
locked-default placeholders as if they were the real config, and (b)
a set_config in the window would silently overwrite a corrupted
baseline once the reload swapped values in.

A new per-status-slot bool pendingUnlockAfterReload records that the
connection is mid-unlock. The re-verify path (storage already
unlocked) is unchanged and authorizes immediately — there is nothing
to reload.

M7 — reloadFromDisk now holds a new file-scope mutex
(g_reloadFromDiskMutex) against itself, parks the radio in sleep
mode before swapping config / channelFile, and reconfigures the
radio with the now-real settings after. Other readers of config.lora
/ channelFile / nodeDatabase do not take this lock today; closing
those races is a wider locking-discipline change outside the audit's
M7 scope. The radio standby+reconfigure prevents the SX12xx from
sitting in a half-old/half-new register set across the swap, which
otherwise required a reboot to recover from.

M8 — RadioInterface::reconfigure() is now called at the end of a
successful reload, so the SX12xx register set actually reflects
the unlocked operator settings (region, modem preset, channels)
rather than staying on the locked-default placeholder. Routed through
a new Router::getRadioIface() accessor — the radio interface is
owned by Router as a unique_ptr and was not exposed.

M9 — NodeDB::loadProto now sets a NodeDB::storageCorruptThisLoad
flag whenever an encrypted file fails to decrypt or proto-decode.
reloadFromDisk consumes the flag and returns false on any failure
instead of silently falling back to defaults. main.cpp's reload
service then calls EncryptedStorage::lockNow() and
PhoneAPI::revokeAllAuth(), and the new
PhoneAPI::completePendingUnlocks(false) emits LOCKED(storage_corrupt)
to every pending connection — they stay unauthorized so any
set_config they send is dropped at the existing unauth gates.
The lock_reason string passes through buildStatus_LH's M13
redaction unchanged because it does not start with token_.

The success path goes through PhoneAPI::completePendingUnlocks(true)
which authorizes each pending connection, emits UNLOCKED with the
current TTL, and clears the screen-lock latch once. Snapshots the
target PhoneAPI* list outside the auth-table lock to avoid re-entry
when setAdminAuthorized takes the same lock.

Verified with an nRF52 lockdown build (rak4631).

* fix(lockdown): UI/pairing fixes for first-pair + content-flash + e-ink (audit)

Closes H13, M19, M20, L4 from the lockdown audit. (L3 dropped per
explicit decision — battery level is not a meaningful security side
channel.)

H13 — BLE pairing PIN was suppressed by the lockdown lock screen on
locked devices. Screen.cpp updateUiFrame's lockdown short-circuit
intercepts before ui->update() runs, so the pairing-PIN overlay
banner that NRF52Bluetooth::onPairingPasskey queued never painted.
Net effect: a freshly-locked device on first BLE pair could not be
unlocked over BLE because the operator could never see the PIN —
chicken and egg.

Adds a new notificationTypeEnum::pairing_pin value and special-cases
it in the short-circuit: paint the LOCKED frame first (so the
underlying background remains the redacted view, never dashboard
content) then let ui->update() composite the PIN banner overlay on
top. The PIN itself is an ephemeral pair-handshake artifact
(regenerated per attempt, dies on banner timeout) and is not
operator content, so this does not regress the redaction guarantee.

NRF52Bluetooth::onPairingPasskey switches from showSimpleBanner to
showOverlayBanner with notificationType = pairing_pin so the
short-circuit's lookup matches.

M19 — Brief content-visible window on Screen::handleSetOn(true)
wake. OLED GDDRAM physically retains the last-rendered frame while
the panel is powered off; the next ui->update() after displayOn() is
async, so an observer (or shoulder-surfer) could see the previous
frame's content for 16-50 ms on every wake. Under MESHTASTIC_LOCKDOWN
we now paint the LOCKED frame into GDDRAM in handleSetOn(false)
before calling displayOff(). On wake the only thing the panel can
flash is the redacted view. Gated on lockdown only — non-lockdown
builds keep the previous frame as a UX cue.

M20 — E-ink panels physically retain the last-rendered image
without power. A power-cycled lockdown handheld kept showing
operator-identifying content (position, messages, nodeinfo) until
the firmware's first natural refresh — which on e-ink can be
seconds into boot. Now, under MESHTASTIC_LOCKDOWN && USE_EINK, the
panel init path in Screen::setup() paints the LOCKED frame and
forces a full refresh (forceDisplay) immediately after ui->init()
and before any other rendering. Persistent pixels are wiped to the
redacted view before an observer can see them. Build-tested on
seeed_wio_tracker_L1_eink; hardware-verified visual confirmation
is pending a T-Echo session.

L4 — Screen::blink() bypasses the normal ui->update() path that
the lockdown short-circuit gates. It draws arbitrary geometry, not
node data, so it does not actually leak today; but any future
change that puts content into blink would silently leak past
redaction. Added an early-return on shouldRedactDisplay() to make
the function honor the redaction contract.

Verified with nRF52 lockdown builds on both rak4631 (OLED) and
seeed_wio_tracker_L1_eink (e-ink).

* fix(lockdown): refuse APPROTECT on vulnerable silicon, gate on provision (audit)

Closes M22 and M23 from the lockdown audit.

M22 — APPROTECT lockout on nRF52840 is publicly known to be bypassable
on every silicon revision shipping in current Meshtastic hardware
(AAB0..AAF0) via SWD glitching, per LimitedResults' published research
on the nRF52 series. Engaging APPROTECT on these revisions has two
bad properties: (1) the lockout is irreversible without a destructive
nrfjprog --recover, and (2) it gives the operator a false sense of
security because the lockout itself can be defeated by anyone with
ten minutes and a glitcher.

enableAPProtect() now reads FICR.INFO.VARIANT (encoded as a 4-byte
ASCII word) and refuses to engage on any known-vulnerable revision,
logging the variant so the operator knows their device's specific
build code. To override (e.g. for end-to-end testing of the engage
path on hardware that's known affected), rebuild with
-DMESHTASTIC_APPROTECT_OVERRIDE_VULNERABLE_SILICON=1.

The vulnerable list is explicit and easy to update: any future
revision shown to be fixed can be removed from the list and APPROTECT
will engage on it as before.

M23 — APPROTECT engagement moved from very early in setup() to
after fsInit() + EncryptedStorage::initLocked(), and gated on
EncryptedStorage::isProvisioned(). A misconfigured CI build of a
lockdown variant flashed to a dev board would otherwise burn SWD on
first boot before the operator had set any passphrase, taking the
board out of the development/recovery workflow with zero real
security benefit (there is no DEK to protect on an unprovisioned
device). Engagement now follows operator intent: SWD locks only
once they've committed to lockdown via passphrase provisioning.

The SWD-attachable window between boot and APPROTECT engagement
widens slightly from this reorder (now ~hundreds of ms while fsInit
runs) but APPROTECT remains effective on the only payload it could
protect (the in-RAM DEK loaded by initLocked which now runs *after*
APPROTECT for already-provisioned devices).

Verified with an nRF52 lockdown build (rak4631).

* tools: harden lockdown_provision.py (audit)

Closes M26-M30 and addresses L7.

M26 — passphrase input. --passphrase on argv now requires
--insecure-passphrase-on-cmdline as an explicit acknowledgement;
without it the tool refuses and points at --passphrase-file or the
interactive prompt. --passphrase-file refuses to read anything that
isn't mode 0600 (so a passphrase another user can read off the
filesystem doesn't silently succeed). With neither, the tool reads
the passphrase via getpass.getpass — and on 'provision' double-prompts
with a confirm.

M27 — provision now requires an explicit 'yes' confirmation unless
--yes is passed, after printing the warning that the passphrase
cannot be recovered. The double-passphrase prompt is built into
gather_passphrase(confirm=True). Reduces the chance of a typo
binding a device to an unrecoverable passphrase.

M28 — 'lock' subcommand gains a 'lock-now' alias, matching how the
audit and wire docs refer to it everywhere. Both forms now require
'yes' confirmation unless --yes is set, so an accidental command
doesn't immediately reboot the device into a locked state.

M29 — the 4-second sleep is gone. Replaced with a StatusFuture
single-shot that the FromRadio interceptor signals when the next
LockdownStatus arrives. provision/unlock/lock wait up to --wait
seconds (default 8) for the actual reply and exit non-zero with the
device's reason on UNLOCK_FAILED, surfacing backoff_seconds in the
error line. Exit codes are now meaningful:
  0 = UNLOCKED
  1 = no status / unexpected
  2 = NEEDS_PROVISION (or a precondition fault: missing pkg, bad args)
  3 = LOCKED (ambiguous: device reported locked rather than the
              expected unlocked result)
  4 = UNLOCK_FAILED
This lets ops scripts decide what to do without parsing stdout.

M30 — top-of-file docstring gained an explicit SECURITY MODEL block
that names the threat model (USB-only, passphrase cleartext on the
cable) and forbids extension to TCP/BLE/UDP without a redesign. A
runtime banner reprints the headline on every invocation. --port
values starting with tcp:/tcp://, ble:/ble://, udp:/udp://, ws:/wss:
are rejected at argument parse before any connection attempt; a
copy-paste of an example into a context with a different --port
cannot silently leak credentials to the wire.

L7 — private meshtastic APIs (_handleFromRadio, _sendToRadio,
_generatePacketId) are still in use because the lib does not yet
dispatch LockdownStatus on a public pubsub topic and there is no
public seam for raw ToRadio. Their use is now wrapped in
getattr-with-clear-error so a future lib version that removes them
produces an actionable error instead of an obscure traceback. The
top-of-file note explains why we're on the private surface.

Verified end-to-end on hardware (R1-Neo + Seeed Wio Tracker L1)
during the audit-remediation hardware test pass:
  - provision (interactive, with confirm and double-prompt)
  - unlock (success returns UNLOCKED + boots TTL)
  - watch (passive listener emits LockdownStatus events)
  - lock-now (with --yes)

* fix(lockdown): H13 — render pairing PIN steady over LOCKED frame

Two bugs in the H13 fix from commit 614b7f001:

1. NotificationRenderer::drawBannercallback's switch had no case for
   the new notificationTypeEnum::pairing_pin. The function fell through
   to no-op so the banner never rendered. Added pairing_pin alongside
   text_banner so it dispatches to drawAlertBannerOverlay (same
   rendering, distinct type so the lockdown short-circuit in Screen.cpp
   can recognise it).

2. updateUiFrame's lockdown short-circuit called ui->update() to
   composite the banner. That redraws the current carousel frame
   (the dashboard) into the host framebuffer BEFORE the overlay
   paints, so the panel flashed dashboard content under the banner
   on every cycle. Replaced with a direct call to drawBannercallback
   so only the banner box is painted on top of the LOCKED pixels.

Also: drawLockdownLockScreen used to commit to the panel
(display->display()) at its end. With the banner overlay then
painting and committing a second time, the panel visibly flickered
between 'just LOCKED' and 'LOCKED + banner' on every render cycle.
Split into drawLockdownLockScreenIntoBuffer (no commit) for the
lockdown short-circuit, and a thin drawLockdownLockScreen wrapper
that calls Buffer + display() for the other call sites that don't
composite anything on top. The short-circuit now commits exactly
once per frame after both LOCKED + any overlay are in the buffer.

Verified end-to-end on hardware (Seeed Wio Tracker L1, OLED):
fresh BLE pair against a locked device now shows the pairing PIN
steadily on top of the LOCKED frame, no flicker, no dashboard
leak, and pair completes normally.

* fix(lockdown): backoff MAC + atomic writes + fault wipe + size cap (audit)

Closes H3, H4, H12, M10, M11, M25 from the lockdown audit. Non-format-
breaking: existing devices keep their .dek and .unlock_token but their
old plaintext .backoff file (6 bytes, no MAC) is silently rejected as
tampered on first read and reseeded with the MAC'd 38-byte format on
the next failed-attempt OR successful unlock.

H3 — Pre-increment the failed-attempt counter BEFORE running the HMAC
verify in unlockWithPassphrase. The previous order wrote the counter
only after a failed verify, so an attacker glitching the chip between
verify and write could skip the increment and bypass backoff. The
slot is now reserved atomically up front; the success path writes
attempts=0 to clear the reservation. Worst case for a legitimate user
who power-cycles mid-success is one phantom attempt — backoff
recovers next try.

H4 — .backoff file is now MAC'd with HMAC-SHA256(ephemeralKEK,
"backoff-auth" || body) (32-byte tag), and written atomically via
SafeFile (tmp + readback verify + rename). readBackoff treats
missing / wrong-size / MAC-fail uniformly as max-attempts (255) so an
attacker who deletes or rewrites the file can only INCREASE the wait,
never decrease it. clearBackoff() now writes an attempts=0 sentinel
instead of removing the file, so 'missing == tamper' is unambiguous
post-provision. bumpBootsSinceFailOnBoot() skips on un-provisioned
devices to avoid false 'tamper' detection during the legitimate fresh
window between fsInit and provisionPassphrase.

H12 — saveDEK and writeUnlockToken now write via SafeFile in
fullAtomic mode (tmp file + readback verify + atomic rename) instead
of remove-then-open-then-write. Power loss during a DEK or token
write previously left the device unable to unlock — the encrypted
prefs files are unreadable without a valid DEK. The atomic path
rolls back to the previous file on partial write.

M10 — readAndConsumeToken's 74-byte stack buffer (entire wrapped
DEK + HMAC, explicitly called out by the audit as never wiped before
return) is now a meshtastic_security::ZeroizingBuffer that the
destructor scrubs on every return path. Same treatment for the
computedHmac stack array next to it, and for the new backoff state
buffers in readBackoff / writeBackoff / computeBackoffHmac. Removes
the manual secure_zero calls those buffers had on success paths and
fixes the missing wipes on the failure-return paths.

M11 — Added EncryptedStorage::secureWipeKeys() public API that
zeros dek/kek/ephemeralKek in BSS without touching flash, no
logging, no locks (safe from interrupt context). HardFault_Impl now
calls it as the very first thing on entry, before the diagnostic
print / coredump path runs, so a hard-fault crash dump won't capture
the DEK / KEK material that the rest of the module leaves in RAM.

M25 — migrateFile now refuses to allocate a buffer for any file
larger than 64 KiB. The legitimate ceiling is well under that on
every supported variant; anything larger is either corrupt or a
DFU-injected OOM attempt.

Verified with an nRF52 lockdown build (rak4631).

* fix(lockdown): MENC header MAC + token rollback counter (audit)

Closes M2 and M4 from the lockdown audit. **FORMAT-BREAKING** — devices
provisioned with prior lockdown firmware must factory-erase /prefs and
reprovision; the previous tokens and encrypted prefs files will not
decrypt under the new HMAC/body layouts.

M2 — The HMAC on MENC encrypted proto files now covers the full on-disk
header (4-byte magic + 13-byte nonce + 4-byte plaintext_len + ciphertext)
instead of just (nonce + ciphertext). Without this, magic and
plaintext_len were integrity-protected only by the equality check
`plaintextLen == ciphertextLen` — which holds today (no padding /
compression / AAD) but would silently produce length-oracle and
downgrade vulnerabilities the instant any of those got added. Putting
the header inside the MAC closes that pre-condition cleanly. The
verify side in readAndDecrypt and the compose side in encryptAndWrite
update in lockstep.

M4 — UTOK gains a 4-byte monotonic counter field inside its MAC'd
body. The highest counter ever issued is persisted to a new
/prefs/.tokmono file MAC'd with HMAC-SHA256(ephemeralKEK,
"tokmono-auth" || counter). On every readAndConsumeToken, any
token whose counter is less than the persisted value is rejected as
a rollback attempt and deleted. Defeats the audit's threat: an
attacker who once captured a token (e.g. bootsRemaining=255 from
before the operator lowered the policy) tries to write it back to
disk later. Counter is incremented monotonically across the device's
lifetime so any captured snapshot loses to the persisted max-seen.

Self-heal: a token whose counter exceeds the persisted value (e.g.
the .tokmono write itself failed after the token committed, or the
.tokmono got wiped via factory-erase) is accepted AND the counter
file is promoted to match. This avoids spuriously rejecting valid
tokens after partial-update recovery.

Threat model caveat (consistent with C2 acceptance): an attacker who
has both flash extraction AND FICR can recompute the .tokmono MAC
and restore a matching pair (.unlock_token + .tokmono) from an
earlier capture. M4 raises the bar to that combined capability;
the flash-write-only attacker is now blocked.

Verified with an nRF52 lockdown build (rak4631).

MIGRATION: devices already provisioned with the prior lockdown
firmware will fail to auto-unlock at boot (token format mismatch),
fall back to LOCKED(needs_auth), and every passphrase attempt will
fail because the encrypted /prefs files are HMAC'd against the old
input. Recovery is: factory-erase via the bootloader UF2 then
re-provision via lockdown_provision.py or the Android app.

* feat(lockdown): make lockdown a runtime client-toggleable setting

Converts MESHTASTIC_LOCKDOWN from a per-variant compile-time flag that
forced lockdown ON into an internal capability that is ALWAYS compiled
in for nRF52 and gated purely at runtime by whether a passphrase has
been provisioned. A device that has never been provisioned (or that the
operator disabled) behaves exactly like stock firmware.

Build/config:
- configuration.h auto-defines MESHTASTIC_LOCKDOWN (+ ACCESS_CONTROL,
  ENCRYPTED_STORAGE, APPROTECT-capable) for ARCH_NRF52 unconditionally.
  No variant sets -DMESHTASTIC_LOCKDOWN anymore. Flash-constrained
  variants can opt out with -DMESHTASTIC_EXCLUDE_LOCKDOWN=1. DEBUG_MUTE
  is no longer coupled to lockdown (a capable-but-off device must log
  normally). rak4631 lands at 96.2% flash with lockdown always-in.

Runtime predicate:
- EncryptedStorage::isLockdownActive() == isProvisioned() (.dek exists)
  is the single source of truth for active/inactive.
- PhoneAPI::getAdminAuthorized() returns true when lockdown is inactive,
  so every existing redaction gate no-ops on a capable-but-off device
  with no per-site changes. The locked-boot defaults path (NodeDB), the
  AdminModule storage-locked gate, the screen-redaction predicate, and
  the plaintext->encrypted migrate block are all additionally gated on
  isLockdownActive() so an un-provisioned device loads/serves plaintext
  normally.
- sendConfigComplete emits LockdownStatus{DISABLED} when capable-but-off
  so the client renders its toggle OFF.

Enable (off->on): client provisions a passphrase. provisionPassphrase
generates the DEK; the existing reload path encrypts the plaintext
config in place (migration runs live with the DEK in RAM) and authorizes
the connection -> UNLOCKED. No reboot.

Disable (on->off): LockdownAuth{passphrase, disable=true}. PhoneAPI
verifies the passphrase (loads DEK), sets lockdownDisablePending; the
main loop runs NodeDB::disableLockdownToPlaintext() which decrypts every
pref via EncryptedStorage::migrateFileToPlaintext() then
removeLockdownArtifacts() deletes the DEK/token/counter/backoff (the
.dek delete is the atomic commit), then reboots into normal mode.
Power-loss safe and re-runnable without a persistent marker — and the
crypto runs live with the operator's passphrase in RAM rather than via
a boot-time marker an attacker could plant to trigger an unprompted
decrypt. APPROTECT is NOT reversed (sticky; permanent on silicon where
it engaged).

Generated bindings (admin.pb.h / mesh.pb.h) regenerated against
protobufs#927 (LockdownAuth.disable, LockdownStatus.State.DISABLED).
Submodule pointer stays at the pinned develop commit; the bindings are
ahead until #927 merges and the submodule is bumped, same flow as the
max_session_seconds work.

Builds clean: rak4631 with no flags now auto-includes lockdown.

NOTE: this changes the LockdownStatus the firmware emits and adds the
disable path; pairs with protobufs#927 and the upcoming Android client
toggle work.

* fix(lockdown): re-lock per-connection auth on BLE reconnect

A provisioned device reused a single BLE PhoneAPI instance, and the
per-connection auth slot (keyed by that instance) was only cleared on the
!isConnected() disconnect transition. A fast disconnect/reconnect could
begin a new config burst while state was still STATE_SEND_PACKETS, so the
reconnected client inherited the prior session's authorization: it received
SecurityConfig in the clear and no LockdownStatus, and never re-authenticated.

Reset the auth slot in NRF52Bluetooth onConnect(), which fires once per
physical link, so every new connection starts locked regardless of whether
the previous link's close() raced the new handshake. handleStartConfig keeps
its !isConnected() reset (do NOT reset on a same-connection want_config: the
post-unlock re-fetch is the client pulling now-unredacted config and must keep
the auth it just earned, otherwise config comes back redacted and set_config
writes get dropped).

* fix(lockdown): persist config on a lockdown-capable but disabled device

saveProto always called encryptAndWrite when encrypted storage was compiled,
and saveToDiskNoRetry skipped every save when !isUnlocked(). On a disabled
(never provisioned) device there is no DEK and isUnlocked() is always false,
so both paths fired and NO config ever persisted: a LoRa region set before
enabling lockdown lived only in RAM, then provisioning migrated the UNSET
default from disk and the region was lost.

Gate both on isLockdownActive(): when lockdown is inactive the device writes
plaintext exactly like stock firmware; the reloadFromDisk migrate pass then
re-saves those plaintext files encrypted once the device is provisioned.
Verified on hardware: region set while disabled now survives enable, reboot,
and unlock.

* fix(lockdown): suppress LoRa region picker under the lock screen

A locked-boot lockdown device installs region=UNSET as a deliberate RAM
placeholder (the real region is in encrypted storage, restored on unlock).
Screen.cpp popped the region picker / onboard message whenever region==UNSET,
so it rendered over the lock screen and trapped input with no way out. Skip it
while the display is being redacted for lockdown.

* fix(lockdown): silence cppcheck void* false positive + ruff docstring lints

The nRF52 `check` (cppcheck --fail-on-defect=low) flagged
arithOperationsOnVoidPointer on EncryptedStorage.cpp buffers. These are
false positives: make_zeroizing_array() returns unique_ptr<uint8_t[], ...>
so .get() is uint8_t*, not void* — cppcheck just can't resolve the
custom-deleter alias. File-scoped suppression, matching the existing
crypto-code convention in suppressions.txt.

Trunk flagged 5 ruff docstring issues in lockdown_provision.py: D301
(backslashes need a raw docstring) and D405/D407/D411/D413 (the EXAMPLES
heading was being parsed as a numpydoc section). Made the docstring raw
and renamed the heading to USAGE to dodge section detection while keeping
the ASCII-box formatting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(lockdown): resolve cppcheck const/null-deref defects

The nRF52 `check` job (pio check --fail-on-defect=low) flagged seven
real cppcheck defects in the lockdown code:

  - EncryptedStorage.cpp: nonce/encDek are read-only views into the
    token buffer -> const uint8_t *.
  - NodeDB.cpp: segments[] lookup table is never mutated -> const.
  - PhoneAPI.cpp: clearStatusSlot_LH's p is only compared; the auth-check
    slot and the hasPendingLockdownStatus loop var are read-only -> const.
  - Screen.cpp: the MESHTASTIC_LOCKDOWN drawLockdownLockScreen() guard
    introduced a redundant null check (nullPointerRedundantCheck) since
    dispdev->displayOff() right below derefs it unguarded, as does the
    rest of the file. Dropped the guard.

Verified with cppcheck 2.21 locally against the project suppressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(lockdown): const-qualify clearAuthSlot_LH param (cppcheck cascade)

Making clearStatusSlot_LH take const PhoneAPI* let cppcheck propagate the
same to clearAuthSlot_LH, whose p is only compared and forwarded. The
remaining PhoneAPI* params (findOrAlloc*Slot_LH) store p into the slot
table, so they correctly stay non-const.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(lockdown): wire runtime-toggle disable flow into provision tool

Addresses Copilot review on tools/lockdown_provision.py — the reference
tool advertised the runtime-toggle disable lifecycle but couldn't exercise
it:

  - _STATE_NAMES: map LockdownStatus.DISABLED so a capable-but-off boot
    prints DISABLED instead of an opaque state=<num>.
  - build_lockdown_auth(): add a disable param that actually sets
    la.disable, failing loudly on pre-runtime-toggle bindings instead of
    silently sending a plain unlock.
  - cmd_disable() + 'disable' subcommand: send LockdownAuth{disable=true,
    passphrase=...} and wait for the resulting LockdownStatus. Mirrors the
    firmware: non-empty passphrase required, DISABLED broadcast precedes
    the reboot, TTL/session fields ignored.
  - _exit_code_for_status(): treat DISABLED as a success (exit 0) like
    UNLOCKED.

All DISABLED/disable references are hasattr-guarded so the tool still
imports and runs the lock/unlock/provision paths against the currently
released meshtastic package (verified: it has LockdownAuth but not yet
disable/DISABLED). Verified with ruff 0.15.13 and black.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 15:39:49 -05:00
60303968bb Add Heltec mesh node t1 (#10416)
* add heltec-mesh-node-t1

* fixed low power

* Update the sensor enumeration values.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Fix memory leak in ICM42607PSensor

* fix  ST7735_MISO  error

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-29 12:47:09 -05:00
4a1ff18f57 feat: add Nordic nRF54L15-DK variant (Zephyr + BLE + LoRa) (#10193)
* feat: add Nordic nRF54L15-DK variant (Zephyr + BLE + LoRa)

Adds a community hardware variant for the Nordic nRF54L15-DK (PCA10156)
with an external EBYTE E22-900M30S (SX1262) LoRa module. First Meshtastic
port running on the Zephyr RTOS; all other Nordic targets use the nRF5
SoftDevice stack.

Scope
-----
- New Zephyr-based platform layer under src/platform/nrf54l15/ providing
  Arduino-compatible shims (Arduino.h, SPI, Wire, Print, Stream) over the
  Zephyr APIs plus a LittleFS-backed InternalFileSystem on SPIM20.
- Bluetooth LE peripheral (NRF54L15Bluetooth.*) built on the Zephyr BT
  host stack, exposing the Meshtastic GATT service with legacy
  connectable advertising, just-works pairing, dynamic MTU exchange
  (up to 247 bytes), and iOS connection-parameter tweaks.
- Variant directory variants/nrf54l15/nrf54l15dk/ with pin map for the
  E22 module on connector J1, PlatformIO env (nrf54l15dk), Zephyr
  DT overlay and a wiring README.
- Zephyr project config (zephyr/prj.conf + board overlay) tuned for
  BT + LoRa: 16 KB main stack, 4 KB BT RX thread, RTT logging in
  immediate mode, newlib-nano heap sized to leave room for the GATT
  pools while still allowing ATT MTU=247.
- extra_scripts/nrf54l15_linker.py works around a PlatformIO + old Ninja
  issue where Zephyr's two-pass linker script generation does not run
  automatically; the post-script parses build.ninja and invokes the
  gcc -E step directly before the final link.
- boards/nrf54l15dk.json board definition (PlatformIO needs it for the
  DK; the Seeed platform only ships the XIAO variants).
- variants/rp2350/rp2350.ini excludes platform/nrf54l15/ from RP2350
  build_src_filter so the shared platform tree does not leak between
  targets.
- .gitignore: add nRF J-Link / RTT debug artifacts (flash.jlink,
  rtt_*.txt).

Shared source changes
---------------------
- src/main.{cpp,h}, src/RedirectablePrint.cpp, src/FSCommon.{cpp,h},
  src/mesh/{Channels,NodeDB,RadioLibInterface,MeshService,PhoneAPI}.cpp,
  src/mesh/RadioLibInterface.h, src/modules/AdminModule.cpp: add small
  guards / helpers so the Zephyr build compiles alongside the Arduino
  targets. Behavior on existing boards is unchanged.

Hardware model
--------------
HW_VENDOR maps to meshtastic_HardwareModel_PRIVATE_HW until a dedicated
protobuf enum value is assigned upstream. The variant declares
custom_meshtastic_hw_model = 132 so the maintainers can wire the new
enum value through the protobufs repo after merge.

Hardware note
-------------
The E22-900M30S does not connect its DIO2 pin to TXEN internally — a
wire/solder bridge between DIO2 and TXEN on the module is required for
TX to work. Details and full pin map are in the variant README.

Validation
----------
Built clean against develop. On real hardware (April 2026) the port
passes end-to-end: iOS companion app pairs and connects, configuration
round-trip works, LoRa TX/RX reaches a canonical tbeam on the same mesh
channel, NodeDB updates propagate both ways, and traceroute completes.

* fix(nrf54l15): use atomic fs_rename instead of copy fallback

Zephyr LittleFS on nrf54l15 supports fs_rename natively, so route it
through the same atomic path as ESP32. The previous copyFile+remove
fallback truncated the destination before copying, leaving 0-byte files
if interrupted mid-write.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(nrf54l15): expand storage_partition from 36KB to 700KB

LittleFS on the default 9-block (36KB) storage_partition ran out of
space during copy-on-write of config.proto, causing fs_write to return
ENOSPC and pb_encode to surface "io error" when saving configuration
via the mobile app.

Reclaim slot1_partition (the MCUboot secondary slot — unused since we
flash directly via J-Link) and grow storage_partition to span
0xb6000..0x165000 (~175 blocks).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(nrf54l15): drop USERPREFS_LORACONFIG_* so LoRa config stays mutable

NodeDB rewrites LoRa config from USERPREFS_LORACONFIG_* on every boot,
which prevented reconfiguration via the BLE/serial app. Drop the
variant-level defaults; users configure region and modem preset through
the app like every other variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(nrf54l15): enforce MITM passkey pairing on GATT service

- Add MESH_PERM_READ/MESH_PERM_WRITE macros (READ_AUTHEN/WRITE_AUTHEN)
  on all mesh service characteristics so clients must complete passkey
  exchange before accessing fromNum/fromRadio/toRadio/logRadio.
- Wire FIXED_PIN mode to bt_passkey_set() so the device advertises a
  known PIN (config.bluetooth.fixed_pin); RANDOM_PIN keeps default
  per-pairing random passkey.
- Reduce BleDeferredThread HARD_WATCHDOG_MS from 3min to 1min.
- prj.conf: CONFIG_BT_SMP_ENFORCE_MITM=y, CONFIG_BT_FIXED_PASSKEY=y,
  CONFIG_BT_SMP_SC_PAIR_ONLY=n (legacy fallback for clients that abort
  SC pairing with reason 0x01 within 150ms).

* fix(nrf54l15): resolve develop-merge conflict + cppcheck warnings

The `Merge branch 'develop'` left two ~RadioLibInterface() declarations
in src/mesh/RadioLibInterface.h: the inline version added upstream by
PR #10254 (which independently applied the same UAF guard this PR was
carrying) and the out-of-line version this PR introduced. GCC rejects
the duplicate, breaking every platform build. Drop the out-of-line
declaration + definition; keep upstream's inline form.

Also silence the 13 cppcheck low warnings introduced by the new
nrf54l15 Arduino shim — Arduino's `String`/`SPISettings` API contract
relies on implicit single-arg constructors used pervasively by
existing Meshtastic code, so suppress `noExplicitConstructor` inline
with a comment instead of breaking the API. The few mechanical wins
(`const tmp[2]`, `const uint32_t *sp`) are applied directly.

* fmt: fix Trunk Check lint issues on nrf54l15-port

- extra_scripts/nrf54l15_linker.py: move regular imports above
  Import("env") to silence E402, add trunk-ignore-all(F821) for the
  PIO/SCons SConstruct injection (matches esp32_pre.py / nrf52_extra.py
  convention)
- src/platform/nrf54l15/NRF54L15Bluetooth.cpp: clang-format 16.0.3
- boards/nrf54l15dk.json + variants/nrf54l15/nrf54l15dk/README.md:
  prettier 3.8.3 (also resolves markdownlint MD060 on README tables)

No behavior change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(nrf54l15): address Copilot review comments + correct clang-format style

Six review threads from the 2026-04-30 Copilot review:

- src/platform/nrf54l15/nrf54l15_main.cpp: validate PSP against the nRF54L15
  SRAM range (0x20000000..0x20040000) and 4-byte alignment before walking the
  faulting thread's stack, and clamp the walk so it never reads past the end
  of RAM. Prevents a second fault inside the fatal handler when PSP is
  corrupted (common in real faults).

- src/platform/nrf54l15/nrf54l15_arduino.cpp: gate the bring-up printk traces
  in digitalWrite/digitalRead (CS/NRESET toggle log, BUSY-before-NRESET
  snapshot, BUSY periodic timeline) behind a new -DNRF54L15_GPIO_DEBUG flag
  that is off by default. The "dev NOT READY" message stays unconditional —
  it indicates a genuine hardware/DTS misconfig.

- src/modules/AdminModule.cpp: don't mutate config.device.output_gpio_enabled
  from handleGetConfig(). Reflect the live pin state in the response payload
  only — a getter must not write back to disk-persisted state.

- src/platform/nrf54l15/InternalFileSystem.h: derive totalBytes() from
  FIXED_PARTITION_SIZE(storage_partition) at compile time so it tracks the
  DK overlay's ~700 KB partition instead of the stale 36 KB hard-coded value.
  Updated the file header comment accordingly.

- extra_scripts/nrf54l15_linker.py: make _extract_gcc_command() handle the
  POSIX Ninja COMMAND format (no `cmd.exe /C "..."` wrapper) in addition to
  the Windows form, so the script doesn't hard-fail on Linux/macOS hosts.

- src/platform/nrf54l15/NRF54L15Bluetooth.cpp: clamp NO_PIN to RANDOM_PIN
  with a one-shot LOG_WARN. The mesh GATT permissions are declared with
  BT_GATT_PERM_*_AUTHEN and prj.conf sets CONFIG_BT_SMP_ENFORCE_MITM=y, so
  NO_PIN with no auth callbacks would leave every characteristic returning
  BT_ATT_ERR_AUTHENTICATION. Falling back to RANDOM_PIN keeps the link
  usable instead of silently broken. Also re-formatted this file with the
  project's .trunk/configs/.clang-format (Linux braces, 4-space indent,
  130-col) — the previous lint-fix commit a2aca3234 accidentally used the
  default LLVM style here, which CI's clang-format would have rejected.

Build verified: pio run -e nrf54l15dk passes, RAM 47.4%, Flash 28.6%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(nrf54l15): address remaining Copilot review threads

Round 2/3 review fixes — bugs first, then docs/portability:

BLE concurrency (NRF54L15Bluetooth.cpp):
- onNowHasData / sendLog / BleDeferredThread / shutdown: acquire
  active_conn under ble_mutex via new acquire_active_conn() helper so
  disconnected_cb can't free the conn between the null check and
  bt_conn_ref/bt_gatt_notify (use-after-unref).
- write_toradio: reject writes that exceed MAX_TO_FROM_RADIO_SIZE with
  ATT_ERR_INVALID_ATTRIBUTE_LEN instead of returning success and silently
  dropping the payload (would hide failed config writes from the phone).
- start_advertising: truncate the device name to fit the 31-byte legacy
  scan-response limit and switch to BT_DATA_NAME_SHORTENED so
  bt_le_adv_start() doesn't reject the payload when the name approaches
  CONFIG_BT_DEVICE_NAME_MAX=32.

Linker / portability:
- main.h: drop the rp2040Loop() forward declaration that had no
  definition and no callers — would surface as a link error if any RP2040
  build added a call to the symbol.
- nrf54l15_arduino.cpp: transfer16() now uses static __aligned(4) DMA
  buffers (matching transfer()), removing the EasyDMA-reachability hazard
  of caller-stack buffers on this part.

Filesystem (InternalFileSystem.h):
- usedBytes(): return real usage from fs_statvfs() instead of 0 so OTA
  / range-test free-space guards work.
- rewindDirectory(): close the dir before reopening — Zephyr fs_dir_t has
  no rewind, and re-fs_opendir on an open handle leaks LittleFS state.

Crash handler (nrf54l15_main.cpp):
- After the stack walk, busy-wait 50 ms to flush RTT/printk and call
  sys_reboot(SYS_REBOOT_COLD) directly so the saved_crash record is
  actually reported on the next boot. Default Zephyr config has
  RESET_ON_FATAL_ERROR=n, so the previous k_fatal_halt() spun forever.

Generalization / config:
- PhoneAPI.cpp: replace the NRF54L15_DK ifdef with a
  MESHTASTIC_EXCLUDE_FILES_MANIFEST capability flag (defined in the
  nrf54l15dk env) so future variants can opt in/out without touching
  shared code.
- variants/nrf54l15/nrf54l15.ini: parameterize libdeps include paths via
  ${PIOENV} so additional nRF54L15 envs sharing nrf54l15_base don't break.
- prj.conf: drop the stale "36 KB storage_partition" comments — the DK
  overlay reclaims slot1 to ~700 KB and runtime size comes from
  FIXED_PARTITION_SIZE.
- nrf54l15dk overlay: remove the zephyr,console / zephyr,shell-uart
  chosen entries that conflicted with CONFIG_UART_CONSOLE=n + RTT
  console; keep uart30 enabled so swapping the console is one Kconfig
  flip away.

Build: nrf54l15dk SUCCESS (flash 28.6%, RAM 47.4%); wiznet_5500_evb_pico2
SUCCESS (verifies the shared main.h change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(nrf54l15dk): use PRIVATE_HW (255) for custom_meshtastic_hw_model

Per @thebentern's review: the nRF54L15-DK is a development kit, not a
canonical Meshtastic SKU, so it falls under HardwareModel::PRIVATE_HW
(255) — the same enum value already used at runtime via HW_VENDOR. The
placeholder 132 is removed; no dedicated enum number will be assigned
for DK boards. Slug stays NRF54L15_DK as a human-readable identifier.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(nrf54l15): unstarve bt_long_wq so SC pairing completes

bt_pub_key_gen() runs the ECC P256 key generation on bt_long_wq.  At default
prio=10 (preemptible) and stack=1400 it gets starved by Meshtastic app
threads at boot — sc_public_key stays NULL for minutes, smp_public_key()
defers with SMP_FLAG_PKEY_SEND, and every SC pairing attempt stalls right
after the public-key exchange.  iOS shows "Connecting…" forever with no
PIN prompt; bleak/CLI fails the first CCC notify write with
"Protocol Error 0x05: Insufficient Authentication".

Set CONFIG_BT_LONG_WQ_PRIO=0 (highest preemptible, ties with main) and
CONFIG_BT_LONG_WQ_STACK_SIZE=4096 (margin for the P256M driver frames).

Validated E2E with iOS Meshtastic app: bt_smp_pkey_ready fires within ~40 s
of boot, 20 SC Passkey Entry rounds complete with matching pcnf/cfm,
encrypt 0x01 / sec_level 0x04 (Authenticated MITM), bonded=1.

* feat(nrf54l15): hardware I2C bus via TWIM30 + sensor telemetry

Adds the Arduino TwoWire layer for the nRF54L15-DK so Meshtastic's
sensor drivers can talk to external I2C devices over the hardware
TWIM30 peripheral.

Bus binding:
- &uart30 disabled in the board overlay (peripheral instance 30 is
  shared between UARTE30 / TWIM30 / SPIM30 — pick one). Console stays
  on RTT via CONFIG_RTT_CONSOLE.
- New i2c30_default / i2c30_sleep pinctrl with SDA=P0.03 / SCL=P0.04.
  External 4.7 kOhm pull-ups required on both lines.
- &i2c30 enabled at I2C_BITRATE_FAST (400 kHz).
- button_3 (SW3, P0.04) deleted from DTS so the pad can be claimed by
  i2c30 pinctrl; SW3 is still wired to the pad on the DK, do not press
  it during I2C use or it will short SCL to GND.

Arduino layer:
- src/platform/nrf54l15/Wire.cpp resolves the DT node at compile time
  via DEVICE_DT_GET(DT_NODELABEL(i2c30)) and dispatches Arduino's
  beginTransmission / write / endTransmission / requestFrom to
  i2c_write / i2c_write_read / i2c_read. Buffer is sized to 256 bytes
  for forward compatibility with the SE050 secure element on the
  custom PCB.
- Wire.h drops the prior compile-only stubs and exposes the real
  TwoWire surface.
- Arduino.h: BitOrder becomes an enum (not #define) so Adafruit_BusIO's
  `typedef BitOrder BusIOBitOrder;` compiles.

Variant + build flags:
- nrf54l15.ini flips HAS_WIRE / HAS_SENSOR / HAS_TELEMETRY from 0 to 1
  and cherry-picks the sensor libs Meshtastic needs (BusIO, Sensor,
  BMP280, BME280, INA219/226/260/3221, SHT4X). The full
  environmental_base group is avoided because it pulls
  Adafruit_SSD1306 / Adafruit_GFX which rely on Arduino pin macros the
  Zephyr shim does not implement.
- nrf54l15dk variant.h defines PIN_WIRE_SDA / PIN_WIRE_SCL for parity
  with the Arduino convention used by other variants. The actual bus
  wiring is fixed by the overlay pinctrl above.

Validated 2026-05-14/15 on the DK with BMP280 @ 0x76 (temperature +
barometric pressure) and INA3221 @ 0x42 (rail voltage / current);
EnvironmentTelemetry / PowerTelemetry packets transmit successfully
over LoRa.

Footprint cost on nrf54l15dk: +45 KB flash, +1.7 KB RAM.

* feat(nodedb): honor USERPREFS for environment telemetry on first boot

installDefaultConfig() now respects two new compile-time prefs:

  USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL
  USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED

The mobile apps enforce a 30 min floor on environment_update_interval
in the settings UI, which makes short-interval bring-up testing of new
sensor hardware painful — you have to wait half an hour for the first
LoRa packet to confirm wiring + driver. With these prefs baked into
the variant, the firmware can ship a freshly-flashed device that
broadcasts on a shorter cadence (e.g. 900 s) the moment storage_partition
is empty.

Both prefs are gated on #ifdef so the behavior is unchanged for any
variant that does not opt in. Documented in userPrefs.jsonc with the
existing telemetry-interval pref block.

* fix(nrf54l15): allow multiple bonded BLE peers

CONFIG_BT_MAX_PAIRED defaults to 1, so once the first peer (e.g. an
iOS phone) has paired and bonded, every subsequent pairing attempt
from a different MAC fails inside bt_keys_get_addr() with no free
key slot — the host returns BT_SECURITY_ERR_KEY_DOES_NOT_EXIST and
the second peer never gets past SMP.

Raise the slot count to 4 so the device can simultaneously hold an
iOS phone, a Windows host, a Linux host, and one spare bond. Add
BT_KEYS_OVERWRITE_OLDEST so that once the table fills, the LRU peer
is evicted on the next pairing rather than rejecting the new peer.
This matches the behavior other Meshtastic ports already provide
(nRF52 uses CONFIG_BT_PERIPHERAL_PRIO_CONN with similar semantics).

Discovered while bringing up the Python CLI on Windows alongside
the existing iOS bond.

* fix(nrf54l15): zero-initialize TwoWire buffers + clang-format Wire

cppcheck on every CI target (esp32s3, rp2040, rp2350, nrf52840, ...) was
failing the build with two `uninitMemberVar` warnings on TwoWire's
constructor: `txBuf` and `rxBuf` (256-byte arrays) were not initialized.
Even though the buffers are only read after txLen/rxLen is set, leaving
them uninitialized is a footgun if any future caller bypasses the
len-set step. Use C++11 value-initialization in the member initializer
list — costs ~512 B of memset at boot, gains a clean cppcheck pass and
defensive-against-future-changes semantics.

Also reformat Wire.{cpp,h} with the project's `.trunk/configs/.clang-format`
config so the Trunk Check Runner passes — clang-format moved the
`<errno.h>` include before the Zephyr-namespaced ones in Wire.cpp and
collapsed two inline overloads to single lines in Wire.h.

* fix(AdminModule): remove dead OUTPUT_GPIO_PIN/GpioOutputModule references

OUTPUT_GPIO_PIN is never defined and modules/GpioOutputModule.h doesn't
exist in the codebase; all #ifdef OUTPUT_GPIO_PIN branches were dead code
introduced by the nRF54L15-DK variant commit. Strips the include, the
output_gpio_enabled OFF→ON/ON→OFF transition logic in handleSetConfig(),
and the digitalRead() reflection in handleGetConfig().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-05-16 06:16:11 -05:00
Ben Meadors d693fd4232 Exclude accelerometer on new MESHTASTIC_EXCLUDE_ACCELEROMETER flag (#10004) 2026-03-25 08:57:29 -05:00
Ben Meadors 299ef95f25 Revert "Add agc reset attempt (#8163)" (#9702)
This reverts commit ac611c4b62.
2026-02-20 11:25:28 -06:00
Jonathan BennettandCopilot ac611c4b62 Add agc reset attempt (#8163)
* Add agc reset attempt

* Add radioLibInterface include

* Trunk

* AGC reset don't crash, don't naively call

* Update src/main.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Use Throttle function

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-04 14:47:44 -06:00
Jonathan Bennett c038cfe69a Move device code from main.cpp to earlyInitVariant (#9438) 2026-01-26 11:54:14 -06:00
Ben Meadors 552df4c88c Supress reboot banner in Reboot OTA 2026-01-14 07:06:40 -06:00
Ben Meadors 3a90781e1b Add support for LilyGo T-Echo Plus (#9149)
* Add t-echo plus support

* T-Echo plus hw model

* Cruft

* Fix

* Added InkHUD style touch backlight control
2026-01-02 20:14:09 -06:00
Ben Meadors 8685436cbb Merge pull request #7773 from meshtastic/master
Backmerge
2025-08-27 20:53:55 -05:00
3f5c30e3b3 T-Lora Pager (#7613)
* initial commit

* preset rotary1 encoder

* define TAB+ESC

* haptic feedback

* allow switch off haptic feedback

* enable audio amplifier

* include PR4684

* fix for tft target

* add ES8311 audio codec

* fix KB scan duplicate

* display workaround to avoid debris

* fix debris on display

* keyboard backlight

* enable screen options

* fsm based bounce-free rotary encoder implementation

* use fsm RotaryEncoder only for T-Lora Pager

* change inputbroker default config to allow using rotary wheel for screens AND menues

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-08-26 09:35:25 -05:00
Ben Meadorsandwhywilson 093a37a2b0 On screen keyboard (#7705)
* Add on-screen keyboard implementation on Trackball device.

* Update On-Screen Keyboard to new layout.

* The on-screen keyboard dynamically adjusts the key size based on the screen.

* Improve input box display on small screens.

* Optimize the virtual keyboard layout and cursor movement logic, and adjust the keyboard starting position for small and wide screens.

* Optimize the text alignment of numeric keys on ssd1306.

---------

Co-authored-by: whywilson <m.tools@qq.com>
2025-08-21 06:31:27 -05:00
Ben Meadors 1daf5aad1f Revert "Add on-screen keyboard implementation on Trackball device. (#7625)" (#7704)
This reverts commit fe3f14a63e.
2025-08-21 06:29:23 -05:00
WilsonandBen Meadors fe3f14a63e Add on-screen keyboard implementation on Trackball device. (#7625)
* Add on-screen keyboard implementation on Wio Tracker L1.

* Update On-Screen Keyboard to new layout.

* The on-screen keyboard dynamically adjusts the key size based on the screen.

* Improve input box display on small screens.

* Optimize the virtual keyboard layout and cursor movement logic, and adjust the keyboard starting position for small and wide screens.

* Optimize the text alignment of numeric keys on ssd1306.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-08-21 18:01:31 +08:00
691327b2db Initial support for the ThinkNode M5 (#7502)
* Initial support for the ThinkNode M5

* Update variants/esp32s3/ELECROW-ThinkNode-M5/platformio.ini

Co-authored-by: Austin <vidplace7@gmail.com>

* Cleanup variant.h for Elecrow Thinknode M5

* Properly detect battery voltage

* Turn backlight off when screen sleeps

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Austin <vidplace7@gmail.com>
2025-08-07 06:28:15 -05:00
Tom Fifield@ColoradohuskyCopilot
baf0e9c7e6 Add detection framework for multiple AirQuality sensors (#7187)
* Add detection framework for multiple AirQuality sensors

Now we have the ability to detect multiple AirQualitySensors,
follow the lead of other sensor types and create supporting methods
and objects for using this information.

Continued cherry-picking to get #4601 over the line :)

Co-Authored-By: @Coloradohusky

* Update src/main.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-01 21:27:44 +10:00
4feaec651f Unify the native display config between legacy display and MUI (#6838)
* Add missed include

* Another Warning fix

* Add another HAS_SCREEN

* Namespace fixes

* Removed depricated destination types and re-factored destination screen

* Get rid of Arduino Strings

* Clean up after Copilot

* SixthLine Def, Screen Rename

Added Sixth Line Definition Screen Rename, and Automatic Line Adjustment

* Consistency is hard - fixed "Sixth"

* System Frame Updates

Adjusted line construction to ensure we fit maximum content per screen.

* Fix up notifications

* Add a couple more ifdef HAS_SCREEN lines

* Add screen->isOverlayBannerShowing()

* Don't forget the invert!

* Adjust Nodelist Center Divider

Adjust Nodelist Center Divider

* Fix variable casting

* Fix entryText variable as empty before update to fix validation

* Altitude is int32_t

* Update PowerTelemetry to have correct data type

* Fix cppcheck warnings (#6945)

* Fix cppcheck warnings

* Adjust logic in Power.cpp for power sensor

---------

Co-authored-by: Jason P <applewiz@mac.com>

* More pixel wrangling so things line up NodeList edition

* Adjust NodeList alignments and plumb some background padding for a possible title fix

* Better alignment for banner notifications

* Move title into drawCommonHeader; initial screen tested

* Fonts make spacing items difficult

* Improved beeping booping and other buzzer based feedback (#6947)

* Improved beeping booping and other buzzer based feedback

* audible button feedback (#6949)

* Refactor

---------

Co-authored-by: todd-herbert <herbert.todd@gmail.com>

* Sandpapered the corners of the notification popup

* Finalize drawCommonHeader migration

* Update Title of Favorite Node Screens

* Update node metric alignment on LoRa screen

* Update the border for popups to separate it from background

* Update PaxcounterModule.cpp with CommonHeader

* Update WiFi screen with CommonHeader and related data reflow

* It was not, in fact, pointing up

* Fix build on wismeshtap

* T-deck trackball debounce

* Fix uptime on Device Focused page to actually detail

* Update Sys screen for new uptime, add label to Freq/Chan on LoRa

* Don't display DOP any longer, make Uptime consistent

* Revert Uptime change on Favorites, Apply to Device Focused

* Label the satelite number to avoid confusion

* Boop boop boop boop

* Correct GPS positioning and string consistency across strings for GPS

* Fix GPS text alignment

* Enable canned messages by default

* Don't wake screen on new nodes

* Cannedmessage list emote support added

* Fn+e emote picker for freetext screen

* Actually block CannedInput actions while display is shown

* Add selection menu to bannerOverlay

* Off by one

* Move to unified text layouts and spacing

* Still my Fav without an "e"

* Fully remove EVENT_NODEDB_UPDATED

* Simply LoRa screen

* Make some char pointers const to fix compilation on native targets

* Update drawCompassNorth to include radius

* Fix warning

* button thread cleanup

* Pull OneButton handling from PowerFSM and add MUI switch (#6973)

* Trunk

* Onebutton Menu Support

* Add temporary clock icon

* Add gps location to fsi

* Banner message state reset

* Cast to char to satisfy compiler

* Better fast handling of input during banner

* Fix warning

* Derp

* oops

* Update ref

* Wire buzzer_mode

* remove legacy string->print()

* Only init screen if one found

* Unsigned Char

* More buttonThread cleaning

* screen.cpp button handling cleanup

* The Great Event Rename of 2025

* Fix the Radiomaster

* Missed trackball type change

* Remove unused function

* Make ButtonThread an InputBroker

* Coffee hadn't kicked in yet

* Add clock icon for Navigation Bar

* Restore clock screen definition code - whoops

* ExternalNotifications now observe inputBroker

* Clock rework (#6992)

* Move Clock bits into ClockRenderer space

* Rework clock into all device navigation

* T-Watch Actually Builds Different

* Compile fix

---------

Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>

* Add AM/PM to Digital Clock

* Flip Seconds and AM/PM on Clock Display

* Tik-tok pixels are hard

* Fix builds on Thinknode M1

* Check for GPS and don't crash

* Don't endif til the end

* Rework the OneButton thread to be much less of a mess. (#6997)

* Rework the OneButton thread to be much less of a mess. And break lots of targets temporarily

* Update src/input/ButtonThread.h

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix GPS toggle

* Send the shutdown event, not just the kbchar

* Honor the back button in a notificaiton popup

* Draw the right size box for popup with options

* Try to un-break all the things

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* 24-hour Clock Should have leading zero, but not 12-hour

* Fixup some compile errors

* Add intRoutine to ButtonThread init, to get more responsive user button back

* Add Timezone picker

* Fix Warning

* Optionally set the initial selection for the chooser popup

* Make back buttons work in canned messages

* Drop the wrapper classes

* LonPressTime now configurable

* Clock Frame can not longer be blank; just add valid time

* Back buttons everywhere!

* Key Verification confirm banner

* Make Elecrow M* top button a back button

* Add settings saves

* EInk responsiveness fixes

* Linux Input Fixes

* Add Native Trackball/Joystick support, and move UserButton to Input

* No Flight Stick Mode

* Send input event

* Add Channel Utilization to Device Focused frame

* Don't shift screens when we draw new ones

* Add showOverlayBanner arguments to no-op

* trunk

* Default Native trackball to NC

* Fix crash in simulator mode

* Add longLong button press

* Get the args right

* Adjust Bluetooth Pairing Screen to account for bottom navigation.

* Trackball everywhere, and unPhone buttons

* Remap visionmaster secondary button to TB_UP

* Kill ScanAndSelect

* trunk

* No longer need the canned messages input filter

* All Canned All the time

* Fix stm32 compile error regarding inputBroker

* Unify tft lineheights (#7033)

* Create variable line heights based upon SCREEN_HEIGHT

* Refactor textPositions into method -> getTextPositions

* Update SharedUIDisplay.h

---------

Co-authored-by: Jason P <applewiz@mac.com>

* Adjust top distance for larger displays

* Adjust icon sizes for larger displays

* Fix Paxcounter compile errors after code updates

* Pixel wrangling to make larger screens fit better

* Alert frame has precedence over banner -- for now

* Unify on ALT_BUTTON

* Align AM/PM to the digit, not the segment on larger displays

* Move some global pin defines into configuration.h

* Scaffolding for BMM150 9-axis gyro

* Alt button behavior

* Don't add the blank GPS frames without HAS_GPS

* EVENT_NODEDB_UPDATED has been retired

* Clean out LOG_WARN messages from debugging

* Add dismiss message function

* Minor buttonThread cleanup

* Add BMM150 support

* Clean up last warning from dev

* Simplify bmm150 init return logic

* Add option to reply to messages

* Add minimal menu upon selecting home screen

* Move Messages to slot 2, rename GPS to Position, move variables nearer functional usage in Screen.cpp

* Properly dismiss message

* T-Deck Trackball press is not user button

* Add select on favorite frame to launch cannedMessage DM

* Minor wording change

* Less capital letters

* Fix empty message check, time isn't reliable

* drop dead code

* Make UIRenderer a static class instead of namespace

* Fix the select on favorite

* Check if message is empty early and then 'return'

* Add kb_found, and show the option to launch freetype if appropriate

* Ignore impossible touchscreen touches

* Auto scroll fix

* Move linebreak after "from" for banners to maximize screen usage.

* Center "No messages to show" on Message frame

* Start consolidating buzzer behavior

* Fixed signed / unsigned warning

* Cast second parameter of max() to make some targets happy

* Cast kbchar to (char) to make arduino string happy

* Shorten the notice of "No messages"

* Add buzzer mode chooser

* Add regionPicker to Lora icon

* Reduce line spacing and reorder Position screen to resolve overlapping issues

* Update message titles, fix GPS icons, add Back options

* Leftover boops

* Remove chirp

* Make the region selection dismissable when a region is already set

* Add read-aloud functionality on messages w/ esp8266sam

* "Last Heard" is a better label

* tweak the beep

* 5 options

* properly tear down freetext upon cancel

* de-convelute canned messages just a bit

* Correct height of Mail icon in navigation bar

* Remove unused warning

* Consolidate time methods into TimeFormatters

* Oops

* Change LoRa Picker Cancel to Back

* Tweak selection characters on Banner

* Message render not scrolling on 5th line

* More fixes for message scrolling

* Remove the safety next on text overflow - we found that root cause

* Add pin definitions to fix compilation for obscure target

* Don't let the touchscreen send unitialized kbchar values

* Make virtual KB just a bit quicker

* No more double tap, swipe!

* Left is left, and Right is right

* Update horizontal lightning bolt design

* Move from solid to dashed separator for Message Frame

* Single emote feature fix

* Manually sort overlapping elements for now

* Freetext and clearer choices

* Fix ESP32 InkHUD builds on the unify-tft branch (#7087)

* Remove BaseUI branding

* Capitalization is fun

* Revert Meshtastic Boot Frame Changes

* Add ANZ_433 LoRa region to picker

* Update settings.json

---------

Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jason P <applewiz@mac.com>
Co-authored-by: todd-herbert <herbert.todd@gmail.com>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-06-21 06:36:04 -05:00
Jonathan Bennett 3a6fc668d8 20948 compass support (#6707)
* Add __has_include blocks for sensors

* Put BMP and BME back in the right sensors

* Split environmental_base to environmental_extra, to compile the working sensor libs for Native

* Remove hard-coded checks for ARCH_PORTDUINO

* Un-clobber bmx160

* Move BusIO to environmental_extra due to Armv7 compile error

* Move to forked BusIO for the moment

* Switch to Meshtastic ICM-20948 lib for Portduino support

* Use 20948 for compass direction

* Compass is more than just RAK4631

* Cleanup for 20948 compass

* use Meshtastic branch of 20948 lib

* Check for HAS_SCREEN for showing calibration screen

* No accelerometerThread on STM32
2025-05-07 18:38:42 -05:00
JorropoandBen Meadors a7ef9e9c08 udp-multicast: remove the thread from the multicast thread API (#6685)
* udp-multicast: remove the thread from the multicast thread API

The whole API is parallel & asynchronous we don't need to start a thread ourself,
the implementation probably does when we call start listening already.

* Take copilot advice and call it a handler

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2025-04-30 05:52:42 -05:00
+9 99d3e5eb70 2.6 changes (#5806)
* 2.6 protos

* [create-pull-request] automated change (#5789)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Hello world support for UDP broadcasts over the LAN on ESP32 (#5779)

* UDP local area network meshing on ESP32

* Logs

* Comment

* Update UdpMulticastThread.h

* Changes

* Only use router->send

* Make NodeDatabase (and file) independent of DeviceState (#5813)

* Make NodeDatabase (and file) independent of DeviceState

* 70

* Remove logging statement no longer needed

* Explicitly set CAD symbols, improve slot time calculation and adjust CW size accordingly (#5772)

* File system persistence fixes

* [create-pull-request] automated change (#6000)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Update ref

* Back to 80

* [create-pull-request] automated change (#6002)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* 2.6 <- Next hop router (#6005)

* Initial version of NextHopRouter

* Set original hop limit in header flags

* Short-circuit to FloodingRouter for broadcasts

* If packet traveled 1 hop, set `relay_node` as `next_hop` for the original transmitter

* Set last byte to 0xFF if it ended at 0x00
As per an idea of @S5NC

* Also update next-hop based on received DM for us

* temp

* Add 1 retransmission for intermediate hops when using NextHopRouter

* Add next_hop and relayed_by in PacketHistory for setting next-hop and handle flooding fallback

* Update protos, store multiple relayers

* Remove next-hop update logic from NeighborInfoModule

* Fix retransmissions

* Improve ACKs for repeated packets and responses

* Stop retransmission even if there's not relay node

* Revert perhapsRebroadcast()

* Remove relayer if we cancel a transmission

* Better checking for fallback to flooding

* Fix newlines in traceroute print logs

* Stop retransmission for original packet

* Use relayID

* Also when want_ack is set, we should try to retransmit

* Fix cppcheck error

* Fix 'router' not in scope error

* Fix another cppcheck error

* Check for hop_limit and also update next hop when `hop_start == hop_limit` on ACK
Also check for broadcast in `getNextHop()`

* Formatting and correct NUM_RETRANSMISSIONS

* Update protos

* Start retransmissions in NextHopRouter if ReliableRouter didn't do it

* Handle repeated/fallback to flooding packets properly
First check if it's not still in the TxQueue

* Guard against clients setting `next_hop`/`relay_node`

* Don't cancel relay if we were the assigned next-hop

* Replies (e.g. tapback emoji) are also a valid confirmation of receipt

---------

Co-authored-by: GUVWAF <thijs@havinga.eu>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: GUVWAF <78759985+GUVWAF@users.noreply.github.com>

* fix "native" compiler errors/warnings NodeDB.h

* fancy T-Deck / SenseCAP Indicator / unPhone / PICOmputer-S3 TFT screen (#3259)

* lib update: light theme

* fix merge issue

* lib update: home buttons + button try-fix

* lib update: icon color fix

* lib update: fix instability/crash on notification

* update lib: timezone

* timezone label

* lib update: fix set owner

* fix spiLock in RadioLibInterface

* add picomputer tft build

* picomputer build

* fix compiler error std::find()

* fix merge

* lib update: theme runtime config

* lib update: packet logger + T-Deck Plus

* lib update: mesh detector

* lib update: fix brightness & trackball crash

* try-fix less paranoia

* sensecap indicator updates

* lib update: indicator fix

* lib update: statistic & some fixes

* lib-update: other T-Deck touch driver

* use custom touch driver for Indicator

* lower tft task prio

* prepare LVGL ST7789 driver

* lib update: try-fix audio

* Drop received packets from self

* Additional decoded packet ignores

* Honor flip & color for Heltec T114 and T190 (#4786)

* Honor TFT_MESH color if defined for Heltec T114 or T190

* Temporary: point lib_deps at fork of Heltec's ST7789 library
For demo only, until ST7789 is merged

* Update lib_deps; tidy preprocessor logic

* Download debian files after firmware zip

* set title for protobufs bump PR (#4792)

* set title for version bump PR (#4791)

* Enable Dependabot

* chore: trunk fmt

* fix dependabot syntax (#4795)

* fix dependabot syntax

* Update dependabot.yml

* Update dependabot.yml

* Bump peter-evans/create-pull-request from 6 to 7 in /.github/workflows (#4797)

* Bump docker/build-push-action from 5 to 6 in /.github/workflows (#4800)

* Actions: Semgrep Images have moved from returntocorp to semgrep (#4774)

https://hub.docker.com/r/returntocorp/semgrep notes: "We've moved!
 Official Docker images for Semgrep now available at semgrep/semgrep."

Patch updates our CI workflow for these images.

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* Bump meshtestic from `31ee3d9` to `37245b3` (#4799)

Bumps [meshtestic](https://github.com/meshtastic/meshTestic) from `31ee3d9` to `37245b3`.
- [Commits](https://github.com/meshtastic/meshTestic/compare/31ee3d90c8bef61e835c3271be2c7cda8c4a5cc2...37245b3d612a9272f546bbb092837bafdad46bc2)

---
updated-dependencies:
- dependency-name: meshtestic
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* [create-pull-request] automated change (#4789)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Bump pnpm/action-setup from 2 to 4 in /.github/workflows (#4798)

Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 2 to 4.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v2...v4)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Raspberry Pico2 - needs protos

* Re-order doDeepSleep (#4802)

Make sure PMU sleep takes place before I2C ends

* [create-pull-request] automated change

* heltec-wireless-bridge
requires Proto PR first

* feat: trigger class update when protobufs are changed

* meshtastic/ is a test suite; protobufs/ contains protobufs;

* Update platform-native to pick up portduino crash fix (#4807)

* Hopefully extract and commit to meshtastic.github.io

* CI fixes

* [Board] DIY "t-energy-s3_e22" (#4782)

* New variant "t-energy-s3_e22"

- Lilygo T-Energy-S3
- NanoVHF "Mesh-v1.06-TTGO-T18" board
- Ebyte E22 Series

* add board_level = extra

* Update variant.h

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>

* Consolidate variant build steps (#4806)

* poc: consolidate variant build steps

* use build-variant action

* only checkout once and clean up after run

* Revert "Consolidate variant build steps (#4806)" (#4816)

This reverts commit 9f8d86cb25.

* Make Ublox code more readable (#4727)

* Simplify Ublox code

Ublox comes in a myriad of versions and settings. Presently our
configuration code does a lot of branching based on versions being
or not being present.

This patch adds version detection earlier in the piece and branches
on the set gnssModel instead to create separate setup methods for Ublox 6,
Ublox 7/8/9, and Ublox10.

Additionally, adds a macro to make the code much shorter and more
readable.

* Make trunk happy

* Make trunk happy

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* Consider the LoRa header when checking packet length

* Minor fix (#4666)

* Minor fixes

It turns out setting a map value with the index notation causes
an lookup that can be avoided with emplace. Apply this to one line in
the StoreForward module.

Fix also Cppcheck-determined highly minor performance increase by
passing gpiochipname as a const reference :)

The amount of cycles used on this laptop while learning about these
callouts from cppcheck is unlikely to ever be more than the cycles
saved by the fixes ;)

* Update PortduinoGlue.cpp

* Revert "Update classes on protobufs update" (#4824)

* Revert "Update classes on protobufs update"

* remove quotes to fix trunk.

---------

Co-authored-by: Tom Fifield <tom@tomfifield.net>

* Implement optional second I2C bus for NRF52840
Enabled at compile-time if WIRE_INFERFACES_COUNT defined as 2

* Add I2C bus to Heltec T114 header pins
SDA: P0.13
SCL: P0.16

Uses bus 1, leaving bus 0 routed to the unpopulated footprint for the RTC (general future-proofing)

* Tidier macros

* Swap SDA and SCL
SDA=P0.16, SCL=P0.13

* Refactor and consolidate time window logic (#4826)

* Refactor and consolidate windowing logic

* Trunk

* Fixes

* More

* Fix braces and remove unused now variables.

There was a brace in src/mesh/RadioLibInterface.cpp that was breaking
compile on some architectures.

Additionally, there were some brace errors in
src/modules/Telemetry/AirQualityTelemetry.cpp
src/modules/Telemetry/EnvironmentTelemetry.cpp
src/mesh/wifi/WiFiAPClient.cpp

Move throttle include in WifiAPClient.cpp to top.

Add Default.h to sleep.cpp

rest of files just remove unused now variables.

* Remove a couple more meows

---------

Co-authored-by: Tom Fifield <tom@tomfifield.net>

* Rename message length headers and set payload max to 255 (#4827)

* Rename message length headers and set payload max to 255

* Add MESHTASTIC_PKC_OVERHEAD

* compare to MESHTASTIC_HEADER_LENGTH

---------

Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>

* Check for null before printing debug (#4835)

* fix merge

* try-fix crash

* lib update: fix neighbors

* fix GPIO0 mode after I2S audio

* lib update: audio fix

* lib update: fixes and improvements

* extra

* added ILI9342 (from master)

* device-ui persistency

* review update

* fix request, add handled

* fix merge issue

* fix merge issue

* remove newline

* remove newlines from debug log

* playing with locks; but needs more testing

* diy mesh-tab initial files

* board definition for mesh-tab (not yet used)

* use DISPLAY_SET_RESOLUTION to avoid hw dependency in code

* no telemetry for Indicator

* 16MB partition for Indicator

* 8MB partition for Indicator

* stability: add SPI lock before saving via littleFS

* dummy for config transfer (#5154)

* update indicator (due to compile and linker errors)

* remove faulty partition line

* fix missing include

* update indicator board

* update mesh-tab ILI9143 TFT

* fix naming

* mesh-tab targets

* try: disable duplicate locks

* fix nodeDB erase loop when free mem returns invalid value (0, -1).

* upgrade toolchain for nrf52 to gcc 9.3.1

* try-fix (workaround) T-Deck audio crash

* update mesh-tab tft configs

* set T-Deck audio to unused 48 (mem mclk)

* swap mclk to gpio 21

* update meshtab voltage divider

* update mesh-tab ini

* Fixed the issue that indicator device uploads via rp2040 serial port in some cases.

* Fixed the issue that the touch I2C address definition was not effective.

* Fixed the issue that the wifi configuration saved to RAM did not take effect.

* rotation fix; added ST7789 3.2" display

* dreamcatcher: assign GPIO44 to audio mclk

* mesh-tab touch updates

* add mesh-tab powersave as default

* fix DIO1 wakeup

* mesh-tab: enable alert message menu

* Streamline board definitions for first tech preview. (#5390)

* Streamline board definitions for first tech preview. TBD: Indicator Support

* add point-of-checkin

* use board/unphone.json

---------

Co-authored-by: mverch67 <manuel.verch@gmx.de>

* fix native targets

* add RadioLib debugging options for (T-Deck)

* fix T-Deck build

* fix native tft targets for rpi

* remove wrong debug defines

* t-deck-tft button is handled in device-ui

* disable default lightsleep for indicator

* Windows Support - Trunk and Platformio (#5397)

* Add support for GPG

* Add usb device support

* Add trunk.io to devcontainer

* Trunk things

* trunk fmt

* formatting

* fix trivy/DS002, checkov/CKV_DOCKER_3

* hide docker extension popup

* fix trivy/DS026, checkov/CKV_DOCKER_2

* fix radioLib warnings for T-Deck target

* wake screen with button only

* use custom touch driver

* define wake button for unphone

* use board definition for mesh-tab

* mesh-tab rotation upside-down

* update platform native

* use MESH_TAB hardware model definition

* radioLib update (fix crash/assert)

* reference seeed indicator fix commit arduino-esp32

* Remove unneeded file change :)

* disable serial module and tcp socket api for standalone devices (#5591)

* disable serial module and tcp socket api for standalone devices
* just disable webserver, leave wifi available
* disable socket api

* mesh-tab: lower I2C touch frequency

* log error when packet queue is full

* add more locking for shared SPI devices (#5595)

* add more locking for shared SPI devices
* call initSPI before the lock is used
* remove old one
* don't double lock
* Add missing unlock
* More missing unlocks
* Add locks to SafeFile, remove from `readcb`, introduce some LockGuards
* fix lock in setupSDCard()
* pull radiolib trunk with SPI-CS fixes
* change ContentHandler to Constructor type locks, where applicable

---------

Co-authored-by: mverch67 <manuel.verch@gmx.de>
Co-authored-by: GUVWAF <thijs@havinga.eu>
Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com>

* T-Deck: revert back to lovyanGFX touch driver

* T-Deck: increase allocated PSRAM by 50%

* mesh-tab: streamline target definitions

* update RadioLib 7.1.2

* mesh-tab: fix touch rotation 4.0 inch display

* Mesh-Tab platformio: 4.0inch: increase SPI frequency to max

* mesh-tab: fix rotation for 3.5 IPS capacitive display

* mesh-tab: fix rotation for 3.2 IPS capacitive display

* restructure device-ui library into sub-directories

* preparations for generic DisplayDriverFactory

* T-Deck: increase LVGL memory size

* update lib

* trunk fmt

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: todd-herbert <herbert.todd@gmail.com>
Co-authored-by: Jason Murray <15822260+scruplelesswizard@users.noreply.github.com>
Co-authored-by: Jason Murray <jason@chaosaffe.io>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Austin <vidplace7@gmail.com>
Co-authored-by: virgil <virgil.wang.cj@gmail.com>
Co-authored-by: Mark Trevor Birss <markbirss@gmail.com>
Co-authored-by: Kalle Lilja <15094562+ThatKalle@users.noreply.github.com>
Co-authored-by: GUVWAF <thijs@havinga.eu>

* Version this

* Update platformio.ini (#6006)

* tested higher speed and it works

* Un-extra

* Add -tft environments to the ci matrix

* Exclude unphone tft for now. Something is wonky

* fixed Indicator touch issue (causing IO expander issues), added more RAM

* update lib

* fixed Indicator touch issue (causing IO expander issues), added more RAM (#6013)

* increase T-Deck PSRAM to avoid too early out-of-memory when messages fill up the storage

* update device-ui lib

* Fix T-Deck SD card detection (#6023)

* increase T-Deck PSRAM to avoid too early out-of-memory when messages fill up the storage

* fix SDCard for T-Deck; allow SPI frequency config

* meshtasticd: Add X11 480x480 preset (#6020)

* Littlefs per device

* 2.6 update

* [create-pull-request] automated change (#6037)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* InkHUD UI for E-Ink (#6034)

* Decouple ButtonThread from sleep.cpp
Reorganize sleep observables. Don't call ButtonThread methods inside doLightSleep. Instead, handle in class with new lightsleep Observables.

* InkHUD: initial commit (WIP)
Publicly discloses the current work in progress. Not ready for use.

* feat: battery icon

* chore: implement meshtastic/firmware #5454
Clean up some inline functions

* feat: menu & settings for "jump to applet"

* Remove the beforeRender pattern
It hugely complicates things. If we can achieve acceptable performance without it, so much the better.

* Remove previous Map Applet
Needs re-implementation to work without the beforeRender pattern

* refactor: reimplement map applet
Doesn't require own position
Doesn't require the beforeRender pattern to precalculate; now all-at-once in render
Lays groundwork for fixed-size map with custom background image

* feat: autoshow
Allow user to select which applets (if any) should be automatically brought to foreground when they have new data to display

* refactor: tidy-up applet constructors
misc. jobs including:
- consistent naming
- move initializer-list-only constructors to header
- give derived applets unique identifiers for MeshModule and OSThread logging

* hotfix: autoshow always uses FAST update
In future, it *will* often use FAST, but this will be controlled by a WindowManager component which has not yet been written.
Hotfixed, in case anybody is attempting to use this development version on their deployed devices.

* refactor: bringToForeground no longer requests FAST update
In situations where an applet has moved to foreground because of user input, requestUpdate can be manually called, to upgrade to FAST refresh.
More permanent solution for #23e1dfc

* refactor: extract string storage from ThreadedMessageApplet
Separates the code responsible for storing the limited message history, which was previously part of the ThreadedMessageApplet.
We're now also using this code to store the "most recent message". Previously, this was stored in the `InkHUD::settings` struct, which was much less space-efficient.
We're also now storing the latest DM, laying the foundation for an applet to display only DMs, which will complement the threaded message applet.

* fix: text wrapping
Attempts to fix a disparity between `Applet::printWrapped` and `Applet::getWrappedTextHeight`, which would occasionally cause a ThreadedMessageApplet message to render "too short", overlapping other text.

* fix: purge old constructor
This one slipped through the last commit..

* feat: DM Applet
Useful in combination with the ThreadedMessageApplets, which don't show DMs

* fix: applets shouldn't handle events while deactivated
Only one or two applets were actually doing this, but I'm making a habit of having all applets return early from their event handling methods (as good practice), even if those methods are disabled elsewhere (e.g. not observing observable, return false from wantPacket)

* refactor: allow requesting update without requesting autoshow
Some applets may want to redraw, if they are displayed, but not feel the information is worth being brought to foreground for. Example: ActiveNodesApplet, when purging old nodes from list.

* feat: custom "Recently Active" duration
Allows users to tailor how long nodes will appear in the "Recents" applets, to suit the activity level of their mesh.

* refactor: rename some applets

* fix: autoshow

* fix: getWrappedTextHeight
Remove the "simulate" option from printWrapped; too hard to keep inline with genuine printing (because of AdafruitGFX Fonts' xAdvance, mabye?). Instead of simulating, we printWrapped as normal, and discard pixel output by setting crop. Both methods are similarly inefficient, apparently.

* fix: text wrapping in ThreadedMessageApplet
Wrong arguments were passed to Applet::printWrapped

* feat: notifications for text messages
Only shown if current applet does not already display the same info. Autoshow takes priority over notifications, if both would be used to display the same info.

* feat: optimize FAST vs FULL updates
New UpdateMediator class counts the number of each update type, and suggets which one to use, if the code doesn't already have an explicit prefence. Also performs "maintenance refreshes" unprovoked if display is not given an opportunity to before a FULL refresh through organic use.

* chore: update todo list

* fix: rare lock-up of buttons

* refactor: backlight
Replaces the initial proof-of-concept frontlight code for T-Echo
Presses less than 5 seconds momentarily illuminate the display
Presses longer than 5 seconds latch the light, requiring another tap to disable
If user has previously removed the T-Echo's capacitive touch button (some DIY projects), the light is controlled by the on-screen menu. This fallback is used by all T-Echo devices, until a press of the capacitive touch button is detected.

* feat: change tile with aux button
Applied to VM-E290.
Working as is, but a refactor of WindowManager::render is expected shortly, which will also tidy code from this push.

* fix: specify out-of-the-box tile assignments
Prevents placeholder applet showing on initial boot, for devices which use a mult-tile layout by default (VM-E290)

* fix: verify settings version when loading

* fix: wrong settings version

* refactor: remove unimplemented argument from requestUpdate
Specified whether or not to update "async", however the implementation was slightly broken, Applet::requestUpdate is only handled next time WindowManager::runOnce is called. This didn't allow code to actually await an update, which was misleading.

* refactor: renaming
Applet::render becomes Applet::onRender.
Tile::displayedApplet becomes Tile::assignedApplet.
New onRender method name allows us to move some of the pre and post render code from WindowManager into new Applet::render method, which will call onRender for us.

* refactor: rendering
Bit of a tidy-up. No intended change in behavior.

* fix: optimize refresh times
Shorter wait between retrying update if display was previously busy.
Set anticipated update durations closer to observed values. No signifacant performance increase, but does decrease the amount of polling required.

* feat: blocking update for E-Ink
Option to wait for display update to complete before proceeding. Important when shutting down the device.

* refactor: allow system applets to lock rendering
Temporarily prevents other applets from rendering.

* feat: boot and shutdown screens

* feat: BluetoothStatus
Adds a meshtastic::Status object which exposes the state of the Bluetooth connection. Intends to allow decoupling of UI code.

* feat: Bluetooth pairing screen

* fix: InkHUD defaults not honored

* fix: random Bluetooth pin for NicheGraphics UIs

* chore: button interrupts tested

* fix: emoji reactions show as blank messages

* fix: autoshow and notification triggered by outgoing message

* feat: save InkHUD data before reboot
Implemented with a new Observable. Previously, config and a few recent messages were saved on shutdown. These were lost if the device rebooted, for example when firmware settings were changed by a client. Now, the InkHUD config and recent messages saved on reboot, the same as during an intentional shutdown.

* feat: imperial distances
Controlled by the config.display.units setting

* fix: hide features which are not yet implemented

* refactor: faster rendering
Previously, only tiles which requested update were re-rendered. Affected tiles had their region blanked before render, pixel by pixel. Benchmarking revealed that it is significantly faster to memset the framebuffer and redraw all tiles.

* refactor: tile ownership
Tiles and Applets now maintain a reciprocal link, which is enforced by asserts. Less confusing than the old situation, where an applet and a tile may disagree on their relationship. Empty tiles are now identified by a nullptr *Applet, instead of by having the placeholderApplet assigned.

* fix: notifications and battery when menu open
Do render notifications in front of menu; don't render battery icon in front of menu.

* fix: simpler defaults
Don't expose new users to multiplexed applets straight away: make them enable the feature for themselves.

* fix: Inputs::TwoButton interrupts, when only one button in use

* fix: ensure display update is complete when ESP32 enters light sleep
Many panels power down automatically, but some require active intervention from us. If light sleep (ESP32) occurs during a display update, these panels could potentially remain powered on, applying voltage the pixels for an extended period of time, and potentially damaging the display.

* fix: honor per-variant user tile limit
Set as the default value for InkHUD::settings.userTiles.maxCount in nicheGraphics.h

* feat: initial InkHUD support for Wireless Paper v1.1 and VM-E213

* refactor: Heard and Recents Applets
Tidier code, significant speed boost. Possibly no noticable change in responsiveness, but rendering now spends much less time blocking execution, which is important for correction functioning of the other firmware components.

* refactor: use a common pio base config
Easier to make any future PlatformIO config changes

* feat: tips
Show information that we think the user might find helpful. Some info shown first boot only. Other info shown when / if relevant.

* fix: text wrapping for '\n'
Previously, the newline was honored, but the adojining word was not printed.

* Decouple ButtonThread from sleep.cpp
Reorganize sleep observables. Don't call ButtonThread methods inside doLightSleep. Instead, handle in class with new lightsleep Observables.

* feat: BluetoothStatus
Adds a meshtastic::Status object which exposes the state of the Bluetooth connection. Intends to allow decoupling of UI code.

* feat: observable for reboot

* refactor: Heltec VM-E290 installDefaultConfig

* fix: random Bluetooth pin for NicheGraphics UIs

* update device-ui: fix touch/crash issue while light sleep

* Collect inkhud

* fix: InkHUD shouldn't nag about timezone (#6040)

* Guard eink drivers w/ MESHTASTIC_INCLUDE_NICHE_GRAPHICS

* Case sensitive perhaps?

* More case-sensitivity instances

* Moar

* RTC

* Yet another case issue!

* Sigh...

* MUI: BT programming mode (#6046)

* allow BT connection with disabled MUI

* Update device-ui

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* MUI: fix nag timeout, disable BT programming mode for native (#6052)

* allow BT connection with disabled MUI

* Update device-ui

* MUI: fix nag timeout default and remove programming mode for native

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>

* remove debuglog leftover

* Wireless Paper: remove stray board_level = extra (#6060)

Makes sure the InkHUD version gets build into the release zip

* Fixed persistence stragglers from NodeDB / Device State divorce (#6059)

* Increase `MAX_THREADS` for InkHUD variants with WiFi (#6064)

* Licensed usage compliance (#6047)

* Prevent psk and legacy admin channel on licensed mode

* Move it

* Consolidate warning strings

* More holes

* Device UI submodule bump

* Prevent licensed users from rebroadcasting unlicensed traffic (#6068)

* Prevent licensed users from rebroadcasting unlicensed traffic

* Added method and enum to make user license status more clear

* MUI: move UI initialization out of main.cpp and adding lightsleep observer + mutex (#6078)

* added device-ui to lightSleep observers for handling graceful sleep; refactoring main.cpp

* bump lib version

* Update device-ui

* unPhone TFT: include into build, enable SD card, increase PSRAM (#6082)

* unPhone-tft: include into build, enable SD card, increase assigned PSRAM

* lib update

* Backup / migrate pub private keys when upgrading to new files in 2.6 (#6096)

* Save a backup of pub/private keys before factory reset

* Fix licensed mode warning

* Unlock spi on else file doesn't exist

* Update device-ui

* Update protos and device-ui

* [create-pull-request] automated change (#6129)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Proto

* [create-pull-request] automated change (#6131)

* Proto update for backup

* [create-pull-request] automated change (#6133)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Update protobufs

* Space

* [create-pull-request] automated change (#6144)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Protos

* [create-pull-request] automated change (#6152)

Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com>

* Updeet

* device-ui lib update

* fix channel OK button

* device-lib update: fix settings panel -> no scrolling

* device-ui lib: last minute update

* defined(SENSECAP_INDICATOR)

* MUI hot-fix pub/priv keys

* MUI hot-fix username dialog

* MUI: BT programming mode button

* Update protobufs

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: GUVWAF <78759985+GUVWAF@users.noreply.github.com>
Co-authored-by: GUVWAF <thijs@havinga.eu>
Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com>
Co-authored-by: Tom Fifield <tom@tomfifield.net>
Co-authored-by: mverch67 <manuel.verch@gmx.de>
Co-authored-by: Manuel <71137295+mverch67@users.noreply.github.com>
Co-authored-by: todd-herbert <herbert.todd@gmail.com>
Co-authored-by: Jason Murray <15822260+scruplelesswizard@users.noreply.github.com>
Co-authored-by: Jason Murray <jason@chaosaffe.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
Co-authored-by: Austin <vidplace7@gmail.com>
Co-authored-by: virgil <virgil.wang.cj@gmail.com>
Co-authored-by: Mark Trevor Birss <markbirss@gmail.com>
Co-authored-by: Kalle Lilja <15094562+ThatKalle@users.noreply.github.com>
Co-authored-by: rcarteraz <robert.l.carter2@gmail.com>
2025-03-01 06:18:33 -06:00
Ben Meadors 0832388482 Remove ATECC crypto chip placeholder code (#5461) 2024-11-27 09:45:54 +01:00
Ben Meadors fe86c40145 Cleanup i2c scan logs and macro to save some bytes and remain consistent (#5455)
* Cleanup i2c scan logs and macro to save some bytes and remain consistent

* Functions are better than macros

* Exclude i2c scan for STM32

* Useless log
2024-11-26 13:59:50 -06:00
DavidandBen Meadors 9456c42fc0 Refactor AccelerometerThread.h (#4831)
* Initial upload

* Tidy up

* Update ICM20948Sensor.cpp

* Update AccelerometerThread.h

* Initial upload

* Tidy up

* Update ICM20948Sensor.cpp

* Update AccelerometerThread.h

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2024-09-25 06:25:31 -05:00
Tom Fifield cf392a4c20 Address some FIXME comments (#4435)
* Address some FIXME comments

These comments have since been addressed by more modern code.
Remove them to reduce the clutter in the codebase.

* Remove 'dumb idea' from SimpleAllocator

4 year old code that was set never to run can probably be safely
deleted.
2024-08-11 07:06:38 -05:00
Ben Meadors d32cdecc06 Pause BLE logging during want_config flow (#4162) 2024-06-22 07:00:48 -05:00
Jonathan Bennett 0852a170a3 Add support for BMX160/RAK12034 compass module (#4021) 2024-06-11 17:47:45 -05:00
Ben Meadors b4a7e78d18 Don't reboot for certain config prefs and make accelerometer thread re-entrant (#3889)
* Don't reboot for certain config prefs and make accelerometer thread re-entrant

* WHOOPS

* Don't reboot for LED heartbeat and button press

* Remove TZ
2024-05-16 17:27:36 -05:00
Jonathan BennettandBen Meadors d47e9bed19 Add multiple SPI devices for Radio, Display, and Touchscreen (#3638)
This changeset gives us the ability to specify a separate SPI device for the LoRa, Display, and Touchscreen. The changes in Portduino also add support for specifying a new SPI speed for each transaction. All together, this means that we can let the Linux OS manage the CS lines, and also get much faster SPI speeds, leading to better framerates.

* Add multiple SPI devices to put Radio, Display, and Touchscreen on each their own

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2024-04-17 14:25:52 -05:00
Jim WhitelawandBen Meadors acc32916c3 Add multiple configuration options for a minimized build (GPS,WiFi,BT,MQTT,Screen). (#3469)
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2024-03-25 06:33:57 -05:00
Gabriele Russo 0bfac7b5f9 Fixes [3074] Heltec Tracker Screen issues + minor fixes (#3213)
* Fix Heltec Tracker Screen issues

Fix Heltec Tracker Screen issues like wrong offsets, display size and screen not shutting down.

Divides board into two different envs for 1.0 and 1.1 version PCB

* Helteck wireless tracker default version V1_1

* rename heltec tracker 1.1 - trunk fmt

rename varian of heltec tracker 1.1 to "heltec tracker" to be retro-compatible.

Trunk formatting.

* Heltec Tracker increase Screen update to 3Hz

Heltec Tracker increase Screen update to 3Hz from 1Hz
2024-02-20 07:27:48 -06:00
Jonathan BennettandBen Meadors 4a867c81c0 Portduino work (#3049)
* Move to Portduino's getMacAddr()

* Add ST7735/S screen support

* Push Raspbian support into native target

* Remove latent pigpio references.

* CardKB defensive programming

* Adds configurable spidev

* Fixes to build on Fedora 40

* ENUMs are not #defines. Pull latest portduino

* Add more configuration options for SPI displays

* Add config.yaml option to set DIO3_TCXO_VOLTAGE

* change tft clear() to fillScreen()
Maintains compatability with ESPI driver.

* Adds TXen and RXen pins to portduino

* Add -c --config options to specify config file

* Fail when a specified config file is unavailable

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2024-01-12 02:00:31 -06:00
Ben Meadors dbac2b1cad Implemented enter (Uf2 usb) DFU mode admin message on NRF52 (#3045)
* Added enter DFU mode admin message

* Trunk
2023-12-27 14:26:40 -06:00
Ben Meadors d14d2c89c3 RTTTL ringtones on T-Deck / T-Watch S3 and potentially more I2S audio enabled devices (#2917)
* WIP

* ESP8266 SAM fun

* I2S audio / ext. notification module

* Remove

* Protos

* Add use_i2s_as_buzzer from protos

* Fixes

* Stuff

* Thing

* Ext. Notification working(ish)

* Remove SAM commented code

* Trunk upgrade

* Trunk

* Fixes

* Slow not fast... :-|

* T-Deck and T-Watch don't use normal buttons

* Stop ext. notification nagging with touchscreen as well

* Add button gpio back for T-Deck, but guard against long-press during ext. notification

* Ext. notification wrap up

* Better place to guard against long-press false positives

* Adjust default gain and guard against non-i2s devices referencing audio-thread

* Simplify guard logic with a boolean

* Supress uninitMemberVar

* Protos merge got out of wack

* Trunk resolution

* Remove extra crap

* Cleanup and thread-interval

* Default to alert message buzzer and add nag timeout

* Formatting
2023-12-12 08:36:37 -06:00
Jonathan Bennett a144d5d6cc Clean up, fix reboot, minimize changes 2023-11-15 20:33:53 -06:00
fc06754e1f Possibly fix #2704 "Heltec Wireless Tracker screen doesn't display anything" (#2749)
* fix #2704


Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Jonathan Bennett <jbennett@incomsystems.biz>
2023-10-04 22:24:25 -05:00
GUVWAF ffcc1a0275 RP2040: Enable ExternalNotification and RangeTest Module, set randomSeed (#2664)
* Enable ExternalNotification (and RangeTest) Module

* Set a random seed at boot
2023-07-29 07:19:58 -05:00
Ben Meadors 0739bc0cea T-Watch S3 Support (#2632)
* T-Watch WIP

* Updates

* Temp

* Update screen spi bus and and backlight en

* Peripherals progress

* Fixes

* Fixes

* Updates

* DRV scaffolding

* Fixed touch-screen driver selection. WIP on DRV haptic feedback

* DRV2605 pmu channel

* Trunk

* Fixes and defaults

* Dropped an s

* Move PMU and turn off screen that way

* Add t-deck and t-watch-s3 to CI and cleanup

* More cleanup
2023-07-22 09:26:54 -05:00
003047baaf Fix various typos (#2607)
* Fix various typos

Found via `codespell -q 3 -L acount,clen,dout`

* Trunk reformatting

---------

Co-authored-by: code8buster <communismisgreat@national.shitposting.agency>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2023-07-14 16:25:20 -05:00
Ben Meadors cd787232ca Use INA for device battery level (#2536)
* WIP

* Continued wip

* We got em

* Voltage sensor base class

* INA voltage

* Log it

* Stacie's mom has got it going on

* Move declaration up

* Last one

* Sneaky little bugger

* Macro guard to avoid calling methods
2023-06-02 06:32:34 -05:00
Thomas Göttgens c1a1b450e3 RadioLib6 support 2023-05-08 14:40:10 +02:00
Ben Meadors 09d48f659e RAK14001 RGB LED support (#2464)
* WIP

* WIP

* Moved it

* More random strobey behavior

* Guard to RAK4630 devices for now

* Oops

* Ship it
2023-05-06 07:17:40 -05:00
Thomas Göttgens 6e685b0a54 Merge branch 'master' into 2264-feature-check-for-low-heap-before-adding-to-nodedb-was-reboot-loop-heap-too-low 2023-03-27 15:31:32 +02:00
Ben Meadors 5cb1f96240 Use accelerometer to wake up screen (#2371)
* WIP accelerometer tinkering

* Debug logging

* RAK LIS3DH sensor

* Deconflict temperature sensor and LIS3DH

* Finishing up

* StateON

* Protobufs

* Default of none

* Formatting
2023-03-23 11:32:04 -05:00
Thomas Göttgens 033e988e6f Merge branch 'master' into 2264-feature-check-for-low-heap-before-adding-to-nodedb-was-reboot-loop-heap-too-low 2023-03-10 19:38:14 +01:00
A. Rager e6d69e2b67 Refactor i2cScan.h To Handle 2 Bus (#2337)
* Break i2cScan out into a set of classes for scanning i2c

* refactor i2cscan addresses to be structs that allow addressing by port + address

* build whoopsies

* trunk fmt

* trunk fmt

* lost some build fixes from the merge

* more cleaning for build safety, RTC behavior
2023-03-08 21:13:46 -06:00
Thomas Göttgens 267db05d69 - G1 Explorer use correct display controller
- adhere UA font in modules
2023-03-08 17:12:25 +01:00