mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-15 16:01:19 +10:00
emdashes begone (#10847)
This commit is contained in:
@@ -153,7 +153,7 @@ extern "C" void logLegacy(const char *level, const char *fmt, ...);
|
||||
#define defaultBLEPin 123456
|
||||
|
||||
#if HAS_ETHERNET && defined(USE_ARDUINO_ETHERNET)
|
||||
#include <Ethernet.h> // arduino-libraries/Ethernet — supports W5500 auto-detect
|
||||
#include <Ethernet.h> // arduino-libraries/Ethernet - supports W5500 auto-detect
|
||||
#elif HAS_ETHERNET && defined(USE_CH390D)
|
||||
#include <ESP32_CH390.h>
|
||||
#elif HAS_ETHERNET && !defined(USE_WS5500)
|
||||
|
||||
@@ -5,7 +5,7 @@ const char *DisplayFormatters::getModemPresetDisplayName(meshtastic_Config_LoRaC
|
||||
bool usePreset)
|
||||
{
|
||||
|
||||
// If use_preset is false, always return "Custom" — callers such as RadioInterface and Channels
|
||||
// If use_preset is false, always return "Custom" - callers such as RadioInterface and Channels
|
||||
// rely on this being a stable literal for channel-name hashing and default-channel detection.
|
||||
if (!usePreset) {
|
||||
return "Custom";
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ using namespace Adafruit_LittleFS_Namespace;
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_NRF54L15)
|
||||
// nRF54L15 — Zephyr LittleFS on 36 KB storage_partition (internal RRAM)
|
||||
// nRF54L15 - Zephyr LittleFS on 36 KB storage_partition (internal RRAM)
|
||||
#include "InternalFileSystem.h"
|
||||
#define FSCom InternalFS
|
||||
#define FSBegin() FSCom.begin()
|
||||
|
||||
+2
-2
@@ -31,7 +31,7 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Internal alias used everywhere in code – do NOT redefine elsewhere.
|
||||
// Internal alias used everywhere in code - do NOT redefine elsewhere.
|
||||
#define MAX_MESSAGES_SAVED MESSAGE_HISTORY_LIMIT
|
||||
|
||||
// Maximum text payload size per message in bytes.
|
||||
@@ -68,7 +68,7 @@ struct StoredMessage {
|
||||
bool isBootRelative; // true = millis()/1000 fallback; false = epoch/RTC absolute
|
||||
AckStatus ackStatus; // Delivery status (only meaningful for our own sent messages)
|
||||
|
||||
// Text storage metadata — rebuilt from flash at boot
|
||||
// Text storage metadata - rebuilt from flash at boot
|
||||
uint16_t textOffset; // Offset into global text pool (valid only after loadFromFlash())
|
||||
uint16_t textLength; // Length of text in bytes
|
||||
|
||||
|
||||
+2
-2
@@ -778,7 +778,7 @@ bool Power::setup()
|
||||
{
|
||||
#ifdef HAS_SGM41562
|
||||
// Initialize the charger early so AnalogBatteryLevel can read charging
|
||||
// state from it. The charger does not provide battery voltage / percent —
|
||||
// state from it. The charger does not provide battery voltage / percent -
|
||||
// those still come from the platform ADC via analogInit() below.
|
||||
initSGM41562(SGM41562_WIRE);
|
||||
#endif
|
||||
@@ -842,7 +842,7 @@ void Power::reboot()
|
||||
// Browser/headless WASM node: no in-process restart. notifyReboot above
|
||||
// already let modules persist; hand off to the host (reboot() ->
|
||||
// location.reload() in a tab, or Module.onReboot() headless). Deliberately
|
||||
// skip the ARCH_PORTDUINO SPI/Wire/Serial teardown below — it would kill the
|
||||
// skip the ARCH_PORTDUINO SPI/Wire/Serial teardown below - it would kill the
|
||||
// radio with no actual restart to follow, leaving a wedged node. Must come
|
||||
// before the ARCH_PORTDUINO arm: the wasm build defines both macros.
|
||||
::reboot();
|
||||
|
||||
+1
-1
@@ -237,7 +237,7 @@ static void serialEnter()
|
||||
{
|
||||
LOG_POWERFSM("State: serialEnter");
|
||||
#ifndef ARCH_NRF52
|
||||
// nRF52 runs BLE on SoftDevice independently of USB serial — no need to disable it.
|
||||
// nRF52 runs BLE on SoftDevice independently of USB serial - no need to disable it.
|
||||
// (Same rationale as nbEnter() which already guards this with #ifdef ARCH_ESP32)
|
||||
setBluetoothEnable(false);
|
||||
#endif
|
||||
|
||||
+10
-10
@@ -581,15 +581,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#endif
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// MESHTASTIC_LOCKDOWN — runtime, client-toggleable hardening (nRF52 only)
|
||||
// MESHTASTIC_LOCKDOWN - runtime, client-toggleable hardening (nRF52 only)
|
||||
//
|
||||
// Lockdown/protect support is opt-in at build time. Builds that need it pass
|
||||
// -DMESHTASTIC_ENABLE_LOCKDOWN=1. When enabled on nRF52 (CC310 hardware
|
||||
// crypto), whether it is ACTIVE is decided entirely at runtime by
|
||||
// EncryptedStorage::isLockdownActive()
|
||||
// (== a passphrase has been provisioned, i.e. /prefs/.dek exists). A device
|
||||
// that has never been provisioned — or that the operator disabled from the
|
||||
// client app — behaves exactly like stock firmware: plaintext storage, no
|
||||
// that has never been provisioned - or that the operator disabled from the
|
||||
// client app - behaves exactly like stock firmware: plaintext storage, no
|
||||
// redaction, normal logging, normal display.
|
||||
//
|
||||
// The operator toggles lockdown from the client app:
|
||||
@@ -597,7 +597,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// firmware generates a DEK, encrypts the stored config, and
|
||||
// authorizes the connection.
|
||||
// on -> off : AdminMessage.lockdown_auth { disable=true } with the
|
||||
// passphrase — decrypts storage back to plaintext and removes
|
||||
// passphrase - decrypts storage back to plaintext and removes
|
||||
// the DEK / token / monotonic-counter / backoff files, then
|
||||
// reboots into normal mode. APPROTECT is the one thing that
|
||||
// does NOT revert (see below).
|
||||
@@ -607,20 +607,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// that genuinely cannot afford the ~tens-of-KB of crypto + access-control code
|
||||
// may also opt out with -DMESHTASTIC_EXCLUDE_LOCKDOWN=1.
|
||||
//
|
||||
// MESHTASTIC_PHONEAPI_ACCESS_CONTROL — per-connection auth + redaction,
|
||||
// MESHTASTIC_PHONEAPI_ACCESS_CONTROL - per-connection auth + redaction,
|
||||
// gated at runtime on isLockdownActive()
|
||||
// MESHTASTIC_ENCRYPTED_STORAGE — AES-128-CTR + HMAC-SHA256 at-rest
|
||||
// MESHTASTIC_ENABLE_APPROTECT — UICR APPROTECT capability. The actual
|
||||
// MESHTASTIC_ENCRYPTED_STORAGE - AES-128-CTR + HMAC-SHA256 at-rest
|
||||
// MESHTASTIC_ENABLE_APPROTECT - UICR APPROTECT capability. The actual
|
||||
// one-way burn happens at runtime, only
|
||||
// once provisioned, only on non-vulnerable
|
||||
// silicon, and is STICKY: disabling
|
||||
// lockdown does NOT (cannot) reverse it.
|
||||
//
|
||||
// DEBUG_MUTE is intentionally NOT coupled to lockdown — a capable-but-off
|
||||
// DEBUG_MUTE is intentionally NOT coupled to lockdown - a capable-but-off
|
||||
// device must log normally. Define DEBUG_MUTE separately for a silent build.
|
||||
//
|
||||
// -DMESHTASTIC_LOCKDOWN_DEBUG=1 keeps the irreversible APPROTECT burn disabled
|
||||
// even when provisioned — for development so dev boards never lose SWD.
|
||||
// even when provisioned - for development so dev boards never lose SWD.
|
||||
// -----------------------------------------------------------------------------
|
||||
#if defined(ARCH_NRF52)
|
||||
#ifndef MESHTASTIC_ENABLE_LOCKDOWN
|
||||
@@ -658,7 +658,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
// Override at build time. Suggested:
|
||||
// carry device: 3600 (1h sessions, periodic re-auth from phone)
|
||||
// tower / infra node: 0 (default — relies on token TTLs only)
|
||||
// tower / infra node: 0 (default - relies on token TTLs only)
|
||||
//
|
||||
// A future LockdownAuth.max_session_seconds proto field will let the
|
||||
// client set this per-token; until that lands the build-time value is
|
||||
|
||||
+1
-1
@@ -1917,7 +1917,7 @@ std::unique_ptr<GPS> GPS::createGps()
|
||||
new_gps->rx_gpio = _rx_gpio;
|
||||
new_gps->tx_gpio = _tx_gpio;
|
||||
#ifdef ARCH_PORTDUINO
|
||||
// Skip chip-specific probing for gpsd — it's a generic NMEA stream.
|
||||
// Skip chip-specific probing for gpsd - it's a generic NMEA stream.
|
||||
if (!portduino_config.gpsd_host.empty())
|
||||
new_gps->gnssModel = GNSS_MODEL_GENERIC_NMEA;
|
||||
#endif
|
||||
|
||||
@@ -20,7 +20,7 @@ void GPSUpdateScheduling::informGotLock()
|
||||
|
||||
// Search finished without obtaining a fix. We still need to mark the end time so
|
||||
// the next sleep is timed correctly, but we must not feed the timeout duration
|
||||
// into predictedMsToGetLock — doing so poisons msUntilNextSearch() and causes
|
||||
// into predictedMsToGetLock - doing so poisons msUntilNextSearch() and causes
|
||||
// down() to fall into GPS_IDLE, leaving the chip awake on subsequent indoor cycles.
|
||||
void GPSUpdateScheduling::informSearchFailed()
|
||||
{
|
||||
|
||||
+11
-11
@@ -2,9 +2,9 @@
|
||||
BaseUI
|
||||
|
||||
Developed and Maintained By:
|
||||
- Ronald Garcia (HarukiToreda) – Lead development and implementation.
|
||||
- JasonP (Xaositek) – Screen layout and icon design, UI improvements and testing.
|
||||
- TonyG (Tropho) – Project management, structural planning, and testing
|
||||
- Ronald Garcia (HarukiToreda) - Lead development and implementation.
|
||||
- JasonP (Xaositek) - Screen layout and icon design, UI improvements and testing.
|
||||
- TonyG (Tropho) - Project management, structural planning, and testing
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -126,7 +126,7 @@ static inline void prepareFrameColorRegions()
|
||||
// "LOCKED" plus battery so the operator can see the device is alive and
|
||||
// charged without leaking any node/channel/message/position content.
|
||||
// Draw the LOCKED frame into the host-side framebuffer. Does NOT commit
|
||||
// to the panel — the caller is responsible for calling display->display()
|
||||
// to the panel - the caller is responsible for calling display->display()
|
||||
// once it has composited any overlays on top. Committing here would cause
|
||||
// visible flicker between "just LOCKED" and "LOCKED + banner overlay" when
|
||||
// the pairing-PIN special-case in updateUiFrame paints the overlay after
|
||||
@@ -166,8 +166,8 @@ static inline void updateUiFrame(OLEDDisplayUi *ui)
|
||||
if (meshtastic_security::shouldRedactDisplay() && screen != nullptr) {
|
||||
OLEDDisplay *display = screen->getDisplayDevice();
|
||||
// Paint LOCKED into the framebuffer WITHOUT committing. We commit
|
||||
// exactly once at the bottom — after any overlay has been composed
|
||||
// on top — so the panel never visibly transitions from "just LOCKED"
|
||||
// exactly once at the bottom - after any overlay has been composed
|
||||
// on top - so the panel never visibly transitions from "just LOCKED"
|
||||
// to "LOCKED + overlay" mid-frame. Committing twice per cycle was
|
||||
// the source of the H13 flicker.
|
||||
drawLockdownLockScreenIntoBuffer(display);
|
||||
@@ -663,7 +663,7 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
|
||||
// 16-50 ms before the next ui->update() lands. Painting the
|
||||
// LOCKED frame now ensures the only thing the operator (or
|
||||
// someone over their shoulder) can see on wake is the redacted
|
||||
// view. Gated on lockdown — non-lockdown builds keep the
|
||||
// view. Gated on lockdown - non-lockdown builds keep the
|
||||
// previous frame as a UX cue that the display is just dimmed.
|
||||
// dispdev is dereferenced unguarded throughout this file (incl.
|
||||
// displayOff() just below), so no null check here.
|
||||
@@ -793,7 +793,7 @@ void Screen::setup()
|
||||
// M20: e-ink panels physically retain the last-rendered image without
|
||||
// power, so a power-cycled lockdown handheld would keep showing
|
||||
// operator-identifying content (position, messages, node info) until
|
||||
// the firmware's first natural refresh — which on e-ink can be seconds
|
||||
// the firmware's first natural refresh - which on e-ink can be seconds
|
||||
// into boot. Force a full refresh to the LOCKED frame here, immediately
|
||||
// after the display is initialised and before any other rendering, so
|
||||
// the persistent pixels are wiped to the redacted view before an
|
||||
@@ -1035,7 +1035,7 @@ int32_t Screen::runOnce()
|
||||
bool suppressRegionOnboard = false;
|
||||
#ifdef MESHTASTIC_LOCKDOWN
|
||||
// While lockdown is active and storage is still locked, config.lora.region
|
||||
// is a deliberate UNSET placeholder — the real region lives in encrypted
|
||||
// is a deliberate UNSET placeholder - the real region lives in encrypted
|
||||
// storage and is restored on unlock (see NodeDB's locked-boot path). Don't
|
||||
// pop the region picker over the lock screen: it would trap input, and the
|
||||
// operator can't set a region until they unlock anyway.
|
||||
@@ -1473,7 +1473,7 @@ void Screen::setFrames(FrameFocus focus)
|
||||
break;
|
||||
|
||||
case FOCUS_PRESERVE:
|
||||
// No more adjustment — force stay on same index
|
||||
// No more adjustment - force stay on same index
|
||||
if (previousFrameCount > fsi.frameCount) {
|
||||
ui->switchToFrame(originalPosition - 1);
|
||||
} else if (previousFrameCount < fsi.frameCount) {
|
||||
@@ -1828,7 +1828,7 @@ void Screen::handleOnPress()
|
||||
void Screen::logFrameChange(const char *reason, uint8_t targetIdx)
|
||||
{
|
||||
// Reverse-map an index to a stable name string keyed off FramePositions
|
||||
// field names — so the pytest harness can assert `name=nodelist_nodes`
|
||||
// field names - so the pytest harness can assert `name=nodelist_nodes`
|
||||
// without caring about how the positions were ordered this boot.
|
||||
const auto &p = framesetInfo.positions;
|
||||
const char *name = "unknown";
|
||||
|
||||
@@ -693,7 +693,7 @@ class Screen : public concurrency::OSThread
|
||||
// so the chatty log doesn't ship in release builds. Enabled via
|
||||
// build_testing_profile(enable_ui_log=True) in mcp-server/userprefs.py.
|
||||
// Member function (not free) because FramesetInfo is a private nested
|
||||
// type — only methods of Screen can reach it.
|
||||
// type - only methods of Screen can reach it.
|
||||
void logFrameChange(const char *reason, uint8_t targetIdx);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -536,7 +536,7 @@ class LGFX : public lgfx::LGFX_Device
|
||||
cfg.memory_width = 240;
|
||||
cfg.memory_height = 320;
|
||||
cfg.offset_x = 0;
|
||||
cfg.offset_y = 0; // No vertical shift needed — panel is top-aligned
|
||||
cfg.offset_y = 0; // No vertical shift needed - panel is top-aligned
|
||||
cfg.offset_rotation = 2; // Rotate 180° to correct upside-down layout
|
||||
#else
|
||||
cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC
|
||||
@@ -1144,8 +1144,8 @@ static LGFX *tft = nullptr;
|
||||
|
||||
#elif defined(VARIANT_DISPLAY_DRIVER)
|
||||
// Board-specific framebuffer backends (class LGFX) can livee in the
|
||||
// variant files — variant_display.h (declaration) and
|
||||
// variant_display.cpp (bodies) — so this shared
|
||||
// variant files - variant_display.h (declaration) and
|
||||
// variant_display.cpp (bodies) - so this shared
|
||||
// file isn't inflated for a single board. It exposes the same surface TFTDisplay
|
||||
// drives, so the generic `tft = new LGFX;` in connect() works.
|
||||
#include "variant_display.h"
|
||||
|
||||
@@ -482,9 +482,9 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
||||
// Weighting for nonlinear segments
|
||||
float milestone1 = 25;
|
||||
float milestone2 = 40;
|
||||
float weight1 = 0.45; // Weight for 0–25%
|
||||
float weight2 = 0.35; // Weight for 25–40%
|
||||
float weight3 = 0.20; // Weight for 40–100%
|
||||
float weight1 = 0.45; // Weight for 0-25%
|
||||
float weight2 = 0.35; // Weight for 25-40%
|
||||
float weight3 = 0.20; // Weight for 40-100%
|
||||
float totalWeight = weight1 + weight2 + weight3;
|
||||
|
||||
int seg1 = chutil_bar_max_fill * (weight1 / totalWeight);
|
||||
|
||||
@@ -182,7 +182,7 @@ static void applyLoraRegion(meshtastic_Config_LoRaConfig_RegionCode region, bool
|
||||
|
||||
// Reconcile the preset with the explicitly chosen region: a preset locked to another
|
||||
// region would leave config.lora invalid until applyModemConfig() repairs it with
|
||||
// error/critical-error side effects — or, for the swappable EU trio, the clamp would
|
||||
// error/critical-error side effects - or, for the swappable EU trio, the clamp would
|
||||
// flip the region right back. The user picked the region, so the preset follows it.
|
||||
const RegionInfo *newRegion = getRegion(region);
|
||||
if (config.lora.use_preset && !newRegion->supportsPreset(config.lora.modem_preset)) {
|
||||
@@ -282,7 +282,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
|
||||
// Guard: without a reboot, reconfigure() applies the region directly, so reject
|
||||
// regions this node can't use up front: unrecognized codes, licensed-only regions,
|
||||
// and radio hardware mismatches (2.4 GHz vs sub-GHz) — the same checks the admin
|
||||
// and radio hardware mismatches (2.4 GHz vs sub-GHz) - the same checks the admin
|
||||
// set-config path applies, but side-effect-free: ignoring a menu selection should
|
||||
// not record a critical error or notify clients. getRadio() used to catch hardware
|
||||
// mismatches post-reboot only.
|
||||
@@ -468,7 +468,7 @@ static constexpr int MAX_PRESET_OPTIONS = 16;
|
||||
|
||||
static BannerOverlayOptions buildRegionPresetBanner()
|
||||
{
|
||||
// Static storage reused each call — safe because the banner is shown immediately after.
|
||||
// Static storage reused each call - safe because the banner is shown immediately after.
|
||||
static const char *optionsArray[MAX_PRESET_OPTIONS];
|
||||
static int optionsEnumArray[MAX_PRESET_OPTIONS];
|
||||
static char presetLabelBuf[MAX_PRESET_OPTIONS][12]; // scratch space for name copies
|
||||
|
||||
@@ -261,7 +261,7 @@ void NotificationRenderer::drawBannercallback(OLEDDisplay *display, OLEDDisplayU
|
||||
case notificationTypeEnum::text_banner:
|
||||
case notificationTypeEnum::selection_picker:
|
||||
case notificationTypeEnum::pairing_pin:
|
||||
// pairing_pin is rendered the same as text_banner — it's just a
|
||||
// pairing_pin is rendered the same as text_banner - it's just a
|
||||
// text banner. The split type exists only so the lockdown UI
|
||||
// short-circuit in Screen.cpp can recognise the BLE pair-PIN
|
||||
// banner as the one safe banner to composite over the LOCKED
|
||||
|
||||
@@ -134,7 +134,7 @@ class SafeFastEPD : public FASTEPD
|
||||
|
||||
void ED047TC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
|
||||
{
|
||||
// Parallel display — SPI parameters are not used
|
||||
// Parallel display - SPI parameters are not used
|
||||
(void)spi;
|
||||
(void)pin_dc;
|
||||
(void)pin_cs;
|
||||
@@ -159,7 +159,7 @@ void ED047TC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_
|
||||
// so variant touch-control polling can read the key reliably.
|
||||
epaper->ioPinMode(10, INPUT);
|
||||
#else
|
||||
#error "ED047TC1 driver: unsupported variant — define T5_S3_EPAPER_PRO_V1 or T5_S3_EPAPER_PRO_V2"
|
||||
#error "ED047TC1 driver: unsupported variant - define T5_S3_EPAPER_PRO_V1 or T5_S3_EPAPER_PRO_V2"
|
||||
#endif
|
||||
|
||||
if (initRc != BBEP_SUCCESS) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
Unlike the other NicheGraphics EInk drivers, this one drives a parallel e-paper
|
||||
panel via the FastEPD library. SPI parameters passed to begin() are ignored.
|
||||
|
||||
The ED047TC1 panel has an inactive pixel border on all four edges (~4–8 physical
|
||||
The ED047TC1 panel has an inactive pixel border on all four edges (~4-8 physical
|
||||
pixels). DISPLAY_WIDTH / DISPLAY_HEIGHT expose a reduced "safe area" to InkHUD so
|
||||
that content is never drawn into this dead zone. The update() method copies the
|
||||
InkHUD frame buffer into the centre of the larger physical 960×540 buffer, using
|
||||
@@ -18,9 +18,9 @@
|
||||
V_OFFSET_TOP and V_OFFSET_BOTTOM (vertical, pixel rows) to position it.
|
||||
|
||||
Changing these constants shifts content inward from each physical edge:
|
||||
H_OFFSET_BYTES = 2 → 16px left margin, 16px right margin (960 – 16 – 16 = 928)
|
||||
H_OFFSET_BYTES = 2 → 16px left margin, 16px right margin (960 - 16 - 16 = 928)
|
||||
V_OFFSET_TOP = 16 → 16px top margin
|
||||
V_OFFSET_BOTTOM = 16 → 16px bottom margin (540 – 16 – 16 = 508)
|
||||
V_OFFSET_BOTTOM = 16 → 16px bottom margin (540 - 16 - 16 = 508)
|
||||
|
||||
*/
|
||||
|
||||
@@ -74,7 +74,7 @@ class ED047TC1 : public EInk
|
||||
public:
|
||||
ED047TC1() : EInk(DISPLAY_WIDTH, DISPLAY_HEIGHT, supported) {}
|
||||
|
||||
// EInk interface — SPI params are not used for this parallel display
|
||||
// EInk interface - SPI params are not used for this parallel display
|
||||
void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = 0xFF) override;
|
||||
void update(uint8_t *imageData, UpdateTypes type) override;
|
||||
|
||||
|
||||
@@ -275,11 +275,11 @@ void InkHUD::MapApplet::drawMapTileBackground(int zoom)
|
||||
|
||||
void InkHUD::MapApplet::onRender(bool full)
|
||||
{
|
||||
// Map center is always the node centroid — tiles are background only.
|
||||
// Map center is always the node centroid - tiles are background only.
|
||||
getMapCenter(&latCenter, &lngCenter);
|
||||
calculateAllMarkers();
|
||||
|
||||
// Show placeholder only if we have no position at all — no tiles, no own node
|
||||
// Show placeholder only if we have no position at all - no tiles, no own node
|
||||
if (!enoughMarkers() && !centerIsOurNode) {
|
||||
printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Node positions", CENTER, MIDDLE);
|
||||
printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE);
|
||||
@@ -324,7 +324,7 @@ void InkHUD::MapApplet::onRender(bool full)
|
||||
float chosenMetersToPx = metersToPxFit; // fallback: fit-scale (may downsample)
|
||||
|
||||
if (s_zoomLocked && s_lockedZoom >= 0) {
|
||||
// Use locked zoom at native 1:1 scale — never zoom out for new nodes
|
||||
// Use locked zoom at native 1:1 scale - never zoom out for new nodes
|
||||
chosenZoom = s_lockedZoom;
|
||||
float mpp = (2.0f * M_PI * R / (256.0f * (float)(1 << chosenZoom))) * cosf(latRad);
|
||||
chosenMetersToPx = 1.0f / mpp;
|
||||
@@ -338,7 +338,7 @@ void InkHUD::MapApplet::onRender(bool full)
|
||||
float mpp = (2.0f * M_PI * R / (256.0f * (float)(1 << zooms[zi]))) * cosf(latRad);
|
||||
float nativeMetersToPx = 1.0f / mpp;
|
||||
if (nativeMetersToPx <= metersToPxFit) {
|
||||
// This zoom at native scale shows all nodes — use it (highest detail that fits)
|
||||
// This zoom at native scale shows all nodes - use it (highest detail that fits)
|
||||
chosenZoom = zooms[zi];
|
||||
chosenMetersToPx = nativeMetersToPx;
|
||||
break;
|
||||
@@ -628,7 +628,7 @@ void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
|
||||
// When zoom is locked, keep center exactly on own node / zero-hop centroid.
|
||||
// Skip bounding-box shift so new distant nodes don't move the zoomed view.
|
||||
if (s_zoomLocked) {
|
||||
// Own node has no position — re-center on zero-hop centroid instead.
|
||||
// Own node has no position - re-center on zero-hop centroid instead.
|
||||
if (!centerIsOurNode) {
|
||||
uint32_t count = 0;
|
||||
float xAvg = 0, yAvg = 0, zAvg = 0;
|
||||
@@ -878,7 +878,7 @@ void InkHUD::MapApplet::calculateAllMarkers()
|
||||
if (node->num == nodeDB->getNodeNum())
|
||||
continue;
|
||||
|
||||
// Skip nodes with unknown hop count — partial info, not useful to plot
|
||||
// Skip nodes with unknown hop count - partial info, not useful to plot
|
||||
if (!node->has_hops_away)
|
||||
continue;
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class MapApplet : public Applet
|
||||
|
||||
MapApplet *asMapApplet() override { return this; } // Identify as MapApplet without RTTI
|
||||
|
||||
// Zoom lock — shared across all MapApplet instances (static)
|
||||
// Zoom lock - shared across all MapApplet instances (static)
|
||||
static constexpr int ZOOM_MAX_NO_TILES = 16;
|
||||
|
||||
void zoomIn();
|
||||
|
||||
@@ -260,7 +260,7 @@ int32_t InkHUD::MenuApplet::runOnce()
|
||||
return OSThread::disable();
|
||||
}
|
||||
|
||||
// Storage for the dynamically-built region preset list — populated in showPage(NODE_CONFIG_PRESET)
|
||||
// Storage for the dynamically-built region preset list - populated in showPage(NODE_CONFIG_PRESET)
|
||||
static constexpr uint8_t MAX_REGION_PRESETS = 16;
|
||||
static meshtastic_Config_LoRaConfig_ModemPreset regionPresets[MAX_REGION_PRESETS];
|
||||
static uint8_t regionPresetCount = 0;
|
||||
@@ -1069,7 +1069,7 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
|
||||
items.push_back(MenuItem("Send", MenuPage::SEND));
|
||||
|
||||
// Map zoom controls — only when viewing a map applet
|
||||
// Map zoom controls - only when viewing a map applet
|
||||
{
|
||||
MapApplet *mapApplet = borrowedTileOwner ? borrowedTileOwner->asMapApplet() : nullptr;
|
||||
if (mapApplet) {
|
||||
|
||||
@@ -190,7 +190,7 @@ ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_Me
|
||||
if (mp.to != NODENUM_BROADCAST)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Store in the global messageStore — this handles sender, timestamp, channel, text, and ack status
|
||||
// Store in the global messageStore - this handles sender, timestamp, channel, text, and ack status
|
||||
messageStore.addFromPacket(mp);
|
||||
|
||||
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
|
||||
|
||||
@@ -260,7 +260,7 @@ If you need to create several similar applets, it might make sense to create a r
|
||||
|
||||
##### Map Tiles
|
||||
|
||||
Map tiles are stored in `MapTile.h` (`src/graphics/niche/InkHUD/Applets/Bases/Map/MapTile.h`). The file committed to the repository contains no tile data by default — the map applets work without tiles, falling back to the original marker-only display.
|
||||
Map tiles are stored in `MapTile.h` (`src/graphics/niche/InkHUD/Applets/Bases/Map/MapTile.h`). The file committed to the repository contains no tile data by default - the map applets work without tiles, falling back to the original marker-only display.
|
||||
|
||||
Tiles are 256×256 pixels, 1-bit (column-major bit packing), compressed per tile with LZ4 to keep flash usage low.
|
||||
|
||||
@@ -489,8 +489,8 @@ We keep this separate latest-message cache for this purpose, because:
|
||||
|
||||
Broadcasts and DMs take different paths into `messageStore`:
|
||||
|
||||
- **Broadcasts** — `ThreadedMessageApplet::handleReceived()` calls `messageStore.addFromPacket()`. `Events::onReceiveTextMessage()` then updates `latestMessage.broadcast` separately for fast access by `AllMessageApplet` and `NotificationApplet`.
|
||||
- **DMs** — `ThreadedMessageApplet` skips DMs entirely. `Events::onReceiveTextMessage()` calls `messageStore.addFromPacket()` directly and stores the result in `latestMessage.dm`.
|
||||
- **Broadcasts** - `ThreadedMessageApplet::handleReceived()` calls `messageStore.addFromPacket()`. `Events::onReceiveTextMessage()` then updates `latestMessage.broadcast` separately for fast access by `AllMessageApplet` and `NotificationApplet`.
|
||||
- **DMs** - `ThreadedMessageApplet` skips DMs entirely. `Events::onReceiveTextMessage()` calls `messageStore.addFromPacket()` directly and stores the result in `latestMessage.dm`.
|
||||
|
||||
#### Saving / Loading
|
||||
|
||||
|
||||
@@ -129,14 +129,14 @@ int InputBroker::handleInputEvent(const InputEvent *event)
|
||||
#ifdef MESHTASTIC_LOCKDOWN
|
||||
// Lockdown: when the display is redacted (storage locked, or screen-lock
|
||||
// latch set after idle) the screen content is hidden, but local input
|
||||
// would otherwise still flow into UI handlers — letting an operator
|
||||
// would otherwise still flow into UI handlers - letting an operator
|
||||
// drive menus, fire canned messages, change settings etc. blind. Eat
|
||||
// the event here so input is no-op until the redaction clears.
|
||||
// The latch is cleared only by unlockScreen() on a successful
|
||||
// passphrase auth (see PhoneAPI::handleLockdownAuthInline) — local
|
||||
// passphrase auth (see PhoneAPI::handleLockdownAuthInline) - local
|
||||
// input does not clear it, even if storage happens to be unlocked.
|
||||
// PowerFSM was already triggered above, so the backlight still wakes
|
||||
// to show the LOCKED frame — the input just doesn't act on anything.
|
||||
// to show the LOCKED frame - the input just doesn't act on anything.
|
||||
if (meshtastic_security::shouldRedactDisplay()) {
|
||||
return 0;
|
||||
}
|
||||
@@ -427,7 +427,7 @@ void InputBroker::Init()
|
||||
}
|
||||
}
|
||||
#ifdef __linux__
|
||||
// Linux evdev keyboard input only — macOS has no <linux/input.h>.
|
||||
// Linux evdev keyboard input only - macOS has no <linux/input.h>.
|
||||
aLinuxInputImpl = new LinuxInputImpl();
|
||||
aLinuxInputImpl->init();
|
||||
#endif
|
||||
|
||||
+15
-15
@@ -400,7 +400,7 @@ void setup()
|
||||
// M23 (audit): APPROTECT engagement moved below fsInit() so we can gate
|
||||
// on EncryptedStorage::isProvisioned(). Engaging on an unprovisioned dev
|
||||
// board permanently locks SWD before the operator has even set a
|
||||
// passphrase — a misconfigured CI build flashed to a developer device
|
||||
// passphrase - a misconfigured CI build flashed to a developer device
|
||||
// would brick its debug port on first boot. Now we only engage when the
|
||||
// device has a DEK file on flash, i.e. the operator has explicitly
|
||||
// committed to lockdown via passphrase provisioning.
|
||||
@@ -436,7 +436,7 @@ void setup()
|
||||
#endif
|
||||
|
||||
// The DEBUG_MUTE "we are muted, FYI" banner spills APP_VERSION / APP_ENV /
|
||||
// APP_REPO out the USB CDC even with logging otherwise suppressed — a free
|
||||
// APP_REPO out the USB CDC even with logging otherwise suppressed - a free
|
||||
// firmware-fingerprinting primitive for an attacker holding the cable.
|
||||
// Under MESHTASTIC_LOCKDOWN we want the device to look uniformly silent
|
||||
// until the operator authenticates, so skip the banner entirely there.
|
||||
@@ -516,9 +516,9 @@ void setup()
|
||||
EncryptedStorage::initLocked();
|
||||
if (!EncryptedStorage::isUnlocked()) {
|
||||
if (!EncryptedStorage::isProvisioned()) {
|
||||
LOG_WARN("Lockdown: Device not provisioned — connect and set a passphrase to unlock storage");
|
||||
LOG_WARN("Lockdown: Device not provisioned - connect and set a passphrase to unlock storage");
|
||||
} else {
|
||||
LOG_WARN("Lockdown: Device locked — connect and provide passphrase to unlock storage");
|
||||
LOG_WARN("Lockdown: Device locked - connect and provide passphrase to unlock storage");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -530,7 +530,7 @@ void setup()
|
||||
// otherwise burn SWD on first boot before the operator has even set a
|
||||
// passphrase, taking the board out of the dev/recovery workflow with
|
||||
// no real security benefit (there's no DEK to protect yet). Once a
|
||||
// DEK file exists, the operator has committed to lockdown — engaging
|
||||
// DEK file exists, the operator has committed to lockdown - engaging
|
||||
// APPROTECT then is the protection they asked for.
|
||||
if (EncryptedStorage::isProvisioned()) {
|
||||
enableAPProtect();
|
||||
@@ -1150,8 +1150,8 @@ uint32_t shutdownAtMsec; // If not zero we will shutdown at this time (used to
|
||||
bool suppressRebootBanner; // If true, suppress "Rebooting..." overlay (used for OTA handoff)
|
||||
|
||||
#if defined(MESHTASTIC_ENCRYPTED_STORAGE) && defined(MESHTASTIC_PHONEAPI_ACCESS_CONTROL)
|
||||
volatile bool lockdownReloadPending; // see main.h — deferred NodeDB reload after lockdown unlock
|
||||
volatile bool lockdownDisablePending; // see main.h — deferred decrypt-revert after lockdown disable
|
||||
volatile bool lockdownReloadPending; // see main.h - deferred NodeDB reload after lockdown unlock
|
||||
volatile bool lockdownDisablePending; // see main.h - deferred decrypt-revert after lockdown disable
|
||||
#endif
|
||||
|
||||
// If a thread does something that might need for it to be rescheduled ASAP it can set this flag
|
||||
@@ -1241,7 +1241,7 @@ void loop()
|
||||
#if defined(MESHTASTIC_ENCRYPTED_STORAGE) && defined(MESHTASTIC_PHONEAPI_ACCESS_CONTROL)
|
||||
if (lockdownDisablePending) {
|
||||
lockdownDisablePending = false;
|
||||
LOG_INFO("Lockdown: disabling — reverting encrypted storage to plaintext");
|
||||
LOG_INFO("Lockdown: disabling - reverting encrypted storage to plaintext");
|
||||
if (nodeDB->disableLockdownToPlaintext()) {
|
||||
LOG_INFO("Lockdown: disabled, rebooting into normal mode");
|
||||
PhoneAPI::broadcastLockdownStatus(meshtastic_LockdownStatus_State_DISABLED, "", 0, 0, 0);
|
||||
@@ -1251,7 +1251,7 @@ void loop()
|
||||
// The DEK file is still present (it's deleted last), so the device
|
||||
// stays in lockdown and the operator can retry disable. Surface
|
||||
// the failure rather than leaving the client hanging.
|
||||
LOG_ERROR("Lockdown: disable revert failed — device remains in lockdown");
|
||||
LOG_ERROR("Lockdown: disable revert failed - device remains in lockdown");
|
||||
PhoneAPI::broadcastLockdownStatus(meshtastic_LockdownStatus_State_LOCKED, "disable_failed", 0, 0, 0);
|
||||
}
|
||||
}
|
||||
@@ -1263,20 +1263,20 @@ void loop()
|
||||
if (!reloadOk) {
|
||||
// Storage decrypt/decode failed during reload. Treat as
|
||||
// unrecoverable for this boot: lock storage, revoke any
|
||||
// auth that managed to slip through (defense in depth — the
|
||||
// auth that managed to slip through (defense in depth - the
|
||||
// cold-unlock path doesn't authorize until completion, but
|
||||
// a concurrent re-verify-path call from another connection
|
||||
// might have), and notify clients. Storage will be locked
|
||||
// on next boot anyway; deferring to the user-visible
|
||||
// notification path is sufficient for now.
|
||||
LOG_ERROR("Lockdown: reload failed — locking and notifying clients");
|
||||
LOG_ERROR("Lockdown: reload failed - locking and notifying clients");
|
||||
EncryptedStorage::lockNow();
|
||||
PhoneAPI::revokeAllAuth();
|
||||
}
|
||||
PhoneAPI::completePendingUnlocks(reloadOk);
|
||||
}
|
||||
|
||||
// Periodic session-expiry check. Cheap — millis() comparison. Don't
|
||||
// Periodic session-expiry check. Cheap - millis() comparison. Don't
|
||||
// hammer it every loop tick; once a second is plenty.
|
||||
static uint32_t lastSessionCheckMs = 0;
|
||||
if (millis() - lastSessionCheckMs > 1000) {
|
||||
@@ -1286,10 +1286,10 @@ void loop()
|
||||
// 1. Budget remains (bootsRemaining > 0): decrement the
|
||||
// on-flash boot count in place, revoke per-connection
|
||||
// auth, re-engage screen redaction, re-arm the uptime
|
||||
// timer — all WITHOUT rebooting. Storage stays unlocked
|
||||
// timer - all WITHOUT rebooting. Storage stays unlocked
|
||||
// so the mesh keeps routing. Clients must re-authenticate
|
||||
// to see content again. The decrement is what enforces
|
||||
// the rollback ceiling — bootsRemaining ticks down
|
||||
// the rollback ceiling - bootsRemaining ticks down
|
||||
// monotonically whether the device reboots or not.
|
||||
// 2. Budget exhausted (bootsRemaining == 0): no more
|
||||
// sessions to grant. Hard lock (token deleted, DEK
|
||||
@@ -1338,7 +1338,7 @@ void loop()
|
||||
RadioLibInterface::instance->pollMissedIrqs();
|
||||
}
|
||||
|
||||
// Periodic AGC reset — warm sleep + recalibrate to prevent stuck AGC gain
|
||||
// Periodic AGC reset - warm sleep + recalibrate to prevent stuck AGC gain
|
||||
static uint32_t lastAgcReset;
|
||||
if (!Throttle::isWithinTimespanMs(lastAgcReset, AGC_RESET_INTERVAL_MS)) {
|
||||
lastAgcReset = millis();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// User-provided mbedTLS config — pulled in AFTER the default mbedtls_config.h
|
||||
// User-provided mbedTLS config - pulled in AFTER the default mbedtls_config.h
|
||||
// via -DMBEDTLS_USER_CONFIG_FILE in the variant's platformio.ini.
|
||||
//
|
||||
// We compile mbedtls source files straight out of pico-sdk on bare metal, so
|
||||
@@ -7,13 +7,13 @@
|
||||
// abort with #error or #include <sys/socket.h>.
|
||||
//
|
||||
// Code paths that touch these symbols are gated by the same MBEDTLS_* macros,
|
||||
// so the linker simply drops the unreachable branches — no manual file
|
||||
// so the linker simply drops the unreachable branches - no manual file
|
||||
// exclusion in the build script.
|
||||
|
||||
#pragma once
|
||||
|
||||
// Entropy: entropy_poll.c does a hard `#error` on non-POSIX/non-Windows
|
||||
// platforms. Tell it to skip the platform-specific entropy plumbing — our
|
||||
// platforms. Tell it to skip the platform-specific entropy plumbing - our
|
||||
// cert module passes a custom f_rng (picoRand → get_rand_64) directly into
|
||||
// every mbedtls call that needs randomness, so we never invoke entropy_poll.
|
||||
#define MBEDTLS_NO_PLATFORM_ENTROPY
|
||||
@@ -45,7 +45,7 @@
|
||||
// Chrome's ClientHello carries TLS 1.3 extensions (post-quantum key
|
||||
// shares, Encrypted ClientHello, etc.) that mbedtls tries to *parse*
|
||||
// during the initial ClientHello processing before deciding to
|
||||
// downgrade — and that parse crashes the board (no handshake state log
|
||||
// downgrade - and that parse crashes the board (no handshake state log
|
||||
// ever fires, the crash is inside the first mbedtls_ssl_handshake()
|
||||
// call). Removing the 1.3 code from the build sidesteps the parsers
|
||||
// entirely; mbedtls will tell Chrome "TLS 1.2 only" via the
|
||||
|
||||
@@ -443,7 +443,7 @@ bool Channels::usesPublicKey(ChannelIndex chIndex)
|
||||
bool Channels::isWellKnownChannel(ChannelIndex chIndex)
|
||||
{
|
||||
const auto &ch = getByIndex(chIndex);
|
||||
// Absent (unencrypted) or single-byte PSK — all the well-known key indexes
|
||||
// Absent (unencrypted) or single-byte PSK - all the well-known key indexes
|
||||
if (ch.settings.psk.size > 1)
|
||||
return false;
|
||||
|
||||
@@ -451,7 +451,7 @@ bool Channels::isWellKnownChannel(ChannelIndex chIndex)
|
||||
for (int p = _meshtastic_Config_LoRaConfig_ModemPreset_MIN; p <= _meshtastic_Config_LoRaConfig_ModemPreset_MAX; p++) {
|
||||
const char *presetName =
|
||||
DisplayFormatters::getModemPresetDisplayName(static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(p), false, true);
|
||||
// Presets without a display name fall through to "Invalid" — never a match
|
||||
// Presets without a display name fall through to "Invalid" - never a match
|
||||
if (strcmp(presetName, "Invalid") != 0 && strcmp(name, presetName) == 0)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ uint32_t Default::getConfiguredOrDefaultMsScaled(uint32_t configured, uint32_t d
|
||||
int8_t throttle =
|
||||
(type == TrafficType::POSITION) ? myRegion->profile->positionThrottle : myRegion->profile->telemetryThrottle;
|
||||
|
||||
// throttle <= 0 means unset; 1 is the neutral multiplier — skip the multiply for performance
|
||||
// throttle <= 0 means unset; 1 is the neutral multiplier - skip the multiply for performance
|
||||
if (throttle <= 1)
|
||||
return baseMs;
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ bool fill(uint8_t *buffer, size_t length, bool useRadioEntropy)
|
||||
filled = true;
|
||||
}
|
||||
#elif defined(__EMSCRIPTEN__)
|
||||
// Browser/wasm: no getrandom/arc4random — fall through to std::random_device,
|
||||
// Browser/wasm: no getrandom/arc4random - fall through to std::random_device,
|
||||
// which emscripten backs with crypto.getRandomValues().
|
||||
#else
|
||||
// arc4random_buf is available on Darwin/BSD and cannot fail.
|
||||
|
||||
@@ -205,7 +205,7 @@ template <typename T> bool LR11x0Interface<T>::reconfigure()
|
||||
err = lora.setOutputPower(power);
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
|
||||
// Apply RX gain mode — valid in STDBY, matches resetAGC() pattern
|
||||
// Apply RX gain mode - valid in STDBY, matches resetAGC() pattern
|
||||
err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
LOG_WARN("LR11x0 setRxBoostedGainMode %s%d", radioLibErr, err);
|
||||
@@ -326,7 +326,7 @@ template <typename T> void LR11x0Interface<T>::resetAGC()
|
||||
|
||||
LOG_DEBUG("LR11x0 AGC reset: warm sleep + Calibrate(0x3F)");
|
||||
|
||||
// 1. Warm sleep — powers down the analog frontend, resetting AGC state
|
||||
// 1. Warm sleep - powers down the analog frontend, resetting AGC state
|
||||
lora.sleep(true, 0);
|
||||
|
||||
// 2. Wake to RC standby for stable calibration
|
||||
|
||||
@@ -211,7 +211,7 @@ template <typename T> bool LR20x0Interface<T>::reconfigure()
|
||||
err = lora.setOutputPower(power);
|
||||
assert(err == RADIOLIB_ERR_NONE);
|
||||
|
||||
// Apply RX gain mode — valid in STDBY, matches resetAGC() pattern
|
||||
// Apply RX gain mode - valid in STDBY, matches resetAGC() pattern
|
||||
err = lora.setRxBoostedGainMode(config.lora.sx126x_rx_boosted_gain);
|
||||
if (err != RADIOLIB_ERR_NONE)
|
||||
LOG_WARN("LR20x0 setRxBoostedGainMode %s%d", radioLibErr, err);
|
||||
@@ -332,7 +332,7 @@ template <typename T> void LR20x0Interface<T>::resetAGC()
|
||||
|
||||
LOG_DEBUG("LR20x0 AGC reset: warm sleep + Calibrate(0x3F)");
|
||||
|
||||
// 1. Warm sleep — powers down the analog frontend, resetting AGC state
|
||||
// 1. Warm sleep - powers down the analog frontend, resetting AGC state
|
||||
lora.sleep(true, 0);
|
||||
|
||||
// 2. Wake to RC standby for stable calibration
|
||||
|
||||
@@ -100,7 +100,7 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
|
||||
meshtastic_NodeInfoLite *origTx = nodeDB->getMeshNode(p->from);
|
||||
// Either relayer of ACK was also a relayer of the packet, or we were the *only* relayer and the ACK came
|
||||
// directly from the destination. checkRelayers is read-only on PacketHistory and O(1), so we run it even
|
||||
// when origTx is absent — that lets us still capture the confirmed hop into the TMM overflow cache below.
|
||||
// when origTx is absent - that lets us still capture the confirmed hop into the TMM overflow cache below.
|
||||
// Single lookup for both relayer checks on the same (request_id, to) pair
|
||||
bool wasAlreadyRelayer = false;
|
||||
bool weWereSoleRelayer = false;
|
||||
@@ -110,7 +110,7 @@ void NextHopRouter::sniffReceived(const meshtastic_MeshPacket *p, const meshtast
|
||||
if ((weWereRelayer && wasAlreadyRelayer) || (getHopsAway(*p) == 0 && weWereSoleRelayer)) {
|
||||
// M1/M2: only learn a next hop whose last byte maps to a single plausible relay. On a dense
|
||||
// mesh the byte may be ambiguous; storing it would aim future DMs at the wrong node. This gate
|
||||
// now protects BOTH the hot-store route (NodeInfoLite.next_hop) AND the TMM overflow cache —
|
||||
// now protects BOTH the hot-store route (NodeInfoLite.next_hop) AND the TMM overflow cache -
|
||||
// the overflow cache deliberately holds many more next-hop bytes (long-tail nodes), so it is
|
||||
// even more collision-prone and must never store an ambiguous byte either. Ambiguous/unknown
|
||||
// -> store nothing and keep flooding (safe).
|
||||
@@ -162,7 +162,7 @@ bool NextHopRouter::perhapsRebroadcast(const meshtastic_MeshPacket *p)
|
||||
if (p->id != 0) {
|
||||
if (isRebroadcaster()) {
|
||||
// NOTE: this is a self-identity match (is the addressed next_hop OUR last byte?), so it
|
||||
// cannot be hardened with resolveLastByte() — a remote node that legitimately shares our
|
||||
// cannot be hardened with resolveLastByte() - a remote node that legitimately shares our
|
||||
// last byte will also match here and rebroadcast. That residual collision needs a wider
|
||||
// on-wire field to fix. M1/M2 instead shrink the blast radius by reducing how often an
|
||||
// ambiguous next_hop byte is ever learned (sniffReceived) or originated (getNextHop).
|
||||
@@ -382,7 +382,7 @@ int32_t NextHopRouter::doRetransmissions()
|
||||
if (!isBroadcast(p.packet->to)) {
|
||||
if (p.numRetransmissions == 1) {
|
||||
// Last retransmission: this directed delivery went un-ACKed. Record the failure
|
||||
// (M3 — accumulates across DMs to age out a flapping/dead route) and reset
|
||||
// (M3 - accumulates across DMs to age out a flapping/dead route) and reset
|
||||
// next_hop so the final try falls back to FloodingRouter.
|
||||
noteRouteFailure(p.packet->to);
|
||||
p.packet->next_hop = NO_NEXT_HOP_PREFERENCE;
|
||||
@@ -401,7 +401,7 @@ int32_t NextHopRouter::doRetransmissions()
|
||||
} else {
|
||||
#if NEXTHOP_EARLY_FLOOD_ON_UNVERIFIED
|
||||
// M4 (gated): if the route isn't proven healthy, don't spend a second directed
|
||||
// attempt — start flooding one retry sooner to cut recovery latency. A verified
|
||||
// attempt - start flooding one retry sooner to cut recovery latency. A verified
|
||||
// route (fresh, zero recent failures) keeps the unchanged directed-retry path so
|
||||
// the sparse-mesh happy path is untouched.
|
||||
RouteHealth *h = findRouteHealth(p.packet->to);
|
||||
|
||||
+25
-25
@@ -723,7 +723,7 @@ template <typename Map> std::vector<NodeNum> snapshotSatelliteNodeNums(const Map
|
||||
}
|
||||
|
||||
// Drop the stalest entry of `map` (staleness proxied via the owner's
|
||||
// last_heard; 0 = owner evicted, i.e. an orphan — first out). Never evicts our
|
||||
// last_heard; 0 = owner evicted, i.e. an orphan - first out). Never evicts our
|
||||
// own node's entry. Caller holds satelliteMutex. Returns false if nothing
|
||||
// could be evicted.
|
||||
template <typename Map> bool evictStalestSatellite(NodeDB &db, Map &map)
|
||||
@@ -1797,7 +1797,7 @@ bool NodeDB::enforceSatelliteCaps()
|
||||
|
||||
#if WARM_NODE_COUNT > 0
|
||||
// Classify an evicted node's hop-protected category for the warm tier. Favorite/ignored/
|
||||
// verified are local flags (rarely reach warm — they're eviction-protected — but classify
|
||||
// verified are local flags (rarely reach warm - they're eviction-protected - but classify
|
||||
// them if they do); otherwise tracker/sensor/tak_tracker are role-protected.
|
||||
static uint8_t warmProtectedCategory(const meshtastic_NodeInfoLite &n)
|
||||
{
|
||||
@@ -1940,7 +1940,7 @@ LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t
|
||||
// check if the file is encrypted and decrypt before protobuf decode
|
||||
if (EncryptedStorage::isEncrypted(filename)) {
|
||||
// ZeroizingArrayPtr wipes the decrypted plaintext (which contains config
|
||||
// secrets — channel PSKs, security private_key, etc.) before delete[],
|
||||
// secrets - channel PSKs, security private_key, etc.) before delete[],
|
||||
// so it isn't recoverable from the heap after this function returns.
|
||||
auto decBuf = meshtastic_security::make_zeroizing_array(protoSize);
|
||||
if (!decBuf) {
|
||||
@@ -2028,7 +2028,7 @@ void NodeDB::demoteOldestHotNodesToWarm()
|
||||
warmStore.absorb(n.num, n.last_heard, n.public_key.size > 0 ? n.public_key.bytes : nullptr, n.role,
|
||||
warmProtectedCategory(n));
|
||||
// Demotion drops the node from the header table, so drop its satellites
|
||||
// too (the eviction chokepoint) — they'd otherwise orphan until the next
|
||||
// too (the eviction chokepoint) - they'd otherwise orphan until the next
|
||||
// enforceSatelliteCaps pass.
|
||||
eraseNodeSatellites(n.num);
|
||||
demoted++;
|
||||
@@ -2079,7 +2079,7 @@ void NodeDB::nodeDBSelfCare()
|
||||
|
||||
const bool satsTrimmed = enforceSatelliteCaps();
|
||||
|
||||
// Ensure self exists, sits at index 0, and carries current owner info — after
|
||||
// Ensure self exists, sits at index 0, and carries current owner info - after
|
||||
// any demotion has freed a slot. Covers the foreign/fixture case where the
|
||||
// loaded file did not contain us at all.
|
||||
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(self);
|
||||
@@ -2090,7 +2090,7 @@ void NodeDB::nodeDBSelfCare()
|
||||
}
|
||||
|
||||
// One-shot rewrite: only when we healed something, and never while storage
|
||||
// is locked — a locked boot loads placeholder defaults that must not be written
|
||||
// is locked - a locked boot loads placeholder defaults that must not be written
|
||||
// over the encrypted store; reloadFromDisk() re-runs self-care once unlocked.
|
||||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||||
const bool storageLocked = EncryptedStorage::isLockdownActive() && !EncryptedStorage::isUnlocked();
|
||||
@@ -2171,7 +2171,7 @@ void NodeDB::loadFromDisk()
|
||||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||||
// Only take the locked-boot defaults path when lockdown is ACTIVE (the
|
||||
// device is provisioned) AND storage is still locked. A lockdown-capable
|
||||
// build that has never been provisioned — or that was disabled — falls
|
||||
// build that has never been provisioned - or that was disabled - falls
|
||||
// through to the normal plaintext load below and behaves like stock.
|
||||
if (EncryptedStorage::isLockdownActive() && !EncryptedStorage::isUnlocked()) {
|
||||
// Encrypted storage is locked. Install defaults and wait for the
|
||||
@@ -2188,7 +2188,7 @@ void NodeDB::loadFromDisk()
|
||||
// would otherwise honour USERPREFS_CONFIG_LORA_REGION (the common shape
|
||||
// for managed deployments) and the LongFast default channel synthesised
|
||||
// by installDefaultChannels, so the device would beacon nodeinfo /
|
||||
// telemetry on the public default PSK before any unlock — and process
|
||||
// telemetry on the public default PSK before any unlock - and process
|
||||
// incoming default-channel packets the same way. Forcing region=UNSET
|
||||
// gates both TX and RX in RadioLibInterface (see the region==UNSET
|
||||
// checks in startSend and readData); tx_enabled=false is belt-and-
|
||||
@@ -2262,7 +2262,7 @@ void NodeDB::loadFromDisk()
|
||||
|
||||
// Left UNTRIMMED on purpose: trim/demote/satellite-cap/self-pin/rewrite all
|
||||
// run in nodeDBSelfCare() once getNodeNum() is valid (still 0 here on a cold
|
||||
// boot, so we could only assume index 0 == self — the very bug being fixed).
|
||||
// boot, so we could only assume index 0 == self - the very bug being fixed).
|
||||
#if WARM_NODE_COUNT > 0
|
||||
// Load the warm tier so its on-disk snapshot is available before the node DB
|
||||
// is exercised (and before nodeDBSelfCare() demotes any overflow into it).
|
||||
@@ -2445,7 +2445,7 @@ void NodeDB::loadFromDisk()
|
||||
}
|
||||
|
||||
// Always-on traffic management: a device that has NEVER configured TMM
|
||||
// (has_traffic_management false — AdminModule always sets the has_ flag on
|
||||
// (has_traffic_management false - AdminModule always sets the has_ flag on
|
||||
// write, even when disabling) gets the fork defaults. Explicitly configured
|
||||
// devices keep their exact settings.
|
||||
if (!moduleConfig.has_traffic_management) {
|
||||
@@ -2479,7 +2479,7 @@ void NodeDB::loadFromDisk()
|
||||
// without saving to disk, so we force a save here to ensure encrypted files exist.
|
||||
//
|
||||
// Only when lockdown is ACTIVE. A capable-but-off device must leave its
|
||||
// files as plaintext — encryptAndWrite would fail anyway (no DEK), but
|
||||
// files as plaintext - encryptAndWrite would fail anyway (no DEK), but
|
||||
// skipping the whole block avoids the wasted attempts and error logs.
|
||||
if (EncryptedStorage::isLockdownActive()) {
|
||||
const char *filesToCheck[] = {configFileName, moduleConfigFileName, channelFileName, deviceStateFileName,
|
||||
@@ -2548,7 +2548,7 @@ void NodeDB::loadFromDisk()
|
||||
// Serializes reloadFromDisk against itself. Other readers of config /
|
||||
// channelFile / nodeDatabase don't take this lock today, so this only
|
||||
// prevents reload-vs-reload races (e.g. fast successive unlocks). It is
|
||||
// not a full data-race fix for those structs — that would require
|
||||
// not a full data-race fix for those structs - that would require
|
||||
// thread-shared locking discipline across the whole codebase, beyond
|
||||
// the audit's M7 scope. The radio standby+reconfigure below keeps the
|
||||
// radio out of the window where SX12xx registers are mid-swap.
|
||||
@@ -2561,7 +2561,7 @@ static concurrency::Lock g_reloadFromDiskMutex;
|
||||
* reconfigure() to push the now-real settings to the chip.
|
||||
*
|
||||
* Returns true iff every encrypted file decrypted and decoded cleanly.
|
||||
* On false the caller MUST treat storage as corrupt — see header.
|
||||
* On false the caller MUST treat storage as corrupt - see header.
|
||||
*/
|
||||
bool NodeDB::reloadFromDisk()
|
||||
{
|
||||
@@ -2580,7 +2580,7 @@ bool NodeDB::reloadFromDisk()
|
||||
loadFromDisk();
|
||||
|
||||
if (storageCorruptThisLoad) {
|
||||
LOG_ERROR("NodeDB: storage decrypt/decode failed during reload — surfacing as corrupt");
|
||||
LOG_ERROR("NodeDB: storage decrypt/decode failed during reload - surfacing as corrupt");
|
||||
// Leave the radio sleeping. Caller will lock storage and emit
|
||||
// a LOCKED(storage_corrupt) status; we must not reconfigure
|
||||
// the chip with the locked-default placeholder values still
|
||||
@@ -2629,7 +2629,7 @@ bool NodeDB::disableLockdownToPlaintext()
|
||||
}
|
||||
}
|
||||
|
||||
// All files are plaintext now — remove the lockdown artifacts. Deleting
|
||||
// All files are plaintext now - remove the lockdown artifacts. Deleting
|
||||
// /prefs/.dek is the atomic commit: after it, isLockdownActive() is false.
|
||||
EncryptedStorage::removeLockdownArtifacts();
|
||||
return true;
|
||||
@@ -2652,11 +2652,11 @@ bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_
|
||||
// Encrypt all files except uiconfig (no secrets) and the DEK file (self-encrypted).
|
||||
// Only when lockdown is ACTIVE (provisioned). A lockdown-capable but DISABLED
|
||||
// device has no DEK, so encryptAndWrite would fail and config would never
|
||||
// persist — it must save plaintext exactly like stock firmware. Once enabled,
|
||||
// persist - it must save plaintext exactly like stock firmware. Once enabled,
|
||||
// the reloadFromDisk migrate pass re-saves these plaintext files encrypted.
|
||||
if (EncryptedStorage::isLockdownActive() && strcmp(filename, uiconfigFileName) != 0) {
|
||||
// ZeroizingArrayPtr wipes the unencrypted protobuf encoding (which contains
|
||||
// config secrets — channel PSKs, security private_key, etc.) before delete[],
|
||||
// config secrets - channel PSKs, security private_key, etc.) before delete[],
|
||||
// so plaintext copies aren't left in heap memory after encryption completes.
|
||||
auto pbBuf = meshtastic_security::make_zeroizing_array(protoSize);
|
||||
if (!pbBuf) {
|
||||
@@ -2853,7 +2853,7 @@ bool NodeDB::saveNodeDatabaseToDisk()
|
||||
// reset the 8s HW watchdog so the second write gets a full budget (issue #10746).
|
||||
watchdog_update();
|
||||
#endif
|
||||
// Same cadence as the node DB; failure is logged but must not propagate —
|
||||
// Same cadence as the node DB; failure is logged but must not propagate -
|
||||
// a false return from here would trigger saveToDisk()'s fsFormat() path.
|
||||
warmStore.saveIfDirty();
|
||||
#endif
|
||||
@@ -2874,14 +2874,14 @@ bool NodeDB::saveToDiskNoRetry(int saveWhat)
|
||||
// When lockdown is ACTIVE but storage is still locked, encryptAndWrite()
|
||||
// returns false for every file. That would cause saveToDisk()'s nRF52 retry
|
||||
// path to call FSCom.format(), wiping all encrypted proto files from flash.
|
||||
// Return true here — "nothing to save, not an error."
|
||||
// Return true here - "nothing to save, not an error."
|
||||
//
|
||||
// Gate on isLockdownActive(): a lockdown-capable but DISABLED device (never
|
||||
// provisioned) also has isUnlocked()==false, but it must persist plaintext
|
||||
// normally — skipping here would silently drop every config write (e.g. the
|
||||
// normally - skipping here would silently drop every config write (e.g. the
|
||||
// LoRa region) until the device is provisioned.
|
||||
if (EncryptedStorage::isLockdownActive() && !EncryptedStorage::isUnlocked()) {
|
||||
LOG_WARN("NodeDB: saveToDisk skipped — encrypted storage locked");
|
||||
LOG_WARN("NodeDB: saveToDisk skipped - encrypted storage locked");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
@@ -3009,7 +3009,7 @@ HopStartStatus classifyHopStart(const meshtastic_MeshPacket &p)
|
||||
return HopStartStatus::INVALID;
|
||||
|
||||
if (p.hop_start == 0) {
|
||||
// hop_start == hop_limit == 0: intentional zero-hop broadcast (e.g. beacon). Valid by definition —
|
||||
// hop_start == hop_limit == 0: intentional zero-hop broadcast (e.g. beacon). Valid by definition -
|
||||
// the packet was never meant to travel any hops, so no hop_start ambiguity applies.
|
||||
if (p.hop_limit == 0)
|
||||
return HopStartStatus::VALID;
|
||||
@@ -3204,7 +3204,7 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
||||
TypeConversions::CopyUserToNodeInfoLite(info, contact.user);
|
||||
if (contact.should_ignore) {
|
||||
// Block the contact and drop its rich satellite data, but keep the
|
||||
// public key copied above — an ignored peer keeps a usable identity
|
||||
// public key copied above - an ignored peer keeps a usable identity
|
||||
// (a verifiable target) rather than a bare node number.
|
||||
if (!setProtectedFlag(info, NODEINFO_BITFIELD_IS_IGNORED_MASK, true))
|
||||
LOG_WARN(PROTECTED_CAP_WARN_FMT, "ignore", contact.node_num, MAX_NUM_NODES - 2);
|
||||
@@ -3355,7 +3355,7 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
||||
#if HAS_VARIABLE_HOPS
|
||||
// Only sample genuine RF-origin packets. The transport check excludes packets received
|
||||
// directly from the broker (TRANSPORT_MQTT), but an MQTT-origin packet rebroadcast onto
|
||||
// LoRa by a gateway arrives as TRANSPORT_LORA with via_mqtt set — count those would
|
||||
// LoRa by a gateway arrives as TRANSPORT_LORA with via_mqtt set - count those would
|
||||
// inflate the local mesh-size estimate with non-RF nodes (and they usually carry
|
||||
// hop_start==0, landing in the hop-0 bucket that pulls the recommendation lowest), so
|
||||
// exclude via_mqtt too.
|
||||
@@ -3706,7 +3706,7 @@ meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
||||
// Don't append past the end of the vector. The protected-node cap
|
||||
// (numProtectedNodes() <= MAX_NUM_NODES-2) means the eviction above frees
|
||||
// a slot in normal operation; this guards the legacy case of a pre-cap
|
||||
// database that is full of protected nodes — refuse rather than overrun.
|
||||
// database that is full of protected nodes - refuse rather than overrun.
|
||||
if (numMeshNodes >= MAX_NUM_NODES)
|
||||
return NULL;
|
||||
// Pre-size before append when run before nodeDBSelfCare() (boot keygen); else at() aborts on nRF52.
|
||||
|
||||
+6
-6
@@ -337,11 +337,11 @@ class NodeDB
|
||||
WarmNodeStore warmStore;
|
||||
#endif
|
||||
|
||||
/// Copy the 32-byte public key for node n — hot store first, then the warm
|
||||
/// Copy the 32-byte public key for node n - hot store first, then the warm
|
||||
/// tier. Returns false if we don't know a key for n.
|
||||
bool copyPublicKey(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out);
|
||||
|
||||
/// Resolve a node's device role — hot store (with user) first, then the role
|
||||
/// Resolve a node's device role - hot store (with user) first, then the role
|
||||
/// cached in the warm tier, else CLIENT. Lets role-aware policy keep firing for
|
||||
/// nodes that have aged out of the hot store.
|
||||
meshtastic_Config_DeviceConfig_Role getNodeRole(NodeNum n);
|
||||
@@ -412,7 +412,7 @@ class NodeDB
|
||||
emptyNodeDatabase.version = DEVICESTATE_CUR_VER;
|
||||
size_t nodeDatabaseSize;
|
||||
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &emptyNodeDatabase);
|
||||
// Decode-stream size ceiling only — no buffer this big is allocated (load
|
||||
// Decode-stream size ceiling only - no buffer this big is allocated (load
|
||||
// streams from the file). Sized for the largest file any prior firmware
|
||||
// could write (250-node ESP32-S3, satellites uncapped) so capacity
|
||||
// downgrades / peer backups still decode; excess is trimmed after load.
|
||||
@@ -480,7 +480,7 @@ class NodeDB
|
||||
/// Returns true iff every encrypted file decrypted and decoded cleanly.
|
||||
/// On false the caller MUST treat the storage as corrupt: leave the
|
||||
/// connection unauthenticated, emit a LOCKED(storage_corrupt) status,
|
||||
/// and refuse to call setAdminAuthorized — otherwise a subsequent
|
||||
/// and refuse to call setAdminAuthorized - otherwise a subsequent
|
||||
/// set_config would re-encrypt a wrong baseline (the locked-default
|
||||
/// values still resident in `config` / `channelFile` / `nodeDatabase`)
|
||||
/// and overwrite the operator's persisted state.
|
||||
@@ -489,7 +489,7 @@ class NodeDB
|
||||
/// Disable lockdown: decrypt every encrypted pref file back to plaintext,
|
||||
/// then remove the DEK / token / counter / backoff artifacts. Requires
|
||||
/// EncryptedStorage to be unlocked (DEK in RAM). Returns false if any
|
||||
/// file failed to revert — in which case the DEK is still present and the
|
||||
/// file failed to revert - in which case the DEK is still present and the
|
||||
/// device remains in lockdown so the operator can retry. APPROTECT is not
|
||||
/// reversed. Called from the main loop via lockdownDisablePending.
|
||||
bool disableLockdownToPlaintext();
|
||||
@@ -571,7 +571,7 @@ class NodeDB
|
||||
bool migrateLegacyNodeDatabase();
|
||||
|
||||
// Route satellite-store decode entries straight into our maps instead of
|
||||
// temp vectors. Must be paired — disarm before any other NodeDatabase decode.
|
||||
// temp vectors. Must be paired - disarm before any other NodeDatabase decode.
|
||||
void armNodeDatabaseDecodeTargets();
|
||||
void disarmNodeDatabaseDecodeTargets();
|
||||
};
|
||||
|
||||
@@ -92,7 +92,7 @@ bool NodeDB::migrateLegacyNodeDatabase()
|
||||
sanitizeUtf8(slim.long_name, sizeof(slim.long_name)); // replace bad bytes so nanopb encode never fails
|
||||
strncpy(slim.short_name, legacy.user.short_name, sizeof(slim.short_name));
|
||||
slim.short_name[sizeof(slim.short_name) - 1] = '\0';
|
||||
sanitizeUtf8(slim.short_name, sizeof(slim.short_name)); // same — v24 names may contain non-UTF-8 bytes
|
||||
sanitizeUtf8(slim.short_name, sizeof(slim.short_name)); // same - v24 names may contain non-UTF-8 bytes
|
||||
slim.hw_model = legacy.user.hw_model;
|
||||
slim.role = legacy.user.role;
|
||||
if (legacy.user.is_licensed)
|
||||
|
||||
@@ -248,7 +248,7 @@ void PacketHistory::hashRemove(NodeNum sender, PacketId id)
|
||||
return;
|
||||
uint16_t idx = hashIndex[bucket];
|
||||
if (idx < recentPacketsCapacity && recentPackets[idx].sender == sender && recentPackets[idx].id == id) {
|
||||
// Found it — delete and re-insert subsequent entries to maintain probe chain integrity
|
||||
// Found it - delete and re-insert subsequent entries to maintain probe chain integrity
|
||||
hashIndex[bucket] = HASH_EMPTY;
|
||||
uint32_t next = (bucket + 1) & hashMask;
|
||||
for (uint32_t j = 0; j < hashCapacity; j++) {
|
||||
@@ -488,7 +488,7 @@ bool PacketHistory::wasRelayer(const uint8_t relayer, const uint32_t id, const N
|
||||
/* Check if a certain node was a relayer of a packet in the history given iterator
|
||||
* @return true if node was indeed a relayer, false if not
|
||||
* NOTE: intentionally byte-domain. Both `relayer` and relayed_by[] are on-wire last bytes, so this
|
||||
* answers "did a relayer with this byte touch the packet" — correct without resolving to a NodeNum.
|
||||
* answers "did a relayer with this byte touch the packet" - correct without resolving to a NodeNum.
|
||||
* The collision risk is neutralized where the result is consumed (route learning in
|
||||
* NextHopRouter::sniffReceived now gates the write through NodeDB::resolveUniqueLastByte). */
|
||||
bool PacketHistory::wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole)
|
||||
|
||||
+52
-52
@@ -64,12 +64,12 @@ static constexpr size_t MAX_AUTH_SLOTS = 6;
|
||||
// status produced for connection A (e.g. UNLOCKED with the active TTL,
|
||||
// or UNLOCK_FAILED with a backoff) cannot be drained by connection B,
|
||||
// which would otherwise learn that A just authenticated or just failed
|
||||
// — a real information leak across local clients.
|
||||
// - a real information leak across local clients.
|
||||
//
|
||||
// File-scope rather than a per-PhoneAPI member because adding any
|
||||
// non-trivial state directly to PhoneAPI broke USB-CDC enumeration on
|
||||
// the current nRF52 framework; the auth-slot table next door uses the
|
||||
// same workaround. Lifecycle is tied to the auth slot table — both are
|
||||
// same workaround. Lifecycle is tied to the auth slot table - both are
|
||||
// keyed by PhoneAPI*, both are cleared together in clearAuthSlot_LH,
|
||||
// and both share g_authSlotsMutex.
|
||||
struct PendingStatusSlot {
|
||||
@@ -78,7 +78,7 @@ struct PendingStatusSlot {
|
||||
bool hasPending = false;
|
||||
// True between a successful passphrase verify and the main-loop
|
||||
// reloadFromDisk that follows. While set, the connection is NOT
|
||||
// yet authorized and no UNLOCKED status has been emitted — the
|
||||
// yet authorized and no UNLOCKED status has been emitted - the
|
||||
// client still sees LOCKED, and any admin op it tries is dropped
|
||||
// by the existing unauth gates. Cleared either way by
|
||||
// completePendingUnlocks once reload finishes.
|
||||
@@ -107,7 +107,7 @@ static PendingStatusSlot *findOrAllocStatusSlot_LH(PhoneAPI *p)
|
||||
// Mirror the auth-slot eviction policy: stale slots can be reused.
|
||||
// A connection that lost its auth slot has nothing meaningful to be
|
||||
// told via a pending status anyway. Never evict a slot mid-unlock
|
||||
// (pendingUnlockAfterReload set) — completing that flow on the
|
||||
// (pendingUnlockAfterReload set) - completing that flow on the
|
||||
// wrong PhoneAPI would authorize the wrong connection.
|
||||
for (auto &s : g_statusSlots) {
|
||||
if (!s.hasPending && !s.pendingUnlockAfterReload) {
|
||||
@@ -143,7 +143,7 @@ static void buildStatus_LH(meshtastic_LockdownStatus &out, meshtastic_LockdownSt
|
||||
memset(&out, 0, sizeof(out));
|
||||
out.state = state;
|
||||
// Collapse the specific token_* reasons to a generic "locked" over
|
||||
// the wire — full detail still goes to local logs. An unauth client
|
||||
// the wire - full detail still goes to local logs. An unauth client
|
||||
// does not need to know whether HMAC failed vs the boot count
|
||||
// hit zero vs the file was the wrong size; all of those mean the
|
||||
// same thing to the client ("locked, ask for passphrase") but
|
||||
@@ -171,7 +171,7 @@ struct PhoneAuthSlot {
|
||||
static PhoneAuthSlot g_authSlots[MAX_AUTH_SLOTS];
|
||||
|
||||
// Global auth epoch. Lock Now bumps it; per-slot `epoch` compared against
|
||||
// this. Wraps at 2^32 revocations — practically unreachable; on wrap the
|
||||
// this. Wraps at 2^32 revocations - practically unreachable; on wrap the
|
||||
// only behavioral effect is that any slot whose epoch happens to match the
|
||||
// new low value would be treated as authorized again, which requires a
|
||||
// pre-existing authorized slot to survive 2^32 lockNow events on the same
|
||||
@@ -179,7 +179,7 @@ static PhoneAuthSlot g_authSlots[MAX_AUTH_SLOTS];
|
||||
static uint32_t g_authEpoch = 1;
|
||||
|
||||
// Single mutex guarding g_authSlots and g_authEpoch. All readers and
|
||||
// writers — including const getters like getAdminAuthorized — must take
|
||||
// writers - including const getters like getAdminAuthorized - must take
|
||||
// it. Granularity is fine because the critical sections are short (a
|
||||
// fixed-size linear scan over 6 entries) and contention is dominated by
|
||||
// getFromRadio's per-call redaction checks, which tolerate brief
|
||||
@@ -191,7 +191,7 @@ static concurrency::Lock g_authSlotsMutex;
|
||||
// evicts the first unauthorized slot found. Refuses to evict an authorized
|
||||
// slot (those represent a live operator session and must outlive the table
|
||||
// pressure of reconnect churn). Returns nullptr only if every slot is
|
||||
// occupied by a different live, authorized PhoneAPI — practically only
|
||||
// occupied by a different live, authorized PhoneAPI - practically only
|
||||
// reachable as a DoS via 7+ simultaneous authed connections, in which
|
||||
// case fail-closed and log.
|
||||
static PhoneAuthSlot *findOrAllocSlot_LH(PhoneAPI *p)
|
||||
@@ -211,7 +211,7 @@ static PhoneAuthSlot *findOrAllocSlot_LH(PhoneAPI *p)
|
||||
}
|
||||
}
|
||||
// Second pass: evict an unauthorized stale slot. Don't touch authorized
|
||||
// ones — those still represent an operator-authenticated session.
|
||||
// ones - those still represent an operator-authenticated session.
|
||||
for (auto &s : g_authSlots) {
|
||||
if (!s.authorized) {
|
||||
s.who = p;
|
||||
@@ -312,7 +312,7 @@ void PhoneAPI::handleStartConfig()
|
||||
#if defined(MESHTASTIC_EXCLUDE_FILES_MANIFEST)
|
||||
// Skip the recursive FS walk. Used by platforms whose Zephyr LittleFS
|
||||
// backend can't safely traverse a deep tree (e.g. nRF54L15) and platforms
|
||||
// that don't support OTA browsing — the manifest is only consumed by
|
||||
// that don't support OTA browsing - the manifest is only consumed by
|
||||
// companion apps for those flows.
|
||||
releaseFilesManifest(filesManifest);
|
||||
#else
|
||||
@@ -444,12 +444,12 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength)
|
||||
case meshtastic_ToRadio_packet_tag:
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!getAdminAuthorized()) {
|
||||
// Allow admin messages addressed to this device — passphrase delivery must get through.
|
||||
// Allow admin messages addressed to this device - passphrase delivery must get through.
|
||||
// AdminModule handles its own is_managed gate for those.
|
||||
// Block everything else — unauthorized clients cannot inject mesh traffic.
|
||||
// Block everything else - unauthorized clients cannot inject mesh traffic.
|
||||
// Require the packet to carry a decoded (not encrypted) payload so portnum is valid.
|
||||
// Refuse to match when our own node number is still 0 (NodeDB
|
||||
// not yet loaded — happens during the locked-default boot path
|
||||
// not yet loaded - happens during the locked-default boot path
|
||||
// before reloadFromDisk). Otherwise a packet with to==0 would
|
||||
// satisfy the equality and bypass the gate.
|
||||
NodeNum ourNum = nodeDB->getNodeNum();
|
||||
@@ -584,12 +584,12 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
fromRadioScratch.my_info = myNodeInfo;
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!getAdminAuthorized()) {
|
||||
// device_id is a stable hardware identifier — useful for an attacker
|
||||
// device_id is a stable hardware identifier - useful for an attacker
|
||||
// to fingerprint / correlate the device across observations. Strip it
|
||||
// for unauthenticated clients. my_node_num is kept (it's broadcast
|
||||
// on the mesh anyway). pio_env / min_app_version reveal the exact
|
||||
// build flavour, useful only for picking which known-CVE to try.
|
||||
// nodedb_count stays — clients need it to decide whether to pull
|
||||
// nodedb_count stays - clients need it to decide whether to pull
|
||||
// the node DB after unlocking.
|
||||
fromRadioScratch.my_info.device_id.size = 0;
|
||||
memset(fromRadioScratch.my_info.device_id.bytes, 0, sizeof(fromRadioScratch.my_info.device_id.bytes));
|
||||
@@ -657,7 +657,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
// position_flags, excluded_modules, optionsCount. None of it
|
||||
// is needed to drive lockdown_auth, and most of it tells an
|
||||
// attacker which CVE / behavior quirks to probe. Wipe the
|
||||
// whole struct — clients re-fetch once authenticated.
|
||||
// whole struct - clients re-fetch once authenticated.
|
||||
memset(&fromRadioScratch.metadata, 0, sizeof(fromRadioScratch.metadata));
|
||||
}
|
||||
#endif
|
||||
@@ -668,7 +668,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
// Tell the client which modem presets are legal in each region so its UI
|
||||
// can block illegal region+preset combinations. This is public RF /
|
||||
// regulatory information (region and modem_preset are already in the
|
||||
// unauthenticated LoRa whitelist below), so it is sent unconditionally —
|
||||
// unauthenticated LoRa whitelist below), so it is sent unconditionally -
|
||||
// even an unauthorized/locked-down client can render a correct picker.
|
||||
LOG_DEBUG("Send region preset map");
|
||||
fromRadioScratch.which_payload_variant = meshtastic_FromRadio_region_presets_tag;
|
||||
@@ -683,7 +683,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
if (!getAdminAuthorized()) {
|
||||
// Unauthenticated: emit a zero-initialized Channel. fromRadioScratch
|
||||
// was memset(0) at the top of getFromRadio(), so leaving .channel
|
||||
// untouched gives the client an empty entry — no name, no PSK, no
|
||||
// untouched gives the client an empty entry - no name, no PSK, no
|
||||
// role. Advances the state machine normally so config_complete_id
|
||||
// still fires.
|
||||
} else
|
||||
@@ -748,7 +748,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
// private knobs (ignore_incoming list, override_duty_cycle,
|
||||
// override_frequency, sx126x_rx_boosted_gain, tx_power,
|
||||
// ignore_mqtt, fem_lna_mode, config_ok_to_mqtt, ...) stay
|
||||
// hidden — they tell an attacker how the operator has tuned
|
||||
// hidden - they tell an attacker how the operator has tuned
|
||||
// the device but are not needed by an unauth client.
|
||||
meshtastic_Config_LoRaConfig whitelist = {};
|
||||
whitelist.use_preset = config.lora.use_preset;
|
||||
@@ -775,7 +775,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
if (!getAdminAuthorized()) {
|
||||
// Unauthenticated: emit an empty SecurityConfig (zero-init from
|
||||
// the top-of-loop memset). No private_key, no admin_keys, no
|
||||
// public_key — nothing for an attacker to inspect.
|
||||
// public_key - nothing for an attacker to inspect.
|
||||
//
|
||||
// Provisioning state (NEEDS_PROVISION vs LOCKED) is conveyed via
|
||||
// the FromRadio.lockdown_status proto sent post-config; clients
|
||||
@@ -906,7 +906,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
if (config_state > (_meshtastic_AdminMessage_ModuleConfigType_MAX + 1)) {
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!getAdminAuthorized()) {
|
||||
// Unauthorized client: skip node DB and file manifest — only send config complete
|
||||
// Unauthorized client: skip node DB and file manifest - only send config complete
|
||||
state = STATE_SEND_COMPLETE_ID;
|
||||
} else
|
||||
#endif
|
||||
@@ -993,7 +993,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
} else if (mqttClientProxyMessageForPhone) {
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!getAdminAuthorized()) {
|
||||
releaseMqttClientProxyPhonePacket(); // Discard — unauthorized client
|
||||
releaseMqttClientProxyPhonePacket(); // Discard - unauthorized client
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
@@ -1004,7 +1004,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
} else if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) {
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!getAdminAuthorized()) {
|
||||
xmodemPacketForPhone = meshtastic_XModem_init_zero; // Discard — unauthorized client
|
||||
xmodemPacketForPhone = meshtastic_XModem_init_zero; // Discard - unauthorized client
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
@@ -1015,7 +1015,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
} else if (hasPendingLockdownStatus()) {
|
||||
concurrency::LockGuard guard(&g_authSlotsMutex);
|
||||
// Look up our own slot only — never another connection's. Re-check
|
||||
// Look up our own slot only - never another connection's. Re-check
|
||||
// hasPending under the lock since a concurrent drain on the same
|
||||
// connection (unlikely but possible if multiple transport
|
||||
// callbacks race against one PhoneAPI) may have grabbed it.
|
||||
@@ -1033,7 +1033,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
} else if (packetForPhone) {
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
if (!getAdminAuthorized()) {
|
||||
releasePhonePacket(); // Discard mesh traffic — unauthorized client
|
||||
releasePhonePacket(); // Discard mesh traffic - unauthorized client
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
@@ -1044,7 +1044,7 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf)
|
||||
releasePhonePacket();
|
||||
}
|
||||
} else if (replayPending()) {
|
||||
// No live packet pending — feed the phone one cached satellite-DB packet.
|
||||
// No live packet pending - feed the phone one cached satellite-DB packet.
|
||||
// popReplayPacket advances through positions->telemetry->environment->status,
|
||||
// and flips replayPhase back to IDLE when everything has been drained.
|
||||
meshtastic_MeshPacket replayPkt;
|
||||
@@ -1222,7 +1222,7 @@ meshtastic_MeshPacket PhoneAPI::makeReplayTelemetryPacket(NodeNum num, const mes
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated — iOS client filters
|
||||
// Mark as if heard over the air, not internally generated - iOS client filters
|
||||
// TRANSPORT_INTERNAL packets out of broadcast peer state updates.
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||
@@ -1327,7 +1327,7 @@ meshtastic_MeshPacket PhoneAPI::makeReplayEnvironmentPacket(uint32_t num, const
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated — iOS client filters
|
||||
// Mark as if heard over the air, not internally generated - iOS client filters
|
||||
// TRANSPORT_INTERNAL packets out of broadcast peer state updates.
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||
@@ -1391,7 +1391,7 @@ meshtastic_MeshPacket PhoneAPI::makeReplayStatusPacket(uint32_t num, const mesht
|
||||
pkt.hop_limit = Default::getConfiguredOrDefaultHopLimit(config.lora.hop_limit);
|
||||
pkt.hop_start = pkt.hop_limit;
|
||||
pkt.priority = meshtastic_MeshPacket_Priority_BACKGROUND;
|
||||
// Mark as if heard over the air, not internally generated — client filters
|
||||
// Mark as if heard over the air, not internally generated - client filters
|
||||
pkt.transport_mechanism = meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA;
|
||||
pkt.which_payload_variant = meshtastic_MeshPacket_decoded_tag;
|
||||
pkt.decoded.portnum = meshtastic_PortNum_NODE_STATUS_APP;
|
||||
@@ -1483,7 +1483,7 @@ bool PhoneAPI::popReplayPacket(meshtastic_MeshPacket &out)
|
||||
}
|
||||
}
|
||||
|
||||
// Queue empty AND no more entries to feed it — phase is exhausted.
|
||||
// Queue empty AND no more entries to feed it - phase is exhausted.
|
||||
advanceReplayPhase();
|
||||
}
|
||||
return false;
|
||||
@@ -1609,7 +1609,7 @@ bool PhoneAPI::available()
|
||||
hasPacket = !!packetForPhone;
|
||||
if (hasPacket)
|
||||
return true;
|
||||
// Trailing replay drain — feeds cached satellite-DB packets alongside
|
||||
// Trailing replay drain - feeds cached satellite-DB packets alongside
|
||||
// (lower priority than) live traffic.
|
||||
return replayPending();
|
||||
}
|
||||
@@ -1666,7 +1666,7 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
|
||||
// (b) Any other admin payload from an unauthorized connection:
|
||||
// dropped here. The previous design relied on AdminModule
|
||||
// to apply isLocalAdminAuthorized() during dispatch, but
|
||||
// AdminModule runs on the Router task — by then the
|
||||
// AdminModule runs on the Router task - by then the
|
||||
// PhoneAPI dispatching task has already exited and the
|
||||
// per-connection auth context is unrecoverable. Putting
|
||||
// the gate here closes that race and covers H6/H7 from the
|
||||
@@ -1678,7 +1678,7 @@ bool PhoneAPI::handleToRadioPacket(meshtastic_MeshPacket &p)
|
||||
if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_AdminMessage_msg, &admin)) {
|
||||
if (admin.which_payload_variant == meshtastic_AdminMessage_lockdown_auth_tag) {
|
||||
handleLockdownAuthInline(admin.lockdown_auth);
|
||||
// Wipe the decoded passphrase scratch — the byte array in
|
||||
// Wipe the decoded passphrase scratch - the byte array in
|
||||
// p.decoded.payload.bytes is wiped by handleLockdownAuthInline.
|
||||
volatile uint8_t *adminVol = const_cast<volatile uint8_t *>(admin.lockdown_auth.passphrase.bytes);
|
||||
for (size_t i = 0; i < sizeof(admin.lockdown_auth.passphrase.bytes); i++)
|
||||
@@ -1769,7 +1769,7 @@ bool PhoneAPI::getAdminAuthorized() const
|
||||
{
|
||||
// Runtime-toggle model: when lockdown is NOT active (a lockdown-capable
|
||||
// build that hasn't been provisioned, or that was disabled), there is
|
||||
// nothing to protect — every connection is implicitly authorized, so
|
||||
// nothing to protect - every connection is implicitly authorized, so
|
||||
// all the `if (!getAdminAuthorized())` redaction gates throughout
|
||||
// getFromRadio() / handleToRadio() become no-ops and the device serves
|
||||
// config exactly like stock firmware. Only once provisioned (lockdown
|
||||
@@ -1779,7 +1779,7 @@ bool PhoneAPI::getAdminAuthorized() const
|
||||
return true;
|
||||
#endif
|
||||
concurrency::LockGuard g(&g_authSlotsMutex);
|
||||
// const_cast is safe — findOrAllocSlot_LH only mutates the slot table,
|
||||
// const_cast is safe - findOrAllocSlot_LH only mutates the slot table,
|
||||
// not the PhoneAPI itself, and the table key is just the pointer.
|
||||
const auto *slot = findOrAllocSlot_LH(const_cast<PhoneAPI *>(this));
|
||||
return slot && slot->authorized && slot->epoch == g_authEpoch;
|
||||
@@ -1790,7 +1790,7 @@ void PhoneAPI::setAdminAuthorized(bool authorized)
|
||||
concurrency::LockGuard g(&g_authSlotsMutex);
|
||||
auto *slot = findOrAllocSlot_LH(this);
|
||||
if (!slot)
|
||||
return; // slot table full — fail-closed
|
||||
return; // slot table full - fail-closed
|
||||
if (authorized) {
|
||||
slot->epoch = g_authEpoch;
|
||||
slot->authorized = true;
|
||||
@@ -1813,7 +1813,7 @@ void PhoneAPI::completePendingUnlocks(bool reloadOk)
|
||||
{
|
||||
// Snapshot fields that we'll need outside the lock (we cannot call
|
||||
// EncryptedStorage / setAdminAuthorized / unlockScreen while holding
|
||||
// g_authSlotsMutex without risking re-entry — setAdminAuthorized
|
||||
// g_authSlotsMutex without risking re-entry - setAdminAuthorized
|
||||
// itself takes the same lock).
|
||||
constexpr size_t kMaxSnapshots = MAX_AUTH_SLOTS;
|
||||
PhoneAPI *targets[kMaxSnapshots] = {};
|
||||
@@ -1825,7 +1825,7 @@ void PhoneAPI::completePendingUnlocks(bool reloadOk)
|
||||
continue;
|
||||
if (targetCount < kMaxSnapshots)
|
||||
targets[targetCount++] = s.who;
|
||||
// Clear the pending flag either way — failure path must not
|
||||
// Clear the pending flag either way - failure path must not
|
||||
// leave it set so a subsequent successful reload retries
|
||||
// against the wrong PhoneAPI.
|
||||
s.pendingUnlockAfterReload = false;
|
||||
@@ -1841,13 +1841,13 @@ void PhoneAPI::completePendingUnlocks(bool reloadOk)
|
||||
p->queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCKED, "", boots, until, 0);
|
||||
}
|
||||
// Screen-lock latch is cleared once any client successfully
|
||||
// unlocks — the operator has proven the passphrase. Matches the
|
||||
// unlocks - the operator has proven the passphrase. Matches the
|
||||
// re-verify path's behavior.
|
||||
if (targetCount > 0)
|
||||
meshtastic_security::unlockScreen();
|
||||
LOG_INFO("Lockdown: post-reload completion: authorized %u connection(s)", (unsigned)targetCount);
|
||||
} else {
|
||||
// Storage corrupt — emit LOCKED(storage_corrupt) to every slot
|
||||
// Storage corrupt - emit LOCKED(storage_corrupt) to every slot
|
||||
// that was awaiting the unlock. setAdminAuthorized is NOT called
|
||||
// so the connection stays redacted and any set_config it sends
|
||||
// is dropped at the existing unauth gates. Caller (main.cpp) has
|
||||
@@ -1866,7 +1866,7 @@ void PhoneAPI::queueLockdownStatus(meshtastic_LockdownStatus_State state, const
|
||||
concurrency::LockGuard guard(&g_authSlotsMutex);
|
||||
auto *slot = findOrAllocStatusSlot_LH(this);
|
||||
if (!slot)
|
||||
return; // slot table exhausted — fail-closed, no status delivered
|
||||
return; // slot table exhausted - fail-closed, no status delivered
|
||||
buildStatus_LH(slot->status, state, lock_reason, boots_remaining, valid_until_epoch, backoff_seconds);
|
||||
slot->hasPending = true;
|
||||
}
|
||||
@@ -1914,14 +1914,14 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
ppVol[zi] = 0;
|
||||
};
|
||||
|
||||
// Lock Now — only honored from a connection that has already proven
|
||||
// Lock Now - only honored from a connection that has already proven
|
||||
// the passphrase. Unauthenticated clients used to be able to trigger
|
||||
// a reboot, which was a trivial local-presence DoS (any BLE/USB
|
||||
// attacker could brick-loop the device). Now lock_now requires
|
||||
// prior auth on this connection.
|
||||
if (la.lock_now) {
|
||||
if (!getAdminAuthorized()) {
|
||||
LOG_WARN("Lockdown: LOCK NOW from unauthorized connection — denied");
|
||||
LOG_WARN("Lockdown: LOCK NOW from unauthorized connection - denied");
|
||||
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCK_FAILED, "", 0, 0, 0);
|
||||
zeroPassphrase();
|
||||
return true;
|
||||
@@ -1938,18 +1938,18 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
// Disable lockdown entirely. Requires the passphrase (must prove
|
||||
// ownership before reverting at-rest encryption). We verify it here to
|
||||
// load the DEK, then hand the heavy decrypt-revert work to the main
|
||||
// loop via lockdownDisablePending — exactly like the unlock reload
|
||||
// loop via lockdownDisablePending - exactly like the unlock reload
|
||||
// path, because decrypting + rewriting nodes.proto is too heavy for
|
||||
// this transport-callback stack. APPROTECT is NOT reversed.
|
||||
if (la.disable) {
|
||||
if (la.passphrase.size < 1) {
|
||||
LOG_WARN("Lockdown: disable with empty passphrase — rejecting");
|
||||
LOG_WARN("Lockdown: disable with empty passphrase - rejecting");
|
||||
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCK_FAILED, "", 0, 0, 0);
|
||||
zeroPassphrase();
|
||||
return true;
|
||||
}
|
||||
if (!EncryptedStorage::isLockdownActive()) {
|
||||
// Already off — nothing to do; report DISABLED so the client UI settles.
|
||||
// Already off - nothing to do; report DISABLED so the client UI settles.
|
||||
LOG_INFO("Lockdown: disable requested but lockdown is not active");
|
||||
queueLockdownStatus(meshtastic_LockdownStatus_State_DISABLED, "", 0, 0, 0);
|
||||
zeroPassphrase();
|
||||
@@ -1972,13 +1972,13 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Empty-passphrase auth was previously a silent success — clients
|
||||
// Empty-passphrase auth was previously a silent success - clients
|
||||
// got no feedback and the device looked the same as it would after
|
||||
// an actual no-op. Emit UNLOCK_FAILED with no backoff so honest
|
||||
// clients can detect their own bug and an attacker still learns
|
||||
// nothing they wouldn't from any other failed attempt.
|
||||
if (la.passphrase.size < 1) {
|
||||
LOG_WARN("Lockdown: lockdown_auth with empty passphrase and lock_now=false — rejecting");
|
||||
LOG_WARN("Lockdown: lockdown_auth with empty passphrase and lock_now=false - rejecting");
|
||||
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCK_FAILED, "", 0, 0, 0);
|
||||
zeroPassphrase();
|
||||
return true;
|
||||
@@ -2017,7 +2017,7 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
if (ok) {
|
||||
needsReload = true;
|
||||
// Mark this slot for the main-loop completion handler. Don't
|
||||
// authorize or emit UNLOCKED yet — `config` / `channelFile`
|
||||
// authorize or emit UNLOCKED yet - `config` / `channelFile`
|
||||
// / `nodeDatabase` still hold the locked-default placeholders
|
||||
// installed by loadFromDisk()'s !isUnlocked() branch. If we
|
||||
// flipped the connection to authorized here, the client could
|
||||
@@ -2040,7 +2040,7 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
ok = EncryptedStorage::unlockWithPassphrase(la.passphrase.bytes, la.passphrase.size, boots, validUntilEpoch,
|
||||
sessionMaxSeconds);
|
||||
if (ok) {
|
||||
// Storage was already unlocked — no reload needed. Authorize
|
||||
// Storage was already unlocked - no reload needed. Authorize
|
||||
// and surface UNLOCKED to the client immediately.
|
||||
setAdminAuthorized(true);
|
||||
LOG_INFO("Lockdown: passphrase verified, this connection authorized");
|
||||
@@ -2055,13 +2055,13 @@ bool PhoneAPI::handleLockdownAuthInline(const meshtastic_LockdownAuth &la)
|
||||
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCKED, "", EncryptedStorage::getBootsRemaining(),
|
||||
EncryptedStorage::getValidUntilEpoch(), 0);
|
||||
} else if (ok && needsReload) {
|
||||
// Cold-unlock path: deliberately no status emission yet — the
|
||||
// Cold-unlock path: deliberately no status emission yet - the
|
||||
// client keeps seeing LOCKED until completePendingUnlocks()
|
||||
// runs after a successful reload.
|
||||
} else {
|
||||
uint32_t backoff = EncryptedStorage::getBackoffSecondsRemaining();
|
||||
queueLockdownStatus(meshtastic_LockdownStatus_State_UNLOCK_FAILED, "", 0, 0, backoff);
|
||||
// Don't log backoff seconds — the client receives it in the
|
||||
// Don't log backoff seconds - the client receives it in the
|
||||
// UNLOCK_FAILED status anyway, and in non-DEBUG_MUTE builds the
|
||||
// numeric value would otherwise spill onto a USB-attached
|
||||
// attacker's serial terminal alongside other diagnostic noise.
|
||||
|
||||
+4
-4
@@ -175,7 +175,7 @@ class PhoneAPI
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
/// Per-connection auth: tracked in a small file-scope slot table keyed
|
||||
/// by PhoneAPI*. Adding state members directly to PhoneAPI broke
|
||||
/// USB-CDC enumeration on current nRF52 framework — even one extra
|
||||
/// USB-CDC enumeration on current nRF52 framework - even one extra
|
||||
/// per-instance uint32_t was enough. Keeping all state out-of-line
|
||||
/// avoids the issue.
|
||||
void setAdminAuthorized(bool authorized);
|
||||
@@ -257,10 +257,10 @@ class PhoneAPI
|
||||
APIType api_type = TYPE_NONE;
|
||||
|
||||
#ifdef MESHTASTIC_PHONEAPI_ACCESS_CONTROL
|
||||
// No per-instance auth members — see method-level note. All state lives
|
||||
// No per-instance auth members - see method-level note. All state lives
|
||||
// in a file-scope slot table in PhoneAPI.cpp keyed by `this` pointer.
|
||||
|
||||
// Pending LockdownStatus storage is NOT a class member — having a
|
||||
// Pending LockdownStatus storage is NOT a class member - having a
|
||||
// meshtastic_LockdownStatus (~50 bytes with the char[33] lock_reason)
|
||||
// as a PhoneAPI member broke USB-CDC enumeration on the nRF52 Adafruit
|
||||
// framework. The exact mechanism wasn't pinned down, but moving the
|
||||
@@ -310,7 +310,7 @@ class PhoneAPI
|
||||
#if defined(MESHTASTIC_ENCRYPTED_STORAGE) && defined(MESHTASTIC_PHONEAPI_ACCESS_CONTROL)
|
||||
/// Synchronously handle a lockdown_auth AdminMessage from the local
|
||||
/// client. Runs inside handleToRadioPacket so the originating
|
||||
/// connection is reachable via `this` — avoids the async context
|
||||
/// connection is reachable via `this` - avoids the async context
|
||||
/// loss that broke the previous AdminModule path. Always consumes the
|
||||
/// packet (returns true): lockdown_auth is local-only and must not be
|
||||
/// forwarded to the mesh router.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
// Max precision on a publicly-decryptable channel. CCPA "precise geolocation" = within a ~564m (1,850ft) radius.
|
||||
// Precision is bit-truncation of latitude_i/longitude_i: the latitude cell stays ~constant in meters worldwide
|
||||
// (~700m at 15 bits), while only the longitude cell varies — widest at the equator, narrowing toward the poles.
|
||||
// (~700m at 15 bits), while only the longitude cell varies - widest at the equator, narrowing toward the poles.
|
||||
// 15 also matches the MQTT map-report public precision ceiling.
|
||||
#define MAX_POSITION_PRECISION_PUBLIC_KEY 15
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ const RegionInfo regions[] = {
|
||||
/*
|
||||
https://lora-alliance.org/wp-content/uploads/2020/11/lorawan_regional_parameters_v1.0.3reva_0.pdf
|
||||
https://standard.nbtc.go.th/getattachment/Standards/%E0%B8%A1%E0%B8%B2%E0%B8%95%E0%B8%A3%E0%B8%90%E0%B8%B2%E0%B8%99%E0%B8%97%E0%B8%B2%E0%B8%87%E0%B9%80%E0%B8%97%E0%B8%84%E0%B8%99%E0%B8%B4%E0%B8%84%E0%B8%82%E0%B8%AD%E0%B8%87%E0%B9%80%E0%B8%84%E0%B8%A3%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%87%E0%B9%82%E0%B8%97%E0%B8%A3%E0%B8%84%E0%B8%A1%E0%B8%99%E0%B8%B2%E0%B8%84%E0%B8%A1/1033-2565.pdf.aspx?lang=th-TH
|
||||
Thailand 920–925 MHz set max TX power to 27 dBm and enforce 10% duty cycle, aligned with NBTC regulations.
|
||||
Thailand 920-925 MHz set max TX power to 27 dBm and enforce 10% duty cycle, aligned with NBTC regulations.
|
||||
*/
|
||||
RDEF(TH, 920.0f, 925.0f, 10, 27, false, false, PROFILE_STD, PRESET(LONG_FAST), 0),
|
||||
|
||||
@@ -246,7 +246,7 @@ const RegionInfo regions[] = {
|
||||
|
||||
/*
|
||||
ITU Region 1 (Europe, Africa, Middle East, former USSR) amateur 2m allocation: 144.000 - 146.000 MHz.
|
||||
Power limit is the regulatory ceiling (1 W / 30 dBm) — individual hardware will cap below this
|
||||
Power limit is the regulatory ceiling (1 W / 30 dBm) - individual hardware will cap below this
|
||||
via its own PA curve; the field here is just the legal upper bound.
|
||||
|
||||
Default slot: 26 (144.510 MHz)
|
||||
|
||||
@@ -371,10 +371,10 @@ void RadioLibInterface::onNotify(uint32_t notification)
|
||||
handleTransmitInterrupt(); // completeSending() already restored the radio to the home config
|
||||
#if !MESHTASTIC_EXCLUDE_BEACON
|
||||
// Pre-switch the radio to the NEXT queued packet's beacon config (no-op for normal traffic).
|
||||
// Not required for correctness — TRANSMIT_DELAY_COMPLETED would switch before CAD anyway — but
|
||||
// Not required for correctness - TRANSMIT_DELAY_COMPLETED would switch before CAD anyway - but
|
||||
// doing it here lets the next beacon skip the switch-only delay cycle and, more importantly,
|
||||
// keeps the post-TX listen window (and the CAD/LBT that follows) on the channel we're about to
|
||||
// transmit on. Only engages when the next packet is itself a beacon — exactly when we want it.
|
||||
// transmit on. Only engages when the next packet is itself a beacon - exactly when we want it.
|
||||
MeshBeaconModule::reconfigureForBeaconTX(this, txQueue.getFront());
|
||||
#endif
|
||||
startReceive();
|
||||
@@ -402,8 +402,8 @@ void RadioLibInterface::onNotify(uint32_t notification)
|
||||
#if !MESHTASTIC_EXCLUDE_BEACON
|
||||
} else if (MeshBeaconModule::beaconTxConfigInvalid(txp)) {
|
||||
// The beacon's target radio config is invalid (bad preset/region, or an
|
||||
// unlicensed node keying up on a ham-only region). Drop the packet — never
|
||||
// transmit it on the current (home) config — and move on to the next queued packet.
|
||||
// unlicensed node keying up on a ham-only region). Drop the packet - never
|
||||
// transmit it on the current (home) config - and move on to the next queued packet.
|
||||
LOG_DEBUG("Beacon: invalid TX radio config, dropping packet 0x%08x", txp->id);
|
||||
meshtastic_MeshPacket *bad = txQueue.dequeue();
|
||||
MeshBeaconModule::clearTargetRadioSettings(bad);
|
||||
|
||||
@@ -160,7 +160,7 @@ class RadioLibInterface : public RadioInterface, protected concurrency::Notified
|
||||
/**
|
||||
* Reset AGC by power-cycling the analog frontend.
|
||||
* Subclasses override with chip-specific calibration sequences.
|
||||
* Safe to call periodically — skips if currently sending or receiving.
|
||||
* Safe to call periodically - skips if currently sending or receiving.
|
||||
*/
|
||||
virtual void resetAGC();
|
||||
|
||||
|
||||
+2
-2
@@ -100,7 +100,7 @@ bool Router::shouldDecrementHopLimit(const meshtastic_MeshPacket *p)
|
||||
return true;
|
||||
}
|
||||
|
||||
// router_preserve_hops: not suitable right now — removed from config until
|
||||
// router_preserve_hops: not suitable right now - removed from config until
|
||||
// the right heuristics for when to preserve vs. exhaust hops are established.
|
||||
// #if HAS_TRAFFIC_MANAGEMENT
|
||||
// if (moduleConfig.has_traffic_management &&
|
||||
@@ -560,7 +560,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
LOG_DEBUG("No public key for 0x%08x, cannot verify XEdDSA signature", p->from);
|
||||
}
|
||||
} else {
|
||||
// Unsigned packet — only reject the class of packet a signing node always signs:
|
||||
// Unsigned packet - only reject the class of packet a signing node always signs:
|
||||
// an unencrypted broadcast small enough to also carry a signature (see perhapsEncode()).
|
||||
// Unicast packets and oversized broadcasts are never signed, so they must not be
|
||||
// hard-failed here even if this node has signed before.
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ class Router : protected concurrency::OSThread, protected PacketHistory
|
||||
void addInterface(std::unique_ptr<RadioInterface> _iface) { iface = std::move(_iface); }
|
||||
|
||||
/**
|
||||
* Borrowed (non-owning) access to the radio interface — used by NodeDB
|
||||
* Borrowed (non-owning) access to the radio interface - used by NodeDB
|
||||
* after a lockdown unlock so it can push the freshly-loaded config to
|
||||
* the SX12xx via reconfigure(). Returns nullptr when no radio has been
|
||||
* attached (e.g. ARCH_PORTDUINO simulator before SimRadio bind).
|
||||
|
||||
@@ -215,7 +215,7 @@ void StreamAPI::emitRebooted()
|
||||
|
||||
void StreamAPI::emitLogRecord(meshtastic_LogRecord_Level level, const char *src, const char *format, va_list arg)
|
||||
{
|
||||
// IMPORTANT: do NOT touch `fromRadioScratch` or `txBuf` here — those
|
||||
// IMPORTANT: do NOT touch `fromRadioScratch` or `txBuf` here - those
|
||||
// belong to the main packet-emission path and a LOG_ firing during
|
||||
// `writeStream()` would corrupt an in-flight encode. We keep a
|
||||
// dedicated `fromRadioScratchLog` + `txBufLog` for log records and
|
||||
|
||||
@@ -107,7 +107,7 @@ class StreamAPI : public PhoneAPI
|
||||
/// re-used `fromRadioScratch` / `txBuf` and corrupted whatever the main
|
||||
/// path had already encoded. Symptoms on the host were
|
||||
/// `google.protobuf.message.DecodeError: Error parsing message with type
|
||||
/// 'meshtastic.protobuf.FromRadio'` — any tool with
|
||||
/// 'meshtastic.protobuf.FromRadio'` - any tool with
|
||||
/// `config.security.debug_log_api_enabled=true` under traffic would see
|
||||
/// torn frames every few messages.
|
||||
///
|
||||
|
||||
@@ -87,7 +87,7 @@ void TransmitHistory::setLastSentToMesh(uint16_t key)
|
||||
const uint8_t flags = (getRTCQuality() == RTCQualityNone) ? ENTRY_FLAG_BOOT_RELATIVE : ENTRY_FLAG_NONE;
|
||||
history[key] = makeStoredTimestamp(now, flags);
|
||||
dirty = true;
|
||||
// Don't flush to disk on every transmit — flash has limited write endurance.
|
||||
// Don't flush to disk on every transmit - flash has limited write endurance.
|
||||
// The in-memory lastMillis map handles throttle during normal operation.
|
||||
// Disk is flushed: before deep sleep (sleep.cpp) and periodically here,
|
||||
// throttled to at most once per 5 minutes. Always save the first time
|
||||
@@ -189,7 +189,7 @@ uint32_t TransmitHistory::getLastSentToMeshMillis(uint16_t key) const
|
||||
// Fall back to epoch conversion (loaded from disk after reboot)
|
||||
auto it = history.find(key);
|
||||
if (it == history.end() || it->second.seconds == 0) {
|
||||
return 0; // No stored time — module has never sent
|
||||
return 0; // No stored time - module has never sent
|
||||
}
|
||||
|
||||
// Convert to a millis()-relative timestamp: millis() - msAgo.
|
||||
@@ -271,7 +271,7 @@ void TransmitHistory::clear()
|
||||
}
|
||||
|
||||
#else
|
||||
// No filesystem available — provide stub with in-memory tracking
|
||||
// No filesystem available - provide stub with in-memory tracking
|
||||
TransmitHistory *transmitHistory = nullptr;
|
||||
|
||||
TransmitHistory *TransmitHistory::getInstance()
|
||||
|
||||
@@ -78,7 +78,7 @@ class TransmitHistory
|
||||
|
||||
/**
|
||||
* Wipe in-memory throttle state + remove the on-disk file. Required
|
||||
* alongside rmDir("/prefs") in factoryReset — otherwise the 5-min
|
||||
* alongside rmDir("/prefs") in factoryReset - otherwise the 5-min
|
||||
* auto-flush resurrects the file from the still-populated maps.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
@@ -94,7 +94,7 @@ meshtastic_Position TypeConversions::ConvertToPosition(meshtastic_PositionLite l
|
||||
position.time = lite.time;
|
||||
// Preserve the peer's broadcast precision; falls back to 0 for entries cached
|
||||
// before the precision_bits field existed in PositionLite (pre-migration data).
|
||||
// iOS treats 0 as "unspecified precision" and won't render the pin — so for
|
||||
// iOS treats 0 as "unspecified precision" and won't render the pin - so for
|
||||
// unset values, declare full precision so the stored lat/lon renders as a point.
|
||||
position.precision_bits = lite.precision_bits == 0 ? 32 : lite.precision_bits;
|
||||
|
||||
|
||||
+11
-11
@@ -12,13 +12,13 @@
|
||||
|
||||
#if defined(NRF52840_XXAA)
|
||||
#include "flash/flash_nrf5x.h"
|
||||
#define WARM_RING_MAGIC 0x324E5257u // "WRN2" — v2: last_heard low bits carry role + protected category
|
||||
#define WARM_RING_MAGIC_V1 0x474E5257u // "WRNG" — v1: last_heard was a plain timestamp.
|
||||
#define WARM_RING_MAGIC 0x324E5257u // "WRN2" - v2: last_heard low bits carry role + protected category
|
||||
#define WARM_RING_MAGIC_V1 0x474E5257u // "WRNG" - v1: last_heard was a plain timestamp.
|
||||
// v1 pages are still read on upgrade: we keep each record's identity + public key but
|
||||
// DISCARD its last_heard (the old timestamp would be misread as role/protected bits).
|
||||
// Records re-rank and re-learn their role on the next contact. Legacy pages convert to
|
||||
// v2 naturally as the ring rotates.
|
||||
// A tombstone is an entry record whose last_heard is all-ones — getTime()
|
||||
// A tombstone is an entry record whose last_heard is all-ones - getTime()
|
||||
// (unix seconds) cannot reach 0xFFFFFFFF until 2106, and erased flash is
|
||||
// detected via num == 0xFFFFFFFF before last_heard is ever inspected.
|
||||
#define WARM_RING_TOMBSTONE 0xFFFFFFFFu
|
||||
@@ -33,9 +33,9 @@ struct WarmStoreHeader {
|
||||
};
|
||||
static_assert(sizeof(WarmStoreHeader) == 16, "header layout is part of the persistence format");
|
||||
|
||||
#define WARM_STORE_MAGIC 0x324D5257u // "WRM2" — v2: last_heard low bits carry role + protected category
|
||||
#define WARM_STORE_MAGIC 0x324D5257u // "WRM2" - v2: last_heard low bits carry role + protected category
|
||||
#define WARM_STORE_MAGIC_V1 \
|
||||
0x314D5257u // "WRM1" — v1: last_heard was a plain timestamp. On upgrade we keep
|
||||
0x314D5257u // "WRM1" - v1: last_heard was a plain timestamp. On upgrade we keep
|
||||
// identity + key but discard last_heard, then rewrite as v2.
|
||||
|
||||
#ifdef FSCom
|
||||
@@ -104,7 +104,7 @@ WarmNodeEntry *WarmNodeStore::place(NodeNum num, uint32_t lastHeard, const uint8
|
||||
slot = &e;
|
||||
break;
|
||||
}
|
||||
// Compare on the time bits only — the low metadata bits (role/protected) must
|
||||
// Compare on the time bits only - the low metadata bits (role/protected) must
|
||||
// not perturb LRU victim selection.
|
||||
if (keyIsSet(e.public_key)) {
|
||||
if (!oldestKeyed || warmTimeOf(e) < warmTimeOf(*oldestKeyed))
|
||||
@@ -252,7 +252,7 @@ bool WarmNodeStore::saveIfDirty()
|
||||
// 3 × 4 KB pages below LittleFS. Mutations append 40 B records (entry snapshot,
|
||||
// or tombstone with last_heard == 0xFFFFFFFF) via the shared flash_nrf5x page
|
||||
// cache; saveIfDirty() is the durability point. A full page reclaims the oldest
|
||||
// (stranded live entries re-appended, then erased). Flash access holds spiLock —
|
||||
// (stranded live entries re-appended, then erased). Flash access holds spiLock -
|
||||
// the page cache is shared with InternalFS/LittleFS.
|
||||
|
||||
bool WarmNodeStore::ringReadHeader(uint8_t page, WarmPageHeader &h, bool *legacy) const
|
||||
@@ -289,7 +289,7 @@ void WarmNodeStore::ringOpenPage(uint8_t page)
|
||||
}
|
||||
|
||||
// Caller holds spiLock. May recurse once via ringAppend if the stranded set
|
||||
// fills the fresh page exactly — bounded by WARM_NODE_COUNT <= 2*kRecordsPerPage.
|
||||
// fills the fresh page exactly - bounded by WARM_NODE_COUNT <= 2*kRecordsPerPage.
|
||||
void WarmNodeStore::ringRotate()
|
||||
{
|
||||
uint8_t target = 0;
|
||||
@@ -441,7 +441,7 @@ void WarmNodeStore::load()
|
||||
memset(e, 0, sizeof(*e));
|
||||
}
|
||||
} else {
|
||||
// v1 (legacy) record: keep identity + key, but discard the old timestamp —
|
||||
// v1 (legacy) record: keep identity + key, but discard the old timestamp -
|
||||
// its low bits would otherwise be misread as role/protected metadata.
|
||||
uint32_t lh = rec.last_heard;
|
||||
if (legacy) {
|
||||
@@ -459,7 +459,7 @@ void WarmNodeStore::load()
|
||||
nextSeq = seqs[k] + 1;
|
||||
// If the head is a v1 page, force the next append to rotate into a fresh v2 page,
|
||||
// so new (v2) records never land in a page whose header says v1 (which would make
|
||||
// a later load discard their last_heard — including the role/protected we just set).
|
||||
// a later load discard their last_heard - including the role/protected we just set).
|
||||
if (legacy)
|
||||
writeSlot = kRecordsPerPage;
|
||||
}
|
||||
@@ -521,7 +521,7 @@ void WarmNodeStore::load()
|
||||
{
|
||||
if (!entries)
|
||||
return;
|
||||
// Clear first — all failure paths below then correctly represent "empty",
|
||||
// Clear first - all failure paths below then correctly represent "empty",
|
||||
// even if load() is called on an already-used instance.
|
||||
memset(entries, 0, WARM_NODE_COUNT * sizeof(WarmNodeEntry));
|
||||
concurrency::LockGuard g(spiLock);
|
||||
|
||||
@@ -23,21 +23,21 @@
|
||||
* Warm ("long-tail") node tier.
|
||||
*
|
||||
* Minimal identity record (NodeNum, last_heard, Curve25519 public key) for nodes
|
||||
* evicted from the hot NodeInfoLite store, so DMs to/from them keep encrypting —
|
||||
* evicted from the hot NodeInfoLite store, so DMs to/from them keep encrypting -
|
||||
* the key is expensive to re-learn, the rest rebuilds from traffic in seconds.
|
||||
* Flat fixed array, linear scan (only on hot-store misses), LRU by last_heard
|
||||
* with keyed entries outranking keyless.
|
||||
*
|
||||
* Persistence: nRF52840 uses a 12 KB raw-flash record-ring below LittleFS
|
||||
* (append + replay + compact-on-rotate — see the backend in WarmNodeStore.cpp,
|
||||
* (append + replay + compact-on-rotate - see the backend in WarmNodeStore.cpp,
|
||||
* link-guarded by nrf52840_s140_v7.ld). Everywhere else: /prefs/warm.dat.
|
||||
*/
|
||||
struct WarmNodeEntry {
|
||||
NodeNum num; // 0 = empty slot
|
||||
uint32_t last_heard; // recency for LRU ordering — see the metadata steal below
|
||||
uint32_t last_heard; // recency for LRU ordering - see the metadata steal below
|
||||
uint8_t public_key[32]; // all-zero = no key (a real key is never all-zero)
|
||||
};
|
||||
static_assert(sizeof(WarmNodeEntry) == 40, "WarmNodeEntry must stay 40 B — persistence format depends on it");
|
||||
static_assert(sizeof(WarmNodeEntry) == 40, "WarmNodeEntry must stay 40 B - persistence format depends on it");
|
||||
|
||||
// Metadata packed into the low bits of last_heard.
|
||||
//
|
||||
@@ -49,7 +49,7 @@ static_assert(sizeof(WarmNodeEntry) == 40, "WarmNodeEntry must stay 40 B — per
|
||||
//
|
||||
// Safe because: a real timestamp can never be all-ones (the tombstone sentinel) before
|
||||
// 2106, and tombstones/erased flash are detected via num before last_heard is read. Only
|
||||
// the LOW bits are stolen — the high (era) bits are untouched, so the time range is intact.
|
||||
// the LOW bits are stolen - the high (era) bits are untouched, so the time range is intact.
|
||||
static constexpr uint32_t WARM_META_BITS = 6; // role(4) + protected(2)
|
||||
static constexpr uint32_t WARM_META_MASK = (1u << WARM_META_BITS) - 1; // 0x3F → 64 s quantum
|
||||
static constexpr uint32_t WARM_TIME_MASK = ~WARM_META_MASK; // 0xFFFFFFC0
|
||||
|
||||
@@ -26,7 +26,7 @@ static constexpr const char *PROTOBUF_SCHEMA =
|
||||
// PhoneAPI subclass for the Ethernet HTTP transport. Mirrors mesh/http/HttpAPI
|
||||
// but lives outside the MESHTASTIC_EXCLUDE_WEBSERVER gate (which is ESP32-only).
|
||||
// A single instance is shared between the HTTP and HTTPS servers since they
|
||||
// represent the same logical "phone" — same state machine, same packet queue.
|
||||
// represent the same logical "phone" - same state machine, same packet queue.
|
||||
class EthHttpAPI : public PhoneAPI
|
||||
{
|
||||
public:
|
||||
@@ -192,7 +192,7 @@ static bool handleFromRadio(IStreamReadWrite &client, const Request &req)
|
||||
// Buffer all packets first so we can emit an accurate Content-Length and
|
||||
// keep the connection alive. Phase 2 used Connection: close framing, which
|
||||
// forced clients to redo the TLS handshake (~625 ms) for every single
|
||||
// /fromradio poll — client.meshtastic.org needs dozens of those during
|
||||
// /fromradio poll - client.meshtastic.org needs dozens of those during
|
||||
// initial sync, so the user-visible load time was 15-30 s of pure
|
||||
// handshakes. With Content-Length + keep-alive a whole sync rides one
|
||||
// handshake. Buffer is dynamic (std::vector) so the common 1-packet case
|
||||
@@ -290,7 +290,7 @@ void handleApiClient(IStreamReadWrite &client)
|
||||
// the parent OSThread is not returning to mainController, and the
|
||||
// RP2350 hardware watchdog (8 s default in arduino-pico) only gets
|
||||
// pet by the main loop. A client.meshtastic.org sync produces ~80
|
||||
// back-to-back requests over a single TLS session — well past the
|
||||
// back-to-back requests over a single TLS session - well past the
|
||||
// watchdog deadline. yield() between requests lets the rest of core0
|
||||
// (Periodic ticks, NTP, MQTT, LoRa packet pump) run + pets the
|
||||
// watchdog; the cap puts a hard ceiling so a chatty client can never
|
||||
@@ -320,7 +320,7 @@ void handleApiClient(IStreamReadWrite &client)
|
||||
keepAlive = handleToRadio(client, req);
|
||||
} else {
|
||||
sendError(client, 404, "Not Found", "unknown endpoint");
|
||||
return; // errors are terminal — Connection: close framing
|
||||
return; // errors are terminal - Connection: close framing
|
||||
}
|
||||
// A handler that emitted an error advertised Connection: close. Stop the
|
||||
// keep-alive loop so any unread/leftover body bytes (e.g. after a 408
|
||||
|
||||
@@ -13,14 +13,14 @@
|
||||
// transports without recompiling the handlers.
|
||||
//
|
||||
// Inherits Print so all `print(int)`, `print(const char *)`, `print(char)`
|
||||
// helpers are available for free — the only thing implementations have to
|
||||
// helpers are available for free - the only thing implementations have to
|
||||
// supply on the write side is `write(uint8_t)` + the bulk `write(buf, len)`.
|
||||
class IStreamReadWrite : public Print
|
||||
{
|
||||
public:
|
||||
virtual ~IStreamReadWrite() = default;
|
||||
|
||||
// Write side — Print pure virtual + bulk override
|
||||
// Write side - Print pure virtual + bulk override
|
||||
size_t write(uint8_t b) override = 0;
|
||||
size_t write(const uint8_t *buf, size_t len) override = 0;
|
||||
using Print::write; // bring in write(const char *str) and friends
|
||||
@@ -34,7 +34,7 @@ class IStreamReadWrite : public Print
|
||||
virtual bool connected() = 0;
|
||||
void flush() override = 0; // Print::flush is virtual void with empty default
|
||||
|
||||
// Logging helper — used by request log line
|
||||
// Logging helper - used by request log line
|
||||
virtual IPAddress remoteIP() = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ static EthApiServerThread *apiThread = nullptr;
|
||||
|
||||
void initEthApiServer()
|
||||
{
|
||||
// Bind the listener (idempotent — deInitEthApiServer() drops apiServer on a
|
||||
// Bind the listener (idempotent - deInitEthApiServer() drops apiServer on a
|
||||
// W5500 reset, and this rebinds it on the restart path).
|
||||
if (!apiServer) {
|
||||
apiServer = new EthernetServer(ETH_API_PORT);
|
||||
@@ -92,7 +92,7 @@ void initEthApiServer()
|
||||
}
|
||||
// The worker is created once and kept for the lifetime of the process. It
|
||||
// idles harmlessly while apiServer is null (runOnce guards on it), so we
|
||||
// never delete it from another thread's runOnce — that would corrupt the
|
||||
// never delete it from another thread's runOnce - that would corrupt the
|
||||
// scheduler's thread list mid-iteration.
|
||||
if (!apiThread)
|
||||
apiThread = new EthApiServerThread(); // OSThread base auto-registers with the scheduler
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
/// Initialize the Ethernet HTTP API server (call after Ethernet is connected).
|
||||
/// Spawns an internal OSThread that polls accept() on a sub-second cadence,
|
||||
/// independent of the 5s Ethernet client periodic — needed because the web
|
||||
/// independent of the 5s Ethernet client periodic - needed because the web
|
||||
/// client makes many small back-to-back requests.
|
||||
void initEthApiServer();
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ static constexpr const char *CERT_PATH = "/eth_cert_v2.der";
|
||||
static constexpr const char *KEY_PATH = "/eth_key_v2.der";
|
||||
static constexpr const char *IP_PATH = "/eth_cert_ip_v2.txt";
|
||||
|
||||
// Random callback for mbedtls — sources entropy from the RP2350 ROSC TRNG via
|
||||
// Random callback for mbedtls - sources entropy from the RP2350 ROSC TRNG via
|
||||
// pico-sdk get_rand_64(). Used directly as f_rng in mbedtls calls so we don't
|
||||
// have to plumb a full mbedtls_entropy_context + ctr_drbg. The hardware TRNG
|
||||
// is cryptographically suitable per pico-sdk docs (ROSC + whitening).
|
||||
@@ -175,7 +175,7 @@ static bool generateCert(IPAddress ip, EthCertMaterial &out)
|
||||
}
|
||||
|
||||
// ExtendedKeyUsage: serverAuth. NSS / Firefox refuse to treat a cert
|
||||
// as a TLS server cert without this extension since 2023 — the error
|
||||
// as a TLS server cert without this extension since 2023 - the error
|
||||
// surfaces as a non-overridable "Secure Connection Failed" with no
|
||||
// "Accept the Risk" path.
|
||||
mbedtls_asn1_sequence ekuSeq;
|
||||
@@ -293,13 +293,13 @@ bool ensureCertForIp(IPAddress ip, EthCertMaterial &out)
|
||||
// current process still has the in-memory cert ready for use.
|
||||
//
|
||||
// Clear the IP commit-marker FIRST so a reset mid-write can't leave the marker
|
||||
// pointing at a half-written (or stale-paired) cert/key — the load path only
|
||||
// pointing at a half-written (or stale-paired) cert/key - the load path only
|
||||
// trusts the cache when the marker matches. Writing the marker LAST commits the
|
||||
// new pair atomically w.r.t. the loader.
|
||||
writeText(IP_PATH, "");
|
||||
if (!writeBinary(CERT_PATH, out.certDer.data(), out.certDer.size()) ||
|
||||
!writeBinary(KEY_PATH, out.keyDer.data(), out.keyDer.size()) || !writeText(IP_PATH, ipStr)) {
|
||||
LOG_WARN("ETH CERT: persist failed — will regenerate next boot");
|
||||
LOG_WARN("ETH CERT: persist failed - will regenerate next boot");
|
||||
} else {
|
||||
LOG_INFO("ETH CERT: persisted to LittleFS");
|
||||
}
|
||||
@@ -310,7 +310,7 @@ bool ensureCertForIp(IPAddress ip, EthCertMaterial &out)
|
||||
// Worker that defers cert gen off the Periodic thread (which has a tight stack
|
||||
// and ticks every 5s alongside reconnect / NTP / MQTT). Waits for a non-zero IP,
|
||||
// generates/loads the cert for it, then keeps polling at CERT_RECHECK_MS so a
|
||||
// DHCP lease change to a new IP regenerates the cert — the SAN must track the
|
||||
// DHCP lease change to a new IP regenerates the cert - the SAN must track the
|
||||
// current address or browsers reject the new one. The steady-state poll is just
|
||||
// localIP() + compare; ECDSA keygen only reruns when the IP actually changes,
|
||||
// and it runs on this thread's own stack (not the Periodic's).
|
||||
@@ -337,7 +337,7 @@ class EthCertThread : public concurrency::OSThread
|
||||
// regenerates whenever its saved IP != ip, so the cert SAN follows.
|
||||
bool ok = ensureCertForIp(ip, material_);
|
||||
if (!ok) {
|
||||
LOG_ERROR("ETH CERT: pipeline FAILED — TLS server will not start");
|
||||
LOG_ERROR("ETH CERT: pipeline FAILED - TLS server will not start");
|
||||
// Don't leave isReady() reporting true with empty material: a later TLS
|
||||
// teardown (e.g. a W5500 reset) would then fail initTlsContext() and stay
|
||||
// disabled. Clear readiness so the TLS worker waits and the next poll
|
||||
|
||||
@@ -35,7 +35,7 @@ bool isEthCertReady();
|
||||
// Snapshot of the generated material once isEthCertReady(). Empty otherwise.
|
||||
const EthCertMaterial &getEthCert();
|
||||
|
||||
// Monotonic counter bumped each time the cert is (re)generated — e.g. when a DHCP
|
||||
// Monotonic counter bumped each time the cert is (re)generated - e.g. when a DHCP
|
||||
// lease change moves us to a new IP. The TLS server reloads when this changes.
|
||||
uint32_t getEthCertGeneration();
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
#include "mesh/eth/ethTlsApiServer.h"
|
||||
#endif
|
||||
#ifdef USE_ARDUINO_ETHERNET
|
||||
#include <Ethernet.h> // arduino-libraries/Ethernet — supports W5100/W5200/W5500
|
||||
#include <Ethernet.h> // arduino-libraries/Ethernet - supports W5100/W5200/W5500
|
||||
// Shorter DHCP timeout so LoRa startup isn't blocked when no DHCP server is present.
|
||||
#define ETH_DHCP_TIMEOUT_MS 10000
|
||||
#else
|
||||
@@ -181,12 +181,12 @@ static int32_t reconnectETH()
|
||||
initEthApiServer();
|
||||
#endif
|
||||
#if HAS_ETHERNET && defined(HAS_ETHERNET_TLS_API) && defined(ARCH_RP2040)
|
||||
// Phase 2.1-bis — cert gen runs on its own OSThread so ECDSA keygen
|
||||
// Phase 2.1-bis - cert gen runs on its own OSThread so ECDSA keygen
|
||||
// + DER encoding + LittleFS write don't share the Periodic stack
|
||||
// (which overflowed in the original inline attempt). The thread
|
||||
// polls for a non-zero IP itself and runs once.
|
||||
initEthCertThread();
|
||||
// Phase 2.2 — TLS server skeleton on TCP/443. The worker waits
|
||||
// Phase 2.2 - TLS server skeleton on TCP/443. The worker waits
|
||||
// until the cert thread signals isEthCertReady() before binding.
|
||||
initEthTlsApiServer();
|
||||
#endif
|
||||
@@ -220,7 +220,7 @@ static int32_t reconnectETH()
|
||||
#if HAS_ETHERNET && defined(HAS_ETHERNET_OTA)
|
||||
ethOTALoop();
|
||||
#endif
|
||||
// ethApiServer runs on its own OSThread (20ms ticks) — not polled here.
|
||||
// ethApiServer runs on its own OSThread (20ms ticks) - not polled here.
|
||||
|
||||
return 5000; // every 5 seconds
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ static const uint32_t OTA_AUTH_COOLDOWN_MS = 5000; // 5s cooldown after failed a
|
||||
static const size_t OTA_NONCE_SIZE = 32;
|
||||
static const size_t OTA_HASH_SIZE = 32;
|
||||
|
||||
// OTA PSK — override via USERPREFS_OTA_PSK in userPrefs.jsonc
|
||||
// OTA PSK - override via USERPREFS_OTA_PSK in userPrefs.jsonc
|
||||
// USERPREFS_OTA_PSK is stringified by PlatformIO (wrapped in quotes), so we
|
||||
// use a char[] and sizeof-1 to exclude the trailing NUL byte from the hash.
|
||||
#ifdef USERPREFS_OTA_PSK
|
||||
@@ -96,7 +96,7 @@ static void computeAuthHash(const uint8_t *nonce, size_t nonceLen, const uint8_t
|
||||
/// Challenge-response authentication. Returns true if client is authenticated.
|
||||
static bool authenticateClient(EthernetClient &client)
|
||||
{
|
||||
// Rate-limit after failed auth — close silently so the error byte is not
|
||||
// Rate-limit after failed auth - close silently so the error byte is not
|
||||
// misinterpreted as part of the nonce by a re-trying client.
|
||||
if (lastAuthFailure != 0 && (millis() - lastAuthFailure) < OTA_AUTH_COOLDOWN_MS) {
|
||||
LOG_WARN("ETH OTA: Auth cooldown active, rejecting connection");
|
||||
@@ -140,7 +140,7 @@ static bool authenticateClient(EthernetClient &client)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Auth success — send ACK
|
||||
// Auth success - send ACK
|
||||
client.write(OTA_ACK);
|
||||
LOG_INFO("ETH OTA: Authentication successful");
|
||||
return true;
|
||||
@@ -179,14 +179,14 @@ static void handleOTAClient(EthernetClient &client)
|
||||
return;
|
||||
}
|
||||
|
||||
// Begin the update — this opens firmware.bin on LittleFS
|
||||
// Begin the update - this opens firmware.bin on LittleFS
|
||||
if (!Update.begin(hdr.firmwareSize)) {
|
||||
LOG_ERROR("ETH OTA: Update.begin() failed, error=%u", Update.getError());
|
||||
client.write(OTA_ERR_BEGIN);
|
||||
return;
|
||||
}
|
||||
|
||||
// ACK the header — client can start sending firmware data
|
||||
// ACK the header - client can start sending firmware data
|
||||
client.write(OTA_ACK);
|
||||
|
||||
// Receive firmware in chunks
|
||||
@@ -252,7 +252,7 @@ static void handleOTAClient(EthernetClient &client)
|
||||
return;
|
||||
}
|
||||
|
||||
// Finalize — this calls picoOTA.commit() which stages the update for the
|
||||
// Finalize - this calls picoOTA.commit() which stages the update for the
|
||||
// bootloader
|
||||
if (!Update.end(true)) {
|
||||
LOG_ERROR("ETH OTA: Update.end() failed, error=%u", Update.getError());
|
||||
@@ -265,7 +265,7 @@ static void handleOTAClient(EthernetClient &client)
|
||||
client.flush();
|
||||
delay(500);
|
||||
|
||||
// Reboot — the built-in bootloader will apply the update from LittleFS
|
||||
// Reboot - the built-in bootloader will apply the update from LittleFS
|
||||
rp2040.reboot();
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ static constexpr uint32_t MEDIUM_THRESHOLD_MS = 30000;
|
||||
static constexpr int32_t ACTIVE_INTERVAL_MS = 20;
|
||||
static constexpr int32_t MEDIUM_INTERVAL_MS = 100;
|
||||
static constexpr int32_t IDLE_INTERVAL_MS = 500;
|
||||
// Matches the keep-alive idle window in ethApiHandlers — if the handler
|
||||
// Matches the keep-alive idle window in ethApiHandlers - if the handler
|
||||
// loop calls read() and netRecv blocked for 10 s, the 3 s idle deadline
|
||||
// inside parseRequest would be irrelevant and the OSThread would stay stuck
|
||||
// long after a quiet browser closed its end of the TCP socket.
|
||||
@@ -52,7 +52,7 @@ static int picoRand(void * /*ctx*/, unsigned char *out, size_t len)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// One-shot TLS context lives in BSS — keeps mbedtls allocations off the
|
||||
// One-shot TLS context lives in BSS - keeps mbedtls allocations off the
|
||||
// OSThread stack (lesson from Phase 2.1-bis: stack budget is tight on M33).
|
||||
static EthernetServer *tlsServer = nullptr;
|
||||
static mbedtls_x509_crt certChain;
|
||||
@@ -73,7 +73,7 @@ static int netSend(void *ctx, const unsigned char *buf, size_t len)
|
||||
// Block-with-yield until the W5500 TX buffer can absorb the chunk.
|
||||
// Returning WANT_WRITE without delay made mbedtls_ssl_handshake() spin
|
||||
// at ~180k iter/s when Chrome was slow to drain the socket during the
|
||||
// ECDHE-ECDSA ServerKeyExchange — the original code logged exactly that
|
||||
// ECDHE-ECDSA ServerKeyExchange - the original code logged exactly that
|
||||
// signature (ret=-0x6880 / WANT_WRITE) tight-looping forever. Firefox
|
||||
// happened to read fast enough that the buffer never filled.
|
||||
uint32_t t0 = millis();
|
||||
@@ -96,12 +96,12 @@ static int netRecv(void *ctx, unsigned char *buf, size_t len)
|
||||
// Block-with-timeout: spin until bytes arrive, the peer closes, or we
|
||||
// exceed the per-recv budget. Pure non-blocking (return WANT_READ) would
|
||||
// require mbedtls_ssl_handshake to be driven from the runOnce dispatcher
|
||||
// — overkill for the Phase 2.2 skeleton with a single in-flight session.
|
||||
// - overkill for the Phase 2.2 skeleton with a single in-flight session.
|
||||
//
|
||||
// Pet the 8 s hardware watchdog from inside the poll loop. We sit here
|
||||
// for up to RECV_TIMEOUT_MS waiting for the next keep-alive request, and
|
||||
// a quiet client can string two such waits back-to-back (6 s) plus the
|
||||
// earlier handshake/handler time — easily past the watchdog deadline.
|
||||
// earlier handshake/handler time - easily past the watchdog deadline.
|
||||
// The main loop()'s watchdog_update() never runs while the OSThread is
|
||||
// inside serveClient(), so it has to be done here.
|
||||
uint32_t t0 = millis();
|
||||
@@ -152,7 +152,7 @@ class MbedTlsStream : public IStreamReadWrite
|
||||
size_t pending = mbedtls_ssl_get_bytes_avail(ssl_);
|
||||
if (pending > 0)
|
||||
return (int)pending;
|
||||
// Best-effort: report network bytes (rough proxy — handlers usually
|
||||
// Best-effort: report network bytes (rough proxy - handlers usually
|
||||
// call read() in a loop and tolerate slow streams).
|
||||
return client_->available();
|
||||
}
|
||||
@@ -197,7 +197,7 @@ class EthTlsApiServerThread : public concurrency::OSThread
|
||||
if (!isEthCertReady())
|
||||
return 500;
|
||||
if (!initTlsContext())
|
||||
return INT32_MAX; // hard fail — TLS server stays disabled
|
||||
return INT32_MAX; // hard fail - TLS server stays disabled
|
||||
loadedCertGen_ = getEthCertGeneration();
|
||||
tlsReady = true;
|
||||
}
|
||||
@@ -330,7 +330,7 @@ void deInitEthTlsApiServer()
|
||||
{
|
||||
// A W5500 chip reset leaves tlsServer bound to a dead socket and the cached
|
||||
// mbedTLS context stale. Reset the worker back to Phase A (free the context,
|
||||
// drop the listener, clear tlsReady) WITHOUT deleting the OSThread — its next
|
||||
// drop the listener, clear tlsReady) WITHOUT deleting the OSThread - its next
|
||||
// runOnce re-waits for isEthCertReady() and rebuilds the context + rebinds
|
||||
// TCP/443. Safe to free here: this runs in reconnectETH (ethConnect thread),
|
||||
// and the cooperative scheduler guarantees tlsThread is not mid-runOnce, so
|
||||
|
||||
@@ -184,7 +184,7 @@ typedef struct _meshtastic_LockdownAuth {
|
||||
token at unlock time: the client-supplied boots_remaining when
|
||||
non-zero, otherwise the firmware default (TOKEN_DEFAULT_BOOTS).
|
||||
Note that boots_remaining == 0 in this message means "use firmware
|
||||
default", NOT "zero boots" — a client computing the ceiling for
|
||||
default", NOT "zero boots" - a client computing the ceiling for
|
||||
display should mirror that resolution rather than multiplying the
|
||||
raw request value.
|
||||
|
||||
@@ -194,7 +194,7 @@ typedef struct _meshtastic_LockdownAuth {
|
||||
|
||||
Uses millis() (CPU uptime), not wall-clock time, so the cap is
|
||||
immune to GPS spoofing, RTC backup-battery removal, and Faraday
|
||||
cage isolation — none of those move the uptime counter. The only
|
||||
cage isolation - none of those move the uptime counter. The only
|
||||
way to reset the session clock is a reboot, which costs a boot
|
||||
from the on-flash, HMAC-bound counter. */
|
||||
uint32_t max_session_seconds;
|
||||
@@ -211,7 +211,7 @@ typedef struct _meshtastic_LockdownAuth {
|
||||
|
||||
NOT reversed by this operation: APPROTECT. Once the debug port
|
||||
lockout has been burned (on silicon where it is effective) it is
|
||||
permanent — disabling lockdown decrypts your data and removes the
|
||||
permanent - disabling lockdown decrypts your data and removes the
|
||||
access gates, but the SWD/JTAG port stays locked for the life of
|
||||
the device (recoverable only via a full chip erase over a debug
|
||||
probe, which destroys all data). Clients should make this
|
||||
|
||||
@@ -332,7 +332,7 @@ typedef enum _meshtastic_CotType {
|
||||
/* y-: TAKTALK room/membership broadcast. Payload carried via the
|
||||
TakTalkRoomData typed variant (sender_callsign, room_id, room_name,
|
||||
participants). The CoT type literally has a trailing dash and no
|
||||
second atom — not a typo. */
|
||||
second atom - not a typo. */
|
||||
meshtastic_CotType_CotType_y = 126
|
||||
} meshtastic_CotType;
|
||||
|
||||
@@ -380,7 +380,7 @@ typedef enum _meshtastic_DrawnShape_Kind {
|
||||
/* u-r-b-bullseye: Bullseye ring with range rings and bearing reference */
|
||||
meshtastic_DrawnShape_Kind_Kind_Bullseye = 7,
|
||||
/* u-d-c-e: Ellipse with distinct major/minor axes (same storage as
|
||||
Kind_Circle — uses major_cm/minor_cm/angle_deg — but receivers
|
||||
Kind_Circle - uses major_cm/minor_cm/angle_deg - but receivers
|
||||
render it as a non-circular ellipse rather than a round circle). */
|
||||
meshtastic_DrawnShape_Kind_Kind_Ellipse = 8,
|
||||
/* u-d-v: 2D vehicle outline drawn on the map. Vertices carry the
|
||||
@@ -400,7 +400,7 @@ typedef enum _meshtastic_DrawnShape_Kind {
|
||||
end of parse; builder uses it to decide which of <strokeColor> /
|
||||
<fillColor> to emit in the reconstructed XML. */
|
||||
typedef enum _meshtastic_DrawnShape_StyleMode {
|
||||
/* Unspecified — receiver infers from which color fields are non-zero. */
|
||||
/* Unspecified - receiver infers from which color fields are non-zero. */
|
||||
meshtastic_DrawnShape_StyleMode_StyleMode_Unspecified = 0,
|
||||
/* Stroke only. No <fillColor> in the source XML. Used for polylines,
|
||||
ranging lines, bullseye rings. */
|
||||
@@ -417,7 +417,7 @@ typedef enum _meshtastic_DrawnShape_StyleMode {
|
||||
alone is ambiguous (e.g. a-u-G could be a 2525 symbol or a custom icon
|
||||
depending on the iconset path). */
|
||||
typedef enum _meshtastic_Marker_Kind {
|
||||
/* Unspecified — fall back to TAKPacketV2.cot_type_id */
|
||||
/* Unspecified - fall back to TAKPacketV2.cot_type_id */
|
||||
meshtastic_Marker_Kind_Kind_Unspecified = 0,
|
||||
/* b-m-p-s-m: Spot map marker */
|
||||
meshtastic_Marker_Kind_Kind_Spot = 1,
|
||||
@@ -680,10 +680,10 @@ typedef struct _meshtastic_AircraftTrack {
|
||||
hundred meters of the anchor has per-vertex deltas in the ±10^4 range.
|
||||
Under sint32+zigzag those encode as 2 bytes each (tag+varint), versus the
|
||||
4 bytes that sfixed32 would always require. At 32 vertices that is ~128
|
||||
bytes of savings — the difference between fitting under the LoRa MTU or
|
||||
bytes of savings - the difference between fitting under the LoRa MTU or
|
||||
not. Absolute coordinates (values ~10^9) would cost sint32 varint 5 bytes
|
||||
per field, which is why TAKPacketV2's top-level latitude_i / longitude_i
|
||||
stay sfixed32 — only small values win with sint32. */
|
||||
stay sfixed32 - only small values win with sint32. */
|
||||
typedef struct _meshtastic_CotGeoPoint {
|
||||
/* Latitude delta from TAKPacketV2.latitude_i, in 1e-7 degree units.
|
||||
Add to the enclosing event's latitude_i to recover the absolute latitude. */
|
||||
@@ -791,7 +791,7 @@ typedef struct _meshtastic_Marker {
|
||||
|
||||
Covers CoT type u-rb-a. The anchor position is on
|
||||
TAKPacketV2.latitude_i/longitude_i; the target endpoint is carried as a
|
||||
CotGeoPoint — same delta-from-anchor encoding used by DrawnShape.vertices
|
||||
CotGeoPoint - same delta-from-anchor encoding used by DrawnShape.vertices
|
||||
so a self-anchored RAB (common case) encodes in zero bytes. */
|
||||
typedef struct _meshtastic_RangeAndBearing {
|
||||
/* Target/anchor endpoint (delta-encoded from TAKPacketV2.latitude_i/longitude_i). */
|
||||
@@ -899,12 +899,12 @@ typedef struct _meshtastic_CasevacReport {
|
||||
same as the envelope callsign but ATAK sometimes carries a distinct
|
||||
ops-number here. */
|
||||
pb_callback_t title;
|
||||
/* Primary medline free-text — the single most clinically important line
|
||||
/* Primary medline free-text - the single most clinically important line
|
||||
on a MEDLINE form (e.g. "2 urgent litter patients, smoke on approach").
|
||||
MUST be preserved under MTU pressure as long as any casevac is sent. */
|
||||
pb_callback_t medline_remarks;
|
||||
/* Line 3 (newer ATAK format): patient counts by precedence level.
|
||||
Coexists with the enum-style `precedence` field (tag 1) — older ATAK
|
||||
Coexists with the enum-style `precedence` field (tag 1) - older ATAK
|
||||
emits a single enum, newer ATAK emits these counts, and both can be
|
||||
set simultaneously. Senders populate whichever style(s) the source
|
||||
XML had; receivers prefer counts when non-zero. */
|
||||
@@ -946,19 +946,19 @@ typedef struct _meshtastic_CasevacReport {
|
||||
(e.g. "Primary HLZ is soccer field"). */
|
||||
pb_callback_t hlz_remarks;
|
||||
/* Per-patient clinical records. Each entry is one patient's ZMIST card
|
||||
(Zap number / Mechanism / Injuries / Signs / Treatment). Repeatable —
|
||||
(Zap number / Mechanism / Injuries / Signs / Treatment). Repeatable -
|
||||
a mass-casualty event can carry 1-6 entries in practice, limited by
|
||||
the 237 B LoRa MTU. */
|
||||
pb_callback_t zmist;
|
||||
} meshtastic_CasevacReport;
|
||||
|
||||
/* Per-patient clinical summary record — one entry per patient in a CASEVAC.
|
||||
/* Per-patient clinical summary record - one entry per patient in a CASEVAC.
|
||||
Maps directly to ATAK's <zMist> child element inside <zMistsMap>.
|
||||
All fields are optional free-text; senders populate what they have. */
|
||||
typedef struct _meshtastic_ZMistEntry {
|
||||
/* Patient identifier / sequence label (e.g. "ZMIST-1", "ZMIST-2"). */
|
||||
pb_callback_t title;
|
||||
/* Zap number — unique patient tracking ID (often a terse code like
|
||||
/* Zap number - unique patient tracking ID (often a terse code like
|
||||
"Gunshot" or a serial). */
|
||||
pb_callback_t z;
|
||||
/* Mechanism of injury (e.g. "Penetrating trauma", "Blast injury"). */
|
||||
@@ -997,7 +997,7 @@ typedef struct _meshtastic_EmergencyAlert {
|
||||
creation time; the fields below carry structured metadata the raw-detail
|
||||
fallback currently loses.
|
||||
|
||||
Fields are deliberately lean — this variant is closer to the MTU ceiling
|
||||
Fields are deliberately lean - this variant is closer to the MTU ceiling
|
||||
than the others, so every string is capped in options. */
|
||||
typedef struct _meshtastic_TaskRequest {
|
||||
/* Short tag for the task category (e.g. "engage", "observe", "recon",
|
||||
@@ -1017,7 +1017,7 @@ typedef struct _meshtastic_TaskRequest {
|
||||
|
||||
/* Weather annotation from <environment> CoT detail element.
|
||||
|
||||
Attaches to any TAKPacketV2 regardless of payload_variant — an Aircraft,
|
||||
Attaches to any TAKPacketV2 regardless of payload_variant - an Aircraft,
|
||||
PLI, or Marker can all carry observed conditions at the emitting station.
|
||||
ATAK-CIV ships an XSD for <environment> but no dedicated handler, so the
|
||||
element round-trips through the generic detail pipeline; this message
|
||||
@@ -1026,7 +1026,7 @@ typedef struct _meshtastic_TaskRequest {
|
||||
Target wire cost: ~6-8 bytes compressed with a fully populated instance.
|
||||
|
||||
Named `TAKEnvironment` (not just `Environment`) because the bare name
|
||||
collides with `SwiftUI.Environment` — every SwiftUI view in a consuming
|
||||
collides with `SwiftUI.Environment` - every SwiftUI view in a consuming
|
||||
iOS app uses the `@Environment` property wrapper, and importing the
|
||||
generated proto module would make `Environment` ambiguous in every one
|
||||
of those files. The `TAK` prefix matches the convention used by the
|
||||
@@ -1055,7 +1055,7 @@ typedef struct _meshtastic_TAKEnvironment {
|
||||
The receiving ATAK client restores those from its own defaults, same as
|
||||
every other CoT carried over Meshtastic today.
|
||||
|
||||
Attaches to any TAKPacketV2 — a PLI with a sensor on the operator's head,
|
||||
Attaches to any TAKPacketV2 - a PLI with a sensor on the operator's head,
|
||||
an Aircraft with a FLIR turret, a Marker dropped on a UAV.
|
||||
Target wire cost: ~7-14 bytes compressed (dominated by model string). */
|
||||
typedef struct _meshtastic_SensorFov {
|
||||
@@ -1065,30 +1065,30 @@ typedef struct _meshtastic_SensorFov {
|
||||
SensorDetailHandler default (270°) and save varint bytes over centi-deg. */
|
||||
uint32_t azimuth_deg;
|
||||
/* Maximum range of the cone in meters.
|
||||
Optional — if unset, receivers should use the ATAK-CIV default of 100m. */
|
||||
Optional - if unset, receivers should use the ATAK-CIV default of 100m. */
|
||||
bool has_range_m;
|
||||
uint32_t range_m;
|
||||
/* Horizontal field of view in whole degrees (cone's angular width).
|
||||
ATAK-CIV default is 45°. */
|
||||
uint32_t fov_horizontal_deg;
|
||||
/* Vertical field of view in whole degrees. ATAK-CIV default is 45°.
|
||||
Optional — a value of 0 means "not set / use horizontal FOV". */
|
||||
Optional - a value of 0 means "not set / use horizontal FOV". */
|
||||
uint32_t fov_vertical_deg;
|
||||
/* Elevation angle in whole degrees. Positive = up, negative = down.
|
||||
Range -90 to +90. sint32 for varint efficiency on small negatives. */
|
||||
int32_t elevation_deg;
|
||||
/* Roll (camera tilt) in whole degrees, -180 to +180.
|
||||
Optional — use 0 if the sensor doesn't track roll. */
|
||||
Optional - use 0 if the sensor doesn't track roll. */
|
||||
int32_t roll_deg;
|
||||
/* Free-form device model identifier, e.g. "FLIR-Boson-640", "SEEK".
|
||||
Optional — empty string means "unknown model" (ATAK-CIV default). */
|
||||
Optional - empty string means "unknown model" (ATAK-CIV default). */
|
||||
pb_callback_t model;
|
||||
} meshtastic_SensorFov;
|
||||
|
||||
/* TAKTALK chat message payload (CoT type m-t-t).
|
||||
|
||||
TAKTALK is an ATAK plugin for voice + text team messaging. The voice
|
||||
audio stream goes over UDP/RTP and is NOT carried by the mesh — only
|
||||
audio stream goes over UDP/RTP and is NOT carried by the mesh - only
|
||||
the text envelope (this message) is. `from_voice` marks messages sent
|
||||
via push-to-talk speech-to-text so receivers can render a mic icon
|
||||
next to the text.
|
||||
@@ -1122,7 +1122,7 @@ typedef struct _meshtastic_TakTalkMessage {
|
||||
Announces a TAKTALK chatroom's friendly name and roster so peers can
|
||||
resolve room UUIDs (used in TakTalkMessage.chatroom_id and
|
||||
GeoChat.room_id) to a display name and participant list. Not a chat
|
||||
message itself — these events are emitted by TAKTALK when rooms are
|
||||
message itself - these events are emitted by TAKTALK when rooms are
|
||||
created or memberships change. */
|
||||
typedef struct _meshtastic_TakTalkRoomData {
|
||||
/* Callsign of the device broadcasting the room state (typically the
|
||||
@@ -1161,7 +1161,7 @@ typedef struct _meshtastic_Marti {
|
||||
primary-vs-cc distinction the same way ATAK does.
|
||||
|
||||
If dest_callsign is [TAKPacketV2.callsign] (self-addressed, unusual but
|
||||
legal — e.g. ATAK echoing back to its own room), the builder still emits
|
||||
legal - e.g. ATAK echoing back to its own room), the builder still emits
|
||||
the element so loopback shapes round-trip cleanly. */
|
||||
pb_callback_t dest_callsign;
|
||||
} meshtastic_Marti;
|
||||
|
||||
@@ -1244,15 +1244,15 @@ typedef struct _meshtastic_LockdownStatus {
|
||||
/* Current lockdown state being reported. */
|
||||
meshtastic_LockdownStatus_State state;
|
||||
/* For LOCKED: machine-readable reason. Known values:
|
||||
"needs_auth" — storage already unlocked, client must auth
|
||||
"token_missing" — no boot token on flash
|
||||
"token_expired" — boot token wall-clock TTL elapsed
|
||||
"token_boots_zero" — boot token boot-count TTL exhausted
|
||||
"token_hmac_fail" — token tampered or wrong device
|
||||
"token_dek_fail" — token DEK decrypt failed
|
||||
"token_wrong_size" — token file corrupted
|
||||
"token_bad_magic" — token file corrupted
|
||||
"not_provisioned" — should generally use NEEDS_PROVISION state instead
|
||||
"needs_auth" - storage already unlocked, client must auth
|
||||
"token_missing" - no boot token on flash
|
||||
"token_expired" - boot token wall-clock TTL elapsed
|
||||
"token_boots_zero" - boot token boot-count TTL exhausted
|
||||
"token_hmac_fail" - token tampered or wrong device
|
||||
"token_dek_fail" - token DEK decrypt failed
|
||||
"token_wrong_size" - token file corrupted
|
||||
"token_bad_magic" - token file corrupted
|
||||
"not_provisioned" - should generally use NEEDS_PROVISION state instead
|
||||
Other values may be added; clients should treat unknown values as
|
||||
"locked, ask for passphrase". */
|
||||
char lock_reason[32];
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
/* Payload for MESH_BEACON_APP packets.
|
||||
Periodically broadcast by nodes in beacon mode.
|
||||
Listeners deliver the text message to the local inbox and cache any offered
|
||||
channel/preset for the client app to act on — the firmware never auto-applies them. */
|
||||
channel/preset for the client app to act on - the firmware never auto-applies them. */
|
||||
typedef struct _meshtastic_MeshBeacon {
|
||||
/* Human-readable beacon message. Max 100 bytes enforced by firmware on send. */
|
||||
char message[101];
|
||||
|
||||
@@ -497,7 +497,7 @@ typedef struct _meshtastic_ModuleConfig_MeshBeaconConfig {
|
||||
/* Single-target TX channel: channel settings (name + PSK) to send beacons on.
|
||||
If unset, beacons go out on the primary channel. Used only when broadcast_targets is empty.
|
||||
NOTE: the single-target path embeds the ChannelSettings inline here, whereas a
|
||||
broadcast_targets entry references a channel-table slot by channel_index instead — see
|
||||
broadcast_targets entry references a channel-table slot by channel_index instead - see
|
||||
BroadcastTarget. The two paths are equal, first-class options; only this representation differs. */
|
||||
bool has_broadcast_on_channel;
|
||||
meshtastic_ChannelSettings broadcast_on_channel;
|
||||
@@ -514,7 +514,7 @@ typedef struct _meshtastic_ModuleConfig_MeshBeaconConfig {
|
||||
each temporarily switching the radio to that entry's preset/region/channel.
|
||||
When empty, the broadcaster uses the scalar broadcast_on_preset / broadcast_on_region /
|
||||
broadcast_on_channel fields instead (the single-target path).
|
||||
Single- and multi-target are equal, first-class options — neither is preferred or
|
||||
Single- and multi-target are equal, first-class options - neither is preferred or
|
||||
deprecated. They differ only in how the TX channel is named: broadcast_on_channel embeds a
|
||||
ChannelSettings inline, while a target references an existing channel-table slot by
|
||||
channel_index (see BroadcastTarget). */
|
||||
|
||||
@@ -125,7 +125,7 @@ static inline int get_max_num_nodes()
|
||||
#elif defined(NRF52840_XXAA)
|
||||
// Keyed on the NRF52840_XXAA build flag, not ARCH_NRF52: the latter (from
|
||||
// architecture.h via configuration.h) isn't defined this early in every include
|
||||
// chain. Backed by the raw-flash ring below LittleFS — see WarmNodeStore.h.
|
||||
// chain. Backed by the raw-flash ring below LittleFS - see WarmNodeStore.h.
|
||||
#define WARM_NODE_COUNT 200
|
||||
#elif (defined(CONFIG_IDF_TARGET_ESP32S3) && defined(BOARD_HAS_PSRAM)) || defined(ARCH_PORTDUINO)
|
||||
#define WARM_NODE_COUNT 2000 // PSRAM-equipped ESP32-S3 / native host; warm cache in PSRAM (~80 KB)
|
||||
@@ -137,7 +137,7 @@ static inline int get_max_num_nodes()
|
||||
#define WARM_NODE_COUNT 150 // RP2040 (264 KB) / RP2350 (520 KB): bounded so warm.dat write fits the 8s watchdog (#10746)
|
||||
#else
|
||||
// nRF52840 is handled explicitly above (200, raw-flash ring). Any other nRF52 (non-XXAA) and any
|
||||
// future non-ESP32/non-RP LittleFS part fall through to this 320 default — flag for review if such a
|
||||
// future non-ESP32/non-RP LittleFS part fall through to this 320 default - flag for review if such a
|
||||
// RAM-constrained nRF52 target is ever added.
|
||||
#define WARM_NODE_COUNT 320 // other LittleFS-backed parts (e.g. non-nRF52840 nRF52)
|
||||
#endif // platform
|
||||
@@ -178,7 +178,7 @@ static inline int get_max_num_nodes()
|
||||
#elif defined(ARCH_ESP32)
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 400 // classic ESP32 / S2 / C3: tightest free heap, ~4 KB (#10705)
|
||||
#else
|
||||
// nRF52 (incl. nRF52840) and RP2040/RP2350 fall through here — there is no nRF/RP branch above,
|
||||
// nRF52 (incl. nRF52840) and RP2040/RP2350 fall through here - there is no nRF/RP branch above,
|
||||
// by design. These parts have no ESP32-style WiFi+BLE coexistence eating the heap, so the larger
|
||||
// 1000-entry (~10 KB) cache fits: nRF52840 is BLE-only on 256 KB RAM; RP2040/RP2350 have 264/520 KB.
|
||||
#define TRAFFIC_MANAGEMENT_CACHE_SIZE 1000 // nRF52 / RP2040 / RP2350 / other non-ESP32
|
||||
|
||||
@@ -46,7 +46,7 @@ Author: Marc Philipp Hammermann
|
||||
mail: marchammermann@googlemail.com
|
||||
|
||||
*/
|
||||
// Mirrors the guard in PiWebServer.h — see comment there. macOS Homebrew
|
||||
// Mirrors the guard in PiWebServer.h - see comment there. macOS Homebrew
|
||||
// provides ulfius + deps; Linux pulls them via apt. Either way, this
|
||||
// translation unit only compiles when the headers are present.
|
||||
#ifdef ARCH_PORTDUINO
|
||||
|
||||
@@ -72,7 +72,7 @@ class UdpMulticastHandler final
|
||||
LOG_DEBUG("Decoding MeshPacket from UDP len=%u", packetLength);
|
||||
bool isPacketDecoded = pb_decode_from_bytes(packet.data(), packetLength, &meshtastic_MeshPacket_msg, &mp);
|
||||
if (isPacketDecoded && router && mp.which_payload_variant == meshtastic_MeshPacket_encrypted_tag) {
|
||||
// Drop packets with spoofed local origin — no legitimate LAN node should send from=0 or our own nodeNum
|
||||
// Drop packets with spoofed local origin - no legitimate LAN node should send from=0 or our own nodeNum
|
||||
if (isFromUs(&mp)) {
|
||||
LOG_WARN("UDP packet with spoofed local from=0x%08x, dropping", mp.from);
|
||||
return;
|
||||
|
||||
+2
-2
@@ -138,7 +138,7 @@ bool sanitizeUtf8(char *buf, size_t bufSize)
|
||||
size_t seqLen;
|
||||
uint32_t minCodepoint;
|
||||
if (b <= 0x7F) {
|
||||
// ASCII — valid single byte
|
||||
// ASCII - valid single byte
|
||||
i++;
|
||||
continue;
|
||||
} else if ((b & 0xE0) == 0xC0) {
|
||||
@@ -160,7 +160,7 @@ bool sanitizeUtf8(char *buf, size_t bufSize)
|
||||
|
||||
// Check that we have enough bytes remaining
|
||||
if (i + seqLen > len) {
|
||||
// Truncated sequence at end of string — replace remaining bytes
|
||||
// Truncated sequence at end of string - replace remaining bytes
|
||||
for (size_t j = i; j < len; j++) {
|
||||
buf[j] = '?';
|
||||
}
|
||||
|
||||
+10
-10
@@ -89,7 +89,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
assert(r);
|
||||
|
||||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||||
// While storage is locked, drop every admin payload — both local and
|
||||
// While storage is locked, drop every admin payload - both local and
|
||||
// remote (PKC, mesh-relayed). Lockdown unlock is the prerequisite for
|
||||
// any admin operation: operators must authenticate via lockdown_auth
|
||||
// first. The lockdown_auth path itself is handled synchronously in
|
||||
@@ -100,9 +100,9 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
// operator has even unlocked it.
|
||||
// Only gate when lockdown is ACTIVE. A lockdown-capable build that hasn't
|
||||
// been provisioned (or was disabled) is not unlocked either, but must
|
||||
// still serve admin normally — so check isLockdownActive() first.
|
||||
// still serve admin normally - so check isLockdownActive() first.
|
||||
if (EncryptedStorage::isLockdownActive() && !EncryptedStorage::isUnlocked()) {
|
||||
LOG_WARN("AdminModule: dropping admin payload — storage locked");
|
||||
LOG_WARN("AdminModule: dropping admin payload - storage locked");
|
||||
return handled;
|
||||
}
|
||||
#endif
|
||||
@@ -136,7 +136,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
// local packets into the remote-PKC key check.
|
||||
//
|
||||
// Under MESHTASTIC_PHONEAPI_ACCESS_CONTROL, the per-connection auth
|
||||
// gate lives in PhoneAPI::handleToRadioPacket — any local admin
|
||||
// gate lives in PhoneAPI::handleToRadioPacket - any local admin
|
||||
// payload other than lockdown_auth is dropped there if the
|
||||
// originating connection is unauthorized. By the time we reach
|
||||
// this branch the connection has already proven the passphrase,
|
||||
@@ -168,7 +168,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
// Note: PKC admin does NOT automatically authorize the
|
||||
// originating local PhoneAPI connection for content
|
||||
// redaction purposes. PKC and the per-connection lockdown
|
||||
// auth slot are independent gates — operators using PKC
|
||||
// auth slot are independent gates - operators using PKC
|
||||
// admin from a local app should still send lockdown_auth
|
||||
// separately to unlock the redacted FromRadio stream.
|
||||
// (The previous auto-authorize path read a shared
|
||||
@@ -217,7 +217,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
|
||||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||||
// lockdown_auth is handled synchronously in
|
||||
// PhoneAPI::handleToRadioPacket — see handleLockdownAuthInline. A
|
||||
// PhoneAPI::handleToRadioPacket - see handleLockdownAuthInline. A
|
||||
// packet should not normally reach AdminModule under that flag set,
|
||||
// but if it ever does (e.g. injected via a non-PhoneAPI path), drop
|
||||
// it silently rather than leaking a partial response.
|
||||
@@ -301,7 +301,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
break;
|
||||
}
|
||||
|
||||
// Hardware supports 2.4 GHz — apply the config.
|
||||
// Hardware supports 2.4 GHz - apply the config.
|
||||
// Fail closed: null instance is treated as incapable.
|
||||
if (RadioLibInterface::instance && RadioLibInterface::instance->wideLora()) {
|
||||
LOG_DEBUG("LORA_24 requested, radio hardware supports 2.4 GHz, applying");
|
||||
@@ -493,7 +493,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||
if (screen)
|
||||
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
|
||||
} else if (mp.from == 0) { // local request from the phone — tell the user why it didn't take
|
||||
} else if (mp.from == 0) { // local request from the phone - tell the user why it didn't take
|
||||
sendWarning(NodeDB::PROTECTED_CAP_WARN_FMT, "favorite", r->set_favorite_node, MAX_NUM_NODES - 2);
|
||||
} else {
|
||||
LOG_WARN("Remote set_favorite_node for 0x%08x refused: protected-node cap", r->set_favorite_node);
|
||||
@@ -522,7 +522,7 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
if (nodeDB->setProtectedFlag(node, NODEINFO_BITFIELD_IS_IGNORED_MASK, true)) {
|
||||
nodeDB->eraseNodeSatellites(node->num);
|
||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||
} else if (mp.from == 0) { // local request from the phone — tell the user why it didn't take
|
||||
} else if (mp.from == 0) { // local request from the phone - tell the user why it didn't take
|
||||
sendWarning(NodeDB::PROTECTED_CAP_WARN_FMT, "ignore", r->set_ignored_node, MAX_NUM_NODES - 2);
|
||||
} else {
|
||||
LOG_WARN("Remote set_ignored_node for 0x%08x refused: protected-node cap", r->set_ignored_node);
|
||||
@@ -1271,7 +1271,7 @@ bool AdminModule::handleSetModuleConfig(const meshtastic_ModuleConfig &c)
|
||||
moduleConfig.has_mesh_beacon = true;
|
||||
moduleConfig.mesh_beacon = beaconCfg;
|
||||
shouldReboot = false;
|
||||
// Payload content changed — invalidate the broadcaster's cache.
|
||||
// Payload content changed - invalidate the broadcaster's cache.
|
||||
if (meshBeaconBroadcastModule)
|
||||
meshBeaconBroadcastModule->invalidateCache();
|
||||
break;
|
||||
|
||||
@@ -6,6 +6,6 @@ AtakPluginModule::AtakPluginModule() : SinglePortModule("atak", meshtastic_PortN
|
||||
|
||||
ProcessMessage AtakPluginModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
(void)mp; // Passthrough — no processing needed, apps handle compression/decompression
|
||||
(void)mp; // Passthrough - no processing needed, apps handle compression/decompression
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
@@ -1369,7 +1369,7 @@ int32_t CannedMessageModule::runOnce()
|
||||
case INPUT_BROKER_RIGHT:
|
||||
break;
|
||||
default:
|
||||
// Only insert ASCII printable characters (32–126)
|
||||
// Only insert ASCII printable characters (32-126)
|
||||
if (this->payload >= 32 && this->payload <= 126) {
|
||||
requestFocus();
|
||||
if (this->cursor == this->freetext.length()) {
|
||||
@@ -2128,7 +2128,7 @@ static float getSnrLimit(meshtastic_Config_LoRaConfig_ModemPreset preset)
|
||||
}
|
||||
}
|
||||
|
||||
// Return Good/Fair/Bad label and set 1–5 bars based on SNR and RSSI
|
||||
// Return Good/Fair/Bad label and set 1-5 bars based on SNR and RSSI
|
||||
static const char *getSignalGrade(float snr, int32_t rssi, float snrLimit, int &bars)
|
||||
{
|
||||
// 5-bar logic: strength inside Good/Fair/Bad category
|
||||
|
||||
@@ -23,7 +23,7 @@ constexpr uint32_t RUN_INTERVAL_MS = 5 * 60 * 1000UL; // Emit micro-summary ever
|
||||
// Persistence
|
||||
// Note: this only needs incrementing if the published arrangement changes. For testing purposes, or prior to widespread release,
|
||||
// it can stay the same even if the internal layout changes.
|
||||
constexpr uint32_t HISTOGRAM_STATE_MAGIC = 0x48535432; // 'HST2' — layout v2
|
||||
constexpr uint32_t HISTOGRAM_STATE_MAGIC = 0x48535432; // 'HST2' - layout v2
|
||||
constexpr uint8_t HISTOGRAM_STATE_VERSION = 1;
|
||||
constexpr const char *HISTOGRAM_STATE_FILE = "/prefs/hopScalingState.bin";
|
||||
|
||||
@@ -282,7 +282,7 @@ void HopScalingModule::rollHour()
|
||||
}
|
||||
|
||||
// 2. Walk scaled hop buckets to produce a hop-limit recommendation.
|
||||
// effectiveMin: walk threshold — first hop whose cumulative count reaches this.
|
||||
// effectiveMin: walk threshold - first hop whose cumulative count reaches this.
|
||||
// effectiveMax: ceiling on the one-hop extension check with GENEROUS politeness.
|
||||
const uint16_t effectiveMin = TARGET_AFFECTED_NODES;
|
||||
const uint16_t effectiveMax = MAX_TARGET_NODES;
|
||||
@@ -351,7 +351,7 @@ void HopScalingModule::rollHour()
|
||||
// 5. Tick down the hold counter; once it reaches zero, halve filteringDenominator toward
|
||||
// samplingDenominator once per rollHour() (= once per hour) rather than a single jump:
|
||||
// avoids a sudden large change in the hop-walk count when samplingDenominator cascaded
|
||||
// down significantly during the hold period. No new hold is placed on each step — the
|
||||
// down significantly during the hold period. No new hold is placed on each step - the
|
||||
// 13-roll hold already guaranteed that re-admitted nodes have full seenHoursAgo history;
|
||||
// further pacing is provided naturally by the 1-step-per-hour rate. denominatorHistory
|
||||
// is updated automatically by the shift at the top of rollHour(), so no backfill here.
|
||||
@@ -405,7 +405,7 @@ void HopScalingModule::trimIfNeeded()
|
||||
// down yet) are left untouched: eviction at samplingDenominator retains exactly those
|
||||
// entries, so the old higher gate remains accurate for those historical hours.
|
||||
// Slots below the new value must be raised because the eviction removed entries that
|
||||
// had been admitted at the looser old gate — the remaining entries represent a 1/N
|
||||
// had been admitted at the looser old gate - the remaining entries represent a 1/N
|
||||
// subsample where N is the new filteringDenominator, not the old smaller value.
|
||||
for (uint8_t h = 0; h < 13; h++)
|
||||
denominatorHistory[h] = std::max(denominatorHistory[h], filteringDenominator);
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
*
|
||||
* Memory layout: 512 bytes total (128 entries × 4 bytes/entry, no padding)
|
||||
* - 16-bit XOR-fold hash of node ID
|
||||
* - 3-bit hops away (0–7)
|
||||
* - 3-bit hops away (0-7)
|
||||
* - 13-bit hourly seen bitmap
|
||||
* All three fields are packed into a single 32-bit Record; sizeof(Record) == 4.
|
||||
*
|
||||
* Sampling:
|
||||
* - A node is added only when passesFilter(hashNodeId(nodeId), samplingDenominator),
|
||||
* i.e. (hash16(nodeId) & (samplingDenominator – 1)) == 0 (hash-space subsample, not raw ID)
|
||||
* i.e. (hash16(nodeId) & (samplingDenominator - 1)) == 0 (hash-space subsample, not raw ID)
|
||||
* - samplingDenominator starts at 1 (sample all), doubles when the list exceeds FILL_HIGH_PCT
|
||||
* - filteringDenominator tracks samplingDenominator upward immediately but does not drop back
|
||||
* down until FILTER_DENOM_HOLD_MS (13 h) have elapsed since the last scale-up
|
||||
@@ -72,18 +72,18 @@ class HopScalingModule : private concurrency::OSThread
|
||||
// This value is deliberately equal to the seenHoursAgo window (13 hours / 13 bits).
|
||||
// Invariant: every entry that existed when a scale-up fired had seenHoursAgo != 0 at
|
||||
// that moment (trimIfNeeded() evicts stale entries before doubling the denominator),
|
||||
// so it remains seenInLast13h for at most 13 more rollHour() calls — exactly the
|
||||
// so it remains seenInLast13h for at most 13 more rollHour() calls - exactly the
|
||||
// hold duration. That means entries from the scale-up event keep counts.total above
|
||||
// the scale-down threshold for the entire hold period under normal (active) mesh
|
||||
// conditions. On a genuinely quieting mesh the scale-down CAN fire before the hold
|
||||
// expires — each firing halves samplingDenominator but filteringDenominator stays
|
||||
// expires - each firing halves samplingDenominator but filteringDenominator stays
|
||||
// elevated, so the hop recommendation correctly stays conservative (MAX_HOP) while
|
||||
// the cascade runs. The cascade is bounded at DENOM_MIN (7 halvings from DENOM_MAX);
|
||||
// when the hold finally expires, step 5 of rollHour() halves filteringDenominator
|
||||
// once per hour (rather than jumping directly to samplingDenominator) until the two
|
||||
// converge, giving the hop-walk a gradual, 1-step-per-hour descent.
|
||||
static constexpr uint32_t FILTER_DENOM_HOLD_MS = 13UL * 60UL * 60UL * 1000UL; // 13 h (documentation only)
|
||||
// Number of rollHour() calls the hold spans — equals the seenHoursAgo window width.
|
||||
// Number of rollHour() calls the hold spans - equals the seenHoursAgo window width.
|
||||
// filteringDenomHoldRollsRemaining is initialised to this value on scale-up and
|
||||
// decremented once per rollHour(); step-down begins when it reaches zero.
|
||||
static constexpr uint8_t FILTER_DENOM_HOLD_ROLLS = 13u;
|
||||
@@ -132,15 +132,15 @@ class HopScalingModule : private concurrency::OSThread
|
||||
struct MeshTrendStats {
|
||||
/// Estimated node count per hour slot (h=0 is the just-completed hour, h=12 is 12 h ago).
|
||||
uint16_t scaledPerHour[13] = {};
|
||||
/// Nodes heard only this hour with no prior bitmap history — indicates new arrivals.
|
||||
/// Nodes heard only this hour with no prior bitmap history - indicates new arrivals.
|
||||
uint16_t newThisHour = 0;
|
||||
/// Nodes heard this hour that also appeared in at least one older hour — stable regulars.
|
||||
/// Nodes heard this hour that also appeared in at least one older hour - stable regulars.
|
||||
uint16_t returningThisHour = 0;
|
||||
/// Nodes heard last hour but silent this hour — potential departures.
|
||||
/// Nodes heard last hour but silent this hour - potential departures.
|
||||
uint16_t lapsedSinceLastHour = 0;
|
||||
/// Nodes absent from the last 4 hours but still present in some older hour (5–13 h) — quieting down.
|
||||
/// Nodes absent from the last 4 hours but still present in some older hour (5-13 h) - quieting down.
|
||||
uint16_t olderThan4h = 0;
|
||||
/// Nodes whose only remaining history is the 13th hour (bit 12 only) — about to age out entirely.
|
||||
/// Nodes whose only remaining history is the 13th hour (bit 12 only) - about to age out entirely.
|
||||
uint16_t agingOut = 0;
|
||||
};
|
||||
|
||||
@@ -257,11 +257,11 @@ class HopScalingModule : private concurrency::OSThread
|
||||
//
|
||||
// Two separate denominators control two distinct gates:
|
||||
//
|
||||
// samplingDenominator — admission gate. A node is added/updated only when
|
||||
// samplingDenominator - admission gate. A node is added/updated only when
|
||||
// passesFilter(hash, samplingDenominator). Lower value = more permissive =
|
||||
// more nodes enter = represents recent mesh state.
|
||||
//
|
||||
// filteringDenominator — counting gate. The hop-walk tally in rollHour() only
|
||||
// filteringDenominator - counting gate. The hop-walk tally in rollHour() only
|
||||
// counts entries that pass passesFilter(hash, filteringDenominator). It moves
|
||||
// up with samplingDenominator immediately (scale-up) but is held at the
|
||||
// elevated value for FILTER_DENOM_HOLD_MS (13 h) after any scale-up before it
|
||||
@@ -272,20 +272,20 @@ class HopScalingModule : private concurrency::OSThread
|
||||
// that also pass F is exactly D/F. Therefore:
|
||||
// raw_count × F = (total × D/F) × F = total × D
|
||||
// The population estimate is the same whether we count with D or with F.
|
||||
// The hold period is not about accuracy — it is about stability: it prevents the
|
||||
// The hold period is not about accuracy - it is about stability: it prevents the
|
||||
// hop recommendation from reacting to recently-admitted nodes that have not yet
|
||||
// accumulated enough seenHoursAgo history to be statistically reliable.
|
||||
//
|
||||
// denominatorHistory[h] — the filteringDenominator used to both gate and scale
|
||||
// denominatorHistory[h] - the filteringDenominator used to both gate and scale
|
||||
// hourlyRaw[h]. Invariant: denominatorHistory[h] always equals the
|
||||
// filteringDenominator that was active when seenHoursAgo bit h was set.
|
||||
// rollHour() advances the array at the very start (before the tally loop), then
|
||||
// gates hourlyRaw[h] per-slot by denominatorHistory[h] — each slot's raw count
|
||||
// gates hourlyRaw[h] per-slot by denominatorHistory[h] - each slot's raw count
|
||||
// and multiplier are therefore always consistent, even when filteringDenominator
|
||||
// changes between rolls (e.g. hold expiry). On scale-up (trimIfNeeded()), the
|
||||
// entire array is backfilled uniformly with the new filteringDenominator to
|
||||
// preserve the invariant retroactively for all 13 slots. Initialised to
|
||||
// DENOM_MIN (1); scaledPerHour slots that draw from a 1 entry are unscaled —
|
||||
// DENOM_MIN (1); scaledPerHour slots that draw from a 1 entry are unscaled -
|
||||
// correct for a fresh instance with no prior history.
|
||||
// -----------------------------------------------------------------------
|
||||
uint8_t samplingDenominator = DENOM_MIN;
|
||||
@@ -321,7 +321,7 @@ class HopScalingModule : private concurrency::OSThread
|
||||
|
||||
// Record field semantics:
|
||||
// nodeHash → XOR-fold of full 32-bit node ID to 16 bits
|
||||
// hops_away → hop distance (0–7)
|
||||
// hops_away → hop distance (0-7)
|
||||
// seenHoursAgo → 13-bit per-hour seen bitmap
|
||||
// bit 0 = seen in the current / most-recent hour
|
||||
// bit 12 = seen 12 hours ago
|
||||
@@ -338,7 +338,7 @@ class HopScalingModule : private concurrency::OSThread
|
||||
static bool passesFilter(uint16_t nodeHash, uint8_t denom) { return (nodeHash & static_cast<uint16_t>(denom - 1u)) == 0u; }
|
||||
|
||||
public:
|
||||
// Clock — public so tests can share the same timebase via HopScalingModule::s_testNowMs
|
||||
// Clock - public so tests can share the same timebase via HopScalingModule::s_testNowMs
|
||||
#ifdef PIO_UNIT_TESTING
|
||||
static uint32_t nowMs() { return s_testNowMs; }
|
||||
#else
|
||||
|
||||
@@ -109,13 +109,13 @@ bool MeshBeaconModule::beaconTxConfigInvalid(const meshtastic_MeshPacket *p)
|
||||
meshtastic_Config_LoRaConfig_ModemPreset preset;
|
||||
meshtastic_Config_LoRaConfig_RegionCode sidecarRegion = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
||||
if (!getTargetRadioSettings(p, &preset, nullptr, nullptr, &sidecarRegion))
|
||||
return false; // not a beacon-switch packet — nothing to validate, normal traffic unaffected
|
||||
return false; // not a beacon-switch packet - nothing to validate, normal traffic unaffected
|
||||
|
||||
const meshtastic_Config_LoRaConfig_RegionCode region =
|
||||
(sidecarRegion != meshtastic_Config_LoRaConfig_RegionCode_UNSET) ? sidecarRegion : config.lora.region;
|
||||
|
||||
// An unlicensed node must never key up on a ham-only (licensed-only) region. The reverse is
|
||||
// allowed: a licensed (ham) node may operate in a non-ham region — and the switch only touches
|
||||
// allowed: a licensed (ham) node may operate in a non-ham region - and the switch only touches
|
||||
// preset/region/channel, never owner.is_licensed, so it cannot deactivate licensed mode.
|
||||
const RegionInfo *r = getRegion(region);
|
||||
if (r && r->profile->licensedOnly && !owner.is_licensed)
|
||||
@@ -156,7 +156,7 @@ bool MeshBeaconModule::reconfigureForBeaconTX(RadioInterface *iface, meshtastic_
|
||||
// heuristic both missed cases (a channel name/PSK swap that left preset/slot/region unchanged would
|
||||
// never be restored) and fired falsely (a legitimate non-beacon channel edit would be reverted on
|
||||
// the next TX). With the flag the restore fires for ANY field we changed and only when we changed
|
||||
// it — including on TX-failure paths, which route through this same restore call.
|
||||
// it - including on TX-failure paths, which route through this same restore call.
|
||||
static bool radioSwitched = false;
|
||||
|
||||
meshtastic_ChannelSettings *primaryCh = &channels.getByIndex(channels.getPrimaryIndex()).settings;
|
||||
@@ -193,7 +193,7 @@ bool MeshBeaconModule::reconfigureForBeaconTX(RadioInterface *iface, meshtastic_
|
||||
targetRegion == config.lora.region && !channelDiffers(targetChannel))
|
||||
return false;
|
||||
|
||||
// Guard: never key up on an invalid target config — bad preset for the region, or an
|
||||
// Guard: never key up on an invalid target config - bad preset for the region, or an
|
||||
// unlicensed node keying up on a ham-only region. Refuse the switch here so we never
|
||||
// transmit on it; the radio driver drops the packet outright (see RadioLibInterface,
|
||||
// beaconTxConfigInvalid) rather than letting it fall through onto the current config.
|
||||
@@ -265,7 +265,7 @@ void MeshBeaconBroadcastModule::rebuildCache()
|
||||
beacon.has_offer_channel = true;
|
||||
beacon.offer_channel = bcfg.broadcast_offer_channel;
|
||||
// PSK is included intentionally: this beacon is a public join-invitation.
|
||||
// The offered channel is not secret — the PSK here is a convenience token,
|
||||
// The offered channel is not secret - the PSK here is a convenience token,
|
||||
// not a security boundary. Operators who want a private channel must
|
||||
// distribute the PSK out-of-band and leave offer_channel unset.
|
||||
}
|
||||
@@ -273,7 +273,7 @@ void MeshBeaconBroadcastModule::rebuildCache()
|
||||
beacon.offer_preset = bcfg.broadcast_offer_preset;
|
||||
beacon.offer_region = bcfg.broadcast_offer_region;
|
||||
// Note: an empty config legitimately encodes to 0 bytes, and pb_encode_to_bytes can't distinguish
|
||||
// that from a (here effectively impossible — buffer is max-sized) failure, so we always clear the
|
||||
// that from a (here effectively impossible - buffer is max-sized) failure, so we always clear the
|
||||
// dirty flag. The combined send is gated on payloadCacheSize > 0, so an empty payload is never TX'd.
|
||||
payloadCacheSize = (pb_size_t)pb_encode_to_bytes(payloadCache, sizeof(payloadCache), &meshtastic_MeshBeacon_msg, &beacon);
|
||||
payloadCacheDirty = false;
|
||||
@@ -291,7 +291,7 @@ void MeshBeaconBroadcastModule::sendBeaconPacket(meshtastic_MeshPacket *p, mesht
|
||||
}
|
||||
|
||||
// perhapsEncode() keys encryption (and the channel-hash hint) off the PRIMARY channel slot, and
|
||||
// the radio-thread channel switch only happens AFTER encryption — so a beacon on an override
|
||||
// the radio-thread channel switch only happens AFTER encryption - so a beacon on an override
|
||||
// channel would otherwise be encrypted with the PRIMARY PSK, not the beacon channel's. Install the
|
||||
// beacon channel into the primary slot for the synchronous duration of send(), then restore.
|
||||
// Meshtastic threading is cooperative (no preemption between the swap and restore).
|
||||
@@ -326,13 +326,13 @@ void MeshBeaconBroadcastModule::sendBeacon()
|
||||
// broadcast_send_as_node: commented out pending further review.
|
||||
// Spoof notes preserved for when this is re-enabled:
|
||||
// broadcast_send_as_node overrides the source NodeNum. NOTE: this is a *node-ID* spoof
|
||||
// only — it rewrites the 'from' field but does NOT forge any signature. Once 'from' is
|
||||
// only - it rewrites the 'from' field but does NOT forge any signature. Once 'from' is
|
||||
// not us, the packet is no longer isFromUs(), so Router::perhapsEncode() skips XEdDSA
|
||||
// signing and receivers get an unsigned packet attributed to another node.
|
||||
// When broadcast_send_as_node == 0 the beacon is genuinely from us and Router::perhapsEncode()
|
||||
// signs it under the same XEdDSA broadcast policy as normal channel messages.
|
||||
// When broadcast_send_as_node rewrites p->from, perhapsEncode() sees isFromUs()=false and
|
||||
// skips setting has_bitfield — must be set explicitly so receivers can classify hop_start
|
||||
// skips setting has_bitfield - must be set explicitly so receivers can classify hop_start
|
||||
// correctly and so ok_to_mqtt is honoured on the spoofed packet.
|
||||
// if (bcfg.broadcast_send_as_node != 0) {
|
||||
// p->from = bcfg.broadcast_send_as_node;
|
||||
@@ -347,8 +347,8 @@ void MeshBeaconBroadcastModule::sendBeacon()
|
||||
|
||||
// ── Packet type decisions ────────────────────────────────────────────────
|
||||
//
|
||||
// FLAG_LEGACY_SPLIT: when both text and offer are present, send TWO packets — A)
|
||||
// MESH_BEACON_APP (offer only) and B) TEXT_MESSAGE_APP (text only) — both on the SAME beacon
|
||||
// FLAG_LEGACY_SPLIT: when both text and offer are present, send TWO packets - A)
|
||||
// MESH_BEACON_APP (offer only) and B) TEXT_MESSAGE_APP (text only) - both on the SAME beacon
|
||||
// radio settings, so nodes that only decode TEXT_MESSAGE_APP still receive the text. Otherwise a
|
||||
// single packet is sent (offer-only, text-only, or the combined offer+text path).
|
||||
//
|
||||
@@ -361,7 +361,7 @@ void MeshBeaconBroadcastModule::sendBeacon()
|
||||
const bool sendTextOnly = splitBoth || (!hasRadioContent && hasText);
|
||||
const bool sendCombined = !legacySplit && hasRadioContent && hasText;
|
||||
|
||||
// Build offer payload once — shared across all targets.
|
||||
// Build offer payload once - shared across all targets.
|
||||
uint8_t offerBuf[meshtastic_MeshBeacon_size] = {};
|
||||
pb_size_t offerSize = 0;
|
||||
if (sendOfferOnly) {
|
||||
@@ -399,7 +399,7 @@ void MeshBeaconBroadcastModule::sendBeacon()
|
||||
|
||||
// Dedup state: the beacon payload is identical across targets, so two targets that resolve to
|
||||
// the same effective radio config (preset + resolved region + channel) would just re-broadcast
|
||||
// the same packet — wasted airtime and a redundant radio switch each. We skip the later one.
|
||||
// the same packet - wasted airtime and a redundant radio switch each. We skip the later one.
|
||||
// Keyed on the *resolved* values so an explicit "current region" dedups against an UNSET one.
|
||||
EffTarget sent[4];
|
||||
meshtastic_Config_LoRaConfig_RegionCode sentRegion[4];
|
||||
@@ -422,7 +422,7 @@ void MeshBeaconBroadcastModule::sendBeacon()
|
||||
tgt.preset = bt.has_preset ? bt.preset : config.lora.modem_preset;
|
||||
tgt.region = bt.region;
|
||||
// Resolve the channel from the device's channel table by index. A slot is only usable
|
||||
// if it is actually configured (has a name or PSK — its key is needed to encrypt). An
|
||||
// if it is actually configured (has a name or PSK - its key is needed to encrypt). An
|
||||
// out-of-range index, or a blank slot, falls back to the default channel for the target
|
||||
// preset (see beaconChannelSettings), exactly as an unset channel_index would.
|
||||
tgt.has_channel = false;
|
||||
@@ -585,14 +585,14 @@ bool MeshBeaconListenerModule::handleReceivedProtobuf(const meshtastic_MeshPacke
|
||||
|
||||
// NOTE: we deliberately do NOT unwrap the text into a synthesized TEXT_MESSAGE_APP for the
|
||||
// phone. The original MESH_BEACON_APP packet already flows to the client (we return CONTINUE),
|
||||
// so a beacon-aware client renders `message` directly — injecting a copy would only duplicate
|
||||
// so a beacon-aware client renders `message` directly - injecting a copy would only duplicate
|
||||
// it. Broadcasters that need non-beacon-aware clients to see the text use FLAG_LEGACY_SPLIT,
|
||||
// which sends a real TEXT_MESSAGE_APP over RF. We also do not fire EVENT_RECEIVED_MSG: a beacon
|
||||
// is an advisory broadcast, not a personal message, and must not wake the device from sleep.
|
||||
if (hasText)
|
||||
LOG_INFO("Beacon: received from 0x%08x: '%.40s'", mp.from, b->message);
|
||||
|
||||
// Cache any offer for the client app — never auto-applied.
|
||||
// Cache any offer for the client app - never auto-applied.
|
||||
if (hasOfferContent) {
|
||||
lastReceivedOffer.valid = true;
|
||||
lastReceivedOffer.sender = mp.from;
|
||||
@@ -602,7 +602,7 @@ bool MeshBeaconListenerModule::handleReceivedProtobuf(const meshtastic_MeshPacke
|
||||
lastReceivedOffer.region = b->offer_region;
|
||||
lastReceivedOffer.preset = b->offer_preset;
|
||||
lastReceivedOffer.received_at =
|
||||
getValidTime(RTCQualityFromNet); // 0 if no RTC fix yet — consumers must not treat 0 as valid
|
||||
getValidTime(RTCQualityFromNet); // 0 if no RTC fix yet - consumers must not treat 0 as valid
|
||||
LOG_INFO("Beacon: stored offer from 0x%08x (preset=%d)", mp.from, b->offer_preset);
|
||||
}
|
||||
|
||||
|
||||
@@ -133,9 +133,9 @@ extern MeshBeaconBroadcastModule *meshBeaconBroadcastModule;
|
||||
|
||||
/**
|
||||
* Listener: receives MESH_BEACON_APP packets and caches any offered channel/preset for the client
|
||||
* app to retrieve. It does NOT unwrap the text into a separate message — the original beacon packet
|
||||
* app to retrieve. It does NOT unwrap the text into a separate message - the original beacon packet
|
||||
* already reaches the client (handler returns CONTINUE), which reads `message` from it directly.
|
||||
* Does NOT auto-apply offered settings — client app must do so explicitly.
|
||||
* Does NOT auto-apply offered settings - client app must do so explicitly.
|
||||
* Active only when the FLAG_LISTEN_ENABLED bit is set in moduleConfig.mesh_beacon.flags.
|
||||
*/
|
||||
class MeshBeaconListenerModule : public ProtobufModule<meshtastic_MeshBeacon>, public Observable<const meshtastic_MeshPacket *>
|
||||
@@ -153,7 +153,7 @@ class MeshBeaconListenerModule : public ProtobufModule<meshtastic_MeshBeacon>, p
|
||||
uint32_t received_at;
|
||||
};
|
||||
|
||||
// Last received offer — accessible to admin/API for client app retrieval.
|
||||
// Last received offer - accessible to admin/API for client app retrieval.
|
||||
static BeaconOffer lastReceivedOffer;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -339,7 +339,7 @@ meshtastic_MeshPacket *PositionModule::allocAtakPli()
|
||||
strncpy(takPacket.device_callsign, owner.long_name, sizeof(takPacket.device_callsign) - 1);
|
||||
takPacket.device_callsign[sizeof(takPacket.device_callsign) - 1] = '\0';
|
||||
|
||||
// CoT uid — ATAK drops PLI entities with empty uid; derive stable "!<nodenum>" id.
|
||||
// CoT uid - ATAK drops PLI entities with empty uid; derive stable "!<nodenum>" id.
|
||||
snprintf(takPacket.uid, sizeof(takPacket.uid), "!%08x", nodeDB->getNodeNum());
|
||||
|
||||
// Encode TAKPacketV2 protobuf, leaving room for flags byte prefix
|
||||
@@ -577,7 +577,7 @@ void PositionModule::sendLostAndFoundText()
|
||||
p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
||||
p->want_ack = false;
|
||||
if (written < 0) {
|
||||
// snprintf encoding error — send an empty payload rather than uninitialized bytes.
|
||||
// snprintf encoding error - send an empty payload rather than uninitialized bytes.
|
||||
p->decoded.payload.size = 0;
|
||||
} else {
|
||||
// Clamp to buffer capacity (snprintf returns "would-have-written" which can exceed the buffer).
|
||||
@@ -598,7 +598,7 @@ static inline void computeImpreciseLatLon(int32_t inLat, int32_t inLon, uint8_t
|
||||
// Build mask for top 'precisionBits' bits of a 32-bit unsigned field
|
||||
const uint32_t mask = (precisionBits == 32) ? UINT32_MAX : (UINT32_MAX << (32 - precisionBits));
|
||||
// Note: latitude_i/longitude_i are stored as signed 32-bit in meshtastic code but
|
||||
// the bitmask logic used previously operated as unsigned—preserve that behavior by
|
||||
// the bitmask logic used previously operated as unsigned-preserve that behavior by
|
||||
// casting to uint32_t for masking, then back to int32_t.
|
||||
uint32_t lat_u = static_cast<uint32_t>(inLat) & mask;
|
||||
uint32_t lon_u = static_cast<uint32_t>(inLon) & mask;
|
||||
|
||||
@@ -69,7 +69,7 @@ class PositionModule : public ProtobufModule<meshtastic_Position>, private concu
|
||||
// precision grid cell, so re-sending would be a duplicate that traffic management dedups
|
||||
// downstream anyway. Used to hold stationary broadcasts to a 12h floor. useConfiguredPrecision
|
||||
// gauges movement at our own configured (unclamped) precision rather than the on-wire
|
||||
// (public-clamped) precision — trackers report finer movement.
|
||||
// (public-clamped) precision - trackers report finer movement.
|
||||
bool positionUnchangedSinceLastSend(const meshtastic_PositionLite &selfPos, bool useConfiguredPrecision);
|
||||
meshtastic_MeshPacket *allocAtakPli();
|
||||
void trySetRtc(meshtastic_Position p, bool isLocal, bool forceUpdate = false);
|
||||
|
||||
@@ -59,14 +59,14 @@ static bool replybotRateLimited(uint32_t from, uint32_t cooldownMs)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// No entry found – insert new sender into the ring
|
||||
// No entry found - insert new sender into the ring
|
||||
replybotCooldown[replybotCooldownIdx].from = from;
|
||||
replybotCooldown[replybotCooldownIdx].lastMs = now;
|
||||
replybotCooldownIdx = (replybotCooldownIdx + 1) % REPLYBOT_COOLDOWN_SLOTS;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Constructor – registers a single text port and marks the module promiscuous
|
||||
// Constructor - registers a single text port and marks the module promiscuous
|
||||
// so that broadcast messages on the primary channel are visible.
|
||||
ReplyBotModule::ReplyBotModule() : SinglePortModule("replybot", meshtastic_PortNum_TEXT_MESSAGE_APP)
|
||||
{
|
||||
@@ -122,7 +122,7 @@ ProcessMessage ReplyBotModule::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// Compute hop count indicator – if the relay_node is non‑zero we know
|
||||
// Compute hop count indicator - if the relay_node is non‑zero we know
|
||||
// there was at least one relay. Some firmware builds support a hop_start
|
||||
// field which could be used for more accurate counts, but here we use
|
||||
// the available relay_node flag only.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
// NEOPIXEL_STATUS_POWER_PIN (required to enable the power/charge pixel)
|
||||
// NEOPIXEL_STATUS_POWER_COLOR (optional, default red 0xFF0000)
|
||||
// NEOPIXEL_STATUS_PAIRING_PIN / _COLOR (default blue 0x0000FF)
|
||||
// Each pixel is a standalone 1-LED strand on its own GPIO — this mirrors how
|
||||
// Each pixel is a standalone 1-LED strand on its own GPIO - this mirrors how
|
||||
// boards like the LilyGo T-Echo-Card expose three independent WS2812s.
|
||||
#if defined(NEOPIXEL_STATUS_POWER_PIN) || defined(NEOPIXEL_STATUS_PAIRING_PIN)
|
||||
#include <Adafruit_NeoPixel.h>
|
||||
|
||||
@@ -436,7 +436,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
|
||||
bool isCooldownOver = (now - lastAlertTime > 60000);
|
||||
|
||||
if (isOwnTelemetry && bannerMsg && isCooldownOver) {
|
||||
LOG_INFO("drawFrame: IAQ %d (own) — showing banner: %s", m.iaq, bannerMsg);
|
||||
LOG_INFO("drawFrame: IAQ %d (own) - showing banner: %s", m.iaq, bannerMsg);
|
||||
screen->showSimpleBanner(bannerMsg, 3000);
|
||||
|
||||
// Only buzz if IAQ is over 200
|
||||
|
||||
@@ -331,7 +331,7 @@ void TraceRouteModule::maybeSetNextHop(NodeNum target, uint8_t nextHopByte)
|
||||
#if HAS_TRAFFIC_MANAGEMENT
|
||||
// Mirror into the TMM overflow cache. Traceroute is the highest-confidence
|
||||
// source (full known route), and this captures the target even when it isn't
|
||||
// in the hot NodeDB — same rationale as the ACK-confirmed path in NextHopRouter.
|
||||
// in the hot NodeDB - same rationale as the ACK-confirmed path in NextHopRouter.
|
||||
if (trafficManagementModule)
|
||||
trafficManagementModule->setNextHop(target, nextHopByte);
|
||||
#endif
|
||||
|
||||
@@ -227,7 +227,7 @@ void TrafficManagementModule::resetStats()
|
||||
|
||||
void TrafficManagementModule::recordRouterHopPreserved()
|
||||
{
|
||||
// router_preserve_hops: not suitable right now — removed from config until
|
||||
// router_preserve_hops: not suitable right now - removed from config until
|
||||
// the right heuristic for when to preserve vs. exhaust is clearer.
|
||||
(void)stats.router_hops_preserved;
|
||||
}
|
||||
@@ -280,7 +280,7 @@ int TrafficManagementModule::peekCachedRole(NodeNum node)
|
||||
* One linear pass tracks the match, the first empty slot, and the eviction
|
||||
* victim. When the cache is full, the victim is the stalest entry (largest
|
||||
* of its three relative timestamps is smallest), preferring entries without
|
||||
* a next_hop hint — those hints are the long-tail routing state the cache
|
||||
* a next_hop hint - those hints are the long-tail routing state the cache
|
||||
* exists to keep, and the maintenance sweep never ages them out.
|
||||
*
|
||||
* @param node NodeNum to find or create
|
||||
@@ -288,7 +288,7 @@ int TrafficManagementModule::peekCachedRole(NodeNum node)
|
||||
* @return Pointer to entry, or nullptr if the cache is unavailable
|
||||
*/
|
||||
// Sender-role resolution for the position hot path. The tier-3 cache is authoritative
|
||||
// here and is kept fresh by updateCachedRoleFromNodeInfo() — i.e. updated at the same
|
||||
// here and is kept fresh by updateCachedRoleFromNodeInfo() - i.e. updated at the same
|
||||
// time NodeDB learns a role, not re-derived on every packet. We only fall back to a
|
||||
// NodeDB scan (tiers 1+2) the first time we start tracking a node, to seed the cache so
|
||||
// a resident special-role node is correct from its very first position. Thereafter the
|
||||
@@ -308,8 +308,8 @@ meshtastic_Config_DeviceConfig_Role TrafficManagementModule::resolveSenderRole(N
|
||||
return static_cast<meshtastic_Config_DeviceConfig_Role>(entry->getCachedRole());
|
||||
}
|
||||
|
||||
// Refresh the tier-3 role cache from an observed NodeInfo — the same event that updates
|
||||
// NodeDB's role — so role changes (including demotion back to CLIENT) are picked up
|
||||
// Refresh the tier-3 role cache from an observed NodeInfo - the same event that updates
|
||||
// NodeDB's role - so role changes (including demotion back to CLIENT) are picked up
|
||||
// without scanning NodeDB on the position hot path. Role is read straight from the
|
||||
// packet's User payload (authoritative regardless of module ordering). Only updates nodes
|
||||
// we already track (findEntry, no create) so NodeInfo from non-position nodes can't pollute
|
||||
@@ -537,7 +537,7 @@ void TrafficManagementModule::cacheNodeInfoPacket(const meshtastic_MeshPacket &m
|
||||
//
|
||||
// A routing hint store. The byte is the last byte of the NodeNum to use as next
|
||||
// hop to reach `dest`. It is written ONLY from NextHopRouter's ACK-confirmed
|
||||
// decision (a bidirectionally-verified relay) — never inferred one-way from
|
||||
// decision (a bidirectionally-verified relay) - never inferred one-way from
|
||||
// relayed traffic. The TMM cache holds confirmed next-hops that have aged out of
|
||||
// the hot NodeDB (NodeInfoLite), and NextHopRouter::getNextHop() consults it as a
|
||||
// fallback after the hot store.
|
||||
@@ -593,7 +593,7 @@ bool TrafficManagementModule::preloadNextHopsFromNodeDB()
|
||||
{
|
||||
#if TRAFFIC_MANAGEMENT_CACHE_SIZE > 0
|
||||
if (!cache || !nodeDB)
|
||||
return false; // prerequisites not ready yet — caller should retry on a later pass
|
||||
return false; // prerequisites not ready yet - caller should retry on a later pass
|
||||
|
||||
uint16_t seeded = 0;
|
||||
concurrency::LockGuard guard(&cacheLock);
|
||||
@@ -675,7 +675,7 @@ uint8_t TrafficManagementModule::computePositionFingerprint(int32_t lat_truncate
|
||||
// 0 is the "no position seen" sentinel for pos_fingerprint, so a real position that happens to
|
||||
// hash to 0 must not collide with it (otherwise its duplicates would never dedup). Remap 0 -> 0xFF,
|
||||
// mirroring NodeDB::getLastByteOfNodeNum()'s 0 -> 0xFF idiom. Cost: the 0x00 bucket merges into
|
||||
// 0xFF (one extra collision in 256 — negligible; the fingerprint already collides every 16 cells).
|
||||
// 0xFF (one extra collision in 256 - negligible; the fingerprint already collides every 16 cells).
|
||||
return fp ? fp : 0xFF;
|
||||
}
|
||||
|
||||
@@ -715,7 +715,7 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
|
||||
logAction("drop", &mp, "unknown");
|
||||
incrementStat(&stats.unknown_packet_drops);
|
||||
ignoreRequest = true; // Suppress NAK for want_response packets
|
||||
return ProcessMessage::STOP; // Consumed — will not be rebroadcast
|
||||
return ProcessMessage::STOP; // Consumed - will not be rebroadcast
|
||||
}
|
||||
}
|
||||
return ProcessMessage::CONTINUE;
|
||||
@@ -746,7 +746,7 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
|
||||
logAction("respond", &mp, "nodeinfo-cache");
|
||||
incrementStat(&stats.nodeinfo_cache_hits);
|
||||
ignoreRequest = true; // We responded; suppress default NAK
|
||||
return ProcessMessage::STOP; // Consumed — request will not be forwarded
|
||||
return ProcessMessage::STOP; // Consumed - request will not be forwarded
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,7 +765,7 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
|
||||
logAction("drop", &mp, "position-dedup");
|
||||
incrementStat(&stats.position_dedup_drops);
|
||||
ignoreRequest = true; // Suppress NAK
|
||||
return ProcessMessage::STOP; // Consumed — duplicate will not be rebroadcast
|
||||
return ProcessMessage::STOP; // Consumed - duplicate will not be rebroadcast
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -782,7 +782,7 @@ ProcessMessage TrafficManagementModule::handleReceived(const meshtastic_MeshPack
|
||||
logAction("drop", &mp, "rate-limit");
|
||||
incrementStat(&stats.rate_limit_drops);
|
||||
ignoreRequest = true; // Suppress NAK
|
||||
return ProcessMessage::STOP; // Consumed — throttled packet will not be rebroadcast
|
||||
return ProcessMessage::STOP; // Consumed - throttled packet will not be rebroadcast
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -803,7 +803,7 @@ void TrafficManagementModule::alterReceived(meshtastic_MeshPacket &mp)
|
||||
return;
|
||||
|
||||
// exhaust_hop_telemetry / exhaust_hop_position / router_preserve_hops:
|
||||
// not suitable right now — the right heuristics for when to exhaust or
|
||||
// not suitable right now - the right heuristics for when to exhaust or
|
||||
// preserve hops need more field data before we expose them as config knobs.
|
||||
// exhaustRequested stays false; perhapsRebroadcast() behaves normally.
|
||||
|
||||
@@ -816,8 +816,8 @@ void TrafficManagementModule::alterReceived(meshtastic_MeshPacket &mp)
|
||||
// ceiling. Guards against forwarding more-precise coordinates than the
|
||||
// channel is intended to carry (e.g. a LongFast channel set to 13-bit /
|
||||
// ~1.5 km). chanPrec==0 means position sharing is disabled on the channel;
|
||||
// skip — not our job to zero positions on relay.
|
||||
// Ham mode (owner.is_licensed) is exempt. Lost-and-found is NOT exempt — its relayed
|
||||
// skip - not our job to zero positions on relay.
|
||||
// Ham mode (owner.is_licensed) is exempt. Lost-and-found is NOT exempt - its relayed
|
||||
// positions get the same precision clamp as any node.
|
||||
// Compile USERPREFS_TMM_APPLY_TO_PRIVATE_CHANNELS to extend to private channels.
|
||||
if (!owner.is_licensed && isPosition && isBroadcast(mp.to)) {
|
||||
@@ -878,7 +878,7 @@ int32_t TrafficManagementModule::runOnce()
|
||||
const uint8_t rateTtlTicks = static_cast<uint8_t>(
|
||||
std::min(static_cast<uint32_t>(15), (rateWindowMs > 0 ? rateWindowMs * 2 : 24 * kRateTimeTickMs) / kRateTimeTickMs));
|
||||
|
||||
// unknown: fixed 12-tick TTL (12 min — 4 ticks past the 5-min default window)
|
||||
// unknown: fixed 12-tick TTL (12 min - 4 ticks past the 5-min default window)
|
||||
const uint8_t unknownTtlTicks = 12;
|
||||
|
||||
const uint8_t nowPosTick = currentPosTick();
|
||||
@@ -978,7 +978,7 @@ bool TrafficManagementModule::shouldDropPosition(const meshtastic_MeshPacket *p,
|
||||
if (!pos->has_latitude_i || !pos->has_longitude_i)
|
||||
return false;
|
||||
|
||||
// Precision is driven by the channel's own position_precision ceiling — the same
|
||||
// Precision is driven by the channel's own position_precision ceiling - the same
|
||||
// grid the channel uses for broadcast. Falls back to the firmware default (19-bit,
|
||||
// ~90m cells) when the channel has no precision configured (chanPrec == 0).
|
||||
const uint32_t chanPrec = getPositionPrecisionForChannel(p->channel);
|
||||
@@ -990,7 +990,7 @@ bool TrafficManagementModule::shouldDropPosition(const meshtastic_MeshPacket *p,
|
||||
const uint8_t fingerprint = computePositionFingerprint(lat_truncated, lon_truncated, precision);
|
||||
// Drop gate uses the RAW configured interval: 0 means "dedup disabled" (the
|
||||
// contract documented below). The 12h default is only for resolution/TTL
|
||||
// sizing (constructor / runOnce), not for deciding whether to drop — feeding
|
||||
// sizing (constructor / runOnce), not for deciding whether to drop - feeding
|
||||
// the default here would silently turn the 0-disables-dedup contract off.
|
||||
uint32_t minIntervalMs = secsToMs(moduleConfig.traffic_management.position_min_interval_secs);
|
||||
|
||||
@@ -1003,12 +1003,12 @@ bool TrafficManagementModule::shouldDropPosition(const meshtastic_MeshPacket *p,
|
||||
// Role exceptions keyed on the originating node's advertised role, resolved across
|
||||
// all three tiers (hot store → warm store → TMM live cache). The position path is
|
||||
// the one place that needs sender-role, and it also keeps tier 3 warm so the
|
||||
// exception survives the node aging out of both NodeDB stores — important in the
|
||||
// exception survives the node aging out of both NodeDB stores - important in the
|
||||
// common dedup-only config, where isRateLimited()'s role write never runs.
|
||||
const meshtastic_Config_DeviceConfig_Role role = resolveSenderRole(p->from, entry, isNew);
|
||||
if (role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) {
|
||||
// Lost-and-found may refresh a duplicate position at most every ~15 min (cap, never
|
||||
// lengthens; quantised to ~2 dedup ticks). Only when dedup is active — never tighten
|
||||
// lengthens; quantised to ~2 dedup ticks). Only when dedup is active - never tighten
|
||||
// past an operator who disabled it (0).
|
||||
const uint32_t lostFoundCapMs = secsToMs(default_traffic_mgmt_lost_and_found_position_min_interval_secs);
|
||||
if (minIntervalMs != 0 && minIntervalMs > lostFoundCapMs)
|
||||
@@ -1253,7 +1253,7 @@ bool TrafficManagementModule::shouldDropUnknown(const meshtastic_MeshPacket *p,
|
||||
}
|
||||
|
||||
// Increment counter, saturating at 63 (6-bit field max). With threshold
|
||||
// capped at 60, a saturated reading always exceeds the limit — no special
|
||||
// capped at 60, a saturated reading always exceeds the limit - no special
|
||||
// already-saturated edge case needed.
|
||||
const uint8_t cur = entry->getUnknownCount();
|
||||
if (cur < 0x3F)
|
||||
|
||||
@@ -33,7 +33,7 @@ class TrafficManagementModule : public MeshModule, private concurrency::OSThread
|
||||
TrafficManagementModule();
|
||||
~TrafficManagementModule();
|
||||
|
||||
// Singleton — no copying or moving
|
||||
// Singleton - no copying or moving
|
||||
TrafficManagementModule(const TrafficManagementModule &) = delete;
|
||||
TrafficManagementModule &operator=(const TrafficManagementModule &) = delete;
|
||||
|
||||
@@ -116,10 +116,10 @@ class TrafficManagementModule : public MeshModule, private concurrency::OSThread
|
||||
// unknown active: getUnknownCount() != 0 (low 6 bits only)
|
||||
//
|
||||
// next_hop: routing hint written only from ACK-confirmed NextHopRouter decisions.
|
||||
// No TTL — keeps the slot alive across maintenance sweeps.
|
||||
// No TTL - keeps the slot alive across maintenance sweeps.
|
||||
//
|
||||
#if _meshtastic_Config_DeviceConfig_Role_MAX > 15
|
||||
#warning "Device role enum max exceeds 15 — TMM 4-bit role cache (rate_count[7:6]/unknown_count[7:6]) will truncate new values"
|
||||
#warning "Device role enum max exceeds 15 - TMM 4-bit role cache (rate_count[7:6]/unknown_count[7:6]) will truncate new values"
|
||||
#endif
|
||||
struct __attribute__((packed)) UnifiedCacheEntry {
|
||||
NodeNum node;
|
||||
@@ -152,7 +152,7 @@ class TrafficManagementModule : public MeshModule, private concurrency::OSThread
|
||||
// =========================================================================
|
||||
//
|
||||
// Plain array, linear scan (same idiom as WarmNodeStore). A lookup walks at
|
||||
// most cacheSize() × 10 B — microseconds at LoRa packet rates, not worth a
|
||||
// most cacheSize() × 10 B - microseconds at LoRa packet rates, not worth a
|
||||
// hash table. Insertion on a full cache evicts the stalest entry,
|
||||
// preferring entries without a next_hop hint (those are the long-tail
|
||||
// routing state this cache exists to keep).
|
||||
@@ -270,7 +270,7 @@ class TrafficManagementModule : public MeshModule, private concurrency::OSThread
|
||||
|
||||
// Resolve a sender's advertised device role for the position hot path. The tier-3
|
||||
// cache (this entry's getCachedRole) is authoritative and is kept fresh by
|
||||
// updateCachedRoleFromNodeInfo() — updated when NodeDB learns a role, not re-derived
|
||||
// updateCachedRoleFromNodeInfo() - updated when NodeDB learns a role, not re-derived
|
||||
// per packet. Only on first tracking (isNew) do we scan NodeDB (hot store → warm
|
||||
// store, via getNodeRole) to seed the cache, so a resident special-role node is
|
||||
// correct from its first position; after that the read is O(1) and survives the node
|
||||
|
||||
+1
-1
@@ -545,7 +545,7 @@ bool MQTT::isValidConfig(const meshtastic_ModuleConfig_MQTTConfig &config, MQTTC
|
||||
}
|
||||
// Perform a lightweight TCP connectivity check without using connectPubSub(),
|
||||
// which mutates the module's isConnected state. This only checks if the server
|
||||
// is reachable — it does not establish an MQTT session.
|
||||
// is reachable - it does not establish an MQTT session.
|
||||
// Settings are always saved regardless of the result.
|
||||
if (isConnectedToNetwork()) {
|
||||
MQTTClient testClient;
|
||||
|
||||
@@ -349,7 +349,7 @@ void cpuDeepSleep(uint32_t msecToWake)
|
||||
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
|
||||
#endif
|
||||
|
||||
// User shutdown (DELAY_FOREVER / portMAX_DELAY): no RTC timer — align with nRF52 system_off semantics.
|
||||
// User shutdown (DELAY_FOREVER / portMAX_DELAY): no RTC timer - align with nRF52 system_off semantics.
|
||||
if (msecToWake != portMAX_DELAY)
|
||||
esp_sleep_enable_timer_wakeup(msecToWake * 1000ULL); // call expects usecs
|
||||
esp_deep_sleep_start();
|
||||
|
||||
@@ -413,7 +413,7 @@ bool NRF52Bluetooth::onPairingPasskey(uint16_t conn_handle, uint8_t const passke
|
||||
"Bluetooth\nPIN\n[M]" + configuredPasskeyText.substr(0, 3) + " " + configuredPasskeyText.substr(3, 6);
|
||||
// Use the pairing_pin notification type so the lockdown UI short-
|
||||
// circuit (Screen.cpp updateUiFrame) allows the overlay through
|
||||
// even on a locked device — see H13 audit fix. The banner content
|
||||
// even on a locked device - see H13 audit fix. The banner content
|
||||
// is the per-attempt ephemeral pair PIN, not operator content.
|
||||
graphics::BannerOverlayOptions opts;
|
||||
opts.message = ble_message.c_str();
|
||||
|
||||
@@ -125,7 +125,7 @@ bool readRegister(uint8_t address, uint8_t reg, uint8_t &value)
|
||||
twim->TXD.PTR = (uint32_t)®
|
||||
twim->TXD.MAXCNT = 1;
|
||||
// LASTTX_SUSPEND shortcut: peripheral suspends automatically after the TX
|
||||
// byte, so EVENTS_SUSPENDED is set before we even poll — no clearing race.
|
||||
// byte, so EVENTS_SUSPENDED is set before we even poll - no clearing race.
|
||||
twim->SHORTS = TWIM_SHORTS_LASTTX_SUSPEND_Msk;
|
||||
twim->TASKS_RESUME = 1;
|
||||
twim->TASKS_STARTTX = 1;
|
||||
|
||||
@@ -58,7 +58,7 @@ extern "C" void HardFault_Impl(uint32_t stack[])
|
||||
// RAM contents, zero the DEK / KEK / ephemeralKEK so they aren't sitting
|
||||
// in BSS for a fault dump to pick up. This is called from the asm naked
|
||||
// HardFault_Handler entry above, so we're effectively in the chip's
|
||||
// exception context — keep this strictly to in-RAM scrubbing, no flash
|
||||
// exception context - keep this strictly to in-RAM scrubbing, no flash
|
||||
// I/O, no logging.
|
||||
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
||||
EncryptedStorage::secureWipeKeys();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Arduino.h — Zephyr compatibility shim for nRF54L15
|
||||
* Arduino.h - Zephyr compatibility shim for nRF54L15
|
||||
*
|
||||
* Provides the Arduino API surface expected by Meshtastic, backed by
|
||||
* Zephyr primitives. Only the subset actually used by Meshtastic is
|
||||
@@ -69,7 +69,7 @@ typedef uint16_t word;
|
||||
#define bit(b) (1UL << (b))
|
||||
#define lowByte(w) ((uint8_t)((w)&0xff))
|
||||
#define highByte(w) ((uint8_t)((w) >> 8))
|
||||
// word(h,l) — only define if not already defined (conflicts with typedef above)
|
||||
// word(h,l) - only define if not already defined (conflicts with typedef above)
|
||||
#undef word
|
||||
#define word(h, l) ((uint16_t)(((h) << 8) | (l)))
|
||||
|
||||
@@ -121,11 +121,11 @@ static inline void interrupts() {}
|
||||
static inline void noInterrupts() {}
|
||||
#define digitalPinToInterrupt(p) (p)
|
||||
|
||||
// ── portMAX_DELAY — freertosinc.h also defines this; let it win ──────────────
|
||||
// ── portMAX_DELAY - freertosinc.h also defines this; let it win ──────────────
|
||||
// We intentionally do NOT define portMAX_DELAY here. freertosinc.h defines
|
||||
// it for the FreeRTOS / Meshtastic threading layer and must not be overridden.
|
||||
|
||||
// ── Timing & system functions — declared with C linkage ──────────────────────
|
||||
// ── Timing & system functions - declared with C linkage ──────────────────────
|
||||
// buzz.cpp and others forward-declare delay() as extern "C"; keep linkage
|
||||
// consistent by wrapping in extern "C" here.
|
||||
#ifdef __cplusplus
|
||||
@@ -146,7 +146,7 @@ void yield(void);
|
||||
#include <cctype>
|
||||
#include <cstdarg>
|
||||
|
||||
// ── C++ STL — include BEFORE defining any min/max helpers ───────────────────
|
||||
// ── C++ STL - include BEFORE defining any min/max helpers ───────────────────
|
||||
// Include algorithm first so its min/max templates are in scope.
|
||||
// We must NOT define min/max as function-like macros: the C++ STL uses
|
||||
// 3-argument versions (min(a,b,comp)) that the preprocessor would treat as
|
||||
@@ -188,7 +188,7 @@ static inline long random(long lo, long hi)
|
||||
return hi > lo ? lo + rand() % (hi - lo) : lo;
|
||||
}
|
||||
|
||||
// ── GPIO — real Zephyr implementation (Phase 3) ──────────────────────────────
|
||||
// ── GPIO - real Zephyr implementation (Phase 3) ──────────────────────────────
|
||||
// Implemented in nrf54l15_arduino.cpp using Zephyr GPIO/SPI APIs.
|
||||
// Pin numbering: P0.n = n, P1.n = 16+n, P2.n = 32+n
|
||||
void pinMode(uint32_t pin, uint32_t mode);
|
||||
@@ -206,12 +206,12 @@ static inline void analogWrite(uint32_t, uint32_t) {}
|
||||
static inline void analogReadResolution(int) {}
|
||||
static inline void analogWriteResolution(int) {}
|
||||
|
||||
// ── __WFI — provided by CMSIS core_cm33.h; do NOT redefine here ─────────────
|
||||
// ── __WFI - provided by CMSIS core_cm33.h; do NOT redefine here ─────────────
|
||||
|
||||
// ── __FlashStringHelper — Arduino PROGMEM string class (no-op on Cortex-M) ──
|
||||
// ── __FlashStringHelper - Arduino PROGMEM string class (no-op on Cortex-M) ──
|
||||
class __FlashStringHelper;
|
||||
|
||||
// ── attachInterrupt / detachInterrupt — real Zephyr GPIO interrupt impl ──────
|
||||
// ── attachInterrupt / detachInterrupt - real Zephyr GPIO interrupt impl ──────
|
||||
typedef void (*voidFuncPtr)(void);
|
||||
void attachInterrupt(uint32_t pin, voidFuncPtr cb, int mode);
|
||||
void detachInterrupt(uint32_t pin);
|
||||
@@ -685,7 +685,7 @@ class HardwareSerial : public Stream
|
||||
String readStringUntil(char) { return String(); }
|
||||
};
|
||||
|
||||
// Uart — nRF52 BSP alias for HardwareSerial (used by GPS.h when ARCH_NRF52)
|
||||
// Uart - nRF52 BSP alias for HardwareSerial (used by GPS.h when ARCH_NRF52)
|
||||
typedef HardwareSerial Uart;
|
||||
|
||||
extern HardwareSerial Serial;
|
||||
@@ -715,7 +715,7 @@ static inline unsigned long pulseIn(uint8_t, uint8_t, unsigned long = 1000000UL)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── strdup / stpcpy — POSIX extensions not in Zephyr newlib ─────────────────
|
||||
// ── strdup / stpcpy - POSIX extensions not in Zephyr newlib ─────────────────
|
||||
#ifndef strdup
|
||||
static inline char *strdup(const char *s)
|
||||
{
|
||||
@@ -735,14 +735,14 @@ static inline char *stpcpy(char *dst, const char *src)
|
||||
}
|
||||
#endif
|
||||
|
||||
// ── strnstr — BSD extension not in Zephyr libc; defined in meshUtils.cpp ─────
|
||||
// ── strnstr - BSD extension not in Zephyr libc; defined in meshUtils.cpp ─────
|
||||
// Declare here so callers (GPS.cpp etc.) don't need ARCH_PORTDUINO.
|
||||
#ifndef STRNSTR
|
||||
#define STRNSTR
|
||||
char *strnstr(const char *s, const char *find, size_t slen);
|
||||
#endif
|
||||
|
||||
// ── strlcpy — BSD extension; implementation in nrf54l15_arduino.cpp ──────────
|
||||
// ── strlcpy - BSD extension; implementation in nrf54l15_arduino.cpp ──────────
|
||||
#ifndef HAVE_STRLCPY
|
||||
#define HAVE_STRLCPY
|
||||
#ifdef __cplusplus
|
||||
@@ -754,7 +754,7 @@ size_t strlcpy(char *dst, const char *src, size_t size);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// ── setenv / getenv / tzset — Zephyr stubs for timezone support ──────────────
|
||||
// ── setenv / getenv / tzset - Zephyr stubs for timezone support ──────────────
|
||||
#include <stdlib.h>
|
||||
static inline int setenv(const char *, const char *, int)
|
||||
{
|
||||
@@ -762,7 +762,7 @@ static inline int setenv(const char *, const char *, int)
|
||||
}
|
||||
static inline void tzset(void) {}
|
||||
|
||||
// ── dbgHeapFree / dbgHeapTotal — nRF52 BSP heap diagnostics ─────────────────
|
||||
// ── dbgHeapFree / dbgHeapTotal - nRF52 BSP heap diagnostics ─────────────────
|
||||
// Used by memGet.cpp when ARCH_NRF52 is defined. Return 0 for Phase 2.
|
||||
static inline uint32_t dbgHeapFree(void)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// IPAddress.h — stub for nRF54L15/Zephyr
|
||||
// IPAddress.h - stub for nRF54L15/Zephyr
|
||||
// MQTT.cpp includes this for IPv4 address representation.
|
||||
// Phase 2: compile-only stub.
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// InternalFileSystem.cpp — Zephyr LittleFS backend for nRF54L15
|
||||
// InternalFileSystem.cpp - Zephyr LittleFS backend for nRF54L15
|
||||
//
|
||||
// Implements Adafruit_LittleFS_Namespace used by FSCommon.h/cpp.
|
||||
// Storage: 36 KB storage_partition in nRF54L15 internal RRAM (defined in
|
||||
@@ -124,7 +124,7 @@ File InternalFileSystem::open(const char *path, const char *mode)
|
||||
// Open as a regular file
|
||||
fs_mode_t flags;
|
||||
if (strcmp(mode, FILE_O_WRITE) == 0) {
|
||||
// Truncate on write — unlink first to ensure a clean start
|
||||
// Truncate on write - unlink first to ensure a clean start
|
||||
fs_unlink(abs);
|
||||
flags = FS_O_WRITE | FS_O_CREATE;
|
||||
} else {
|
||||
@@ -198,7 +198,7 @@ bool InternalFileSystem::rmdir_r(const char *path)
|
||||
struct fs_dir_t dir;
|
||||
fs_dir_t_init(&dir);
|
||||
if (fs_opendir(&dir, abs) != 0) {
|
||||
// Not a directory — try to delete as file
|
||||
// Not a directory - try to delete as file
|
||||
return fs_unlink(abs) == 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// InternalFileSystem.h — Zephyr LittleFS backend for nRF54L15
|
||||
// InternalFileSystem.h - Zephyr LittleFS backend for nRF54L15
|
||||
//
|
||||
// Implements the Adafruit InternalFileSystem API subset used by Meshtastic,
|
||||
// backed by Zephyr's fs/littlefs on the storage_partition of the nRF54L15's
|
||||
// internal RRAM. Partition size is taken from the DTS at compile time via
|
||||
// FIXED_PARTITION_SIZE(storage_partition) — the DK overlay currently maps
|
||||
// FIXED_PARTITION_SIZE(storage_partition) - the DK overlay currently maps
|
||||
// ~700 KB into slot1 (see zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay).
|
||||
//
|
||||
// Mount point: /lfs
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// NRF52Bluetooth.h — stub for nRF54L15/Zephyr
|
||||
// NRF52Bluetooth.h - stub for nRF54L15/Zephyr
|
||||
// main.h includes this when ARCH_NRF52 is defined.
|
||||
// Bluetooth is excluded (MESHTASTIC_EXCLUDE_BLUETOOTH=1); this satisfies the
|
||||
// include chain without pulling in the nRF52 Bluefruit SDK.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// NRF54L15Bluetooth.cpp — Zephyr BLE GATT peripheral for Meshtastic nRF54L15
|
||||
// NRF54L15Bluetooth.cpp - Zephyr BLE GATT peripheral for Meshtastic nRF54L15
|
||||
//
|
||||
// GATT profile (identical UUIDs to the nRF52 / NimBLE implementations):
|
||||
// Service: 6ba1b218-15a8-461f-9fa8-5dcae273eafd
|
||||
@@ -37,7 +37,7 @@
|
||||
#include <zephyr/sys/reboot.h>
|
||||
|
||||
// ── UUID definitions (little-endian per Bluetooth spec)
|
||||
// ─────────────────────── Syntax: replace hyphens with commas, prefix 0x —
|
||||
// ─────────────────────── Syntax: replace hyphens with commas, prefix 0x -
|
||||
// matches BT_UUID_128_ENCODE doc.
|
||||
|
||||
#define MESH_SVC_UUID_VAL BT_UUID_128_ENCODE(0x6ba1b218, 0x15a8, 0x461f, 0x9fa8, 0x5dcae273eafd)
|
||||
@@ -61,7 +61,7 @@ static K_MUTEX_DEFINE(ble_mutex);
|
||||
// no active connection. Caller MUST bt_conn_unref() when done.
|
||||
//
|
||||
// Reading `active_conn` outside this lock races with disconnected_cb which can
|
||||
// unref + null it on the BT RX thread — touching the freed pointer (even just
|
||||
// unref + null it on the BT RX thread - touching the freed pointer (even just
|
||||
// to bt_conn_ref it) is a use-after-free.
|
||||
static struct bt_conn *acquire_active_conn()
|
||||
{
|
||||
@@ -77,7 +77,7 @@ static struct bt_conn *acquire_active_conn()
|
||||
static bool bt_initialized = false; // bt_enable() called at most once
|
||||
static bool ble_enabled = false; // set by setup(), cleared by shutdown()
|
||||
|
||||
// Forward declarations — BT_GATT_SERVICE_DEFINE(mesh_svc, ...) is below, but
|
||||
// Forward declarations - BT_GATT_SERVICE_DEFINE(mesh_svc, ...) is below, but
|
||||
// read_fromradio() (defined earlier) needs to reference the service to notify
|
||||
// on fromNum after each non-empty read.
|
||||
#define FROMNUM_ATTR_IDX 2
|
||||
@@ -111,7 +111,7 @@ static void adv_restart_work_fn(struct k_work *work)
|
||||
static uint16_t fromnum_ccc_val = 0;
|
||||
static uint16_t logradio_ccc_val = 0;
|
||||
|
||||
// Scratch buffers — only one BLE operation at a time
|
||||
// Scratch buffers - only one BLE operation at a time
|
||||
static uint8_t fromRadioBytes[meshtastic_FromRadio_size];
|
||||
static size_t fromRadioLen = 0;
|
||||
static uint8_t toRadioBytes[meshtastic_ToRadio_size];
|
||||
@@ -127,7 +127,7 @@ static uint32_t fromNumValue = 0;
|
||||
// buffer under a mutex and let BleDeferredThread (running on the Meshtastic
|
||||
// OSThread scheduler, 24 KB stack) do the actual call outside the lock.
|
||||
//
|
||||
// The mutex makes the producer/consumer handoff race-free — producer may
|
||||
// The mutex makes the producer/consumer handoff race-free - producer may
|
||||
// overwrite a pending buffer the consumer hasn't read yet (dropped packet),
|
||||
// but partial reads / torn writes are impossible.
|
||||
K_MUTEX_DEFINE(pendingToRadioMutex);
|
||||
@@ -141,7 +141,7 @@ static bool pendingToRadio = false;
|
||||
// LE Disconnection Complete event to the host: when iOS tears down the link
|
||||
// (either explicitly by the user or via supervision timeout), the LL layer
|
||||
// drops the connection but disconnected_cb never fires, active_conn stays
|
||||
// non-null and advertising never restarts — the device vanishes from scans
|
||||
// non-null and advertising never restarts - the device vanishes from scans
|
||||
// until power cycle. Track the connected timestamp and the last time we
|
||||
// observed ATT traffic; a long ATT idle on an "active" connection means we
|
||||
// are zombied. A cold reboot is the only path that reliably recovers (any
|
||||
@@ -228,7 +228,7 @@ static ssize_t write_toradio(struct bt_conn *conn, const struct bt_gatt_attr *at
|
||||
return BT_GATT_ERR(BT_ATT_ERR_INVALID_ATTRIBUTE_LEN);
|
||||
}
|
||||
|
||||
// Deduplicate — drop packet if identical to the last one we processed
|
||||
// Deduplicate - drop packet if identical to the last one we processed
|
||||
if (memcmp(lastToRadio, buf, len) != 0) {
|
||||
memcpy(lastToRadio, buf, len);
|
||||
if (len < MAX_TO_FROM_RADIO_SIZE) {
|
||||
@@ -236,7 +236,7 @@ static ssize_t write_toradio(struct bt_conn *conn, const struct bt_gatt_attr *at
|
||||
}
|
||||
// Defer handleToRadio() to BleDeferredThread (24 KB stack).
|
||||
// Running it here on bt_workq (6 KB) overflows during handleStartConfig.
|
||||
// Always overwrite pending — we already dedup'd above via lastToRadio,
|
||||
// Always overwrite pending - we already dedup'd above via lastToRadio,
|
||||
// so any new write here is genuinely new data that must be delivered.
|
||||
k_mutex_lock(&pendingToRadioMutex, K_FOREVER);
|
||||
memcpy(pendingToRadioBuf, buf, len);
|
||||
@@ -274,19 +274,19 @@ static ssize_t write_toradio(struct bt_conn *conn, const struct bt_gatt_attr *at
|
||||
|
||||
BT_GATT_SERVICE_DEFINE(mesh_svc, BT_GATT_PRIMARY_SERVICE(&mesh_svc_uuid.uuid),
|
||||
|
||||
// fromNum: READ | NOTIFY — packet-counter triggers phone to read fromRadio
|
||||
// fromNum: READ | NOTIFY - packet-counter triggers phone to read fromRadio
|
||||
BT_GATT_CHARACTERISTIC(&fromnum_uuid.uuid, BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY, MESH_PERM_READ,
|
||||
read_fromnum, NULL, &fromNumValue),
|
||||
BT_GATT_CCC(fromnum_ccc_changed, MESH_PERM_READ | MESH_PERM_WRITE),
|
||||
|
||||
// fromRadio: READ — phone polls this after receiving a fromNum notification
|
||||
// fromRadio: READ - phone polls this after receiving a fromNum notification
|
||||
BT_GATT_CHARACTERISTIC(&fromradio_uuid.uuid, BT_GATT_CHRC_READ, MESH_PERM_READ, read_fromradio, NULL,
|
||||
NULL),
|
||||
|
||||
// toRadio: WRITE — phone sends protobuf packets to the device
|
||||
// toRadio: WRITE - phone sends protobuf packets to the device
|
||||
BT_GATT_CHARACTERISTIC(&toradio_uuid.uuid, BT_GATT_CHRC_WRITE, MESH_PERM_WRITE, NULL, write_toradio, NULL),
|
||||
|
||||
// logRadio: READ | NOTIFY | INDICATE — log stream to phone when connected
|
||||
// logRadio: READ | NOTIFY | INDICATE - log stream to phone when connected
|
||||
BT_GATT_CHARACTERISTIC(&logradio_uuid.uuid,
|
||||
BT_GATT_CHRC_READ | BT_GATT_CHRC_NOTIFY | BT_GATT_CHRC_INDICATE, MESH_PERM_READ,
|
||||
read_logradio, NULL, NULL),
|
||||
@@ -308,7 +308,7 @@ BT_GATT_SERVICE_DEFINE(mesh_svc, BT_GATT_PRIMARY_SERVICE(&mesh_svc_uuid.uuid),
|
||||
// the subsequent bt_le_ext_adv_delete() sends LE Remove Advertising Set
|
||||
// (0x203c) which times out → kernel oops at hci_core.c:506.
|
||||
//
|
||||
// With CONFIG_BT_EXT_ADV=n the host uses pure legacy HCI commands — the same
|
||||
// With CONFIG_BT_EXT_ADV=n the host uses pure legacy HCI commands - the same
|
||||
// path Nordic NCS uses in all nRF54L15 examples (peripheral_uart,
|
||||
// peripheral_lbs) and which is universally iOS-compatible. The legacy data
|
||||
// payload is 31 bytes:
|
||||
@@ -328,7 +328,7 @@ static void start_advertising()
|
||||
|
||||
// Legacy scan-response payload is 31 bytes total. Each AD entry costs 2
|
||||
// bytes (length + type), leaving 29 bytes for the name. With
|
||||
// CONFIG_BT_DEVICE_NAME_MAX=32 the name can exceed that — truncate and
|
||||
// CONFIG_BT_DEVICE_NAME_MAX=32 the name can exceed that - truncate and
|
||||
// mark as SHORTENED so bt_le_adv_start() doesn't reject the payload.
|
||||
constexpr size_t LEGACY_SCAN_RSP_NAME_MAX = 31 - 2;
|
||||
bool name_shortened = full_name_len > LEGACY_SCAN_RSP_NAME_MAX;
|
||||
@@ -397,7 +397,7 @@ static void connected_cb(struct bt_conn *conn, uint8_t err)
|
||||
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
|
||||
// nRF54L15-DK has no screen — cannot display a PIN to the user.
|
||||
// nRF54L15-DK has no screen - cannot display a PIN to the user.
|
||||
// Requesting BT_SECURITY_L2 causes the OS to show a pairing dialog that
|
||||
// the user dismisses, triggering disconnect + advertising restart failure.
|
||||
// Skip security negotiation; the Meshtastic app works over plain GATT.
|
||||
@@ -428,7 +428,7 @@ static void disconnected_cb(struct bt_conn *conn, uint8_t reason)
|
||||
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
|
||||
bluetoothStatus->updateStatus(&newStatus);
|
||||
|
||||
// Schedule advertising restart via work queue — NOT from this callback
|
||||
// Schedule advertising restart via work queue - NOT from this callback
|
||||
// directly. disconnected_cb runs on the BT RX thread; calling
|
||||
// bt_le_adv_start() here would deadlock (see adv_restart_work comment above).
|
||||
if (ble_enabled) {
|
||||
@@ -442,7 +442,7 @@ static void security_changed_cb(struct bt_conn *conn, bt_security_t level, enum
|
||||
if (err == BT_SECURITY_ERR_PIN_OR_KEY_MISSING) {
|
||||
// Phone has a stale bond (device was wiped/reflashed). Unpair the stale
|
||||
// entry so the phone re-pairs cleanly on the next connection attempt.
|
||||
LOG_WARN("BLE stale bond detected (key missing) — unpairing");
|
||||
LOG_WARN("BLE stale bond detected (key missing) - unpairing");
|
||||
bt_unpair(BT_ID_DEFAULT, bt_conn_get_dst(conn));
|
||||
bt_conn_disconnect(conn, BT_HCI_ERR_AUTH_FAIL);
|
||||
} else if (err) {
|
||||
@@ -523,7 +523,7 @@ void BluetoothPhoneAPI::onNowHasData(uint32_t fromRadioNum)
|
||||
return;
|
||||
|
||||
// active_conn may be torn down on another thread while we're dispatching
|
||||
// this notify — acquire under ble_mutex so disconnected_cb can't free the
|
||||
// this notify - acquire under ble_mutex so disconnected_cb can't free the
|
||||
// conn between the null check and bt_conn_ref.
|
||||
struct bt_conn *conn = acquire_active_conn();
|
||||
if (!conn)
|
||||
@@ -606,7 +606,7 @@ class BleDeferredThread : public concurrency::OSThread
|
||||
uint32_t now = k_uptime_get_32();
|
||||
uint32_t att_idle = now - last_att_time_ms;
|
||||
|
||||
// Liveness probe — only when ATT has been quiet for a while.
|
||||
// Liveness probe - only when ATT has been quiet for a while.
|
||||
if (att_idle > IDLE_BEFORE_PROBE_MS && (now - last_probe_ms) >= PROBE_INTERVAL_MS &&
|
||||
(fromnum_ccc_val & BT_GATT_CCC_NOTIFY)) {
|
||||
last_probe_ms = now;
|
||||
@@ -620,7 +620,7 @@ class BleDeferredThread : public concurrency::OSThread
|
||||
}
|
||||
bt_conn_unref(conn);
|
||||
|
||||
// Hard ceiling — last-resort reboot if probes miss the zombie.
|
||||
// Hard ceiling - last-resort reboot if probes miss the zombie.
|
||||
if (att_idle > HARD_WATCHDOG_MS && (now - connect_time_ms) > HARD_WATCHDOG_MS) {
|
||||
LOG_WARN("BLE zombie (hard watchdog %us); rebooting", HARD_WATCHDOG_MS / 1000);
|
||||
k_sleep(K_MSEC(50));
|
||||
@@ -635,7 +635,7 @@ static BleDeferredThread *bleDeferredThread = nullptr;
|
||||
// ── BT stack pre-initializer (call from main thread before OSThreads start) ──
|
||||
//
|
||||
// bt_enable() requires substantially more stack than a Meshtastic OSThread
|
||||
// (PowerFSMThread) provides — calling it there causes a stack overflow.
|
||||
// (PowerFSMThread) provides - calling it there causes a stack overflow.
|
||||
// Call this from nrf54l15Setup() (main Zephyr thread, CONFIG_MAIN_STACK_SIZE)
|
||||
// so that by the time NRF54L15Bluetooth::setup() runs from PowerFSMThread,
|
||||
// bt_initialized is already true and bt_enable() is skipped.
|
||||
@@ -653,7 +653,7 @@ void nrf54l15_bt_preinit()
|
||||
|
||||
// Phase 7: load bonding keys from LittleFS (/lfs/bt_settings).
|
||||
// LittleFS is already mounted by fsInit() before nrf54l15Setup() runs.
|
||||
// On first boot the file doesn't exist — settings_load() returns 0 (OK).
|
||||
// On first boot the file doesn't exist - settings_load() returns 0 (OK).
|
||||
// On subsequent boots, previously bonded peers are restored so the
|
||||
// phone can reconnect without re-pairing.
|
||||
err = settings_load();
|
||||
@@ -697,7 +697,7 @@ static bool nrf54l15_bt_init_common()
|
||||
bt_conn_auth_cb_register(&auth_cb);
|
||||
bt_conn_auth_info_cb_register(&auth_info_cb);
|
||||
|
||||
// FIXED_PIN — register the configured passkey so the mobile app prompts
|
||||
// FIXED_PIN - register the configured passkey so the mobile app prompts
|
||||
// the user for that specific number instead of a random display-only PIN.
|
||||
// RANDOM_PIN (and clamped NO_PIN) keeps the default behavior: Zephyr
|
||||
// generates a fresh passkey on each pairing attempt and fires
|
||||
@@ -797,7 +797,7 @@ void NRF54L15Bluetooth::sendLog(const uint8_t *logMessage, size_t length)
|
||||
return;
|
||||
}
|
||||
// Send as notify regardless of whether client subscribed to NOTIFY or
|
||||
// INDICATE — bt_gatt_indicate() requires a params struct with a callback;
|
||||
// INDICATE - bt_gatt_indicate() requires a params struct with a callback;
|
||||
// notify is simpler and the app accepts both. Change to indicate if
|
||||
// compatibility issues arise.
|
||||
bt_gatt_notify(conn, &mesh_svc.attrs[LOGRADIO_ATTR_IDX], logMessage, (uint16_t)length);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// NRF54L15Bluetooth.h — Zephyr BLE backend for nRF54L15
|
||||
// NRF54L15Bluetooth.h - Zephyr BLE backend for nRF54L15
|
||||
//
|
||||
// Implements the same interface as NRF52Bluetooth (same method names and
|
||||
// signatures) so main.cpp and AdminModule can use nrf52Bluetooth pointer
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Nrf52SaadcLock.h — stub for nRF54L15/Zephyr
|
||||
// Nrf52SaadcLock.h - stub for nRF54L15/Zephyr
|
||||
// Power.cpp includes this when ARCH_NRF52 is defined.
|
||||
// Phase 2: compile-only stub.
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Print.h — shim for nRF54L15/Zephyr
|
||||
// Print.h - shim for nRF54L15/Zephyr
|
||||
// Meshtastic includes <Print.h> separately; redirect to our Arduino.h shim.
|
||||
#pragma once
|
||||
#include "Arduino.h"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* SPI.h — Arduino SPI shim for Zephyr/nRF54L15
|
||||
* SPI.h - Arduino SPI shim for Zephyr/nRF54L15
|
||||
*
|
||||
* Provides the Arduino SPIClass interface backed by Zephyr's SPI API. The
|
||||
* backing controller is SPIM00 (HP domain, 3.0 V); the implementation in
|
||||
@@ -7,7 +7,7 @@
|
||||
* bus is configured in zephyr/boards/nrf54l15dk_nrf54l15_cpuapp.overlay.
|
||||
* RadioLib uses ArduinoHal which calls transfer() byte-by-byte.
|
||||
*
|
||||
* CS pin is handled by RadioLib via digitalWrite() — hardware CS is not used.
|
||||
* CS pin is handled by RadioLib via digitalWrite() - hardware CS is not used.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
@@ -25,7 +25,7 @@ struct SPISettings {
|
||||
uint8_t bitOrder;
|
||||
uint8_t dataMode;
|
||||
|
||||
// Arduino API allows `SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0))` — implicit form is intentional.
|
||||
// Arduino API allows `SPI.beginTransaction(SPISettings(8000000, MSBFIRST, SPI_MODE0))` - implicit form is intentional.
|
||||
// cppcheck-suppress noExplicitConstructor
|
||||
SPISettings(uint32_t clock = 4000000, uint8_t bitOrder = MSBFIRST, uint8_t dataMode = SPI_MODE0)
|
||||
: clock(clock), bitOrder(bitOrder), dataMode(dataMode)
|
||||
@@ -46,7 +46,7 @@ class SPIClass
|
||||
void setClockDivider(uint8_t div) {}
|
||||
void setFrequency(uint32_t freq) {}
|
||||
|
||||
// Real Zephyr SPI implementation — defined in nrf54l15_arduino.cpp
|
||||
// Real Zephyr SPI implementation - defined in nrf54l15_arduino.cpp
|
||||
uint8_t transfer(uint8_t data);
|
||||
uint16_t transfer16(uint16_t data);
|
||||
void transfer(void *buf, size_t count);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Stream.h — shim for nRF54L15/Zephyr
|
||||
// Stream.h - shim for nRF54L15/Zephyr
|
||||
// StreamAPI.h and other Meshtastic headers include <Stream.h>.
|
||||
// Redirect to our Arduino.h shim which defines the Stream base class.
|
||||
#pragma once
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Tone.h — shim for nRF54L15/Zephyr
|
||||
// Tone.h - shim for nRF54L15/Zephyr
|
||||
// Tone functions are stubbed in Arduino.h; this header satisfies direct includes.
|
||||
#pragma once
|
||||
#include "Arduino.h"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user