mirror of
https://github.com/meshcore-dev/MeshCore.git
synced 2026-08-14 05:09:46 +00:00
Merge pull request #2982 from meshcore-dev/config-serializer
Config serializer
This commit is contained in:
@@ -189,17 +189,22 @@ bool DataStore::saveMainIdentity(const mesh::LocalIdentity &identity) {
|
||||
return identity_store.save("_main", identity);
|
||||
}
|
||||
|
||||
void DataStore::loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon) {
|
||||
if (_fs->exists("/new_prefs")) {
|
||||
loadPrefsInt("/new_prefs", prefs, node_lat, node_lon); // new filename
|
||||
} else if (_fs->exists("/node_prefs")) {
|
||||
loadPrefsInt("/node_prefs", prefs, node_lat, node_lon);
|
||||
savePrefs(prefs, node_lat, node_lon); // save to new filename
|
||||
_fs->remove("/node_prefs"); // remove old
|
||||
void DataStore::loadPrefs(NodePrefs& prefs) {
|
||||
if (_fs->exists("/prefs.json")) {
|
||||
File file = openRead(_fs, "/prefs.json");
|
||||
if (file) {
|
||||
prefs.loadSerial(file); // new Serial prefs
|
||||
file.close();
|
||||
}
|
||||
} else if (_fs->exists("/new_prefs")) {
|
||||
loadPrefsInt("/new_prefs", prefs);
|
||||
if (savePrefs(prefs) ) { // save to new format
|
||||
//_fs->remove("/new_prefs"); // remove old
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& node_lat, double& node_lon) {
|
||||
void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs) {
|
||||
File file = openRead(_fs, filename);
|
||||
if (file) {
|
||||
uint8_t pad[8];
|
||||
@@ -207,12 +212,12 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
file.read((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0
|
||||
file.read((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4
|
||||
file.read(pad, 4); // 36
|
||||
file.read((uint8_t *)&node_lat, sizeof(node_lat)); // 40
|
||||
file.read((uint8_t *)&node_lon, sizeof(node_lon)); // 48
|
||||
file.read((uint8_t *)&_prefs.node_lat, sizeof(_prefs.node_lat)); // 40
|
||||
file.read((uint8_t *)&_prefs.node_lon, sizeof(_prefs.node_lon)); // 48
|
||||
file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56
|
||||
file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60
|
||||
file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
|
||||
file.read((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62
|
||||
file.read((uint8_t *)&_prefs._client_repeat, sizeof(_prefs._client_repeat)); // 62
|
||||
file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
|
||||
file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
|
||||
file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
|
||||
@@ -234,48 +239,21 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
file.read((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90
|
||||
file.read((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121
|
||||
|
||||
// migrate old fields
|
||||
_prefs.setRepeatEn(_prefs._client_repeat != 0);
|
||||
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_lon) {
|
||||
File file = openWrite(_fs, "/new_prefs");
|
||||
bool DataStore::savePrefs(NodePrefs& _prefs) {
|
||||
File file = openWrite(_fs, "/prefs.json");
|
||||
if (file) {
|
||||
uint8_t pad[8];
|
||||
memset(pad, 0, sizeof(pad));
|
||||
|
||||
file.write((uint8_t *)&_prefs.airtime_factor, sizeof(float)); // 0
|
||||
file.write((uint8_t *)_prefs.node_name, sizeof(_prefs.node_name)); // 4
|
||||
file.write(pad, 4); // 36
|
||||
file.write((uint8_t *)&node_lat, sizeof(node_lat)); // 40
|
||||
file.write((uint8_t *)&node_lon, sizeof(node_lon)); // 48
|
||||
file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56
|
||||
file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60
|
||||
file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61
|
||||
file.write((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62
|
||||
file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63
|
||||
file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64
|
||||
file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68
|
||||
file.write((uint8_t *)&_prefs.telemetry_mode_base, sizeof(_prefs.telemetry_mode_base)); // 69
|
||||
file.write((uint8_t *)&_prefs.telemetry_mode_loc, sizeof(_prefs.telemetry_mode_loc)); // 70
|
||||
file.write((uint8_t *)&_prefs.telemetry_mode_env, sizeof(_prefs.telemetry_mode_env)); // 71
|
||||
file.write((uint8_t *)&_prefs.rx_delay_base, sizeof(_prefs.rx_delay_base)); // 72
|
||||
file.write((uint8_t *)&_prefs.advert_loc_policy, sizeof(_prefs.advert_loc_policy)); // 76
|
||||
file.write((uint8_t *)&_prefs.multi_acks, sizeof(_prefs.multi_acks)); // 77
|
||||
file.write((uint8_t *)&_prefs.path_hash_mode, sizeof(_prefs.path_hash_mode)); // 78
|
||||
file.write(pad, 1); // 79
|
||||
file.write((uint8_t *)&_prefs.ble_pin, sizeof(_prefs.ble_pin)); // 80
|
||||
file.write((uint8_t *)&_prefs.buzzer_quiet, sizeof(_prefs.buzzer_quiet)); // 84
|
||||
file.write((uint8_t *)&_prefs.gps_enabled, sizeof(_prefs.gps_enabled)); // 85
|
||||
file.write((uint8_t *)&_prefs.gps_interval, sizeof(_prefs.gps_interval)); // 86
|
||||
file.write((uint8_t *)&_prefs.autoadd_config, sizeof(_prefs.autoadd_config)); // 87
|
||||
file.write((uint8_t *)&_prefs.autoadd_max_hops, sizeof(_prefs.autoadd_max_hops)); // 88
|
||||
file.write((uint8_t *)&_prefs.rx_boosted_gain, sizeof(_prefs.rx_boosted_gain)); // 89
|
||||
file.write((uint8_t *)_prefs.default_scope_name, sizeof(_prefs.default_scope_name)); // 90
|
||||
file.write((uint8_t *)_prefs.default_scope_key, sizeof(_prefs.default_scope_key)); // 121
|
||||
|
||||
bool success = _prefs.saveSerial(file);
|
||||
file.close();
|
||||
return success;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void DataStore::loadContacts(DataStoreHost* host) {
|
||||
|
||||
@@ -19,7 +19,7 @@ class DataStore {
|
||||
mesh::RTCClock* _clock;
|
||||
IdentityStore identity_store;
|
||||
|
||||
void loadPrefsInt(const char *filename, NodePrefs& prefs, double& node_lat, double& node_lon);
|
||||
void loadPrefsInt(const char *filename, NodePrefs& prefs);
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
void checkAdvBlobFile();
|
||||
#endif
|
||||
@@ -33,8 +33,8 @@ public:
|
||||
FILESYSTEM* getSecondaryFS() const { return _fsExtra; }
|
||||
bool loadMainIdentity(mesh::LocalIdentity &identity);
|
||||
bool saveMainIdentity(const mesh::LocalIdentity &identity);
|
||||
void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon);
|
||||
void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon);
|
||||
void loadPrefs(NodePrefs& prefs);
|
||||
bool savePrefs(NodePrefs& prefs);
|
||||
void loadContacts(DataStoreHost* host);
|
||||
void saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL);
|
||||
void loadChannels(DataStoreHost* host);
|
||||
|
||||
@@ -483,7 +483,7 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
|
||||
}
|
||||
|
||||
bool MyMesh::allowPacketForward(const mesh::Packet* packet) {
|
||||
return _prefs.client_repeat != 0;
|
||||
return _prefs.isRepeatEn();
|
||||
}
|
||||
|
||||
void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis) {
|
||||
@@ -876,7 +876,6 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
send_unscoped = false;
|
||||
|
||||
// defaults
|
||||
memset(&_prefs, 0, sizeof(_prefs));
|
||||
_prefs.airtime_factor = 1.0;
|
||||
strcpy(_prefs.node_name, "NONAME");
|
||||
_prefs.freq = LORA_FREQ;
|
||||
@@ -887,6 +886,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
|
||||
_prefs.gps_enabled = 0; // GPS disabled by default
|
||||
_prefs.gps_interval = 0; // No automatic GPS updates by default
|
||||
//_prefs.rx_delay_base = 10.0f; enable once new algo fixed
|
||||
_prefs.setRepeatEn(false);
|
||||
#if defined(USE_SX1262) || defined(USE_SX1268)
|
||||
#ifdef SX126X_RX_BOOSTED_GAIN
|
||||
_prefs.rx_boosted_gain = SX126X_RX_BOOSTED_GAIN;
|
||||
@@ -931,7 +931,9 @@ void MyMesh::begin(bool has_display) {
|
||||
#endif
|
||||
|
||||
// load persisted prefs
|
||||
_store->loadPrefs(_prefs, sensors.node_lat, sensors.node_lon);
|
||||
_store->loadPrefs(_prefs);
|
||||
sensors.node_lat = _prefs.node_lat;
|
||||
sensors.node_lon = _prefs.node_lon;
|
||||
|
||||
// sanitise bad pref values
|
||||
_prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
|
||||
@@ -1031,7 +1033,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
i += 40;
|
||||
StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20);
|
||||
i += 20;
|
||||
out_frame[i++] = _prefs.client_repeat; // v9+
|
||||
out_frame[i++] = _prefs.isRepeatEn() ? 1 : 0; // v9+
|
||||
out_frame[i++] = _prefs.path_hash_mode; // v10+
|
||||
_serial->writeFrame(out_frame, i);
|
||||
} else if (cmd_frame[0] == CMD_APP_START &&
|
||||
@@ -1393,7 +1395,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
||||
_prefs.cr = cr;
|
||||
_prefs.freq = (float)freq / 1000.0;
|
||||
_prefs.bw = (float)bw / 1000.0;
|
||||
_prefs.client_repeat = repeat;
|
||||
_prefs.setRepeatEn(repeat != 0);
|
||||
savePrefs();
|
||||
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
|
||||
@@ -165,7 +165,11 @@ protected:
|
||||
}
|
||||
|
||||
public:
|
||||
void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); }
|
||||
void savePrefs() {
|
||||
_prefs.node_lat = sensors.node_lat;
|
||||
_prefs.node_lon = sensors.node_lon;
|
||||
_store->savePrefs(_prefs);
|
||||
}
|
||||
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
void applyGpsPrefs() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include <cstdint> // For uint8_t, uint32_t
|
||||
#include <helpers/ConfigSerializer.h>
|
||||
|
||||
#define TELEM_MODE_DENY 0
|
||||
#define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags
|
||||
@@ -8,30 +9,128 @@
|
||||
#define ADVERT_LOC_NONE 0
|
||||
#define ADVERT_LOC_SHARE 1
|
||||
|
||||
struct NodePrefs { // persisted to file
|
||||
float airtime_factor;
|
||||
class NodePrefs : public ConfigSerializer { // persisted to file
|
||||
public:
|
||||
float airtime_factor = 0;
|
||||
char node_name[32];
|
||||
float freq;
|
||||
uint8_t sf;
|
||||
uint8_t cr;
|
||||
uint8_t multi_acks;
|
||||
uint8_t manual_add_contacts;
|
||||
float bw;
|
||||
int8_t tx_power_dbm;
|
||||
uint8_t telemetry_mode_base;
|
||||
uint8_t telemetry_mode_loc;
|
||||
uint8_t telemetry_mode_env;
|
||||
float rx_delay_base;
|
||||
uint32_t ble_pin;
|
||||
uint8_t advert_loc_policy;
|
||||
uint8_t buzzer_quiet;
|
||||
uint8_t gps_enabled; // GPS enabled flag (0=disabled, 1=enabled)
|
||||
uint32_t gps_interval; // GPS read interval in seconds
|
||||
uint8_t autoadd_config; // bitmask for auto-add contacts config
|
||||
uint8_t rx_boosted_gain; // SX126x RX boosted gain mode (0=power saving, 1=boosted)
|
||||
uint8_t client_repeat;
|
||||
uint8_t path_hash_mode; // which path mode to use when sending
|
||||
uint8_t autoadd_max_hops; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
|
||||
double node_lat = 0, node_lon = 0;
|
||||
float freq = 0;
|
||||
uint8_t sf = 0;
|
||||
uint8_t cr = 0;
|
||||
uint8_t multi_acks = 0;
|
||||
uint8_t manual_add_contacts = 0;
|
||||
float bw = 0;
|
||||
int8_t tx_power_dbm = 0;
|
||||
uint8_t telemetry_mode_base = 0;
|
||||
uint8_t telemetry_mode_loc = 0;
|
||||
uint8_t telemetry_mode_env = 0;
|
||||
float rx_delay_base = 0;
|
||||
uint32_t ble_pin = 0;
|
||||
uint8_t advert_loc_policy = 0;
|
||||
uint8_t buzzer_quiet = 0;
|
||||
uint8_t gps_enabled = 0; // GPS enabled flag (0=disabled, 1=enabled)
|
||||
uint32_t gps_interval = 0; // GPS read interval in seconds
|
||||
uint8_t autoadd_config = 0; // bitmask for auto-add contacts config
|
||||
uint8_t rx_boosted_gain = 0; // SX126x RX boosted gain mode (0=power saving, 1=boosted)
|
||||
uint8_t _client_repeat = 0; // DEPRECATED -> use repeat.disable_fwd
|
||||
uint8_t path_hash_mode = 0; // which path mode to use when sending
|
||||
uint8_t autoadd_max_hops = 0; // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
|
||||
char default_scope_name[31];
|
||||
uint8_t default_scope_key[16];
|
||||
|
||||
private:
|
||||
class RadioPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now)
|
||||
NodePrefs* _parent;
|
||||
protected:
|
||||
void structure() override {
|
||||
def("freq", _parent->freq);
|
||||
def("bw", _parent->bw);
|
||||
def("sf", _parent->sf);
|
||||
def("cr", _parent->cr);
|
||||
//def("cad", _parent->cad_enabled);
|
||||
//def("int_thr", _parent->interference_threshold);
|
||||
def("rxgain", _parent->rx_boosted_gain);
|
||||
def("fem_rxgain", _parent->rx_boosted_gain);
|
||||
def("tx", _parent->tx_power_dbm);
|
||||
def("af", _parent->airtime_factor);
|
||||
def("rxdelay", _parent->rx_delay_base);
|
||||
//def("f_txdelay", _parent->tx_delay_factor); currently hard-coded
|
||||
//def("d_txdelay", _parent->direct_tx_delay_factor); currently hard-coded
|
||||
//def("agc_int", _parent->agc_reset_interval);
|
||||
def("hash_mode", _parent->path_hash_mode);
|
||||
def("multi_ack", _parent->multi_acks);
|
||||
}
|
||||
public:
|
||||
RadioPrefs(NodePrefs* parent) : _parent(parent) { }
|
||||
};
|
||||
RadioPrefs radio;
|
||||
|
||||
class GPSPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now)
|
||||
NodePrefs* _parent;
|
||||
protected:
|
||||
void structure() override {
|
||||
def("en", _parent->gps_enabled); // boolean
|
||||
def("int", _parent->gps_interval); // interval in seconds
|
||||
def("adv_loc", _parent->advert_loc_policy);
|
||||
}
|
||||
public:
|
||||
GPSPrefs(NodePrefs* parent) : _parent(parent) { }
|
||||
};
|
||||
GPSPrefs gps;
|
||||
|
||||
class RepeatPrefs : public ConfigSerializer { // COPIED from CommonCLI (for now)
|
||||
public:
|
||||
uint8_t disable_fwd = 1;
|
||||
protected:
|
||||
void structure() override {
|
||||
def("disable", disable_fwd);
|
||||
//def("f_max", flood_max);
|
||||
//def("f_max_uns", flood_max_unscoped);
|
||||
//def("f_max_adv", flood_max_advert);
|
||||
//def("loop", loop_detect);
|
||||
}
|
||||
};
|
||||
RepeatPrefs repeat;
|
||||
|
||||
class CompanionPrefs : public ConfigSerializer {
|
||||
NodePrefs* _parent;
|
||||
protected:
|
||||
void structure() override {
|
||||
def("auto_max", _parent->autoadd_max_hops); // 0 = no limit, 1 = direct (0 hops), N = up to N-1 hops (max 64)
|
||||
def("defs_nm", _parent->default_scope_name, sizeof(_parent->default_scope_name));
|
||||
def("defs_key", (void *) _parent->default_scope_key, sizeof(_parent->default_scope_key));
|
||||
def("pin", _parent->ble_pin);
|
||||
def("buzz_q", _parent->buzzer_quiet);
|
||||
def("auto_add", _parent->autoadd_config); // bitmask for auto-add contacts config
|
||||
def("man_add", _parent->manual_add_contacts);
|
||||
def("tel_base", _parent->telemetry_mode_base);
|
||||
def("tel_loc", _parent->telemetry_mode_loc);
|
||||
def("tel_env", _parent->telemetry_mode_env);
|
||||
}
|
||||
public:
|
||||
CompanionPrefs(NodePrefs* parent) : _parent(parent) { }
|
||||
};
|
||||
CompanionPrefs companion;
|
||||
|
||||
protected:
|
||||
void structure() override {
|
||||
def("name", node_name, sizeof(node_name));
|
||||
//def("adv_int", advert_interval);
|
||||
//def("f_adv_int", flood_advert_interval);
|
||||
def("lat", node_lat);
|
||||
def("lon", node_lon);
|
||||
def("radio", radio);
|
||||
def("gps", gps);
|
||||
def("repeat", repeat);
|
||||
def("comp", companion);
|
||||
}
|
||||
public:
|
||||
NodePrefs() : radio(this), gps(this), companion(this) {
|
||||
node_name[0] = 0;
|
||||
default_scope_name[0] = 0;
|
||||
memset(default_scope_key, 0, sizeof(default_scope_key));
|
||||
}
|
||||
// new accessor methods
|
||||
bool isRepeatEn() const { return repeat.disable_fwd == 0; }
|
||||
void setRepeatEn(bool en) { repeat.disable_fwd = en ? 0 : 1; }
|
||||
};
|
||||
@@ -872,7 +872,6 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
#endif
|
||||
|
||||
// defaults
|
||||
memset(&_prefs, 0, sizeof(_prefs));
|
||||
_prefs.airtime_factor = 1.0;
|
||||
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
|
||||
_prefs.tx_delay_factor = 0.5f; // was 0.25f
|
||||
|
||||
@@ -628,7 +628,6 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
recv_pkt_region = NULL;
|
||||
|
||||
// defaults
|
||||
memset(&_prefs, 0, sizeof(_prefs));
|
||||
_prefs.airtime_factor = 1.0;
|
||||
_prefs.rx_delay_base = 0.0f; // off by default, was 10.0
|
||||
_prefs.tx_delay_factor = 0.5f; // was 0.25f;
|
||||
|
||||
@@ -710,7 +710,6 @@ SensorMesh::SensorMesh(mesh::MainBoard& board, mesh::Radio& radio, mesh::Millise
|
||||
set_radio_at = revert_radio_at = 0;
|
||||
|
||||
// defaults
|
||||
memset(&_prefs, 0, sizeof(_prefs));
|
||||
_prefs.airtime_factor = 1.0;
|
||||
_prefs.rx_delay_base = 0.0f; // turn off by default, was 10.0;
|
||||
_prefs.tx_delay_factor = 0.5f; // was 0.25f
|
||||
|
||||
+3
-2
@@ -100,7 +100,7 @@ lib_deps =
|
||||
extends = arduino_base
|
||||
upload_protocol = picotool
|
||||
board_build.core = earlephilhower
|
||||
platform = https://github.com/maxgerhardt/platform-raspberrypi.git#4e22a0d ; framework-arduinopico @ 1.50600.0+sha.6a1d13e9
|
||||
platform = https://github.com/maxgerhardt/platform-raspberrypi.git ; framework-arduinopico @ 1.50600.0+sha.6a1d13e9
|
||||
build_flags = ${arduino_base.build_flags}
|
||||
-D RP2040_PLATFORM
|
||||
|
||||
@@ -163,11 +163,12 @@ build_flags = -std=c++17
|
||||
-I src
|
||||
-I test/mocks
|
||||
test_build_src = yes
|
||||
test_filter = test_utils
|
||||
test_ignore = test_kiss_modem
|
||||
build_src_filter =
|
||||
-<*>
|
||||
+<../src/Utils.cpp>
|
||||
+<../src/Packet.cpp>
|
||||
+<../src/helpers/ConfigSerializer.cpp>
|
||||
lib_deps =
|
||||
google/googletest @ 1.17.0
|
||||
|
||||
|
||||
+24
-64
@@ -28,16 +28,25 @@ static bool isValidName(const char *n) {
|
||||
}
|
||||
|
||||
void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
if (fs->exists("/com_prefs")) {
|
||||
loadPrefsInt(fs, "/com_prefs"); // new filename
|
||||
} else if (fs->exists("/node_prefs")) {
|
||||
loadPrefsInt(fs, "/node_prefs");
|
||||
savePrefs(fs); // save to new filename
|
||||
fs->remove("/node_prefs"); // remove old
|
||||
if (fs->exists("/prefs.json")) {
|
||||
#if defined(RP2040_PLATFORM)
|
||||
File file = fs->open("/prefs.json", "r");
|
||||
#else
|
||||
File file = fs->open("/prefs.json");
|
||||
#endif
|
||||
if (file) {
|
||||
_prefs->loadSerial(file); // new Serial prefs
|
||||
file.close();
|
||||
}
|
||||
} else if (fs->exists("/com_prefs")) {
|
||||
loadPrefsInt(fs, "/com_prefs");
|
||||
if (savePrefs(fs)) { // save to new Serial prefs
|
||||
// fs->remove("/com_prefs"); // remove old
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // Legacy prefs loader
|
||||
#if defined(RP2040_PLATFORM)
|
||||
File file = fs->open(filename, "r");
|
||||
#else
|
||||
@@ -130,70 +139,21 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
}
|
||||
}
|
||||
|
||||
void CommonCLI::savePrefs(FILESYSTEM* fs) {
|
||||
bool CommonCLI::savePrefs(FILESYSTEM* fs) {
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
fs->remove("/com_prefs");
|
||||
File file = fs->open("/com_prefs", FILE_O_WRITE);
|
||||
fs->remove("/prefs.json");
|
||||
File file = fs->open("/prefs.json", FILE_O_WRITE);
|
||||
#elif defined(RP2040_PLATFORM)
|
||||
File file = fs->open("/com_prefs", "w");
|
||||
File file = fs->open("/prefs.json", "w");
|
||||
#else
|
||||
File file = fs->open("/com_prefs", "w", true);
|
||||
File file = fs->open("/prefs.json", "w", true);
|
||||
#endif
|
||||
if (file) {
|
||||
uint8_t pad[8];
|
||||
memset(pad, 0, sizeof(pad));
|
||||
|
||||
file.write((uint8_t *)&_prefs->airtime_factor, sizeof(_prefs->airtime_factor)); // 0
|
||||
file.write((uint8_t *)&_prefs->node_name, sizeof(_prefs->node_name)); // 4
|
||||
file.write(pad, 4); // 36
|
||||
file.write((uint8_t *)&_prefs->node_lat, sizeof(_prefs->node_lat)); // 40
|
||||
file.write((uint8_t *)&_prefs->node_lon, sizeof(_prefs->node_lon)); // 48
|
||||
file.write((uint8_t *)&_prefs->password[0], sizeof(_prefs->password)); // 56
|
||||
file.write((uint8_t *)&_prefs->freq, sizeof(_prefs->freq)); // 72
|
||||
file.write((uint8_t *)&_prefs->tx_power_dbm, sizeof(_prefs->tx_power_dbm)); // 76
|
||||
file.write((uint8_t *)&_prefs->disable_fwd, sizeof(_prefs->disable_fwd)); // 77
|
||||
file.write((uint8_t *)&_prefs->advert_interval, sizeof(_prefs->advert_interval)); // 78
|
||||
file.write(pad, 1); // 79 : 1 byte unused (rx_boosted_gain moved to end)
|
||||
file.write((uint8_t *)&_prefs->rx_delay_base, sizeof(_prefs->rx_delay_base)); // 80
|
||||
file.write((uint8_t *)&_prefs->tx_delay_factor, sizeof(_prefs->tx_delay_factor)); // 84
|
||||
file.write((uint8_t *)&_prefs->guest_password[0], sizeof(_prefs->guest_password)); // 88
|
||||
file.write((uint8_t *)&_prefs->direct_tx_delay_factor, sizeof(_prefs->direct_tx_delay_factor)); // 104
|
||||
file.write(pad, 4); // 108 : 4 byte unused
|
||||
file.write((uint8_t *)&_prefs->sf, sizeof(_prefs->sf)); // 112
|
||||
file.write((uint8_t *)&_prefs->cr, sizeof(_prefs->cr)); // 113
|
||||
file.write((uint8_t *)&_prefs->allow_read_only, sizeof(_prefs->allow_read_only)); // 114
|
||||
file.write((uint8_t *)&_prefs->multi_acks, sizeof(_prefs->multi_acks)); // 115
|
||||
file.write((uint8_t *)&_prefs->bw, sizeof(_prefs->bw)); // 116
|
||||
file.write((uint8_t *)&_prefs->agc_reset_interval, sizeof(_prefs->agc_reset_interval)); // 120
|
||||
file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 121
|
||||
file.write((uint8_t *)&_prefs->loop_detect, sizeof(_prefs->loop_detect)); // 122
|
||||
file.write(pad, 1); // 123
|
||||
file.write((uint8_t *)&_prefs->flood_max, sizeof(_prefs->flood_max)); // 124
|
||||
file.write((uint8_t *)&_prefs->flood_advert_interval, sizeof(_prefs->flood_advert_interval)); // 125
|
||||
file.write((uint8_t *)&_prefs->interference_threshold, sizeof(_prefs->interference_threshold)); // 126
|
||||
file.write((uint8_t *)&_prefs->bridge_enabled, sizeof(_prefs->bridge_enabled)); // 127
|
||||
file.write((uint8_t *)&_prefs->bridge_delay, sizeof(_prefs->bridge_delay)); // 128
|
||||
file.write((uint8_t *)&_prefs->bridge_pkt_src, sizeof(_prefs->bridge_pkt_src)); // 130
|
||||
file.write((uint8_t *)&_prefs->bridge_baud, sizeof(_prefs->bridge_baud)); // 131
|
||||
file.write((uint8_t *)&_prefs->bridge_channel, sizeof(_prefs->bridge_channel)); // 135
|
||||
file.write((uint8_t *)&_prefs->bridge_secret, sizeof(_prefs->bridge_secret)); // 136
|
||||
file.write((uint8_t *)&_prefs->powersaving_enabled, sizeof(_prefs->powersaving_enabled)); // 152
|
||||
file.write(pad, 3); // 153
|
||||
file.write((uint8_t *)&_prefs->gps_enabled, sizeof(_prefs->gps_enabled)); // 156
|
||||
file.write((uint8_t *)&_prefs->gps_interval, sizeof(_prefs->gps_interval)); // 157
|
||||
file.write((uint8_t *)&_prefs->advert_loc_policy, sizeof(_prefs->advert_loc_policy)); // 161
|
||||
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
|
||||
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
|
||||
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
|
||||
file.write((uint8_t *)&_prefs->rx_boosted_gain, sizeof(_prefs->rx_boosted_gain)); // 290
|
||||
file.write((uint8_t *)&_prefs->flood_max_unscoped, sizeof(_prefs->flood_max_unscoped)); // 291
|
||||
file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292
|
||||
file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293
|
||||
file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294
|
||||
// next: 295
|
||||
|
||||
bool success = _prefs->saveSerial(file);
|
||||
file.close();
|
||||
return success;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#define MIN_LOCAL_ADVERT_INTERVAL 60
|
||||
|
||||
+162
-38
@@ -5,6 +5,7 @@
|
||||
#include <helpers/SensorManager.h>
|
||||
#include <helpers/ClientACL.h>
|
||||
#include <helpers/RegionMap.h>
|
||||
#include <helpers/ConfigSerializer.h>
|
||||
|
||||
#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE)
|
||||
#define WITH_BRIDGE
|
||||
@@ -19,52 +20,175 @@
|
||||
#define LOOP_DETECT_MODERATE 2
|
||||
#define LOOP_DETECT_STRICT 3
|
||||
|
||||
struct NodePrefs { // persisted to file
|
||||
float airtime_factor;
|
||||
class NodePrefs : public ConfigSerializer {
|
||||
public:
|
||||
// in-memory backing data
|
||||
float airtime_factor = 0;
|
||||
char node_name[32];
|
||||
double node_lat, node_lon;
|
||||
double node_lat = 0, node_lon = 0;
|
||||
char password[16];
|
||||
float freq;
|
||||
int8_t tx_power_dbm;
|
||||
uint8_t disable_fwd;
|
||||
uint8_t advert_interval; // minutes / 2
|
||||
uint8_t flood_advert_interval; // hours
|
||||
float rx_delay_base;
|
||||
float tx_delay_factor;
|
||||
float freq = 0;
|
||||
int8_t tx_power_dbm = 0;
|
||||
uint8_t disable_fwd = 0;
|
||||
uint8_t advert_interval = 0; // minutes / 2
|
||||
uint8_t flood_advert_interval = 0; // hours
|
||||
float rx_delay_base = 0;
|
||||
float tx_delay_factor = 0;
|
||||
char guest_password[16];
|
||||
float direct_tx_delay_factor;
|
||||
float direct_tx_delay_factor = 0;
|
||||
uint32_t guard;
|
||||
uint8_t sf;
|
||||
uint8_t cr;
|
||||
uint8_t allow_read_only;
|
||||
uint8_t multi_acks;
|
||||
float bw;
|
||||
uint8_t flood_max;
|
||||
uint8_t flood_max_unscoped;
|
||||
uint8_t flood_max_advert;
|
||||
uint8_t interference_threshold;
|
||||
uint8_t agc_reset_interval; // secs / 4
|
||||
uint8_t sf = 0;
|
||||
uint8_t cr = 0;
|
||||
uint8_t allow_read_only = 0;
|
||||
uint8_t multi_acks = 0;
|
||||
float bw = 0;
|
||||
uint8_t flood_max = 0;
|
||||
uint8_t flood_max_unscoped = 0;
|
||||
uint8_t flood_max_advert = 0;
|
||||
uint8_t interference_threshold = 0;
|
||||
uint8_t agc_reset_interval = 0; // secs / 4
|
||||
// Bridge settings
|
||||
uint8_t bridge_enabled; // boolean
|
||||
uint16_t bridge_delay; // milliseconds (default 500 ms)
|
||||
uint8_t bridge_pkt_src; // 0 = logTx, 1 = logRx (default logTx)
|
||||
uint32_t bridge_baud; // 9600, 19200, 38400, 57600, 115200 (default 115200)
|
||||
uint8_t bridge_channel; // 1-14 (ESP-NOW only)
|
||||
uint8_t bridge_enabled = 0; // boolean
|
||||
uint16_t bridge_delay = 0; // milliseconds (default 500 ms)
|
||||
uint8_t bridge_pkt_src = 0; // 0 = logTx, 1 = logRx (default logTx)
|
||||
uint32_t bridge_baud = 0; // 9600, 19200, 38400, 57600, 115200 (default 115200)
|
||||
uint8_t bridge_channel = 0; // 1-14 (ESP-NOW only)
|
||||
char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only)
|
||||
// Power setting
|
||||
uint8_t powersaving_enabled; // boolean
|
||||
uint8_t powersaving_enabled = 0; // boolean
|
||||
// Gps settings
|
||||
uint8_t gps_enabled;
|
||||
uint32_t gps_interval; // in seconds
|
||||
uint8_t advert_loc_policy;
|
||||
uint32_t discovery_mod_timestamp;
|
||||
float adc_multiplier;
|
||||
uint8_t gps_enabled = 0;
|
||||
uint32_t gps_interval = 0; // in seconds
|
||||
uint8_t advert_loc_policy = 0;
|
||||
uint32_t discovery_mod_timestamp = 0;
|
||||
float adc_multiplier = 0;
|
||||
char owner_info[120];
|
||||
uint8_t rx_boosted_gain; // power settings
|
||||
uint8_t radio_fem_rxgain; // LoRa FEM RX gain setting
|
||||
uint8_t path_hash_mode; // which path mode to use when sending
|
||||
uint8_t loop_detect;
|
||||
uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean)
|
||||
uint8_t rx_boosted_gain = 0; // power settings
|
||||
uint8_t radio_fem_rxgain = 0; // LoRa FEM RX gain setting
|
||||
uint8_t path_hash_mode = 0; // which path mode to use when sending
|
||||
uint8_t loop_detect = 0;
|
||||
uint8_t cad_enabled = 0; // hardware Channel Activity Detection before TX (boolean)
|
||||
|
||||
private:
|
||||
class RadioPrefs : public ConfigSerializer {
|
||||
NodePrefs* _parent;
|
||||
protected:
|
||||
void structure() override {
|
||||
def("freq", _parent->freq);
|
||||
def("bw", _parent->bw);
|
||||
def("sf", _parent->sf);
|
||||
def("cr", _parent->cr);
|
||||
def("cad", _parent->cad_enabled);
|
||||
def("int_thr", _parent->interference_threshold);
|
||||
def("rxgain", _parent->rx_boosted_gain);
|
||||
def("fem_rxgain", _parent->rx_boosted_gain);
|
||||
def("tx", _parent->tx_power_dbm);
|
||||
def("af", _parent->airtime_factor);
|
||||
def("rxdelay", _parent->rx_delay_base);
|
||||
def("f_txdelay", _parent->tx_delay_factor);
|
||||
def("d_txdelay", _parent->direct_tx_delay_factor);
|
||||
def("agc_int", _parent->agc_reset_interval);
|
||||
def("hash_mode", _parent->path_hash_mode);
|
||||
def("multi_ack", _parent->multi_acks);
|
||||
}
|
||||
public:
|
||||
RadioPrefs(NodePrefs* parent) : _parent(parent) { }
|
||||
};
|
||||
RadioPrefs radio;
|
||||
|
||||
class BridgePrefs : public ConfigSerializer {
|
||||
NodePrefs* _parent;
|
||||
protected:
|
||||
void structure() override {
|
||||
def("en", _parent->bridge_enabled); // boolean
|
||||
def("delay", _parent->bridge_delay); // milliseconds (default 500 ms)
|
||||
def("src", _parent->bridge_pkt_src); // 0 = logTx, 1 = logRx (default logTx)
|
||||
def("baud", _parent->bridge_baud); // 9600, 19200, 38400, 57600, 115200 (default 115200)
|
||||
def("ch", _parent->bridge_channel); // 1-14 (ESP-NOW only)
|
||||
def("secret", _parent->bridge_secret, sizeof(_parent->bridge_secret)); // for XOR encryption of bridge packets (ESP-NOW only)
|
||||
}
|
||||
public:
|
||||
BridgePrefs(NodePrefs* parent) : _parent(parent) { }
|
||||
};
|
||||
BridgePrefs bridge;
|
||||
|
||||
class GPSPrefs : public ConfigSerializer {
|
||||
NodePrefs* _parent;
|
||||
protected:
|
||||
void structure() override {
|
||||
def("en", _parent->gps_enabled); // boolean
|
||||
def("int", _parent->gps_interval); // interval in seconds
|
||||
def("adv_loc", _parent->advert_loc_policy);
|
||||
}
|
||||
public:
|
||||
GPSPrefs(NodePrefs* parent) : _parent(parent) { }
|
||||
};
|
||||
GPSPrefs gps;
|
||||
|
||||
class PowerPrefs : public ConfigSerializer {
|
||||
NodePrefs* _parent;
|
||||
protected:
|
||||
void structure() override {
|
||||
def("adc_mult", _parent->adc_multiplier);
|
||||
def("pwr_sav_en", _parent->powersaving_enabled);
|
||||
}
|
||||
public:
|
||||
PowerPrefs(NodePrefs* parent) : _parent(parent) { }
|
||||
};
|
||||
PowerPrefs power;
|
||||
|
||||
class RepeatPrefs : public ConfigSerializer {
|
||||
NodePrefs* _parent;
|
||||
protected:
|
||||
void structure() override {
|
||||
def("disable", _parent->disable_fwd);
|
||||
def("f_max", _parent->flood_max);
|
||||
def("f_max_uns", _parent->flood_max_unscoped);
|
||||
def("f_max_adv", _parent->flood_max_advert);
|
||||
def("loop", _parent->loop_detect);
|
||||
}
|
||||
public:
|
||||
RepeatPrefs(NodePrefs* parent) : _parent(parent) { }
|
||||
};
|
||||
RepeatPrefs repeat;
|
||||
|
||||
class RoomPrefs : public ConfigSerializer {
|
||||
NodePrefs* _parent;
|
||||
protected:
|
||||
void structure() override {
|
||||
def("rd_only", _parent->allow_read_only);
|
||||
}
|
||||
public:
|
||||
RoomPrefs(NodePrefs* parent) : _parent(parent) { }
|
||||
};
|
||||
RoomPrefs room;
|
||||
|
||||
protected:
|
||||
void structure() override {
|
||||
def("name", node_name, sizeof(node_name));
|
||||
def("pass", password, sizeof(password));
|
||||
def("guest", guest_password, sizeof(guest_password));
|
||||
def("owner", owner_info, sizeof(owner_info));
|
||||
def("adv_int", advert_interval);
|
||||
def("f_adv_int", flood_advert_interval);
|
||||
def("lat", node_lat);
|
||||
def("lon", node_lon);
|
||||
def("radio", radio);
|
||||
def("bridge", bridge);
|
||||
def("gps", gps);
|
||||
def("repeat", repeat);
|
||||
def("room", room);
|
||||
def("power", power);
|
||||
}
|
||||
|
||||
public:
|
||||
NodePrefs() : ConfigSerializer(), bridge(this), gps(this), radio(this), power(this), repeat(this), room(this) {
|
||||
node_name[0] = 0;
|
||||
password[0] = 0;
|
||||
guest_password[0] = 0;
|
||||
bridge_secret[0] = 0;
|
||||
owner_info[0] = 0;
|
||||
}
|
||||
};
|
||||
|
||||
class CommonCLICallbacks {
|
||||
@@ -139,7 +263,7 @@ public:
|
||||
: _board(&board), _rtc(&rtc), _sensors(&sensors), _region_map(®ion_map), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { }
|
||||
|
||||
void loadPrefs(FILESYSTEM* _fs);
|
||||
void savePrefs(FILESYSTEM* _fs);
|
||||
bool savePrefs(FILESYSTEM* _fs);
|
||||
void handleCommand(uint32_t sender_timestamp, char* command, char* reply);
|
||||
uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
#include "ConfigSerializer.h"
|
||||
|
||||
bool ConfigSerializer::saveSerial(Stream& s) {
|
||||
Context context(&s, OP::WRITE);
|
||||
_context = &context; // set the context for structure() call
|
||||
s.print("{"); // root object
|
||||
_first = true;
|
||||
structure();
|
||||
if (s.print("}") != 1) context.success = false; // failure detect
|
||||
_context = NULL;
|
||||
return context.success;
|
||||
}
|
||||
|
||||
#define TOK_ERROR -1
|
||||
#define TOK_EOF 0
|
||||
#define TOK_KEY 1
|
||||
#define TOK_VALUE 2
|
||||
#define TOK_START_OBJ 3
|
||||
#define TOK_END_OBJ 4
|
||||
#define TOK_WHITESPACE 5
|
||||
|
||||
static bool is_whitespace(char c) {
|
||||
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
|
||||
}
|
||||
static bool is_key_char(char c) {
|
||||
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_';
|
||||
}
|
||||
static bool is_value_char(char c) {
|
||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || c == '-' || c == '.';
|
||||
}
|
||||
|
||||
#define EXPECT_OPEN_BRACE 0
|
||||
#define EXPECT_KEY 1
|
||||
#define EXPECT_VAL_OR_OBJ 2
|
||||
#define EXPECT_STRING_VAL 3
|
||||
#define EXPECT_STRING_ESCAPE 4
|
||||
#define EXPECT_COMMA_OR_CLOSE 5
|
||||
#define EXPECT_COMMA_OR_KEY 6
|
||||
#define EXPECT_COMMA_OR_KEY_OR_CLOSE 7
|
||||
|
||||
int ConfigSerializer::Context::readNext() {
|
||||
char c;
|
||||
if (pending) {
|
||||
c = pending;
|
||||
pending = 0;
|
||||
} else {
|
||||
if (_f->available() == 0) return TOK_EOF;
|
||||
|
||||
int n = _f->read();
|
||||
if (n < 0) return TOK_EOF;
|
||||
c = (char)n;
|
||||
}
|
||||
|
||||
switch (rd_mode) {
|
||||
case EXPECT_OPEN_BRACE:
|
||||
if (c == '{') { rd_mode = EXPECT_KEY; return TOK_START_OBJ; }
|
||||
if (is_whitespace(c)) return TOK_WHITESPACE;
|
||||
return TOK_ERROR;
|
||||
|
||||
case EXPECT_COMMA_OR_KEY_OR_CLOSE:
|
||||
if (c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; }
|
||||
case EXPECT_COMMA_OR_KEY:
|
||||
if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; }
|
||||
case EXPECT_KEY:
|
||||
if (rd_len > 0 && c == ':') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_VAL_OR_OBJ; return TOK_KEY; }
|
||||
if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE;
|
||||
if (rd_len < CONFIG_MAX_KEYLEN-1 && is_key_char(c)) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; }
|
||||
return TOK_ERROR;
|
||||
|
||||
case EXPECT_VAL_OR_OBJ:
|
||||
if (rd_len == 0 && is_whitespace(c)) return TOK_WHITESPACE;
|
||||
if (rd_len == 0 && c == '"') { rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; }
|
||||
if (rd_len == 0 && c == '{') { rd_mode = EXPECT_KEY; return TOK_START_OBJ; }
|
||||
if (is_value_char(c) && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; }
|
||||
if (rd_len > 0 && (c == ',' || c == '}' || is_whitespace(c))) { pending = c; rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_COMMA_OR_CLOSE; return TOK_VALUE; }
|
||||
return TOK_ERROR;
|
||||
|
||||
case EXPECT_STRING_ESCAPE:
|
||||
if ((c == 'n') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = '\n'; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; }
|
||||
if ((c == 'r') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = '\r'; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; }
|
||||
if ((c == '"' || c == '\\' || c == '/') && rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; rd_mode = EXPECT_STRING_VAL; return TOK_WHITESPACE; }
|
||||
return TOK_ERROR; // unsupport escape
|
||||
|
||||
case EXPECT_STRING_VAL:
|
||||
if (c == '"') { rd_buf[rd_len] = 0; rd_len = 0; rd_mode = EXPECT_COMMA_OR_CLOSE; return TOK_VALUE; }
|
||||
if (c == '\\') { rd_mode = EXPECT_STRING_ESCAPE; return TOK_WHITESPACE; }
|
||||
if (rd_len < CONFIG_MAX_TOKEN_LEN-1) { rd_buf[rd_len++] = c; return TOK_WHITESPACE; }
|
||||
return TOK_ERROR;
|
||||
|
||||
case EXPECT_COMMA_OR_CLOSE:
|
||||
if (c == ',') { rd_mode = EXPECT_KEY; return TOK_WHITESPACE; }
|
||||
if (c == '}') { rd_mode = EXPECT_COMMA_OR_KEY_OR_CLOSE; return TOK_END_OBJ; }
|
||||
if (is_whitespace(c)) return TOK_WHITESPACE;
|
||||
return TOK_ERROR;
|
||||
}
|
||||
return TOK_ERROR; // unknown mode
|
||||
}
|
||||
|
||||
bool ConfigSerializer::loadSerial(Stream& s) {
|
||||
Context context(&s, OP::READ);
|
||||
_context = &context; // set the context for structure() call
|
||||
uint8_t sp = 0; // object nesting stack pointer
|
||||
int next_tok;
|
||||
|
||||
// parse the Json file
|
||||
while ((next_tok = context.readNext()) > TOK_EOF) {
|
||||
if (next_tok == TOK_KEY) {
|
||||
context.setKey(sp, context.getToken());
|
||||
} else if (next_tok == TOK_VALUE) {
|
||||
_depth = 1; // re-run the structure() hierarchy again (looking for specific key, at specific depth)
|
||||
structure();
|
||||
} else if (next_tok == TOK_START_OBJ) {
|
||||
if (sp < CONFIG_MAX_DEPTH - 1) {
|
||||
sp++;
|
||||
} else {
|
||||
//Serial.printf("Error: max nesting reached"); // TODO: debug logging
|
||||
context.success = false;
|
||||
break;
|
||||
}
|
||||
} else if (next_tok == TOK_END_OBJ) {
|
||||
if (sp > 0) {
|
||||
sp--;
|
||||
} else {
|
||||
//Serial.printf("Error: too many closing '}'"); // TODO: debug logging
|
||||
context.success = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sp != 0 || next_tok == TOK_ERROR) {
|
||||
context.success = false; // unmatched { }, or other parse error
|
||||
}
|
||||
_context = NULL;
|
||||
return context.success;
|
||||
}
|
||||
|
||||
void ConfigSerializer::writeComma() {
|
||||
if (_first) {
|
||||
_first = false;
|
||||
} else {
|
||||
_context->file()->print(","); // comma separated properties
|
||||
}
|
||||
}
|
||||
|
||||
#include <Utils.h>
|
||||
|
||||
void ConfigSerializer::def(const char* key, void* value, size_t len) {
|
||||
if (_context->op() == OP::WRITE) {
|
||||
writeComma();
|
||||
_context->file()->print(key);
|
||||
_context->file()->print(":\"");
|
||||
mesh::Utils::printHex(*_context->file(), (uint8_t*) value, len);
|
||||
_context->file()->print("\"");
|
||||
} else {
|
||||
if (_context->keyMatch(_depth, key)) {
|
||||
memset(value, 0, len);
|
||||
mesh::Utils::fromHex((uint8_t *)value, len, _context->getToken());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigSerializer::def(const char* key, char* value, size_t max_len) {
|
||||
if (_context->op() == OP::WRITE) {
|
||||
writeComma();
|
||||
_context->file()->print(key);
|
||||
_context->file()->print(":\"");
|
||||
char c;
|
||||
while ((c = *value++) != 0) { // TODO: handle UTF-8 encoding
|
||||
if (c == '"') {
|
||||
_context->file()->print("\\\"");
|
||||
} else if (c == '\\') {
|
||||
_context->file()->print("\\\\");
|
||||
} else if (c == '\n') {
|
||||
_context->file()->print("\\n");
|
||||
} else if (c == '\r') {
|
||||
_context->file()->print("\\r");
|
||||
} else {
|
||||
_context->file()->print(c);
|
||||
}
|
||||
}
|
||||
_context->file()->print("\"");
|
||||
} else {
|
||||
if (_context->keyMatch(_depth, key)) {
|
||||
strncpy(value, _context->getToken(), max_len - 1);
|
||||
value[max_len - 1] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigSerializer::def(const char* key, int32_t& value) {
|
||||
if (_context->op() == OP::WRITE) {
|
||||
writeComma();
|
||||
_context->file()->print(key);
|
||||
_context->file()->print(":");
|
||||
_context->file()->print(value);
|
||||
} else {
|
||||
if (_context->keyMatch(_depth, key)) {
|
||||
value = atol(_context->getToken());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigSerializer::def(const char* key, uint32_t& value) {
|
||||
if (_context->op() == OP::WRITE) {
|
||||
writeComma();
|
||||
_context->file()->print(key);
|
||||
_context->file()->print(":");
|
||||
_context->file()->print(value);
|
||||
} else {
|
||||
if (_context->keyMatch(_depth, key)) {
|
||||
value = atol(_context->getToken());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigSerializer::def(const char* key, int16_t& value) {
|
||||
if (_context->op() == OP::WRITE) {
|
||||
writeComma();
|
||||
_context->file()->print(key);
|
||||
_context->file()->print(":");
|
||||
_context->file()->print((int32_t) value, 10);
|
||||
} else {
|
||||
if (_context->keyMatch(_depth, key)) {
|
||||
value = atol(_context->getToken());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigSerializer::def(const char* key, uint16_t& value) {
|
||||
if (_context->op() == OP::WRITE) {
|
||||
writeComma();
|
||||
_context->file()->print(key);
|
||||
_context->file()->print(":");
|
||||
_context->file()->print((uint32_t) value, 10);
|
||||
} else {
|
||||
if (_context->keyMatch(_depth, key)) {
|
||||
value = atoi(_context->getToken());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigSerializer::def(const char* key, uint8_t& value) {
|
||||
if (_context->op() == OP::WRITE) {
|
||||
writeComma();
|
||||
_context->file()->print(key);
|
||||
_context->file()->print(":");
|
||||
_context->file()->print((uint32_t) value, 10);
|
||||
} else {
|
||||
if (_context->keyMatch(_depth, key)) {
|
||||
value = atoi(_context->getToken());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigSerializer::def(const char* key, int8_t& value) {
|
||||
if (_context->op() == OP::WRITE) {
|
||||
writeComma();
|
||||
_context->file()->print(key);
|
||||
_context->file()->print(":");
|
||||
_context->file()->print((int32_t) value, 10);
|
||||
} else {
|
||||
if (_context->keyMatch(_depth, key)) {
|
||||
value = atoi(_context->getToken());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigSerializer::def(const char* key, bool& value) {
|
||||
if (_context->op() == OP::WRITE) {
|
||||
writeComma();
|
||||
_context->file()->print(key);
|
||||
_context->file()->print(":");
|
||||
_context->file()->print(value ? "true" : "false");
|
||||
} else {
|
||||
if (_context->keyMatch(_depth, key)) {
|
||||
value = strcmp(_context->getToken(), "true") == 0 || atoi(_context->getToken()) != 0; // 'true' or a non-zero number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigSerializer::def(const char* key, double& value) {
|
||||
if (_context->op() == OP::WRITE) {
|
||||
writeComma();
|
||||
_context->file()->print(key);
|
||||
_context->file()->print(":");
|
||||
if (value == 0.0) {
|
||||
_context->file()->print("0"); // shorter encoding
|
||||
} else {
|
||||
_context->file()->print(value, 6); // REVISIT: how many dec places?
|
||||
}
|
||||
} else {
|
||||
if (_context->keyMatch(_depth, key)) {
|
||||
value = atof(_context->getToken());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigSerializer::def(const char* key, float& value) {
|
||||
if (_context->op() == OP::WRITE) {
|
||||
writeComma();
|
||||
_context->file()->print(key);
|
||||
_context->file()->print(":");
|
||||
if (value == 0.0f) {
|
||||
_context->file()->print("0"); // shorter encoding
|
||||
} else {
|
||||
_context->file()->print(value, 4); // REVISIT: how many dec places?
|
||||
}
|
||||
} else {
|
||||
if (_context->keyMatch(_depth, key)) {
|
||||
value = (float) atof(_context->getToken());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigSerializer::def(const char* key, ConfigSerializer& sub_obj) {
|
||||
if (_context->op() == OP::WRITE) {
|
||||
writeComma();
|
||||
_context->file()->print(key);
|
||||
_context->file()->print(":{");
|
||||
sub_obj._context = _context; // inherit the Context
|
||||
sub_obj._first = true;
|
||||
sub_obj.structure(); // recurse into sub object
|
||||
if (_context->file()->print("}") != 1) _context->success = false; // failure detect
|
||||
} else {
|
||||
if (_context->keyMatch(_depth, key)) {
|
||||
sub_obj._context = _context; // inherit the Context
|
||||
sub_obj._depth = _depth + 1;
|
||||
sub_obj.structure(); // recurse into sub object
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
#ifndef CONFIG_MAX_DEPTH
|
||||
#define CONFIG_MAX_DEPTH 8
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_MAX_KEYLEN
|
||||
#define CONFIG_MAX_KEYLEN 16
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_MAX_TOKEN_LEN
|
||||
#define CONFIG_MAX_TOKEN_LEN 128
|
||||
#endif
|
||||
|
||||
class ConfigSerializer {
|
||||
bool _first;
|
||||
int8_t _depth;
|
||||
|
||||
enum OP { READ, WRITE };
|
||||
|
||||
class Context {
|
||||
Stream* _f;
|
||||
OP _op;
|
||||
uint8_t rd_len;
|
||||
uint8_t rd_mode;
|
||||
char pending;
|
||||
char rd_buf[CONFIG_MAX_TOKEN_LEN];
|
||||
char _keys[CONFIG_MAX_DEPTH][CONFIG_MAX_KEYLEN];
|
||||
|
||||
public:
|
||||
bool success = true;
|
||||
Context(Stream* f, OP op) : _f(f), _op(op) { rd_buf[rd_len = 0] = 0; rd_mode = 0; pending = 0; }
|
||||
OP op() const { return _op; }
|
||||
Stream* file() const { return _f; }
|
||||
int readNext();
|
||||
const char* getToken() const { return rd_buf; }
|
||||
bool keyMatch(int8_t depth, const char* key) { return strcmp(key, _keys[depth]) == 0; }
|
||||
void setKey(uint8_t depth, const char* key) { strcpy(_keys[depth], key); }
|
||||
};
|
||||
|
||||
Context* _context = NULL;
|
||||
|
||||
void writeComma();
|
||||
|
||||
protected:
|
||||
ConfigSerializer() { }
|
||||
|
||||
void def(const char* key, char* value, size_t max_len); // max_len inclusive of null
|
||||
void def(const char* key, void* value, size_t len); // binary blob (encoded in hex)
|
||||
void def(const char* key, int32_t& value);
|
||||
void def(const char* key, int16_t& value);
|
||||
void def(const char* key, int8_t& value);
|
||||
void def(const char* key, uint32_t& value);
|
||||
void def(const char* key, uint16_t& value);
|
||||
void def(const char* key, uint8_t& value);
|
||||
void def(const char* key, float& value);
|
||||
void def(const char* key, double& value);
|
||||
void def(const char* key, bool& value);
|
||||
void def(const char* key, ConfigSerializer& sub_obj);
|
||||
|
||||
virtual void structure() = 0;
|
||||
|
||||
public:
|
||||
bool loadSerial(Stream& s);
|
||||
bool saveSerial(Stream& s);
|
||||
};
|
||||
+65
-15
@@ -1,23 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
class Stream {
|
||||
// Mock Stream class for native testing
|
||||
// Provides minimal interface needed by Utils.h
|
||||
|
||||
#define DEC 10
|
||||
#define HEX 16
|
||||
#define OCT 8
|
||||
#define BIN 2
|
||||
|
||||
class Print
|
||||
{
|
||||
public:
|
||||
virtual size_t write(uint8_t b) { return 1; }
|
||||
size_t write(const char *str)
|
||||
{
|
||||
if(str == NULL) {
|
||||
return 0;
|
||||
}
|
||||
return write((const uint8_t *) str, strlen(str));
|
||||
}
|
||||
virtual size_t write(const uint8_t *buffer, size_t size) {
|
||||
size_t t = 0;
|
||||
for (int i = 0; i < size; i++) { t += write(buffer[i]); }
|
||||
return t;
|
||||
}
|
||||
size_t write(const char *buffer, size_t size)
|
||||
{
|
||||
return write((const uint8_t *) buffer, size);
|
||||
}
|
||||
|
||||
virtual size_t print(unsigned char b, int r = DEC) { return 0; }
|
||||
virtual size_t print(int v, int r = DEC) { return 0; }
|
||||
virtual size_t print(unsigned int v, int r = DEC) { return 0; }
|
||||
virtual size_t print(long v, int r = DEC) { return 0; }
|
||||
virtual size_t print(unsigned long v, int r = DEC) { return 0; }
|
||||
virtual size_t print(long long v, int r = DEC) { return 0; }
|
||||
virtual size_t print(unsigned long long v, int r = DEC) { return 0; }
|
||||
virtual size_t print(double v, int p = 2) { return 0; }
|
||||
|
||||
size_t print(char c) { return write(c); }
|
||||
size_t print(const char* str) { return write(str); }
|
||||
|
||||
//size_t println(void) { return 0; }
|
||||
|
||||
virtual void flush() { /* Empty implementation for backward compatibility */ }
|
||||
};
|
||||
|
||||
class Stream: public Print
|
||||
{
|
||||
public:
|
||||
virtual ~Stream() = default;
|
||||
virtual size_t write(uint8_t) { return 1; }
|
||||
virtual size_t write(const uint8_t* buffer, size_t size) {
|
||||
size_t total = 0;
|
||||
for (size_t i = 0; i < size; i++) {
|
||||
total += write(buffer[i]);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
virtual int available() { return 0; }
|
||||
virtual int availableForWrite() { return 0; }
|
||||
virtual int read() { return -1; }
|
||||
virtual void flush() {}
|
||||
virtual void print(char) {}
|
||||
virtual void print(const char*) {}
|
||||
};
|
||||
virtual int peek() { return 0; }
|
||||
|
||||
virtual size_t readBytes(char *buffer, size_t length) {
|
||||
size_t i = 0;
|
||||
while (i < length && available()) {
|
||||
buffer[i++] = read();
|
||||
}
|
||||
return i;
|
||||
}
|
||||
virtual size_t readBytes(uint8_t *buffer, size_t length)
|
||||
{
|
||||
return readBytes((char *) buffer, length);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,180 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "helpers/ConfigSerializer.h"
|
||||
|
||||
#define TEST_INT_S "56"
|
||||
#define TEST_INT 56
|
||||
#define TEST_FLOAT_S "-6.123"
|
||||
#define TEST_FLOAT -6.1230f
|
||||
#define TEST_DOUBLE_S "12.123456"
|
||||
#define TEST_DOUBLE 12.123456
|
||||
|
||||
class MockInputStream : public Stream {
|
||||
const char* _text;
|
||||
int pos, len;
|
||||
public:
|
||||
MockInputStream(const char* text) : _text(text) { pos = 0; len = strlen(text); }
|
||||
int available() override { return len - pos; }
|
||||
int read() override { if (pos < len) { return _text[pos++]; } return -1; }
|
||||
int peek() override { if (pos < len) { return _text[pos]; } return -1; }
|
||||
};
|
||||
|
||||
class MockPrintStream : public Stream {
|
||||
int len = 0;
|
||||
uint8_t _buf[1024];
|
||||
public:
|
||||
size_t write(uint8_t b) override {
|
||||
if (len < sizeof(_buf)) {
|
||||
_buf[len++] = b;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t print(unsigned char b, int r) override { if (b == TEST_INT) return Print::print(TEST_INT_S); return 0; }
|
||||
size_t print(int v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
|
||||
size_t print(unsigned int v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
|
||||
size_t print(long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
|
||||
size_t print(unsigned long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
|
||||
size_t print(long long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
|
||||
size_t print(unsigned long long v, int r) override { if (v == TEST_INT) return Print::print(TEST_INT_S); return 0; }
|
||||
size_t print(double v, int p = 2) override {
|
||||
if (p == 6) return Print::print(TEST_DOUBLE_S);
|
||||
if (p == 4) return Print::print(TEST_FLOAT_S);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int getLength() const { return len; }
|
||||
const uint8_t* getBytes() const { return _buf; }
|
||||
};
|
||||
|
||||
class TestStruct : public ConfigSerializer {
|
||||
protected:
|
||||
void structure() override {
|
||||
def("age", age);
|
||||
def("flags", flags);
|
||||
def("name", name, sizeof(name));
|
||||
}
|
||||
public:
|
||||
int32_t age;
|
||||
char name[16];
|
||||
uint8_t flags;
|
||||
};
|
||||
|
||||
// ── saveSerial: basic ───────────────────────────────────────────────────────
|
||||
|
||||
TEST(ConfigSerializer, SaveSerial_Basic) {
|
||||
MockPrintStream s;
|
||||
TestStruct data;
|
||||
|
||||
data.age = TEST_INT;
|
||||
data.flags = TEST_INT;
|
||||
strcpy(data.name, "Scott");
|
||||
|
||||
bool success = data.saveSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
auto l = s.getLength();
|
||||
const char* expect = "{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"}";
|
||||
EXPECT_EQ(strlen(expect), l);
|
||||
|
||||
bool match = memcmp(s.getBytes(), expect, l) == 0;
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
|
||||
TEST(ConfigSerializer, SaveSerial_EscChars) {
|
||||
MockPrintStream s;
|
||||
TestStruct data;
|
||||
|
||||
data.age = TEST_INT;
|
||||
data.flags = TEST_INT;
|
||||
strcpy(data.name, "\"Scott\"\n");
|
||||
|
||||
bool success = data.saveSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
auto l = s.getLength();
|
||||
const char* expect = "{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"\\\"Scott\\\"\\n\"}";
|
||||
EXPECT_EQ(strlen(expect), l);
|
||||
|
||||
bool match = memcmp(s.getBytes(), expect, l) == 0;
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
// ── loadSerial: basic ───────────────────────────────────────────────────────
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_Basic) {
|
||||
MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"}");
|
||||
TestStruct data;
|
||||
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
EXPECT_EQ(TEST_INT, data.age);
|
||||
EXPECT_EQ(TEST_INT, data.flags);
|
||||
bool match = strcmp("Scott", data.name) == 0;
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_HandleWhitespace) {
|
||||
MockInputStream s(" { age: " TEST_INT_S " , flags: " TEST_INT_S " , name: \"Scott\" } ");
|
||||
TestStruct data;
|
||||
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
EXPECT_EQ(TEST_INT, data.age);
|
||||
EXPECT_EQ(TEST_INT, data.flags);
|
||||
bool match = strcmp("Scott", data.name) == 0;
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_EscChars) {
|
||||
MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"\\\"Scott\\\"\\n\"}");
|
||||
TestStruct data;
|
||||
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
bool match = strcmp("\"Scott\"\n", data.name) == 0;
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_UnmatchedBraces) {
|
||||
MockInputStream s("{age:" TEST_INT_S ",flags:" TEST_INT_S ",name:\"Scott\"");
|
||||
TestStruct data;
|
||||
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_FALSE(success);
|
||||
}
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_MissingCommas) {
|
||||
MockInputStream s("{age:" TEST_INT_S " flags:" TEST_INT_S " name:\"Scott\"}");
|
||||
TestStruct data;
|
||||
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_FALSE(success);
|
||||
}
|
||||
|
||||
TEST(ConfigSerializer, LoadSerial_IgnoreUnknowns) {
|
||||
MockInputStream s("{age:" TEST_INT_S ",xxx:" TEST_INT_S ",name:\"Scott\"}");
|
||||
TestStruct data;
|
||||
data.flags = 1;
|
||||
|
||||
// should ignore the 'xxx' property
|
||||
bool success = data.loadSerial(s);
|
||||
EXPECT_TRUE(success);
|
||||
|
||||
EXPECT_EQ(TEST_INT, data.age);
|
||||
EXPECT_EQ(1, data.flags); // flags should be unmodified
|
||||
bool match = strcmp("Scott", data.name) == 0;
|
||||
EXPECT_TRUE(match);
|
||||
}
|
||||
|
||||
|
||||
// ── main ───────────────────────────────────────────────────────
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
Reference in New Issue
Block a user