* Add T-Beam BPF (144-148 Mhz LoRa) * minor correction to fix compiler warnings * Add T-Beam BPF (144-148 Mhz LoRa) * minor correction to fix compiler warnings * Add ITU regions for this device and make GPS work. * Switch pin after defining it as output Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Lora CS is indeed 1, SD Card CS is 10 * Include the back option. * Fix compilation with pioarduino (USB_MODE) * Default ham to narrow_fast * Default PROFILE_HAM to slot 17 This is an appropriate default in the USA but not the EU. The slot override really should follow the region itself, not the regionprofile. * Fix for ITU 2/3 split * Add ITU region options to MenuAction enum * Add HAS_HAM_2M definition to variant headers for 2M support * Re-add PROFILE_HAM regionprofile Accidentally removed in last merge * Trunk fmt * Initial default slots * Switch to TinyFast Still need to flesh out the default channels * Adjust slotOverrides for TinyFast * RadioLib doesn't accept 15.625 kHz Use 15.6 instead * Set RF95 pins for T-Beam Supreme May cause regressions!! * Remove other-variant changes (BPF-only) These have been moved to other PRs * Remove mismatch guarding (we need a more comprehensive approach) * Add comment back * Add template for PA curve * This is a 5-6W radio!! Add TX_GAIN_LORA * Trunk fmt because NomDeTom hates emdashes --------- Co-authored-by: Thomas Göttgens <tgoettgens@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
4405 lines
178 KiB
C++
4405 lines
178 KiB
C++
#include "configuration.h"
|
|
#if !MESHTASTIC_EXCLUDE_GPS
|
|
#include "GPS.h"
|
|
#endif
|
|
#include "../detect/ScanI2C.h"
|
|
#include "Channels.h"
|
|
#include "CryptoEngine.h"
|
|
#include "Default.h"
|
|
#include "FSCommon.h"
|
|
#include "MeshRadio.h"
|
|
#include "MeshService.h"
|
|
#include "MessageStore.h"
|
|
#include "NodeDB.h"
|
|
#include "PacketHistory.h"
|
|
#include "PowerFSM.h"
|
|
#include "RadioInterface.h"
|
|
#include "Router.h"
|
|
#include "SPILock.h"
|
|
#include "SafeFile.h"
|
|
#include "TransmitHistory.h"
|
|
#include "TypeConversions.h"
|
|
#include "error.h"
|
|
#include "gps/RTC.h"
|
|
#include "main.h"
|
|
#include "memory/MemAudit.h"
|
|
#include "mesh-pb-constants.h"
|
|
#include "mesh/generated/meshtastic/deviceonly_legacy.pb.h"
|
|
#include "meshUtils.h"
|
|
#include "modules/NeighborInfoModule.h"
|
|
#include "target_specific.h"
|
|
#if HAS_VARIABLE_HOPS
|
|
#include "modules/HopScalingModule.h"
|
|
#endif
|
|
#if HAS_TRAFFIC_MANAGEMENT
|
|
#include "modules/TrafficManagementModule.h"
|
|
#endif
|
|
#include "xmodem.h"
|
|
#include <ErriezCRC32.h>
|
|
#include <algorithm>
|
|
#include <pb_decode.h>
|
|
#include <pb_encode.h>
|
|
#include <power/PowerHAL.h>
|
|
#include <vector>
|
|
|
|
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
|
#include "security/EncryptedStorage.h"
|
|
#include "security/SecureZero.h"
|
|
#endif
|
|
|
|
#ifdef ARCH_ESP32
|
|
#if HAS_WIFI
|
|
#include "mesh/wifi/WiFiAPClient.h"
|
|
#endif
|
|
#include "SPILock.h"
|
|
#include "modules/StoreForwardModule.h"
|
|
#include <Preferences.h>
|
|
#include <nvs_flash.h>
|
|
#endif
|
|
|
|
#ifdef ARCH_PORTDUINO
|
|
#include "modules/StoreForwardModule.h"
|
|
#include "platform/portduino/PortduinoGlue.h"
|
|
#endif
|
|
|
|
#ifdef ARCH_NRF52
|
|
#include <bluefruit.h>
|
|
#include <utility/bonding.h>
|
|
#endif
|
|
|
|
#ifdef ARCH_RP2040
|
|
#include <hardware/watchdog.h>
|
|
#endif
|
|
|
|
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
|
|
#include <MeshtasticOTA.h>
|
|
#endif
|
|
|
|
NodeDB *nodeDB = nullptr;
|
|
|
|
// we have plenty of ram so statically alloc this tempbuf (for now)
|
|
EXT_RAM_BSS_ATTR meshtastic_DeviceState devicestate;
|
|
meshtastic_MyNodeInfo &myNodeInfo = devicestate.my_node;
|
|
meshtastic_NodeDatabase nodeDatabase;
|
|
meshtastic_LocalConfig config;
|
|
meshtastic_DeviceUIConfig uiconfig{.screen_brightness = 153, .screen_timeout = 30};
|
|
meshtastic_LocalModuleConfig moduleConfig;
|
|
meshtastic_ChannelFile channelFile;
|
|
|
|
#ifdef USERPREFS_USE_ADMIN_KEY_0
|
|
static unsigned char userprefs_admin_key_0[] = USERPREFS_USE_ADMIN_KEY_0;
|
|
#endif
|
|
#ifdef USERPREFS_USE_ADMIN_KEY_1
|
|
static unsigned char userprefs_admin_key_1[] = USERPREFS_USE_ADMIN_KEY_1;
|
|
#endif
|
|
#ifdef USERPREFS_USE_ADMIN_KEY_2
|
|
static unsigned char userprefs_admin_key_2[] = USERPREFS_USE_ADMIN_KEY_2;
|
|
#endif
|
|
|
|
// Weak empty variant initialization function.
|
|
// May be redefined by variant files.
|
|
void variantDefaultConfig() __attribute__((weak));
|
|
void variantDefaultConfig() {}
|
|
|
|
void variantDefaultModuleConfig() __attribute__((weak));
|
|
void variantDefaultModuleConfig() {}
|
|
|
|
#ifdef HELTEC_MESH_NODE_T114
|
|
|
|
uint32_t read8(uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst)
|
|
{
|
|
uint32_t ret = 0;
|
|
uint8_t SDAPIN = mosi;
|
|
pinMode(SDAPIN, INPUT_PULLUP);
|
|
digitalWrite(dc, HIGH);
|
|
for (int i = 0; i < dummy; i++) { // any dummy clocks
|
|
digitalWrite(sck, HIGH);
|
|
delay(1);
|
|
digitalWrite(sck, LOW);
|
|
delay(1);
|
|
}
|
|
for (int i = 0; i < bits; i++) { // read results
|
|
ret <<= 1;
|
|
delay(1);
|
|
if (digitalRead(SDAPIN))
|
|
ret |= 1;
|
|
;
|
|
digitalWrite(sck, HIGH);
|
|
delay(1);
|
|
digitalWrite(sck, LOW);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
void write9(uint8_t val, uint8_t dc_val, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst)
|
|
{
|
|
pinMode(mosi, OUTPUT);
|
|
digitalWrite(dc, dc_val);
|
|
for (int i = 0; i < 8; i++) { // send command
|
|
digitalWrite(mosi, (val & 0x80) != 0);
|
|
delay(1);
|
|
digitalWrite(sck, HIGH);
|
|
delay(1);
|
|
digitalWrite(sck, LOW);
|
|
val <<= 1;
|
|
}
|
|
}
|
|
|
|
uint32_t readwrite8(uint8_t cmd, uint8_t bits, uint8_t dummy, uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst)
|
|
{
|
|
digitalWrite(cs, LOW);
|
|
write9(cmd, 0, cs, sck, mosi, dc, rst);
|
|
uint32_t ret = read8(bits, dummy, cs, sck, mosi, dc, rst);
|
|
digitalWrite(cs, HIGH);
|
|
return ret;
|
|
}
|
|
|
|
uint32_t get_st7789_id(uint8_t cs, uint8_t sck, uint8_t mosi, uint8_t dc, uint8_t rst)
|
|
{
|
|
pinMode(cs, OUTPUT);
|
|
digitalWrite(cs, HIGH);
|
|
pinMode(cs, OUTPUT);
|
|
pinMode(sck, OUTPUT);
|
|
pinMode(mosi, OUTPUT);
|
|
pinMode(dc, OUTPUT);
|
|
pinMode(rst, OUTPUT);
|
|
digitalWrite(rst, LOW); // Hardware Reset
|
|
delay(10);
|
|
digitalWrite(rst, HIGH);
|
|
delay(10);
|
|
|
|
readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst);
|
|
uint32_t ID = readwrite8(0x04, 24, 1, cs, sck, mosi, dc, rst); // ST7789 needs twice
|
|
return ID;
|
|
}
|
|
|
|
#endif
|
|
|
|
// When armed by loadFromDisk, the decode callback writes satellite entries
|
|
// straight into these maps instead of the temp vectors. Nullptr = legacy
|
|
// push_back-to-vector path for backup/restore and other decoders.
|
|
namespace
|
|
{
|
|
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
std::map<NodeNum, meshtastic_PositionLite> *s_decodePositionsTarget = nullptr;
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
std::map<NodeNum, meshtastic_DeviceMetrics> *s_decodeTelemetryTarget = nullptr;
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
std::map<NodeNum, meshtastic_EnvironmentMetrics> *s_decodeEnvironmentTarget = nullptr;
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
std::map<NodeNum, meshtastic_StatusMessage> *s_decodeStatusTarget = nullptr;
|
|
#endif
|
|
} // namespace
|
|
|
|
bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_t *field)
|
|
{
|
|
const auto *iter = reinterpret_cast<const pb_field_iter_t *>(field);
|
|
switch (iter->tag) {
|
|
case meshtastic_NodeDatabase_nodes_tag: {
|
|
if (ostream) {
|
|
const auto *vec = static_cast<const std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
|
|
for (auto item : *vec) {
|
|
item.snr_q4 = (int32_t)(item.snr * 4.0f);
|
|
item.snr = 0.0f;
|
|
if (!pb_encode_tag_for_field(ostream, iter))
|
|
return false;
|
|
if (!pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item))
|
|
return false;
|
|
}
|
|
}
|
|
if (istream && istream->bytes_left) {
|
|
meshtastic_NodeInfoLite node = meshtastic_NodeInfoLite_init_zero;
|
|
auto *vec = static_cast<std::vector<meshtastic_NodeInfoLite> *>(iter->pData);
|
|
if (pb_decode(istream, meshtastic_NodeInfoLite_fields, &node)) {
|
|
if (node.snr_q4)
|
|
node.snr = node.snr_q4 / 4.0f;
|
|
node.snr_q4 = 0;
|
|
vec->push_back(node);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
case meshtastic_NodeDatabase_positions_tag: {
|
|
if (ostream) {
|
|
const auto *vec = static_cast<const std::vector<meshtastic_NodePositionEntry> *>(iter->pData);
|
|
for (auto item : *vec) {
|
|
if (!pb_encode_tag_for_field(ostream, iter))
|
|
return false;
|
|
if (!pb_encode_submessage(ostream, meshtastic_NodePositionEntry_fields, &item))
|
|
return false;
|
|
}
|
|
}
|
|
if (istream && istream->bytes_left) {
|
|
meshtastic_NodePositionEntry entry = meshtastic_NodePositionEntry_init_zero;
|
|
if (pb_decode(istream, meshtastic_NodePositionEntry_fields, &entry)) {
|
|
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
if (s_decodePositionsTarget) {
|
|
if (entry.has_position)
|
|
(*s_decodePositionsTarget)[entry.num] = entry.position;
|
|
return true;
|
|
}
|
|
#endif
|
|
auto *vec = static_cast<std::vector<meshtastic_NodePositionEntry> *>(iter->pData);
|
|
vec->push_back(entry);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
case meshtastic_NodeDatabase_telemetry_tag: {
|
|
if (ostream) {
|
|
const auto *vec = static_cast<const std::vector<meshtastic_NodeTelemetryEntry> *>(iter->pData);
|
|
for (auto item : *vec) {
|
|
if (!pb_encode_tag_for_field(ostream, iter))
|
|
return false;
|
|
if (!pb_encode_submessage(ostream, meshtastic_NodeTelemetryEntry_fields, &item))
|
|
return false;
|
|
}
|
|
}
|
|
if (istream && istream->bytes_left) {
|
|
meshtastic_NodeTelemetryEntry entry = meshtastic_NodeTelemetryEntry_init_zero;
|
|
if (pb_decode(istream, meshtastic_NodeTelemetryEntry_fields, &entry)) {
|
|
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
if (s_decodeTelemetryTarget) {
|
|
if (entry.has_device_metrics)
|
|
(*s_decodeTelemetryTarget)[entry.num] = entry.device_metrics;
|
|
return true;
|
|
}
|
|
#endif
|
|
auto *vec = static_cast<std::vector<meshtastic_NodeTelemetryEntry> *>(iter->pData);
|
|
vec->push_back(entry);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
case meshtastic_NodeDatabase_status_tag: {
|
|
if (ostream) {
|
|
const auto *vec = static_cast<const std::vector<meshtastic_NodeStatusEntry> *>(iter->pData);
|
|
for (auto item : *vec) {
|
|
if (!pb_encode_tag_for_field(ostream, iter))
|
|
return false;
|
|
if (!pb_encode_submessage(ostream, meshtastic_NodeStatusEntry_fields, &item))
|
|
return false;
|
|
}
|
|
}
|
|
if (istream && istream->bytes_left) {
|
|
meshtastic_NodeStatusEntry entry = meshtastic_NodeStatusEntry_init_zero;
|
|
if (pb_decode(istream, meshtastic_NodeStatusEntry_fields, &entry)) {
|
|
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
if (s_decodeStatusTarget) {
|
|
if (entry.has_status)
|
|
(*s_decodeStatusTarget)[entry.num] = entry.status;
|
|
return true;
|
|
}
|
|
#endif
|
|
auto *vec = static_cast<std::vector<meshtastic_NodeStatusEntry> *>(iter->pData);
|
|
vec->push_back(entry);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
case meshtastic_NodeDatabase_environment_tag: {
|
|
if (ostream) {
|
|
const auto *vec = static_cast<const std::vector<meshtastic_NodeEnvironmentEntry> *>(iter->pData);
|
|
for (auto item : *vec) {
|
|
if (!pb_encode_tag_for_field(ostream, iter))
|
|
return false;
|
|
if (!pb_encode_submessage(ostream, meshtastic_NodeEnvironmentEntry_fields, &item))
|
|
return false;
|
|
}
|
|
}
|
|
if (istream && istream->bytes_left) {
|
|
meshtastic_NodeEnvironmentEntry entry = meshtastic_NodeEnvironmentEntry_init_zero;
|
|
if (pb_decode(istream, meshtastic_NodeEnvironmentEntry_fields, &entry)) {
|
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
if (s_decodeEnvironmentTarget) {
|
|
if (entry.has_environment_metrics)
|
|
(*s_decodeEnvironmentTarget)[entry.num] = entry.environment_metrics;
|
|
return true;
|
|
}
|
|
#endif
|
|
auto *vec = static_cast<std::vector<meshtastic_NodeEnvironmentEntry> *>(iter->pData);
|
|
vec->push_back(entry);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void NodeDB::armNodeDatabaseDecodeTargets()
|
|
{
|
|
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
nodePositions.clear();
|
|
s_decodePositionsTarget = &nodePositions;
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
nodeTelemetry.clear();
|
|
s_decodeTelemetryTarget = &nodeTelemetry;
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
nodeEnvironment.clear();
|
|
s_decodeEnvironmentTarget = &nodeEnvironment;
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
nodeStatus.clear();
|
|
s_decodeStatusTarget = &nodeStatus;
|
|
#endif
|
|
}
|
|
|
|
void NodeDB::disarmNodeDatabaseDecodeTargets()
|
|
{
|
|
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
s_decodePositionsTarget = nullptr;
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
s_decodeTelemetryTarget = nullptr;
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
s_decodeEnvironmentTarget = nullptr;
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
s_decodeStatusTarget = nullptr;
|
|
#endif
|
|
}
|
|
|
|
/** The current change # for radio settings. Starts at 0 on boot and any time the radio settings
|
|
* might have changed is incremented. Allows others to detect they might now be on a new channel.
|
|
*/
|
|
uint32_t radioGeneration;
|
|
|
|
// getMacAddr() and getDeviceId() are the per-architecture hooks declared in target_specific.h.
|
|
|
|
/**
|
|
*
|
|
* Normally userids are unique and start with +country code to look like Signal phone numbers.
|
|
* But there are some special ids used when we haven't yet been configured by a user. In that case
|
|
* we use !macaddr (no colons).
|
|
*/
|
|
meshtastic_User &owner = devicestate.owner;
|
|
|
|
// The slim NodeInfoLite header defines the local long_name cap; the wire-facing
|
|
// meshtastic_User stays wider so names from senders built against the older
|
|
// 39-byte limit still decode (nanopb halts on string overflow).
|
|
static_assert(MAX_LONG_NAME_BYTES + 1 == sizeof(meshtastic_NodeInfoLite::long_name),
|
|
"MAX_LONG_NAME_BYTES must match the NodeInfoLite storage width");
|
|
static_assert(sizeof(meshtastic_User::long_name) > MAX_LONG_NAME_BYTES,
|
|
"wire User.long_name must be wider than the local cap so clampLongName stays in bounds");
|
|
|
|
meshtastic_Position localPosition = meshtastic_Position_init_default;
|
|
meshtastic_CriticalErrorCode error_code =
|
|
meshtastic_CriticalErrorCode_NONE; // For the error code, only show values from this boot (discard value from flash)
|
|
uint32_t error_address = 0;
|
|
|
|
static uint8_t ourMacAddr[6];
|
|
|
|
NodeDB::NodeDB()
|
|
{
|
|
LOG_INFO("Init NodeDB");
|
|
loadFromDisk();
|
|
cleanupMeshDB();
|
|
|
|
uint32_t devicestateCRC = crc32Buffer(&devicestate, sizeof(devicestate));
|
|
uint32_t nodeDatabaseCRC = crc32Buffer(&nodeDatabase, sizeof(nodeDatabase));
|
|
uint32_t configCRC = crc32Buffer(&config, sizeof(config));
|
|
uint32_t channelFileCRC = crc32Buffer(&channelFile, sizeof(channelFile));
|
|
|
|
int saveWhat = 0;
|
|
// Re-read the device id from silicon each boot via the per-arch getDeviceId(); clear the
|
|
// disk-loaded value first so a failed/empty derivation leaves it unset rather than stale.
|
|
myNodeInfo.device_id.size = 0;
|
|
memset(myNodeInfo.device_id.bytes, 0, sizeof(myNodeInfo.device_id.bytes));
|
|
if (getDeviceId(myNodeInfo.device_id.bytes)) {
|
|
myNodeInfo.device_id.size = sizeof(myNodeInfo.device_id.bytes);
|
|
}
|
|
|
|
// likewise - we always want the app requirements to come from the running appload
|
|
myNodeInfo.min_app_version = 30200; // format is Mmmss (where M is 1+the numeric major number. i.e. 30200 means 2.2.00
|
|
pickNewNodeNum();
|
|
|
|
// Set our board type so we can share it with others
|
|
owner.hw_model = HW_VENDOR;
|
|
// Ensure user (nodeinfo) role is set to whatever we're configured to
|
|
owner.role = config.device.role;
|
|
// Ensure macaddr is set to our macaddr as it will be copied in our info below
|
|
memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr));
|
|
// Ensure owner.id is always derived from the node number
|
|
snprintf(owner.id, sizeof(owner.id), "!%08x", getNodeNum());
|
|
|
|
if (!config.has_security) {
|
|
config.has_security = true;
|
|
config.security = meshtastic_Config_SecurityConfig_init_default;
|
|
config.security.serial_enabled = config.device.serial_enabled;
|
|
config.security.is_managed = config.device.is_managed;
|
|
}
|
|
|
|
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
|
// Generate crypto keys if needed using consolidated function
|
|
// Set my node num uint32 value to bytes from the public key (if we have one)
|
|
// Generate identity and crypto keys if needed; this will create a new identity if one does not exist.
|
|
// Skip on a degraded boot: the keypair isn't in RAM, so minting one would change our NodeNum.
|
|
if (!configDecodeFailed)
|
|
generateCryptoKeyPair(nullptr);
|
|
#elif !(MESHTASTIC_EXCLUDE_PKI)
|
|
// Calculate Curve25519 public and private keys
|
|
if (config.security.private_key.size == 32 && config.security.public_key.size == 32) {
|
|
owner.public_key.size = config.security.public_key.size;
|
|
memcpy(owner.public_key.bytes, config.security.public_key.bytes, config.security.public_key.size);
|
|
crypto->setDHPrivateKey(config.security.private_key.bytes);
|
|
// Set my node num uint32 value to bytes from the new public key
|
|
myNodeInfo.my_node_num = crc32Buffer(config.security.public_key.bytes, config.security.public_key.size);
|
|
}
|
|
#endif
|
|
// Identity is now established, so run the self-care pass on the store
|
|
// loadFromDisk() deliberately left untrimmed: confirm self, trim/demote only
|
|
// non-self overflow, pin self to index 0, rewrite once if healed.
|
|
nodeDBSelfCare();
|
|
|
|
// If we migrated from legacy during loadFromDisk(), persist the migrated DB
|
|
// only after identity and self-care are established.
|
|
if (migrationSavePending) {
|
|
saveNodeDatabaseToDisk();
|
|
migrationSavePending = false;
|
|
}
|
|
|
|
// If node database has not been saved for the first time, save it now
|
|
#ifdef FSCom
|
|
if (!FSCom.exists(nodeDatabaseFileName)) {
|
|
saveNodeDatabaseToDisk();
|
|
}
|
|
#endif
|
|
|
|
#ifdef ARCH_ESP32
|
|
Preferences preferences;
|
|
preferences.begin("meshtastic", false);
|
|
myNodeInfo.reboot_count = preferences.getUInt("rebootCounter", 0);
|
|
preferences.end();
|
|
LOG_DEBUG("Number of Device Reboots: %d", myNodeInfo.reboot_count);
|
|
#endif
|
|
|
|
// UA_868 is obsolete; migrate to EU_868 before resetRadioConfig() below validates the region.
|
|
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UA_868) {
|
|
LOG_INFO("UA_868 region is obsolete, migrating saved config to EU_868");
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;
|
|
}
|
|
|
|
resetRadioConfig(); // If bogus settings got saved, then fix them
|
|
// nodeDB->LOG_DEBUG("region=%d, NODENUM=0x%x, dbsize=%d", config.lora.region, myNodeInfo.my_node_num, numMeshNodes);
|
|
|
|
// Uncomment below to always enable UDP broadcasts
|
|
// config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST;
|
|
|
|
// If we are setup to broadcast on any default channel slot (with default frequency slot semantics),
|
|
// ensure that the telemetry intervals are coerced to the role-aware minimum value.
|
|
if (channels.hasDefaultChannel()) {
|
|
LOG_DEBUG("Coerce telemetry to role-aware minimum on defaults");
|
|
moduleConfig.telemetry.device_update_interval = Default::getConfiguredOrMinimumValue(
|
|
moduleConfig.telemetry.device_update_interval, min_default_telemetry_interval_secs);
|
|
moduleConfig.telemetry.environment_update_interval = Default::getConfiguredOrMinimumValue(
|
|
moduleConfig.telemetry.environment_update_interval, min_default_telemetry_interval_secs);
|
|
moduleConfig.telemetry.air_quality_interval = Default::getConfiguredOrMinimumValue(
|
|
moduleConfig.telemetry.air_quality_interval, min_default_telemetry_interval_secs);
|
|
moduleConfig.telemetry.power_update_interval = Default::getConfiguredOrMinimumValue(
|
|
moduleConfig.telemetry.power_update_interval, min_default_telemetry_interval_secs);
|
|
moduleConfig.telemetry.health_update_interval = Default::getConfiguredOrMinimumValue(
|
|
moduleConfig.telemetry.health_update_interval, min_default_telemetry_interval_secs);
|
|
}
|
|
// Enforce position broadcast minimums if we would send positions over a default channel
|
|
// Check channels the same way PositionModule::sendOurPosition() does - first channel with position_precision set
|
|
bool positionUsesDefaultChannel = false;
|
|
for (uint8_t i = 0; i < channels.getNumChannels(); i++) {
|
|
if (channels.getByIndex(i).settings.has_module_settings &&
|
|
channels.getByIndex(i).settings.module_settings.position_precision != 0) {
|
|
positionUsesDefaultChannel = channels.isDefaultChannel(i);
|
|
break;
|
|
}
|
|
}
|
|
if (positionUsesDefaultChannel) {
|
|
LOG_DEBUG("Coerce position broadcasts to role-aware minimum and smart broadcast min of 5 minutes on defaults");
|
|
config.position.position_broadcast_secs =
|
|
Default::getConfiguredOrMinimumValue(config.position.position_broadcast_secs, min_default_broadcast_interval_secs);
|
|
config.position.broadcast_smart_minimum_interval_secs = Default::getConfiguredOrMinimumValue(
|
|
config.position.broadcast_smart_minimum_interval_secs, min_default_broadcast_smart_minimum_interval_secs);
|
|
}
|
|
// FIXME: UINT32_MAX intervals overflows Apple clients until they are fully patched
|
|
if (config.device.node_info_broadcast_secs > MAX_INTERVAL)
|
|
config.device.node_info_broadcast_secs = MAX_INTERVAL;
|
|
if (config.position.position_broadcast_secs > MAX_INTERVAL)
|
|
config.position.position_broadcast_secs = MAX_INTERVAL;
|
|
if (config.position.gps_update_interval > MAX_INTERVAL)
|
|
config.position.gps_update_interval = MAX_INTERVAL;
|
|
if (config.position.gps_attempt_time > MAX_INTERVAL)
|
|
config.position.gps_attempt_time = MAX_INTERVAL;
|
|
if (config.position.position_flags > MAX_INTERVAL)
|
|
config.position.position_flags = MAX_INTERVAL;
|
|
if (config.position.rx_gpio > MAX_INTERVAL)
|
|
config.position.rx_gpio = MAX_INTERVAL;
|
|
if (config.position.tx_gpio > MAX_INTERVAL)
|
|
config.position.tx_gpio = MAX_INTERVAL;
|
|
if (config.position.broadcast_smart_minimum_distance > MAX_INTERVAL)
|
|
config.position.broadcast_smart_minimum_distance = MAX_INTERVAL;
|
|
if (config.position.broadcast_smart_minimum_interval_secs > MAX_INTERVAL)
|
|
config.position.broadcast_smart_minimum_interval_secs = MAX_INTERVAL;
|
|
if (config.position.gps_en_gpio > MAX_INTERVAL)
|
|
config.position.gps_en_gpio = MAX_INTERVAL;
|
|
if (moduleConfig.neighbor_info.update_interval > MAX_INTERVAL)
|
|
moduleConfig.neighbor_info.update_interval = MAX_INTERVAL;
|
|
if (moduleConfig.telemetry.device_update_interval > MAX_INTERVAL)
|
|
moduleConfig.telemetry.device_update_interval = MAX_INTERVAL;
|
|
if (moduleConfig.telemetry.environment_update_interval > MAX_INTERVAL)
|
|
moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL;
|
|
if (moduleConfig.telemetry.air_quality_interval > MAX_INTERVAL)
|
|
moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL;
|
|
if (moduleConfig.telemetry.health_update_interval > MAX_INTERVAL)
|
|
moduleConfig.telemetry.health_update_interval = MAX_INTERVAL;
|
|
|
|
if (moduleConfig.mqtt.has_map_report_settings &&
|
|
moduleConfig.mqtt.map_report_settings.publish_interval_secs < default_map_publish_interval_secs) {
|
|
moduleConfig.mqtt.map_report_settings.publish_interval_secs = default_map_publish_interval_secs;
|
|
}
|
|
|
|
// If a fixed position is configured, restore the persisted position into localPosition at boot.
|
|
// This keeps position broadcasts / MQTT map reports working after reboot on GPS-less nodes.
|
|
if (config.position.fixed_position) {
|
|
meshtastic_PositionLite fixedPos;
|
|
if (copyNodePosition(getNodeNum(), fixedPos) && (fixedPos.latitude_i != 0 || fixedPos.longitude_i != 0)) {
|
|
setLocalPosition(TypeConversions::ConvertToPosition(fixedPos));
|
|
LOG_INFO("Restored fixed position to localPosition: lat=%d lon=%d", fixedPos.latitude_i, fixedPos.longitude_i);
|
|
}
|
|
}
|
|
|
|
// Ensure that the neighbor info update interval is coerced to the minimum
|
|
moduleConfig.neighbor_info.update_interval =
|
|
Default::getConfiguredOrMinimumValue(moduleConfig.neighbor_info.update_interval, min_neighbor_info_broadcast_secs);
|
|
|
|
// Don't let licensed users to rebroadcast encrypted packets
|
|
if (owner.is_licensed) {
|
|
config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY;
|
|
}
|
|
|
|
#if !HAS_TFT
|
|
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
|
|
// On a device without MUI, this display mode makes no sense, and will break logic.
|
|
config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT;
|
|
config.bluetooth.enabled = true;
|
|
}
|
|
#endif
|
|
|
|
if (devicestateCRC != crc32Buffer(&devicestate, sizeof(devicestate)))
|
|
saveWhat |= SEGMENT_DEVICESTATE;
|
|
if (nodeDatabaseCRC != crc32Buffer(&nodeDatabase, sizeof(nodeDatabase)))
|
|
saveWhat |= SEGMENT_NODEDATABASE;
|
|
// Don't persist on a degraded boot: it would overwrite the unreadable-but-maybe-transient config file
|
|
// with no-key UNSET defaults. Runtime reconfiguration (admin set) still persists normally.
|
|
if (!configDecodeFailed && configCRC != crc32Buffer(&config, sizeof(config)))
|
|
saveWhat |= SEGMENT_CONFIG;
|
|
if (channelFileCRC != crc32Buffer(&channelFile, sizeof(channelFile)))
|
|
saveWhat |= SEGMENT_CHANNELS;
|
|
|
|
if (config.position.gps_enabled) {
|
|
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED;
|
|
config.position.gps_enabled = 0;
|
|
}
|
|
#ifdef USERPREFS_FIRMWARE_EDITION
|
|
myNodeInfo.firmware_edition = USERPREFS_FIRMWARE_EDITION;
|
|
#endif
|
|
#ifdef USERPREFS_FIXED_GPS
|
|
if (myNodeInfo.reboot_count == 1) { // Check if First boot ever or after Factory Reset.
|
|
meshtastic_Position fixedGPS = meshtastic_Position_init_default;
|
|
#ifdef USERPREFS_FIXED_GPS_LAT
|
|
fixedGPS.latitude_i = (int32_t)(USERPREFS_FIXED_GPS_LAT * 1e7);
|
|
fixedGPS.has_latitude_i = true;
|
|
#endif
|
|
#ifdef USERPREFS_FIXED_GPS_LON
|
|
fixedGPS.longitude_i = (int32_t)(USERPREFS_FIXED_GPS_LON * 1e7);
|
|
fixedGPS.has_longitude_i = true;
|
|
#endif
|
|
#ifdef USERPREFS_FIXED_GPS_ALT
|
|
fixedGPS.altitude = USERPREFS_FIXED_GPS_ALT;
|
|
fixedGPS.has_altitude = true;
|
|
#endif
|
|
#if defined(USERPREFS_FIXED_GPS_LAT) && defined(USERPREFS_FIXED_GPS_LON)
|
|
fixedGPS.location_source = meshtastic_Position_LocSource_LOC_MANUAL;
|
|
config.has_position = true;
|
|
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
{
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
nodePositions[info->num] = TypeConversions::ConvertToPositionLite(fixedGPS);
|
|
}
|
|
#endif
|
|
nodeDB->setLocalPosition(fixedGPS);
|
|
config.position.fixed_position = true;
|
|
#endif
|
|
}
|
|
#endif
|
|
sortMeshDB();
|
|
saveToDisk(saveWhat);
|
|
bootInitializationInProgress = false;
|
|
}
|
|
|
|
/**
|
|
* Most (but not always) of the time we want to treat packets 'from' the local phone (where from == 0), as if they originated on
|
|
* the local node. If from is zero this function returns our node number instead
|
|
*/
|
|
NodeNum getFrom(const meshtastic_MeshPacket *p)
|
|
{
|
|
return (p->from == 0) ? nodeDB->getNodeNum() : p->from;
|
|
}
|
|
|
|
// Returns true if the packet originated from the local node
|
|
bool isFromUs(const meshtastic_MeshPacket *p)
|
|
{
|
|
return p->from == 0 || p->from == nodeDB->getNodeNum();
|
|
}
|
|
|
|
// Returns true if the packet is destined to us
|
|
bool isToUs(const meshtastic_MeshPacket *p)
|
|
{
|
|
return p->to == nodeDB->getNodeNum();
|
|
}
|
|
|
|
bool isBroadcast(uint32_t dest)
|
|
{
|
|
return dest == NODENUM_BROADCAST || dest == NODENUM_BROADCAST_NO_LORA;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
template <typename Map, typename Value> bool copySatelliteEntry(const Map &map, NodeNum n, Value &out)
|
|
{
|
|
auto it = map.find(n);
|
|
if (it == map.end())
|
|
return false;
|
|
out = it->second;
|
|
return true;
|
|
}
|
|
|
|
template <typename Map> std::vector<NodeNum> snapshotSatelliteNodeNums(const Map &map, NodeNum exclude)
|
|
{
|
|
std::vector<NodeNum> result;
|
|
result.reserve(map.size());
|
|
for (const auto &kv : map) {
|
|
if (kv.first != exclude)
|
|
result.push_back(kv.first);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
// 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
|
|
// own node's entry. Caller holds satelliteMutex. Returns false if nothing
|
|
// could be evicted.
|
|
template <typename Map> bool evictStalestSatellite(NodeDB &db, Map &map)
|
|
{
|
|
auto victim = map.end();
|
|
uint32_t victimTs = UINT32_MAX;
|
|
for (auto it = map.begin(); it != map.end(); ++it) {
|
|
if (it->first == db.getNodeNum())
|
|
continue;
|
|
uint32_t ts = db.hotNodeLastHeard(it->first);
|
|
if (ts < victimTs) {
|
|
victimTs = ts;
|
|
victim = it;
|
|
}
|
|
}
|
|
if (victim == map.end())
|
|
return false;
|
|
map.erase(victim);
|
|
return true;
|
|
}
|
|
|
|
// Keep `map` within MAX_SATELLITE_NODES ahead of inserting `incoming` (the
|
|
// tier-1/tier-2 split: only the freshest MAX_SATELLITE_NODES nodes carry
|
|
// satellite payloads). Caller holds satelliteMutex.
|
|
template <typename Map> void evictSatelliteOverCap(NodeDB &db, Map &map, NodeNum incoming)
|
|
{
|
|
if (map.size() < MAX_SATELLITE_NODES || map.count(incoming))
|
|
return;
|
|
evictStalestSatellite(db, map);
|
|
}
|
|
} // namespace
|
|
|
|
void NodeDB::resetRadioConfig(bool is_fresh_install)
|
|
{
|
|
if (is_fresh_install) {
|
|
radioGeneration++;
|
|
}
|
|
|
|
if (channelFile.channels_count != MAX_NUM_CHANNELS) {
|
|
LOG_INFO("Set default channel and radio preferences!");
|
|
|
|
channels.initDefaults();
|
|
}
|
|
|
|
channels.onConfigChanged();
|
|
|
|
// Update the global myRegion
|
|
initRegion();
|
|
}
|
|
|
|
bool NodeDB::factoryReset(bool eraseBleBonds)
|
|
{
|
|
LOG_INFO("Perform factory reset!");
|
|
// first, remove the "/prefs" (this removes most prefs)
|
|
spiLock->lock();
|
|
rmDir("/prefs"); // this uses spilock internally...
|
|
|
|
#ifdef FSCom
|
|
if (FSCom.exists("/static/rangetest.csv") && !FSCom.remove("/static/rangetest.csv")) {
|
|
LOG_ERROR("Could not remove rangetest.csv file");
|
|
}
|
|
#endif
|
|
|
|
spiLock->unlock();
|
|
|
|
// rmDir above nuked the .dat file, but TransmitHistory's in-memory
|
|
// cache auto-flushes every 5 min and would resurrect it.
|
|
if (transmitHistory) {
|
|
transmitHistory->clear();
|
|
}
|
|
#if HAS_SCREEN
|
|
messageStore.clearAllMessages();
|
|
#endif
|
|
|
|
#if WARM_NODE_COUNT > 0
|
|
// On nRF52840 the warm tier lives in raw flash outside /prefs, so rmDir
|
|
// didn't touch it; clear it and persist the empty store.
|
|
warmStore.clear();
|
|
warmStore.saveIfDirty();
|
|
#endif
|
|
#if HAS_TRAFFIC_MANAGEMENT
|
|
// Factory reset forgets everything; TMM's RAM caches must not survive to resurrect
|
|
// identities (the device usually reboots after this, but don't rely on it).
|
|
if (trafficManagementModule)
|
|
trafficManagementModule->purgeAll();
|
|
#endif
|
|
|
|
// second, install default state (this will deal with the duplicate mac address issue)
|
|
installDefaultNodeDatabase();
|
|
installDefaultDeviceState();
|
|
installDefaultConfig(!eraseBleBonds); // Also preserve the private key if we're not erasing BLE bonds
|
|
installDefaultModuleConfig();
|
|
installDefaultChannels();
|
|
// third, write everything to disk
|
|
saveToDisk();
|
|
if (eraseBleBonds) {
|
|
LOG_INFO("Erase BLE bonds");
|
|
#ifdef ARCH_ESP32
|
|
// This will erase what's in NVS including ssl keys, persistent variables and ble pairing
|
|
nvs_flash_erase();
|
|
#endif
|
|
|
|
#ifdef ARCH_NRF52
|
|
LOG_INFO("Clear bluetooth bonds!");
|
|
bond_print_list(BLE_GAP_ROLE_PERIPH);
|
|
bond_print_list(BLE_GAP_ROLE_CENTRAL);
|
|
Bluefruit.Periph.clearBonds();
|
|
Bluefruit.Central.clearBonds();
|
|
#endif
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void NodeDB::installDefaultNodeDatabase()
|
|
{
|
|
LOG_DEBUG("Install default NodeDatabase");
|
|
nodeDatabase.version = DEVICESTATE_CUR_VER;
|
|
nodeDatabase.nodes = std::vector<meshtastic_NodeInfoLite>(MAX_NUM_NODES);
|
|
numMeshNodes = 0;
|
|
meshNodes = &nodeDatabase.nodes;
|
|
concurrency::LockGuard satelliteGuard(&satelliteMutex);
|
|
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
nodePositions.clear();
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
nodeTelemetry.clear();
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
nodeEnvironment.clear();
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
nodeStatus.clear();
|
|
#endif
|
|
}
|
|
|
|
void NodeDB::installDefaultConfig(bool preserveKey = false)
|
|
{
|
|
uint8_t private_key_temp[32];
|
|
bool shouldPreserveKey = preserveKey && config.has_security && config.security.private_key.size == 32;
|
|
if (shouldPreserveKey) {
|
|
memcpy(private_key_temp, config.security.private_key.bytes, config.security.private_key.size);
|
|
}
|
|
LOG_INFO("Install default LocalConfig");
|
|
memset(&config, 0, sizeof(meshtastic_LocalConfig));
|
|
config.version = DEVICESTATE_CUR_VER;
|
|
config.has_device = true;
|
|
config.has_display = true;
|
|
config.has_lora = true;
|
|
config.has_position = true;
|
|
config.has_power = true;
|
|
config.has_network = true;
|
|
config.has_bluetooth = (HAS_BLUETOOTH ? true : false);
|
|
config.has_security = true;
|
|
config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL;
|
|
|
|
config.lora.sx126x_rx_boosted_gain = true;
|
|
config.lora.tx_enabled =
|
|
true; // FIXME: maybe false in the future, and setting region to enable it. (unset region forces it off)
|
|
config.lora.override_duty_cycle = false;
|
|
config.lora.config_ok_to_mqtt = false;
|
|
#if HAS_LORA_FEM
|
|
config.lora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_ENABLED;
|
|
#else
|
|
config.lora.fem_lna_mode = meshtastic_Config_LoRaConfig_FEM_LNA_Mode_NOT_PRESENT;
|
|
#endif
|
|
|
|
#if HAS_TFT // For the devices that support MUI, default to that
|
|
config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
|
|
#endif
|
|
|
|
#if defined(TFT_WIDTH) && defined(TFT_HEIGHT) && (TFT_WIDTH >= 200 || TFT_HEIGHT >= 200)
|
|
config.display.enable_message_bubbles = true;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_CONFIG_DEVICE_ROLE
|
|
// Restrict ROUTER*, LOST AND FOUND roles for security reasons
|
|
if (IS_ONE_OF(USERPREFS_CONFIG_DEVICE_ROLE, meshtastic_Config_DeviceConfig_Role_ROUTER,
|
|
meshtastic_Config_DeviceConfig_Role_ROUTER_LATE, meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND)) {
|
|
LOG_WARN("ROUTER roles are restricted, falling back to CLIENT role");
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
} else {
|
|
config.device.role = USERPREFS_CONFIG_DEVICE_ROLE;
|
|
}
|
|
#else
|
|
config.device.role = meshtastic_Config_DeviceConfig_Role_CLIENT; // Default to client.
|
|
#endif
|
|
|
|
#ifdef USERPREFS_CONFIG_LORA_REGION
|
|
config.lora.region = USERPREFS_CONFIG_LORA_REGION;
|
|
#else
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_LORACONFIG_TX_POWER
|
|
config.lora.tx_power = USERPREFS_LORACONFIG_TX_POWER;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_LORACONFIG_MODEM_PRESET
|
|
config.lora.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET;
|
|
#else
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_LORACONFIG_USE_PRESET
|
|
config.lora.use_preset = USERPREFS_LORACONFIG_USE_PRESET;
|
|
#else
|
|
config.lora.use_preset = true;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_LORACONFIG_BANDWIDTH
|
|
config.lora.bandwidth = USERPREFS_LORACONFIG_BANDWIDTH;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_LORACONFIG_SPREAD_FACTOR
|
|
config.lora.spread_factor = USERPREFS_LORACONFIG_SPREAD_FACTOR;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_LORACONFIG_CODING_RATE
|
|
config.lora.coding_rate = USERPREFS_LORACONFIG_CODING_RATE;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY
|
|
config.lora.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY;
|
|
#endif
|
|
|
|
config.lora.hop_limit = HOP_RELIABLE;
|
|
#ifdef USERPREFS_CONFIG_LORA_IGNORE_MQTT
|
|
config.lora.ignore_mqtt = USERPREFS_CONFIG_LORA_IGNORE_MQTT;
|
|
#else
|
|
config.lora.ignore_mqtt = false;
|
|
#endif
|
|
|
|
// Initialize admin_key_count to zero
|
|
byte numAdminKeys = 0;
|
|
|
|
#ifdef USERPREFS_USE_ADMIN_KEY_0
|
|
// Check if USERPREFS_ADMIN_KEY_0 is non-empty
|
|
if (sizeof(userprefs_admin_key_0) > 0) {
|
|
memcpy(config.security.admin_key[0].bytes, userprefs_admin_key_0, 32);
|
|
config.security.admin_key[0].size = 32;
|
|
numAdminKeys++;
|
|
}
|
|
#endif
|
|
|
|
#ifdef USERPREFS_USE_ADMIN_KEY_1
|
|
// Check if USERPREFS_ADMIN_KEY_1 is non-empty
|
|
if (sizeof(userprefs_admin_key_1) > 0) {
|
|
memcpy(config.security.admin_key[1].bytes, userprefs_admin_key_1, 32);
|
|
config.security.admin_key[1].size = 32;
|
|
numAdminKeys++;
|
|
}
|
|
#endif
|
|
|
|
#ifdef USERPREFS_USE_ADMIN_KEY_2
|
|
// Check if USERPREFS_ADMIN_KEY_2 is non-empty
|
|
if (sizeof(userprefs_admin_key_2) > 0) {
|
|
memcpy(config.security.admin_key[2].bytes, userprefs_admin_key_2, 32);
|
|
config.security.admin_key[2].size = 32;
|
|
numAdminKeys++;
|
|
}
|
|
#endif
|
|
|
|
config.security.admin_key_count = numAdminKeys;
|
|
|
|
// Left at COMPATIBLE when signature checking is compiled out, so we never report a policy
|
|
// nothing enforces (mirrors the set-config guard in AdminModule).
|
|
#if defined(USERPREFS_CONFIG_SECURITY_PACKET_SIGNATURE_POLICY) && !(MESHTASTIC_EXCLUDE_PKI) && !(MESHTASTIC_EXCLUDE_XEDDSA)
|
|
config.security.packet_signature_policy = USERPREFS_CONFIG_SECURITY_PACKET_SIGNATURE_POLICY;
|
|
#endif
|
|
|
|
if (shouldPreserveKey) {
|
|
config.security.private_key.size = 32;
|
|
memcpy(config.security.private_key.bytes, private_key_temp, config.security.private_key.size);
|
|
printBytes("Restored key", config.security.private_key.bytes, config.security.private_key.size);
|
|
} else {
|
|
config.security.private_key.size = 0;
|
|
}
|
|
config.security.public_key.size = 0;
|
|
|
|
#ifdef PIN_GPS_EN
|
|
config.position.gps_en_gpio = PIN_GPS_EN;
|
|
#endif
|
|
|
|
#if defined(USERPREFS_CONFIG_GPS_MODE)
|
|
config.position.gps_mode = USERPREFS_CONFIG_GPS_MODE;
|
|
#elif !HAS_GPS || GPS_DEFAULT_NOT_PRESENT
|
|
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT;
|
|
#elif !defined(GPS_RX_PIN)
|
|
if (config.position.rx_gpio == 0)
|
|
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT;
|
|
else
|
|
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED;
|
|
#else
|
|
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_CONFIG_SMART_POSITION_ENABLED
|
|
config.position.position_broadcast_smart_enabled = USERPREFS_CONFIG_SMART_POSITION_ENABLED;
|
|
#else
|
|
config.position.position_broadcast_smart_enabled = true;
|
|
#endif
|
|
|
|
config.position.broadcast_smart_minimum_distance = 100;
|
|
config.position.broadcast_smart_minimum_interval_secs = default_broadcast_smart_minimum_interval_secs;
|
|
if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER &&
|
|
config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_LATE)
|
|
config.device.node_info_broadcast_secs = default_node_info_broadcast_secs;
|
|
config.security.serial_enabled = true;
|
|
config.security.admin_channel_enabled = false;
|
|
resetRadioConfig(true); // This also triggers NodeInfo/Position requests since we're fresh
|
|
strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32);
|
|
|
|
#if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \
|
|
defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT) || defined(RAK_WISMESH_TAP_V2)) && \
|
|
HAS_TFT
|
|
// switch BT off by default; use TFT programming mode or hotkey to enable
|
|
config.bluetooth.enabled = false;
|
|
#else
|
|
// default to bluetooth capability of platform as default
|
|
config.bluetooth.enabled = true;
|
|
#endif
|
|
|
|
config.bluetooth.fixed_pin = defaultBLEPin;
|
|
|
|
#if defined(USE_EINK) || defined(HAS_SPI_TFT) || defined(USE_SPISSD1306)
|
|
bool hasScreen = true;
|
|
#ifdef HELTEC_MESH_NODE_T114
|
|
uint32_t st7789_id = get_st7789_id(ST7789_NSS, ST7789_SCK, ST7789_SDA, ST7789_RS, ST7789_RESET);
|
|
if (st7789_id == 0xFFFFFF) {
|
|
hasScreen = false;
|
|
}
|
|
#endif // HELTEC_MESH_NODE_T114
|
|
#elif ARCH_PORTDUINO
|
|
bool hasScreen = false;
|
|
if (portduino_config.displayPanel)
|
|
hasScreen = true;
|
|
else
|
|
hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C;
|
|
#elif MESHTASTIC_INCLUDE_NICHE_GRAPHICS // See "src/graphics/niche"
|
|
bool hasScreen = true; // Use random pin for Bluetooth pairing
|
|
#else
|
|
bool hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_FIXED_BLUETOOTH
|
|
config.bluetooth.fixed_pin = USERPREFS_FIXED_BLUETOOTH;
|
|
config.bluetooth.mode = meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN;
|
|
#else
|
|
config.bluetooth.mode = hasScreen ? meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN
|
|
: meshtastic_Config_BluetoothConfig_PairingMode_FIXED_PIN;
|
|
#endif
|
|
|
|
// for backward compat, default position flags are ALT+MSL
|
|
config.position.position_flags =
|
|
(meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE_MSL |
|
|
meshtastic_Config_PositionConfig_PositionFlags_SPEED | meshtastic_Config_PositionConfig_PositionFlags_HEADING |
|
|
meshtastic_Config_PositionConfig_PositionFlags_DOP | meshtastic_Config_PositionConfig_PositionFlags_SATINVIEW);
|
|
|
|
// Set default value for 'Mesh via UDP'
|
|
#if HAS_UDP_MULTICAST
|
|
#ifdef USERPREFS_NETWORK_ENABLED_PROTOCOLS
|
|
config.network.enabled_protocols = USERPREFS_NETWORK_ENABLED_PROTOCOLS;
|
|
#else
|
|
config.network.enabled_protocols = 0;
|
|
#endif // Network enabled protocols
|
|
#endif // UDP Multicast
|
|
|
|
#ifdef USERPREFS_NETWORK_WIFI_ENABLED
|
|
config.network.wifi_enabled = USERPREFS_NETWORK_WIFI_ENABLED;
|
|
#endif
|
|
|
|
#if USE_ETHERNET_DEFAULT
|
|
config.network.eth_enabled = true;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_NETWORK_WIFI_SSID
|
|
strncpy(config.network.wifi_ssid, USERPREFS_NETWORK_WIFI_SSID, sizeof(config.network.wifi_ssid));
|
|
#endif
|
|
|
|
#ifdef USERPREFS_NETWORK_WIFI_PSK
|
|
strncpy(config.network.wifi_psk, USERPREFS_NETWORK_WIFI_PSK, sizeof(config.network.wifi_psk));
|
|
#endif
|
|
|
|
#if defined(USERPREFS_NETWORK_IPV6_ENABLED)
|
|
config.network.ipv6_enabled = USERPREFS_NETWORK_IPV6_ENABLED;
|
|
#else
|
|
config.network.ipv6_enabled = default_network_ipv6_enabled;
|
|
#endif
|
|
|
|
#ifdef DISPLAY_FLIP_SCREEN
|
|
config.display.flip_screen = true;
|
|
#endif
|
|
|
|
#ifdef RAK4630
|
|
config.display.wake_on_tap_or_motion = true;
|
|
#endif
|
|
|
|
#if defined(T_WATCH_S3) || defined(SENSECAP_INDICATOR)
|
|
config.display.screen_on_secs = 30;
|
|
config.display.wake_on_tap_or_motion = true;
|
|
#endif
|
|
|
|
#ifdef COMPASS_ORIENTATION
|
|
config.display.compass_orientation = COMPASS_ORIENTATION;
|
|
#endif
|
|
|
|
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
|
|
if (MeshtasticOTA::isUpdated()) {
|
|
MeshtasticOTA::recoverConfig(&config.network);
|
|
}
|
|
#endif
|
|
|
|
#ifdef USERPREFS_CONFIG_DEVICE_ROLE
|
|
// Apply role-specific defaults when role is set via user preferences
|
|
installRoleDefaults(config.device.role);
|
|
#endif
|
|
|
|
initConfigIntervals();
|
|
variantDefaultConfig();
|
|
variantDefaultModuleConfig();
|
|
}
|
|
|
|
void NodeDB::initConfigIntervals()
|
|
{
|
|
#ifdef USERPREFS_CONFIG_GPS_UPDATE_INTERVAL
|
|
config.position.gps_update_interval = USERPREFS_CONFIG_GPS_UPDATE_INTERVAL;
|
|
#else
|
|
config.position.gps_update_interval = default_gps_update_interval;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL
|
|
config.position.position_broadcast_secs = USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL;
|
|
#else
|
|
config.position.position_broadcast_secs = default_broadcast_interval_secs;
|
|
#endif
|
|
|
|
config.power.ls_secs = default_ls_secs;
|
|
config.power.min_wake_secs = default_min_wake_secs;
|
|
config.power.sds_secs = default_sds_secs;
|
|
config.power.wait_bluetooth_secs = default_wait_bluetooth_secs;
|
|
|
|
config.display.screen_on_secs = default_screen_on_secs;
|
|
|
|
#if defined(USE_POWERSAVE)
|
|
config.power.is_power_saving = true;
|
|
config.display.screen_on_secs = 30;
|
|
config.power.wait_bluetooth_secs = 30;
|
|
#endif
|
|
}
|
|
|
|
// Always-on traffic management defaults. Only booleans are written; every
|
|
// numeric field stays 0 and resolves to its default_traffic_mgmt_* macro at
|
|
// use (e.g. position dedup precision/interval), so fork-wide tuning changes
|
|
// take effect without another migration. Rate limiting and the features that
|
|
// exhaust or reshape relayed traffic (exhaust_hop_*, drop_unknown_enabled,
|
|
// nodeinfo_direct_response) stay opt-in.
|
|
static void installTrafficManagementDefaults(meshtastic_LocalModuleConfig &mc)
|
|
{
|
|
mc.has_traffic_management = true;
|
|
mc.traffic_management = meshtastic_ModuleConfig_TrafficManagementConfig_init_zero;
|
|
#if HAS_TRAFFIC_MANAGEMENT
|
|
// Position dedup ships enabled at the 11-hour default window on all supported targets.
|
|
// STM32WL is excluded at compile time (HAS_TRAFFIC_MANAGEMENT=0 in mesh-pb-constants.h).
|
|
// Set position_min_interval_secs=0 at runtime to disable dedup.
|
|
mc.traffic_management.position_min_interval_secs = default_traffic_mgmt_position_min_interval_secs;
|
|
#endif
|
|
}
|
|
|
|
// --- 2.8 position/telemetry opt-in migration helpers -------------------------------------------------
|
|
// Pure field mutators (no I/O), so the native test suite can exercise them directly. The version gate
|
|
// and saveToDisk live in loadFromDisk() below.
|
|
|
|
void optInDisablePositionSharing(meshtastic_ChannelFile &cf)
|
|
{
|
|
for (pb_size_t i = 0; i < cf.channels_count; i++) {
|
|
// Only flip PUBLIC / default-PSK channels. A channel with a real private key is a deliberate
|
|
// trusted-group setup where the "leak location to strangers" concern doesn't apply, so its
|
|
// configured precision (including full precision) is preserved.
|
|
if (!channelFileUsesPublicKey(cf, (ChannelIndex)i))
|
|
continue;
|
|
cf.channels[i].settings.has_module_settings = true;
|
|
cf.channels[i].settings.module_settings.position_precision = 0;
|
|
}
|
|
}
|
|
|
|
void optInDisableTelemetryBroadcast(meshtastic_LocalModuleConfig &mc)
|
|
{
|
|
// Every mesh-broadcast telemetry enable flag (each gates its module's sendTelemetry() to the mesh).
|
|
mc.telemetry.device_telemetry_enabled = false;
|
|
mc.telemetry.environment_measurement_enabled = false;
|
|
mc.telemetry.air_quality_enabled = false;
|
|
mc.telemetry.power_measurement_enabled = false;
|
|
mc.telemetry.health_measurement_enabled = false;
|
|
// Position leak via the public MQTT map. Leave map_reporting_enabled alone (anonymous presence is
|
|
// still allowed) and strip only the location component.
|
|
mc.mqtt.map_report_settings.should_report_location = false;
|
|
}
|
|
|
|
void NodeDB::installDefaultModuleConfig()
|
|
{
|
|
LOG_INFO("Install default ModuleConfig");
|
|
memset(&moduleConfig, 0, sizeof(meshtastic_ModuleConfig));
|
|
|
|
moduleConfig.version = DEVICESTATE_CUR_VER;
|
|
moduleConfig.has_mqtt = true;
|
|
moduleConfig.has_range_test = true;
|
|
moduleConfig.has_serial = true;
|
|
moduleConfig.has_store_forward = true;
|
|
moduleConfig.has_telemetry = true;
|
|
moduleConfig.has_external_notification = true;
|
|
#if defined(PIN_BUZZER) || defined(PIN_VIBRATION) || defined(LED_NOTIFICATION) || defined(PCA_LED_NOTIFICATION) || \
|
|
defined(NEOPIXEL_STATUS_NOTIFICATION_PIN)
|
|
moduleConfig.external_notification.enabled = true;
|
|
#endif
|
|
|
|
#if defined(PIN_BUZZER)
|
|
moduleConfig.external_notification.output_buzzer = PIN_BUZZER;
|
|
moduleConfig.external_notification.use_pwm = true;
|
|
moduleConfig.external_notification.alert_message_buzzer = true;
|
|
#endif
|
|
|
|
#if defined(PIN_VIBRATION)
|
|
moduleConfig.external_notification.output_vibra = PIN_VIBRATION;
|
|
moduleConfig.external_notification.alert_message_vibra = true;
|
|
moduleConfig.external_notification.output_ms = 500;
|
|
#endif
|
|
|
|
#if defined(LED_NOTIFICATION)
|
|
moduleConfig.external_notification.output = LED_NOTIFICATION;
|
|
moduleConfig.external_notification.active = LED_STATE_ON;
|
|
moduleConfig.external_notification.alert_message = true;
|
|
moduleConfig.external_notification.output_ms = 1000;
|
|
#endif
|
|
|
|
#if defined(PIN_VIBRATION)
|
|
moduleConfig.external_notification.nag_timeout = 2;
|
|
#elif defined(PIN_BUZZER) || defined(LED_NOTIFICATION) || defined(NEOPIXEL_STATUS_NOTIFICATION_PIN)
|
|
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
|
|
#endif
|
|
|
|
#ifdef HAS_I2S
|
|
// Don't worry about the other settings for T-Watch, we'll also use the DRV2056 behavior for notifications
|
|
moduleConfig.external_notification.enabled = true;
|
|
moduleConfig.external_notification.use_i2s_as_buzzer = true;
|
|
moduleConfig.external_notification.alert_message_buzzer = true;
|
|
#if HAS_TFT
|
|
if (moduleConfig.external_notification.nag_timeout == default_ringtone_nag_secs)
|
|
moduleConfig.external_notification.nag_timeout = 0;
|
|
#else
|
|
moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs;
|
|
#endif // HAS_TFT
|
|
#endif // HAS_I2S
|
|
|
|
#ifdef NANO_G2_ULTRA
|
|
moduleConfig.external_notification.enabled = true;
|
|
moduleConfig.external_notification.alert_message = true;
|
|
moduleConfig.external_notification.output_ms = 100;
|
|
moduleConfig.external_notification.active = true;
|
|
#endif // NANO_G2_ULTRA
|
|
|
|
#ifdef T_LORA_PAGER
|
|
moduleConfig.canned_message.updown1_enabled = true;
|
|
moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A;
|
|
moduleConfig.canned_message.inputbroker_pin_b = ROTARY_B;
|
|
moduleConfig.canned_message.inputbroker_pin_press = ROTARY_PRESS;
|
|
moduleConfig.canned_message.inputbroker_event_cw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(28);
|
|
moduleConfig.canned_message.inputbroker_event_ccw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar(29);
|
|
moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT;
|
|
#endif
|
|
|
|
moduleConfig.has_canned_message = true;
|
|
|
|
#if USERPREFS_MQTT_ENABLED && !MESHTASTIC_EXCLUDE_MQTT
|
|
moduleConfig.mqtt.enabled = true;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_MQTT_ADDRESS
|
|
strncpy(moduleConfig.mqtt.address, USERPREFS_MQTT_ADDRESS, sizeof(moduleConfig.mqtt.address));
|
|
#else
|
|
strncpy(moduleConfig.mqtt.address, default_mqtt_address, sizeof(moduleConfig.mqtt.address));
|
|
#endif
|
|
|
|
#ifdef USERPREFS_MQTT_USERNAME
|
|
strncpy(moduleConfig.mqtt.username, USERPREFS_MQTT_USERNAME, sizeof(moduleConfig.mqtt.username));
|
|
#else
|
|
strncpy(moduleConfig.mqtt.username, default_mqtt_username, sizeof(moduleConfig.mqtt.username));
|
|
#endif
|
|
|
|
#ifdef USERPREFS_MQTT_PASSWORD
|
|
strncpy(moduleConfig.mqtt.password, USERPREFS_MQTT_PASSWORD, sizeof(moduleConfig.mqtt.password));
|
|
#else
|
|
strncpy(moduleConfig.mqtt.password, default_mqtt_password, sizeof(moduleConfig.mqtt.password));
|
|
#endif
|
|
|
|
#ifdef USERPREFS_MQTT_ROOT_TOPIC
|
|
strncpy(moduleConfig.mqtt.root, USERPREFS_MQTT_ROOT_TOPIC, sizeof(moduleConfig.mqtt.root));
|
|
#else
|
|
strncpy(moduleConfig.mqtt.root, default_mqtt_root, sizeof(moduleConfig.mqtt.root));
|
|
#endif
|
|
|
|
#ifdef USERPREFS_MQTT_ENCRYPTION_ENABLED
|
|
moduleConfig.mqtt.encryption_enabled = USERPREFS_MQTT_ENCRYPTION_ENABLED;
|
|
#else
|
|
moduleConfig.mqtt.encryption_enabled = default_mqtt_encryption_enabled;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_MQTT_TLS_ENABLED
|
|
moduleConfig.mqtt.tls_enabled = USERPREFS_MQTT_TLS_ENABLED;
|
|
#else
|
|
moduleConfig.mqtt.tls_enabled = default_mqtt_tls_enabled;
|
|
#endif
|
|
|
|
moduleConfig.has_neighbor_info = true;
|
|
moduleConfig.neighbor_info.enabled = false;
|
|
|
|
installTrafficManagementDefaults(moduleConfig);
|
|
|
|
moduleConfig.has_detection_sensor = true;
|
|
moduleConfig.detection_sensor.enabled = false;
|
|
moduleConfig.detection_sensor.detection_trigger_type = meshtastic_ModuleConfig_DetectionSensorConfig_TriggerType_LOGIC_HIGH;
|
|
moduleConfig.detection_sensor.minimum_broadcast_secs = 45;
|
|
|
|
moduleConfig.has_ambient_lighting = true;
|
|
moduleConfig.ambient_lighting.current = 10;
|
|
// Default to a color based on our node number
|
|
moduleConfig.ambient_lighting.red = (myNodeInfo.my_node_num & 0xFF0000) >> 16;
|
|
moduleConfig.ambient_lighting.green = (myNodeInfo.my_node_num & 0x00FF00) >> 8;
|
|
moduleConfig.ambient_lighting.blue = myNodeInfo.my_node_num & 0x0000FF;
|
|
|
|
#if !MESHTASTIC_EXCLUDE_BEACON
|
|
moduleConfig.has_mesh_beacon = true;
|
|
// Default flags: listen on, broadcast off, legacy split on.
|
|
moduleConfig.mesh_beacon.flags = meshtastic_ModuleConfig_MeshBeaconConfig_Flags_FLAG_LISTEN_ENABLED |
|
|
meshtastic_ModuleConfig_MeshBeaconConfig_Flags_FLAG_LEGACY_SPLIT;
|
|
// Set or clear a single beacon flag bit from a USERPREFS boolean.
|
|
#define BEACON_APPLY_FLAG(enabled, flag) \
|
|
do { \
|
|
if (enabled) \
|
|
moduleConfig.mesh_beacon.flags |= (uint32_t)(flag); \
|
|
else \
|
|
moduleConfig.mesh_beacon.flags &= ~(uint32_t)(flag); \
|
|
} while (0)
|
|
#ifdef USERPREFS_MESH_BEACON_LISTEN_ENABLED
|
|
BEACON_APPLY_FLAG(USERPREFS_MESH_BEACON_LISTEN_ENABLED, meshtastic_ModuleConfig_MeshBeaconConfig_Flags_FLAG_LISTEN_ENABLED);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_BROADCAST_ENABLED
|
|
BEACON_APPLY_FLAG(USERPREFS_MESH_BEACON_BROADCAST_ENABLED,
|
|
meshtastic_ModuleConfig_MeshBeaconConfig_Flags_FLAG_BROADCAST_ENABLED);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_MESSAGE
|
|
strncpy(moduleConfig.mesh_beacon.broadcast_message, USERPREFS_MESH_BEACON_MESSAGE,
|
|
sizeof(moduleConfig.mesh_beacon.broadcast_message) - 1);
|
|
moduleConfig.mesh_beacon.broadcast_message[sizeof(moduleConfig.mesh_beacon.broadcast_message) - 1] = '\0';
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_INTERVAL_SECS
|
|
moduleConfig.mesh_beacon.broadcast_interval_secs =
|
|
(USERPREFS_MESH_BEACON_INTERVAL_SECS != 0 &&
|
|
USERPREFS_MESH_BEACON_INTERVAL_SECS < default_mesh_beacon_min_broadcast_interval_secs)
|
|
? default_mesh_beacon_min_broadcast_interval_secs
|
|
: USERPREFS_MESH_BEACON_INTERVAL_SECS;
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_OFFER_PRESET
|
|
moduleConfig.mesh_beacon.has_broadcast_offer_preset = true;
|
|
moduleConfig.mesh_beacon.broadcast_offer_preset = USERPREFS_MESH_BEACON_OFFER_PRESET;
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_OFFER_REGION
|
|
moduleConfig.mesh_beacon.broadcast_offer_region = USERPREFS_MESH_BEACON_OFFER_REGION;
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_OFFER_CHANNEL_NAME
|
|
moduleConfig.mesh_beacon.has_broadcast_offer_channel = true;
|
|
strncpy(moduleConfig.mesh_beacon.broadcast_offer_channel.name, USERPREFS_MESH_BEACON_OFFER_CHANNEL_NAME,
|
|
sizeof(moduleConfig.mesh_beacon.broadcast_offer_channel.name) - 1);
|
|
moduleConfig.mesh_beacon.broadcast_offer_channel.name[sizeof(moduleConfig.mesh_beacon.broadcast_offer_channel.name) - 1] =
|
|
'\0';
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_OFFER_CHANNEL_PSK
|
|
moduleConfig.mesh_beacon.has_broadcast_offer_channel = true;
|
|
static const uint8_t beaconOfferPsk[] = USERPREFS_MESH_BEACON_OFFER_CHANNEL_PSK;
|
|
static_assert(sizeof(beaconOfferPsk) <= sizeof(moduleConfig.mesh_beacon.broadcast_offer_channel.psk.bytes),
|
|
"USERPREFS_MESH_BEACON_OFFER_CHANNEL_PSK exceeds the 32-byte channel PSK buffer");
|
|
memcpy(moduleConfig.mesh_beacon.broadcast_offer_channel.psk.bytes, beaconOfferPsk, sizeof(beaconOfferPsk));
|
|
moduleConfig.mesh_beacon.broadcast_offer_channel.psk.size = sizeof(beaconOfferPsk);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_ON_PRESET
|
|
moduleConfig.mesh_beacon.has_broadcast_on_preset = true;
|
|
moduleConfig.mesh_beacon.broadcast_on_preset = USERPREFS_MESH_BEACON_ON_PRESET;
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_ON_REGION
|
|
moduleConfig.mesh_beacon.broadcast_on_region = USERPREFS_MESH_BEACON_ON_REGION;
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_ON_CHANNEL_NAME
|
|
moduleConfig.mesh_beacon.has_broadcast_on_channel = true;
|
|
strncpy(moduleConfig.mesh_beacon.broadcast_on_channel.name, USERPREFS_MESH_BEACON_ON_CHANNEL_NAME,
|
|
sizeof(moduleConfig.mesh_beacon.broadcast_on_channel.name) - 1);
|
|
moduleConfig.mesh_beacon.broadcast_on_channel.name[sizeof(moduleConfig.mesh_beacon.broadcast_on_channel.name) - 1] = '\0';
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_ON_CHANNEL_PSK
|
|
moduleConfig.mesh_beacon.has_broadcast_on_channel = true;
|
|
static const uint8_t beaconOnPsk[] = USERPREFS_MESH_BEACON_ON_CHANNEL_PSK;
|
|
static_assert(sizeof(beaconOnPsk) <= sizeof(moduleConfig.mesh_beacon.broadcast_on_channel.psk.bytes),
|
|
"USERPREFS_MESH_BEACON_ON_CHANNEL_PSK exceeds the 32-byte channel PSK buffer");
|
|
memcpy(moduleConfig.mesh_beacon.broadcast_on_channel.psk.bytes, beaconOnPsk, sizeof(beaconOnPsk));
|
|
moduleConfig.mesh_beacon.broadcast_on_channel.psk.size = sizeof(beaconOnPsk);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_ON_CHANNEL_NUM
|
|
moduleConfig.mesh_beacon.has_broadcast_on_channel = true;
|
|
moduleConfig.mesh_beacon.broadcast_on_channel.channel_num = USERPREFS_MESH_BEACON_ON_CHANNEL_NUM;
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_LEGACY_SPLIT
|
|
BEACON_APPLY_FLAG(USERPREFS_MESH_BEACON_LEGACY_SPLIT, meshtastic_ModuleConfig_MeshBeaconConfig_Flags_FLAG_LEGACY_SPLIT);
|
|
#endif
|
|
#undef BEACON_APPLY_FLAG
|
|
// Per-preset broadcast targets (up to 4). Each TARGET_<N>_* key bumps broadcast_targets_count as needed.
|
|
#define BEACON_TARGET_PRESET(N, VAL) \
|
|
do { \
|
|
if (moduleConfig.mesh_beacon.broadcast_targets_count < (N) + 1) \
|
|
moduleConfig.mesh_beacon.broadcast_targets_count = (N) + 1; \
|
|
moduleConfig.mesh_beacon.broadcast_targets[(N)].has_preset = true; \
|
|
moduleConfig.mesh_beacon.broadcast_targets[(N)].preset = (VAL); \
|
|
} while (0)
|
|
#define BEACON_TARGET_REGION(N, VAL) \
|
|
do { \
|
|
if (moduleConfig.mesh_beacon.broadcast_targets_count < (N) + 1) \
|
|
moduleConfig.mesh_beacon.broadcast_targets_count = (N) + 1; \
|
|
moduleConfig.mesh_beacon.broadcast_targets[(N)].region = (VAL); \
|
|
} while (0)
|
|
// Target channel is referenced by index into the device's channel table (0..MAX_NUM_CHANNELS-1).
|
|
#define BEACON_TARGET_CH_INDEX(N, VAL) \
|
|
do { \
|
|
if (moduleConfig.mesh_beacon.broadcast_targets_count < (N) + 1) \
|
|
moduleConfig.mesh_beacon.broadcast_targets_count = (N) + 1; \
|
|
moduleConfig.mesh_beacon.broadcast_targets[(N)].has_channel_index = true; \
|
|
moduleConfig.mesh_beacon.broadcast_targets[(N)].channel_index = (VAL); \
|
|
} while (0)
|
|
#ifdef USERPREFS_MESH_BEACON_TARGET_0_PRESET
|
|
BEACON_TARGET_PRESET(0, USERPREFS_MESH_BEACON_TARGET_0_PRESET);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_TARGET_0_REGION
|
|
BEACON_TARGET_REGION(0, USERPREFS_MESH_BEACON_TARGET_0_REGION);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_TARGET_0_CHANNEL_INDEX
|
|
BEACON_TARGET_CH_INDEX(0, USERPREFS_MESH_BEACON_TARGET_0_CHANNEL_INDEX);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_TARGET_1_PRESET
|
|
BEACON_TARGET_PRESET(1, USERPREFS_MESH_BEACON_TARGET_1_PRESET);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_TARGET_1_REGION
|
|
BEACON_TARGET_REGION(1, USERPREFS_MESH_BEACON_TARGET_1_REGION);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_TARGET_1_CHANNEL_INDEX
|
|
BEACON_TARGET_CH_INDEX(1, USERPREFS_MESH_BEACON_TARGET_1_CHANNEL_INDEX);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_TARGET_2_PRESET
|
|
BEACON_TARGET_PRESET(2, USERPREFS_MESH_BEACON_TARGET_2_PRESET);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_TARGET_2_REGION
|
|
BEACON_TARGET_REGION(2, USERPREFS_MESH_BEACON_TARGET_2_REGION);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_TARGET_2_CHANNEL_INDEX
|
|
BEACON_TARGET_CH_INDEX(2, USERPREFS_MESH_BEACON_TARGET_2_CHANNEL_INDEX);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_TARGET_3_PRESET
|
|
BEACON_TARGET_PRESET(3, USERPREFS_MESH_BEACON_TARGET_3_PRESET);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_TARGET_3_REGION
|
|
BEACON_TARGET_REGION(3, USERPREFS_MESH_BEACON_TARGET_3_REGION);
|
|
#endif
|
|
#ifdef USERPREFS_MESH_BEACON_TARGET_3_CHANNEL_INDEX
|
|
BEACON_TARGET_CH_INDEX(3, USERPREFS_MESH_BEACON_TARGET_3_CHANNEL_INDEX);
|
|
#endif
|
|
#undef BEACON_TARGET_PRESET
|
|
#undef BEACON_TARGET_REGION
|
|
#undef BEACON_TARGET_CH_INDEX
|
|
#endif // !MESHTASTIC_EXCLUDE_BEACON
|
|
|
|
initModuleConfigIntervals();
|
|
}
|
|
|
|
void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role)
|
|
{
|
|
if (role == meshtastic_Config_DeviceConfig_Role_ROUTER) {
|
|
initConfigIntervals();
|
|
initModuleConfigIntervals();
|
|
moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs;
|
|
config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY;
|
|
owner.has_is_unmessagable = true;
|
|
owner.is_unmessagable = true;
|
|
} else if (role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) {
|
|
moduleConfig.telemetry.device_update_interval = ONE_DAY;
|
|
owner.has_is_unmessagable = true;
|
|
owner.is_unmessagable = true;
|
|
} else if (role == meshtastic_Config_DeviceConfig_Role_SENSOR) {
|
|
owner.has_is_unmessagable = true;
|
|
owner.is_unmessagable = true;
|
|
moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs;
|
|
moduleConfig.telemetry.environment_measurement_enabled = true;
|
|
moduleConfig.telemetry.environment_update_interval = 300;
|
|
} else if (role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) {
|
|
config.position.position_broadcast_smart_enabled = false;
|
|
config.position.position_broadcast_secs = 300; // Every 5 minutes
|
|
} else if (role == meshtastic_Config_DeviceConfig_Role_TAK) {
|
|
config.device.node_info_broadcast_secs = ONE_DAY;
|
|
config.position.position_broadcast_smart_enabled = false;
|
|
config.position.position_broadcast_secs = ONE_DAY;
|
|
// Remove Altitude MSL from flags since CoTs use HAE (height above ellipsoid)
|
|
config.position.position_flags =
|
|
(meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_SPEED |
|
|
meshtastic_Config_PositionConfig_PositionFlags_HEADING | meshtastic_Config_PositionConfig_PositionFlags_DOP);
|
|
moduleConfig.telemetry.device_update_interval = ONE_DAY;
|
|
} else if (role == meshtastic_Config_DeviceConfig_Role_TRACKER) {
|
|
owner.has_is_unmessagable = true;
|
|
owner.is_unmessagable = true;
|
|
moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs;
|
|
} else if (role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) {
|
|
owner.has_is_unmessagable = true;
|
|
owner.is_unmessagable = true;
|
|
config.device.node_info_broadcast_secs = ONE_DAY;
|
|
config.position.position_broadcast_smart_enabled = true;
|
|
config.position.position_broadcast_secs = 3 * 60; // Every 3 minutes
|
|
config.position.broadcast_smart_minimum_distance = 20;
|
|
config.position.broadcast_smart_minimum_interval_secs = 15;
|
|
// Remove Altitude MSL from flags since CoTs use HAE (height above ellipsoid)
|
|
config.position.position_flags =
|
|
(meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_SPEED |
|
|
meshtastic_Config_PositionConfig_PositionFlags_HEADING | meshtastic_Config_PositionConfig_PositionFlags_DOP);
|
|
moduleConfig.telemetry.device_update_interval = ONE_DAY;
|
|
} else if (role == meshtastic_Config_DeviceConfig_Role_CLIENT_HIDDEN) {
|
|
config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_LOCAL_ONLY;
|
|
config.device.node_info_broadcast_secs = MAX_INTERVAL;
|
|
config.position.position_broadcast_smart_enabled = false;
|
|
config.position.position_broadcast_secs = MAX_INTERVAL;
|
|
moduleConfig.neighbor_info.update_interval = MAX_INTERVAL;
|
|
moduleConfig.telemetry.device_update_interval = MAX_INTERVAL;
|
|
moduleConfig.telemetry.environment_update_interval = MAX_INTERVAL;
|
|
moduleConfig.telemetry.air_quality_interval = MAX_INTERVAL;
|
|
moduleConfig.telemetry.health_update_interval = MAX_INTERVAL;
|
|
}
|
|
}
|
|
|
|
void NodeDB::initModuleConfigIntervals()
|
|
{
|
|
// Zero out telemetry intervals so that they coalesce to defaults in Default.h
|
|
#ifdef USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL
|
|
moduleConfig.telemetry.device_update_interval = USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL;
|
|
#else
|
|
moduleConfig.telemetry.device_update_interval = MAX_INTERVAL;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED
|
|
moduleConfig.telemetry.environment_measurement_enabled = USERPREFS_CONFIG_ENVIRONMENT_MEASUREMENT_ENABLED;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL
|
|
moduleConfig.telemetry.environment_update_interval = USERPREFS_CONFIG_ENV_TELEM_UPDATE_INTERVAL;
|
|
#else
|
|
moduleConfig.telemetry.environment_update_interval = 0;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_CONFIG_ENV_SCREEN_SCREEN_ENABLED
|
|
moduleConfig.telemetry.environment_screen_enabled = USERPREFS_CONFIG_ENV_SCREEN_SCREEN_ENABLED;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_CONFIG_AQ_TELEM_UPDATE_INTERVAL
|
|
moduleConfig.telemetry.air_quality_interval = USERPREFS_CONFIG_AQ_TELEM_UPDATE_INTERVAL;
|
|
#else
|
|
moduleConfig.telemetry.air_quality_interval = 0;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_CONFIG_AQ_MEASUREMENT_ENABLED
|
|
moduleConfig.telemetry.air_quality_enabled = USERPREFS_CONFIG_AQ_MEASUREMENT_ENABLED;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_CONFIG_AQ_SCREEN_ENABLED
|
|
moduleConfig.telemetry.air_quality_screen_enabled = USERPREFS_CONFIG_AQ_SCREEN_ENABLED;
|
|
#endif
|
|
|
|
moduleConfig.telemetry.power_update_interval = 0;
|
|
moduleConfig.telemetry.health_update_interval = 0;
|
|
moduleConfig.neighbor_info.update_interval = 0;
|
|
moduleConfig.paxcounter.paxcounter_update_interval = 0;
|
|
}
|
|
|
|
void NodeDB::installDefaultChannels()
|
|
{
|
|
LOG_INFO("Install default ChannelFile");
|
|
memset(&channelFile, 0, sizeof(meshtastic_ChannelFile));
|
|
channelFile.version = DEVICESTATE_CUR_VER;
|
|
}
|
|
|
|
void NodeDB::resetNodes(bool keepFavorites)
|
|
{
|
|
if (!config.position.fixed_position)
|
|
clearLocalPosition();
|
|
NodeNum ourNum = getNodeNum();
|
|
numMeshNodes = 1;
|
|
if (keepFavorites) {
|
|
LOG_INFO("Clearing node database - preserving favorites");
|
|
for (size_t i = 0; i < meshNodes->size(); i++) {
|
|
meshtastic_NodeInfoLite &node = meshNodes->at(i);
|
|
if (i > 0 && !nodeInfoLiteIsFavorite(&node)) {
|
|
eraseNodeSatellites(node.num);
|
|
node = meshtastic_NodeInfoLite();
|
|
} else {
|
|
numMeshNodes += 1;
|
|
}
|
|
};
|
|
} else {
|
|
LOG_INFO("Clearing node database - removing favorites");
|
|
for (size_t i = 1; i < meshNodes->size(); i++) {
|
|
const NodeNum gone = meshNodes->at(i).num;
|
|
if (gone)
|
|
eraseNodeSatellites(gone);
|
|
}
|
|
std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite());
|
|
}
|
|
(void)ourNum;
|
|
#if WARM_NODE_COUNT > 0
|
|
warmStore.clear(); // warm entries are never favorites; a DB reset clears them too
|
|
#endif
|
|
#if HAS_TRAFFIC_MANAGEMENT
|
|
// A user-initiated DB reset forgets everything; TMM's caches must not resurrect it.
|
|
if (trafficManagementModule)
|
|
trafficManagementModule->purgeAll();
|
|
#endif
|
|
|
|
devicestate.has_rx_waypoint = false;
|
|
saveNodeDatabaseToDisk();
|
|
saveDeviceStateToDisk();
|
|
if (neighborInfoModule && moduleConfig.neighbor_info.enabled)
|
|
neighborInfoModule->resetNeighbors();
|
|
}
|
|
|
|
void NodeDB::removeNodeByNum(NodeNum nodeNum)
|
|
{
|
|
int newPos = 0, removed = 0;
|
|
for (int i = 0; i < numMeshNodes; i++) {
|
|
if (meshNodes->at(i).num != nodeNum)
|
|
meshNodes->at(newPos++) = meshNodes->at(i);
|
|
else
|
|
removed++;
|
|
}
|
|
numMeshNodes -= removed;
|
|
if (removed) {
|
|
// Clear exactly the slots compaction vacated. Sizing this from `removed` (rather than a
|
|
// fixed one) keeps it inside the vector when nothing matched and the store is full.
|
|
const size_t first = numMeshNodes;
|
|
const size_t last = std::min(first + removed, nodeDatabase.nodes.size());
|
|
std::fill(nodeDatabase.nodes.begin() + first, nodeDatabase.nodes.begin() + last, meshtastic_NodeInfoLite());
|
|
}
|
|
// Drop the node's satellite stores and warm-tier copy regardless of which tier it lived in, so
|
|
// an explicit removal fully forgets it.
|
|
eraseNodeSatellites(nodeNum);
|
|
#if WARM_NODE_COUNT > 0
|
|
// Explicit user removal: don't let the warm tier resurrect the node
|
|
warmStore.remove(nodeNum);
|
|
#endif
|
|
#if HAS_TRAFFIC_MANAGEMENT
|
|
// Explicit removal is full removal: the TrafficManagement caches (unified slot +
|
|
// NodeInfo identity cache) must not keep serving or resurrect the node either.
|
|
if (trafficManagementModule)
|
|
trafficManagementModule->purgeNode(nodeNum);
|
|
#endif
|
|
|
|
LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed);
|
|
saveNodeDatabaseToDisk();
|
|
}
|
|
|
|
void NodeDB::clearLocalPosition()
|
|
{
|
|
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
nodePositions.erase(getNodeNum());
|
|
#endif
|
|
setLocalPosition(meshtastic_Position_init_default);
|
|
localPositionUpdatedSinceBoot = false;
|
|
}
|
|
|
|
bool NodeDB::copyNodePosition(NodeNum n, meshtastic_PositionLite &out) const
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
|
(void)n;
|
|
(void)out;
|
|
return false;
|
|
#else
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
return copySatelliteEntry(nodePositions, n, out);
|
|
#endif
|
|
}
|
|
|
|
bool NodeDB::copyNodeTelemetry(NodeNum n, meshtastic_DeviceMetrics &out) const
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
(void)n;
|
|
(void)out;
|
|
return false;
|
|
#else
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
return copySatelliteEntry(nodeTelemetry, n, out);
|
|
#endif
|
|
}
|
|
|
|
bool NodeDB::copyNodeEnvironment(NodeNum n, meshtastic_EnvironmentMetrics &out) const
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
(void)n;
|
|
(void)out;
|
|
return false;
|
|
#else
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
return copySatelliteEntry(nodeEnvironment, n, out);
|
|
#endif
|
|
}
|
|
|
|
bool NodeDB::copyNodeStatus(NodeNum n, meshtastic_StatusMessage &out) const
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_STATUSDB
|
|
(void)n;
|
|
(void)out;
|
|
return false;
|
|
#else
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
return copySatelliteEntry(nodeStatus, n, out);
|
|
#endif
|
|
}
|
|
|
|
std::vector<NodeNum> NodeDB::snapshotPositionNodeNums(NodeNum exclude) const
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
|
(void)exclude;
|
|
return {};
|
|
#else
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
return snapshotSatelliteNodeNums(nodePositions, exclude);
|
|
#endif
|
|
}
|
|
|
|
std::vector<NodeNum> NodeDB::snapshotTelemetryNodeNums(NodeNum exclude) const
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
(void)exclude;
|
|
return {};
|
|
#else
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
return snapshotSatelliteNodeNums(nodeTelemetry, exclude);
|
|
#endif
|
|
}
|
|
|
|
std::vector<NodeNum> NodeDB::snapshotEnvironmentNodeNums(NodeNum exclude) const
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
(void)exclude;
|
|
return {};
|
|
#else
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
return snapshotSatelliteNodeNums(nodeEnvironment, exclude);
|
|
#endif
|
|
}
|
|
|
|
std::vector<NodeNum> NodeDB::snapshotStatusNodeNums(NodeNum exclude) const
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_STATUSDB
|
|
(void)exclude;
|
|
return {};
|
|
#else
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
return snapshotSatelliteNodeNums(nodeStatus, exclude);
|
|
#endif
|
|
}
|
|
|
|
void NodeDB::setNodeStatus(NodeNum n, const meshtastic_StatusMessage &status)
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_STATUSDB
|
|
(void)n;
|
|
(void)status;
|
|
#else
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
evictSatelliteOverCap(*this, nodeStatus, n);
|
|
nodeStatus[n] = status;
|
|
#endif
|
|
}
|
|
|
|
void NodeDB::touchNodePositionTime(NodeNum n, uint32_t time)
|
|
{
|
|
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
|
(void)n;
|
|
(void)time;
|
|
#else
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
evictSatelliteOverCap(*this, nodePositions, n);
|
|
nodePositions[n].time = time;
|
|
#endif
|
|
}
|
|
|
|
void NodeDB::eraseNodeSatellites(NodeNum n)
|
|
{
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
nodePositions.erase(n);
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
nodeTelemetry.erase(n);
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
nodeEnvironment.erase(n);
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
nodeStatus.erase(n);
|
|
#endif
|
|
}
|
|
|
|
bool NodeDB::enforceSatelliteCaps()
|
|
{
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
bool trimmedAny = false;
|
|
auto trim = [this, &trimmedAny](auto &map, const char *name) {
|
|
const size_t before = map.size();
|
|
while (map.size() > MAX_SATELLITE_NODES) {
|
|
if (!evictStalestSatellite(*this, map))
|
|
break;
|
|
}
|
|
if (map.size() != before) {
|
|
trimmedAny = true;
|
|
LOG_MIGRATION("Trimmed %s satellites %u -> %u (cap %d)", name, (unsigned)before, (unsigned)map.size(),
|
|
MAX_SATELLITE_NODES);
|
|
}
|
|
};
|
|
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
trim(nodePositions, "position");
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
trim(nodeTelemetry, "telemetry");
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
trim(nodeEnvironment, "environment");
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
trim(nodeStatus, "status");
|
|
#endif
|
|
|
|
(void)trim; // all four maps may be compiled out
|
|
|
|
// Approximate satellite heap usage: each std::map entry is one rb-tree node,
|
|
// value_type plus ~44 B of node overhead (parent/left/right pointers, color,
|
|
// allocator rounding on 32-bit targets - an estimate, not exact bookkeeping).
|
|
size_t satBytes = 0;
|
|
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
satBytes += nodePositions.size() * (sizeof(decltype(nodePositions)::value_type) + 44);
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
satBytes += nodeTelemetry.size() * (sizeof(decltype(nodeTelemetry)::value_type) + 44);
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
satBytes += nodeEnvironment.size() * (sizeof(decltype(nodeEnvironment)::value_type) + 44);
|
|
#endif
|
|
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
satBytes += nodeStatus.size() * (sizeof(decltype(nodeStatus)::value_type) + 44);
|
|
#endif
|
|
memaudit::set("satmaps", satBytes);
|
|
|
|
return trimmedAny;
|
|
}
|
|
|
|
#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
|
|
// them if they do); otherwise tracker/sensor/tak_tracker are role-protected.
|
|
static uint8_t warmProtectedCategory(const meshtastic_NodeInfoLite &n)
|
|
{
|
|
if (nodeInfoLiteHasXeddsaSigned(&n))
|
|
return static_cast<uint8_t>(WarmProtected::XeddsaSigner);
|
|
if (n.bitfield & (NODEINFO_BITFIELD_IS_FAVORITE_MASK | NODEINFO_BITFIELD_IS_IGNORED_MASK |
|
|
NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK))
|
|
return static_cast<uint8_t>(WarmProtected::Flag);
|
|
if (IS_ONE_OF(n.role, meshtastic_Config_DeviceConfig_Role_TRACKER, meshtastic_Config_DeviceConfig_Role_SENSOR,
|
|
meshtastic_Config_DeviceConfig_Role_TAK_TRACKER))
|
|
return static_cast<uint8_t>(WarmProtected::Role);
|
|
return static_cast<uint8_t>(WarmProtected::None);
|
|
}
|
|
|
|
// The warm tier packs the device role into a 4-bit field (WARM_ROLE_MASK). Fail the build
|
|
// loudly if a new role outgrows it, rather than silently truncating role on eviction.
|
|
static_assert(_meshtastic_Config_DeviceConfig_Role_MAX <= WARM_ROLE_MASK,
|
|
"device role no longer fits the 4-bit warm metadata field");
|
|
#endif // WARM_NODE_COUNT > 0
|
|
|
|
void NodeDB::cleanupMeshDB()
|
|
{
|
|
int newPos = 0, removed = 0;
|
|
for (int i = 0; i < numMeshNodes; i++) {
|
|
meshtastic_NodeInfoLite &n = meshNodes->at(i);
|
|
// Keep ignored (blocked) nodes even without user info: a block set by
|
|
// bare node ID has no NodeInfo and would otherwise be purged here,
|
|
// silently dropping the block.
|
|
if (nodeInfoLiteHasUser(&n) || nodeInfoLiteIsIgnored(&n)) {
|
|
if (n.public_key.size > 0) {
|
|
if (memfll(n.public_key.bytes, 0, n.public_key.size)) {
|
|
n.public_key.size = 0;
|
|
}
|
|
}
|
|
if (newPos != i)
|
|
meshNodes->at(newPos++) = n;
|
|
else
|
|
newPos++;
|
|
} else {
|
|
// No user info - drop this node and its satellites
|
|
const NodeNum gone = n.num;
|
|
if (gone) {
|
|
#if WARM_NODE_COUNT > 0
|
|
// Keep any key we learned (e.g. via a DM before the NodeInfo
|
|
// exchange completed) rather than losing it with the purge.
|
|
if (n.public_key.size == 32)
|
|
warmStore.absorb(gone, n.last_heard, n.public_key.bytes, n.role, warmProtectedCategory(n),
|
|
nodeInfoLiteHasXeddsaSigned(&n));
|
|
#endif
|
|
|
|
eraseNodeSatellites(gone);
|
|
}
|
|
removed++;
|
|
}
|
|
}
|
|
numMeshNodes -= removed;
|
|
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + removed,
|
|
meshtastic_NodeInfoLite());
|
|
LOG_DEBUG("cleanupMeshDB purged %d entries", removed);
|
|
}
|
|
|
|
void NodeDB::installDefaultDeviceState()
|
|
{
|
|
LOG_INFO("Install default DeviceState");
|
|
// memset(&devicestate, 0, sizeof(meshtastic_DeviceState));
|
|
|
|
// init our devicestate with valid flags so protobuf writing/reading will work
|
|
devicestate.has_my_node = true;
|
|
devicestate.has_owner = true;
|
|
devicestate.version = DEVICESTATE_CUR_VER;
|
|
devicestate.receive_queue_count = 0; // Not yet implemented FIXME
|
|
devicestate.has_rx_waypoint = false;
|
|
|
|
generatePacketId(); // FIXME - ugly way to init current_packet_id;
|
|
|
|
// Set default owner name
|
|
pickNewNodeNum(); // based on macaddr now
|
|
#ifdef USERPREFS_CONFIG_OWNER_LONG_NAME
|
|
snprintf(owner.long_name, sizeof(owner.long_name), (const char *)USERPREFS_CONFIG_OWNER_LONG_NAME);
|
|
#else
|
|
snprintf(owner.long_name, sizeof(owner.long_name), "Meshtastic %04x", getNodeNum() & 0x0ffff);
|
|
#endif
|
|
|
|
clampLongName(owner.long_name); // vendor userprefs may exceed the local cap
|
|
|
|
#ifdef USERPREFS_CONFIG_OWNER_SHORT_NAME
|
|
snprintf(owner.short_name, sizeof(owner.short_name), (const char *)USERPREFS_CONFIG_OWNER_SHORT_NAME);
|
|
#else
|
|
snprintf(owner.short_name, sizeof(owner.short_name), "%04x", getNodeNum() & 0x0ffff);
|
|
#endif
|
|
|
|
snprintf(owner.id, sizeof(owner.id), "!%08x", getNodeNum()); // Default node ID now based on nodenum
|
|
memcpy(owner.macaddr, ourMacAddr, sizeof(owner.macaddr));
|
|
owner.has_is_unmessagable = true;
|
|
owner.is_unmessagable = false;
|
|
|
|
#ifdef HAS_HAM_2M_ONLY
|
|
// Ham-band-only hardware defaults to licensed operation. The user can still flip this off later
|
|
// (e.g. a commercial operator on an adjacent allocation who wants to keep encryption on) - we
|
|
// only set the default here, not on every boot.
|
|
owner.is_licensed = true;
|
|
#endif
|
|
}
|
|
|
|
// We reserve a few nodenums for future use
|
|
#define NUM_RESERVED 4
|
|
|
|
/**
|
|
* get our starting (provisional) nodenum from flash.
|
|
*/
|
|
void NodeDB::pickNewNodeNum()
|
|
{
|
|
NodeNum nodeNum = myNodeInfo.my_node_num;
|
|
getMacAddr(ourMacAddr); // Make sure ourMacAddr is set
|
|
if (nodeNum == 0) {
|
|
// Pick an initial nodenum based on the macaddr
|
|
nodeNum = (ourMacAddr[2] << 24) | (ourMacAddr[3] << 16) | (ourMacAddr[4] << 8) | ourMacAddr[5];
|
|
}
|
|
|
|
// Identity check via public key (or "empty slot?" when no keys yet);
|
|
// macaddr no longer lives on the slim header.
|
|
auto isOurOwnEntry = [&](const meshtastic_NodeInfoLite *n) -> bool {
|
|
if (!n)
|
|
return false;
|
|
if (owner.public_key.size == 32 && n->public_key.size == 32)
|
|
return memcmp(n->public_key.bytes, owner.public_key.bytes, 32) == 0;
|
|
return !nodeInfoLiteHasUser(n);
|
|
};
|
|
|
|
meshtastic_NodeInfoLite *found;
|
|
while (((found = getMeshNode(nodeNum)) && !isOurOwnEntry(found)) ||
|
|
(nodeNum == NODENUM_BROADCAST || nodeNum < NUM_RESERVED)) {
|
|
NodeNum candidate = random(NUM_RESERVED, LONG_MAX); // try a new random choice
|
|
if (found)
|
|
LOG_WARN("NOTE! Our desired nodenum 0x%08x is invalid or in use, picking 0x%08x", nodeNum, candidate);
|
|
nodeNum = candidate;
|
|
}
|
|
LOG_DEBUG("Use nodenum 0x%08x ", nodeNum);
|
|
|
|
myNodeInfo.my_node_num = nodeNum;
|
|
}
|
|
|
|
/** Load a protobuf from a file, return LoadFileResult */
|
|
LoadFileResult NodeDB::loadProto(const char *filename, size_t protoSize, size_t objSize, const pb_msgdesc_t *fields,
|
|
void *dest_struct)
|
|
{
|
|
LoadFileResult state = LoadFileResult::OTHER_FAILURE;
|
|
|
|
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
|
// 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[],
|
|
// so it isn't recoverable from the heap after this function returns.
|
|
auto decBuf = meshtastic_security::make_zeroizing_array(protoSize);
|
|
if (!decBuf) {
|
|
LOG_ERROR("OOM decrypting %s", filename);
|
|
return LoadFileResult::OTHER_FAILURE;
|
|
}
|
|
size_t decLen = 0;
|
|
if (EncryptedStorage::readAndDecrypt(filename, decBuf.get(), protoSize, decLen)) {
|
|
LOG_INFO("Load encrypted %s", filename);
|
|
pb_istream_t stream = pb_istream_from_buffer(decBuf.get(), decLen);
|
|
if (fields != &meshtastic_NodeDatabase_msg)
|
|
memset(dest_struct, 0, objSize);
|
|
if (!pb_decode(&stream, fields, dest_struct)) {
|
|
LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
|
|
state = LoadFileResult::DECODE_FAILED;
|
|
storageCorruptThisLoad = true;
|
|
} else {
|
|
LOG_INFO("Loaded encrypted %s successfully", filename);
|
|
state = LoadFileResult::LOAD_SUCCESS;
|
|
}
|
|
} else {
|
|
LOG_ERROR("Decrypt failed for %s, treating as corrupt", filename);
|
|
state = LoadFileResult::DECODE_FAILED;
|
|
storageCorruptThisLoad = true;
|
|
}
|
|
return state;
|
|
}
|
|
#endif
|
|
|
|
#ifdef FSCom
|
|
concurrency::LockGuard g(spiLock);
|
|
|
|
auto f = FSCom.open(filename, FILE_O_READ);
|
|
|
|
if (f) {
|
|
LOG_INFO("Load %s", filename);
|
|
pb_istream_t stream = {&readcb, &f, protoSize};
|
|
if (fields != &meshtastic_NodeDatabase_msg &&
|
|
fields != &meshtastic_NodeDatabase_Legacy_msg) // both NodeDatabase descriptors contain std::vector members
|
|
memset(dest_struct, 0, objSize);
|
|
if (!pb_decode(&stream, fields, dest_struct)) {
|
|
LOG_ERROR("Error: can't decode protobuf %s", PB_GET_ERROR(&stream));
|
|
state = LoadFileResult::DECODE_FAILED;
|
|
} else {
|
|
LOG_INFO("Loaded %s successfully", filename);
|
|
state = LoadFileResult::LOAD_SUCCESS;
|
|
}
|
|
f.close();
|
|
} else {
|
|
LOG_ERROR("Could not open / read %s", filename);
|
|
}
|
|
#else
|
|
LOG_ERROR("ERROR: Filesystem not implemented");
|
|
state = LoadFileResult::NO_FILESYSTEM;
|
|
#endif
|
|
return state;
|
|
}
|
|
|
|
#if WARM_NODE_COUNT > 0
|
|
void NodeDB::demoteOldestHotNodesToWarm()
|
|
{
|
|
const int keep = MAX_NUM_NODES;
|
|
if (numMeshNodes <= keep)
|
|
return;
|
|
|
|
// Protected nodes (favorite/ignored/verified) outrank recency and are demoted
|
|
// only when the store is full of them; within a class, most-recently-heard
|
|
// wins. Index 0 is self and stays put (sort from +1), as in runtime eviction.
|
|
std::sort(meshNodes->begin() + 1, meshNodes->begin() + numMeshNodes,
|
|
[](const meshtastic_NodeInfoLite &a, const meshtastic_NodeInfoLite &b) {
|
|
const bool ka = nodeInfoLiteIsProtected(&a);
|
|
const bool kb = nodeInfoLiteIsProtected(&b);
|
|
if (ka != kb)
|
|
return ka;
|
|
return a.last_heard > b.last_heard;
|
|
});
|
|
|
|
int demoted = 0;
|
|
for (int i = keep; i < numMeshNodes; i++) {
|
|
const meshtastic_NodeInfoLite &n = (*meshNodes)[i];
|
|
if (n.num == 0)
|
|
continue;
|
|
// Keep the public key if we have one (40 B warm record); keyless nodes
|
|
// still get a placeholder so re-admission restores last_heard.
|
|
warmStore.absorb(n.num, n.last_heard, n.public_key.size > 0 ? n.public_key.bytes : nullptr, n.role,
|
|
warmProtectedCategory(n), nodeInfoLiteHasXeddsaSigned(&n));
|
|
// Demotion drops the node from the header table, so drop its satellites
|
|
// too (the eviction chokepoint) - they'd otherwise orphan until the next
|
|
// enforceSatelliteCaps pass.
|
|
eraseNodeSatellites(n.num);
|
|
demoted++;
|
|
}
|
|
numMeshNodes = keep; // the resize() in loadFromDisk reclaims the demoted tail
|
|
LOG_MIGRATION("NodeDB migration: demoted %d node(s) over %d into the warm tier (keepers preferred)", demoted, keep);
|
|
}
|
|
#endif
|
|
|
|
void NodeDB::nodeDBSelfCare()
|
|
{
|
|
if (!meshNodes)
|
|
return;
|
|
|
|
const NodeNum self = getNodeNum();
|
|
const bool nodesOverCap = numMeshNodes > MAX_NUM_NODES;
|
|
|
|
// Confirm self is present and its key matches what we just (re)derived. A
|
|
// non-empty DB that doesn't contain us means a foreign/over-cap or corrupt
|
|
// nodes.proto was loaded; an empty DB is just a fresh device (no warning).
|
|
meshtastic_NodeInfoLite *selfNode = getMeshNode(self);
|
|
if (!selfNode && numMeshNodes > 0) {
|
|
LOG_WARN("NodeDB self-care: self 0x%08x absent from DB, re-adding", (unsigned)self);
|
|
} else if (selfNode && owner.public_key.size == 32 && selfNode->public_key.size == 32 &&
|
|
memcmp(selfNode->public_key.bytes, owner.public_key.bytes, 32) != 0) {
|
|
LOG_WARN("NodeDB self-care: self 0x%08x key mismatch, refreshing", (unsigned)self);
|
|
}
|
|
|
|
// Maintenance that must never touch self. Pin self to index 0 first so
|
|
// the positional demote/eviction scans (which skip index 0) provably exclude
|
|
// us, wherever the loaded file happened to place our row.
|
|
if (selfNode && numMeshNodes > 0 && selfNode != &meshNodes->at(0)) {
|
|
std::swap(meshNodes->at(0), *selfNode);
|
|
}
|
|
|
|
#if WARM_NODE_COUNT > 0
|
|
if (nodesOverCap)
|
|
demoteOldestHotNodesToWarm(); // demotes oldest NON-self overflow; index 0 (us) left in place
|
|
#endif
|
|
|
|
if (numMeshNodes > MAX_NUM_NODES) {
|
|
LOG_WARN("NodeDB self-care: %d over cap %d, truncating", numMeshNodes, MAX_NUM_NODES);
|
|
numMeshNodes = MAX_NUM_NODES;
|
|
}
|
|
// Normalise the backing store to the hot cap so getOrCreateMeshNode always
|
|
// has spare slots to append into (it indexes meshNodes->at(numMeshNodes++)).
|
|
meshNodes->resize(MAX_NUM_NODES);
|
|
memaudit::set("nodedb", MAX_NUM_NODES * sizeof(meshtastic_NodeInfoLite));
|
|
|
|
const bool satsTrimmed = enforceSatelliteCaps();
|
|
|
|
// 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);
|
|
if (info) {
|
|
TypeConversions::CopyUserToNodeInfoLite(info, owner);
|
|
if (info != &meshNodes->at(0))
|
|
std::swap(meshNodes->at(0), *info);
|
|
}
|
|
|
|
// 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
|
|
// over the encrypted store; reloadFromDisk() re-runs self-care once unlocked.
|
|
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
|
const bool storageLocked = EncryptedStorage::isLockdownActive() && !EncryptedStorage::isUnlocked();
|
|
#else
|
|
const bool storageLocked = false;
|
|
#endif
|
|
|
|
if ((nodesOverCap || satsTrimmed) && !storageLocked) {
|
|
LOG_MIGRATION("NodeDB self-care: healed store (nodes-over-cap:%s sats-trimmed:%s); rewriting nodes.proto once",
|
|
nodesOverCap ? "yes" : "no", satsTrimmed ? "yes" : "no");
|
|
saveNodeDatabaseToDisk();
|
|
#if WARM_NODE_COUNT > 0
|
|
warmStore.saveIfDirty();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void NodeDB::loadFromDisk()
|
|
{
|
|
// Mark the current device state as completely unusable, so that if we fail reading the entire file from
|
|
// disk we will still factoryReset to restore things.
|
|
devicestate.version = 0;
|
|
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
|
// Reset the per-load decrypt-failure tracker. Set by loadProto on any
|
|
// encrypted file that fails to decrypt or proto-decode; consumed by
|
|
// reloadFromDisk to surface storage corruption to the operator instead
|
|
// of silently falling back to defaults.
|
|
storageCorruptThisLoad = false;
|
|
#endif
|
|
|
|
migrationSavePending = false;
|
|
configDecodeFailed = false;
|
|
configLoadComplete = false;
|
|
|
|
#if !USERPREFS_EVENT_MODE
|
|
#ifdef FSCom
|
|
const char *eventProfileFiles[] = {EVENT_CONFIG_FILE_NAME, EVENT_CHANNEL_FILE_NAME, EVENT_BACKUP_FILE_NAME};
|
|
spiLock->lock();
|
|
for (const char *filename : eventProfileFiles) {
|
|
if (FSCom.exists(filename) && !FSCom.remove(filename))
|
|
LOG_WARN("Unable to remove stale event profile file %s", filename);
|
|
}
|
|
spiLock->unlock();
|
|
#endif
|
|
#endif
|
|
|
|
#if USERPREFS_EVENT_MODE
|
|
// Seed only a missing event config; never overwrite normal files after a corrupt event config.
|
|
bool eventConfigMissing = false;
|
|
eventProfileStorageUnavailable = false;
|
|
#ifdef FSCom
|
|
spiLock->lock();
|
|
eventConfigMissing = !FSCom.exists(configFileName);
|
|
if (eventConfigMissing) {
|
|
const size_t totalBytes = fsTotalBytes();
|
|
const size_t usedBytes = fsUsedBytes();
|
|
eventProfileStorageUnavailable = !hasEventProfileStorageSpace(totalBytes, usedBytes);
|
|
if (eventProfileStorageUnavailable) {
|
|
LOG_ERROR("Event profile requires %u bytes free; only %u bytes available. Profile changes will not persist.",
|
|
static_cast<unsigned>(EVENT_PROFILE_STORAGE_RESERVATION_BYTES),
|
|
static_cast<unsigned>(totalBytes >= usedBytes ? totalBytes - usedBytes : 0));
|
|
}
|
|
}
|
|
spiLock->unlock();
|
|
#endif
|
|
bool initializedEventConfig = false;
|
|
#endif
|
|
|
|
meshtastic_Config_SecurityConfig backupSecurity = meshtastic_Config_SecurityConfig_init_zero;
|
|
|
|
#ifdef ARCH_ESP32
|
|
spiLock->lock();
|
|
// If the legacy deviceState exists, start over with a factory reset
|
|
if (FSCom.exists("/static/static"))
|
|
rmDir("/static/static"); // Remove bad static web files bundle from initial 2.5.13 release
|
|
spiLock->unlock();
|
|
#endif
|
|
|
|
#ifdef FSCom
|
|
#if defined(FACTORY_INSTALL) && !defined(ARCH_PORTDUINO)
|
|
spiLock->lock();
|
|
if (!FSCom.exists("/prefs/" xstr(BUILD_EPOCH))) {
|
|
LOG_WARN("Factory Install Reset!");
|
|
rmDir("/prefs");
|
|
FSCom.mkdir("/prefs");
|
|
File f2 = FSCom.open("/prefs/" xstr(BUILD_EPOCH), FILE_O_WRITE);
|
|
if (f2) {
|
|
f2.flush();
|
|
f2.close();
|
|
}
|
|
}
|
|
spiLock->unlock();
|
|
#endif // FACTORY_INSTALL, not PORTDUINO
|
|
spiLock->lock();
|
|
if (FSCom.exists(legacyPrefFileName)) {
|
|
spiLock->unlock();
|
|
LOG_WARN("Legacy prefs version found, factory resetting");
|
|
if (loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg,
|
|
&config) == LoadFileResult::LOAD_SUCCESS &&
|
|
config.has_security && config.security.private_key.size > 0) {
|
|
LOG_DEBUG("Saving backup of security config and keys");
|
|
backupSecurity = config.security;
|
|
}
|
|
spiLock->lock();
|
|
rmDir("/prefs");
|
|
spiLock->unlock();
|
|
} else {
|
|
spiLock->unlock();
|
|
}
|
|
|
|
#endif // FSCom
|
|
|
|
#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
|
|
// 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
|
|
// passphrase over BLE/serial; PhoneAPI::handleLockdownAuthInline
|
|
// calls reloadFromDisk() once the storage is unlocked.
|
|
LOG_WARN("NodeDB: Encrypted storage locked, using default config until unlocked");
|
|
installDefaultNodeDatabase();
|
|
installDefaultDeviceState();
|
|
installDefaultConfig();
|
|
installDefaultModuleConfig();
|
|
installDefaultChannels();
|
|
|
|
// Hold the radio silent until the operator unlocks. installDefaultConfig
|
|
// 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
|
|
// 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-
|
|
// suspenders for any code path that does not consult region directly.
|
|
// reloadFromDisk() restores the persisted lora config when the
|
|
// operator unlocks.
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
config.lora.tx_enabled = false;
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
// Arm the direct-into-map decode so satellite entries skip the temp vectors.
|
|
{
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
armNodeDatabaseDecodeTargets();
|
|
}
|
|
struct Disarm {
|
|
NodeDB &self;
|
|
~Disarm() { self.disarmNodeDatabaseDecodeTargets(); }
|
|
} disarm{*this};
|
|
|
|
// Avoid push_back's power-of-2 capacity growth wasting RAM at small N.
|
|
nodeDatabase.nodes.reserve(MAX_NUM_NODES);
|
|
|
|
auto state = loadProto(nodeDatabaseFileName, getMaxNodesAllocatedSize(), sizeof(meshtastic_NodeDatabase),
|
|
&meshtastic_NodeDatabase_msg, &nodeDatabase);
|
|
if (nodeDatabase.version < DEVICESTATE_MIN_VER) {
|
|
LOG_WARN("NodeDatabase %d is old, discard", nodeDatabase.version);
|
|
installDefaultNodeDatabase();
|
|
} else if (nodeDatabase.version < DEVICESTATE_CUR_VER) {
|
|
if (migrateLegacyNodeDatabase())
|
|
migrationSavePending = true;
|
|
else
|
|
installDefaultNodeDatabase();
|
|
} else {
|
|
meshNodes = &nodeDatabase.nodes;
|
|
numMeshNodes = nodeDatabase.nodes.size();
|
|
// Counts computed outside LOG_INFO() so cppcheck doesn't choke on #if in macro args.
|
|
const unsigned posCount =
|
|
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
(unsigned)nodePositions.size();
|
|
#else
|
|
0u;
|
|
#endif
|
|
|
|
const unsigned telCount =
|
|
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
(unsigned)nodeTelemetry.size();
|
|
#else
|
|
0u;
|
|
#endif
|
|
|
|
const unsigned envCount =
|
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
(unsigned)nodeEnvironment.size();
|
|
#else
|
|
0u;
|
|
#endif
|
|
|
|
const unsigned statusCount =
|
|
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
(unsigned)nodeStatus.size();
|
|
#else
|
|
0u;
|
|
#endif
|
|
|
|
LOG_INFO("Loaded saved nodedatabase v%d: %d nodes, %u pos, %u tel, %u env, %u status", nodeDatabase.version,
|
|
nodeDatabase.nodes.size(), posCount, telCount, envCount, statusCount);
|
|
}
|
|
|
|
// 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).
|
|
#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).
|
|
warmStore.load();
|
|
#endif
|
|
|
|
// static DeviceState scratch; We no longer read into a tempbuf because this structure is 15KB of valuable RAM
|
|
state = loadProto(deviceStateFileName, meshtastic_DeviceState_size, sizeof(meshtastic_DeviceState),
|
|
&meshtastic_DeviceState_msg, &devicestate);
|
|
|
|
// See https://github.com/meshtastic/firmware/issues/4184#issuecomment-2269390786
|
|
// It is very important to try and use the saved prefs even if we fail to read meshtastic_DeviceState. Because most of our
|
|
// critical config may still be valid (in the other files - loaded next).
|
|
// Also, if we did fail on reading we probably failed on the enormous (and non critical) nodeDB. So DO NOT install default
|
|
// device state.
|
|
// if (state != LoadFileResult::LOAD_SUCCESS) {
|
|
// installDefaultDeviceState(); // Our in RAM copy might now be corrupt
|
|
//} else {
|
|
if ((state != LoadFileResult::LOAD_SUCCESS) || (devicestate.version < DEVICESTATE_MIN_VER)) {
|
|
LOG_WARN("Devicestate %d is old or invalid, discard", devicestate.version);
|
|
installDefaultDeviceState();
|
|
|
|
// Attempt recovery of owner fields from our own NodeDB entry if available.
|
|
meshtastic_NodeInfoLite *us = getMeshNode(getNodeNum());
|
|
if (nodeInfoLiteHasUser(us)) {
|
|
LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x",
|
|
us->num);
|
|
// owner.long_name (40) is wider than the lite source (25); bound by the source
|
|
memcpy(owner.long_name, us->long_name, sizeof(us->long_name));
|
|
owner.long_name[sizeof(us->long_name) - 1] = '\0';
|
|
memcpy(owner.short_name, us->short_name, sizeof(owner.short_name));
|
|
owner.short_name[sizeof(owner.short_name) - 1] = '\0';
|
|
owner.is_licensed = nodeInfoLiteIsLicensed(us);
|
|
owner.has_is_unmessagable = nodeInfoLiteHasIsUnmessagable(us);
|
|
owner.is_unmessagable = nodeInfoLiteIsUnmessagable(us);
|
|
|
|
// Save the recovered owner to device state on disk
|
|
saveToDisk(SEGMENT_DEVICESTATE);
|
|
}
|
|
} else {
|
|
LOG_INFO("Loaded saved devicestate version %d", devicestate.version);
|
|
}
|
|
|
|
// Devicestate saved by firmware that allowed 39-byte names gets clamped on
|
|
// first load; from here on owner never carries more than the local cap.
|
|
clampLongName(owner.long_name);
|
|
|
|
state = loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg,
|
|
&config);
|
|
#if USERPREFS_EVENT_MODE
|
|
if (eventConfigMissing && state != LoadFileResult::LOAD_SUCCESS) {
|
|
const LoadFileResult eventConfigState = state;
|
|
const LoadFileResult standardConfigState =
|
|
loadProto(STANDARD_CONFIG_FILE_NAME, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig),
|
|
&meshtastic_LocalConfig_msg, &config);
|
|
if (standardConfigState == LoadFileResult::LOAD_SUCCESS) {
|
|
// Preserve the user's identity and non-radio preferences, then
|
|
// replace only LoRa with this event build's compiled defaults.
|
|
const meshtastic_LocalConfig standardConfig = config;
|
|
installDefaultConfig(true);
|
|
const meshtastic_Config_LoRaConfig eventLora = config.lora;
|
|
config = standardConfig;
|
|
config.has_lora = true;
|
|
config.lora = eventLora;
|
|
state = LoadFileResult::LOAD_SUCCESS;
|
|
initializedEventConfig = true;
|
|
LOG_INFO("Initialized event config without modifying %s", STANDARD_CONFIG_FILE_NAME);
|
|
} else {
|
|
// Keep the event load outcome because loadProto() clears config before decoding.
|
|
// A normal decode failure must not create a replacement identity.
|
|
state = standardConfigState == LoadFileResult::DECODE_FAILED ? LoadFileResult::DECODE_FAILED : eventConfigState;
|
|
}
|
|
}
|
|
#endif
|
|
if (state == LoadFileResult::DECODE_FAILED) {
|
|
// Config file present but undecodable this boot (corruption / torn write / transient decrypt fail).
|
|
// loadProto() already zeroed `config`, so the keypair is gone from RAM; minting a new one would change
|
|
// our NodeNum (== crc32(public_key)) and orphan us on the mesh. configDecodeFailed freezes identity and
|
|
// skips persisting (see ctor), so a transient failure self-heals on the next clean boot. A genuinely
|
|
// absent config returns OTHER_FAILURE, so this never fires on first boot. Boot degraded + radio-silent.
|
|
LOG_ERROR("Config decode failed - freezing identity, booting degraded (radio silent until restored)");
|
|
configDecodeFailed = true;
|
|
installDefaultConfig(true);
|
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
config.lora.tx_enabled = false;
|
|
} else if (state != LoadFileResult::LOAD_SUCCESS) {
|
|
// No decodable config to work with: the file is absent (first boot) or could not be opened (OTHER_FAILURE
|
|
// / NO_FILESYSTEM). Unlike DECODE_FAILED there are no usable contents to protect, so install defaults.
|
|
installDefaultConfig();
|
|
} else if (config.version < DEVICESTATE_MIN_VER) {
|
|
LOG_WARN("config %d is old, discard", config.version);
|
|
installDefaultConfig(true);
|
|
} else {
|
|
LOG_INFO("Loaded saved config version %d", config.version);
|
|
}
|
|
configLoadComplete = true;
|
|
|
|
// Coerce LoRa config fields derived from presets while bootstrapping.
|
|
// Some clients/UI components display bandwidth/spread_factor directly from config even in preset mode.
|
|
if (config.has_lora && config.lora.use_preset) {
|
|
RadioInterface::clampConfigLora(config.lora);
|
|
}
|
|
|
|
#if defined(USERPREFS_LORA_TX_DISABLED) && USERPREFS_LORA_TX_DISABLED
|
|
config.lora.tx_enabled = false;
|
|
#endif
|
|
|
|
// Always-apply LoRa overrides: applied after loading saved config so they
|
|
// take effect even when NVS already has a valid config (e.g. region-locked
|
|
// dev boards with no BLE/serial to set the region at runtime).
|
|
#ifdef USERPREFS_CONFIG_LORA_REGION
|
|
// Skip on a degraded boot to keep the radio silent (identity is already protected by the keygen gate).
|
|
if (!configDecodeFailed)
|
|
config.lora.region = USERPREFS_CONFIG_LORA_REGION;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_LORACONFIG_USE_PRESET
|
|
config.lora.use_preset = USERPREFS_LORACONFIG_USE_PRESET;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_LORACONFIG_BANDWIDTH
|
|
config.lora.bandwidth = USERPREFS_LORACONFIG_BANDWIDTH;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_LORACONFIG_SPREAD_FACTOR
|
|
config.lora.spread_factor = USERPREFS_LORACONFIG_SPREAD_FACTOR;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_LORACONFIG_CODING_RATE
|
|
config.lora.coding_rate = USERPREFS_LORACONFIG_CODING_RATE;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY
|
|
config.lora.override_frequency = USERPREFS_LORACONFIG_OVERRIDE_FREQUENCY;
|
|
#endif
|
|
|
|
#if USERPREFS_EVENT_MODE
|
|
if (initializedEventConfig) {
|
|
// This is the first durable event-profile write. A failed write is
|
|
// safe: normal files remain untouched and the next event boot retries.
|
|
if (!saveToDisk(SEGMENT_CONFIG))
|
|
LOG_ERROR("Unable to persist initial event config");
|
|
}
|
|
#endif
|
|
|
|
if (backupSecurity.private_key.size > 0) {
|
|
LOG_DEBUG("Restoring backup of security config");
|
|
config.security = backupSecurity;
|
|
saveToDisk(SEGMENT_CONFIG);
|
|
}
|
|
|
|
// Make sure we load hard coded admin keys even when the configuration file has none.
|
|
// Initialize admin_key_count to zero
|
|
byte numAdminKeys = 0;
|
|
#if defined(USERPREFS_USE_ADMIN_KEY_0) || defined(USERPREFS_USE_ADMIN_KEY_1) || defined(USERPREFS_USE_ADMIN_KEY_2)
|
|
uint16_t sum = 0;
|
|
#endif
|
|
|
|
#ifdef USERPREFS_USE_ADMIN_KEY_0
|
|
|
|
for (uint8_t b = 0; b < 32; b++) {
|
|
sum += config.security.admin_key[0].bytes[b];
|
|
}
|
|
if (sum == 0) {
|
|
numAdminKeys += 1;
|
|
LOG_INFO("Admin 0 key zero. Loading hard coded key from user preferences.");
|
|
memcpy(config.security.admin_key[0].bytes, userprefs_admin_key_0, 32);
|
|
config.security.admin_key[0].size = 32;
|
|
}
|
|
#endif
|
|
|
|
#ifdef USERPREFS_USE_ADMIN_KEY_1
|
|
sum = 0;
|
|
for (uint8_t b = 0; b < 32; b++) {
|
|
sum += config.security.admin_key[1].bytes[b];
|
|
}
|
|
if (sum == 0) {
|
|
numAdminKeys += 1;
|
|
LOG_INFO("Admin 1 key zero. Loading hard coded key from user preferences.");
|
|
memcpy(config.security.admin_key[1].bytes, userprefs_admin_key_1, 32);
|
|
config.security.admin_key[1].size = 32;
|
|
}
|
|
#endif
|
|
|
|
#ifdef USERPREFS_USE_ADMIN_KEY_2
|
|
sum = 0;
|
|
for (uint8_t b = 0; b < 32; b++) {
|
|
sum += config.security.admin_key[2].bytes[b];
|
|
}
|
|
if (sum == 0) {
|
|
numAdminKeys += 1;
|
|
LOG_INFO("Admin 2 key zero. Loading hard coded key from user preferences.");
|
|
memcpy(config.security.admin_key[2].bytes, userprefs_admin_key_2, 32);
|
|
config.security.admin_key[2].size = 32;
|
|
}
|
|
#endif
|
|
|
|
if (numAdminKeys > 0) {
|
|
LOG_INFO("Saving %d hard coded admin keys.", numAdminKeys);
|
|
config.security.admin_key_count = numAdminKeys;
|
|
saveToDisk(SEGMENT_CONFIG);
|
|
}
|
|
|
|
state = loadProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, sizeof(meshtastic_LocalModuleConfig),
|
|
&meshtastic_LocalModuleConfig_msg, &moduleConfig);
|
|
if (state != LoadFileResult::LOAD_SUCCESS) {
|
|
installDefaultModuleConfig(); // Our in RAM copy might now be corrupt
|
|
} else {
|
|
if (moduleConfig.version < DEVICESTATE_MIN_VER) {
|
|
LOG_WARN("moduleConfig %d is old, discard", moduleConfig.version);
|
|
installDefaultModuleConfig();
|
|
} else {
|
|
LOG_INFO("Loaded saved moduleConfig version %d", moduleConfig.version);
|
|
}
|
|
}
|
|
|
|
// Always-on traffic management: a device that has NEVER configured TMM
|
|
// (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) {
|
|
LOG_INFO("Traffic management never configured, installing always-on defaults");
|
|
installTrafficManagementDefaults(moduleConfig);
|
|
saveToDisk(SEGMENT_MODULECONFIG);
|
|
}
|
|
|
|
state = loadProto(channelFileName, meshtastic_ChannelFile_size, sizeof(meshtastic_ChannelFile), &meshtastic_ChannelFile_msg,
|
|
&channelFile);
|
|
if (state != LoadFileResult::LOAD_SUCCESS) {
|
|
installDefaultChannels(); // Our in RAM copy might now be corrupt
|
|
} else {
|
|
if (channelFile.version < DEVICESTATE_MIN_VER) {
|
|
LOG_WARN("channelFile %d is old, discard", channelFile.version);
|
|
installDefaultChannels();
|
|
} else {
|
|
LOG_INFO("Loaded saved channelFile version %d", channelFile.version);
|
|
}
|
|
}
|
|
|
|
state = loadProto(uiconfigFileName, meshtastic_DeviceUIConfig_size, sizeof(meshtastic_DeviceUIConfig),
|
|
&meshtastic_DeviceUIConfig_msg, &uiconfig);
|
|
if (state == LoadFileResult::LOAD_SUCCESS) {
|
|
LOG_INFO("Loaded UIConfig");
|
|
}
|
|
|
|
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
|
// Ensure all config segments are persisted to encrypted storage.
|
|
// installDefaultConfig/installDefaultModuleConfig only set in-memory structs
|
|
// 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
|
|
// skipping the whole block avoids the wasted attempts and error logs.
|
|
if (EncryptedStorage::isLockdownActive()) {
|
|
const char *filesToCheck[] = {configFileName, moduleConfigFileName, channelFileName, deviceStateFileName,
|
|
nodeDatabaseFileName};
|
|
const int segments[] = {SEGMENT_CONFIG, SEGMENT_MODULECONFIG, SEGMENT_CHANNELS, SEGMENT_DEVICESTATE,
|
|
SEGMENT_NODEDATABASE};
|
|
int toSave = 0;
|
|
for (size_t i = 0; i < sizeof(segments) / sizeof(segments[0]); i++) {
|
|
if (!EncryptedStorage::isEncrypted(filesToCheck[i])) {
|
|
toSave |= segments[i];
|
|
}
|
|
}
|
|
if (toSave) {
|
|
LOG_INFO("Lockdown: Saving unencrypted segments to encrypted storage (mask=0x%x)", toSave);
|
|
saveToDisk(toSave);
|
|
}
|
|
|
|
// Migrate any remaining plaintext proto files (from standard firmware upgrade)
|
|
for (const char *fn : filesToCheck) {
|
|
if (!EncryptedStorage::isEncrypted(fn)) {
|
|
LOG_INFO("Migrating %s to encrypted storage", fn);
|
|
EncryptedStorage::migrateFile(fn);
|
|
}
|
|
}
|
|
|
|
// Backups are outside saveToDisk(), but can contain radio profile PSKs. Only event builds
|
|
// introduce a second backup file, so leave normal-firmware backup handling unchanged.
|
|
#if USERPREFS_EVENT_MODE
|
|
#ifdef FSCom
|
|
spiLock->lock();
|
|
const bool activeBackupExists = FSCom.exists(backupFileName);
|
|
spiLock->unlock();
|
|
if (activeBackupExists && !EncryptedStorage::isEncrypted(backupFileName)) {
|
|
LOG_INFO("Migrating %s to encrypted storage", backupFileName);
|
|
if (!EncryptedStorage::migrateFile(backupFileName)) {
|
|
LOG_ERROR("Unable to migrate %s to encrypted storage", backupFileName);
|
|
storageCorruptThisLoad = true;
|
|
}
|
|
}
|
|
#endif
|
|
#endif
|
|
|
|
// Event firmware keeps the normal radio profile inactive, so migrate it separately.
|
|
#if USERPREFS_EVENT_MODE
|
|
#ifdef FSCom
|
|
const char *inactiveRadioProfileFiles[] = {STANDARD_CONFIG_FILE_NAME, STANDARD_CHANNEL_FILE_NAME,
|
|
STANDARD_BACKUP_FILE_NAME};
|
|
for (const char *fn : inactiveRadioProfileFiles) {
|
|
spiLock->lock();
|
|
const bool exists = FSCom.exists(fn);
|
|
spiLock->unlock();
|
|
if (exists && !EncryptedStorage::isEncrypted(fn)) {
|
|
LOG_INFO("Migrating inactive radio profile %s to encrypted storage", fn);
|
|
if (!EncryptedStorage::migrateFile(fn)) {
|
|
LOG_ERROR("Unable to migrate %s to encrypted storage", fn);
|
|
storageCorruptThisLoad = true;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
// 2.4.X - configuration migration to update new default intervals
|
|
if (moduleConfig.version < 23) {
|
|
LOG_DEBUG("ModuleConfig version %d is stale, upgrading to new default intervals", moduleConfig.version);
|
|
moduleConfig.version = DEVICESTATE_CUR_VER;
|
|
if (moduleConfig.telemetry.device_update_interval == 900)
|
|
moduleConfig.telemetry.device_update_interval = 0;
|
|
if (moduleConfig.telemetry.environment_update_interval == 900)
|
|
moduleConfig.telemetry.environment_update_interval = 0;
|
|
if (moduleConfig.telemetry.air_quality_interval == 900)
|
|
moduleConfig.telemetry.air_quality_interval = 0;
|
|
if (moduleConfig.telemetry.power_update_interval == 900)
|
|
moduleConfig.telemetry.power_update_interval = 0;
|
|
if (moduleConfig.neighbor_info.update_interval == 900)
|
|
moduleConfig.neighbor_info.update_interval = 0;
|
|
if (moduleConfig.paxcounter.paxcounter_update_interval == 900)
|
|
moduleConfig.paxcounter.paxcounter_update_interval = 0;
|
|
|
|
saveToDisk(SEGMENT_MODULECONFIG);
|
|
}
|
|
|
|
// 2.8 - privacy: one-time flip of position sharing and device telemetry to OPT-IN for nodes upgrading
|
|
// from a build that shipped them on-by-default. Gated on a dedicated watermark (POSITION_TELEMETRY_OPTIN_VER)
|
|
// so it runs exactly once and does NOT re-clobber a user who later re-enables sharing (ordinary saves never
|
|
// re-stamp .version, so a re-enabled node stays at the watermark and skips this block on the next boot).
|
|
// Position is disabled only on public/default-PSK channels; private-PSK channels are preserved.
|
|
if (channelFile.version < POSITION_TELEMETRY_OPTIN_VER) {
|
|
LOG_INFO("Opt-in migration: disabling position broadcast on public channels");
|
|
optInDisablePositionSharing(channelFile);
|
|
channelFile.version = POSITION_TELEMETRY_OPTIN_VER;
|
|
saveToDisk(SEGMENT_CHANNELS);
|
|
}
|
|
if (moduleConfig.version < POSITION_TELEMETRY_OPTIN_VER) {
|
|
LOG_INFO("Opt-in migration: forcing device telemetry broadcast to opt-in");
|
|
optInDisableTelemetryBroadcast(moduleConfig);
|
|
moduleConfig.version = POSITION_TELEMETRY_OPTIN_VER;
|
|
saveToDisk(SEGMENT_MODULECONFIG);
|
|
}
|
|
|
|
if (channels.ensureLicensedOperation()) {
|
|
LOG_WARN("Licensed operation removed persisted channel encryption/admin access");
|
|
saveToDisk(SEGMENT_CHANNELS);
|
|
}
|
|
#if ARCH_PORTDUINO
|
|
// set any config overrides
|
|
if (portduino_config.has_configDisplayMode) {
|
|
config.display.displaymode = (_meshtastic_Config_DisplayConfig_DisplayMode)portduino_config.configDisplayMode;
|
|
}
|
|
if (portduino_config.has_statusMessage) {
|
|
moduleConfig.has_statusmessage = true;
|
|
strncpy(moduleConfig.statusmessage.node_status, portduino_config.statusMessage.c_str(),
|
|
sizeof(moduleConfig.statusmessage.node_status));
|
|
moduleConfig.statusmessage.node_status[sizeof(moduleConfig.statusmessage.node_status) - 1] = '\0';
|
|
}
|
|
if (portduino_config.enable_UDP) {
|
|
config.network.enabled_protocols = meshtastic_Config_NetworkConfig_ProtocolFlags_UDP_BROADCAST;
|
|
}
|
|
|
|
#endif
|
|
}
|
|
|
|
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
|
// 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
|
|
// 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.
|
|
static concurrency::Lock g_reloadFromDiskMutex;
|
|
|
|
/**
|
|
* Re-run loadFromDisk() after encrypted storage is unlocked at runtime.
|
|
* Holds the radio in standby across the file IO + proto decode so the
|
|
* SX12xx is not mid-RX/TX when config.lora is overwritten, then calls
|
|
* 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.
|
|
*/
|
|
bool NodeDB::reloadFromDisk()
|
|
{
|
|
concurrency::LockGuard guard(&g_reloadFromDiskMutex);
|
|
LOG_INFO("NodeDB: Reloading config from encrypted storage after unlock");
|
|
|
|
RadioInterface *rIface = router ? router->getRadioIface() : nullptr;
|
|
|
|
// Park the radio while config.lora / channelFile swap. Without this,
|
|
// a concurrent send or receive can read half-old / half-new state
|
|
// (channel keys, region, modem preset) and the SX12xx ends up in
|
|
// an inconsistent register set that only a reboot recovers from.
|
|
if (rIface)
|
|
rIface->sleep();
|
|
|
|
loadFromDisk();
|
|
|
|
if (storageCorruptThisLoad) {
|
|
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
|
|
// sitting in config.lora.
|
|
return false;
|
|
}
|
|
|
|
// loadFromDisk() leaves the store untrimmed; run self-care now (getNodeNum()
|
|
// is valid at runtime) to trim/demote non-self overflow, pin self to index 0
|
|
// and normalise the backing store before the node DB is exercised again.
|
|
nodeDBSelfCare();
|
|
|
|
// Preserve constructor ordering: persist any migration only after self-care.
|
|
if (migrationSavePending) {
|
|
saveNodeDatabaseToDisk();
|
|
migrationSavePending = false;
|
|
}
|
|
|
|
// Push the now-real config to the radio.
|
|
if (rIface) {
|
|
channels.onConfigChanged();
|
|
rIface->reconfigure();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool NodeDB::disableLockdownToPlaintext()
|
|
{
|
|
concurrency::LockGuard guard(&g_reloadFromDiskMutex);
|
|
if (!EncryptedStorage::isUnlocked()) {
|
|
LOG_ERROR("NodeDB: disable requested but storage not unlocked");
|
|
return false;
|
|
}
|
|
LOG_INFO("NodeDB: reverting encrypted prefs to plaintext for lockdown disable");
|
|
|
|
// Decrypt each encrypted pref back to plaintext IN PLACE. Mirror of the
|
|
// plaintext->encrypted migrate loop above. Order does not matter here;
|
|
// EncryptedStorage::removeLockdownArtifacts() (which deletes the DEK,
|
|
// the commit point) only runs after every file is confirmed plaintext.
|
|
const char *filesToCheck[] = {STANDARD_CONFIG_FILE_NAME, STANDARD_CHANNEL_FILE_NAME, STANDARD_BACKUP_FILE_NAME,
|
|
EVENT_CONFIG_FILE_NAME, EVENT_CHANNEL_FILE_NAME, EVENT_BACKUP_FILE_NAME,
|
|
moduleConfigFileName, deviceStateFileName, nodeDatabaseFileName};
|
|
for (const char *fn : filesToCheck) {
|
|
if (!EncryptedStorage::migrateFileToPlaintext(fn)) {
|
|
LOG_ERROR("NodeDB: failed to revert %s to plaintext; aborting disable (device stays in lockdown)", fn);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
#endif
|
|
|
|
/** Save a protobuf from a file, return true for success */
|
|
bool NodeDB::saveProto(const char *filename, size_t protoSize, const pb_msgdesc_t *fields, const void *dest_struct,
|
|
bool fullAtomic)
|
|
{
|
|
|
|
// Only the radio profile is at risk from an unverified config load, so only defer those writes.
|
|
// Devicestate/nodedb/module writes must still land, otherwise boot-time recovery (e.g. loadFromDisk()
|
|
// restoring owner fields) is dropped and never retried.
|
|
if (isRadioProfileFile(filename) &&
|
|
shouldDeferBootPersistence(bootInitializationInProgress, configLoadComplete, configDecodeFailed)) {
|
|
LOG_WARN("NodeDB: deferred boot write to %s until config recovery completes", filename);
|
|
return true;
|
|
}
|
|
|
|
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
|
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
|
if (!powerHAL_isPowerLevelSafe()) {
|
|
LOG_ERROR("Error: trying to saveProto() on unsafe device power level.");
|
|
return false;
|
|
}
|
|
|
|
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
|
// 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,
|
|
// 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[],
|
|
// so plaintext copies aren't left in heap memory after encryption completes.
|
|
auto pbBuf = meshtastic_security::make_zeroizing_array(protoSize);
|
|
if (!pbBuf) {
|
|
LOG_ERROR("OOM encoding %s for encryption", filename);
|
|
return false;
|
|
}
|
|
|
|
pb_ostream_t stream = pb_ostream_from_buffer(pbBuf.get(), protoSize);
|
|
if (!pb_encode(&stream, fields, dest_struct)) {
|
|
LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream));
|
|
return false;
|
|
}
|
|
|
|
size_t encodedSize = stream.bytes_written;
|
|
bool ok = EncryptedStorage::encryptAndWrite(filename, pbBuf.get(), encodedSize, fullAtomic);
|
|
|
|
if (!ok) {
|
|
LOG_ERROR("EncryptedStorage: Failed to encrypt and write %s", filename);
|
|
}
|
|
return ok;
|
|
}
|
|
#endif
|
|
|
|
bool okay = false;
|
|
#ifdef FSCom
|
|
auto f = SafeFile(filename, fullAtomic);
|
|
|
|
LOG_INFO("Save %s", filename);
|
|
pb_ostream_t stream = {&writecb, static_cast<Print *>(&f), protoSize};
|
|
|
|
if (!pb_encode(&stream, fields, dest_struct)) {
|
|
LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream));
|
|
} else {
|
|
okay = true;
|
|
}
|
|
|
|
bool writeSucceeded = f.close();
|
|
|
|
if (!okay || !writeSucceeded) {
|
|
LOG_ERROR("Can't write prefs!");
|
|
}
|
|
#else
|
|
LOG_ERROR("ERROR: Filesystem not implemented");
|
|
#endif
|
|
return okay;
|
|
}
|
|
|
|
bool NodeDB::saveChannelsToDisk()
|
|
{
|
|
|
|
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
|
// and all writes will fail anyway.
|
|
if (!powerHAL_isPowerLevelSafe()) {
|
|
LOG_ERROR("Error: trying to saveChannelsToDisk() on unsafe device power level.");
|
|
return false;
|
|
}
|
|
|
|
#ifdef FSCom
|
|
spiLock->lock();
|
|
FSCom.mkdir("/prefs");
|
|
spiLock->unlock();
|
|
#endif
|
|
|
|
return saveProto(channelFileName, meshtastic_ChannelFile_size, &meshtastic_ChannelFile_msg, &channelFile);
|
|
}
|
|
|
|
bool NodeDB::saveDeviceStateToDisk()
|
|
{
|
|
|
|
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
|
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
|
if (!powerHAL_isPowerLevelSafe()) {
|
|
LOG_ERROR("Error: trying to saveDeviceStateToDisk() on unsafe device power level.");
|
|
return false;
|
|
}
|
|
|
|
#ifdef FSCom
|
|
spiLock->lock();
|
|
FSCom.mkdir("/prefs");
|
|
spiLock->unlock();
|
|
#endif
|
|
// Note: if MAX_NUM_NODES=100 and meshtastic_NodeInfoLite_size=166, so will be approximately 17KB
|
|
// Because so huge we _must_ not use fullAtomic, because the filesystem is probably too small to hold two copies of this
|
|
return saveProto(deviceStateFileName, meshtastic_DeviceState_size, &meshtastic_DeviceState_msg, &devicestate, true);
|
|
}
|
|
|
|
bool NodeDB::saveNodeDatabaseToDisk()
|
|
{
|
|
// Don't persist the node DB until this device has a PKI keypair
|
|
// TODO: revisit when https://github.com/meshtastic/firmware/pull/10478 lands
|
|
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
|
if (owner.public_key.size != 32 && !owner.is_licensed) {
|
|
LOG_DEBUG("Skip NodeDB without key");
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
|
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
|
if (!powerHAL_isPowerLevelSafe()) {
|
|
LOG_ERROR("Error: trying to saveNodeDatabaseToDisk() on unsafe device power level.");
|
|
return false;
|
|
}
|
|
|
|
// Defer (don't fail) while xmodem holds the prefs file handle. Returning false
|
|
// would propagate through saveToDisk() and trigger fsFormat() mid-transfer.
|
|
#ifdef FSCom
|
|
if (xModem.isBusy()) {
|
|
LOG_DEBUG("Deferring NodeDB save: xmodem transfer in progress");
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
#ifdef FSCom
|
|
spiLock->lock();
|
|
FSCom.mkdir("/prefs");
|
|
spiLock->unlock();
|
|
#endif
|
|
|
|
// Project the maps into the on-disk vectors just before encoding; cleared
|
|
// again on the way out so we don't carry duplicate state.
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
#if !MESHTASTIC_EXCLUDE_POSITIONDB
|
|
nodeDatabase.positions.clear();
|
|
nodeDatabase.positions.reserve(nodePositions.size());
|
|
for (const auto &kv : nodePositions) {
|
|
meshtastic_NodePositionEntry entry = meshtastic_NodePositionEntry_init_default;
|
|
entry.num = kv.first;
|
|
entry.has_position = true;
|
|
entry.position = kv.second;
|
|
nodeDatabase.positions.push_back(entry);
|
|
}
|
|
#else
|
|
nodeDatabase.positions.clear();
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
nodeDatabase.telemetry.clear();
|
|
nodeDatabase.telemetry.reserve(nodeTelemetry.size());
|
|
for (const auto &kv : nodeTelemetry) {
|
|
meshtastic_NodeTelemetryEntry entry = meshtastic_NodeTelemetryEntry_init_default;
|
|
entry.num = kv.first;
|
|
entry.has_device_metrics = true;
|
|
entry.device_metrics = kv.second;
|
|
nodeDatabase.telemetry.push_back(entry);
|
|
}
|
|
#else
|
|
nodeDatabase.telemetry.clear();
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
nodeDatabase.environment.clear();
|
|
nodeDatabase.environment.reserve(nodeEnvironment.size());
|
|
for (const auto &kv : nodeEnvironment) {
|
|
meshtastic_NodeEnvironmentEntry entry = meshtastic_NodeEnvironmentEntry_init_default;
|
|
entry.num = kv.first;
|
|
entry.has_environment_metrics = true;
|
|
entry.environment_metrics = kv.second;
|
|
nodeDatabase.environment.push_back(entry);
|
|
}
|
|
#else
|
|
nodeDatabase.environment.clear();
|
|
#endif
|
|
|
|
#if !MESHTASTIC_EXCLUDE_STATUSDB
|
|
nodeDatabase.status.clear();
|
|
nodeDatabase.status.reserve(nodeStatus.size());
|
|
for (const auto &kv : nodeStatus) {
|
|
meshtastic_NodeStatusEntry entry = meshtastic_NodeStatusEntry_init_default;
|
|
entry.num = kv.first;
|
|
entry.has_status = true;
|
|
entry.status = kv.second;
|
|
nodeDatabase.status.push_back(entry);
|
|
}
|
|
#else
|
|
nodeDatabase.status.clear();
|
|
#endif
|
|
|
|
size_t nodeDatabaseSize;
|
|
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase);
|
|
bool ok = saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false);
|
|
|
|
nodeDatabase.positions.clear();
|
|
nodeDatabase.positions.shrink_to_fit();
|
|
nodeDatabase.telemetry.clear();
|
|
nodeDatabase.telemetry.shrink_to_fit();
|
|
nodeDatabase.environment.clear();
|
|
nodeDatabase.environment.shrink_to_fit();
|
|
nodeDatabase.status.clear();
|
|
nodeDatabase.status.shrink_to_fit();
|
|
#if WARM_NODE_COUNT > 0
|
|
#ifdef ARCH_RP2040
|
|
// nodes.proto + warm.dat are written back-to-back without the loop running between them;
|
|
// 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 -
|
|
// a false return from here would trigger saveToDisk()'s fsFormat() path.
|
|
warmStore.saveIfDirty();
|
|
#endif
|
|
return ok;
|
|
}
|
|
|
|
bool NodeDB::saveToDiskNoRetry(int saveWhat)
|
|
{
|
|
|
|
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
|
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
|
if (!powerHAL_isPowerLevelSafe()) {
|
|
LOG_ERROR("Error: trying to saveToDiskNoRetry() on unsafe device power level.");
|
|
return false;
|
|
}
|
|
|
|
#ifdef MESHTASTIC_ENCRYPTED_STORAGE
|
|
// 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."
|
|
//
|
|
// 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
|
|
// LoRa region) until the device is provisioned.
|
|
if (EncryptedStorage::isLockdownActive() && !EncryptedStorage::isUnlocked()) {
|
|
LOG_WARN("NodeDB: saveToDisk skipped - encrypted storage locked");
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
bool success = true;
|
|
#ifdef FSCom
|
|
spiLock->lock();
|
|
FSCom.mkdir("/prefs");
|
|
spiLock->unlock();
|
|
#endif
|
|
|
|
#if USERPREFS_EVENT_MODE
|
|
if (eventProfileStorageUnavailable) {
|
|
if (saveWhat & SEGMENT_CONFIG) {
|
|
LOG_WARN("Skipping event config write: insufficient profile storage at boot");
|
|
saveWhat &= ~SEGMENT_CONFIG;
|
|
}
|
|
if (saveWhat & SEGMENT_CHANNELS) {
|
|
LOG_WARN("Skipping event channel write: insufficient profile storage at boot");
|
|
saveWhat &= ~SEGMENT_CHANNELS;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (saveWhat & SEGMENT_CONFIG) {
|
|
config.has_device = true;
|
|
config.has_display = true;
|
|
config.has_lora = true;
|
|
config.has_position = true;
|
|
config.has_power = true;
|
|
config.has_network = true;
|
|
config.has_bluetooth = true;
|
|
config.has_security = true;
|
|
|
|
success &= saveProto(configFileName, meshtastic_LocalConfig_size, &meshtastic_LocalConfig_msg, &config);
|
|
}
|
|
|
|
if (saveWhat & SEGMENT_MODULECONFIG) {
|
|
moduleConfig.has_canned_message = true;
|
|
moduleConfig.has_external_notification = true;
|
|
moduleConfig.has_mqtt = true;
|
|
moduleConfig.has_range_test = true;
|
|
moduleConfig.has_serial = true;
|
|
moduleConfig.has_store_forward = true;
|
|
moduleConfig.has_telemetry = true;
|
|
moduleConfig.has_neighbor_info = true;
|
|
moduleConfig.has_detection_sensor = true;
|
|
moduleConfig.has_ambient_lighting = true;
|
|
moduleConfig.has_audio = true;
|
|
moduleConfig.has_paxcounter = true;
|
|
moduleConfig.has_statusmessage = true;
|
|
moduleConfig.has_tak = true;
|
|
#if !MESHTASTIC_EXCLUDE_BEACON
|
|
moduleConfig.has_mesh_beacon = true;
|
|
#endif
|
|
|
|
success &=
|
|
saveProto(moduleConfigFileName, meshtastic_LocalModuleConfig_size, &meshtastic_LocalModuleConfig_msg, &moduleConfig);
|
|
}
|
|
|
|
if (saveWhat & SEGMENT_CHANNELS) {
|
|
success &= saveChannelsToDisk();
|
|
}
|
|
|
|
if (saveWhat & SEGMENT_DEVICESTATE) {
|
|
success &= saveDeviceStateToDisk();
|
|
}
|
|
|
|
if (saveWhat & SEGMENT_NODEDATABASE) {
|
|
success &= saveNodeDatabaseToDisk();
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
bool NodeDB::saveToDisk(int saveWhat)
|
|
{
|
|
LOG_DEBUG("Save to disk %d", saveWhat);
|
|
|
|
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
|
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
|
if (!powerHAL_isPowerLevelSafe()) {
|
|
LOG_ERROR("Error: trying to saveToDisk() on unsafe device power level.");
|
|
return false;
|
|
}
|
|
|
|
bool success = saveToDiskNoRetry(saveWhat);
|
|
|
|
if (!success) {
|
|
LOG_ERROR("Failed to save to disk, retrying");
|
|
spiLock->lock();
|
|
fsFormat();
|
|
spiLock->unlock();
|
|
|
|
success = saveToDiskNoRetry(saveWhat);
|
|
|
|
RECORD_CRITICALERROR(success ? meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE
|
|
: meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
const meshtastic_NodeInfoLite *NodeDB::readNextMeshNode(uint32_t &readIndex)
|
|
{
|
|
if (readIndex < numMeshNodes)
|
|
return &meshNodes->at(readIndex++);
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
/// Given a node, return how many seconds in the past (vs now) that we last heard from it
|
|
uint32_t sinceLastSeen(const meshtastic_NodeInfoLite *n)
|
|
{
|
|
uint32_t now = getTime();
|
|
|
|
int delta = (int)(now - n->last_heard);
|
|
if (delta < 0) // our clock must be slightly off still - not set from GPS yet
|
|
delta = 0;
|
|
|
|
return delta;
|
|
}
|
|
|
|
uint32_t sinceReceived(const meshtastic_MeshPacket *p)
|
|
{
|
|
uint32_t now = getTime();
|
|
|
|
int delta = (int)(now - p->rx_time);
|
|
if (delta < 0) // our clock must be slightly off still - not set from GPS yet
|
|
delta = 0;
|
|
|
|
return delta;
|
|
}
|
|
|
|
HopStartStatus classifyHopStart(const meshtastic_MeshPacket &p)
|
|
{
|
|
// Guard against invalid values.
|
|
if (p.hop_start < p.hop_limit)
|
|
return HopStartStatus::INVALID;
|
|
|
|
if (p.hop_start == 0) {
|
|
// hop_start == 0 is either a modern zero-hop broadcast (e.g. beacon) or pre-2.3.0 firmware (585805c)
|
|
// that never populated hop_start. Firmware 2.5.0 (bf34329) introduced a bitfield that is always
|
|
// present; use it to tell the two apart. The bitfield is encrypted under the channel key, so this can
|
|
// only be resolved for decoded packets - until then the status stays MISSING_OR_UNKNOWN. Callers
|
|
// acting before decode must therefore not treat UNKNOWN as a drop (the bitfield isn't readable yet);
|
|
// the verdict is re-checked post-decode in Router::handleReceived.
|
|
if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield)
|
|
return HopStartStatus::VALID;
|
|
return HopStartStatus::MISSING_OR_UNKNOWN;
|
|
}
|
|
|
|
return HopStartStatus::VALID;
|
|
}
|
|
|
|
int8_t getHopsAway(const meshtastic_MeshPacket &p, int8_t defaultIfUnknown)
|
|
{
|
|
// Firmware prior to 2.3.0 (585805c) lacked a hop_start field. Firmware version 2.5.0 (bf34329) introduced a
|
|
// bitfield that is always present. Use the presence of the bitfield to determine if the origin's firmware
|
|
// version is guaranteed to have hop_start populated. Note that this can only be done for decoded packets as
|
|
// the bitfield is encrypted under the channel encryption key. For encrypted packets, this returns
|
|
// defaultIfUnknown when hop_start is 0.
|
|
if (p.hop_start == 0 && !(p.which_payload_variant == meshtastic_MeshPacket_decoded_tag && p.decoded.has_bitfield))
|
|
return defaultIfUnknown; // Cannot reliably determine the number of hops.
|
|
|
|
// Guard against invalid values.
|
|
if (p.hop_start < p.hop_limit)
|
|
return defaultIfUnknown;
|
|
|
|
return p.hop_start - p.hop_limit;
|
|
}
|
|
|
|
#define NUM_ONLINE_SECS (60 * 60 * 2) // 2 hrs to consider someone offline
|
|
|
|
size_t NodeDB::getNumOnlineMeshNodes(bool localOnly)
|
|
{
|
|
size_t numseen = 0;
|
|
|
|
// FIXME this implementation is kinda expensive
|
|
for (int i = 0; i < numMeshNodes; i++) {
|
|
if (localOnly && nodeInfoLiteViaMqtt(&meshNodes->at(i)))
|
|
continue;
|
|
if (sinceLastSeen(&meshNodes->at(i)) < NUM_ONLINE_SECS)
|
|
numseen++;
|
|
}
|
|
|
|
return numseen;
|
|
}
|
|
|
|
#include "MeshModule.h"
|
|
#include "Throttle.h"
|
|
|
|
// Minimum spacing between evictions once the node database is full.
|
|
#define NODEDB_FULL_EVICTION_INTERVAL_MS (2 * 1000UL)
|
|
|
|
static constexpr uint32_t HOPSTART_DROP_LOG_INTERVAL_MS = 15000;
|
|
|
|
void logHopStartDrop(const meshtastic_MeshPacket &p, const char *context)
|
|
{
|
|
static uint32_t lastLogMs = 0;
|
|
if (Throttle::isWithinTimespanMs(lastLogMs, HOPSTART_DROP_LOG_INTERVAL_MS)) {
|
|
return;
|
|
}
|
|
lastLogMs = millis();
|
|
const bool decoded = (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag);
|
|
const bool hasBitfield = decoded && p.decoded.has_bitfield;
|
|
LOG_DEBUG(
|
|
"Drop packet (%s): hop_start invalid/missing (from=0x%08x id=%u hop_start=%u hop_limit=%u decoded=%d has_bitfield=%d)",
|
|
context ? context : "unknown", p.from, p.id, p.hop_start, p.hop_limit, decoded, hasBitfield);
|
|
}
|
|
|
|
/** Update position info for this node based on received position data
|
|
*/
|
|
void NodeDB::updatePosition(uint32_t nodeId, const meshtastic_Position &p, RxSource src)
|
|
{
|
|
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
|
|
if (!info) {
|
|
return;
|
|
}
|
|
|
|
#if MESHTASTIC_EXCLUDE_POSITIONDB
|
|
// Build flag opted out: header still tracks last_heard via updateFrom; we
|
|
// simply don't cache the position payload anywhere on this device.
|
|
if (src == RX_SRC_LOCAL) {
|
|
LOG_INFO("updatePosition LOCAL (PositionDB excluded) time=%u lat=%d lon=%d alt=%d", p.time, p.latitude_i, p.longitude_i,
|
|
p.altitude);
|
|
setLocalPosition(p);
|
|
}
|
|
(void)nodeId;
|
|
updateGUIforNode = info;
|
|
notifyObservers(true);
|
|
#else
|
|
{
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
evictSatelliteOverCap(*this, nodePositions, nodeId);
|
|
meshtastic_PositionLite &slot = nodePositions[nodeId]; // creates default-zero entry if missing
|
|
|
|
if (src == RX_SRC_LOCAL) {
|
|
// Local packet, fully authoritative
|
|
LOG_INFO("updatePosition LOCAL pos@%x time=%u lat=%d lon=%d alt=%d", p.timestamp, p.time, p.latitude_i, p.longitude_i,
|
|
p.altitude);
|
|
|
|
setLocalPosition(p);
|
|
slot = TypeConversions::ConvertToPositionLite(p);
|
|
} else if ((p.time > 0) && !p.latitude_i && !p.longitude_i && !p.timestamp && !p.location_source) {
|
|
// FIXME SPECIAL TIME SETTING PACKET FROM EUD TO RADIO
|
|
// (stop-gap fix for issue #900)
|
|
LOG_DEBUG("updatePosition SPECIAL time setting time=%u", p.time);
|
|
slot.time = p.time;
|
|
} else {
|
|
// Be careful to only update fields that have been set by the REMOTE sender
|
|
// A lot of position reports don't have time populated. In that case, be careful to not blow away the time we
|
|
// recorded based on the packet rxTime
|
|
//
|
|
// FIXME perhaps handle RX_SRC_USER separately?
|
|
LOG_INFO("updatePosition REMOTE node=0x%08x time=%u lat=%d lon=%d", nodeId, p.time, p.latitude_i, p.longitude_i);
|
|
|
|
// First, back up fields that we want to protect from overwrite
|
|
uint32_t tmp_time = slot.time;
|
|
|
|
// Next, update atomically
|
|
slot = TypeConversions::ConvertToPositionLite(p);
|
|
|
|
// Last, restore any fields that may have been overwritten
|
|
if (!slot.time)
|
|
slot.time = tmp_time;
|
|
}
|
|
}
|
|
updateGUIforNode = info;
|
|
notifyObservers(true); // Force an update whether or not our node counts have changed
|
|
#endif
|
|
}
|
|
|
|
/** Update telemetry info for this node based on received metrics. Stores
|
|
* device_metrics and environment_metrics into their respective satellite
|
|
* maps; other variants (air_quality, power, local_stats, health) are
|
|
* intentionally not retained per-node.
|
|
*/
|
|
void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src)
|
|
{
|
|
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
|
|
if (!info)
|
|
return;
|
|
|
|
if (t.which_variant == meshtastic_Telemetry_device_metrics_tag) {
|
|
if (src == RX_SRC_LOCAL) {
|
|
LOG_DEBUG("updateTelemetry LOCAL device");
|
|
} else {
|
|
LOG_DEBUG("updateTelemetry REMOTE device node=0x%08x", nodeId);
|
|
}
|
|
#if !MESHTASTIC_EXCLUDE_TELEMETRYDB
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
evictSatelliteOverCap(*this, nodeTelemetry, nodeId);
|
|
nodeTelemetry[nodeId] = t.variant.device_metrics;
|
|
#endif
|
|
|
|
} else if (t.which_variant == meshtastic_Telemetry_environment_metrics_tag) {
|
|
if (src == RX_SRC_LOCAL) {
|
|
LOG_DEBUG("updateTelemetry LOCAL env");
|
|
} else {
|
|
LOG_DEBUG("updateTelemetry REMOTE env node=0x%08x", nodeId);
|
|
}
|
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB
|
|
concurrency::LockGuard guard(&satelliteMutex);
|
|
evictSatelliteOverCap(*this, nodeEnvironment, nodeId);
|
|
nodeEnvironment[nodeId] = t.variant.environment_metrics;
|
|
#endif
|
|
|
|
} else {
|
|
return; // air_quality / power / local_stats / health: not stored per-node
|
|
}
|
|
updateGUIforNode = info;
|
|
notifyObservers(true);
|
|
}
|
|
|
|
/**
|
|
* Update the node database with a new contact
|
|
*/
|
|
void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
|
{
|
|
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(contact.node_num);
|
|
if (!info || !contact.has_user) {
|
|
return;
|
|
}
|
|
// If the local node has this node marked as manually verified
|
|
// and the client does not, do not allow the client to update the
|
|
// saved public key.
|
|
if (nodeInfoLiteIsKeyManuallyVerified(info) && !contact.manually_verified) {
|
|
if (contact.user.public_key.size != info->public_key.size ||
|
|
memcmp(contact.user.public_key.bytes, info->public_key.bytes, info->public_key.size) != 0) {
|
|
return;
|
|
}
|
|
}
|
|
info->num = contact.node_num;
|
|
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
|
|
// (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);
|
|
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_IS_FAVORITE_MASK, false);
|
|
eraseNodeSatellites(contact.node_num);
|
|
#if HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
|
|
messageStore.deleteAllMessagesFromNode(contact.node_num);
|
|
#endif
|
|
} else {
|
|
/* Clients are sending add_contact before every text message DM (because clients may hold a larger node database with
|
|
* public keys than the radio holds). However, we don't want to update last_heard just because we sent someone a DM!
|
|
*/
|
|
|
|
/* "Boring old nodes" are the first to be evicted out of the node database when full. This includes a newly-zeroed
|
|
* nodeinfo because it has: !is_favorite && last_heard==0. To keep this from happening when we addFromContact, we set the
|
|
* new node as a favorite, and we leave last_heard alone (even if it's zero).
|
|
*/
|
|
if (config.device.role == meshtastic_Config_DeviceConfig_Role_CLIENT_BASE) {
|
|
// Special case for CLIENT_BASE: is_favorite has special meaning, and we don't want to automatically set it
|
|
// without the user doing so deliberately. We don't normally expect users to use a CLIENT_BASE to send DMs or to add
|
|
// contacts, but we should make sure it doesn't auto-favorite in case they do. Instead, as a workaround, we'll set
|
|
// last_heard to now, so that the add_contact node doesn't immediately get evicted.
|
|
info->last_heard = getTime();
|
|
} else {
|
|
// Normal case: set is_favorite to prevent expiration.
|
|
// last_heard will remain as-is (or remain 0 if this entry wasn't in the nodeDB).
|
|
// If the protected cap refuses the favorite, fall back to stamping last_heard so the
|
|
// contact still isn't the first eviction victim.
|
|
if (!setProtectedFlag(info, NODEINFO_BITFIELD_IS_FAVORITE_MASK, true))
|
|
info->last_heard = getTime();
|
|
}
|
|
|
|
// As the clients will begin sending the contact with DMs, we want to strictly check if the node is manually verified
|
|
if (contact.manually_verified) {
|
|
if (!setProtectedFlag(info, NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK, true))
|
|
LOG_WARN(PROTECTED_CAP_WARN_FMT, "verify", contact.node_num, MAX_NUM_NODES - 2);
|
|
}
|
|
// Mark the node's key as manually verified to indicate trustworthiness.
|
|
updateGUIforNode = info;
|
|
sortMeshDB();
|
|
notifyObservers(true); // Force an update whether or not our node counts have changed
|
|
}
|
|
saveNodeDatabaseToDisk();
|
|
}
|
|
|
|
/** Update user info and channel for this node based on received user data
|
|
*/
|
|
bool NodeDB::updateUser(uint32_t nodeId, meshtastic_User &p, uint8_t channelIndex, bool xeddsaSigned)
|
|
{
|
|
// Only a signed update may change the identity of a node that has proven it signs; our own record is
|
|
// exempt. Checked before getOrCreateMeshNode so a refused update cannot evict or write the warm tier.
|
|
const meshtastic_NodeInfoLite *existing = getMeshNode(nodeId);
|
|
if (nodeId != getNodeNum() && existing && nodeInfoLiteHasXeddsaSigned(existing) && !xeddsaSigned) {
|
|
LOG_WARN("Refusing unsigned identity update for node 0x%08x that previously signed", nodeId);
|
|
return false;
|
|
}
|
|
|
|
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
|
|
if (!info) {
|
|
return false;
|
|
}
|
|
|
|
#if !(MESHTASTIC_EXCLUDE_PKI)
|
|
if (p.public_key.size == 32 && nodeId != nodeDB->getNodeNum()) {
|
|
printBytes("Incoming Pubkey: ", p.public_key.bytes, 32);
|
|
|
|
// Alert the user if a remote node is advertising public key that matches our own
|
|
if (owner.public_key.size == 32 && memcmp(p.public_key.bytes, owner.public_key.bytes, 32) == 0) {
|
|
if (!duplicateWarned) {
|
|
duplicateWarned = true;
|
|
// Sanitize before embedding long_name in the phone-facing ClientNotification string
|
|
// (defense-in-depth vs PB_VALIDATE_UTF8).
|
|
char safeName[sizeof(p.long_name)];
|
|
strncpy(safeName, p.long_name, sizeof(safeName));
|
|
safeName[sizeof(safeName) - 1] = '\0';
|
|
sanitizeUtf8(safeName, sizeof(safeName));
|
|
char warning[] =
|
|
"Remote device %s has advertised your public key. This may indicate a compromised key. You may need "
|
|
"to regenerate your public keys.";
|
|
LOG_WARN(warning, safeName);
|
|
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
|
if (cn) {
|
|
cn->level = meshtastic_LogRecord_Level_WARNING;
|
|
cn->time = getValidTime(RTCQualityFromNet);
|
|
snprintf(cn->message, sizeof(cn->message), warning, safeName);
|
|
service->sendClientNotification(cn);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
if (info->public_key.size == 32) { // if we have a key for this user already, don't overwrite with a new one
|
|
// if the key doesn't match, don't update nodeDB at all.
|
|
if (p.public_key.size != 32 || (memcmp(p.public_key.bytes, info->public_key.bytes, 32) != 0)) {
|
|
LOG_WARN("Public Key mismatch, dropping NodeInfo");
|
|
return false;
|
|
}
|
|
LOG_INFO("Public Key set for node, not updating!");
|
|
} else if (p.public_key.size == 32) {
|
|
LOG_INFO("Update Node Pubkey!");
|
|
}
|
|
#endif
|
|
|
|
// Always ensure user.id is derived from nodeId, regardless of what was received
|
|
snprintf(p.id, sizeof(p.id), "!%08x", nodeId);
|
|
|
|
meshtastic_NodeInfoLite before = *info;
|
|
TypeConversions::CopyUserToNodeInfoLite(info, p);
|
|
bool changed =
|
|
(memcmp(before.long_name, info->long_name, sizeof(info->long_name)) != 0) ||
|
|
(memcmp(before.short_name, info->short_name, sizeof(info->short_name)) != 0) || (before.hw_model != info->hw_model) ||
|
|
(before.role != info->role) || (before.public_key.size != info->public_key.size) ||
|
|
(info->public_key.size > 0 && memcmp(before.public_key.bytes, info->public_key.bytes, info->public_key.size) != 0) ||
|
|
(before.bitfield != info->bitfield) || (info->channel != channelIndex);
|
|
|
|
if (info->public_key.size == 32) {
|
|
printBytes("Saved Pubkey: ", info->public_key.bytes, 32);
|
|
}
|
|
if (nodeId != getNodeNum())
|
|
info->channel = channelIndex; // Set channel we need to use to reach this node (but don't set our own channel)
|
|
LOG_DEBUG("Update changed=%d user %s/%s, id=0x%08x, channel=%d", changed, info->long_name, info->short_name, nodeId,
|
|
info->channel);
|
|
|
|
if (changed) {
|
|
updateGUIforNode = info;
|
|
notifyObservers(true); // Force an update whether or not our node counts have changed
|
|
|
|
// We just changed something about a User,
|
|
// store our DB unless we just did so less than a minute ago
|
|
|
|
if (!Throttle::isWithinTimespanMs(lastNodeDbSave, ONE_MINUTE_MS)) {
|
|
saveToDisk(SEGMENT_NODEDATABASE);
|
|
lastNodeDbSave = millis();
|
|
} else {
|
|
LOG_DEBUG("Defer NodeDB saveToDisk for now");
|
|
}
|
|
}
|
|
|
|
#if HAS_TRAFFIC_MANAGEMENT
|
|
// Write-through: every accepted remote-identity commit lands here (NodeInfoModule,
|
|
// MeshService, and TMM's requester learning all funnel through updateUser; the two
|
|
// key-write sites that bypass it call onNodeKeyCommitted instead), so TMM's NodeInfo
|
|
// cache reflects the commit immediately rather than at the next reconcile pass. Runs on
|
|
// acceptance, not on `changed`: an identical update still proves the identity is
|
|
// current. `p` is the post-hygiene payload; signerKnown transfers only key-matched
|
|
// verified-signer status (isVerifiedSignerForKey semantics), never a bare node flag.
|
|
if (nodeId != getNodeNum() && trafficManagementModule) {
|
|
const bool signerKnown = p.public_key.size == 32 && isVerifiedSignerForKey(nodeId, p.public_key.bytes);
|
|
trafficManagementModule->onNodeIdentityCommitted(nodeId, p, signerKnown);
|
|
}
|
|
#endif
|
|
|
|
return changed;
|
|
}
|
|
|
|
/// given a subpacket sniffed from the network, update our DB state
|
|
/// we updateGUI and updateGUIforNode if we think our this change is big enough for a redraw
|
|
void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
|
{
|
|
if (mp.from == getNodeNum()) {
|
|
LOG_DEBUG("Ignore update from self");
|
|
return;
|
|
}
|
|
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.from) {
|
|
LOG_DEBUG("Update DB node 0x%08x, rx_time=%u", mp.from, mp.rx_time);
|
|
|
|
// mp.from is unauthenticated, so rate-limit admission once the database is full: otherwise
|
|
// invented node numbers churn it at packet rate and push real neighbours out.
|
|
meshtastic_NodeInfoLite *info = getMeshNode(getFrom(&mp));
|
|
if (!info) {
|
|
if (isFull()) {
|
|
if (Throttle::isWithinTimespanMs(lastFullEvictionMs, NODEDB_FULL_EVICTION_INTERVAL_MS)) {
|
|
LOG_DEBUG("Node database full, defer admitting 0x%08x", mp.from);
|
|
return;
|
|
}
|
|
lastFullEvictionMs = millis();
|
|
}
|
|
info = getOrCreateMeshNode(getFrom(&mp));
|
|
}
|
|
if (!info) {
|
|
return;
|
|
}
|
|
|
|
if (mp.rx_time) // if the packet has a valid timestamp use it to update our last_heard
|
|
info->last_heard = mp.rx_time;
|
|
|
|
if (mp.rx_snr)
|
|
info->snr = mp.rx_snr; // keep the most recent SNR we received for this node.
|
|
|
|
nodeInfoLiteSetBit(info, NODEINFO_BITFIELD_VIA_MQTT_MASK,
|
|
mp.via_mqtt); // Store if we received this packet via MQTT
|
|
|
|
#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
|
|
// 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.
|
|
if (mp.transport_mechanism == meshtastic_MeshPacket_TransportMechanism_TRANSPORT_LORA && !mp.via_mqtt &&
|
|
hopScalingModule) {
|
|
uint8_t hopCount = std::max(int8_t(0), getHopsAway(mp));
|
|
hopScalingModule->samplePacketForHistogram(mp.from, hopCount);
|
|
}
|
|
#endif
|
|
|
|
// If hopStart was set and there wasn't someone messing with the limit in the middle, add hopsAway
|
|
const int8_t hopsAway = getHopsAway(mp);
|
|
if (hopsAway >= 0) {
|
|
info->has_hops_away = true;
|
|
info->hops_away = hopsAway;
|
|
}
|
|
sortMeshDB();
|
|
}
|
|
}
|
|
|
|
int NodeDB::numProtectedNodes() const
|
|
{
|
|
int count = 0;
|
|
for (int i = 0; i < numMeshNodes; i++)
|
|
if (nodeInfoLiteIsProtected(&meshNodes->at(i)))
|
|
count++;
|
|
return count;
|
|
}
|
|
|
|
bool NodeDB::setProtectedFlag(meshtastic_NodeInfoLite *node, uint32_t mask, bool on)
|
|
{
|
|
if (!node)
|
|
return false;
|
|
if (!on) {
|
|
nodeInfoLiteSetBit(node, mask, false);
|
|
return true;
|
|
}
|
|
// Adding a flag to a node that is already protected doesn't grow the
|
|
// protected set, so it's always allowed. A newly-protected node is refused
|
|
// once the protected set has reached MAX_NUM_NODES-2, leaving two evictable
|
|
// slots so getOrCreateMeshNode can always make room.
|
|
if (nodeInfoLiteIsProtected(node) || numProtectedNodes() < MAX_NUM_NODES - 2) {
|
|
nodeInfoLiteSetBit(node, mask, true);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool NodeDB::set_favorite(bool is_favorite, uint32_t nodeId)
|
|
{
|
|
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
|
|
if (!lite)
|
|
return false;
|
|
if (nodeInfoLiteIsFavorite(lite) == is_favorite)
|
|
return true; // already in the requested state
|
|
if (setProtectedFlag(lite, NODEINFO_BITFIELD_IS_FAVORITE_MASK, is_favorite)) {
|
|
sortMeshDB();
|
|
saveNodeDatabaseToDisk();
|
|
return true;
|
|
}
|
|
LOG_WARN(PROTECTED_CAP_WARN_FMT, "favorite", nodeId, MAX_NUM_NODES - 2);
|
|
return false;
|
|
}
|
|
|
|
bool NodeDB::isFavorite(uint32_t nodeId)
|
|
{
|
|
// returns true if nodeId is_favorite; false if not or not found
|
|
|
|
// NODENUM_BROADCAST will never be in the DB
|
|
if (nodeId == NODENUM_BROADCAST)
|
|
return false;
|
|
|
|
const meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
|
|
|
|
if (lite) {
|
|
return nodeInfoLiteIsFavorite(lite);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool NodeDB::isFromOrToFavoritedNode(const meshtastic_MeshPacket &p)
|
|
{
|
|
// This method is logically equivalent to:
|
|
// return isFavorite(p.from) || isFavorite(p.to);
|
|
// but is more efficient by:
|
|
// 1. doing only one pass through the database, instead of two
|
|
// 2. exiting early when a favorite is found, or if both from and to have been seen
|
|
|
|
if (p.to == NODENUM_BROADCAST)
|
|
return isFavorite(p.from); // we never store NODENUM_BROADCAST in the DB, so we only need to check p.from
|
|
|
|
meshtastic_NodeInfoLite *lite = NULL;
|
|
|
|
bool seenFrom = false;
|
|
bool seenTo = false;
|
|
|
|
for (int i = 0; i < numMeshNodes; i++) {
|
|
lite = &meshNodes->at(i);
|
|
|
|
if (lite->num == p.from) {
|
|
if (nodeInfoLiteIsFavorite(lite))
|
|
return true;
|
|
|
|
seenFrom = true;
|
|
}
|
|
|
|
if (lite->num == p.to) {
|
|
if (nodeInfoLiteIsFavorite(lite))
|
|
return true;
|
|
|
|
seenTo = true;
|
|
}
|
|
|
|
if (seenFrom && seenTo)
|
|
return false; // we've seen both, and neither is a favorite, so we can stop searching early
|
|
|
|
// Note: if we knew that sortMeshDB was always called after any change to is_favorite, we could exit early after searching
|
|
// all favorited nodes first.
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void NodeDB::pause_sort(bool paused)
|
|
{
|
|
sortingIsPaused = paused;
|
|
}
|
|
|
|
void NodeDB::sortMeshDB()
|
|
{
|
|
if (!sortingIsPaused && (lastSort == 0 || !Throttle::isWithinTimespanMs(lastSort, 1000 * 5))) {
|
|
lastSort = millis();
|
|
bool changed = true;
|
|
while (changed) { // dumb reverse bubble sort, but probably not bad for what we're doing
|
|
changed = false;
|
|
for (int i = numMeshNodes - 1; i > 0; i--) { // lowest case this should examine is i == 1
|
|
if (meshNodes->at(i - 1).num == getNodeNum()) {
|
|
// noop
|
|
} else if (meshNodes->at(i).num ==
|
|
getNodeNum()) { // in the oddball case our own node num is not at location 0, put it there
|
|
// TODO: Look for at(i-1) also matching own node num, and throw the DB in the trash
|
|
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
|
|
changed = true;
|
|
} else if (nodeInfoLiteIsFavorite(&meshNodes->at(i)) && !nodeInfoLiteIsFavorite(&meshNodes->at(i - 1))) {
|
|
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
|
|
changed = true;
|
|
} else if (!nodeInfoLiteIsFavorite(&meshNodes->at(i)) && nodeInfoLiteIsFavorite(&meshNodes->at(i - 1))) {
|
|
// noop
|
|
} else if (meshNodes->at(i).last_heard > meshNodes->at(i - 1).last_heard) {
|
|
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
LOG_INFO("Sort took %u milliseconds", millis() - lastSort);
|
|
}
|
|
}
|
|
|
|
uint8_t NodeDB::getMeshNodeChannel(NodeNum n)
|
|
{
|
|
const meshtastic_NodeInfoLite *info = getMeshNode(n);
|
|
if (!info) {
|
|
return 0; // defaults to PRIMARY
|
|
}
|
|
return info->channel;
|
|
}
|
|
|
|
std::string NodeDB::getNodeId() const
|
|
{
|
|
char nodeId[16];
|
|
snprintf(nodeId, sizeof(nodeId), "!%08x", myNodeInfo.my_node_num);
|
|
return std::string(nodeId);
|
|
}
|
|
|
|
/// Find a node in our DB, return null for missing
|
|
/// NOTE: This function might be called from an ISR
|
|
meshtastic_NodeInfoLite *NodeDB::getMeshNode(NodeNum n)
|
|
{
|
|
for (int i = 0; i < numMeshNodes; i++)
|
|
if (meshNodes->at(i).num == n)
|
|
return &meshNodes->at(i);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
ResolvedNode NodeDB::resolveLastByte(uint8_t lastByte, bool requireDirectNeighbor)
|
|
{
|
|
ResolvedNode result; // defaults to {None, 0}
|
|
|
|
// 0 is the NO_RELAY_NODE / NO_NEXT_HOP_PREFERENCE sentinel (also what MQTT-sourced packets carry
|
|
// when hop_start==0). getLastByteOfNodeNum() never yields 0, so nothing can legitimately match.
|
|
if (lastByte == 0)
|
|
return result;
|
|
|
|
const NodeNum self = getNodeNum();
|
|
NodeNum firstMatch = 0;
|
|
uint8_t matches = 0;
|
|
|
|
for (size_t i = 0; i < numMeshNodes; i++) {
|
|
const meshtastic_NodeInfoLite *node = &meshNodes->at(i);
|
|
|
|
// Candidate gate: never resolve to ourselves, the sentinels, or an ignored node.
|
|
if (node->num == self || node->num == 0 || node->num == NODENUM_BROADCAST)
|
|
continue;
|
|
if (nodeInfoLiteIsIgnored(node))
|
|
continue;
|
|
if (getLastByteOfNodeNum(node->num) != lastByte) // cheapest discriminator last
|
|
continue;
|
|
|
|
// Relevance gate: is this node a plausible relay for the requested scope?
|
|
bool relevant;
|
|
if (requireDirectNeighbor) {
|
|
relevant = node->has_hops_away && node->hops_away == 0 && sinceLastSeen(node) < NEXTHOP_NEIGHBOR_FRESH_SECS;
|
|
} else {
|
|
const bool directNeighbor = node->has_hops_away && node->hops_away == 0;
|
|
const bool routerRole =
|
|
IS_ONE_OF(node->role, meshtastic_Config_DeviceConfig_Role_ROUTER, meshtastic_Config_DeviceConfig_Role_ROUTER_LATE,
|
|
meshtastic_Config_DeviceConfig_Role_CLIENT_BASE);
|
|
relevant = directNeighbor || nodeInfoLiteIsFavorite(node) || routerRole;
|
|
}
|
|
if (!relevant)
|
|
continue;
|
|
|
|
if (++matches == 1) {
|
|
firstMatch = node->num;
|
|
} else {
|
|
// A second relevant candidate shares this byte: ambiguous. No further scanning can
|
|
// change that, so stop early and report the collision.
|
|
result.status = LastByteResolution::Ambiguous;
|
|
result.num = 0;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
if (matches == 1) {
|
|
result.status = LastByteResolution::Unique;
|
|
result.num = firstMatch;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool NodeDB::resolveUniqueLastByte(uint8_t lastByte, bool requireDirectNeighbor, NodeNum *outNum)
|
|
{
|
|
ResolvedNode r = resolveLastByte(lastByte, requireDirectNeighbor);
|
|
if (r.status == LastByteResolution::Unique) {
|
|
if (outNum)
|
|
*outNum = r.num;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// returns true if the maximum number of nodes is reached or we are running low on memory
|
|
bool NodeDB::isFull()
|
|
{
|
|
return (numMeshNodes >= MAX_NUM_NODES) || (memGet.getFreeHeap() < MINIMUM_SAFE_FREE_HEAP);
|
|
}
|
|
|
|
uint32_t NodeDB::hotNodeLastHeard(NodeNum n) const
|
|
{
|
|
for (int i = 0; i < numMeshNodes; i++)
|
|
if (meshNodes->at(i).num == n)
|
|
return meshNodes->at(i).last_heard;
|
|
return 0;
|
|
}
|
|
|
|
bool NodeDB::copyPublicKeyAuthoritative(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out)
|
|
{
|
|
const meshtastic_NodeInfoLite *info = getMeshNode(n);
|
|
if (info && info->public_key.size == 32) {
|
|
out = info->public_key;
|
|
return true;
|
|
}
|
|
#if WARM_NODE_COUNT > 0
|
|
if (warmStore.copyKey(n, out.bytes)) {
|
|
out.size = 32;
|
|
return true;
|
|
}
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
bool NodeDB::copyPublicKey(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out)
|
|
{
|
|
if (copyPublicKeyAuthoritative(n, out))
|
|
return true;
|
|
#if HAS_TRAFFIC_MANAGEMENT
|
|
// Last resort: a key the TrafficManagement NodeInfo cache learned from an observed frame
|
|
// for a node no longer in either NodeDB tier. This extends the pool of peers we can
|
|
// encrypt to. Keys here may be trust-on-first-use (see copyPublicKey's signerProven), the
|
|
// same first-contact trust NodeDB itself applies via updateUser().
|
|
if (trafficManagementModule && trafficManagementModule->copyPublicKey(n, out.bytes)) {
|
|
out.size = 32;
|
|
return true;
|
|
}
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
bool NodeDB::copyPublicKeyForDecrypt(NodeNum n, meshtastic_NodeInfoLite_public_key_t &out)
|
|
{
|
|
if (copyPublicKeyAuthoritative(n, out))
|
|
return true;
|
|
#if HAS_TRAFFIC_MANAGEMENT
|
|
// A cold-tier cache key backs an authenticated decrypt only when signer-proven; unverified TOFU
|
|
// cache keys must not. Outbound encryption still uses the opportunistic copyPublicKey().
|
|
bool signerProven = false;
|
|
if (trafficManagementModule && trafficManagementModule->copyPublicKey(n, out.bytes, &signerProven) && signerProven) {
|
|
out.size = 32;
|
|
return true;
|
|
}
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
bool NodeDB::isVerifiedSignerForKey(NodeNum n, const uint8_t *key32)
|
|
{
|
|
if (!key32)
|
|
return false;
|
|
// Hot store is authoritative when present; a node lives in the hot XOR warm tier, so if the
|
|
// hot store holds it the warm tier does not, and we decide entirely from the hot entry.
|
|
const meshtastic_NodeInfoLite *info = getMeshNode(n);
|
|
if (info)
|
|
return info->public_key.size == 32 && nodeInfoLiteHasXeddsaSigned(info) && memcmp(info->public_key.bytes, key32, 32) == 0;
|
|
#if WARM_NODE_COUNT > 0
|
|
uint8_t warmKey[32];
|
|
if (warmStore.copyKey(n, warmKey) && memcmp(warmKey, key32, 32) == 0)
|
|
return warmStore.isVerifiedSigner(n);
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
bool NodeDB::isKnownXeddsaSigner(NodeNum n)
|
|
{
|
|
// A node lives in the hot XOR warm tier, so the hot verdict is final when present.
|
|
const meshtastic_NodeInfoLite *info = getMeshNode(n);
|
|
if (info)
|
|
return nodeInfoLiteHasXeddsaSigned(info);
|
|
#if WARM_NODE_COUNT > 0
|
|
return warmStore.isVerifiedSigner(n);
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void NodeDB::commitRemoteKey(NodeNum n, const uint8_t key32[32], KeyCommitTrust trust)
|
|
{
|
|
if (!key32 || n == 0)
|
|
return;
|
|
// Local copy first: callers may pass the node's own key bytes back in (e.g. manual
|
|
// verification re-committing an already-stored key), and memcpy forbids overlap.
|
|
uint8_t key[32];
|
|
memcpy(key, key32, 32);
|
|
|
|
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(n);
|
|
if (!info)
|
|
return;
|
|
// Unconditional overwrite - deliberately NOT updateUser()'s "don't replace a known key" pin.
|
|
// That pin protects against unauthenticated NodeInfo broadcasts; the only callers here are
|
|
// possession/authority-proven (ManuallyVerified = user confirmed the key; AdminChannelProven =
|
|
// decrypted via the admin key with p->from bound into the AEAD nonce), i.e. exactly the paths
|
|
// meant to establish or rotate a key. Keep new call sites to that same trust bar.
|
|
memcpy(info->public_key.bytes, key, 32);
|
|
info->public_key.size = 32;
|
|
|
|
#if HAS_TRAFFIC_MANAGEMENT
|
|
// Write-through, mirroring updateUser()'s identity hook: without it the TrafficManagement
|
|
// NodeInfo cache diverges until the next hourly reconcile.
|
|
if (trafficManagementModule)
|
|
trafficManagementModule->onNodeKeyCommitted(n, key, trust == KeyCommitTrust::ManuallyVerified);
|
|
#endif
|
|
}
|
|
|
|
meshtastic_Config_DeviceConfig_Role NodeDB::getNodeRole(NodeNum n)
|
|
{
|
|
const meshtastic_NodeInfoLite *info = getMeshNode(n);
|
|
if (nodeInfoLiteHasUser(info))
|
|
return info->role;
|
|
#if WARM_NODE_COUNT > 0
|
|
// Hot-store miss: fall back to the role the warm tier cached at eviction.
|
|
uint8_t role = 0, prot = 0;
|
|
if (warmStore.lookupMeta(n, role, prot))
|
|
return static_cast<meshtastic_Config_DeviceConfig_Role>(role);
|
|
#endif
|
|
return meshtastic_Config_DeviceConfig_Role_CLIENT;
|
|
}
|
|
|
|
/// Find a node in our DB, create an empty NodeInfo if missing
|
|
meshtastic_NodeInfoLite *NodeDB::getOrCreateMeshNode(NodeNum n)
|
|
{
|
|
meshtastic_NodeInfoLite *lite = getMeshNode(n);
|
|
|
|
if (!lite) {
|
|
if (isFull()) {
|
|
LOG_INFO("Node database full with %i nodes and %u bytes free. Erasing oldest entry", numMeshNodes,
|
|
memGet.getFreeHeap());
|
|
// look for oldest node and erase it
|
|
uint32_t oldest = UINT32_MAX;
|
|
uint32_t oldestBoring = UINT32_MAX;
|
|
int oldestIndex = -1;
|
|
int oldestBoringIndex = -1;
|
|
for (int i = 1; i < numMeshNodes; i++) {
|
|
const meshtastic_NodeInfoLite *cand = &meshNodes->at(i);
|
|
const bool isFavoriteNode = nodeInfoLiteIsFavorite(cand);
|
|
const bool isIgnored = nodeInfoLiteIsIgnored(cand);
|
|
const bool isVerified = nodeInfoLiteIsKeyManuallyVerified(cand);
|
|
// Simply the oldest non-favorite, non-ignored, non-verified node
|
|
if (!isFavoriteNode && !isIgnored && !isVerified && cand->last_heard < oldest) {
|
|
oldest = cand->last_heard;
|
|
oldestIndex = i;
|
|
}
|
|
// The oldest "boring" node
|
|
if (!isFavoriteNode && !isIgnored && cand->public_key.size == 0 && cand->last_heard < oldestBoring) {
|
|
oldestBoring = cand->last_heard;
|
|
oldestBoringIndex = i;
|
|
}
|
|
}
|
|
// if we found a "boring" node, evict it
|
|
if (oldestBoringIndex != -1) {
|
|
oldestIndex = oldestBoringIndex;
|
|
}
|
|
|
|
if (oldestIndex != -1) {
|
|
const meshtastic_NodeInfoLite &evicted = meshNodes->at(oldestIndex);
|
|
#if WARM_NODE_COUNT > 0
|
|
// Demote to the warm tier so the identity (and crucially the
|
|
// PKI key) outlives the hot-store slot.
|
|
warmStore.absorb(evicted.num, evicted.last_heard, evicted.public_key.size == 32 ? evicted.public_key.bytes : NULL,
|
|
evicted.role, warmProtectedCategory(evicted), nodeInfoLiteHasXeddsaSigned(&evicted));
|
|
#endif
|
|
eraseNodeSatellites(evicted.num);
|
|
// Shove the remaining nodes down the chain
|
|
for (int i = oldestIndex; i < numMeshNodes - 1; i++) {
|
|
meshNodes->at(i) = meshNodes->at(i + 1);
|
|
}
|
|
(numMeshNodes)--;
|
|
}
|
|
}
|
|
// 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.
|
|
if (numMeshNodes >= MAX_NUM_NODES)
|
|
return NULL;
|
|
// Pre-size before append when run before nodeDBSelfCare() (boot keygen); else at() aborts on nRF52.
|
|
if (static_cast<size_t>(numMeshNodes) >= meshNodes->size())
|
|
meshNodes->resize(numMeshNodes + 1);
|
|
// add the node at the end
|
|
lite = &meshNodes->at((numMeshNodes)++);
|
|
|
|
// everything is missing except the nodenum
|
|
memset(lite, 0, sizeof(*lite));
|
|
lite->num = n;
|
|
#if WARM_NODE_COUNT > 0
|
|
// Re-admission: restore what the warm tier kept for this node
|
|
WarmNodeEntry warm;
|
|
if (warmStore.take(n, warm)) {
|
|
lite->last_heard = warmTimeOf(warm); // mask off the stolen metadata bits
|
|
// Restore the role the warm tier cached, so re-admission isn't stuck at CLIENT
|
|
// until the next NodeInfo arrives.
|
|
lite->role = static_cast<meshtastic_Config_DeviceConfig_Role>(warmRoleOf(warm));
|
|
// Restore the signer bit too: it is learned from verified traffic, not from
|
|
// NodeInfo, so a round trip through the warm tier must not relearn it from zero.
|
|
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, warmSignerOf(warm));
|
|
if (!memfll(warm.public_key, 0, sizeof(warm.public_key))) {
|
|
lite->public_key.size = 32;
|
|
memcpy(lite->public_key.bytes, warm.public_key, 32);
|
|
}
|
|
if (warmProtOf(warm) == static_cast<uint8_t>(WarmProtected::XeddsaSigner))
|
|
nodeInfoLiteSetBit(lite, NODEINFO_BITFIELD_HAS_XEDDSA_SIGNED_MASK, true);
|
|
LOG_MIGRATION("Rehydrated node 0x%08x from warm tier (key=%d)", n, lite->public_key.size == 32);
|
|
}
|
|
#endif
|
|
#if HAS_TRAFFIC_MANAGEMENT
|
|
// Name rehydration: the warm tier keeps a node's key but not its name, so a re-admitted
|
|
// long-tail node is nameless until its next NodeInfo. The TrafficManagement NodeInfo
|
|
// cache is much larger and often still holds the full User. Restore it - but only when
|
|
// its cached key matches the key we just restored from warm, so a name never attaches to
|
|
// a different identity than the one we encrypt to. No-op without the TMM NodeInfo cache
|
|
// or when no key is present (key-matched by design). CopyUserToNodeInfoLite sets only the
|
|
// user-related bits, so the warm-restored signer bit survives.
|
|
if (lite->public_key.size == 32 && !nodeInfoLiteHasUser(lite) && trafficManagementModule) {
|
|
meshtastic_User tmmUser = meshtastic_User_init_zero;
|
|
if (trafficManagementModule->copyUser(n, tmmUser) && tmmUser.public_key.size == 32 &&
|
|
memcmp(tmmUser.public_key.bytes, lite->public_key.bytes, 32) == 0) {
|
|
TypeConversions::CopyUserToNodeInfoLite(lite, tmmUser);
|
|
LOG_INFO("Rehydrated node 0x%08x identity from TMM NodeInfo cache", n);
|
|
}
|
|
}
|
|
#endif
|
|
LOG_INFO("Adding node to database with %i nodes and %u bytes free!", numMeshNodes, memGet.getFreeHeap());
|
|
}
|
|
|
|
return lite;
|
|
}
|
|
|
|
/// Sometimes we will have Position objects that only have a time, so check for
|
|
/// valid lat/lon
|
|
bool NodeDB::hasValidPosition(const meshtastic_NodeInfoLite *n)
|
|
{
|
|
if (!n)
|
|
return false;
|
|
if (n->num == getNodeNum()) {
|
|
return localPosition.latitude_i != 0 || localPosition.longitude_i != 0;
|
|
}
|
|
meshtastic_PositionLite pos;
|
|
return copyNodePosition(n->num, pos) && (pos.latitude_i != 0 || pos.longitude_i != 0);
|
|
}
|
|
|
|
/// If we have a node / user and they report is_licensed = true
|
|
/// we consider them licensed
|
|
UserLicenseStatus NodeDB::getLicenseStatus(uint32_t nodeNum)
|
|
{
|
|
const meshtastic_NodeInfoLite *info = getMeshNode(nodeNum);
|
|
if (!nodeInfoLiteHasUser(info))
|
|
return UserLicenseStatus::NotKnown;
|
|
return nodeInfoLiteIsLicensed(info) ? UserLicenseStatus::Licensed : UserLicenseStatus::NotLicensed;
|
|
}
|
|
|
|
#if !defined(MESHTASTIC_EXCLUDE_PKI)
|
|
bool NodeDB::checkLowEntropyPublicKey(const meshtastic_Config_SecurityConfig_public_key_t &keyToTest)
|
|
{
|
|
if (keyToTest.size == 32) {
|
|
uint8_t keyHash[32] = {0};
|
|
memcpy(keyHash, keyToTest.bytes, keyToTest.size);
|
|
crypto->hash(keyHash, 32);
|
|
for (uint16_t i = 0; i < sizeof(LOW_ENTROPY_HASHES) / sizeof(LOW_ENTROPY_HASHES[0]); i++) {
|
|
if (memcmp(keyHash, LOW_ENTROPY_HASHES[i], sizeof(LOW_ENTROPY_HASHES[0])) == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
bool NodeDB::generateCryptoKeyPair(const uint8_t *privateKey)
|
|
{
|
|
#if !(MESHTASTIC_EXCLUDE_PKI_KEYGEN || MESHTASTIC_EXCLUDE_PKI)
|
|
// Generate identity keys once a LoRa region is set. Licensed operation still needs the identity
|
|
// key for plaintext signatures, even though the key is never used for PKI encryption.
|
|
bool regionBlocksKeygen = config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
|
#if ARCH_PORTDUINO
|
|
if (portduino_config.lora_module == use_simradio)
|
|
regionBlocksKeygen = false;
|
|
#endif
|
|
if (regionBlocksKeygen) {
|
|
return false;
|
|
}
|
|
|
|
bool keygenSuccess = false;
|
|
// Record whether the stored key is a known compromised/low-entropy key so main.cpp can warn the
|
|
// user. A detected low-entropy key is regenerated below, but the flag stays set so the
|
|
// "Compromised keys were detected and regenerated" notification still fires.
|
|
keyIsLowEntropy = checkLowEntropyPublicKey(config.security.public_key);
|
|
|
|
// If a specific private key was provided, use it
|
|
if (privateKey != nullptr) {
|
|
LOG_INFO("Using provided private key for PKI");
|
|
memcpy(config.security.private_key.bytes, privateKey, 32);
|
|
config.security.private_key.size = 32;
|
|
config.security.public_key.size = 32;
|
|
|
|
// Generate public key from the provided private key
|
|
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
|
keygenSuccess = true;
|
|
} else {
|
|
LOG_ERROR("Failed to generate public key from provided private key");
|
|
return false;
|
|
}
|
|
}
|
|
// Try to regenerate public key from existing private key if it's valid and not low entropy
|
|
else if (config.security.private_key.size == 32 && !keyIsLowEntropy) {
|
|
config.security.public_key.size = 32;
|
|
LOG_DEBUG("Regenerate PKI public key from existing private key");
|
|
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
|
keygenSuccess = true;
|
|
}
|
|
} else {
|
|
// Generate a new key pair
|
|
LOG_INFO("Generate new PKI keys");
|
|
config.security.public_key.size = 32;
|
|
config.security.private_key.size = 32;
|
|
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
|
keygenSuccess = true;
|
|
}
|
|
|
|
// Update sizes and copy to owner if successful
|
|
if (keygenSuccess) {
|
|
owner.public_key.size = 32;
|
|
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
|
|
|
|
// Set the DH private key for crypto operations
|
|
LOG_DEBUG("Set DH private key for crypto operations");
|
|
crypto->setDHPrivateKey(config.security.private_key.bytes);
|
|
|
|
if (createNewIdentity() && owner.is_licensed)
|
|
licensedIdentityMigrationPending = true;
|
|
}
|
|
return keygenSuccess;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool NodeDB::notifyPendingLicensedIdentityMigration()
|
|
{
|
|
if (!licensedIdentityMigrationPending || !service)
|
|
return false;
|
|
meshtastic_ClientNotification *notification = clientNotificationPool.allocZeroed();
|
|
if (!notification)
|
|
return false;
|
|
notification->level = meshtastic_LogRecord_Level_WARNING;
|
|
notification->time = getValidTime(RTCQualityFromNet);
|
|
snprintf(notification->message, sizeof(notification->message), "%s", LICENSED_IDENTITY_MIGRATION_WARNING);
|
|
service->sendClientNotification(notification);
|
|
licensedIdentityMigrationPending = false;
|
|
return true;
|
|
}
|
|
|
|
bool NodeDB::createNewIdentity()
|
|
{
|
|
uint32_t oldNodeNum = getNodeNum();
|
|
uint32_t newNodeNum = crc32Buffer(config.security.public_key.bytes, config.security.public_key.size);
|
|
|
|
// If the key hasn't changed, nothing to do
|
|
if (newNodeNum == oldNodeNum)
|
|
return false;
|
|
|
|
// Remove the old node entry entirely rather than retiring it in place. Flagging the old identity as
|
|
// ignored (the previous behavior) left a keyless ghost of ourselves that survived cleanup/eviction, was
|
|
// still streamed to clients, and made any DM/admin aimed at it fail forever with PKI_SEND_FAIL_PUBLIC_KEY.
|
|
// removeNodeByNum() drops the lite entry, its satellite stores, and the warm-tier copy.
|
|
if (getMeshNode(oldNodeNum) != NULL) {
|
|
LOG_DEBUG("Old node num %u is now %u, removing stale identity", oldNodeNum, newNodeNum);
|
|
removeNodeByNum(oldNodeNum);
|
|
} else {
|
|
// Lite entry already absent: drop any orphaned satellite-store entries directly.
|
|
eraseNodeSatellites(oldNodeNum);
|
|
}
|
|
|
|
myNodeInfo.my_node_num = newNodeNum;
|
|
|
|
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum());
|
|
if (!info)
|
|
return false;
|
|
TypeConversions::CopyUserToNodeInfoLite(info, owner);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NodeDB::backupPreferences(meshtastic_AdminMessage_BackupLocation location)
|
|
{
|
|
bool success = false;
|
|
lastBackupAttempt = millis();
|
|
#ifdef FSCom
|
|
if (location == meshtastic_AdminMessage_BackupLocation_FLASH) {
|
|
meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero;
|
|
backup.version = DEVICESTATE_CUR_VER;
|
|
backup.timestamp = getValidTime(RTCQuality::RTCQualityDevice, false);
|
|
backup.has_config = true;
|
|
backup.config = config;
|
|
backup.has_module_config = true;
|
|
backup.module_config = moduleConfig;
|
|
backup.has_channels = true;
|
|
backup.channels = channelFile;
|
|
backup.has_owner = true;
|
|
backup.owner = owner;
|
|
|
|
size_t backupSize;
|
|
pb_get_encoded_size(&backupSize, meshtastic_BackupPreferences_fields, &backup);
|
|
|
|
spiLock->lock();
|
|
FSCom.mkdir("/backups");
|
|
spiLock->unlock();
|
|
success = saveProto(backupFileName, backupSize, &meshtastic_BackupPreferences_msg, &backup);
|
|
|
|
if (success) {
|
|
LOG_INFO("Saved backup preferences");
|
|
} else {
|
|
LOG_ERROR("Failed to save backup preferences to file");
|
|
}
|
|
} else if (location == meshtastic_AdminMessage_BackupLocation_SD) {
|
|
// TODO: After more mainline SD card support
|
|
}
|
|
#endif
|
|
return success;
|
|
}
|
|
|
|
bool NodeDB::restorePreferences(meshtastic_AdminMessage_BackupLocation location, int restoreWhat)
|
|
{
|
|
bool success = false;
|
|
#ifdef FSCom
|
|
if (location == meshtastic_AdminMessage_BackupLocation_FLASH) {
|
|
spiLock->lock();
|
|
if (!FSCom.exists(backupFileName)) {
|
|
spiLock->unlock();
|
|
LOG_WARN("Could not restore. No backup file found");
|
|
return false;
|
|
} else {
|
|
spiLock->unlock();
|
|
}
|
|
meshtastic_BackupPreferences backup = meshtastic_BackupPreferences_init_zero;
|
|
success = loadProto(backupFileName, meshtastic_BackupPreferences_size, sizeof(meshtastic_BackupPreferences),
|
|
&meshtastic_BackupPreferences_msg, &backup);
|
|
if (success) {
|
|
if (restoreWhat & SEGMENT_CONFIG) {
|
|
config = backup.config;
|
|
LOG_DEBUG("Restored config");
|
|
}
|
|
if (restoreWhat & SEGMENT_MODULECONFIG) {
|
|
moduleConfig = backup.module_config;
|
|
LOG_DEBUG("Restored module config");
|
|
}
|
|
if (restoreWhat & SEGMENT_DEVICESTATE) {
|
|
devicestate.owner = backup.owner;
|
|
LOG_DEBUG("Restored device state");
|
|
}
|
|
if (restoreWhat & SEGMENT_CHANNELS) {
|
|
channelFile = backup.channels;
|
|
LOG_DEBUG("Restored channels");
|
|
}
|
|
|
|
if (owner.is_licensed && channels.ensureLicensedOperation()) {
|
|
restoreWhat |= SEGMENT_CHANNELS;
|
|
LOG_WARN("Licensed operation sanitized restored channel encryption/admin access");
|
|
}
|
|
if (restoreWhat & SEGMENT_CHANNELS)
|
|
channels.onConfigChanged();
|
|
|
|
success = saveToDisk(restoreWhat);
|
|
if (success) {
|
|
LOG_INFO("Restored preferences from backup");
|
|
} else {
|
|
LOG_ERROR("Failed to save restored preferences to flash");
|
|
}
|
|
} else {
|
|
LOG_ERROR("Failed to restore preferences from backup file");
|
|
}
|
|
} else if (location == meshtastic_AdminMessage_BackupLocation_SD) {
|
|
// TODO: After more mainline SD card support
|
|
}
|
|
#endif
|
|
return success;
|
|
}
|
|
|
|
/// Record an error that should be reported via analytics
|
|
void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, const char *filename)
|
|
{
|
|
if (filename) {
|
|
LOG_ERROR("NOTE! Record critical error %d at %s:%lu", code, filename, address);
|
|
} else {
|
|
LOG_ERROR("NOTE! Record critical error %d, address=0x%lx", code, address);
|
|
}
|
|
|
|
// Record error to DB
|
|
error_code = code;
|
|
error_address = address;
|
|
|
|
// Currently portuino is mostly used for simulation. Make sure the user notices something really bad happened
|
|
#ifdef ARCH_PORTDUINO
|
|
LOG_ERROR("A critical failure occurred");
|
|
// TODO: Determine if other critical errors should also cause an immediate exit
|
|
if (code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_RECOVERABLE ||
|
|
code == meshtastic_CriticalErrorCode_FLASH_CORRUPTION_UNRECOVERABLE)
|
|
exit(2);
|
|
#endif
|
|
}
|