mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 18:31:58 +00:00
refactor(prefs): settings saved as /prefs.json, upstream's ConfigSerializer
NodePrefs is now NodePrefsFields (the fields) plus how they're saved: one JSON key per field, upstream's keys under "radio", "gps", "repeat", "comp" and "custom" (so either firmware reads what the other wrote of them), ours under "solo". Saved atomically (temp file, then renamed), through a buffer. 2.0-rc's binary /prefs is removed, not converted: 2.0 starts from the defaults; /new_prefs from before 2.0 is still imported. From upstream with it: txdelay / direct.txdelay / agc.reset.interval (prefs and CLI), and the board's own settings through getCustom() (Board::attachDynamicPrefs) in place of the merge's stopgap adapter. ConfigSerializer reads values up to 191 characters (was 127): 140-byte texts, an 80-byte table in hex. Test: every field round-trips through the JSON byte for byte; the alarm label tests expect "Once" (the label changed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -218,25 +218,40 @@ bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Prefs ──
|
// ── Prefs ──
|
||||||
// /prefs: a header, the node's position, then NodePrefs as it is in memory
|
// /prefs.json (NodePrefs::structure()). The JSON goes through a buffer: the
|
||||||
// (see the comment at the top of NodePrefs.h). Only the build that wrote it
|
// serializer reads and writes it a character at a time, each a file call.
|
||||||
// reads it back, so the struct's padding and alignment are no concern.
|
class BufferedFile : public Stream {
|
||||||
struct PrefsHeader {
|
File& _f;
|
||||||
uint32_t magic; // PREFS_MAGIC
|
uint8_t _buf[256];
|
||||||
uint16_t version; // NodePrefs::PREFS_VERSION
|
size_t _n = 0, _at = 0;
|
||||||
uint16_t size; // sizeof(NodePrefs) when written
|
bool _ok = true;
|
||||||
uint32_t crc; // CRC-32 of the position and the struct's bytes
|
public:
|
||||||
};
|
explicit BufferedFile(File& f) : _f(f) { }
|
||||||
static const uint32_t PREFS_MAGIC = 0x534F4C4F; // "SOLO"
|
// Reading (a file opened to read).
|
||||||
|
int available() override { fill(); return (int)(_n - _at); }
|
||||||
static uint32_t crc32(uint32_t crc, const uint8_t* p, size_t n) {
|
int read() override { fill(); return _at < _n ? _buf[_at++] : -1; }
|
||||||
crc = ~crc;
|
int peek() override { fill(); return _at < _n ? _buf[_at] : -1; }
|
||||||
while (n--) {
|
void fill() {
|
||||||
crc ^= *p++;
|
if (_at < _n) return;
|
||||||
for (int k = 0; k < 8; k++) crc = (crc >> 1) ^ (0xEDB88320u & (0u - (crc & 1)));
|
int got = (int)_f.read(_buf, sizeof(_buf));
|
||||||
|
_n = got > 0 ? (size_t)got : 0;
|
||||||
|
_at = 0;
|
||||||
}
|
}
|
||||||
return ~crc;
|
// 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
|
// 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.
|
// as the default rather than as a setting no screen can show.
|
||||||
@@ -425,37 +440,18 @@ static void removeStrayDir(FILESYSTEM* fs, const char* path) {
|
|||||||
// `prefs` comes in holding the defaults; what the file holds replaces them.
|
// `prefs` comes in holding the defaults; what the file holds replaces them.
|
||||||
void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) {
|
void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) {
|
||||||
removeStrayDir(_fs, "/prefs");
|
removeStrayDir(_fs, "/prefs");
|
||||||
File file = openRead(_fs, "/prefs");
|
if (_fs->exists("/prefs")) _fs->remove("/prefs"); // 2.0-rc's binary settings: 2.0 starts from the defaults
|
||||||
|
File file = openRead(_fs, "/prefs.json");
|
||||||
if (file) {
|
if (file) {
|
||||||
PrefsHeader h;
|
BufferedFile in(file);
|
||||||
NodePrefs* tmp = new NodePrefs(prefs); // a field the file is too short for keeps its default
|
prefs.node_lat = node_lat;
|
||||||
double lat = 0, lon = 0;
|
prefs.node_lon = node_lon;
|
||||||
bool ok = file.read((uint8_t*)&h, sizeof(h)) == sizeof(h)
|
// Saved whole (a temp file, then renamed): a file that doesn't parse to
|
||||||
&& h.magic == PREFS_MAGIC && h.version == NodePrefs::PREFS_VERSION
|
// the end was damaged since. What was read of it stays.
|
||||||
&& file.read((uint8_t*)&lat, sizeof(lat)) == sizeof(lat)
|
if (!prefs.loadSerial(in)) MESH_DEBUG_PRINTLN("prefs: /prefs.json damaged, the rest from the defaults");
|
||||||
&& file.read((uint8_t*)&lon, sizeof(lon)) == sizeof(lon);
|
|
||||||
if (ok) {
|
|
||||||
const size_t n = h.size < sizeof(NodePrefs) ? h.size : sizeof(NodePrefs);
|
|
||||||
ok = file.read((uint8_t*)tmp, n) == (int)n;
|
|
||||||
uint32_t crc = crc32(0, (const uint8_t*)&lat, sizeof(lat));
|
|
||||||
crc = crc32(crc, (const uint8_t*)&lon, sizeof(lon));
|
|
||||||
crc = crc32(crc, (const uint8_t*)tmp, n);
|
|
||||||
// Written by a newer build (a downgrade): its extra fields are skipped,
|
|
||||||
// but the CRC covers them, so read them through it rather than losing
|
|
||||||
// every setting to a mismatch.
|
|
||||||
for (size_t left = h.size - n; ok && left > 0; ) {
|
|
||||||
uint8_t buf[32];
|
|
||||||
size_t k = left < sizeof(buf) ? left : sizeof(buf);
|
|
||||||
ok = file.read(buf, k) == (int)k;
|
|
||||||
crc = crc32(crc, buf, k);
|
|
||||||
left -= k;
|
|
||||||
}
|
|
||||||
ok = ok && crc == h.crc;
|
|
||||||
}
|
|
||||||
file.close();
|
file.close();
|
||||||
if (ok) { prefs = *tmp; node_lat = lat; node_lon = lon; }
|
node_lat = prefs.node_lat;
|
||||||
else MESH_DEBUG_PRINTLN("prefs: /prefs unreadable (another version or damaged), defaults kept");
|
node_lon = prefs.node_lon;
|
||||||
delete tmp;
|
|
||||||
} else { // first boot after an update from before 2.0 (/node_prefs: older still, the same layout)
|
} 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" }) {
|
for (const char* old : { "/new_prefs", "/node_prefs" }) {
|
||||||
if (!_fs->exists(old)) continue;
|
if (!_fs->exists(old)) continue;
|
||||||
@@ -468,25 +464,23 @@ void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
prefs.clearDirty();
|
||||||
sanitize(prefs);
|
sanitize(prefs);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DataStore::savePrefs(const NodePrefs& prefs, double node_lat, double node_lon) {
|
void DataStore::savePrefs(NodePrefs& prefs, double node_lat, double node_lon) {
|
||||||
// Atomic temp-then-rename (see commitTempFile): an interrupted save leaves
|
// Atomic temp-then-rename (see commitTempFile): an interrupted save leaves
|
||||||
// the previous /prefs in place.
|
// the previous /prefs.json in place.
|
||||||
File file = ::openWrite(_fs, "/prefs.tmp");
|
File file = ::openWrite(_fs, "/prefs.tmp");
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
PrefsHeader h = { PREFS_MAGIC, NodePrefs::PREFS_VERSION, (uint16_t)sizeof(NodePrefs), 0 };
|
prefs.node_lat = node_lat;
|
||||||
h.crc = crc32(0, (const uint8_t*)&node_lat, sizeof(node_lat));
|
prefs.node_lon = node_lon;
|
||||||
h.crc = crc32(h.crc, (const uint8_t*)&node_lon, sizeof(node_lon));
|
BufferedFile out(file);
|
||||||
h.crc = crc32(h.crc, (const uint8_t*)&prefs, sizeof(NodePrefs));
|
bool ok = prefs.saveSerial(out);
|
||||||
// Once the flash fills, writes come back short: only a whole record replaces the old one.
|
out.flush();
|
||||||
bool ok = file.write((const uint8_t*)&h, sizeof(h)) == sizeof(h)
|
ok = ok && out.ok(); // once the flash fills, writes come back short
|
||||||
&& file.write((const uint8_t*)&node_lat, sizeof(node_lat)) == sizeof(node_lat)
|
|
||||||
&& file.write((const uint8_t*)&node_lon, sizeof(node_lon)) == sizeof(node_lon)
|
|
||||||
&& file.write((const uint8_t*)&prefs, sizeof(NodePrefs)) == sizeof(NodePrefs);
|
|
||||||
file.close();
|
file.close();
|
||||||
if (ok) commitTempFile(_fs, "/prefs.tmp", "/prefs");
|
if (ok) { commitTempFile(_fs, "/prefs.tmp", "/prefs.json"); prefs.clearDirty(); }
|
||||||
else _fs->remove("/prefs.tmp");
|
else _fs->remove("/prefs.tmp");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public:
|
|||||||
bool loadMainIdentity(mesh::LocalIdentity &identity);
|
bool loadMainIdentity(mesh::LocalIdentity &identity);
|
||||||
bool saveMainIdentity(const mesh::LocalIdentity &identity);
|
bool saveMainIdentity(const mesh::LocalIdentity &identity);
|
||||||
void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon);
|
void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon);
|
||||||
void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon);
|
void savePrefs(NodePrefs& prefs, double node_lat, double node_lon);
|
||||||
void loadContacts(DataStoreHost* host);
|
void loadContacts(DataStoreHost* host);
|
||||||
void saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL);
|
void saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL);
|
||||||
// Returns true if a channels file (current or legacy) existed and was
|
// Returns true if a channels file (current or legacy) existed and was
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
|
uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
|
||||||
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f);
|
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.tx_delay_factor);
|
||||||
uint32_t d = getRNG()->nextInt(0, 5*t + 1);
|
uint32_t d = getRNG()->nextInt(0, 5*t + 1);
|
||||||
// Yield filter (Tools > Repeater): scale the flood retransmit delay so a
|
// Yield filter (Tools > Repeater): scale the flood retransmit delay so a
|
||||||
// mobile companion waits longer and lets better-sited fixed repeaters win the
|
// mobile companion waits longer and lets better-sited fixed repeaters win the
|
||||||
@@ -320,7 +320,7 @@ uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) {
|
|||||||
return d * (1 + _prefs.repeat_delay_boost);
|
return d * (1 + _prefs.repeat_delay_boost);
|
||||||
}
|
}
|
||||||
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
|
uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) {
|
||||||
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.2f);
|
uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * _prefs.direct_tx_delay_factor);
|
||||||
return getRNG()->nextInt(0, 5*t + 1);
|
return getRNG()->nextInt(0, 5*t + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1316,29 +1316,6 @@ void MyMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
|
|||||||
if (_listener) _listener->onACKRecv(ack_crc);
|
if (_listener) _listener->onACKRecv(ack_crc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The board's own settings (upstream's dynamic prefs, a key-value store), over
|
|
||||||
// the NodePrefs fields that hold them here: a board with a LoRa front end
|
|
||||||
// reads "fem_rxgain" / "fem_txgain" at begin() and writes them from its CLI.
|
|
||||||
bool MyMesh::BoardPrefs::getByKey(const char* key, char* value, size_t max_len) {
|
|
||||||
uint8_t* f = field(key);
|
|
||||||
if (!f || max_len < 2) return false;
|
|
||||||
value[0] = *f ? '1' : '0';
|
|
||||||
value[1] = 0;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
bool MyMesh::BoardPrefs::setByKey(const char* key, const char* value) {
|
|
||||||
uint8_t* f = field(key);
|
|
||||||
if (!f) return false;
|
|
||||||
*f = strcmp(value, "1") == 0 ? 1 : 0;
|
|
||||||
_mesh->savePrefs();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
uint8_t* MyMesh::BoardPrefs::field(const char* key) {
|
|
||||||
if (strcmp(key, "fem_rxgain") == 0) return &_mesh->_prefs.radio_fem_rxgain;
|
|
||||||
if (strcmp(key, "fem_txgain") == 0) return &_mesh->_prefs.radio_fem_txgain;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store)
|
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store)
|
||||||
// Sized to match simple_repeater's pool (32), not the old client-only 16: with the
|
// Sized to match simple_repeater's pool (32), not the old client-only 16: with the
|
||||||
// on-device Repeater toggle, queued retransmits (adverts/channel flood from
|
// on-device Repeater toggle, queued retransmits (adverts/channel flood from
|
||||||
@@ -1347,7 +1324,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
|||||||
// silently drops every incoming packet — DMs and channels included — until a slot
|
// silently drops every incoming packet — DMs and channels included — until a slot
|
||||||
// frees up.
|
// frees up.
|
||||||
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(32), tables),
|
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(32), tables),
|
||||||
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _listener(NULL), _iter(0), _board_prefs(this) {
|
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _listener(NULL), _iter(0) {
|
||||||
_iter_started = false;
|
_iter_started = false;
|
||||||
_cli_rescue = false;
|
_cli_rescue = false;
|
||||||
cli_command[0] = 0;
|
cli_command[0] = 0;
|
||||||
@@ -1392,9 +1369,11 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
|||||||
repeat_scope_count = 0;
|
repeat_scope_count = 0;
|
||||||
|
|
||||||
// defaults
|
// defaults
|
||||||
memset(&_prefs, 0, sizeof(_prefs));
|
_prefs.clearFields();
|
||||||
memset(_prefs.ch_scope_idx, NodePrefs::CH_SCOPE_DEFAULT, sizeof(_prefs.ch_scope_idx));
|
memset(_prefs.ch_scope_idx, NodePrefs::CH_SCOPE_DEFAULT, sizeof(_prefs.ch_scope_idx));
|
||||||
_prefs.airtime_factor = 1.0;
|
_prefs.airtime_factor = 1.0;
|
||||||
|
_prefs.tx_delay_factor = 0.5f;
|
||||||
|
_prefs.direct_tx_delay_factor = 0.2f;
|
||||||
strcpy(_prefs.node_name, "NONAME");
|
strcpy(_prefs.node_name, "NONAME");
|
||||||
_prefs.freq = LORA_FREQ;
|
_prefs.freq = LORA_FREQ;
|
||||||
_prefs.sf = LORA_SF;
|
_prefs.sf = LORA_SF;
|
||||||
@@ -1507,6 +1486,8 @@ void MyMesh::begin() {
|
|||||||
if (isnan(_prefs.airtime_factor) || isinf(_prefs.airtime_factor)) _prefs.airtime_factor = 0;
|
if (isnan(_prefs.airtime_factor) || isinf(_prefs.airtime_factor)) _prefs.airtime_factor = 0;
|
||||||
if (isnan(_prefs.rx_delay_base) || isinf(_prefs.rx_delay_base)) _prefs.rx_delay_base = 0;
|
if (isnan(_prefs.rx_delay_base) || isinf(_prefs.rx_delay_base)) _prefs.rx_delay_base = 0;
|
||||||
_prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
|
_prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
|
||||||
|
_prefs.tx_delay_factor = constrain(_prefs.tx_delay_factor, 0, 2.0f);
|
||||||
|
_prefs.direct_tx_delay_factor = constrain(_prefs.direct_tx_delay_factor, 0, 2.0f);
|
||||||
_prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f);
|
_prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f);
|
||||||
_prefs.freq = constrain(_prefs.freq, 150.0f, 2500.0f);
|
_prefs.freq = constrain(_prefs.freq, 150.0f, 2500.0f);
|
||||||
_prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f);
|
_prefs.bw = constrain(_prefs.bw, 7.8f, 500.0f);
|
||||||
@@ -1540,7 +1521,7 @@ void MyMesh::begin() {
|
|||||||
#else
|
#else
|
||||||
radio_driver.setPowerSaving(false); // see Features.h FEAT_RX_POWERSAVE -- ignore any stale persisted rx_powersave byte
|
radio_driver.setPowerSaving(false); // see Features.h FEAT_RX_POWERSAVE -- ignore any stale persisted rx_powersave byte
|
||||||
#endif
|
#endif
|
||||||
board.attachDynamicPrefs(&_board_prefs); // FEM gains: the board reads them now, and its CLI sets them
|
board.attachDynamicPrefs(_prefs.getCustom()); // the board's own settings (FEM gains): read now, set by its CLI
|
||||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
#include <Mesh.h>
|
#include <Mesh.h>
|
||||||
#include <helpers/ui/DisplayDriver.h>
|
#include <helpers/ui/DisplayDriver.h>
|
||||||
#include "Features.h" // FEAT_RX_POWERSAVE, used by MyMesh.cpp
|
#include "Features.h" // FEAT_RX_POWERSAVE, used by MyMesh.cpp
|
||||||
#include <helpers/KeyValueStore.h>
|
|
||||||
|
|
||||||
// Forward declaration for UITask
|
// Forward declaration for UITask
|
||||||
class UITask;
|
class UITask;
|
||||||
@@ -285,6 +284,9 @@ protected:
|
|||||||
float getAirtimeBudgetFactor() const override;
|
float getAirtimeBudgetFactor() const override;
|
||||||
int getInterferenceThreshold() const override;
|
int getInterferenceThreshold() const override;
|
||||||
bool getCADEnabled() const override;
|
bool getCADEnabled() const override;
|
||||||
|
int getAGCResetInterval() const override {
|
||||||
|
return ((int)_prefs.agc_reset_interval) * 4000; // milliseconds
|
||||||
|
}
|
||||||
int calcRxDelay(float score, uint32_t air_time) const override;
|
int calcRxDelay(float score, uint32_t air_time) const override;
|
||||||
uint32_t getRetransmitDelay(const mesh::Packet *packet) override;
|
uint32_t getRetransmitDelay(const mesh::Packet *packet) override;
|
||||||
uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override;
|
uint32_t getDirectRetransmitDelay(const mesh::Packet *packet) override;
|
||||||
@@ -699,15 +701,6 @@ private:
|
|||||||
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
|
bool send_unscoped; // force un-scoped flood (instead of using send_scope)
|
||||||
char cli_command[80];
|
char cli_command[80];
|
||||||
char reply_buf[166];
|
char reply_buf[166];
|
||||||
class BoardPrefs : public KeyValueStore { // MyMesh.cpp
|
|
||||||
MyMesh* _mesh;
|
|
||||||
uint8_t* field(const char* key);
|
|
||||||
public:
|
|
||||||
BoardPrefs(MyMesh* mesh) : _mesh(mesh) { }
|
|
||||||
bool setByKey(const char* key, const char* value) override;
|
|
||||||
bool getByKey(const char* key, char* value, size_t max_len) override;
|
|
||||||
};
|
|
||||||
BoardPrefs _board_prefs;
|
|
||||||
uint8_t app_target_ver;
|
uint8_t app_target_ver;
|
||||||
uint8_t *sign_data;
|
uint8_t *sign_data;
|
||||||
uint32_t sign_data_len;
|
uint32_t sign_data_len;
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ bool MyMesh::handleCliCommand(const char* command, char* reply, int n) {
|
|||||||
command += 3;
|
command += 3;
|
||||||
}
|
}
|
||||||
if (cliRadioCommand(command, reply, n)) return true;
|
if (cliRadioCommand(command, reply, n)) return true;
|
||||||
if (n >= 64 && board.handleCommand(command, 0, reply)) return true; // the board's own (radio.fem.*); short replies
|
if (n >= 64 && board.handleCommand(command, 0, reply)) { // the board's own (radio.fem.*); short replies
|
||||||
|
if (_prefs.isDirty()) savePrefs();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (strcmp(command, "reboot") == 0 || strcmp(command, "poweroff") == 0 || strcmp(command, "shutdown") == 0) {
|
if (strcmp(command, "reboot") == 0 || strcmp(command, "poweroff") == 0 || strcmp(command, "shutdown") == 0) {
|
||||||
bool restart = command[0] == 'r';
|
bool restart = command[0] == 'r';
|
||||||
@@ -185,6 +188,25 @@ bool MyMesh::cliRadioCommand(const char* command, char* reply, int n) {
|
|||||||
else { _prefs.rx_delay_base = d; savePrefs(); snprintf(reply, n, "OK"); }
|
else { _prefs.rx_delay_base = d; savePrefs(); snprintf(reply, n, "OK"); }
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (strcmp(command, "get txdelay") == 0) { snprintf(reply, n, "> %s", StrHelper::ftoa(_prefs.tx_delay_factor)); return true; }
|
||||||
|
if (strcmp(command, "get direct.txdelay") == 0) { snprintf(reply, n, "> %s", StrHelper::ftoa(_prefs.direct_tx_delay_factor)); return true; }
|
||||||
|
if (memcmp(command, "set txdelay ", 12) == 0 || memcmp(command, "set direct.txdelay ", 19) == 0) {
|
||||||
|
const bool direct = command[4] == 'd';
|
||||||
|
float f = atof(&command[direct ? 19 : 12]);
|
||||||
|
if (!(f >= 0 && f <= 2.0f)) { snprintf(reply, n, "Error, must be 0-2"); return true; }
|
||||||
|
(direct ? _prefs.direct_tx_delay_factor : _prefs.tx_delay_factor) = f;
|
||||||
|
savePrefs();
|
||||||
|
snprintf(reply, n, "OK");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (strcmp(command, "get agc.reset.interval") == 0) { snprintf(reply, n, "> %d", _prefs.agc_reset_interval * 4); return true; }
|
||||||
|
if (memcmp(command, "set agc.reset.interval ", 23) == 0) {
|
||||||
|
int secs = constrain(atoi(&command[23]), 0, 255 * 4);
|
||||||
|
_prefs.agc_reset_interval = (uint8_t)(secs / 4);
|
||||||
|
savePrefs();
|
||||||
|
snprintf(reply, n, "OK - interval rounded to %d", _prefs.agc_reset_interval * 4);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (strcmp(command, "get path.hash.mode") == 0) { snprintf(reply, n, "> %d", (int)_prefs.path_hash_mode); return true; }
|
if (strcmp(command, "get path.hash.mode") == 0) { snprintf(reply, n, "> %d", (int)_prefs.path_hash_mode); return true; }
|
||||||
if (memcmp(command, "set path.hash.mode ", 19) == 0) {
|
if (memcmp(command, "set path.hash.mode ", 19) == 0) {
|
||||||
int m = atoi(&command[19]);
|
int m = atoi(&command[19]);
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
#include <helpers/ConfigSerializer.h>
|
||||||
|
#include <helpers/DynamicConfigSerializer.h>
|
||||||
|
|
||||||
// Firmware's own boot-default radio params — used both as the companion's
|
// Firmware's own boot-default radio params — used both as the companion's
|
||||||
// initial freq/sf/bw/cr (MyMesh.cpp) and, here, as the seed for a never-
|
// initial freq/sf/bw/cr (MyMesh.cpp) and, here, as the seed for a never-
|
||||||
@@ -44,12 +46,11 @@ static inline float defaultRepeaterFreqForBand(float companion_freq) {
|
|||||||
#define ADVERT_SOUND_SCOPE_ALL 0
|
#define ADVERT_SOUND_SCOPE_ALL 0
|
||||||
#define ADVERT_SOUND_SCOPE_ZERO_HOP 1
|
#define ADVERT_SOUND_SCOPE_ZERO_HOP 1
|
||||||
|
|
||||||
struct NodePrefs { // persisted to file
|
// The settings' fields. NodePrefs (below) adds how they're saved: /prefs.json,
|
||||||
// Saved to /prefs as it is in memory (DataStore::savePrefs): a new field
|
// one key per field (NodePrefs::structure()). A new field goes anywhere here
|
||||||
// goes at the END of the struct and needs nothing else -- an older file is
|
// and gets a key there; a file without it leaves the default (MyMesh's
|
||||||
// shorter, so the field keeps its default (MyMesh's constructor). Moving,
|
// constructor), and a key no field has any more is skipped.
|
||||||
// resizing or removing a field shifts the ones after it: bump PREFS_VERSION
|
struct NodePrefsFields {
|
||||||
// then, and every device starts again from the defaults.
|
|
||||||
|
|
||||||
// ── Identity & scope ──────────────────────────────────────────────────
|
// ── Identity & scope ──────────────────────────────────────────────────
|
||||||
char node_name[32];
|
char node_name[32];
|
||||||
@@ -95,6 +96,12 @@ struct NodePrefs { // persisted to file
|
|||||||
// these yet: they keep the constructor default (MyMesh.cpp).
|
// these yet: they keep the constructor default (MyMesh.cpp).
|
||||||
uint8_t radio_fem_rxgain;
|
uint8_t radio_fem_rxgain;
|
||||||
uint8_t radio_fem_txgain;
|
uint8_t radio_fem_txgain;
|
||||||
|
// Retransmit delay factors (x airtime, the random window for a forwarded
|
||||||
|
// flood / direct packet) and the AGC reset interval (seconds / 4, 0 = off),
|
||||||
|
// as upstream's (CLI txdelay / direct.txdelay / agc.reset.interval).
|
||||||
|
float tx_delay_factor;
|
||||||
|
float direct_tx_delay_factor;
|
||||||
|
uint8_t agc_reset_interval;
|
||||||
// User-saved radio presets, written by the "Save current..." entry in the
|
// User-saved radio presets, written by the "Save current..." entry in the
|
||||||
// shared preset picker (Settings > Radio and Tools > Repeater both populate
|
// shared preset picker (Settings > Radio and Tools > Repeater both populate
|
||||||
// these same slots). name[0] == '\0' marks an empty slot.
|
// these same slots). name[0] == '\0' marks an empty slot.
|
||||||
@@ -624,8 +631,6 @@ struct NodePrefs { // persisted to file
|
|||||||
|
|
||||||
// /prefs' layout (see the comment at the top). A file of another version
|
// /prefs' layout (see the comment at the top). A file of another version
|
||||||
// isn't read: the defaults stand.
|
// isn't read: the defaults stand.
|
||||||
static const uint16_t PREFS_VERSION = 2; // 2: 2.0 layout, rc1-rc3 files start from the defaults
|
|
||||||
|
|
||||||
// Bit-index for each home page. Used by page_order (entries store bit+1) and
|
// Bit-index for each home page. Used by page_order (entries store bit+1) and
|
||||||
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
|
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
|
||||||
// and SettingsScreen::homePageBitIndex route their per-enum lookups through these.
|
// and SettingsScreen::homePageBitIndex route their per-enum lookups through these.
|
||||||
@@ -696,6 +701,264 @@ struct NodePrefs { // persisted to file
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// The settings, saved as /prefs.json (DataStore). What upstream's companion
|
||||||
|
// has is under its keys -- "radio", "gps", "repeat", "comp", "custom" -- so
|
||||||
|
// either firmware reads what the other wrote of it; the rest is under "solo".
|
||||||
|
// Keys are at most 15 characters (ConfigSerializer). Byte arrays and 64-bit
|
||||||
|
// masks go as hex.
|
||||||
|
class NodePrefs : public NodePrefsFields, public ConfigSerializer {
|
||||||
|
// A section: a ConfigSerializer whose structure() is one of NodePrefs's.
|
||||||
|
class Section : public ConfigSerializer {
|
||||||
|
NodePrefs* _p;
|
||||||
|
void (NodePrefs::*_fn)(Section&);
|
||||||
|
protected:
|
||||||
|
void structure() override { (_p->*_fn)(*this); }
|
||||||
|
public:
|
||||||
|
Section(NodePrefs* p, void (NodePrefs::*fn)(Section&)) : _p(p), _fn(fn) { }
|
||||||
|
using ConfigSerializer::def;
|
||||||
|
template <typename T> void hex(const char* key, T& v) { def(key, (void*)&v, sizeof(v)); }
|
||||||
|
void sub(const char* key, Section& s) { def(key, s); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// The board's own settings (Board::attachDynamicPrefs()): the LoRa front
|
||||||
|
// end's gains are fields here; any other key a board has is kept in
|
||||||
|
// `custom` as it is upstream.
|
||||||
|
class FemPrefs : public KeyValueStore {
|
||||||
|
NodePrefs* _p;
|
||||||
|
uint8_t* field(const char* key) {
|
||||||
|
if (strcmp(key, "fem_rxgain") == 0) return &_p->radio_fem_rxgain;
|
||||||
|
if (strcmp(key, "fem_txgain") == 0) return &_p->radio_fem_txgain;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
FemPrefs(NodePrefs* p) : _p(p) { }
|
||||||
|
bool getByKey(const char* key, char* value, size_t max_len) override {
|
||||||
|
uint8_t* f = field(key);
|
||||||
|
if (!f || max_len < 2) return false;
|
||||||
|
value[0] = *f ? '1' : '0';
|
||||||
|
value[1] = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool setByKey(const char* key, const char* value) override {
|
||||||
|
uint8_t* f = field(key);
|
||||||
|
if (!f) return false;
|
||||||
|
*f = atoi(value) ? 1 : 0;
|
||||||
|
_p->markDirty();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── upstream's sections ──
|
||||||
|
void radioKeys(Section& s) {
|
||||||
|
s.def("freq", freq); s.def("bw", bw); s.def("sf", sf); s.def("cr", cr);
|
||||||
|
s.def("cad", cad_enabled); s.def("int_thr", interference_threshold);
|
||||||
|
s.def("rxgain", rx_boosted_gain);
|
||||||
|
s.def("fem_rxgain", radio_fem_rxgain); s.def("fem_txgain", radio_fem_txgain);
|
||||||
|
s.def("tx", tx_power_dbm); s.def("af", airtime_factor); s.def("rxdelay", rx_delay_base);
|
||||||
|
s.def("f_txdelay", tx_delay_factor); s.def("d_txdelay", direct_tx_delay_factor);
|
||||||
|
s.def("agc_int", agc_reset_interval);
|
||||||
|
s.def("hash_mode", path_hash_mode); s.def("multi_ack", multi_acks);
|
||||||
|
}
|
||||||
|
void gpsKeys(Section& s) {
|
||||||
|
s.def("en", gps_enabled); s.def("int", gps_interval); s.def("adv_loc", advert_loc_policy);
|
||||||
|
}
|
||||||
|
void repeatKeys(Section& s) { // upstream keeps "disable", the inverse
|
||||||
|
uint8_t off = client_repeat ? 0 : 1;
|
||||||
|
s.def("disable", off);
|
||||||
|
client_repeat = off ? 0 : 1;
|
||||||
|
}
|
||||||
|
void compKeys(Section& s) {
|
||||||
|
s.def("auto_max", autoadd_max_hops);
|
||||||
|
s.def("defs_nm", default_scope_name, sizeof(default_scope_name));
|
||||||
|
s.hex("defs_key", default_scope_key);
|
||||||
|
s.def("pin", ble_pin); s.def("buzz_q", buzzer_quiet);
|
||||||
|
s.def("auto_add", autoadd_config); s.def("man_add", manual_add_contacts);
|
||||||
|
s.def("tel_base", telemetry_mode_base); s.def("tel_loc", telemetry_mode_loc);
|
||||||
|
s.def("tel_env", telemetry_mode_env);
|
||||||
|
s.def("tz_offset", tz_offset_hours);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── this firmware's ("solo") ──
|
||||||
|
void radioSolo(Section& s) {
|
||||||
|
s.def("rx_ps", rx_powersave); s.def("apc", tx_apc); s.def("dm_resend", dm_resend_count);
|
||||||
|
char k[4] = "p0";
|
||||||
|
for (int i = 0; i < USER_RADIO_PRESET_MAX; i++) {
|
||||||
|
k[1] = '0' + i;
|
||||||
|
s.sub(k, preset[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void presetKeys(Section& s) {
|
||||||
|
for (int i = 0; i < USER_RADIO_PRESET_MAX; i++) {
|
||||||
|
if (&s != &preset[i]) continue;
|
||||||
|
UserRadioPreset& p = user_radio_presets[i];
|
||||||
|
s.def("name", p.name, sizeof(p.name));
|
||||||
|
s.def("freq", p.freq); s.def("bw", p.bw); s.def("sf", p.sf); s.def("cr", p.cr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void repeaterSolo(Section& s) {
|
||||||
|
s.def("skip_adv", repeat_skip_adverts); s.def("max_hops", repeat_max_hops);
|
||||||
|
s.def("delay_boost", repeat_delay_boost); s.def("min_snr", repeat_min_snr);
|
||||||
|
s.def("sup_dup", repeat_suppress_dup); s.def("scope_only", repeat_scope_only);
|
||||||
|
s.def("extra_scopes", repeat_extra_scope_mask);
|
||||||
|
s.def("use_prof", repeater_use_profile);
|
||||||
|
s.def("freq", repeater_freq); s.def("bw", repeater_bw);
|
||||||
|
s.def("sf", repeater_sf); s.def("cr", repeater_cr);
|
||||||
|
}
|
||||||
|
void powerSolo(Section& s) {
|
||||||
|
s.def("ble_off", ble_off);
|
||||||
|
s.def("low_mv", low_batt_mv); s.def("batt_disp", batt_display_mode);
|
||||||
|
s.hex("curve", batt_curve_mv);
|
||||||
|
}
|
||||||
|
void displaySolo(Section& s) {
|
||||||
|
s.def("bright", display_brightness); s.def("bright_pct", display_brightness_pct);
|
||||||
|
s.def("auto_off", auto_off_secs); s.def("auto_lock", auto_lock);
|
||||||
|
s.def("lock_compact", lock_compact);
|
||||||
|
s.def("rot", display_rotation); s.def("joy_rot", joystick_rotation);
|
||||||
|
s.def("eink_full", eink_full_refresh_every);
|
||||||
|
s.def("text_scale", ui_text_scale); s.def("icon_scale", ui_icon_scale);
|
||||||
|
s.def("pages", home_pages_mask); s.hex("dash", dashboard_fields);
|
||||||
|
s.hex("order", page_order); s.def("order_set", page_order_set);
|
||||||
|
s.def("ch_sort", ch_sort); s.def("flow", flow_show);
|
||||||
|
s.def("kb", keyboard_type); s.def("kb_alt", keyboard_alt_alphabet);
|
||||||
|
s.def("kb_main", keyboard_main_alphabet); s.def("kb_cardkb", keyboard_cardkb_compact);
|
||||||
|
}
|
||||||
|
void clockSolo(Section& s) {
|
||||||
|
s.def("hide_sec", clock_hide_seconds); s.def("h12", clock_12h);
|
||||||
|
s.def("alarm", alarm_on); s.def("alarm_h", alarm_hour); s.def("alarm_m", alarm_min);
|
||||||
|
s.def("alarm_rep", alarm_repeat_mask);
|
||||||
|
}
|
||||||
|
void soundSolo(Section& s) {
|
||||||
|
s.def("vol", buzzer_volume); s.def("auto", buzzer_auto);
|
||||||
|
s.def("quiet", quiet_hours); s.def("quiet_from", quiet_from); s.def("quiet_to", quiet_to);
|
||||||
|
s.def("msg_wake", msg_wake); s.def("msg_alert", msg_alert);
|
||||||
|
s.def("rt_bpm", ringtone_bpm_idx); s.def("rt_len", ringtone_len); s.hex("rt", ringtone_notes);
|
||||||
|
s.def("rt2_bpm", ringtone2_bpm_idx); s.def("rt2_len", ringtone2_len); s.hex("rt2", ringtone2_notes);
|
||||||
|
s.def("mel_dm", notif_melody_dm); s.def("mel_ch", notif_melody_ch); s.def("mel_ad", notif_melody_ad);
|
||||||
|
s.def("adv_scope", advert_sound_scope);
|
||||||
|
s.hex("ch_over", ch_notif_override); s.hex("ch_muted", ch_notif_muted);
|
||||||
|
s.hex("ch_mel_set", ch_notif_melody_set); s.hex("ch_mel_2", ch_notif_melody_2);
|
||||||
|
s.hex("dm", dm_notif); s.hex("dm_mel", dm_melody);
|
||||||
|
}
|
||||||
|
void scopeSolo(Section& s) { s.hex("ch", ch_scope_idx); }
|
||||||
|
void botSolo(Section& s) {
|
||||||
|
s.def("dm_en", bot_enabled); s.def("dm_trig", bot_trigger, sizeof(bot_trigger));
|
||||||
|
s.def("dm_reply", bot_reply_dm, sizeof(bot_reply_dm));
|
||||||
|
s.def("dm_cmd", bot_commands_enabled); s.def("dm_act", bot_actions_dm); s.def("dm_scope", bot_dm_scope);
|
||||||
|
s.def("ch_en", bot_channel_enabled); s.def("ch_idx", bot_channel_idx);
|
||||||
|
s.def("ch_trig", bot_trigger_ch, sizeof(bot_trigger_ch));
|
||||||
|
s.def("ch_reply", bot_reply_ch, sizeof(bot_reply_ch));
|
||||||
|
s.def("ch_cmd", bot_commands_ch); s.def("ch_act", bot_actions_ch);
|
||||||
|
s.def("rm_en", bot_room_enabled); s.hex("rm_pfx", bot_room_prefix);
|
||||||
|
s.def("rm_trig", bot_trigger_room, sizeof(bot_trigger_room));
|
||||||
|
s.def("rm_reply", bot_reply_room, sizeof(bot_reply_room));
|
||||||
|
s.def("rm_cmd", bot_commands_room); s.def("rm_act", bot_actions_room);
|
||||||
|
s.def("q_start", bot_quiet_start); s.def("q_end", bot_quiet_end);
|
||||||
|
}
|
||||||
|
void gpsSolo(Section& s) {
|
||||||
|
s.def("imperial", units_imperial);
|
||||||
|
s.def("tr_delta", trail_min_delta_idx); s.def("tr_pace", trail_show_pace);
|
||||||
|
s.def("tr_pause", trail_autopause_idx); s.def("tr_lowbatt", trail_autosave_lowbatt);
|
||||||
|
s.def("avg", gps_avg_idx); s.def("trk_shared", track_shared_loc);
|
||||||
|
s.def("adv_auto", advert_auto_interval_sec); s.def("adv_prec", advert_loc_prec);
|
||||||
|
}
|
||||||
|
void shareSolo(Section& s) {
|
||||||
|
s.def("en", loc_share_enabled); s.def("tgt", loc_share_target_type);
|
||||||
|
s.def("ch", loc_share_channel_idx); s.hex("dm", loc_share_dm_prefix);
|
||||||
|
s.def("move", loc_share_move_idx); s.def("int", loc_share_interval_idx);
|
||||||
|
s.def("hb", loc_share_heartbeat_idx); s.def("scope", loc_share_scope);
|
||||||
|
s.def("dur", loc_share_duration_idx);
|
||||||
|
}
|
||||||
|
void locatorSolo(Section& s) {
|
||||||
|
s.def("en", locator_enabled); s.def("has", locator_has_target);
|
||||||
|
s.def("radius", locator_radius_idx); s.def("mode", locator_mode);
|
||||||
|
s.def("lat", locator_lat_1e6); s.def("lon", locator_lon_1e6);
|
||||||
|
s.def("label", locator_label, sizeof(locator_label));
|
||||||
|
s.def("kind", locator_target_kind); s.hex("key", locator_key);
|
||||||
|
s.def("beep", locator_beeper);
|
||||||
|
}
|
||||||
|
void listsSolo(Section& s) {
|
||||||
|
s.def("dm_all", dm_show_all); s.def("room_fav", room_fav_only);
|
||||||
|
s.hex("ch_fav", ch_fav_bitmask); s.def("ch_fav_only", ch_fav_only);
|
||||||
|
s.hex("favs", favourite_contacts); s.hex("fav_kinds", favourite_kinds);
|
||||||
|
s.def("fav_sort", fav_sort); s.def("expiry", contact_expiry_idx);
|
||||||
|
}
|
||||||
|
void gpioSolo(Section& s) {
|
||||||
|
s.def("m1", gpio1_mode); s.def("m2", gpio2_mode); s.def("m3", gpio3_mode); s.def("m4", gpio4_mode);
|
||||||
|
}
|
||||||
|
void msgsSolo(Section& s) {
|
||||||
|
char k[4] = "m0";
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
k[1] = '0' + i;
|
||||||
|
s.def(k, custom_msgs[i], sizeof(custom_msgs[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void lockSolo(Section& s) { s.hex("hash", lock_screen_password); s.hex("salt", lock_screen_password_salt); }
|
||||||
|
void soloKeys(Section& s) {
|
||||||
|
s.sub("radio", radio_solo); s.sub("repeater", repeater_solo); s.sub("power", power_solo);
|
||||||
|
s.sub("display", display_solo); s.sub("clock", clock_solo); s.sub("sound", sound_solo);
|
||||||
|
s.sub("scope", scope_solo); s.sub("bot", bot_solo); s.sub("gps", gps_solo);
|
||||||
|
s.sub("share", share_solo); s.sub("locator", locator_solo); s.sub("lists", lists_solo);
|
||||||
|
s.sub("gpio", gpio_solo); s.sub("msgs", msgs_solo); s.sub("lock", lock_solo);
|
||||||
|
}
|
||||||
|
|
||||||
|
Section radio, gps, repeat, comp;
|
||||||
|
Section solo, radio_solo, preset[USER_RADIO_PRESET_MAX], repeater_solo, power_solo, display_solo,
|
||||||
|
clock_solo, sound_solo, scope_solo, bot_solo, gps_solo, share_solo, locator_solo,
|
||||||
|
lists_solo, gpio_solo, msgs_solo, lock_solo;
|
||||||
|
FemPrefs fem;
|
||||||
|
DynamicConfigSerializer custom;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void structure() override {
|
||||||
|
def("name", node_name, sizeof(node_name));
|
||||||
|
def("lat", node_lat);
|
||||||
|
def("lon", node_lon);
|
||||||
|
def("radio", radio);
|
||||||
|
def("gps", gps);
|
||||||
|
def("repeat", repeat);
|
||||||
|
def("comp", comp);
|
||||||
|
def("custom", custom);
|
||||||
|
def("solo", solo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// The node's position as the file holds it (MyMesh keeps it in `sensors`):
|
||||||
|
// DataStore sets it before a save and reads it after a load.
|
||||||
|
double node_lat = 0, node_lon = 0;
|
||||||
|
|
||||||
|
NodePrefs()
|
||||||
|
: radio(this, &NodePrefs::radioKeys), gps(this, &NodePrefs::gpsKeys),
|
||||||
|
repeat(this, &NodePrefs::repeatKeys), comp(this, &NodePrefs::compKeys),
|
||||||
|
solo(this, &NodePrefs::soloKeys), radio_solo(this, &NodePrefs::radioSolo),
|
||||||
|
preset{ {this, &NodePrefs::presetKeys}, {this, &NodePrefs::presetKeys},
|
||||||
|
{this, &NodePrefs::presetKeys}, {this, &NodePrefs::presetKeys} },
|
||||||
|
repeater_solo(this, &NodePrefs::repeaterSolo), power_solo(this, &NodePrefs::powerSolo),
|
||||||
|
display_solo(this, &NodePrefs::displaySolo), clock_solo(this, &NodePrefs::clockSolo),
|
||||||
|
sound_solo(this, &NodePrefs::soundSolo), scope_solo(this, &NodePrefs::scopeSolo),
|
||||||
|
bot_solo(this, &NodePrefs::botSolo), gps_solo(this, &NodePrefs::gpsSolo),
|
||||||
|
share_solo(this, &NodePrefs::shareSolo), locator_solo(this, &NodePrefs::locatorSolo),
|
||||||
|
lists_solo(this, &NodePrefs::listsSolo), gpio_solo(this, &NodePrefs::gpioSolo),
|
||||||
|
msgs_solo(this, &NodePrefs::msgsSolo), lock_solo(this, &NodePrefs::lockSolo),
|
||||||
|
fem(this), custom(&fem) {
|
||||||
|
clearFields();
|
||||||
|
}
|
||||||
|
// The sections point back at this object: it isn't copied.
|
||||||
|
NodePrefs(const NodePrefs&) = delete;
|
||||||
|
NodePrefs& operator=(const NodePrefs&) = delete;
|
||||||
|
|
||||||
|
// Every field zero (MyMesh then sets the defaults).
|
||||||
|
void clearFields() { memset(static_cast<NodePrefsFields*>(this), 0, sizeof(NodePrefsFields)); }
|
||||||
|
|
||||||
|
// The board's settings (upstream's getCustom()), for Board::attachDynamicPrefs().
|
||||||
|
KeyValueStore* getCustom() { return &custom; }
|
||||||
|
bool isRepeatEn() const { return client_repeat != 0; }
|
||||||
|
void setRepeatEn(bool en) { client_repeat = en ? 1 : 0; }
|
||||||
|
|
||||||
|
bool isDirty() const override { return ConfigSerializer::isDirty() || custom.isDirty(); }
|
||||||
|
void clearDirty() override { ConfigSerializer::clearDirty(); custom.clearDirty(); }
|
||||||
|
using ConfigSerializer::markDirty;
|
||||||
|
};
|
||||||
|
|
||||||
// Local hour `h` inside [start, end): a window with start > end wraps past
|
// Local hour `h` inside [start, end): a window with start > end wraps past
|
||||||
// midnight; start == end is no window. Shared by the bot's quiet hours and the
|
// midnight; start == end is no window. Shared by the bot's quiet hours and the
|
||||||
// sound's.
|
// sound's.
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef CONFIG_MAX_TOKEN_LEN
|
#ifndef CONFIG_MAX_TOKEN_LEN
|
||||||
#define CONFIG_MAX_TOKEN_LEN 128
|
// A value read whole: the companion's longest are 140-byte texts and an
|
||||||
|
// 80-byte table in hex (160).
|
||||||
|
#define CONFIG_MAX_TOKEN_LEN 192
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
class ConfigSerializer {
|
class ConfigSerializer {
|
||||||
|
|||||||
@@ -77,14 +77,14 @@ TEST(AlarmRepeatRoundTrip, AnArbitraryNonPresetMaskReadsAsOff) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(AlarmRepeatLabel, MatchesEachPresetIndex) {
|
TEST(AlarmRepeatLabel, MatchesEachPresetIndex) {
|
||||||
EXPECT_STREQ("OFF", NodePrefs::alarmRepeatLabel(0));
|
EXPECT_STREQ("Once", NodePrefs::alarmRepeatLabel(0));
|
||||||
EXPECT_STREQ("Daily", NodePrefs::alarmRepeatLabel(1));
|
EXPECT_STREQ("Daily", NodePrefs::alarmRepeatLabel(1));
|
||||||
EXPECT_STREQ("Weekdays", NodePrefs::alarmRepeatLabel(2));
|
EXPECT_STREQ("Weekdays", NodePrefs::alarmRepeatLabel(2));
|
||||||
EXPECT_STREQ("Weekends", NodePrefs::alarmRepeatLabel(3));
|
EXPECT_STREQ("Weekends", NodePrefs::alarmRepeatLabel(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(AlarmRepeatLabel, OutOfRangeIndexClampsToOff) {
|
TEST(AlarmRepeatLabel, OutOfRangeIndexClampsToOnce) {
|
||||||
EXPECT_STREQ("OFF", NodePrefs::alarmRepeatLabel(99));
|
EXPECT_STREQ("Once", NodePrefs::alarmRepeatLabel(99));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(KeyboardAlphabetLabel, MatchesEachAlphabetIndex) {
|
TEST(KeyboardAlphabetLabel, MatchesEachAlphabetIndex) {
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
// NodePrefs saved as /prefs.json (NodePrefs::structure()) and read back: every
|
||||||
|
// field comes back as it was -- one without a key would come back zero. The
|
||||||
|
// fields are packed here (this test only), so that comparing them byte for
|
||||||
|
// byte has no padding in it, which no key holds. The serializer's own headers
|
||||||
|
// first, unpacked, as the library they're compiled into has them.
|
||||||
|
#include <helpers/ConfigSerializer.h>
|
||||||
|
#include <helpers/DynamicConfigSerializer.h>
|
||||||
|
#pragma pack(push, 1)
|
||||||
|
#include "../../examples/companion_radio/NodePrefs.h"
|
||||||
|
#pragma pack(pop)
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class StringOut : public Stream {
|
||||||
|
public:
|
||||||
|
std::string text;
|
||||||
|
size_t write(uint8_t b) override { text += (char)b; return 1; }
|
||||||
|
size_t print(unsigned char v, int) override { return out(std::to_string(v)); }
|
||||||
|
size_t print(int v, int) override { return out(std::to_string(v)); }
|
||||||
|
size_t print(unsigned int v, int) override { return out(std::to_string(v)); }
|
||||||
|
size_t print(long v, int) override { return out(std::to_string(v)); }
|
||||||
|
size_t print(unsigned long v, int) override { return out(std::to_string(v)); }
|
||||||
|
size_t print(long long v, int) override { return out(std::to_string(v)); }
|
||||||
|
size_t print(unsigned long long v, int) override { return out(std::to_string(v)); }
|
||||||
|
size_t print(double v, int p) override {
|
||||||
|
char t[48];
|
||||||
|
snprintf(t, sizeof(t), "%.*f", p, v);
|
||||||
|
return out(t);
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
size_t out(const std::string& s) { text += s; return s.size(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class StringIn : public Stream {
|
||||||
|
const std::string& _s;
|
||||||
|
size_t _at = 0;
|
||||||
|
public:
|
||||||
|
explicit StringIn(const std::string& s) : _s(s) { }
|
||||||
|
int available() override { return (int)(_s.size() - _at); }
|
||||||
|
int read() override { return _at < _s.size() ? (uint8_t)_s[_at++] : -1; }
|
||||||
|
int peek() override { return _at < _s.size() ? (uint8_t)_s[_at] : -1; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Every byte of the fields a different value; texts printable (quotes and
|
||||||
|
// backslashes among them) and terminated; floats ones the file's four
|
||||||
|
// decimals keep exactly.
|
||||||
|
void fill(NodePrefs& p) {
|
||||||
|
uint8_t* b = (uint8_t*)static_cast<NodePrefsFields*>(&p);
|
||||||
|
for (size_t i = 0; i < sizeof(NodePrefsFields); i++) b[i] = (uint8_t)(33 + (i * 7) % 90);
|
||||||
|
auto text = [](char* s, size_t n) { s[n - 1] = 0; };
|
||||||
|
text(p.node_name, sizeof(p.node_name));
|
||||||
|
text(p.default_scope_name, sizeof(p.default_scope_name));
|
||||||
|
text(p.bot_trigger, sizeof(p.bot_trigger));
|
||||||
|
text(p.bot_reply_dm, sizeof(p.bot_reply_dm));
|
||||||
|
text(p.bot_reply_ch, sizeof(p.bot_reply_ch));
|
||||||
|
text(p.bot_trigger_ch, sizeof(p.bot_trigger_ch));
|
||||||
|
text(p.bot_trigger_room, sizeof(p.bot_trigger_room));
|
||||||
|
text(p.bot_reply_room, sizeof(p.bot_reply_room));
|
||||||
|
text(p.locator_label, sizeof(p.locator_label));
|
||||||
|
for (auto& m : p.custom_msgs) text(m, sizeof(m));
|
||||||
|
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++) {
|
||||||
|
auto& r = p.user_radio_presets[i];
|
||||||
|
text(r.name, sizeof(r.name));
|
||||||
|
r.freq = 869.525f + i; r.bw = 62.5f;
|
||||||
|
}
|
||||||
|
p.airtime_factor = 1.5f; p.freq = 869.618f; p.bw = 62.5f; p.rx_delay_base = 2.25f;
|
||||||
|
p.tx_delay_factor = 0.75f; p.direct_tx_delay_factor = 0.125f;
|
||||||
|
p.repeater_freq = 433.875f; p.repeater_bw = 125.0f;
|
||||||
|
p.client_repeat = 1; // a flag in the file (as upstream's "disable")
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(NodePrefsJson, EveryFieldRoundTrips) {
|
||||||
|
NodePrefs a;
|
||||||
|
fill(a);
|
||||||
|
a.node_lat = 50.061234; a.node_lon = 19.937654;
|
||||||
|
StringOut out;
|
||||||
|
ASSERT_TRUE(a.saveSerial(out));
|
||||||
|
|
||||||
|
NodePrefs b;
|
||||||
|
StringIn in(out.text);
|
||||||
|
ASSERT_TRUE(b.loadSerial(in)) << out.text;
|
||||||
|
const uint8_t* pa = (const uint8_t*)static_cast<NodePrefsFields*>(&a);
|
||||||
|
const uint8_t* pb = (const uint8_t*)static_cast<NodePrefsFields*>(&b);
|
||||||
|
for (size_t i = 0; i < sizeof(NodePrefsFields); i++)
|
||||||
|
ASSERT_EQ(pa[i], pb[i]) << "first difference at byte " << i << " of NodePrefsFields";
|
||||||
|
EXPECT_DOUBLE_EQ(a.node_lat, b.node_lat);
|
||||||
|
EXPECT_DOUBLE_EQ(a.node_lon, b.node_lon);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NodePrefsJson, UpstreamKeysWhereUpstreamHasThem) {
|
||||||
|
NodePrefs a;
|
||||||
|
strcpy(a.node_name, "Kraków 1");
|
||||||
|
a.freq = 869.618f; a.sf = 8; a.tx_power_dbm = 22; a.client_repeat = 1; a.tz_offset_hours = 2;
|
||||||
|
StringOut out;
|
||||||
|
ASSERT_TRUE(a.saveSerial(out));
|
||||||
|
const std::string& t = out.text;
|
||||||
|
EXPECT_NE(std::string::npos, t.find("name:\"Kraków 1\""));
|
||||||
|
EXPECT_NE(std::string::npos, t.find("radio:{freq:869.6180"));
|
||||||
|
EXPECT_NE(std::string::npos, t.find("sf:8"));
|
||||||
|
EXPECT_NE(std::string::npos, t.find("tx:22"));
|
||||||
|
EXPECT_NE(std::string::npos, t.find("repeat:{disable:0}"));
|
||||||
|
EXPECT_NE(std::string::npos, t.find("tz_offset:2"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NodePrefsJson, AMissingKeyKeepsItsDefault) {
|
||||||
|
NodePrefs b;
|
||||||
|
b.sf = 9; b.buzzer_volume = 3;
|
||||||
|
StringIn in(std::string("{radio:{sf:11}}"));
|
||||||
|
ASSERT_TRUE(b.loadSerial(in));
|
||||||
|
EXPECT_EQ(11, b.sf);
|
||||||
|
EXPECT_EQ(3, b.buzzer_volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NodePrefsJson, BoardSettingsGoToTheFemFields) {
|
||||||
|
NodePrefs p;
|
||||||
|
KeyValueStore* kv = p.getCustom();
|
||||||
|
ASSERT_TRUE(kv->setByKey("fem_rxgain", "1"));
|
||||||
|
EXPECT_EQ(1, p.radio_fem_rxgain);
|
||||||
|
EXPECT_TRUE(p.isDirty());
|
||||||
|
char v[8];
|
||||||
|
ASSERT_TRUE(kv->getByKey("fem_rxgain", v, sizeof(v) - 1));
|
||||||
|
EXPECT_STREQ("1", v);
|
||||||
|
ASSERT_TRUE(kv->setByKey("fan", "2")); // a key with no field: kept in "custom"
|
||||||
|
StringOut out;
|
||||||
|
ASSERT_TRUE(p.saveSerial(out));
|
||||||
|
EXPECT_NE(std::string::npos, out.text.find("custom:{fan:\"2\"}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
return RUN_ALL_TESTS();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user