emdashes begone (#10847)

This commit is contained in:
Tom
2026-07-01 19:01:27 -05:00
committed by GitHub
parent dee94e0758
commit 3becaf2d95
276 changed files with 1795 additions and 1793 deletions
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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";
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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"
+3 -3
View File
@@ -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);
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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) {
+4 -4
View File
@@ -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
+3 -3
View File
@@ -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
+4 -4
View File
@@ -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
View File
@@ -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();
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+5 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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();
};
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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)
+4 -4
View File
@@ -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);
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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).
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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.
///
+3 -3
View File
@@ -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()
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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
View File
@@ -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);
+5 -5
View File
@@ -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
+4 -4
View File
@@ -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
+3 -3
View File
@@ -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;
};
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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();
+6 -6
View File
@@ -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
+1 -1
View File
@@ -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();
+4 -4
View File
@@ -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
}
+7 -7
View File
@@ -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();
}
+8 -8
View File
@@ -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
+3 -3
View File
@@ -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
+23 -23
View File
@@ -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;
+9 -9
View File
@@ -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). */
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+1 -1
View File
@@ -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;
}
+2 -2
View File
@@ -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
+4 -4
View File
@@ -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);
+18 -18
View File
@@ -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
+17 -17
View File
@@ -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);
}
+3 -3
View File
@@ -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:
+3 -3
View File
@@ -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;
+1 -1
View File
@@ -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);
+3 -3
View File
@@ -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.
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+21 -21
View File
@@ -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)
+5 -5
View File
@@ -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
View File
@@ -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;
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -125,7 +125,7 @@ bool readRegister(uint8_t address, uint8_t reg, uint8_t &value)
twim->TXD.PTR = (uint32_t)&reg;
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;
+1 -1
View File
@@ -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();
+15 -15
View File
@@ -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 -1
View File
@@ -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
+3 -3
View File
@@ -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;
}
+2 -2
View File
@@ -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 -1
View File
@@ -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.
+25 -25
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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"
+4 -4
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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