Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8336a54b9 | ||
|
|
abef0d85a2 | ||
|
|
b39fbd4ed3 | ||
|
|
6b3f975ba5 | ||
|
|
e028663658 | ||
|
|
bf68b9e597 | ||
|
|
a9b98f47e9 | ||
|
|
90a3ac5938 |
@@ -87,6 +87,12 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="2.7.26" date="2026-06-10">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.26</url>
|
||||
</release>
|
||||
<release version="2.7.25" date="2026-05-23">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.25</url>
|
||||
</release>
|
||||
<release version="2.7.24" date="2026-05-08">
|
||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.24</url>
|
||||
</release>
|
||||
|
||||
Vendored
+12
@@ -1,3 +1,15 @@
|
||||
meshtasticd (2.7.26.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.26
|
||||
|
||||
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Wed, 10 Jun 2026 00:19:23 +0000
|
||||
|
||||
meshtasticd (2.7.25.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.25
|
||||
|
||||
-- GitHub Actions <github-actions[bot]@users.noreply.github.com> Sat, 23 May 2026 01:16:20 +0000
|
||||
|
||||
meshtasticd (2.7.24.0) unstable; urgency=medium
|
||||
|
||||
* Version 2.7.24
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
# trunk-ignore-all(ruff/F821)
|
||||
# trunk-ignore-all(flake8/F821)
|
||||
#
|
||||
# Whole-image LTO for nrf52840 (~-60KB; ~-23KB beyond src-only LTO), EXCEPT the objects
|
||||
# that own interrupt/exception handlers.
|
||||
#
|
||||
# Every ISR is referenced only from the assembly vector table (gcc_startup_nrf52840.S),
|
||||
# which LTO cannot see -> whole-program LTO judges the handlers dead, removes them, and
|
||||
# the weak `b .` Default_Handler stubs prevail -> the IRQ lands in an infinite loop and the
|
||||
# chip hangs (or the peripheral silently stalls). Compiling the handler-bearing objects
|
||||
# WITHOUT LTO lets ordinary linking keep the strong handlers; everything else stays LTO'd:
|
||||
# - framework core (/FrameworkArduino/, /cores/nRF5/): every nrfx ISR + the FreeRTOS
|
||||
# SVC/PendSV port.
|
||||
# - TinyUSB nrf port (Adafruit_TinyUSB_nrf.cpp): USBD_IRQHandler (USB data path).
|
||||
# - library .cpp files that own a vector ISR (would otherwise be silently dropped):
|
||||
# bluefruit.cpp -> SD_EVT/SWI2_EGU2 (SoftDevice BLE-event delivery -- advertising
|
||||
# hangs without it)
|
||||
# Wire_nRF52.cpp -> SPIM0/TWIM0 + SPIM1/TWIM1 (interrupt-driven I2C/SPI)
|
||||
# PDM.cpp -> PDM_IRQHandler (PDM microphone)
|
||||
# RotaryEncoder.cpp -> QDEC_IRQHandler (hardware quadrature/rotary encoder)
|
||||
#
|
||||
# A post-link guard (bottom of this file) fails the build if a critical handler was dropped
|
||||
# anyway -- so a future deps bump or a new ISR-owning library becomes a red build, not a field
|
||||
# hang. To hunt a dropped ISR by hand: nm the .elf for `_IRQHandler$` symbols marked `W`, then
|
||||
# grep the libs/framework for who defines them.
|
||||
#
|
||||
# HW-validated: RAK4631 (SX1262) + muzi-base (LR1121).
|
||||
import glob
|
||||
import os
|
||||
|
||||
Import("env")
|
||||
|
||||
env.Append(LINKFLAGS=["-flto", "-flto-partition=1to1"])
|
||||
|
||||
# The -fno-lto re-compiles below run with the global env, which lacks the framework's
|
||||
# bundled-library include dirs -- and those libs cross-include each other (Wire pulls in
|
||||
# Adafruit_TinyUSB.h, which pulls in SPI.h, ...). Add every bundled-lib dir (+ its src/) so
|
||||
# the re-compiles resolve without chasing headers one at a time.
|
||||
_fw = env.PioPlatform().get_package_dir("framework-arduinoadafruitnrf52") or ""
|
||||
_extra_inc = []
|
||||
for _d in sorted(glob.glob(os.path.join(_fw, "libraries", "*"))):
|
||||
if os.path.isdir(_d):
|
||||
_extra_inc.append(_d)
|
||||
if os.path.isdir(os.path.join(_d, "src")):
|
||||
_extra_inc.append(os.path.join(_d, "src"))
|
||||
|
||||
FRAMEWORK = ("/FrameworkArduino/", "/cores/nRF5/")
|
||||
USB_ISR = "Adafruit_TinyUSB_nrf" # USBD_IRQHandler
|
||||
# Library .cpp files that define vector-table ISRs (the rest of their lib stays LTO'd):
|
||||
LIB_ISR = ("/bluefruit.cpp", "/Wire_nRF52.cpp", "/PDM.cpp", "/RotaryEncoder.cpp")
|
||||
|
||||
|
||||
def _no_lto(node):
|
||||
try:
|
||||
path = node.get_abspath()
|
||||
except Exception:
|
||||
path = str(node)
|
||||
path = path.replace(
|
||||
"\\", "/"
|
||||
) # normalize Windows backslashes so matches work cross-platform
|
||||
if (
|
||||
USB_ISR in path
|
||||
or any(s in path for s in FRAMEWORK)
|
||||
or any(s in path for s in LIB_ISR)
|
||||
):
|
||||
return env.Object(
|
||||
node,
|
||||
CCFLAGS=env["CCFLAGS"] + ["-fno-lto"],
|
||||
CPPPATH=env["CPPPATH"] + _extra_inc,
|
||||
)
|
||||
return node
|
||||
|
||||
|
||||
env.AddBuildMiddleware(_no_lto)
|
||||
|
||||
|
||||
# --- post-link guard: catch a dropped ISR handler at build time (CI footgun protection) ----
|
||||
# After every link, fail the build if one of these critical vector-table handlers resolved to
|
||||
# the weak `b .` Default_Handler stub -- i.e. LTO (or a deps bump, or a new ISR-owning library
|
||||
# that nobody added to LIB_ISR) silently dropped it. A dropped handler hangs the chip the
|
||||
# instant that IRQ fires; this turns a field hang into a red build. CI builds every nrf52840
|
||||
# target, so this runs on every PR automatically. All five are used by every nrf52840
|
||||
# Meshtastic build; if a board deliberately stops using one, edit this tuple on purpose.
|
||||
_REQUIRED_STRONG = (
|
||||
"SWI2_EGU2_IRQHandler", # SoftDevice BLE event (SD_EVT) -- advertising & connections
|
||||
"GPIOTE_IRQHandler", # GPIO interrupts: radio DIO + buttons
|
||||
"RTC1_IRQHandler", # FreeRTOS scheduler tick
|
||||
"USBD_IRQHandler", # USB CDC (serial console + 1200bps DFU trigger)
|
||||
"POWER_CLOCK_IRQHandler", # HF/LF clock + power (HFCLK start for radio & SoftDevice)
|
||||
)
|
||||
|
||||
_tc = env.PioPlatform().get_package_dir("toolchain-gccarmnoneeabi") or ""
|
||||
_NM = os.path.join(_tc, "bin", "arm-none-eabi-nm")
|
||||
if not os.path.isfile(_NM):
|
||||
_NM = "arm-none-eabi-nm" # fall back to PATH
|
||||
|
||||
|
||||
def _assert_isr_handlers_survived(source, target, env):
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
try:
|
||||
# Resolve the ELF at build time; target[0] is the buildprog alias, not the file.
|
||||
elf = env.subst("$BUILD_DIR/${PROGNAME}.elf")
|
||||
out = subprocess.check_output([_NM, elf], universal_newlines=True)
|
||||
except Exception as exc: # tooling hiccup: warn loudly, don't wedge the build
|
||||
print("nrf52_lto: WARNING - ISR-handler guard skipped (nm failed: %s)" % exc)
|
||||
return
|
||||
# nm line: "<addr> <type> <symbol>". type 'T'/'t' = strong (good); 'W'/'w' = weak stub.
|
||||
kind = {}
|
||||
for line in out.split("\n"):
|
||||
f = line.split()
|
||||
if len(f) >= 3 and f[-1].endswith("_IRQHandler"):
|
||||
kind[f[-1]] = f[-2]
|
||||
dropped = [h for h in _REQUIRED_STRONG if kind.get(h, "W").upper() != "T"]
|
||||
if dropped:
|
||||
sys.stderr.write(
|
||||
"\n*** nrf52 LTO guard: interrupt handler(s) DROPPED: %s ***\n"
|
||||
"Each resolved to the weak Default_Handler stub, so the chip hangs when that IRQ\n"
|
||||
"fires. Compile the .cpp that defines the handler with -fno-lto by adding it to\n"
|
||||
"LIB_ISR in extra_scripts/nrf52_lto.py. Find the owner of FOO_IRQHandler with:\n"
|
||||
" grep -rl FOO_IRQHandler <framework-arduinoadafruitnrf52>/{libraries,cores}\n\n"
|
||||
% ", ".join(dropped)
|
||||
)
|
||||
from SCons.Script import Exit
|
||||
|
||||
Exit(1) # canonical SCons build-abort -> red build
|
||||
print(
|
||||
"nrf52_lto: ISR-handler guard OK -- %d critical handlers strong"
|
||||
% len(_REQUIRED_STRONG)
|
||||
)
|
||||
|
||||
|
||||
# Attach to the phony "buildprog" alias, NOT the .elf file node: SCons can skip a post-action
|
||||
# on a file target during an incremental relink (observed), but the buildprog alias runs every
|
||||
# build -- so the guard fires on local incremental rebuilds and clean CI builds alike.
|
||||
env.AddPostAction("buildprog", _assert_isr_handlers_survived)
|
||||
@@ -14,6 +14,7 @@
|
||||
* For more information, see: https://meshtastic.org/
|
||||
*/
|
||||
#include "power.h"
|
||||
#include "BluetoothCommon.h"
|
||||
#include "MessageStore.h"
|
||||
#include "NodeDB.h"
|
||||
#include "PowerFSM.h"
|
||||
@@ -962,6 +963,10 @@ void Power::readPowerStatus()
|
||||
lastLogTime = millis();
|
||||
}
|
||||
newStatus.notifyObservers(&powerStatus2);
|
||||
|
||||
// Mirror battery level to the BLE Battery Service (0x2A19); the platform layer clamps and dedupes.
|
||||
if (hasBattery == OptTrue)
|
||||
updateBatteryLevel(powerStatus2.getBatteryChargePercent());
|
||||
#ifdef DEBUG_HEAP
|
||||
if (lastheap != memGet.getFreeHeap()) {
|
||||
// Use stack-allocated buffer to avoid heap allocations in monitoring code
|
||||
|
||||
+299
-12
@@ -17,7 +17,10 @@
|
||||
#include "main.h" // pmu_found
|
||||
#include "sleep.h"
|
||||
|
||||
#include "FSCommon.h"
|
||||
#include "GPSUpdateScheduling.h"
|
||||
#include "SPILock.h"
|
||||
#include "SafeFile.h"
|
||||
#include "cas.h"
|
||||
#include "ubx.h"
|
||||
|
||||
@@ -71,6 +74,67 @@ static struct uBloxGnssModelInfo {
|
||||
#define GPS_SOL_EXPIRY_MS 5000 // in millis. give 1 second time to combine different sentences. NMEA Frequency isn't higher anyway
|
||||
#define NMEA_MSG_GXGSA "GNGSA" // GSA message (GPGSA, GNGSA etc)
|
||||
|
||||
namespace
|
||||
{
|
||||
// Versioned on-disk record for persisted GPS probe results.
|
||||
constexpr uint32_t GPS_PROBE_CACHE_MAGIC = 0x47504348UL; // "GPCH"
|
||||
constexpr uint16_t GPS_PROBE_CACHE_VERSION = 1;
|
||||
constexpr const char *GPS_PROBE_CACHE_FILE = "/prefs/gps_probe_cache.dat";
|
||||
|
||||
struct GPSProbeCacheRecord {
|
||||
uint32_t magic;
|
||||
uint16_t version;
|
||||
uint16_t reserved;
|
||||
uint32_t baud;
|
||||
uint8_t model;
|
||||
};
|
||||
|
||||
bool isValidGnssModel(uint8_t model)
|
||||
{
|
||||
// Keep persisted values bounded to known enum range.
|
||||
return model <= static_cast<uint8_t>(GNSS_MODEL_CM121);
|
||||
}
|
||||
|
||||
bool isValidProbeBaud(uint32_t baud)
|
||||
{
|
||||
// Conservative sanity range for UART baud values.
|
||||
return baud >= 1200 && baud <= 921600;
|
||||
}
|
||||
|
||||
template <typename T> bool sawNmeaSentenceAtBaud(T *serialGps, uint32_t timeoutMs)
|
||||
{
|
||||
// Lightweight passive check: look for at least one complete
|
||||
// "$...,<field>\n" style NMEA sentence.
|
||||
const uint32_t deadline = millis() + timeoutMs;
|
||||
bool sawDollar = false;
|
||||
bool sawComma = false;
|
||||
|
||||
while ((int32_t)(millis() - deadline) < 0) {
|
||||
while (serialGps->available()) {
|
||||
char c = static_cast<char>(serialGps->read());
|
||||
if (c == '$') {
|
||||
sawDollar = true;
|
||||
sawComma = false;
|
||||
continue;
|
||||
}
|
||||
if (c == ',') {
|
||||
sawComma = true;
|
||||
}
|
||||
if (c == '\n' || c == '\r') {
|
||||
if (sawDollar && sawComma) {
|
||||
return true;
|
||||
}
|
||||
sawDollar = false;
|
||||
sawComma = false;
|
||||
}
|
||||
}
|
||||
delay(10);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// For logging
|
||||
static const char *getGPSPowerStateString(GPSPowerState state)
|
||||
{
|
||||
@@ -492,6 +556,201 @@ static const int rareSerialSpeeds[3] = {4800, 57600, GPS_BAUDRATE};
|
||||
#define GPS_PROBETRIES 2
|
||||
#endif
|
||||
|
||||
bool GPS::loadProbeCache()
|
||||
{
|
||||
#ifdef FSCom
|
||||
// Load the last known-good GPS model/baud pair so we can avoid a full probe
|
||||
// sweep on every boot.
|
||||
triedProbeCache = true; // Latch this boot's load attempt, even if no cache.
|
||||
GPSProbeCacheRecord record = {};
|
||||
size_t bytesRead = 0;
|
||||
|
||||
spiLock->lock();
|
||||
auto file = FSCom.open(GPS_PROBE_CACHE_FILE, FILE_O_READ);
|
||||
if (!file) {
|
||||
spiLock->unlock();
|
||||
return false;
|
||||
}
|
||||
bytesRead = file.read(reinterpret_cast<uint8_t *>(&record), sizeof(record));
|
||||
file.close();
|
||||
spiLock->unlock();
|
||||
|
||||
const bool headerValid = (bytesRead == sizeof(record)) && (record.magic == GPS_PROBE_CACHE_MAGIC) &&
|
||||
(record.version == GPS_PROBE_CACHE_VERSION) && (record.reserved == 0U);
|
||||
if (!headerValid || !isValidGnssModel(record.model) || !isValidProbeBaud(record.baud)) {
|
||||
clearProbeCache(); // Drop corrupt/invalid cache so next boot can
|
||||
// recover.
|
||||
return false;
|
||||
}
|
||||
|
||||
cachedProbeBaud = static_cast<int32_t>(record.baud);
|
||||
cachedProbeModel = static_cast<GnssModel_t>(record.model);
|
||||
hasProbeCache = true;
|
||||
triedProbeCache = false;
|
||||
LOG_INFO("Loaded cached GPS probe: baud=%u", record.baud);
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void GPS::clearProbeCache()
|
||||
{
|
||||
// Invalidate in-memory and on-disk cache so next boot is forced to do a
|
||||
// full probe.
|
||||
hasProbeCache = false;
|
||||
triedProbeCache = true;
|
||||
cachedProbeBaud = 0;
|
||||
cachedProbeModel = GNSS_MODEL_UNKNOWN;
|
||||
#ifdef FSCom
|
||||
spiLock->lock();
|
||||
if (FSCom.exists(GPS_PROBE_CACHE_FILE)) {
|
||||
FSCom.remove(GPS_PROBE_CACHE_FILE);
|
||||
}
|
||||
spiLock->unlock();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool GPS::saveProbeCache() const
|
||||
{
|
||||
#ifdef FSCom
|
||||
if (gnssModel == GNSS_MODEL_UNKNOWN || !isValidGnssModel(static_cast<uint8_t>(gnssModel)) ||
|
||||
!isValidProbeBaud(detectedBaud)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
spiLock->lock();
|
||||
FSCom.mkdir("/prefs");
|
||||
spiLock->unlock();
|
||||
GPSProbeCacheRecord record = {
|
||||
GPS_PROBE_CACHE_MAGIC, GPS_PROBE_CACHE_VERSION, 0, static_cast<uint32_t>(detectedBaud), static_cast<uint8_t>(gnssModel),
|
||||
};
|
||||
|
||||
auto file = SafeFile(GPS_PROBE_CACHE_FILE, true);
|
||||
spiLock->lock();
|
||||
const size_t written = file.write(reinterpret_cast<const uint8_t *>(&record), sizeof(record));
|
||||
spiLock->unlock();
|
||||
return (written == sizeof(record)) && file.close();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool GPS::verifyCachedProbePresence()
|
||||
{
|
||||
if (!hasProbeCache || cachedProbeModel == GNSS_MODEL_UNKNOWN || !isValidProbeBaud(cachedProbeBaud)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if defined(ARCH_NRF52) || defined(ARCH_PORTDUINO) || defined(ARCH_STM32WL)
|
||||
_serial_gps->end();
|
||||
_serial_gps->begin(cachedProbeBaud);
|
||||
#elif defined(ARCH_RP2040)
|
||||
_serial_gps->end();
|
||||
_serial_gps->setFIFOSize(256);
|
||||
_serial_gps->begin(cachedProbeBaud);
|
||||
#else
|
||||
if (_serial_gps->baudRate() != cachedProbeBaud) {
|
||||
LOG_DEBUG("Set GPS Baud to %i (cached verify)", cachedProbeBaud);
|
||||
_serial_gps->updateBaudRate(cachedProbeBaud);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Before trusting cached model/baud, require either active model-specific
|
||||
// response or passive NMEA flow.
|
||||
clearBuffer();
|
||||
bool present = false;
|
||||
|
||||
// Model-specific "active ping" checks to avoid false stale decisions on
|
||||
// modules that start streaming late.
|
||||
const char *cachedProbeModelName = "UNKNOWN";
|
||||
switch (cachedProbeModel) {
|
||||
case GNSS_MODEL_MTK:
|
||||
cachedProbeModelName = "L76K/MTK";
|
||||
_serial_gps->write("$PCAS06,0*1B\r\n");
|
||||
present = (getACK("$GPTXT,01,01,02,SW=", 700) == GNSS_RESPONSE_OK);
|
||||
break;
|
||||
case GNSS_MODEL_MTK_L76B:
|
||||
cachedProbeModelName = "L76B";
|
||||
case GNSS_MODEL_MTK_PA1010D:
|
||||
if (cachedProbeModel == GNSS_MODEL_MTK_PA1010D)
|
||||
cachedProbeModelName = "PA1010D";
|
||||
case GNSS_MODEL_MTK_PA1616S:
|
||||
if (cachedProbeModel == GNSS_MODEL_MTK_PA1616S)
|
||||
cachedProbeModelName = "PA1616S";
|
||||
case GNSS_MODEL_LS20031:
|
||||
if (cachedProbeModel == GNSS_MODEL_LS20031)
|
||||
cachedProbeModelName = "LS20031";
|
||||
_serial_gps->write("$PMTK605*31\r\n");
|
||||
present = (getACK("$PMTK705", 900) == GNSS_RESPONSE_OK);
|
||||
break;
|
||||
case GNSS_MODEL_AG3335:
|
||||
cachedProbeModelName = "AG3335";
|
||||
case GNSS_MODEL_AG3352:
|
||||
if (cachedProbeModel == GNSS_MODEL_AG3352)
|
||||
cachedProbeModelName = "AG3352";
|
||||
_serial_gps->write("$PAIR021*39\r\n");
|
||||
present = (getACK("$PAIR021,", 900) == GNSS_RESPONSE_OK);
|
||||
break;
|
||||
case GNSS_MODEL_ATGM336H:
|
||||
cachedProbeModelName = "ATGM336H";
|
||||
_serial_gps->write("$PCAS06,1*1A\r\n");
|
||||
present = (getACK("$GPTXT,01,01,02,HW=ATGM", 900) == GNSS_RESPONSE_OK);
|
||||
break;
|
||||
case GNSS_MODEL_UC6580:
|
||||
cachedProbeModelName = "UC6580/UM600";
|
||||
_serial_gps->write("$PDTINFO\r\n");
|
||||
present = (getACK("UC6580", 900) == GNSS_RESPONSE_OK) || (getACK("UM600", 900) == GNSS_RESPONSE_OK);
|
||||
break;
|
||||
case GNSS_MODEL_CM121:
|
||||
cachedProbeModelName = "CM121";
|
||||
_serial_gps->write("$PDTINFO\r\n");
|
||||
present = (getACK("CM121", 900) == GNSS_RESPONSE_OK);
|
||||
break;
|
||||
case GNSS_MODEL_UBLOX6:
|
||||
case GNSS_MODEL_UBLOX7:
|
||||
case GNSS_MODEL_UBLOX8:
|
||||
case GNSS_MODEL_UBLOX9:
|
||||
case GNSS_MODEL_UBLOX10: {
|
||||
if (cachedProbeModel == GNSS_MODEL_UBLOX6)
|
||||
cachedProbeModelName = "U-blox 6";
|
||||
else if (cachedProbeModel == GNSS_MODEL_UBLOX7)
|
||||
cachedProbeModelName = "U-blox 7";
|
||||
else if (cachedProbeModel == GNSS_MODEL_UBLOX8)
|
||||
cachedProbeModelName = "U-blox 8";
|
||||
else if (cachedProbeModel == GNSS_MODEL_UBLOX9)
|
||||
cachedProbeModelName = "U-blox 9";
|
||||
else if (cachedProbeModel == GNSS_MODEL_UBLOX10)
|
||||
cachedProbeModelName = "U-blox 10";
|
||||
|
||||
uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x00, 0x00};
|
||||
UBXChecksum(cfg_rate, sizeof(cfg_rate));
|
||||
_serial_gps->write(cfg_rate, sizeof(cfg_rate));
|
||||
present = (getACK(0x06, 0x08, 900) != GNSS_RESPONSE_NONE);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (!present) {
|
||||
// Some modules may not respond to probes while still streaming NMEA, so
|
||||
// allow a passive fallback check.
|
||||
present = sawNmeaSentenceAtBaud(_serial_gps, 3000);
|
||||
}
|
||||
if (!present) {
|
||||
LOG_WARN("Cached GPS probe is stale (%s @ %d), clearing cache", cachedProbeModelName, cachedProbeBaud);
|
||||
clearProbeCache();
|
||||
cachedProbeFailedThisBoot = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
detectedBaud = cachedProbeBaud;
|
||||
gnssModel = cachedProbeModel;
|
||||
LOG_INFO("Using cached GPS probe: %s @ %d", cachedProbeModelName, detectedBaud);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Setup the GPS based on the model detected.
|
||||
* We detect the GPS by cycling through a set of baud rates, first common then rare.
|
||||
@@ -503,25 +762,46 @@ bool GPS::setup()
|
||||
{
|
||||
if (!didSerialInit) {
|
||||
int msglen = 0;
|
||||
if (cachedProbeFailedThisBoot) {
|
||||
// If cached verification failed, suppress further probing until
|
||||
// reboot.
|
||||
didSerialInit = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (tx_gpio && gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||
if (probeTries < GPS_PROBETRIES) {
|
||||
if (!hasProbeCache && !triedProbeCache) {
|
||||
(void)loadProbeCache();
|
||||
}
|
||||
|
||||
if (hasProbeCache && !triedProbeCache) {
|
||||
triedProbeCache = true;
|
||||
if (!verifyCachedProbePresence()) {
|
||||
// Cache was stale and got wiped; skip scanning this boot
|
||||
// and let next boot do a full probe.
|
||||
didSerialInit = true;
|
||||
return true;
|
||||
}
|
||||
} else if (probeTries < GPS_PROBETRIES) {
|
||||
// No usable cache: walk common baud rates first.
|
||||
gnssModel = probe(serialSpeeds[speedSelect]);
|
||||
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||
if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) {
|
||||
speedSelect = 0;
|
||||
++probeTries;
|
||||
}
|
||||
if (gnssModel != GNSS_MODEL_UNKNOWN) {
|
||||
detectedBaud = serialSpeeds[speedSelect];
|
||||
} else if (currentStep == 0 && ++speedSelect == array_count(serialSpeeds)) {
|
||||
speedSelect = 0;
|
||||
++probeTries;
|
||||
}
|
||||
}
|
||||
// Rare Serial Speeds
|
||||
#ifndef CONFIG_IDF_TARGET_ESP32C6
|
||||
if (probeTries == GPS_PROBETRIES) {
|
||||
else if (probeTries == GPS_PROBETRIES) {
|
||||
// Then try less common baud rates before giving up.
|
||||
gnssModel = probe(rareSerialSpeeds[speedSelect]);
|
||||
if (gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||
if (currentStep == 0 && ++speedSelect == array_count(rareSerialSpeeds)) {
|
||||
LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE);
|
||||
return true;
|
||||
}
|
||||
if (gnssModel != GNSS_MODEL_UNKNOWN) {
|
||||
detectedBaud = rareSerialSpeeds[speedSelect];
|
||||
} else if (currentStep == 0 && ++speedSelect == array_count(rareSerialSpeeds)) {
|
||||
LOG_WARN("Give up on GPS probe and set to %d", GPS_BAUDRATE);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -529,6 +809,7 @@ bool GPS::setup()
|
||||
|
||||
if (gnssModel != GNSS_MODEL_UNKNOWN) {
|
||||
setConnected();
|
||||
(void)saveProbeCache();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -1102,6 +1383,12 @@ int32_t GPS::runOnce()
|
||||
if (!setup())
|
||||
return currentDelay; // Setup failed, re-run in two seconds
|
||||
|
||||
if (cachedProbeFailedThisBoot || gnssModel == GNSS_MODEL_UNKNOWN) {
|
||||
LOG_WARN("GPS not detected at cached settings; marked not present "
|
||||
"for this boot");
|
||||
return disable();
|
||||
}
|
||||
|
||||
// We have now loaded our saved preferences from flash
|
||||
if (config.position.gps_mode != meshtastic_Config_PositionConfig_GpsMode_ENABLED) {
|
||||
return disable();
|
||||
|
||||
@@ -155,8 +155,19 @@ class GPS : private concurrency::OSThread
|
||||
* @return true if we've acquired a new location
|
||||
*/
|
||||
virtual bool lookForLocation();
|
||||
// Load persisted GPS model+baud from /prefs.
|
||||
bool loadProbeCache();
|
||||
// Clear persisted GPS model+baud cache.
|
||||
void clearProbeCache();
|
||||
// Persist the currently detected GPS model+baud.
|
||||
bool saveProbeCache() const;
|
||||
// Verify the cached model+baud still maps to a live GPS device.
|
||||
bool verifyCachedProbePresence();
|
||||
|
||||
GnssModel_t gnssModel = GNSS_MODEL_UNKNOWN;
|
||||
int32_t detectedBaud = GPS_BAUDRATE;
|
||||
int32_t cachedProbeBaud = 0;
|
||||
GnssModel_t cachedProbeModel = GNSS_MODEL_UNKNOWN;
|
||||
|
||||
TinyGPSPlus reader;
|
||||
uint8_t fixQual = 0; // fix quality from GPGGA
|
||||
@@ -178,6 +189,12 @@ class GPS : private concurrency::OSThread
|
||||
|
||||
uint8_t speedSelect = 0;
|
||||
uint8_t probeTries = 0;
|
||||
// Cache file is successfully loaded.
|
||||
bool hasProbeCache = false;
|
||||
// Ensures cached probe is attempted once per boot.
|
||||
bool triedProbeCache = false;
|
||||
// Latched when cached presence check fails
|
||||
bool cachedProbeFailedThisBoot = false;
|
||||
|
||||
/**
|
||||
* hasValidLocation - indicates that the position variables contain a complete
|
||||
|
||||
@@ -271,7 +271,7 @@ void Screen::showNodePicker(const char *message, uint32_t durationMs, std::funct
|
||||
}
|
||||
|
||||
// Called to trigger a banner with custom message and duration
|
||||
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits,
|
||||
void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
|
||||
std::function<void(uint32_t)> bannerCallback)
|
||||
{
|
||||
#ifdef USE_EINK
|
||||
@@ -284,7 +284,10 @@ void Screen::showNumberPicker(const char *message, uint32_t durationMs, uint8_t
|
||||
NotificationRenderer::alertBannerCallback = bannerCallback;
|
||||
NotificationRenderer::pauseBanner = false;
|
||||
NotificationRenderer::curSelected = 0;
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
|
||||
if (useBase16)
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::hex_picker;
|
||||
else
|
||||
NotificationRenderer::current_notification_type = notificationTypeEnum::number_picker;
|
||||
NotificationRenderer::numDigits = digits;
|
||||
NotificationRenderer::currentNumber = 0;
|
||||
|
||||
|
||||
@@ -311,7 +311,8 @@ class Screen : public concurrency::OSThread
|
||||
void showOverlayBanner(BannerOverlayOptions);
|
||||
|
||||
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(uint32_t)> bannerCallback);
|
||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, std::function<void(uint32_t)> bannerCallback);
|
||||
void showNumberPicker(const char *message, uint32_t durationMs, uint8_t digits, bool useBase16,
|
||||
std::function<void(uint32_t)> bannerCallback);
|
||||
void showTextInput(const char *header, const char *initialText, uint32_t durationMs,
|
||||
std::function<void(const std::string &)> textCallback);
|
||||
|
||||
|
||||
@@ -578,7 +578,11 @@ void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
#endif
|
||||
|
||||
display->setColor(BLACK);
|
||||
#if GRAPHICS_TFT_COLORING_ENABLED
|
||||
display->fillRect(0, footerY, SCREEN_WIDTH, footerH);
|
||||
#else
|
||||
display->fillRect(0, footerY, connection_icon_width + 1, footerH);
|
||||
#endif
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||
|
||||
@@ -126,6 +126,7 @@ void launchReplyForMessage(const StoredMessage &message, bool freetext)
|
||||
|
||||
menuHandler::screenMenus menuHandler::menuQueue = MenuNone;
|
||||
uint32_t menuHandler::pickedNodeNum = 0;
|
||||
meshtastic_Config_LoRaConfig_RegionCode menuHandler::pendingRegion = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
||||
bool test_enabled = false;
|
||||
uint8_t test_count = 0;
|
||||
|
||||
@@ -174,6 +175,36 @@ void menuHandler::OnboardMessage()
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
static void applyLoraRegion(meshtastic_Config_LoRaConfig_RegionCode region, bool isHam)
|
||||
{
|
||||
config.lora.region = region;
|
||||
config.lora.channel_num = 0; // Reset to default channel
|
||||
|
||||
if (isHam && adminModule) {
|
||||
meshtastic_HamParameters hamParams = meshtastic_HamParameters_init_zero;
|
||||
strncpy(hamParams.call_sign, "N0CALL", sizeof(hamParams.call_sign) - 1);
|
||||
strncpy(hamParams.short_name, "N0CL", sizeof(hamParams.short_name));
|
||||
hamParams.tx_power = config.lora.tx_power;
|
||||
hamParams.frequency = config.lora.override_frequency;
|
||||
adminModule->handleSetHamMode(hamParams);
|
||||
}
|
||||
auto changes = SEGMENT_CONFIG;
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
||||
if (crypto) {
|
||||
crypto->ensurePkiKeys(config.security, owner);
|
||||
}
|
||||
#endif
|
||||
initRegion();
|
||||
if (getEffectiveDutyCycle() < 100) {
|
||||
config.lora.ignore_mqtt = true;
|
||||
}
|
||||
if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) {
|
||||
sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name);
|
||||
changes |= SEGMENT_MODULECONFIG;
|
||||
}
|
||||
service->reloadConfig(changes);
|
||||
}
|
||||
|
||||
void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
{
|
||||
static const LoraRegionOption regionOptions[] = {
|
||||
@@ -242,27 +273,20 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
return;
|
||||
}
|
||||
|
||||
config.lora.region = selectedRegion;
|
||||
auto changes = SEGMENT_CONFIG;
|
||||
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
||||
if (crypto) {
|
||||
crypto->ensurePkiKeys(config.security, owner);
|
||||
bool hamMode = getRegion(selectedRegion)->profile->licensedOnly;
|
||||
if (hamMode) {
|
||||
LOG_INFO("User chose an amateur radio mode region");
|
||||
pendingRegion = selectedRegion;
|
||||
menuQueue = HamModeConfirm;
|
||||
screen->runNow();
|
||||
} else if (owner.is_licensed) {
|
||||
LOG_INFO("Licensed user chose a non-ham region; prompting to revert licensed mode");
|
||||
pendingRegion = selectedRegion;
|
||||
menuQueue = LicensedToNormalConfirm;
|
||||
screen->runNow();
|
||||
} else {
|
||||
applyLoraRegion(selectedRegion, false);
|
||||
}
|
||||
#endif
|
||||
config.lora.tx_enabled = true;
|
||||
initRegion();
|
||||
if (getEffectiveDutyCycle() < 100) {
|
||||
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
|
||||
}
|
||||
|
||||
if (strncmp(moduleConfig.mqtt.root, default_mqtt_root, strlen(default_mqtt_root)) == 0) {
|
||||
// Default broker is in use, so subscribe to the appropriate MQTT root topic for this region
|
||||
sprintf(moduleConfig.mqtt.root, "%s/%s", default_mqtt_root, myRegion->name);
|
||||
changes |= SEGMENT_MODULECONFIG;
|
||||
}
|
||||
|
||||
service->reloadConfig(changes);
|
||||
});
|
||||
|
||||
bannerOptions.durationMs = duration;
|
||||
@@ -279,6 +303,38 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||
screen->showOverlayBanner(bannerOptions);
|
||||
}
|
||||
|
||||
void menuHandler::hamModeConfirmMenu()
|
||||
{
|
||||
static const char *confirmOptions[] = {"No", "Yes"};
|
||||
BannerOverlayOptions confirmBanner;
|
||||
confirmBanner.message = "I confirm I am a\nlicensed amateur\nradio operator";
|
||||
confirmBanner.optionsArrayPtr = confirmOptions;
|
||||
confirmBanner.optionsCount = 2;
|
||||
confirmBanner.bannerCallback = [](int selected) {
|
||||
if (selected == 1)
|
||||
applyLoraRegion(pendingRegion, true);
|
||||
};
|
||||
screen->showOverlayBanner(confirmBanner);
|
||||
}
|
||||
|
||||
void menuHandler::licensedToNormalConfirmMenu()
|
||||
{
|
||||
static const char *confirmOptions[] = {"Keep licensed", "Revert to Normal"};
|
||||
BannerOverlayOptions confirmBanner;
|
||||
confirmBanner.message = "Revert licensed\nmode? This will\nre-enable encryption.";
|
||||
confirmBanner.optionsArrayPtr = confirmOptions;
|
||||
confirmBanner.optionsCount = 2;
|
||||
confirmBanner.bannerCallback = [](int selected) {
|
||||
if (selected == 1) {
|
||||
owner.is_licensed = false;
|
||||
config.lora.override_duty_cycle = false;
|
||||
service->reloadOwner(false);
|
||||
}
|
||||
applyLoraRegion(pendingRegion, false);
|
||||
};
|
||||
screen->showOverlayBanner(confirmBanner);
|
||||
}
|
||||
|
||||
void menuHandler::deviceRolePicker()
|
||||
{
|
||||
static const char *optionsArray[] = {"Back", "Client", "Client Mute", "Lost and Found", "Tracker"};
|
||||
@@ -2233,8 +2289,10 @@ void menuHandler::testMenu()
|
||||
|
||||
void menuHandler::numberTest()
|
||||
{
|
||||
screen->showNumberPicker("Pick a number\n ", 30000, 4,
|
||||
[](int number_picked) -> void { LOG_WARN("Nodenum: %u", number_picked); });
|
||||
screen->showNumberPicker("Verify Nodenum:\n ", 30000, 8, true, [](int number_picked) -> void {
|
||||
LOG_WARN("Nodenum: 0x%08x", number_picked);
|
||||
keyVerificationModule->sendInitialRequest(number_picked);
|
||||
});
|
||||
}
|
||||
|
||||
void menuHandler::wifiBaseMenu()
|
||||
@@ -2418,8 +2476,7 @@ void menuHandler::keyVerificationFinalPrompt()
|
||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||
options.bannerCallback = [=](int selected) {
|
||||
if (selected == 1) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(keyVerificationModule->getCurrentRemoteNode());
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
keyVerificationModule->commitVerifiedRemoteNode();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(options);
|
||||
@@ -2822,6 +2879,12 @@ void menuHandler::handleMenuSwitch(OLEDDisplay *display)
|
||||
case ThemeMenu:
|
||||
themeMenu();
|
||||
break;
|
||||
case HamModeConfirm:
|
||||
hamModeConfirmMenu();
|
||||
break;
|
||||
case LicensedToNormalConfirm:
|
||||
licensedToNormalConfirmMenu();
|
||||
break;
|
||||
}
|
||||
menuQueue = MenuNone;
|
||||
}
|
||||
|
||||
@@ -55,10 +55,13 @@ class menuHandler
|
||||
FrameToggles,
|
||||
DisplayUnits,
|
||||
MessageBubblesMenu,
|
||||
ThemeMenu
|
||||
ThemeMenu,
|
||||
HamModeConfirm,
|
||||
LicensedToNormalConfirm
|
||||
};
|
||||
static screenMenus menuQueue;
|
||||
static uint32_t pickedNodeNum; // node selected by NodePicker for ManageNodeMenu
|
||||
static meshtastic_Config_LoRaConfig_RegionCode pendingRegion;
|
||||
|
||||
static void OnboardMessage();
|
||||
static void LoraRegionPicker(uint32_t duration = 30000);
|
||||
@@ -111,6 +114,8 @@ class menuHandler
|
||||
static void messageBubblesMenu();
|
||||
static void themeMenu();
|
||||
static void textMessageMenu();
|
||||
static void hamModeConfirmMenu();
|
||||
static void licensedToNormalConfirmMenu();
|
||||
|
||||
private:
|
||||
static void saveUIConfig();
|
||||
|
||||
@@ -1144,11 +1144,16 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
||||
bool origBold = config.display.heading_bold;
|
||||
config.display.heading_bold = false;
|
||||
|
||||
// Display Region and Channel Utilization
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
|
||||
if (!config.lora.tx_enabled) {
|
||||
const char *txdisabled = "Transmit Disabled";
|
||||
display->drawString(x, getTextPositions(display)[line], txdisabled);
|
||||
} else {
|
||||
drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
|
||||
// Display Region and Channel Utilization
|
||||
if (currentResolution == ScreenResolution::UltraLow) {
|
||||
drawNodes(display, x, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
|
||||
} else {
|
||||
drawNodes(display, x + 1, getTextPositions(display)[line] + 2, nodeStatus, -1, false, "online");
|
||||
}
|
||||
}
|
||||
char uptimeStr[32] = "";
|
||||
if (currentResolution != ScreenResolution::UltraLow) {
|
||||
|
||||
@@ -233,6 +233,29 @@ bool CryptoEngine::setDHPublicKey(uint8_t *pubKey)
|
||||
return true;
|
||||
}
|
||||
|
||||
void CryptoEngine::setPendingPublicKey(uint32_t node, const uint8_t *key)
|
||||
{
|
||||
pendingKeyVerificationNode = node;
|
||||
memcpy(pendingKeyVerificationPublicKey, key, 32);
|
||||
hasPendingKeyVerificationKey = true;
|
||||
}
|
||||
|
||||
void CryptoEngine::clearPendingPublicKey()
|
||||
{
|
||||
pendingKeyVerificationNode = 0;
|
||||
memset(pendingKeyVerificationPublicKey, 0, 32);
|
||||
hasPendingKeyVerificationKey = false;
|
||||
}
|
||||
|
||||
bool CryptoEngine::getPendingPublicKey(uint32_t node, meshtastic_NodeInfoLite_public_key_t &out)
|
||||
{
|
||||
if (!hasPendingKeyVerificationKey || node == 0 || node != pendingKeyVerificationNode)
|
||||
return false;
|
||||
out.size = 32;
|
||||
memcpy(out.bytes, pendingKeyVerificationPublicKey, 32);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
concurrency::Lock *cryptLock;
|
||||
|
||||
|
||||
@@ -50,6 +50,15 @@ class CryptoEngine
|
||||
virtual bool setDHPublicKey(uint8_t *publicKey);
|
||||
virtual void hash(uint8_t *bytes, size_t numBytes);
|
||||
|
||||
// Temporary holder for a peer's not-yet-verified public key, learned in-band during an
|
||||
// in-progress key-verification handshake before it is committed to NodeDB. Lets the Router
|
||||
// run the DH handshake to encode/decode the follow-on PKI packet. Single slot is enough:
|
||||
// only one verification runs at a time. Discarded when the handshake ends (resetToIdle).
|
||||
void setPendingPublicKey(uint32_t node, const uint8_t *key);
|
||||
void clearPendingPublicKey();
|
||||
// Fills `out` (size set to 32) and returns true iff a pending key is held for `node`.
|
||||
bool getPendingPublicKey(uint32_t node, meshtastic_NodeInfoLite_public_key_t &out);
|
||||
|
||||
virtual void aesSetKey(const uint8_t *key, size_t key_len);
|
||||
|
||||
virtual void aesEncrypt(uint8_t *in, uint8_t *out);
|
||||
@@ -85,6 +94,9 @@ class CryptoEngine
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
uint8_t shared_key[32] = {0};
|
||||
uint8_t private_key[32] = {0};
|
||||
uint32_t pendingKeyVerificationNode = 0;
|
||||
uint8_t pendingKeyVerificationPublicKey[32] = {0};
|
||||
bool hasPendingKeyVerificationKey = false;
|
||||
#endif
|
||||
/**
|
||||
* Init our 128 bit nonce for a new packet
|
||||
|
||||
+41
-11
@@ -485,14 +485,25 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
bool decrypted = false;
|
||||
ChannelIndex chIndex = 0;
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
// Resolve the sender's public key: prefer the one stored in NodeDB, otherwise fall back to a
|
||||
// not-yet-verified key held during an in-progress key-verification handshake. The latter lets us
|
||||
// DH-decode the follow-on PKI packet before the peer's key has been committed to NodeDB.
|
||||
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
|
||||
bool haveRemoteKey = false;
|
||||
auto *fromNode = nodeDB->getMeshNode(p->from);
|
||||
if (fromNode != nullptr && fromNode->public_key.size > 0) {
|
||||
remotePublic = fromNode->public_key;
|
||||
haveRemoteKey = true;
|
||||
} else if (crypto->getPendingPublicKey(p->from, remotePublic)) {
|
||||
haveRemoteKey = true;
|
||||
}
|
||||
// Attempt PKI decryption first
|
||||
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && nodeDB->getMeshNode(p->from) != nullptr &&
|
||||
nodeDB->getMeshNode(p->from)->public_key.size > 0 && nodeDB->getMeshNode(p->to) != nullptr &&
|
||||
nodeDB->getMeshNode(p->to)->public_key.size > 0 && rawSize > MESHTASTIC_PKC_OVERHEAD) {
|
||||
if (p->channel == 0 && isToUs(p) && p->to > 0 && !isBroadcast(p->to) && haveRemoteKey &&
|
||||
nodeDB->getMeshNode(p->to) != nullptr && nodeDB->getMeshNode(p->to)->public_key.size > 0 &&
|
||||
rawSize > MESHTASTIC_PKC_OVERHEAD) {
|
||||
LOG_DEBUG("Attempt PKI decryption");
|
||||
|
||||
if (crypto->decryptCurve25519(p->from, nodeDB->getMeshNode(p->from)->public_key, p->id, rawSize, p->encrypted.bytes,
|
||||
bytes)) {
|
||||
if (crypto->decryptCurve25519(p->from, remotePublic, p->id, rawSize, p->encrypted.bytes, bytes)) {
|
||||
LOG_INFO("PKI Decryption worked!");
|
||||
|
||||
meshtastic_Data decodedtmp;
|
||||
@@ -503,7 +514,7 @@ DecodeState perhapsDecode(meshtastic_MeshPacket *p)
|
||||
decrypted = true;
|
||||
LOG_INFO("Packet decrypted using PKI!");
|
||||
p->pki_encrypted = true;
|
||||
memcpy(&p->public_key.bytes, nodeDB->getMeshNode(p->from)->public_key.bytes, 32);
|
||||
memcpy(&p->public_key.bytes, remotePublic.bytes, 32);
|
||||
p->public_key.size = 32;
|
||||
p->decoded = decodedtmp;
|
||||
p->which_payload_variant = meshtastic_MeshPacket_decoded_tag; // change type to decoded
|
||||
@@ -677,6 +688,19 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
|
||||
#if !(MESHTASTIC_EXCLUDE_PKI)
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(p->to);
|
||||
// Resolve the destination's public key: prefer NodeDB, otherwise (for a key-verification
|
||||
// follow-on packet that explicitly requested PKI) fall back to the not-yet-verified key held
|
||||
// during an in-progress handshake. This lets us DH-encode the follow-on packet before the
|
||||
// peer's key has been committed to NodeDB.
|
||||
meshtastic_NodeInfoLite_public_key_t destPublic = {0, {0}};
|
||||
bool haveDestKey = false;
|
||||
if (node != nullptr && node->public_key.size == 32) {
|
||||
destPublic = node->public_key;
|
||||
haveDestKey = true;
|
||||
} else if (p->pki_encrypted && p->decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP &&
|
||||
crypto->getPendingPublicKey(p->to, destPublic)) {
|
||||
haveDestKey = true;
|
||||
}
|
||||
// We may want to retool things so we can send a PKC packet when the client specifies a key and nodenum, even if the node
|
||||
// is not in the local nodedb
|
||||
// First, only PKC encrypt packets we are originating
|
||||
@@ -694,23 +718,29 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p)
|
||||
config.security.private_key.size == 32 && !isBroadcast(p->to) &&
|
||||
// Some portnums either make no sense to send with PKC
|
||||
p->decoded.portnum != meshtastic_PortNum_TRACEROUTE_APP && p->decoded.portnum != meshtastic_PortNum_NODEINFO_APP &&
|
||||
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP) {
|
||||
p->decoded.portnum != meshtastic_PortNum_ROUTING_APP && p->decoded.portnum != meshtastic_PortNum_POSITION_APP &&
|
||||
// We allow Key Verification messages to be sent without a known destination key, since the point of those messages is
|
||||
// to exchange keys. The first exchange (no usable key yet) falls through to channel encryption; the follow-on packet
|
||||
// uses the pending key resolved into haveDestKey/destPublic above.
|
||||
// Though possible the first packet each direction should go non-pkc
|
||||
// to handle the case where the remote node has our key, but we don't have theirs.
|
||||
!(p->decoded.portnum == meshtastic_PortNum_KEY_VERIFICATION_APP && !haveDestKey)) {
|
||||
LOG_DEBUG("Use PKI!");
|
||||
if (numbytes + MESHTASTIC_HEADER_LENGTH + MESHTASTIC_PKC_OVERHEAD > MAX_LORA_PAYLOAD_LEN)
|
||||
return meshtastic_Routing_Error_TOO_LARGE;
|
||||
// Check for a known public key for the destination
|
||||
if (node == nullptr || node->public_key.size != 32) {
|
||||
// Check for a usable public key for the destination (NodeDB or a pending key-verification key)
|
||||
if (!haveDestKey) {
|
||||
LOG_WARN("Unknown public key for destination node 0x%08x (portnum %d), refusing to send legacy DM", p->to,
|
||||
p->decoded.portnum);
|
||||
return meshtastic_Routing_Error_PKI_SEND_FAIL_PUBLIC_KEY;
|
||||
}
|
||||
if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) &&
|
||||
if (p->pki_encrypted && !memfll(p->public_key.bytes, 0, 32) && node != nullptr &&
|
||||
memcmp(p->public_key.bytes, node->public_key.bytes, 32) != 0) {
|
||||
LOG_WARN("Client public key differs from requested: 0x%02x, stored key begins 0x%02x", *p->public_key.bytes,
|
||||
*node->public_key.bytes);
|
||||
return meshtastic_Routing_Error_PKI_FAILED;
|
||||
}
|
||||
crypto->encryptCurve25519(p->to, getFrom(p), node->public_key, p->id, numbytes, bytes, p->encrypted.bytes);
|
||||
crypto->encryptCurve25519(p->to, getFrom(p), destPublic, p->id, numbytes, bytes, p->encrypted.bytes);
|
||||
numbytes += MESHTASTIC_PKC_OVERHEAD;
|
||||
p->channel = 0;
|
||||
p->pki_encrypted = true;
|
||||
|
||||
@@ -1463,6 +1463,10 @@ void AdminModule::handleSetHamMode(const meshtastic_HamParameters &p)
|
||||
}
|
||||
channels.onConfigChanged();
|
||||
|
||||
if (strcmp(p.call_sign, "N0CALL") == 0) {
|
||||
config.lora.tx_enabled = false;
|
||||
}
|
||||
|
||||
service->reloadOwner(false);
|
||||
saveChanges(SEGMENT_CONFIG | SEGMENT_NODEDATABASE | SEGMENT_DEVICESTATE | SEGMENT_CHANNELS);
|
||||
}
|
||||
|
||||
@@ -67,7 +67,11 @@ class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Obser
|
||||
private:
|
||||
bool handleSetModuleConfig(const meshtastic_ModuleConfig &c);
|
||||
void handleSetChannel();
|
||||
|
||||
public:
|
||||
void handleSetHamMode(const meshtastic_HamParameters &req);
|
||||
|
||||
private:
|
||||
void handleStoreDeviceUIConfig(const meshtastic_DeviceUIConfig &uicfg);
|
||||
void handleSendInputEvent(const meshtastic_AdminMessage_InputEvent &inputEvent);
|
||||
void reboot(int32_t seconds);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#if !MESHTASTIC_EXCLUDE_PKI
|
||||
#include "KeyVerificationModule.h"
|
||||
#include "CryptoEngine.h"
|
||||
#include "MeshService.h"
|
||||
#include "RTC.h"
|
||||
#include "graphics/draw/MenuHandler.h"
|
||||
#include "main.h"
|
||||
#include "meshUtils.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/NodeInfoModule.h"
|
||||
#include <SHA256.h>
|
||||
|
||||
KeyVerificationModule *keyVerificationModule;
|
||||
@@ -48,9 +50,7 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
|
||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_VERIFY &&
|
||||
request->key_verification.nonce == currentNonce) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (remoteNodePtr)
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
commitVerifiedRemoteNode();
|
||||
resetToIdle();
|
||||
} else if (request->key_verification.message_type == meshtastic_KeyVerificationAdmin_MessageType_DO_NOT_VERIFY) {
|
||||
resetToIdle();
|
||||
@@ -63,9 +63,8 @@ AdminMessageHandleResult KeyVerificationModule::handleAdminMessageForModule(cons
|
||||
bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshtastic_KeyVerification *r)
|
||||
{
|
||||
updateState();
|
||||
if (mp.pki_encrypted == false) {
|
||||
return false;
|
||||
}
|
||||
// Note: pki_encrypted is not required here. The first response (M2) may arrive channel-encrypted in
|
||||
// the bootstrap case; the follow-on hash1 packet (M3) is required to be PKI in its branch below.
|
||||
if (mp.from != currentRemoteNode) { // because the inital connection request is handled in allocReply()
|
||||
return false;
|
||||
}
|
||||
@@ -74,9 +73,14 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
}
|
||||
|
||||
if (currentState == KEY_VERIFICATION_SENDER_HAS_INITIATED && r->nonce == currentNonce && r->hash2.size == 32 &&
|
||||
r->hash1.size == 0) {
|
||||
r->hash1.size == 32) {
|
||||
memcpy(hash2, r->hash2.bytes, 32);
|
||||
IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, [](int number_picked) -> void {
|
||||
// The response carries the responder's public key in hash1. If we don't already hold it, stash it
|
||||
// as a pending key so the Router can PKI-encrypt our follow-on packet (committed to NodeDB on accept).
|
||||
auto *responderNode = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (responderNode == nullptr || responderNode->public_key.size != 32)
|
||||
crypto->setPendingPublicKey(currentRemoteNode, r->hash1.bytes);
|
||||
IF_SCREEN(screen->showNumberPicker("Enter Security Number", 60000, 6, false, [](int number_picked) -> void {
|
||||
keyVerificationModule->processSecurityNumber(number_picked);
|
||||
});)
|
||||
|
||||
@@ -91,9 +95,10 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
service->sendClientNotification(cn);
|
||||
LOG_INFO("Received hash2");
|
||||
currentState = KEY_VERIFICATION_SENDER_AWAITING_NUMBER;
|
||||
return true;
|
||||
return false;
|
||||
|
||||
} else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && r->hash1.size == 32 && r->nonce == currentNonce) {
|
||||
} else if (currentState == KEY_VERIFICATION_RECEIVER_AWAITING_HASH1 && mp.pki_encrypted && r->hash1.size == 32 &&
|
||||
r->nonce == currentNonce) {
|
||||
if (memcmp(hash1, r->hash1.bytes, 32) == 0) {
|
||||
memset(message, 0, sizeof(message));
|
||||
sprintf(message, "Verification: \n");
|
||||
@@ -106,10 +111,9 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
||||
options.notificationType = graphics::notificationTypeEnum::selection_picker;
|
||||
options.bannerCallback =
|
||||
[=](int selected) {
|
||||
LOG_WARN("User selected %d for key verification", selected);
|
||||
if (selected == 1) {
|
||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (remoteNodePtr)
|
||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
keyVerificationModule->commitVerifiedRemoteNode();
|
||||
}
|
||||
};
|
||||
screen->showOverlayBanner(options);)
|
||||
@@ -135,22 +139,29 @@ bool KeyVerificationModule::sendInitialRequest(NodeNum remoteNode)
|
||||
{
|
||||
LOG_DEBUG("keyVerification start");
|
||||
// generate nonce
|
||||
updateState();
|
||||
updateState(false);
|
||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||
IF_SCREEN(graphics::menuHandler::menuQueue = graphics::menuHandler::ThrottleMessage;)
|
||||
return false;
|
||||
}
|
||||
updateState(true);
|
||||
currentNonce = random();
|
||||
currentNonceTimestamp = getTime();
|
||||
currentRemoteNode = remoteNode;
|
||||
meshtastic_KeyVerification KeyVerification = meshtastic_KeyVerification_init_zero;
|
||||
KeyVerification.nonce = currentNonce;
|
||||
KeyVerification.hash2.size = 0;
|
||||
KeyVerification.hash1.size = 0;
|
||||
// Carry our public key in the otherwise-unused hash1 field so a peer that does not yet hold our
|
||||
// key can learn it from this first message (bootstrap / onboarding).
|
||||
KeyVerification.hash1.size = 32;
|
||||
memcpy(KeyVerification.hash1.bytes, owner.public_key.bytes, 32);
|
||||
meshtastic_MeshPacket *p = allocDataProtobuf(KeyVerification);
|
||||
p->to = remoteNode;
|
||||
p->channel = 0;
|
||||
p->pki_encrypted = true;
|
||||
// Only request PKI when we already hold the destination's key. Otherwise this first message goes out
|
||||
// channel-encrypted (the Router falls back) so the peer can bootstrap from the key carried in hash1.
|
||||
auto *remoteNodePtr = nodeDB->getMeshNode(remoteNode);
|
||||
p->pki_encrypted = (remoteNodePtr != nullptr && remoteNodePtr->public_key.size == 32);
|
||||
p->decoded.want_response = true;
|
||||
p->priority = meshtastic_MeshPacket_Priority_HIGH;
|
||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||
@@ -167,9 +178,6 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
if (currentState != KEY_VERIFICATION_IDLE) { // TODO: cooldown period
|
||||
LOG_WARN("Key Verification requested, but already in a request");
|
||||
return nullptr;
|
||||
} else if (!currentRequest->pki_encrypted) {
|
||||
LOG_WARN("Key Verification requested, but not in a PKI packet");
|
||||
return nullptr;
|
||||
}
|
||||
currentState = KEY_VERIFICATION_RECEIVER_AWAITING_HASH1;
|
||||
|
||||
@@ -184,15 +192,35 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
response.nonce = scratch.nonce;
|
||||
currentRemoteNode = req.from;
|
||||
currentNonceTimestamp = getTime();
|
||||
currentSecurityNumber = random(1, 999999);
|
||||
currentSecurityNumber = random(1, 999999); // fixme, use better random
|
||||
|
||||
// generate hash1
|
||||
// Resolve the requester's public key. When the request arrived PKI-encrypted the Router populated
|
||||
// currentRequest->public_key; otherwise (channel-encrypted bootstrap) it rides in the hash1 field.
|
||||
// If we don't already hold the key in NodeDB, stash it as a pending key so the Router can DH-decode
|
||||
// the follow-on PKI packet. The pending key is only committed to NodeDB once verification is accepted.
|
||||
const uint8_t *senderKey = nullptr;
|
||||
if (currentRequest->pki_encrypted && currentRequest->public_key.size == 32) {
|
||||
senderKey = currentRequest->public_key.bytes; // this is bizarre, fixme
|
||||
} else if (scratch.hash1.size == 32) {
|
||||
senderKey = scratch.hash1.bytes;
|
||||
}
|
||||
if (senderKey == nullptr) {
|
||||
LOG_WARN("Key Verification request without a usable public key");
|
||||
resetToIdle();
|
||||
return nullptr;
|
||||
}
|
||||
auto *senderNode = nodeDB->getMeshNode(currentRemoteNode);
|
||||
bool senderKeyInNodeDB = (senderNode != nullptr && senderNode->public_key.size == 32);
|
||||
if (!senderKeyInNodeDB)
|
||||
crypto->setPendingPublicKey(currentRemoteNode, senderKey);
|
||||
|
||||
// generate local hash1
|
||||
hash.reset();
|
||||
hash.update(¤tSecurityNumber, sizeof(currentSecurityNumber));
|
||||
hash.update(¤tNonce, sizeof(currentNonce));
|
||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||
hash.update(&ourNodeNum, sizeof(ourNodeNum));
|
||||
hash.update(currentRequest->public_key.bytes, currentRequest->public_key.size);
|
||||
hash.update(senderKey, 32);
|
||||
hash.update(owner.public_key.bytes, owner.public_key.size);
|
||||
hash.finalize(hash1, 32);
|
||||
|
||||
@@ -201,13 +229,17 @@ meshtastic_MeshPacket *KeyVerificationModule::allocReply()
|
||||
hash.update(¤tNonce, sizeof(currentNonce));
|
||||
hash.update(hash1, 32);
|
||||
hash.finalize(hash2, 32);
|
||||
response.hash1.size = 0;
|
||||
// Carry our public key in hash1 of the response so the requester can bootstrap our key as well.
|
||||
response.hash1.size = 32;
|
||||
memcpy(response.hash1.bytes, owner.public_key.bytes, 32);
|
||||
response.hash2.size = 32;
|
||||
memcpy(response.hash2.bytes, hash2, 32);
|
||||
|
||||
responsePacket = allocDataProtobuf(response);
|
||||
|
||||
responsePacket->pki_encrypted = true;
|
||||
// PKI-encrypt the response only if we already held the requester's key. In the bootstrap case it goes
|
||||
// out channel-encrypted so the requester (who lacks our key) can decode it and read hash1.
|
||||
responsePacket->pki_encrypted = senderKeyInNodeDB;
|
||||
IF_SCREEN(snprintf(message, 25, "Security Number \n%03u %03u", currentSecurityNumber / 1000, currentSecurityNumber % 1000);
|
||||
screen->showSimpleBanner(message, 30000); LOG_WARN("%s", message);)
|
||||
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
||||
@@ -231,11 +263,16 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
NodeNum ourNodeNum = nodeDB->getNodeNum();
|
||||
uint8_t scratch_hash[32] = {0};
|
||||
LOG_WARN("received security number: %u", incomingNumber);
|
||||
meshtastic_NodeInfoLite *remoteNodePtr = nullptr;
|
||||
remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (!remoteNodePtr || !nodeInfoLiteHasUser(remoteNodePtr) || remoteNodePtr->public_key.size != 32) {
|
||||
currentState = KEY_VERIFICATION_IDLE;
|
||||
return; // should we throw an error here?
|
||||
meshtastic_NodeInfoLite *remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||
// Resolve the remote public key: NodeDB if known, otherwise the pending key learned during this
|
||||
// handshake (bootstrap case).
|
||||
meshtastic_NodeInfoLite_public_key_t remotePublic = {0, {0}};
|
||||
if (remoteNodePtr != nullptr && remoteNodePtr->public_key.size == 32) {
|
||||
remotePublic = remoteNodePtr->public_key;
|
||||
} else if (!crypto->getPendingPublicKey(currentRemoteNode, remotePublic)) {
|
||||
LOG_WARN("No public key available for remote node, aborting key verification");
|
||||
resetToIdle();
|
||||
return;
|
||||
}
|
||||
LOG_WARN("hashing ");
|
||||
// calculate hash1
|
||||
@@ -246,7 +283,7 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
hash.update(¤tRemoteNode, sizeof(currentRemoteNode));
|
||||
hash.update(owner.public_key.bytes, owner.public_key.size);
|
||||
|
||||
hash.update(remoteNodePtr->public_key.bytes, remoteNodePtr->public_key.size);
|
||||
hash.update(remotePublic.bytes, 32);
|
||||
hash.finalize(hash1, 32);
|
||||
|
||||
hash.reset();
|
||||
@@ -289,13 +326,13 @@ void KeyVerificationModule::processSecurityNumber(uint32_t incomingNumber)
|
||||
return;
|
||||
}
|
||||
|
||||
void KeyVerificationModule::updateState()
|
||||
void KeyVerificationModule::updateState(bool resetTimer)
|
||||
{
|
||||
if (currentState != KEY_VERIFICATION_IDLE) {
|
||||
// check for the 60 second timeout
|
||||
if (currentNonceTimestamp < getTime() - 60) {
|
||||
resetToIdle();
|
||||
} else {
|
||||
} else if (resetTimer) {
|
||||
currentNonceTimestamp = getTime();
|
||||
}
|
||||
}
|
||||
@@ -310,6 +347,31 @@ void KeyVerificationModule::resetToIdle()
|
||||
currentSecurityNumber = 0;
|
||||
currentRemoteNode = 0;
|
||||
currentState = KEY_VERIFICATION_IDLE;
|
||||
// Discard any not-yet-verified key learned during this handshake; on reject/timeout it is never trusted.
|
||||
crypto->clearPendingPublicKey();
|
||||
}
|
||||
|
||||
void KeyVerificationModule::commitVerifiedRemoteNode()
|
||||
{
|
||||
// The remote node already has a NodeDB entry by this point (packets were exchanged during the
|
||||
// handshake), so getMeshNode is sufficient; bail defensively if it is somehow absent.
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(currentRemoteNode);
|
||||
if (!node) {
|
||||
LOG_WARN("Attempted to commit key, but unknown node");
|
||||
return;
|
||||
}
|
||||
// If we only held the peer's key as a pending (unverified) key during the handshake, commit it to
|
||||
// NodeDB now that the user has confirmed the verification, so future PKI traffic can use it.
|
||||
meshtastic_NodeInfoLite_public_key_t pending = {0, {0}};
|
||||
if (node->public_key.size != 32 && crypto->getPendingPublicKey(currentRemoteNode, pending))
|
||||
node->public_key = pending;
|
||||
node->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||
LOG_WARN("Node %u manually verified with security number %u", currentRemoteNode, currentSecurityNumber);
|
||||
if (nodeInfoModule)
|
||||
nodeInfoModule->sendOurNodeInfo(currentRemoteNode, false, node->channel, true);
|
||||
// todo: initiate save
|
||||
crypto->clearPendingPublicKey();
|
||||
currentState = KEY_VERIFICATION_IDLE;
|
||||
}
|
||||
|
||||
void KeyVerificationModule::generateVerificationCode(char *readableCode)
|
||||
|
||||
@@ -12,6 +12,39 @@ enum KeyVerificationState {
|
||||
KEY_VERIFICATION_RECEIVER_AWAITING_HASH1,
|
||||
};
|
||||
|
||||
// KeyVerification Module overview
|
||||
// This module allows for two useful functions. First, it implements a 2sv process that can manually verify a trustworthy
|
||||
// connection with another node. It specifically verifies that the other node holds the correct private key for its public key, so
|
||||
// it is resistant to MitM attacks. Second, it can be used to bootstrap trust in a new node by carrying the public key in the
|
||||
// initial unencrypted message (in the hash1 field of the KeyVerification protobuf). This allows a user to manually verify a new
|
||||
// node even if they don't have that node in the local nodeDB at all.
|
||||
|
||||
// The handshake process is as follows (NodeA = initiator, NodeB = responder):
|
||||
// 1. NodeA sends a KeyVerification message containing a random nonce and its own public key (in the
|
||||
// hash1 field) to NodeB. Implemented in sendInitialRequest(). It is PKI-encrypted if NodeA already
|
||||
// holds NodeB's key, otherwise channel-encrypted (the bootstrap case).
|
||||
//
|
||||
// 2. NodeB replies (allocReply()) with its own public key (hash1 field) and hash2. NodeB generates a
|
||||
// random 6-digit security number and stashes NodeA's public key (as a pending key if not already in
|
||||
// the nodeDB). It computes hash1 = SHA256(securityNumber, nonce, NodeA_num, NodeB_num, PK_A, PK_B),
|
||||
// then hash2 = SHA256(nonce, hash1). The reply is PKI-encrypted only if NodeB already held NodeA's
|
||||
// key; in the bootstrap case it is channel-encrypted so NodeA can read NodeB's key from hash1.
|
||||
//
|
||||
// 3. NodeA receives the reply (handleReceivedProtobuf()), checks the nonce, stashes NodeB's public key,
|
||||
// and prompts the user to enter the security number. The security number is never sent over the mesh
|
||||
// and must be communicated over a secondary channel. processSecurityNumber() recomputes hash1 from
|
||||
// the entered number and verifies SHA256(nonce, hash1) matches the received hash2. NodeA then sends
|
||||
// its hash1 back to NodeB in a PKI-encrypted KeyVerification message (the follow-on PKI packet) and
|
||||
// shows the KeyVerificationFinalPrompt menu, displaying 8 characters derived from hash1.
|
||||
//
|
||||
// 4. NodeB receives NodeA's hash1 (handleReceivedProtobuf(); required to be PKI-encrypted), checks it
|
||||
// matches the hash1 NodeB generated, and shows the same 8-character code for final confirmation.
|
||||
//
|
||||
// The final on-screen code comparison is the actual manual verification: the user confirms the codes
|
||||
// match on both devices, proving the two nodes agree on the same public keys (no MitM substitution).
|
||||
// PKI-encrypting the follow-on packet additionally proves each node holds the private key for the
|
||||
// agreed public key.
|
||||
|
||||
class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification> //, private concurrency::OSThread //
|
||||
{
|
||||
// CallbackObserver<KeyVerificationModule, const meshtastic::Status *> nodeStatusObserver =
|
||||
@@ -29,6 +62,7 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
|
||||
bool sendInitialRequest(NodeNum remoteNode);
|
||||
void generateVerificationCode(char *); // fills char with the user readable verification code
|
||||
uint32_t getCurrentRemoteNode() { return currentRemoteNode; }
|
||||
void commitVerifiedRemoteNode(); // Commit a pending key to NodeDB and mark the node manually verified
|
||||
|
||||
protected:
|
||||
/* Called to handle a particular incoming message
|
||||
@@ -58,8 +92,8 @@ class KeyVerificationModule : public ProtobufModule<meshtastic_KeyVerification>
|
||||
char message[40] = {0};
|
||||
|
||||
void processSecurityNumber(uint32_t);
|
||||
void updateState(); // check the timeouts and maybe reset the state to idle
|
||||
void resetToIdle(); // Zero out module state
|
||||
void updateState(bool resetTimer = true); // check the timeouts and maybe reset the state to idle
|
||||
void resetToIdle(); // Zero out module state
|
||||
};
|
||||
|
||||
extern KeyVerificationModule *keyVerificationModule;
|
||||
@@ -40,6 +40,7 @@ constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
|
||||
|
||||
BLECharacteristic *fromNumCharacteristic;
|
||||
BLECharacteristic *BatteryCharacteristic;
|
||||
static int lastBatteryLevel = -1; // last value written to 0x2A19, to skip redundant writes/notifies
|
||||
BLECharacteristic *logRadioCharacteristic;
|
||||
BLEServer *bleServer;
|
||||
|
||||
@@ -718,6 +719,8 @@ void NimbleBluetooth::deinit()
|
||||
#endif
|
||||
|
||||
BLEDevice::deinit(true);
|
||||
BatteryCharacteristic = nullptr; // freed by deinit; clear so updateBatteryLevel() won't touch it
|
||||
lastBatteryLevel = -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -856,16 +859,31 @@ void NimbleBluetooth::setupService()
|
||||
BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic)
|
||||
(uint16_t)0x2a19, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY);
|
||||
BatteryCharacteristic->addDescriptor(batteryLevelDescriptor);
|
||||
// Seed an initial 0-100 level so an early read of 0x2A19 returns a valid value.
|
||||
uint8_t initialLevel = (powerStatus && powerStatus->getHasBattery()) ? powerStatus->getBatteryChargePercent() : 0;
|
||||
if (initialLevel > 100)
|
||||
initialLevel = 100;
|
||||
BatteryCharacteristic->setValue(&initialLevel, 1);
|
||||
lastBatteryLevel = initialLevel;
|
||||
batteryService->start();
|
||||
}
|
||||
|
||||
/// Given a level between 0-100, update the BLE attribute
|
||||
void updateBatteryLevel(uint8_t level)
|
||||
{
|
||||
if ((config.bluetooth.enabled == true) && nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||
BatteryCharacteristic->setValue(&level, 1);
|
||||
if (!config.bluetooth.enabled || !BatteryCharacteristic)
|
||||
return;
|
||||
|
||||
if (level > 100) // 0x2A19 must stay within the BAS 0-100 range
|
||||
level = 100;
|
||||
if (level == lastBatteryLevel)
|
||||
return;
|
||||
lastBatteryLevel = level;
|
||||
|
||||
// Cache the value so a READ works without a subscriber; notify only when connected.
|
||||
BatteryCharacteristic->setValue(&level, 1);
|
||||
if (nimbleBluetooth && nimbleBluetooth->isConnected())
|
||||
BatteryCharacteristic->notify();
|
||||
}
|
||||
}
|
||||
|
||||
void NimbleBluetooth::clearBonds()
|
||||
|
||||
@@ -15,8 +15,9 @@ static BLECharacteristic fromRadio = BLECharacteristic(BLEUuid(FROMRADIO_UUID_16
|
||||
static BLECharacteristic toRadio = BLECharacteristic(BLEUuid(TORADIO_UUID_16));
|
||||
static BLECharacteristic logRadio = BLECharacteristic(BLEUuid(LOGRADIO_UUID_16));
|
||||
|
||||
static BLEDis bledis; // DIS (Device Information Service) helper class instance
|
||||
static BLEBas blebas; // BAS (Battery Service) helper class instance
|
||||
static BLEDis bledis; // DIS (Device Information Service) helper class instance
|
||||
static BLEBas blebas; // BAS (Battery Service) helper class instance
|
||||
static int lastBatteryLevel = -1; // last value written to BAS, to skip redundant writes/notifies
|
||||
#ifndef BLE_DFU_SECURE
|
||||
static BLEDfu bledfu; // DFU software update helper service
|
||||
#else
|
||||
@@ -336,6 +337,7 @@ void NRF52Bluetooth::setup()
|
||||
LOG_INFO("Init the Battery Service");
|
||||
blebas.begin();
|
||||
blebas.write(0); // Unknown battery level for now
|
||||
lastBatteryLevel = 0;
|
||||
// Setup the Heart Rate Monitor service using
|
||||
// BLEService and BLECharacteristic classes
|
||||
LOG_INFO("Init the Mesh bluetooth service");
|
||||
@@ -355,6 +357,14 @@ void NRF52Bluetooth::resumeAdvertising()
|
||||
/// Given a level between 0-100, update the BLE attribute
|
||||
void updateBatteryLevel(uint8_t level)
|
||||
{
|
||||
if (!nrf52Bluetooth) // skip until the Battery Service has been begun in setup()
|
||||
return;
|
||||
|
||||
if (level > 100) // BAS battery level must stay within 0-100
|
||||
level = 100;
|
||||
if (level == lastBatteryLevel)
|
||||
return;
|
||||
lastBatteryLevel = level;
|
||||
blebas.write(level);
|
||||
}
|
||||
void NRF52Bluetooth::clearBonds()
|
||||
|
||||
@@ -14,6 +14,7 @@ platform_packages =
|
||||
extra_scripts =
|
||||
${env.extra_scripts}
|
||||
extra_scripts/nrf52_extra.py
|
||||
pre:extra_scripts/nrf52_lto.py
|
||||
|
||||
build_type = release
|
||||
build_flags =
|
||||
@@ -26,6 +27,8 @@ build_flags =
|
||||
-DMESHTASTIC_EXCLUDE_PAXCOUNTER=1
|
||||
-Os
|
||||
-std=gnu++17
|
||||
-flto ; whole-image LTO (~-60KB) on every nrf52840 target; nrf52_lto.py (pre: extra_script) keeps the interrupt handlers out of LTO so they survive
|
||||
-fmerge-all-constants ; fold identical constants image-wide (~0.7KB; same flag stm32 uses)
|
||||
build_unflags =
|
||||
-Ofast
|
||||
-Og
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
[VERSION]
|
||||
major = 2
|
||||
minor = 8
|
||||
build = 0
|
||||
minor = 7
|
||||
build = 26
|
||||
|
||||
Reference in New Issue
Block a user