#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 #include "xmodem.h" #include #include #include #include #include #include #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 #include #endif #ifdef ARCH_PORTDUINO #include "modules/StoreForwardModule.h" #include "platform/portduino/PortduinoGlue.h" #endif #ifdef ARCH_NRF52 #include #include #endif #ifdef ARCH_RP2040 #include #endif #if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI #include #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 *s_decodePositionsTarget = nullptr; #endif #if !MESHTASTIC_EXCLUDE_TELEMETRYDB std::map *s_decodeTelemetryTarget = nullptr; #endif #if !MESHTASTIC_EXCLUDE_ENVIRONMENTDB std::map *s_decodeEnvironmentTarget = nullptr; #endif #if !MESHTASTIC_EXCLUDE_STATUSDB std::map *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(field); switch (iter->tag) { case meshtastic_NodeDatabase_nodes_tag: { if (ostream) { const auto *vec = static_cast *>(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 *>(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 *>(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 *>(iter->pData); vec->push_back(entry); } } return true; } case meshtastic_NodeDatabase_telemetry_tag: { if (ostream) { const auto *vec = static_cast *>(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 *>(iter->pData); vec->push_back(entry); } } return true; } case meshtastic_NodeDatabase_status_tag: { if (ostream) { const auto *vec = static_cast *>(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 *>(iter->pData); vec->push_back(entry); } } return true; } case meshtastic_NodeDatabase_environment_tag: { if (ostream) { const auto *vec = static_cast *>(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 *>(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); } /** * 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 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 std::vector snapshotSatelliteNodeNums(const Map &map, NodeNum exclude) { std::vector 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 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 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 // 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(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 > 0; 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; 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__* 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 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 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 NodeDB::snapshotPositionNodeNums(NodeNum exclude) const { #if MESHTASTIC_EXCLUDE_POSITIONDB (void)exclude; return {}; #else concurrency::LockGuard guard(&satelliteMutex); return snapshotSatelliteNodeNums(nodePositions, exclude); #endif } std::vector NodeDB::snapshotTelemetryNodeNums(NodeNum exclude) const { #if MESHTASTIC_EXCLUDE_TELEMETRYDB (void)exclude; return {}; #else concurrency::LockGuard guard(&satelliteMutex); return snapshotSatelliteNodeNums(nodeTelemetry, exclude); #endif } std::vector NodeDB::snapshotEnvironmentNodeNums(NodeNum exclude) const { #if MESHTASTIC_EXCLUDE_ENVIRONMENTDB (void)exclude; return {}; #else concurrency::LockGuard guard(&satelliteMutex); return snapshotSatelliteNodeNums(nodeEnvironment, exclude); #endif } std::vector 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 (n.bitfield & (NODEINFO_BITFIELD_IS_FAVORITE_MASK | NODEINFO_BITFIELD_IS_IGNORED_MASK | NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK)) return static_cast(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(WarmProtected::Role); return static_cast(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; } // 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; 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 (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); } // 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 (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 (int i = 0; i < 5; 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); } } } #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 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[] = {configFileName, moduleConfigFileName, channelFileName, 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) { // 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(&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 (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; #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" 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); } 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) { 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"); } } 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); meshtastic_NodeInfoLite *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::copyPublicKey(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; } 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(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(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(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); } LOG_MIGRATION("Rehydrated node 0x%08x from warm tier (key=%d)", n, lite->public_key.size == 32); } #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) // Only generate keys for non-licensed users and if the LoRa region is set. The native simulator // boots region-UNSET but still needs a keypair so PKI-encrypted DMs work between sim nodes, so // allow keygen there regardless of region. bool regionBlocksKeygen = config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET; #if ARCH_PORTDUINO if (portduino_config.lora_module == use_simradio) regionBlocksKeygen = false; #endif if (owner.is_licensed || 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); // Conditionally create new identity based on parameter createNewIdentity(); } return keygenSuccess; #else return false; #endif } 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"); } 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 }