mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
Past the oldest message in a conversation, Up reads the next 12 from the log. Pages, not a longer list: the messages screen measures its whole list every frame. Also from the full-screen view, and for a conversation the ring has lost. Settings > Messages > Delete after hides older messages at once and frees the flash a segment at a time, never with the clock unset or behind the log; Delete history clears the log and the rings. A batch is also written after 12 new messages, as a room's backlog could leave the ring unsaved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1053 lines
38 KiB
C++
1053 lines
38 KiB
C++
#include <Arduino.h>
|
|
#include "DataStore.h"
|
|
#include "Features.h" // FEAT_JOYSTICK_ROTATION_SETTING (else `#if !FEAT_…` is always true)
|
|
#include "ui-core/Battery.h" // validCurve() for the saved battery curve
|
|
#include <target.h> // radio_driver — repeater-profile freq bounds (getFreqBounds)
|
|
|
|
#if defined(EXTRAFS) || defined(QSPIFLASH)
|
|
#define MAX_BLOBRECS 100
|
|
#else
|
|
#define MAX_BLOBRECS 20
|
|
#endif
|
|
|
|
DataStore::DataStore(FILESYSTEM& fs, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(nullptr), _clock(&clock),
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
identity_store(fs, "")
|
|
#elif defined(RP2040_PLATFORM)
|
|
identity_store(fs, "/identity")
|
|
#else
|
|
identity_store(fs, "/identity")
|
|
#endif
|
|
{
|
|
}
|
|
|
|
#if defined(EXTRAFS) || defined(QSPIFLASH)
|
|
DataStore::DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock) : _fs(&fs), _fsExtra(&fsExtra), _clock(&clock),
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
identity_store(fs, "")
|
|
#elif defined(RP2040_PLATFORM)
|
|
identity_store(fs, "/identity")
|
|
#else
|
|
identity_store(fs, "/identity")
|
|
#endif
|
|
{
|
|
}
|
|
#endif
|
|
|
|
static File openWrite(FILESYSTEM* fs, const char* filename) {
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
fs->remove(filename);
|
|
return fs->open(filename, FILE_O_WRITE);
|
|
#elif defined(RP2040_PLATFORM)
|
|
return fs->open(filename, "w");
|
|
#else
|
|
return fs->open(filename, "w", true);
|
|
#endif
|
|
}
|
|
|
|
// Atomically swap a fully-written temp file over its final path. LittleFS
|
|
// (nRF52/STM32) rename replaces an existing destination atomically, so a crash
|
|
// leaves either the old file or the new one intact — never a truncated mix.
|
|
// Other Arduino filesystems can't rename onto an existing file, so the
|
|
// destination is dropped first (a metadata-only window, vs. the record-by-record
|
|
// write window of a direct overwrite). Returns false if the swap fails.
|
|
static bool commitTempFile(FILESYSTEM* fs, const char* tmp, const char* final_path) {
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
return fs->rename(tmp, final_path);
|
|
#else
|
|
fs->remove(final_path);
|
|
return fs->rename(tmp, final_path);
|
|
#endif
|
|
}
|
|
|
|
// Before a load: a restart between commitTempFile()'s remove and rename leaves
|
|
// only the temp file, complete. (A save cut short leaves the old file beside
|
|
// it, and the next save overwrites the temp. Only the very first save, cut
|
|
// short, leaves a temp alone that is incomplete: the loaders stop at a short
|
|
// record, and the JSON reader keeps what it read.)
|
|
static void recoverTempFile(FILESYSTEM* fs, const char* tmp, const char* final_path) {
|
|
if (!fs->exists(final_path) && fs->exists(tmp)) fs->rename(tmp, final_path);
|
|
}
|
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
static uint32_t _ContactsChannelsTotalBlocks = 0;
|
|
#endif
|
|
|
|
void DataStore::begin() {
|
|
#if defined(RP2040_PLATFORM)
|
|
identity_store.begin();
|
|
#endif
|
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
_ContactsChannelsTotalBlocks = _getContactsChannelsFS()->_getFS()->cfg->block_count;
|
|
checkAdvBlobFile();
|
|
#if defined(EXTRAFS) || defined(QSPIFLASH)
|
|
migrateToSecondaryFS();
|
|
#endif
|
|
#else
|
|
// init 'blob store' support
|
|
_fs->mkdir("/bl");
|
|
#endif
|
|
}
|
|
|
|
#if defined(ESP32)
|
|
#include <helpers/esp32/InternalFS.h>
|
|
#include <nvs_flash.h>
|
|
#elif defined(RP2040_PLATFORM)
|
|
#include <LittleFS.h>
|
|
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
#if defined(QSPIFLASH)
|
|
#include <CustomLFS_QSPIFlash.h>
|
|
#elif defined(EXTRAFS)
|
|
#include <CustomLFS.h>
|
|
#else
|
|
#include <InternalFileSystem.h>
|
|
#endif
|
|
#endif
|
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
int _countLfsBlock(void *p, lfs_block_t block){
|
|
if (block > _ContactsChannelsTotalBlocks) {
|
|
MESH_DEBUG_PRINTLN("ERROR: Block %d exceeds filesystem bounds - CORRUPTION DETECTED!", block);
|
|
return LFS_ERR_CORRUPT; // return error to abort lfs_traverse() gracefully
|
|
}
|
|
lfs_size_t *size = (lfs_size_t*) p;
|
|
*size += 1;
|
|
return 0;
|
|
}
|
|
|
|
lfs_ssize_t _getLfsUsedBlockCount(FILESYSTEM* fs) {
|
|
lfs_size_t size = 0;
|
|
int err = lfs_traverse(fs->_getFS(), _countLfsBlock, &size);
|
|
if (err) {
|
|
MESH_DEBUG_PRINTLN("ERROR: lfs_traverse() error: %d", err);
|
|
return 0;
|
|
}
|
|
return size;
|
|
}
|
|
#endif
|
|
|
|
uint32_t DataStore::getStorageUsedKb() const {
|
|
#if defined(ESP32)
|
|
return ESP32_FS.usedBytes() / 1024;
|
|
#elif defined(RP2040_PLATFORM)
|
|
FSInfo info;
|
|
info.usedBytes = 0;
|
|
_fs->info(info);
|
|
return info.usedBytes / 1024;
|
|
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg;
|
|
int usedBlockCount = _getLfsUsedBlockCount(_getContactsChannelsFS());
|
|
int usedBytes = config->block_size * usedBlockCount;
|
|
return usedBytes / 1024;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
uint32_t DataStore::getStorageTotalKb() const {
|
|
#if defined(ESP32)
|
|
return ESP32_FS.totalBytes() / 1024;
|
|
#elif defined(RP2040_PLATFORM)
|
|
FSInfo info;
|
|
info.totalBytes = 0;
|
|
_fs->info(info);
|
|
return info.totalBytes / 1024;
|
|
#elif defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
const lfs_config* config = _getContactsChannelsFS()->_getFS()->cfg;
|
|
int totalBytes = config->block_size * config->block_count;
|
|
return totalBytes / 1024;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
File DataStore::openRead(const char* filename) {
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
return _fs->open(filename, FILE_O_READ);
|
|
#elif defined(RP2040_PLATFORM)
|
|
return _fs->open(filename, "r");
|
|
#else
|
|
return _fs->open(filename, "r", false);
|
|
#endif
|
|
}
|
|
|
|
File DataStore::openRead(FILESYSTEM* fs, const char* filename) {
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
return fs->open(filename, FILE_O_READ);
|
|
#elif defined(RP2040_PLATFORM)
|
|
return fs->open(filename, "r");
|
|
#else
|
|
return fs->open(filename, "r", false);
|
|
#endif
|
|
}
|
|
|
|
File DataStore::openWrite(const char* filename) {
|
|
return ::openWrite(_fs, filename);
|
|
}
|
|
|
|
bool DataStore::commitFile(const char* tmp_path, const char* final_path) {
|
|
return commitTempFile(_fs, tmp_path, final_path);
|
|
}
|
|
|
|
bool DataStore::removeFile(const char* filename) {
|
|
return _fs->remove(filename);
|
|
}
|
|
|
|
bool DataStore::removeFile(FILESYSTEM* fs, const char* filename) {
|
|
return fs->remove(filename);
|
|
}
|
|
|
|
bool DataStore::formatFileSystem() {
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
if (_fsExtra == nullptr) {
|
|
return _fs->format();
|
|
} else {
|
|
return _fs->format() && _fsExtra->format();
|
|
}
|
|
#elif defined(RP2040_PLATFORM)
|
|
return LittleFS.format();
|
|
#elif defined(ESP32)
|
|
bool fs_success = ((ESP32_FS_CLASS *)_fs)->format();
|
|
esp_err_t nvs_err = nvs_flash_erase(); // no need to reinit, will be done by reboot
|
|
return fs_success && (nvs_err == ESP_OK);
|
|
#elif defined(SIM_PLATFORM)
|
|
return _fs->format();
|
|
#else
|
|
#error "need to implement format()"
|
|
#endif
|
|
}
|
|
|
|
bool DataStore::loadMainIdentity(mesh::LocalIdentity &identity) {
|
|
return identity_store.load("_main", identity);
|
|
}
|
|
|
|
bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) {
|
|
return identity_store.save("_main", identity);
|
|
}
|
|
|
|
// ── Prefs ──
|
|
// /prefs.json (NodePrefs::structure()). The JSON goes through a buffer: the
|
|
// serializer reads and writes it a character at a time, each a file call.
|
|
class BufferedFile : public Stream {
|
|
File& _f;
|
|
uint8_t _buf[256];
|
|
size_t _n = 0, _at = 0;
|
|
bool _ok = true;
|
|
public:
|
|
explicit BufferedFile(File& f) : _f(f) { }
|
|
// Reading (a file opened to read).
|
|
int available() override { fill(); return (int)(_n - _at); }
|
|
int read() override { fill(); return _at < _n ? _buf[_at++] : -1; }
|
|
int peek() override { fill(); return _at < _n ? _buf[_at] : -1; }
|
|
void fill() {
|
|
if (_at < _n) return;
|
|
int got = (int)_f.read(_buf, sizeof(_buf));
|
|
_n = got > 0 ? (size_t)got : 0;
|
|
_at = 0;
|
|
}
|
|
// Writing (a file opened to write): flush() at the end.
|
|
size_t write(uint8_t c) override { return write(&c, 1); }
|
|
size_t write(const uint8_t* p, size_t len) override {
|
|
for (size_t i = 0; i < len; i++) {
|
|
if (_n == sizeof(_buf)) flush();
|
|
_buf[_n++] = p[i];
|
|
}
|
|
return len;
|
|
}
|
|
void flush() override {
|
|
if (_n && _f.write(_buf, _n) != _n) _ok = false;
|
|
_n = 0;
|
|
}
|
|
bool ok() const { return _ok; }
|
|
};
|
|
|
|
// Every value inside its range, whatever the file held: a damaged byte reads
|
|
// as the default rather than as a setting no screen can show.
|
|
static void sanitize(NodePrefs& p) {
|
|
#if !FEAT_JOYSTICK_ROTATION_SETTING
|
|
p.joystick_rotation = 0; // no setting for it on this build
|
|
#endif
|
|
{ // only the duty-cycle presets Settings offers
|
|
static const uint32_t GPS_DUTY_PRESETS[] = { 0, 60, 300, 900, 1800, 3600 };
|
|
bool known = false;
|
|
for (uint32_t v : GPS_DUTY_PRESETS) if (p.gps_interval == v) { known = true; break; }
|
|
if (!known) p.gps_interval = 0;
|
|
}
|
|
auto flag = [](uint8_t& v, uint8_t def) { if (v > 1) v = def; };
|
|
if (p.ringtone_len > 32) p.ringtone_len = 0;
|
|
if (p.ringtone2_len > 32) p.ringtone2_len = 0;
|
|
if (p.notif_melody_dm > 3) p.notif_melody_dm = 0;
|
|
if (p.notif_melody_ch > 3) p.notif_melody_ch = 0;
|
|
if (p.notif_melody_ad > 3) p.notif_melody_ad = 0;
|
|
if (p.page_order_set != NodePrefs::PAGE_ORDER_MAGIC) p.page_order_set = 0;
|
|
for (uint8_t i = 0; i < NodePrefs::PAGE_ORDER_LEN; i++)
|
|
if (p.page_order[i] > NodePrefs::HPB_COUNT) p.page_order[i] = 0;
|
|
flag(p.repeat_skip_adverts, 0);
|
|
if (p.repeat_max_hops > 64) p.repeat_max_hops = 0;
|
|
if (p.repeat_delay_boost > 8) p.repeat_delay_boost = 0;
|
|
if (p.repeat_min_snr != NodePrefs::REPEAT_SNR_DISABLED && (p.repeat_min_snr < -20 || p.repeat_min_snr > 10))
|
|
p.repeat_min_snr = NodePrefs::REPEAT_SNR_DISABLED; // the UI's -20..10
|
|
flag(p.repeat_suppress_dup, 0);
|
|
flag(p.repeat_scope_only, 0);
|
|
flag(p.repeater_use_profile, 0);
|
|
float lo, hi;
|
|
radio_driver.getFreqBounds(lo, hi);
|
|
if (!isValidRepeaterProfile(p.repeater_freq, p.repeater_bw, p.repeater_sf, p.repeater_cr, lo, hi))
|
|
seedDefaultRepeaterProfile(p);
|
|
flag(p.track_shared_loc, 0);
|
|
flag(p.loc_share_enabled, 0);
|
|
flag(p.loc_share_target_type, 0);
|
|
if (p.loc_share_channel_idx >= MAX_GROUP_CHANNELS) p.loc_share_channel_idx = 0;
|
|
if (p.loc_share_move_idx >= NodePrefs::LOC_SHARE_MOVE_COUNT) p.loc_share_move_idx = 1;
|
|
if (p.loc_share_interval_idx >= NodePrefs::LOC_SHARE_INTERVAL_COUNT) p.loc_share_interval_idx = 1;
|
|
if (p.loc_share_heartbeat_idx >= NodePrefs::LOC_SHARE_HEARTBEAT_COUNT) p.loc_share_heartbeat_idx = 0;
|
|
if (p.loc_share_scope > ScopeList::MAX_SCOPE_ENTRIES + 1) p.loc_share_scope = 0;
|
|
if (p.loc_share_duration_idx >= NodePrefs::LOC_SHARE_DURATION_COUNT) p.loc_share_duration_idx = 0;
|
|
flag(p.locator_enabled, 0);
|
|
flag(p.locator_has_target, 0);
|
|
flag(p.locator_beeper, 0);
|
|
if (p.locator_radius_idx >= NodePrefs::LOCATOR_RADIUS_COUNT) p.locator_radius_idx = 1;
|
|
if (p.locator_mode >= NodePrefs::LOCATOR_MODE_COUNT) p.locator_mode = 0;
|
|
if (p.locator_target_kind > 2) p.locator_target_kind = 0;
|
|
p.locator_label[sizeof(p.locator_label) - 1] = '\0';
|
|
if (p.trail_autopause_idx >= NodePrefs::TRAIL_AUTOPAUSE_COUNT) p.trail_autopause_idx = 0;
|
|
flag(p.trail_autosave_lowbatt, 0);
|
|
if (p.gps_avg_idx >= NodePrefs::GPS_AVG_COUNT) p.gps_avg_idx = 0;
|
|
flag(p.alarm_on, 0);
|
|
if (p.alarm_hour > 23) p.alarm_hour = 0;
|
|
if (p.alarm_min > 59) p.alarm_min = 0;
|
|
if (NodePrefs::alarmRepeatIdxForMask(p.alarm_repeat_mask) == 0) p.alarm_repeat_mask = 0;
|
|
flag(p.keyboard_type, 0);
|
|
if (p.keyboard_alt_alphabet >= NodePrefs::KB_ALPHABET_COUNT) p.keyboard_alt_alphabet = 0;
|
|
if (p.keyboard_main_alphabet >= NodePrefs::KB_ALPHABET_COUNT) p.keyboard_main_alphabet = 0;
|
|
flag(p.keyboard_cardkb_compact, 0);
|
|
flag(p.bot_commands_enabled, 0);
|
|
flag(p.bot_commands_ch, 0);
|
|
flag(p.bot_commands_room, 0);
|
|
flag(p.bot_actions_dm, 0);
|
|
flag(p.bot_actions_ch, 0);
|
|
flag(p.bot_actions_room, 0);
|
|
flag(p.bot_dm_scope, 0);
|
|
flag(p.bot_room_enabled, 0);
|
|
if (p.bot_quiet_start > 23) p.bot_quiet_start = 0;
|
|
if (p.bot_quiet_end > 23) p.bot_quiet_end = 0;
|
|
flag(p.units_imperial, 0);
|
|
flag(p.trail_show_pace, 0);
|
|
if (p.advert_sound_scope > 1) p.advert_sound_scope = ADVERT_SOUND_SCOPE_ALL;
|
|
flag(p.rx_powersave, 0);
|
|
flag(p.tx_apc, 0);
|
|
if (p.dm_resend_count > 5) p.dm_resend_count = 2;
|
|
if (p.gpio1_mode > 4) p.gpio1_mode = 0; // gpio1/2 can be analog (AIN0/AIN5)
|
|
if (p.gpio2_mode > 4) p.gpio2_mode = 0;
|
|
if (p.gpio3_mode > 3) p.gpio3_mode = 0;
|
|
if (p.gpio4_mode > 3) p.gpio4_mode = 0;
|
|
if (p.interference_threshold > 30) p.interference_threshold = 0;
|
|
flag(p.cad_enabled, 0);
|
|
for (uint8_t i = 0; i < NodePrefs::FAVOURITES_COUNT; i++)
|
|
if (p.favourite_kinds[i] > NodePrefs::FAV_KIND_MAX) p.favourite_kinds[i] = NodePrefs::FAV_KIND_CONTACT;
|
|
flag(p.fav_sort, 1);
|
|
flag(p.msg_wake, 1);
|
|
if (p.msg_keep >= NodePrefs::MSG_KEEP_COUNT) p.msg_keep = 0;
|
|
if (p.msg_expire >= NodePrefs::CONTACT_EXPIRY_COUNT) p.msg_expire = 0;
|
|
if (p.contact_expiry_idx >= NodePrefs::CONTACT_EXPIRY_COUNT) p.contact_expiry_idx = 0;
|
|
if (p.dst_rule >= NodePrefs::DST_COUNT) p.dst_rule = NodePrefs::DST_OFF;
|
|
if (p.display_brightness_pct > 100) p.display_brightness_pct = 0;
|
|
flag(p.quiet_hours, 0);
|
|
if (p.quiet_from > 23) p.quiet_from = 22;
|
|
if (p.quiet_to > 23) p.quiet_to = 7;
|
|
if (!battery::validCurve(p.batt_curve_mv)) memset(p.batt_curve_mv, 0, sizeof(p.batt_curve_mv));
|
|
flag(p.lock_compact, 0);
|
|
flag(p.ble_off, 0);
|
|
if (p.msg_alert > NodePrefs::MSG_ALERT_OFF) p.msg_alert = NodePrefs::MSG_ALERT_NORMAL;
|
|
if (p.advert_loc_prec >= NodePrefs::ADV_LOC_PREC_COUNT) p.advert_loc_prec = 0;
|
|
if (p.ui_text_scale > NodePrefs::UI_TEXT_SCALE_MAX) p.ui_text_scale = 0;
|
|
#ifdef EINK_FONT_SCALE // the Text size row shows the scale in use: start at the build's
|
|
if (p.ui_text_scale == 0) p.ui_text_scale = EINK_FONT_SCALE;
|
|
#endif
|
|
if (p.ui_icon_scale > NodePrefs::UI_ICON_SCALE_MAX) p.ui_icon_scale = 0;
|
|
}
|
|
|
|
// The settings a firmware before 2.0 kept in /new_prefs that still matter
|
|
// after the update: its first block, laid out the same in upstream MeshCore
|
|
// and in every Solo -- the node's name and position, its radio, the BLE PIN
|
|
// and the default scope. Without them a device would come back on the
|
|
// default radio, deaf to its mesh. The rest starts from the defaults.
|
|
static void importOldPrefs(File& f, NodePrefs& p, double& lat, double& lon) {
|
|
uint8_t pad[4];
|
|
f.read((uint8_t*)&p.airtime_factor, sizeof(p.airtime_factor));
|
|
f.read((uint8_t*)p.node_name, sizeof(p.node_name));
|
|
f.read(pad, 4);
|
|
f.read((uint8_t*)&lat, sizeof(lat));
|
|
f.read((uint8_t*)&lon, sizeof(lon));
|
|
f.read((uint8_t*)&p.freq, sizeof(p.freq));
|
|
f.read((uint8_t*)&p.sf, sizeof(p.sf));
|
|
f.read((uint8_t*)&p.cr, sizeof(p.cr));
|
|
f.read((uint8_t*)&p.client_repeat, sizeof(p.client_repeat));
|
|
f.read((uint8_t*)&p.manual_add_contacts, sizeof(p.manual_add_contacts));
|
|
f.read((uint8_t*)&p.bw, sizeof(p.bw));
|
|
f.read((uint8_t*)&p.tx_power_dbm, sizeof(p.tx_power_dbm));
|
|
f.read((uint8_t*)&p.telemetry_mode_base, sizeof(p.telemetry_mode_base));
|
|
f.read((uint8_t*)&p.telemetry_mode_loc, sizeof(p.telemetry_mode_loc));
|
|
f.read((uint8_t*)&p.telemetry_mode_env, sizeof(p.telemetry_mode_env));
|
|
f.read((uint8_t*)&p.rx_delay_base, sizeof(p.rx_delay_base));
|
|
f.read((uint8_t*)&p.advert_loc_policy, sizeof(p.advert_loc_policy));
|
|
f.read((uint8_t*)&p.multi_acks, sizeof(p.multi_acks));
|
|
f.read((uint8_t*)&p.path_hash_mode, sizeof(p.path_hash_mode));
|
|
f.read(pad, 1);
|
|
f.read((uint8_t*)&p.ble_pin, sizeof(p.ble_pin));
|
|
f.read((uint8_t*)&p.buzzer_quiet, sizeof(p.buzzer_quiet));
|
|
f.read((uint8_t*)&p.gps_enabled, sizeof(p.gps_enabled));
|
|
f.read((uint8_t*)&p.gps_interval, sizeof(p.gps_interval));
|
|
f.read((uint8_t*)&p.autoadd_config, sizeof(p.autoadd_config));
|
|
f.read((uint8_t*)&p.autoadd_max_hops, sizeof(p.autoadd_max_hops));
|
|
f.read((uint8_t*)&p.rx_boosted_gain, sizeof(p.rx_boosted_gain));
|
|
f.read((uint8_t*)p.default_scope_name, sizeof(p.default_scope_name));
|
|
f.read((uint8_t*)p.default_scope_key, sizeof(p.default_scope_key));
|
|
p.node_name[sizeof(p.node_name) - 1] = '\0';
|
|
p.default_scope_name[sizeof(p.default_scope_name) - 1] = '\0';
|
|
if (p.client_repeat > 1) p.client_repeat = 0;
|
|
if (p.buzzer_quiet > 1) p.buzzer_quiet = 0;
|
|
}
|
|
|
|
// A device that ran Meshtastic before (the L1 ships with it) can still have
|
|
// its settings folder at /prefs: our file can't be read from a folder, and the
|
|
// rename that commits every save can't replace one, so no setting survived a
|
|
// restart. The folder is of no use here: it goes.
|
|
#if defined(ESP32)
|
|
static void removeTree(FILESYSTEM* fs, const char* path) {
|
|
File dir = fs->open(path, "r", false);
|
|
if (!dir) return;
|
|
File f = dir.openNextFile();
|
|
while (f) {
|
|
char sub[64];
|
|
snprintf(sub, sizeof(sub), "%s", f.path());
|
|
const bool is_dir = f.isDirectory();
|
|
f.close();
|
|
if (is_dir) removeTree(fs, sub); else fs->remove(sub);
|
|
f = dir.openNextFile();
|
|
}
|
|
dir.close();
|
|
fs->rmdir(path);
|
|
}
|
|
#endif
|
|
static void removeStrayDir(FILESYSTEM* fs, const char* path) {
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(ESP32)
|
|
#if defined(ESP32)
|
|
File f = fs->open(path, "r", false);
|
|
#else
|
|
File f = fs->open(path, FILE_O_READ);
|
|
#endif
|
|
const bool is_dir = f && f.isDirectory();
|
|
if (f) f.close();
|
|
if (!is_dir) return;
|
|
#if defined(ESP32)
|
|
removeTree(fs, path);
|
|
#else
|
|
fs->rmdir_r(path);
|
|
#endif
|
|
#endif
|
|
}
|
|
|
|
// `prefs` comes in holding the defaults; what the file holds replaces them.
|
|
void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) {
|
|
removeStrayDir(_fs, "/prefs");
|
|
if (_fs->exists("/prefs")) _fs->remove("/prefs"); // 2.0-rc's binary settings: 2.0 starts from the defaults
|
|
recoverTempFile(_fs, "/prefs.tmp", "/prefs.json");
|
|
File file = openRead(_fs, "/prefs.json");
|
|
if (file) {
|
|
BufferedFile in(file);
|
|
prefs.node_lat = node_lat;
|
|
prefs.node_lon = node_lon;
|
|
// Saved whole (a temp file, then renamed): a file that doesn't parse to
|
|
// the end was damaged since. What was read of it stays.
|
|
if (!prefs.loadSerial(in)) MESH_DEBUG_PRINTLN("prefs: /prefs.json damaged, the rest from the defaults");
|
|
file.close();
|
|
node_lat = prefs.node_lat;
|
|
node_lon = prefs.node_lon;
|
|
} else { // first boot after an update from before 2.0 (/node_prefs: older still, the same layout)
|
|
for (const char* old : { "/new_prefs", "/node_prefs" }) {
|
|
if (!_fs->exists(old)) continue;
|
|
if ((file = openRead(_fs, old))) {
|
|
importOldPrefs(file, prefs, node_lat, node_lon);
|
|
file.close();
|
|
savePrefs(prefs, node_lat, node_lon);
|
|
}
|
|
_fs->remove(old);
|
|
break;
|
|
}
|
|
}
|
|
prefs.clearDirty();
|
|
sanitize(prefs);
|
|
}
|
|
|
|
void DataStore::savePrefs(NodePrefs& prefs, double node_lat, double node_lon) {
|
|
// Atomic temp-then-rename (see commitTempFile): an interrupted save leaves
|
|
// the previous /prefs.json in place.
|
|
File file = ::openWrite(_fs, "/prefs.tmp");
|
|
if (!file) return;
|
|
prefs.node_lat = node_lat;
|
|
prefs.node_lon = node_lon;
|
|
BufferedFile out(file);
|
|
bool ok = prefs.saveSerial(out);
|
|
out.flush();
|
|
ok = ok && out.ok(); // once the flash fills, writes come back short
|
|
file.close();
|
|
if (ok) { commitTempFile(_fs, "/prefs.tmp", "/prefs.json"); prefs.clearDirty(); }
|
|
else _fs->remove("/prefs.tmp");
|
|
}
|
|
|
|
void DataStore::saveRTCTime() {
|
|
uint32_t t = _clock->getCurrentTime();
|
|
if (t < 1000000000UL) return; // don't save if time not yet synced
|
|
File file = ::openWrite(_fs, "/rtc_save");
|
|
if (file) {
|
|
file.write((uint8_t *)&t, sizeof(t));
|
|
file.close();
|
|
}
|
|
}
|
|
|
|
// The day this firmware was built (__DATE__, "Mmm dd yyyy"), 00:00 UTC: the
|
|
// clock can't be any earlier than that.
|
|
static uint32_t buildDayEpoch() {
|
|
static const char M[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
|
|
const char* d = __DATE__;
|
|
int m = 1;
|
|
while (m <= 12 && strncmp(M + 3 * (m - 1), d, 3) != 0) m++;
|
|
int day = atoi(d + 4), y = atoi(d + 7);
|
|
if (m > 12 || day < 1 || y < 2024) return 0;
|
|
y -= m <= 2; // days from 1970-01-01 (H. Hinnant's days_from_civil)
|
|
int era = y / 400, yoe = y - era * 400;
|
|
int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + day - 1;
|
|
int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
|
return (uint32_t)(era * 146097 + doe - 719468) * 86400UL;
|
|
}
|
|
|
|
void DataStore::restoreRTCTime() {
|
|
File file = openRead(_fs, "/rtc_save");
|
|
uint32_t t = 0;
|
|
if (file) {
|
|
file.read((uint8_t *)&t, sizeof(t));
|
|
file.close();
|
|
}
|
|
// Saved at power-off and every half hour (MyMesh::loop()); a flash or a
|
|
// reset in between can leave it far behind -- never earlier than the build.
|
|
uint32_t built = buildDayEpoch();
|
|
if (t < built) t = built;
|
|
#ifndef SIM_PLATFORM
|
|
// Real hardware has no other way to know the time before a GPS fix or
|
|
// a phone/CLI sync, so restoring the last-known saved time is the
|
|
// right call there. The sim's RTCClock (SimRTCClock.h) is already
|
|
// backed by the real host wall clock (time(NULL)) from the moment it's
|
|
// constructed -- overwriting that with a stale save from a previous
|
|
// visit (persisted via IDBFS, see the site's "returning visitor" note)
|
|
// would make a returning instance's on-screen clock drift away from
|
|
// the visitor's own real time instead of just showing it.
|
|
// Only ever forward: a board with its own RTC chip may already know better.
|
|
if (t > 1000000000UL && t > _clock->getCurrentTime()) _clock->setCurrentTime(t);
|
|
#endif
|
|
}
|
|
|
|
void DataStore::loadContacts(DataStoreHost* host) {
|
|
recoverTempFile(_getContactsChannelsFS(), "/contacts3.tmp", "/contacts3");
|
|
File file = openRead(_getContactsChannelsFS(), "/contacts3");
|
|
if (file) {
|
|
bool full = false;
|
|
while (!full) {
|
|
ContactInfo c;
|
|
uint8_t pub_key[32];
|
|
uint8_t unused;
|
|
|
|
bool success = (file.read(pub_key, 32) == 32);
|
|
success = success && (file.read((uint8_t *)&c.name, 32) == 32);
|
|
success = success && (file.read(&c.type, 1) == 1);
|
|
success = success && (file.read(&c.flags, 1) == 1);
|
|
success = success && (file.read(&unused, 1) == 1);
|
|
success = success && (file.read((uint8_t *)&c.sync_since, 4) == 4); // was 'reserved'
|
|
success = success && (file.read((uint8_t *)&c.out_path_len, 1) == 1);
|
|
success = success && (file.read((uint8_t *)&c.last_advert_timestamp, 4) == 4);
|
|
success = success && (file.read(c.out_path, 64) == 64);
|
|
success = success && (file.read((uint8_t *)&c.lastmod, 4) == 4);
|
|
success = success && (file.read((uint8_t *)&c.gps_lat, 4) == 4);
|
|
success = success && (file.read((uint8_t *)&c.gps_lon, 4) == 4);
|
|
|
|
if (!success) break; // EOF
|
|
|
|
c.id = mesh::Identity(pub_key);
|
|
if (!host->onContactLoaded(c)) full = true;
|
|
}
|
|
file.close();
|
|
}
|
|
}
|
|
|
|
void DataStore::saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c)) {
|
|
FILESYSTEM* fs = _getContactsChannelsFS();
|
|
// Write to a temp file, then atomically rename it over /contacts3 only once
|
|
// every record has written cleanly. The old code truncated /contacts3 up
|
|
// front and wrote in place, so a crash, reset or full flash mid-save wiped
|
|
// the entire contact list. Now an interrupted save leaves the previous good
|
|
// file untouched.
|
|
File file = ::openWrite(fs, "/contacts3.tmp");
|
|
if (!file) return;
|
|
|
|
bool ok = true;
|
|
uint32_t idx = 0;
|
|
ContactInfo c;
|
|
uint8_t unused = 0;
|
|
|
|
while (host->getContactForSave(idx, c)) {
|
|
if (filter && !filter(c)) {
|
|
idx++; // advance to next contact
|
|
continue;
|
|
}
|
|
bool success = (file.write(c.id.pub_key, 32) == 32);
|
|
success = success && (file.write((uint8_t *)&c.name, 32) == 32);
|
|
success = success && (file.write(&c.type, 1) == 1);
|
|
success = success && (file.write(&c.flags, 1) == 1);
|
|
success = success && (file.write(&unused, 1) == 1);
|
|
success = success && (file.write((uint8_t *)&c.sync_since, 4) == 4);
|
|
success = success && (file.write((uint8_t *)&c.out_path_len, 1) == 1);
|
|
success = success && (file.write((uint8_t *)&c.last_advert_timestamp, 4) == 4);
|
|
success = success && (file.write(c.out_path, 64) == 64);
|
|
success = success && (file.write((uint8_t *)&c.lastmod, 4) == 4);
|
|
success = success && (file.write((uint8_t *)&c.gps_lat, 4) == 4);
|
|
success = success && (file.write((uint8_t *)&c.gps_lon, 4) == 4);
|
|
|
|
if (!success) { ok = false; break; } // write failed (e.g. flash full)
|
|
|
|
idx++; // advance to next contact
|
|
}
|
|
file.close();
|
|
|
|
if (ok) {
|
|
commitTempFile(fs, "/contacts3.tmp", "/contacts3");
|
|
} else {
|
|
fs->remove("/contacts3.tmp"); // keep the previous good /contacts3
|
|
}
|
|
}
|
|
|
|
bool DataStore::loadChannels(DataStoreHost* host) {
|
|
FILESYSTEM* fs = _getContactsChannelsFS();
|
|
recoverTempFile(fs, "/channels3.tmp", "/channels3");
|
|
File file = openRead(fs, "/channels3");
|
|
if (file) {
|
|
// /channels3: the leading 4-byte field's first byte is the channel's
|
|
// original slot index (see saveChannels()) — load it back into that
|
|
// exact slot. The old /channels2 format instead reassigned indices
|
|
// 0,1,2… sequentially on every load, which silently shifted every
|
|
// later channel down a slot once an earlier one was removed — anything
|
|
// that remembers a channel by index (Live Share's target, the bot's
|
|
// channel, per-channel melody) would then point at the wrong channel
|
|
// after the next reboot.
|
|
bool full = false;
|
|
uint8_t skipped = 0;
|
|
while (!full) {
|
|
ChannelDetails ch;
|
|
uint8_t hdr[4];
|
|
|
|
bool success = (file.read(hdr, 4) == 4);
|
|
success = success && (file.read((uint8_t *)ch.name, 32) == 32);
|
|
success = success && (file.read((uint8_t *)ch.channel.secret, 32) == 32);
|
|
|
|
if (!success) break; // EOF
|
|
|
|
// Sanity check: an all-zero secret means the entry is uninitialised
|
|
// or the file format was corrupted by a previous firmware (different
|
|
// layout). Loading such a channel makes findChannelIdx() match the
|
|
// wrong slot for incoming messages — drop it. The companion app can
|
|
// re-sync the channel afterwards.
|
|
bool secret_empty = true;
|
|
for (int b = 0; b < 32; b++) {
|
|
if (ch.channel.secret[b] != 0) { secret_empty = false; break; }
|
|
}
|
|
if (secret_empty) {
|
|
skipped++;
|
|
continue;
|
|
}
|
|
// Defensive: ensure name is null-terminated so callers can treat it
|
|
// as a C string regardless of how the file was written.
|
|
ch.name[31] = '\0';
|
|
|
|
if (!host->onChannelLoaded(hdr[0], ch)) full = true;
|
|
}
|
|
file.close();
|
|
if (skipped > 0) {
|
|
MESH_DEBUG_PRINTLN("loadChannels: skipped %u corrupted/empty channel entr%s",
|
|
(unsigned)skipped, skipped == 1 ? "y" : "ies");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// One-time migration from the old /channels2 format (sequential index,
|
|
// reassigned on every load — the bug /channels3 above replaces). Loads
|
|
// with that old semantics once, then resaves as /channels3 so this
|
|
// fallback is never hit again on this device.
|
|
file = openRead(fs, "/channels2");
|
|
if (file) {
|
|
bool full = false;
|
|
uint8_t channel_idx = 0;
|
|
while (!full) {
|
|
ChannelDetails ch;
|
|
uint8_t unused[4];
|
|
|
|
bool success = (file.read(unused, 4) == 4);
|
|
success = success && (file.read((uint8_t *)ch.name, 32) == 32);
|
|
success = success && (file.read((uint8_t *)ch.channel.secret, 32) == 32);
|
|
if (!success) break; // EOF
|
|
|
|
bool secret_empty = true;
|
|
for (int b = 0; b < 32; b++) if (ch.channel.secret[b] != 0) { secret_empty = false; break; }
|
|
if (secret_empty) continue;
|
|
ch.name[31] = '\0';
|
|
|
|
if (host->onChannelLoaded(channel_idx, ch)) channel_idx++;
|
|
else full = true;
|
|
}
|
|
file.close();
|
|
saveChannels(host); // write /channels3 so the migration runs only once
|
|
return true;
|
|
}
|
|
|
|
return false; // neither file exists -- genuinely fresh device
|
|
}
|
|
|
|
void DataStore::saveChannels(DataStoreHost* host) {
|
|
FILESYSTEM* fs = _getContactsChannelsFS();
|
|
// Same atomic temp-then-rename pattern as saveContacts() — never truncate the
|
|
// live /channels3 before the new copy is fully written.
|
|
File file = ::openWrite(fs, "/channels3.tmp");
|
|
if (!file) return;
|
|
|
|
bool ok = true;
|
|
uint8_t channel_idx = 0;
|
|
ChannelDetails ch;
|
|
|
|
while (host->getChannelForSave(channel_idx, ch)) {
|
|
uint8_t idx = channel_idx++;
|
|
// getChannelForSave() returns every slot up to MAX_GROUP_CHANNELS, so skip
|
|
// the unused ones (all-zero secret) rather than writing all 40 — otherwise
|
|
// the file is always ~2.7 KB and wears the flash needlessly. Unlike the old
|
|
// /channels2 format, loadChannels() no longer compacts: the slot index
|
|
// travels with the record (hdr[0] below) so a removed channel just leaves
|
|
// a hole instead of shifting every later index down a slot.
|
|
bool empty = true;
|
|
for (int b = 0; b < 32; b++) if (ch.channel.secret[b]) { empty = false; break; }
|
|
if (empty) continue;
|
|
|
|
uint8_t hdr[4] = { idx, 0, 0, 0 };
|
|
bool success = (file.write(hdr, 4) == 4);
|
|
success = success && (file.write((uint8_t *)ch.name, 32) == 32);
|
|
success = success && (file.write((uint8_t *)ch.channel.secret, 32) == 32);
|
|
if (!success) { ok = false; break; } // write failed
|
|
}
|
|
file.close();
|
|
|
|
if (ok) {
|
|
commitTempFile(fs, "/channels3.tmp", "/channels3");
|
|
} else {
|
|
fs->remove("/channels3.tmp"); // keep the previous good /channels3
|
|
}
|
|
}
|
|
|
|
bool DataStore::loadScopeList(ScopeList& list, const NodePrefs& prefs) {
|
|
recoverTempFile(_fs, "/scopes1.tmp", "/scopes1");
|
|
File file = openRead("/scopes1");
|
|
if (file) {
|
|
uint8_t hdr[2] = { 0, 0 }; // default_idx is read back below even if the header read fails
|
|
bool success = (file.read(hdr, 2) == 2);
|
|
uint8_t count = success ? hdr[1] : 0;
|
|
if (count > ScopeList::MAX_SCOPE_ENTRIES) count = 0; // corrupt header -- start empty rather than overrun entries[]
|
|
|
|
uint8_t loaded = 0;
|
|
for (uint8_t i = 0; i < count; i++) {
|
|
ScopeEntry e;
|
|
bool ok = (file.read((uint8_t *)e.name, sizeof(e.name)) == sizeof(e.name));
|
|
ok = ok && (file.read(e.key, sizeof(e.key)) == sizeof(e.key));
|
|
if (!ok) break; // truncated file -- keep whatever loaded fine so far
|
|
e.name[sizeof(e.name) - 1] = '\0';
|
|
list.entries[loaded++] = e;
|
|
}
|
|
file.close();
|
|
list.count = loaded;
|
|
list.default_idx = list.clamp(hdr[0]);
|
|
return false; // the file was already there -- nothing migrated this boot
|
|
}
|
|
|
|
// No /scopes1 yet -- one-time migration of an existing single
|
|
// default_scope_name/key (Settings > Radio > Scope, pre-list) into list
|
|
// entry 1 and mark it default, which covers DMs and the relay filter. The
|
|
// caller finishes the job for channels by seeding their per-channel picks
|
|
// once channels[] is loaded (see this function's return value). A
|
|
// never-configured device just stays at the default-constructed ScopeList
|
|
// (empty, default_idx 0 == "*").
|
|
list.count = 0;
|
|
list.default_idx = 0;
|
|
bool migrated = (prefs.default_scope_name[0] != '\0');
|
|
if (migrated) {
|
|
ScopeEntry& e = list.entries[0];
|
|
StrHelper::strncpy(e.name, prefs.default_scope_name, sizeof(e.name));
|
|
memcpy(e.key, prefs.default_scope_key, sizeof(e.key)); // already-derived key, no need to re-derive
|
|
list.count = 1;
|
|
list.default_idx = 1;
|
|
}
|
|
saveScopeList(list); // write /scopes1 so this migration runs only once
|
|
return migrated; // caller seeds the existing channels with entry 1
|
|
}
|
|
|
|
void DataStore::saveScopeList(const ScopeList& list) {
|
|
File file = ::openWrite(_fs, "/scopes1.tmp");
|
|
if (!file) return;
|
|
|
|
uint8_t hdr[2] = { list.default_idx, list.count };
|
|
bool ok = (file.write(hdr, 2) == 2);
|
|
for (uint8_t i = 0; ok && i < list.count; i++) {
|
|
ok = (file.write((uint8_t *)list.entries[i].name, sizeof(list.entries[i].name)) == sizeof(list.entries[i].name));
|
|
ok = ok && (file.write(list.entries[i].key, sizeof(list.entries[i].key)) == sizeof(list.entries[i].key));
|
|
}
|
|
file.close();
|
|
|
|
if (ok) {
|
|
commitTempFile(_fs, "/scopes1.tmp", "/scopes1");
|
|
} else {
|
|
_fs->remove("/scopes1.tmp"); // keep the previous good /scopes1
|
|
}
|
|
}
|
|
|
|
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
|
|
|
#define MAX_ADVERT_PKT_LEN (2 + 32 + PUB_KEY_SIZE + 4 + SIGNATURE_SIZE + MAX_ADVERT_DATA_SIZE)
|
|
|
|
struct BlobRec {
|
|
uint32_t timestamp;
|
|
uint8_t key[7];
|
|
uint8_t len;
|
|
uint8_t data[MAX_ADVERT_PKT_LEN];
|
|
};
|
|
|
|
void DataStore::checkAdvBlobFile() {
|
|
if (!_getContactsChannelsFS()->exists("/adv_blobs")) {
|
|
File file = ::openWrite(_getContactsChannelsFS(), "/adv_blobs");
|
|
if (file) {
|
|
BlobRec zeroes;
|
|
memset(&zeroes, 0, sizeof(zeroes));
|
|
for (int i = 0; i < MAX_BLOBRECS; i++) { // pre-allocate to fixed size
|
|
file.write((uint8_t *) &zeroes, sizeof(zeroes));
|
|
}
|
|
file.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
void DataStore::migrateToSecondaryFS() {
|
|
// migrate old adv_blobs, contacts3 and channels2 files to secondary FS if they don't already exist
|
|
if (!_fsExtra->exists("/adv_blobs")) {
|
|
if (_fs->exists("/adv_blobs")) {
|
|
File oldAdvBlobs = openRead(_fs, "/adv_blobs");
|
|
File newAdvBlobs = ::openWrite(_fsExtra, "/adv_blobs");
|
|
|
|
if (oldAdvBlobs && newAdvBlobs) {
|
|
BlobRec rec;
|
|
size_t count = 0;
|
|
|
|
// Copy 20 BlobRecs from old to new
|
|
while (count < 20 && oldAdvBlobs.read((uint8_t *)&rec, sizeof(rec)) == sizeof(rec)) {
|
|
newAdvBlobs.seek(count * sizeof(BlobRec));
|
|
newAdvBlobs.write((uint8_t *)&rec, sizeof(rec));
|
|
count++;
|
|
}
|
|
}
|
|
if (oldAdvBlobs) oldAdvBlobs.close();
|
|
if (newAdvBlobs) newAdvBlobs.close();
|
|
_fs->remove("/adv_blobs");
|
|
}
|
|
}
|
|
if (!_fsExtra->exists("/contacts3")) {
|
|
if (_fs->exists("/contacts3")) {
|
|
File oldFile = openRead(_fs, "/contacts3");
|
|
File newFile = ::openWrite(_fsExtra, "/contacts3");
|
|
|
|
if (oldFile && newFile) {
|
|
uint8_t buf[64];
|
|
int n;
|
|
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
|
|
newFile.write(buf, n);
|
|
}
|
|
}
|
|
if (oldFile) oldFile.close();
|
|
if (newFile) newFile.close();
|
|
_fs->remove("/contacts3");
|
|
}
|
|
}
|
|
if (!_fsExtra->exists("/channels2")) {
|
|
if (_fs->exists("/channels2")) {
|
|
File oldFile = openRead(_fs, "/channels2");
|
|
File newFile = ::openWrite(_fsExtra, "/channels2");
|
|
|
|
if (oldFile && newFile) {
|
|
uint8_t buf[64];
|
|
int n;
|
|
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
|
|
newFile.write(buf, n);
|
|
}
|
|
}
|
|
if (oldFile) oldFile.close();
|
|
if (newFile) newFile.close();
|
|
_fs->remove("/channels2");
|
|
}
|
|
}
|
|
// cleanup nodes which have been testing the extra fs, copy _main.id and new_prefs back to primary
|
|
if (_fsExtra->exists("/_main.id")) {
|
|
if (_fs->exists("/_main.id")) {_fs->remove("/_main.id");}
|
|
File oldFile = openRead(_fsExtra, "/_main.id");
|
|
File newFile = ::openWrite(_fs, "/_main.id");
|
|
|
|
if (oldFile && newFile) {
|
|
uint8_t buf[64];
|
|
int n;
|
|
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
|
|
newFile.write(buf, n);
|
|
}
|
|
}
|
|
if (oldFile) oldFile.close();
|
|
if (newFile) newFile.close();
|
|
_fsExtra->remove("/_main.id");
|
|
}
|
|
if (_fsExtra->exists("/new_prefs")) {
|
|
if (_fs->exists("/new_prefs")) {_fs->remove("/new_prefs");}
|
|
File oldFile = openRead(_fsExtra, "/new_prefs");
|
|
File newFile = ::openWrite(_fs, "/new_prefs");
|
|
|
|
if (oldFile && newFile) {
|
|
uint8_t buf[64];
|
|
int n;
|
|
while ((n = oldFile.read(buf, sizeof(buf))) > 0) {
|
|
newFile.write(buf, n);
|
|
}
|
|
}
|
|
if (oldFile) oldFile.close();
|
|
if (newFile) newFile.close();
|
|
_fsExtra->remove("/new_prefs");
|
|
}
|
|
// remove files from where they should not be anymore
|
|
if (_fs->exists("/adv_blobs")) {
|
|
_fs->remove("/adv_blobs");
|
|
}
|
|
if (_fs->exists("/contacts3")) {
|
|
_fs->remove("/contacts3");
|
|
}
|
|
if (_fs->exists("/channels2")) {
|
|
_fs->remove("/channels2");
|
|
}
|
|
if (_fsExtra->exists("/_main.id")) {
|
|
_fsExtra->remove("/_main.id");
|
|
}
|
|
if (_fsExtra->exists("/new_prefs")) {
|
|
_fsExtra->remove("/new_prefs");
|
|
}
|
|
}
|
|
|
|
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
|
|
File file = openRead(_getContactsChannelsFS(), "/adv_blobs");
|
|
uint8_t len = 0; // 0 = not found
|
|
if (file) {
|
|
BlobRec tmp;
|
|
while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) {
|
|
if (memcmp(key, tmp.key, sizeof(tmp.key)) == 0) { // only match by 7 byte prefix
|
|
len = tmp.len;
|
|
memcpy(dest_buf, tmp.data, len);
|
|
break;
|
|
}
|
|
}
|
|
file.close();
|
|
}
|
|
return len;
|
|
}
|
|
|
|
bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) {
|
|
if (len < PUB_KEY_SIZE+4+SIGNATURE_SIZE || len > MAX_ADVERT_PKT_LEN) return false;
|
|
checkAdvBlobFile();
|
|
File file = _getContactsChannelsFS()->open("/adv_blobs", FILE_O_WRITE);
|
|
if (file) {
|
|
uint32_t pos = 0, found_pos = 0;
|
|
uint32_t min_timestamp = 0xFFFFFFFF;
|
|
|
|
// search for matching key OR evict by oldest timestamp
|
|
BlobRec tmp;
|
|
file.seek(0);
|
|
while (file.read((uint8_t *) &tmp, sizeof(tmp)) == sizeof(tmp)) {
|
|
if (memcmp(key, tmp.key, sizeof(tmp.key)) == 0) { // only match by 7 byte prefix
|
|
found_pos = pos;
|
|
break;
|
|
}
|
|
if (tmp.timestamp < min_timestamp) {
|
|
min_timestamp = tmp.timestamp;
|
|
found_pos = pos;
|
|
}
|
|
|
|
pos += sizeof(tmp);
|
|
}
|
|
|
|
memcpy(tmp.key, key, sizeof(tmp.key)); // just record 7 byte prefix of key
|
|
memcpy(tmp.data, src_buf, len);
|
|
tmp.len = len;
|
|
tmp.timestamp = _clock->getCurrentTime();
|
|
|
|
file.seek(found_pos);
|
|
file.write((uint8_t *) &tmp, sizeof(tmp));
|
|
|
|
file.close();
|
|
return true;
|
|
}
|
|
return false; // error
|
|
}
|
|
bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) {
|
|
return true; // this is just a stub on NRF52/STM32 platforms
|
|
}
|
|
#else
|
|
inline void makeBlobPath(const uint8_t key[], int key_len, char* path, size_t path_size) {
|
|
char fname[18];
|
|
if (key_len > 8) key_len = 8; // just use first 8 bytes (prefix)
|
|
mesh::Utils::toHex(fname, key, key_len);
|
|
sprintf(path, "/bl/%s", fname);
|
|
}
|
|
|
|
uint8_t DataStore::getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]) {
|
|
char path[64];
|
|
makeBlobPath(key, key_len, path, sizeof(path));
|
|
|
|
if (_fs->exists(path)) {
|
|
File f = openRead(_fs, path);
|
|
if (f) {
|
|
int len = f.read(dest_buf, 255); // currently MAX 255 byte blob len supported!!
|
|
f.close();
|
|
return len;
|
|
}
|
|
}
|
|
return 0; // not found
|
|
}
|
|
|
|
bool DataStore::putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len) {
|
|
char path[64];
|
|
makeBlobPath(key, key_len, path, sizeof(path));
|
|
|
|
File f = ::openWrite(_fs, path);
|
|
if (f) {
|
|
int n = f.write(src_buf, len);
|
|
f.close();
|
|
if (n == len) return true; // success!
|
|
|
|
_fs->remove(path); // blob was only partially written!
|
|
}
|
|
return false; // error
|
|
}
|
|
|
|
bool DataStore::deleteBlobByKey(const uint8_t key[], int key_len) {
|
|
char path[64];
|
|
makeBlobPath(key, key_len, path, sizeof(path));
|
|
|
|
_fs->remove(path);
|
|
|
|
return true; // return true even if file did not exist
|
|
}
|
|
#endif
|