mirror of
https://github.com/meshtastic/firmware.git
synced 2026-05-30 19:55:48 -04:00
* stm32wl: check HAL_FLASH_Unlock() return in _internal_flash_erase _internal_flash_prog already checks HAL_FLASH_Unlock() and returns LFS_ERR_IO on failure. _internal_flash_erase discarded the return value, proceeding to erase even if the flash was not unlocked. Apply the same check for consistency and safety. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * stm32wl: fix _internal_flash_prog to abort on first write error Previously the programming loop continued to the next doubleword after HAL_FLASH_Program() failed, potentially writing to invalid addresses and returning a misleading error code only at the end (last iteration). HAL_FLASH_Lock() was also skipped on the mid-loop early return path. - Move bounds check before the loop (validate full range at once) - Break on first HAL error so subsequent doublewords are not written - Move HAL_FLASH_Lock() after the loop so it always runs Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * stm32wl: clear stale flash SR error flags before erase and program Stale error flags in FLASH->SR from a previous failed operation can cause HAL_FLASH_Program() or HAL_FLASHEx_Erase() to return HAL_ERROR immediately without attempting the operation. Add __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_ALL_ERRORS) after each HAL_FLASH_Unlock() in both _internal_flash_prog and _internal_flash_erase to ensure a clean state before each operation. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * stm32wl: reject flash prog writes not aligned to 8-byte doubleword The STM32WL HAL minimum write unit is one 64-bit doubleword (8 bytes). _internal_flash_prog silently truncated any trailing bytes when size % 8 != 0 because dw_count = size / 8 drops the remainder. Return LFS_ERR_INVAL early so LittleFS sees the error rather than a silent short write. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(nrf52,fs): use atomic SafeFile rename instead of direct write NRF52 was bypassing the .tmp/readback/rename path entirely — openFile() deleted the target file and wrote directly to it, and close() returned true without verifying the write or renaming anything. Adafruit_LittleFS::rename() calls lfs_rename() directly (confirmed at Adafruit_LittleFS.cpp:205). Remove both ARCH_NRF52 guards so NRF52 follows the same write-to-.tmp → readback-hash → rename path used by all other platforms. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(admin): skip uiconfig.proto save on devices without a screen handleStoreDeviceUIConfig() was writing /prefs/uiconfig.proto unconditionally. MenuHandler.cpp is already gated behind #if HAS_SCREEN, so there is no path that populates UI config on screen-less platforms. Guard the save with #if HAS_SCREEN to avoid wasting a flash block on devices that will never use it. The read path (handleGetDeviceUIConfig) does not touch the filesystem and needs no change. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fs: enable format-on-retry for all platforms in saveToDisk The FSCom.format() call on save failure was guarded to ARCH_NRF52 with a comment that other platforms were not ready (bug #4184). STM32WL was added to the guard in a prior commit. All platforms now expose format semantics and the retry logic is identical — remove the guard. To keep NodeDB.cpp platform-agnostic and fix a CI failure on native-tft (portduino's fs::FS has no format() method), introduce fsFormat() in FSCommon as the single call-site for all callers: - Embedded (ESP32, NRF52, STM32WL, RP2040): delegates to FSCom.format() - Portduino: rmDir("/prefs") + FSBegin() (a no-op on portduino). rmDir("/prefs") is already called unconditionally by factoryReset() (NodeDB.cpp:504), so both primitives are proven on portduino. Replace both direct FSCom.format() calls in NodeDB.cpp with fsFormat(). Note: we do not run portduino locally — portduino/native build testers please verify the format-on-retry path. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * DO NOT MERGE: nrf52(fs): add File() default constructor bound to InternalFS Adds File() to the Adafruit LittleFS File class (in the Meshtastic Adafruit_nRF52_Arduino fork), delegating to File(InternalFS). This matches the default-constructible File API on all other platforms. The constructor is implemented in Adafruit_LittleFS_File.cpp rather than inline in the header to avoid a circular include between Adafruit_LittleFS_File.h and InternalFileSystem.h. FOLLOW-UP REQUIRED: nrf52.ini points to a commit SHA on the mesh-malaysia/Adafruit_nRF52_Arduino fork instead of the upstream meshtastic framework. Once meshtastic/Adafruit_nRF52_Arduino#5 is merged, revert nrf52.ini to point back to the upstream meshtastic framework URL. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * stm32wl(fs): add File() default constructor and document LFS tunables Adds File() to STM32_LittleFS_Namespace::File, delegating to File(InternalFS). Implemented in the .cpp to avoid a circular include between STM32_LittleFS_File.h (which cannot include LittleFS.h) and the InternalFS extern declaration. This matches the File API on ESP32/RP2040/Portduino and is a prerequisite for removing the ARCH_STM32WL guard in xmodem.h. No behavior change — the constructor leaves the file in the same closed/unattached state as File(InternalFS) would. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fs: remove arch-specific ifdefs from FSCommon, SafeFile, xmodem Now that NRF52 and STM32WL have File() default constructors and NRF52 has working atomic SafeFile rename, the capability gaps are closed. Remove all per-arch guards across the shared FS layer: FSCommon.cpp — renameFile(): Use FSCom.rename() on all platforms. Adafruit_LittleFS::rename() calls lfs_rename() directly (Adafruit_LittleFS.cpp:205). The copy+delete fallback on NRF52/RP2040 was never necessary. FSCommon.cpp — getFiles(): Replace four ARCH_ESP32 guards with a single filepath pointer at the top of the loop (file.path() on ESP32, file.name() elsewhere). Fix strcpy(fileInfo.file_name, filepath): bounded to sizeof(fileInfo.file_name)-1 with explicit NUL termination to prevent overflow of the 228-byte meshtastic_FileInfo::file_name array. FSCommon.cpp — listDir(): Same filepath pointer approach. NRF52/STM32WL were in an else-branch that only logged but never deleted — now all platforms follow the unified del path. 12 guards → 2. Fix three strncpy(buffer, ..., sizeof(buffer)) calls that did not NUL-terminate when source length >= sizeof(buffer) (255 bytes). Add explicit buffer[sizeof(buffer)-1] = '\0' after each. FSCommon.cpp — rmDir(): Use listDir(del=true) everywhere. The ARCH_NRF52 rmdir_r() path and the ARCH_ESP32|RP2040|PORTDUINO listDir() path collapse to one line. SafeFile.cpp: ARCH_NRF52 bypass removed (handled in preceding commit). xmodem.h: File file; now works on all platforms via default constructors added in the two preceding commits. Remaining #ifdef ARCH_ESP32 in FSCommon.cpp: exactly 4, all for the file.path() vs file.name() API difference (ESP32 Arduino LittleFS returns the full path; all others return only the name). That difference lives in the framework and cannot be closed here. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * stm32wl(fs): add write-behind page cache, reduce virtual block size and FS reservation (FORMAT BREAK) Adds a write-behind (RMW) page cache to the STM32WL LittleFS driver, modelled after the NRF52 Adafruit approach (flash_cache.c). This allows LFS to use 256-byte virtual blocks backed by 2048-byte physical pages: the erase/prog callbacks accumulate changes in a 2 KB RAM buffer; the sync callback (and page eviction on page-change) flushes with a single HAL physical-erase + doubleword-program pass. LFS tunables changed (FORMAT BREAK — superblock parameters): block_size: 2048 B → 256 B (8 virtual blocks per physical page) read_size: 2048 B → 256 B (= block_size) prog_size: 2048 B → 256 B (= block_size; hardware min is 8 B) block_count: 112 → 80 (14 phys pages → 10 phys pages = 20 KiB) Benefits: - Internal fragmentation: max 2047 B/file → max 255 B/file - Heap per open LFS file: ~4 KB → 512 B (prog + read buffers) - Code flash headroom: 6.7 KB → ~14.1 KB (+7.4 KB) - Block budget: 80 virtual blocks, worst-case peak ~20, ~60 free Updates board_upload.maximum_size in wio-e5/platformio.ini from 233472 (256 KB − 28 KB) to 241664 (256 KB − 20 KB) to match the reduced FS reservation. Justification for the format break: the prior STM32WL firmware had several flash write bugs fixed earlier in this series (missing error flag clearing, no abort on first write failure, unaligned write acceptance). These bugs very likely caused silent config corruption on deployed devices. The format break should be treated as an enhancement: it provides a clean, reliably-written starting point. Users will need to reconfigure their device once after this update. Correctness fixes applied to the cache implementation: - alignas(8) on _page_cache: the buffer was uint8_t[] (alignment 1) but _flash_cache_flush casts it to const uint64_t* — undefined behaviour per C++ standard, potential Cortex-M hardfault. alignas(8) guarantees the required alignment for the doubleword cast. - HAL_FLASH_Lock() return value: was discarded. Now assigned to lock_rc and propagated into rc if prior writes succeeded, so LFS sees the error rather than a false success. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> * stm32wl(fs): reduce FS reservation from 10 pages to 7 pages (FORMAT BREAK) Reduces LFS_FLASH_TOTAL_SIZE from 10 × 2 KiB pages (20 KiB) to 7 × 2 KiB pages (14 KiB), freeing 6 KiB for firmware. board_upload.maximum_size updated accordingly across all STM32WL variants: 241664 (256 KiB - 20 KiB) → 247808 (256 KiB - 14 KiB) This is a FORMAT BREAK: existing filesystems must be erased before use. Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Andrew Yong <me@ndoo.sg> * fix(fs): return false in renameFile() when FSCom is not defined Avoids undefined behavior and -Wreturn-type warnings in configurations that compile FSCommon.cpp without a filesystem backend. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Signed-off-by: Andrew Yong <me@ndoo.sg> Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2307 lines
91 KiB
C++
2307 lines
91 KiB
C++
#include "configuration.h"
|
|
#if !MESHTASTIC_EXCLUDE_GPS
|
|
#include "GPS.h"
|
|
#endif
|
|
#include "../detect/ScanI2C.h"
|
|
#include "Channels.h"
|
|
#include "CryptoEngine.h"
|
|
#include "Default.h"
|
|
#include "FSCommon.h"
|
|
#include "MeshRadio.h"
|
|
#include "MeshService.h"
|
|
#include "NodeDB.h"
|
|
#include "PacketHistory.h"
|
|
#include "PowerFSM.h"
|
|
#include "RTC.h"
|
|
#include "RadioInterface.h"
|
|
#include "Router.h"
|
|
#include "SPILock.h"
|
|
#include "SafeFile.h"
|
|
#include "TransmitHistory.h"
|
|
#include "TypeConversions.h"
|
|
#include "error.h"
|
|
#include "main.h"
|
|
#include "mesh-pb-constants.h"
|
|
#include "meshUtils.h"
|
|
#include "modules/NeighborInfoModule.h"
|
|
#include <ErriezCRC32.h>
|
|
#include <algorithm>
|
|
#include <pb_decode.h>
|
|
#include <pb_encode.h>
|
|
#include <power/PowerHAL.h>
|
|
#include <vector>
|
|
|
|
#ifdef ARCH_ESP32
|
|
#if HAS_WIFI
|
|
#include "mesh/wifi/WiFiAPClient.h"
|
|
#endif
|
|
#include "SPILock.h"
|
|
#include "modules/StoreForwardModule.h"
|
|
#include <Preferences.h>
|
|
#include <esp_efuse.h>
|
|
#include <esp_efuse_table.h>
|
|
#include <nvs_flash.h>
|
|
#include <soc/efuse_reg.h>
|
|
#include <soc/soc.h>
|
|
#endif
|
|
|
|
#ifdef ARCH_PORTDUINO
|
|
#include "modules/StoreForwardModule.h"
|
|
#include "platform/portduino/PortduinoGlue.h"
|
|
#endif
|
|
|
|
#ifdef ARCH_NRF52
|
|
#include <bluefruit.h>
|
|
#include <utility/bonding.h>
|
|
#endif
|
|
|
|
#if defined(ARCH_ESP32) && !MESHTASTIC_EXCLUDE_WIFI
|
|
#include <MeshtasticOTA.h>
|
|
#endif
|
|
|
|
NodeDB *nodeDB = nullptr;
|
|
|
|
// we have plenty of ram so statically alloc this tempbuf (for now)
|
|
EXT_RAM_BSS_ATTR meshtastic_DeviceState devicestate;
|
|
meshtastic_MyNodeInfo &myNodeInfo = devicestate.my_node;
|
|
meshtastic_NodeDatabase nodeDatabase;
|
|
meshtastic_LocalConfig config;
|
|
meshtastic_DeviceUIConfig uiconfig{.screen_brightness = 153, .screen_timeout = 30};
|
|
meshtastic_LocalModuleConfig moduleConfig;
|
|
meshtastic_ChannelFile channelFile;
|
|
|
|
#ifdef USERPREFS_USE_ADMIN_KEY_0
|
|
static unsigned char userprefs_admin_key_0[] = USERPREFS_USE_ADMIN_KEY_0;
|
|
#endif
|
|
#ifdef USERPREFS_USE_ADMIN_KEY_1
|
|
static unsigned char userprefs_admin_key_1[] = USERPREFS_USE_ADMIN_KEY_1;
|
|
#endif
|
|
#ifdef USERPREFS_USE_ADMIN_KEY_2
|
|
static unsigned char userprefs_admin_key_2[] = USERPREFS_USE_ADMIN_KEY_2;
|
|
#endif
|
|
|
|
#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
|
|
|
|
bool meshtastic_NodeDatabase_callback(pb_istream_t *istream, pb_ostream_t *ostream, const pb_field_iter_t *field)
|
|
{
|
|
if (ostream) {
|
|
std::vector<meshtastic_NodeInfoLite> const *vec = (std::vector<meshtastic_NodeInfoLite> *)field->pData;
|
|
for (auto item : *vec) {
|
|
if (!pb_encode_tag_for_field(ostream, field))
|
|
return false;
|
|
pb_encode_submessage(ostream, meshtastic_NodeInfoLite_fields, &item);
|
|
}
|
|
}
|
|
if (istream) {
|
|
meshtastic_NodeInfoLite node; // this gets good data
|
|
std::vector<meshtastic_NodeInfoLite> *vec = (std::vector<meshtastic_NodeInfoLite> *)field->pData;
|
|
|
|
if (istream->bytes_left && pb_decode(istream, meshtastic_NodeInfoLite_fields, &node))
|
|
vec->push_back(node);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/** 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;
|
|
|
|
// FIXME - move this somewhere else
|
|
extern void getMacAddr(uint8_t *dmac);
|
|
|
|
/**
|
|
*
|
|
* 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;
|
|
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;
|
|
// Get device unique id
|
|
#if defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) || defined(CONFIG_IDF_TARGET_ESP32C6)
|
|
uint32_t unique_id[4];
|
|
// ESP32 factory burns a unique id in efuse for S2+ series and evidently C3+ series
|
|
// This is used for HMACs in the esp-rainmaker AIOT platform and seems to be a good choice for us
|
|
esp_err_t err = esp_efuse_read_field_blob(ESP_EFUSE_OPTIONAL_UNIQUE_ID, unique_id, sizeof(unique_id) * 8);
|
|
if (err == ESP_OK) {
|
|
memcpy(myNodeInfo.device_id.bytes, unique_id, sizeof(unique_id));
|
|
myNodeInfo.device_id.size = 16;
|
|
} else {
|
|
LOG_WARN("Failed to read unique id from efuse");
|
|
}
|
|
#elif defined(ARCH_NRF52)
|
|
// Nordic applies a FIPS compliant Random ID to each chip at the factory
|
|
// We concatenate the device address to the Random ID to create a unique ID for now
|
|
// This will likely utilize a crypto module in the future
|
|
uint64_t device_id_start = ((uint64_t)NRF_FICR->DEVICEID[1] << 32) | NRF_FICR->DEVICEID[0];
|
|
uint64_t device_id_end = ((uint64_t)NRF_FICR->DEVICEADDR[1] << 32) | NRF_FICR->DEVICEADDR[0];
|
|
memcpy(myNodeInfo.device_id.bytes, &device_id_start, sizeof(device_id_start));
|
|
memcpy(myNodeInfo.device_id.bytes + sizeof(device_id_start), &device_id_end, sizeof(device_id_end));
|
|
myNodeInfo.device_id.size = 16;
|
|
// Uncomment below to print the device id
|
|
#elif ARCH_PORTDUINO
|
|
if (portduino_config.has_device_id) {
|
|
memcpy(myNodeInfo.device_id.bytes, portduino_config.device_id, 16);
|
|
myNodeInfo.device_id.size = 16;
|
|
}
|
|
#else
|
|
// FIXME - implement for other platforms
|
|
#endif
|
|
|
|
// if (myNodeInfo.device_id.size == 16) {
|
|
// std::string deviceIdHex;
|
|
// for (size_t i = 0; i < myNodeInfo.device_id.size; ++i) {
|
|
// char buf[3];
|
|
// snprintf(buf, sizeof(buf), "%02X", myNodeInfo.device_id.bytes[i]);
|
|
// deviceIdHex += buf;
|
|
// }
|
|
// LOG_DEBUG("Device ID (HEX): %s", deviceIdHex.c_str());
|
|
// }
|
|
|
|
// 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
|
|
// Note! We do this after loading saved settings, so that if somehow an invalid nodenum was stored in preferences we won't
|
|
// keep using that nodenum forever. Crummy guess at our nodenum (but we will check against the nodedb to avoid conflicts)
|
|
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)
|
|
|
|
if (!owner.is_licensed && config.lora.region != meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
|
bool keygenSuccess = false;
|
|
keyIsLowEntropy = checkLowEntropyPublicKey(config.security.public_key);
|
|
if (config.security.private_key.size == 32 && !keyIsLowEntropy) {
|
|
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
|
keygenSuccess = true;
|
|
}
|
|
} else {
|
|
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
|
keygenSuccess = true;
|
|
}
|
|
if (keygenSuccess) {
|
|
config.security.public_key.size = 32;
|
|
config.security.private_key.size = 32;
|
|
owner.public_key.size = 32;
|
|
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
|
|
}
|
|
}
|
|
#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);
|
|
}
|
|
#endif
|
|
// Include our owner in the node db under our nodenum
|
|
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(getNodeNum());
|
|
info->user = TypeConversions::ConvertToUserLite(owner);
|
|
info->has_user = true;
|
|
|
|
// 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
|
|
|
|
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;
|
|
}
|
|
|
|
// 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;
|
|
if (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;
|
|
info->has_position = true;
|
|
info->position = TypeConversions::ConvertToPositionLite(fixedGPS);
|
|
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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
// second, install default state (this will deal with the duplicate mac address issue)
|
|
installDefaultNodeDatabase();
|
|
installDefaultDeviceState();
|
|
installDefaultConfig(!eraseBleBonds); // Also preserve the private key if we're not erasing BLE bonds
|
|
installDefaultModuleConfig();
|
|
installDefaultChannels();
|
|
// third, write everything to disk
|
|
saveToDisk();
|
|
if (eraseBleBonds) {
|
|
LOG_INFO("Erase BLE bonds");
|
|
#ifdef ARCH_ESP32
|
|
// This will erase what's in NVS including ssl keys, persistent variables and ble pairing
|
|
nvs_flash_erase();
|
|
#endif
|
|
#ifdef ARCH_NRF52
|
|
LOG_INFO("Clear bluetooth bonds!");
|
|
bond_print_list(BLE_GAP_ROLE_PERIPH);
|
|
bond_print_list(BLE_GAP_ROLE_CENTRAL);
|
|
Bluefruit.Periph.clearBonds();
|
|
Bluefruit.Central.clearBonds();
|
|
#endif
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void NodeDB::installDefaultNodeDatabase()
|
|
{
|
|
LOG_DEBUG("Install default NodeDatabase");
|
|
nodeDatabase.version = DEVICESTATE_CUR_VER;
|
|
nodeDatabase.nodes = std::vector<meshtastic_NodeInfoLite>(MAX_NUM_NODES);
|
|
numMeshNodes = 0;
|
|
meshNodes = &nodeDatabase.nodes;
|
|
}
|
|
|
|
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_MODEM_PRESET
|
|
config.lora.modem_preset = USERPREFS_LORACONFIG_MODEM_PRESET;
|
|
#else
|
|
config.lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
|
|
#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)) && \
|
|
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(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7789_CS) || \
|
|
defined(HX8357_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_SPISSD1306) || \
|
|
defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR)
|
|
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
|
|
#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
|
|
#endif
|
|
|
|
#ifdef USERPREFS_NETWORK_WIFI_ENABLED
|
|
config.network.wifi_enabled = USERPREFS_NETWORK_WIFI_ENABLED;
|
|
#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();
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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)
|
|
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)
|
|
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
|
|
#endif
|
|
#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
|
|
#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;
|
|
|
|
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;
|
|
|
|
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
|
|
moduleConfig.telemetry.environment_update_interval = 0;
|
|
moduleConfig.telemetry.air_quality_interval = 0;
|
|
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();
|
|
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 && !node.is_favorite) {
|
|
node = meshtastic_NodeInfoLite();
|
|
} else {
|
|
numMeshNodes += 1;
|
|
}
|
|
};
|
|
} else {
|
|
LOG_INFO("Clearing node database - removing favorites");
|
|
std::fill(nodeDatabase.nodes.begin() + 1, nodeDatabase.nodes.end(), meshtastic_NodeInfoLite());
|
|
}
|
|
devicestate.has_rx_text_message = false;
|
|
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;
|
|
std::fill(nodeDatabase.nodes.begin() + numMeshNodes, nodeDatabase.nodes.begin() + numMeshNodes + 1,
|
|
meshtastic_NodeInfoLite());
|
|
LOG_DEBUG("NodeDB::removeNodeByNum purged %d entries. Save changes", removed);
|
|
saveNodeDatabaseToDisk();
|
|
}
|
|
|
|
void NodeDB::clearLocalPosition()
|
|
{
|
|
meshtastic_NodeInfoLite *node = getMeshNode(nodeDB->getNodeNum());
|
|
node->position.latitude_i = 0;
|
|
node->position.longitude_i = 0;
|
|
node->position.altitude = 0;
|
|
node->position.time = 0;
|
|
setLocalPosition(meshtastic_Position_init_default);
|
|
localPositionUpdatedSinceBoot = false;
|
|
}
|
|
|
|
void NodeDB::cleanupMeshDB()
|
|
{
|
|
int newPos = 0, removed = 0;
|
|
for (int i = 0; i < numMeshNodes; i++) {
|
|
if (meshNodes->at(i).has_user) {
|
|
if (meshNodes->at(i).user.public_key.size > 0) {
|
|
if (memfll(meshNodes->at(i).user.public_key.bytes, 0, meshNodes->at(i).user.public_key.size)) {
|
|
meshNodes->at(i).user.public_key.size = 0;
|
|
}
|
|
}
|
|
if (newPos != i)
|
|
meshNodes->at(newPos++) = meshNodes->at(i);
|
|
else
|
|
newPos++;
|
|
} else {
|
|
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;
|
|
devicestate.has_rx_text_message = 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
|
|
#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];
|
|
}
|
|
|
|
meshtastic_NodeInfoLite *found;
|
|
while (((found = getMeshNode(nodeNum)) && memcmp(found->user.macaddr, ourMacAddr, sizeof(ourMacAddr)) != 0) ||
|
|
(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%x is invalid or in use, by MAC ending in 0x%02x%02x vs our 0x%02x%02x, so "
|
|
"trying for 0x%x",
|
|
nodeNum, found->user.macaddr[4], found->user.macaddr[5], ourMacAddr[4], ourMacAddr[5], candidate);
|
|
nodeNum = candidate;
|
|
}
|
|
LOG_DEBUG("Use nodenum 0x%x ", 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 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) // contains a vector object
|
|
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;
|
|
}
|
|
|
|
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;
|
|
|
|
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
|
|
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
|
|
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 {
|
|
meshNodes = &nodeDatabase.nodes;
|
|
numMeshNodes = nodeDatabase.nodes.size();
|
|
LOG_INFO("Loaded saved nodedatabase version %d, with nodes count: %d", nodeDatabase.version, nodeDatabase.nodes.size());
|
|
}
|
|
|
|
if (numMeshNodes > MAX_NUM_NODES) {
|
|
LOG_WARN("Node count %d exceeds MAX_NUM_NODES %d, truncating", numMeshNodes, MAX_NUM_NODES);
|
|
numMeshNodes = MAX_NUM_NODES;
|
|
}
|
|
meshNodes->resize(MAX_NUM_NODES);
|
|
|
|
// 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 (us && us->has_user) {
|
|
LOG_WARN("Restoring owner fields (long_name/short_name/is_licensed/is_unmessagable) from NodeDB for our node 0x%08x",
|
|
us->num);
|
|
memcpy(owner.long_name, us->user.long_name, sizeof(owner.long_name));
|
|
owner.long_name[sizeof(owner.long_name) - 1] = '\0';
|
|
memcpy(owner.short_name, us->user.short_name, sizeof(owner.short_name));
|
|
owner.short_name[sizeof(owner.short_name) - 1] = '\0';
|
|
owner.is_licensed = us->user.is_licensed;
|
|
owner.has_is_unmessagable = us->user.has_is_unmessagable;
|
|
owner.is_unmessagable = us->user.is_unmessagable;
|
|
|
|
// Save the recovered owner to device state on disk
|
|
saveToDisk(SEGMENT_DEVICESTATE);
|
|
}
|
|
} else {
|
|
LOG_INFO("Loaded saved devicestate version %d", devicestate.version);
|
|
}
|
|
|
|
state = loadProto(configFileName, meshtastic_LocalConfig_size, sizeof(meshtastic_LocalConfig), &meshtastic_LocalConfig_msg,
|
|
&config);
|
|
if (state != LoadFileResult::LOAD_SUCCESS) {
|
|
installDefaultConfig(); // Our in RAM copy might now be corrupt
|
|
} 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
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
#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
|
|
}
|
|
|
|
/** 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;
|
|
}
|
|
|
|
bool okay = false;
|
|
#ifdef FSCom
|
|
auto f = SafeFile(filename, fullAtomic);
|
|
|
|
LOG_INFO("Save %s", filename);
|
|
pb_ostream_t stream = {&writecb, static_cast<Print *>(&f), protoSize};
|
|
|
|
if (!pb_encode(&stream, fields, dest_struct)) {
|
|
LOG_ERROR("Error: can't encode protobuf %s", PB_GET_ERROR(&stream));
|
|
} else {
|
|
okay = true;
|
|
}
|
|
|
|
bool writeSucceeded = f.close();
|
|
|
|
if (!okay || !writeSucceeded) {
|
|
LOG_ERROR("Can't write prefs!");
|
|
}
|
|
#else
|
|
LOG_ERROR("ERROR: Filesystem not implemented");
|
|
#endif
|
|
return okay;
|
|
}
|
|
|
|
bool NodeDB::saveChannelsToDisk()
|
|
{
|
|
|
|
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
|
// and all writes will fail anyway.
|
|
if (!powerHAL_isPowerLevelSafe()) {
|
|
LOG_ERROR("Error: trying to saveChannelsToDisk() on unsafe device power level.");
|
|
return false;
|
|
}
|
|
|
|
#ifdef FSCom
|
|
spiLock->lock();
|
|
FSCom.mkdir("/prefs");
|
|
spiLock->unlock();
|
|
#endif
|
|
return saveProto(channelFileName, meshtastic_ChannelFile_size, &meshtastic_ChannelFile_msg, &channelFile);
|
|
}
|
|
|
|
bool NodeDB::saveDeviceStateToDisk()
|
|
{
|
|
|
|
// do not try to save anything if power level is not safe. In many cases flash will be lock-protected
|
|
// and all writes will fail anyway. Device should be sleeping at this point anyway.
|
|
if (!powerHAL_isPowerLevelSafe()) {
|
|
LOG_ERROR("Error: trying to saveDeviceStateToDisk() on unsafe device power level.");
|
|
return false;
|
|
}
|
|
|
|
#ifdef FSCom
|
|
spiLock->lock();
|
|
FSCom.mkdir("/prefs");
|
|
spiLock->unlock();
|
|
#endif
|
|
// Note: if MAX_NUM_NODES=100 and meshtastic_NodeInfoLite_size=166, so will be approximately 17KB
|
|
// Because so huge we _must_ not use fullAtomic, because the filesystem is probably too small to hold two copies of this
|
|
return saveProto(deviceStateFileName, meshtastic_DeviceState_size, &meshtastic_DeviceState_msg, &devicestate, true);
|
|
}
|
|
|
|
bool NodeDB::saveNodeDatabaseToDisk()
|
|
{
|
|
|
|
// 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;
|
|
}
|
|
|
|
#ifdef FSCom
|
|
spiLock->lock();
|
|
FSCom.mkdir("/prefs");
|
|
spiLock->unlock();
|
|
#endif
|
|
size_t nodeDatabaseSize;
|
|
pb_get_encoded_size(&nodeDatabaseSize, meshtastic_NodeDatabase_fields, &nodeDatabase);
|
|
return saveProto(nodeDatabaseFileName, nodeDatabaseSize, &meshtastic_NodeDatabase_msg, &nodeDatabase, false);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
|
|
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) {
|
|
// 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.
|
|
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 && meshNodes->at(i).via_mqtt)
|
|
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%x 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 (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);
|
|
info->position = 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);
|
|
info->position.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%x 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 = info->position.time;
|
|
|
|
// Next, update atomically
|
|
info->position = TypeConversions::ConvertToPositionLite(p);
|
|
|
|
// Last, restore any fields that may have been overwritten
|
|
if (!info->position.time)
|
|
info->position.time = tmp_time;
|
|
}
|
|
info->has_position = true;
|
|
updateGUIforNode = info;
|
|
notifyObservers(true); // Force an update whether or not our node counts have changed
|
|
}
|
|
|
|
/** Update telemetry info for this node based on received metrics
|
|
* We only care about device telemetry here
|
|
*/
|
|
void NodeDB::updateTelemetry(uint32_t nodeId, const meshtastic_Telemetry &t, RxSource src)
|
|
{
|
|
meshtastic_NodeInfoLite *info = getOrCreateMeshNode(nodeId);
|
|
// Environment metrics should never go to NodeDb but we'll safegaurd anyway
|
|
if (!info || t.which_variant != meshtastic_Telemetry_device_metrics_tag) {
|
|
return;
|
|
}
|
|
|
|
if (src == RX_SRC_LOCAL) {
|
|
// Local packet, fully authoritative
|
|
LOG_DEBUG("updateTelemetry LOCAL");
|
|
} else {
|
|
LOG_DEBUG("updateTelemetry REMOTE node=0x%x ", nodeId);
|
|
}
|
|
info->device_metrics = t.variant.device_metrics;
|
|
info->has_device_metrics = true;
|
|
updateGUIforNode = info;
|
|
notifyObservers(true); // Force an update whether or not our node counts have changed
|
|
}
|
|
|
|
/**
|
|
* 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 ((info->bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) && !contact.manually_verified) {
|
|
if (contact.user.public_key.size != info->user.public_key.size ||
|
|
memcmp(contact.user.public_key.bytes, info->user.public_key.bytes, info->user.public_key.size) != 0) {
|
|
return;
|
|
}
|
|
}
|
|
info->num = contact.node_num;
|
|
info->has_user = true;
|
|
info->user = TypeConversions::ConvertToUserLite(contact.user);
|
|
if (contact.should_ignore) {
|
|
// If should_ignore is set,
|
|
// we need to clear the public key and other cruft, in addition to setting the node as ignored
|
|
info->is_ignored = true;
|
|
info->is_favorite = false;
|
|
info->has_device_metrics = false;
|
|
info->has_position = false;
|
|
info->user.public_key.size = 0;
|
|
memset(info->user.public_key.bytes, 0, sizeof(info->user.public_key.bytes));
|
|
} 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).
|
|
info->is_favorite = true;
|
|
}
|
|
|
|
// 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) {
|
|
info->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
|
}
|
|
// 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;
|
|
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, p.long_name);
|
|
meshtastic_ClientNotification *cn = clientNotificationPool.allocZeroed();
|
|
cn->level = meshtastic_LogRecord_Level_WARNING;
|
|
cn->time = getValidTime(RTCQualityFromNet);
|
|
sprintf(cn->message, warning, p.long_name);
|
|
service->sendClientNotification(cn);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
if (info->user.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->user.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);
|
|
|
|
// Both of info->user and p start as filled with zero so I think this is okay
|
|
auto lite = TypeConversions::ConvertToUserLite(p);
|
|
bool changed = memcmp(&info->user, &lite, sizeof(info->user)) || (info->channel != channelIndex);
|
|
|
|
info->user = lite;
|
|
if (info->user.public_key.size == 32) {
|
|
printBytes("Saved Pubkey: ", info->user.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->user.long_name, info->user.short_name, nodeId,
|
|
info->channel);
|
|
info->has_user = true;
|
|
|
|
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%x, 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.
|
|
|
|
info->via_mqtt = mp.via_mqtt; // Store if we received this packet via MQTT
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
|
|
void NodeDB::set_favorite(bool is_favorite, uint32_t nodeId)
|
|
{
|
|
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
|
|
if (lite && lite->is_favorite != is_favorite) {
|
|
lite->is_favorite = is_favorite;
|
|
sortMeshDB();
|
|
saveNodeDatabaseToDisk();
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
meshtastic_NodeInfoLite *lite = getMeshNode(nodeId);
|
|
|
|
if (lite) {
|
|
return lite->is_favorite;
|
|
}
|
|
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 (lite->is_favorite)
|
|
return true;
|
|
|
|
seenFrom = true;
|
|
}
|
|
|
|
if (lite->num == p.to) {
|
|
if (lite->is_favorite)
|
|
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 (meshNodes->at(i).is_favorite && !meshNodes->at(i - 1).is_favorite) {
|
|
std::swap(meshNodes->at(i), meshNodes->at(i - 1));
|
|
changed = true;
|
|
} else if (!meshNodes->at(i).is_favorite && meshNodes->at(i - 1).is_favorite) {
|
|
// 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;
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
/// 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++) {
|
|
// Simply the oldest non-favorite, non-ignored, non-verified node
|
|
if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored &&
|
|
!(meshNodes->at(i).bitfield & NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK) &&
|
|
meshNodes->at(i).last_heard < oldest) {
|
|
oldest = meshNodes->at(i).last_heard;
|
|
oldestIndex = i;
|
|
}
|
|
// The oldest "boring" node
|
|
if (!meshNodes->at(i).is_favorite && !meshNodes->at(i).is_ignored && meshNodes->at(i).user.public_key.size == 0 &&
|
|
meshNodes->at(i).last_heard < oldestBoring) {
|
|
oldestBoring = meshNodes->at(i).last_heard;
|
|
oldestBoringIndex = i;
|
|
}
|
|
}
|
|
// if we found a "boring" node, evict it
|
|
if (oldestBoringIndex != -1) {
|
|
oldestIndex = oldestBoringIndex;
|
|
}
|
|
|
|
if (oldestIndex != -1) {
|
|
// Shove the remaining nodes down the chain
|
|
for (int i = oldestIndex; i < numMeshNodes - 1; i++) {
|
|
meshNodes->at(i) = meshNodes->at(i + 1);
|
|
}
|
|
(numMeshNodes)--;
|
|
}
|
|
}
|
|
// add the node at the end
|
|
lite = &meshNodes->at((numMeshNodes)++);
|
|
|
|
// everything is missing except the nodenum
|
|
memset(lite, 0, sizeof(*lite));
|
|
lite->num = n;
|
|
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)
|
|
{
|
|
return n->has_position && (n->position.latitude_i != 0 || n->position.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)
|
|
{
|
|
meshtastic_NodeInfoLite *info = getMeshNode(nodeNum);
|
|
if (!info || !info->has_user) {
|
|
return UserLicenseStatus::NotKnown;
|
|
}
|
|
return info->user.is_licensed ? 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::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
|
|
}
|