Files
MeshCore/variants/m5stack_cardputer_adv/CardputerDataStore.cpp
T
mikecarper 9a2211e85a Fix Cardputer delivery, live settings and power management
Keep selected chats bound to full contact keys and route keyboard channel sends through the active transport. Deliver accepted plain, room and Meshtext messages to local history and notifications once; disarm rejected sends and retries.

Use shared live radio settings, GPS acquisition and power-saving policy, screen timers and red night palette. Report save failures, preserve verified custom preferences, and guard display controls and shutdown.

Add executable sanitizer regressions for routing, retries, selection, storage faults, UI lifecycle and GPS. Both Companion builds and the shared T-Deck regression pass; no physical Cardputer testing performed.
2026-10-27 22:59:26 -07:00

231 lines
7.5 KiB
C++

#include "CardputerDataStore.h"
#include <SD.h> // Keep optional framework-SD discovery local to this variant.
#include <helpers/ContactFileTransaction.h>
#include <errno.h>
#include <math.h>
#include <sys/stat.h>
#include "globals.h"
static void buf_to_hex_fast(const unsigned char *src, size_t src_len, char *dst) {
static const char hex_digits[] = "0123456789abcdef";
for (size_t i = 0; i < src_len; i++) {
dst[i * 2] = hex_digits[(src[i] >> 4) & 0x0F]; // High nibble
dst[i * 2 + 1] = hex_digits[src[i] & 0x0F]; // Low nibble
}
dst[src_len * 2] = '\0';
}
void CardputerDataStore::beginCustom(FILESYSTEM *history_fs) {
_history_fs = history_fs;
if (!_history_fs) {
return;
}
_history_fs->mkdir(CUSTOM_DATA_DIR);
_history_fs->mkdir(HISTORY_DIR);
_history_fs->mkdir(HISTORY_DIR_CHANNEL);
_history_fs->mkdir(HISTORY_DIR_DIRECT);
}
static bool validCustomPrefs(const CustomNodePrefs &prefs) {
return isfinite(prefs.battery_correction) && prefs.battery_correction >= 0.5f
&& prefs.battery_correction <= 2.0f && prefs.text_level < 3;
}
static bool customPrefsPresence(FILESYSTEM *fs, const char *path, bool &present) {
(void)fs; // Custom preferences always use the default internal SPIFFS mount.
char vfs_path[96];
const int length = snprintf(vfs_path, sizeof(vfs_path), "/spiffs%s", path);
if (length < 0 || static_cast<size_t>(length) >= sizeof(vfs_path)) return false;
struct stat info;
const int result = ::stat(vfs_path, &info);
if (result != 0 && errno != ENOENT) return false;
present = result == 0;
return true;
}
static bool readCustomPrefs(FILESYSTEM *fs, const char *path, CustomNodePrefs &prefs) {
File file = fs->open(path, FILE_READ);
if (!file) return false;
CustomNodePrefs candidate;
const bool valid = !file.isDirectory() && file.size() == sizeof(candidate)
&& file.read(reinterpret_cast<uint8_t *>(&candidate), sizeof(candidate)) == sizeof(candidate)
&& validCustomPrefs(candidate);
file.close();
if (valid) prefs = candidate;
return valid;
}
bool CardputerDataStore::loadCustomPrefs(CustomNodePrefs &prefs) {
_custom_prefs_write_blocked = false;
bool present = false, backup_present = false;
if (!customPrefsPresence(_fs, CUSTOM_PREFS_FILE, present)
|| !customPrefsPresence(_fs, CUSTOM_PREFS_FILE ".bak", backup_present)) {
_custom_prefs_write_blocked = true;
return false;
}
if (present && readCustomPrefs(_fs, CUSTOM_PREFS_FILE, prefs)) return true;
CustomNodePrefs backup;
if (backup_present && readCustomPrefs(_fs, CUSTOM_PREFS_FILE ".bak", backup)) {
prefs = backup;
if (present && !_fs->remove(CUSTOM_PREFS_FILE)) {
_custom_prefs_write_blocked = true;
return true;
}
_fs->rename(CUSTOM_PREFS_FILE ".bak", CUSTOM_PREFS_FILE);
CustomNodePrefs verified;
_custom_prefs_write_blocked = !readCustomPrefs(_fs, CUSTOM_PREFS_FILE, verified)
|| memcmp(&verified, &backup, sizeof(backup)) != 0;
return true;
}
// An unreadable existing image is not permission to replace its only copy.
_custom_prefs_write_blocked = present || backup_present;
return false;
}
bool CardputerDataStore::saveCustomPrefs(const CustomNodePrefs &prefs) {
if (_custom_prefs_write_blocked || !validCustomPrefs(prefs)) return false;
mesh::ContactFileTransaction transaction(_fs, CUSTOM_PREFS_FILE, customPrefsPresence);
bool valid = transaction && transaction.write(reinterpret_cast<const uint8_t *>(&prefs), sizeof(prefs)) == sizeof(prefs);
mesh::ContactFileTransaction::CommitProgress progress;
do {
progress = transaction.serviceCommit(valid, 1, true);
} while (progress == mesh::ContactFileTransaction::CommitProgress::Pending);
if (progress != mesh::ContactFileTransaction::CommitProgress::Succeeded) {
_custom_prefs_write_blocked = transaction.outcome() == mesh::ContactFileTransaction::Outcome::Indeterminate;
return false;
}
CustomNodePrefs verified;
if (readCustomPrefs(_fs, CUSTOM_PREFS_FILE, verified) && memcmp(&verified, &prefs, sizeof(prefs)) == 0) {
_fs->remove(CUSTOM_PREFS_FILE ".bak");
return true;
}
// Keep the old verified image until the public candidate has been read back.
CustomNodePrefs backup;
bool backup_present = false;
if (!customPrefsPresence(_fs, CUSTOM_PREFS_FILE ".bak", backup_present)
|| (backup_present && !readCustomPrefs(_fs, CUSTOM_PREFS_FILE ".bak", backup))
|| !_fs->remove(CUSTOM_PREFS_FILE)) {
_custom_prefs_write_blocked = true;
return false;
}
if (backup_present) {
_fs->rename(CUSTOM_PREFS_FILE ".bak", CUSTOM_PREFS_FILE);
_custom_prefs_write_blocked = !readCustomPrefs(_fs, CUSTOM_PREFS_FILE, verified)
|| memcmp(&verified, &backup, sizeof(backup)) != 0;
} else {
bool present = false;
_custom_prefs_write_blocked = !customPrefsPresence(_fs, CUSTOM_PREFS_FILE, present) || present;
}
return false;
}
void CardputerDataStore::storeMessage(const uint8_t pkey[PUB_KEY_SIZE], const char *text, bool is_sent,
bool is_channel) {
if (!_history_fs) {
return;
}
char path[128];
char msg_copy[MAX_MESSAGE_LENGTH + 1] = { 0 };
char pkey_hex[PUB_KEY_SIZE * 2 + 1];
uint8_t key_len = is_channel ? (PUB_KEY_SIZE / 2) : PUB_KEY_SIZE;
buf_to_hex_fast(pkey, key_len, pkey_hex);
int msg_len;
for (msg_len = 0; msg_len < MAX_MESSAGE_LENGTH && text[msg_len] != '\0'; msg_len++) {
if (text[msg_len] == '\n' || text[msg_len] == '\r') {
msg_copy[msg_len] = ' '; // Swap newline with space
} else {
msg_copy[msg_len] = text[msg_len];
}
}
msg_copy[msg_len] = '\0';
snprintf(path, sizeof(path), "%s/%s", is_channel ? HISTORY_DIR_CHANNEL : HISTORY_DIR_DIRECT, pkey_hex);
File file = _history_fs->open(path, FILE_APPEND, true);
if (!file) {
return;
}
file.print(is_sent ? ">> " : "<< ");
file.println(msg_copy);
file.close();
}
static void push_history_line(ChatHistory &history, char *buf) {
int len = strlen(buf);
if (len < 4 || (buf[0] != '>' && buf[0] != '<') || buf[0] != buf[1]) {
return;
}
HistoryMessage *msg = history.push_ref();
msg->out = buf[0] == '>';
strncpy(msg->text, buf + 3, sizeof(msg->text));
msg->text[sizeof(msg->text) - 1] = '\0';
}
void CardputerDataStore::loadMessages(const uint8_t pkey[PUB_KEY_SIZE], bool is_channel,
ChatHistory &history) {
history.clear();
if (!_history_fs) {
return;
}
char path[128];
char pkey_hex[PUB_KEY_SIZE * 2 + 1];
char line_buf[256] = { 0 };
uint8_t file_read_buf[1024];
uint8_t key_len = is_channel ? (PUB_KEY_SIZE / 2) : PUB_KEY_SIZE;
buf_to_hex_fast(pkey, key_len, pkey_hex);
snprintf(path, sizeof(path), "%s/%s", is_channel ? HISTORY_DIR_CHANNEL : HISTORY_DIR_DIRECT, pkey_hex);
if (!_history_fs->exists(path)) {
return;
}
File file = _history_fs->open(path, FILE_READ, false);
if (!file) {
return;
}
size_t line_idx = 0;
while (file.available()) {
int bytesRead = file.read(file_read_buf, sizeof(file_read_buf));
if (bytesRead <= 0) {
break;
}
for (int b = 0; b < bytesRead; b++) {
char c = (char)file_read_buf[b];
if (c == '\n' || line_idx >= sizeof(line_buf) - 1) {
line_buf[line_idx] = '\0';
line_idx = 0;
push_history_line(history, line_buf);
} else if (c != '\r') {
line_buf[line_idx++] = c;
}
}
}
// Handle any remaining text if the file didn't end with a trailing LF
if (line_idx > 0) {
line_buf[line_idx] = '\0';
push_history_line(history, line_buf);
}
file.close();
}