mirror of
https://github.com/pelgraine/Meck.git
synced 2026-06-16 08:01:02 +02:00
522 lines
17 KiB
C
522 lines
17 KiB
C
#pragma once
|
|
// ---------------------------------------------------------------------------
|
|
// MeckImport.h -- Boot-time config import from MeshCore-app-compatible JSON.
|
|
//
|
|
// Checks for /meshcore/import.json on SD card. If found, parses it and
|
|
// applies the sections present:
|
|
// - Identity: replaces device keypair (requires reboot)
|
|
// - Radio: applies frequency, BW, SF, CR, TX power and device settings
|
|
// - Channels: merges by name (skips existing, adds new to empty slots)
|
|
// - Contacts: merges by pub_key (skips duplicates)
|
|
//
|
|
// After successful import the file is renamed to import_done.json.
|
|
// If identity was changed the device reboots automatically.
|
|
//
|
|
// Compatible with MeshCore companion app config export format.
|
|
// Handles both unit systems: freq in MHz or kHz, BW in kHz or Hz.
|
|
//
|
|
// Usage from main.cpp setup(), after the_mesh.begin():
|
|
// meckImportConfig(the_mesh, sensors.node_lat, sensors.node_lon,
|
|
// sdCardReady);
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#include <SD.h>
|
|
#include <helpers/ContactInfo.h>
|
|
#include <helpers/ChannelDetails.h>
|
|
|
|
// Fallback defines
|
|
#ifndef MAX_GROUP_CHANNELS
|
|
#define MAX_GROUP_CHANNELS 20
|
|
#endif
|
|
#ifndef AUTO_ADD_OVERWRITE_OLDEST
|
|
#define AUTO_ADD_OVERWRITE_OLDEST (1 << 0)
|
|
#define AUTO_ADD_CHAT (1 << 1)
|
|
#define AUTO_ADD_REPEATER (1 << 2)
|
|
#define AUTO_ADD_ROOM_SERVER (1 << 3)
|
|
#define AUTO_ADD_SENSOR (1 << 4)
|
|
#endif
|
|
|
|
// Parser state machine
|
|
enum MeckImportSection : uint8_t {
|
|
IMP_ROOT,
|
|
IMP_RADIO,
|
|
IMP_POSITION,
|
|
IMP_OTHER,
|
|
IMP_AUTOADD,
|
|
IMP_CHANNELS_ARRAY,
|
|
IMP_CHANNEL_OBJ,
|
|
IMP_CONTACTS_ARRAY,
|
|
IMP_CONTACT_OBJ,
|
|
};
|
|
|
|
// Strip leading whitespace from a C string, returning pointer into the buffer.
|
|
static char* meck_imp_trim(char* s) {
|
|
while (*s == ' ' || *s == '\t') s++;
|
|
return s;
|
|
}
|
|
|
|
// Extract the JSON key from a line like: "key": value
|
|
// Writes the key into keyBuf, returns pointer to the value portion (after ": "),
|
|
// or NULL if line doesn't contain a key-value pair.
|
|
static char* meck_imp_parse_kv(char* line, char* keyBuf, int keyBufSize) {
|
|
char* q1 = strchr(line, '"');
|
|
if (!q1) return NULL;
|
|
char* q2 = strchr(q1 + 1, '"');
|
|
if (!q2) return NULL;
|
|
|
|
int klen = q2 - q1 - 1;
|
|
if (klen >= keyBufSize) klen = keyBufSize - 1;
|
|
memcpy(keyBuf, q1 + 1, klen);
|
|
keyBuf[klen] = '\0';
|
|
|
|
char* valStart = q2 + 1;
|
|
while (*valStart == ':' || *valStart == ' ' || *valStart == '\t') valStart++;
|
|
|
|
// Strip trailing comma and whitespace
|
|
int vlen = strlen(valStart);
|
|
while (vlen > 0 && (valStart[vlen-1] == ',' || valStart[vlen-1] == '\n' ||
|
|
valStart[vlen-1] == '\r' || valStart[vlen-1] == ' ')) {
|
|
valStart[--vlen] = '\0';
|
|
}
|
|
|
|
return valStart;
|
|
}
|
|
|
|
// Extract a string value by stripping surrounding quotes and unescaping.
|
|
static void meck_imp_extract_string(char* dest, int destSize, char* val) {
|
|
if (val[0] == '"') val++;
|
|
int slen = strlen(val);
|
|
if (slen > 0 && val[slen-1] == '"') val[slen-1] = '\0';
|
|
|
|
int wi = 0;
|
|
for (int ri = 0; val[ri] && wi < destSize - 1; ri++) {
|
|
if (val[ri] == '\\' && val[ri+1]) { ri++; }
|
|
dest[wi++] = val[ri];
|
|
}
|
|
dest[wi] = '\0';
|
|
}
|
|
|
|
// Check for /meshcore/import.json and apply config if present.
|
|
// mesh: initialised MyMesh (after begin())
|
|
// node_lat/lon: references to update position (sensors.node_lat/lon)
|
|
// sdReady: whether SD card is mounted
|
|
//
|
|
// Requires MyMesh::saveMainIdentity() public method (add to MyMesh.h if missing):
|
|
// void saveMainIdentity() { _store->saveMainIdentity(self_id); }
|
|
//
|
|
// Returns: 0 = no import file found
|
|
// 1 = imported successfully (will reboot if identity changed)
|
|
// -1 = error during import
|
|
static int meckImportConfig(MyMesh& mesh,
|
|
double& node_lat, double& node_lon,
|
|
bool sdReady) {
|
|
if (!sdReady) return 0;
|
|
|
|
const char* importPath = "/meshcore/import.json";
|
|
const char* donePath = "/meshcore/import_done.json";
|
|
|
|
if (!SD.exists(importPath)) return 0;
|
|
|
|
Serial.printf("Config Import: found %s\n", importPath);
|
|
File f = SD.open(importPath, "r");
|
|
if (!f) {
|
|
Serial.println("Config Import: failed to open file");
|
|
return -1;
|
|
}
|
|
|
|
NodePrefs* prefs = mesh.getNodePrefs();
|
|
|
|
// Accumulated state
|
|
bool identityChanged = false;
|
|
bool prefsChanged = false;
|
|
bool contactsChanged = false;
|
|
|
|
// Identity buffers
|
|
uint8_t imp_pub[PUB_KEY_SIZE];
|
|
uint8_t imp_prv[PRV_KEY_SIZE];
|
|
bool gotPub = false, gotPrv = false;
|
|
|
|
// Channel accumulator
|
|
char ch_name[32];
|
|
uint8_t ch_secret[PUB_KEY_SIZE];
|
|
bool ch_gotName = false, ch_gotSecret = false;
|
|
int channelsAdded = 0, channelsSkipped = 0;
|
|
|
|
// Contact accumulator
|
|
ContactInfo ct;
|
|
uint8_t ct_pubkey[PUB_KEY_SIZE];
|
|
bool ct_gotPubkey = false, ct_gotType = false;
|
|
int contactsAdded = 0, contactsSkipped = 0;
|
|
|
|
MeckImportSection section = IMP_ROOT;
|
|
char lineBuf[256];
|
|
char key[48];
|
|
|
|
while (f.available()) {
|
|
// Read one line
|
|
int len = 0;
|
|
while (f.available() && len < (int)sizeof(lineBuf) - 1) {
|
|
char ch = f.read();
|
|
if (ch == '\n') break;
|
|
lineBuf[len++] = ch;
|
|
}
|
|
lineBuf[len] = '\0';
|
|
char* line = meck_imp_trim(lineBuf);
|
|
|
|
// --- Bracket / section transitions ---
|
|
|
|
// Closing bracket: pop section
|
|
if (line[0] == '}' || (line[0] == '}' && line[1] == ',')) {
|
|
if (section == IMP_CHANNEL_OBJ) {
|
|
// End of channel object -- try to add
|
|
if (ch_gotName && ch_gotSecret) {
|
|
// Check for existing channel with same name
|
|
bool exists = false;
|
|
ChannelDetails existing;
|
|
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
|
if (mesh.getChannel(i, existing) && existing.name[0] != '\0') {
|
|
if (strcmp(existing.name, ch_name) == 0) {
|
|
exists = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (exists) {
|
|
channelsSkipped++;
|
|
} else {
|
|
// Find first empty slot
|
|
bool added = false;
|
|
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
|
ChannelDetails slot;
|
|
if (!mesh.getChannel(i, slot) || slot.name[0] == '\0') {
|
|
ChannelDetails newCh;
|
|
memset(&newCh, 0, sizeof(newCh));
|
|
strncpy(newCh.name, ch_name, sizeof(newCh.name));
|
|
newCh.name[31] = '\0';
|
|
memcpy(newCh.channel.secret, ch_secret, PUB_KEY_SIZE);
|
|
if (mesh.setChannel(i, newCh)) {
|
|
channelsAdded++;
|
|
added = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (!added) {
|
|
Serial.println("Config Import: no empty channel slots");
|
|
}
|
|
}
|
|
}
|
|
ch_gotName = ch_gotSecret = false;
|
|
section = IMP_CHANNELS_ARRAY;
|
|
continue;
|
|
}
|
|
if (section == IMP_CONTACT_OBJ) {
|
|
// End of contact object -- try to add
|
|
if (ct_gotPubkey && ct_gotType) {
|
|
ct.id = mesh::Identity(ct_pubkey);
|
|
if (mesh.lookupContactByPubKey(ct_pubkey, PUB_KEY_SIZE) != NULL) {
|
|
contactsSkipped++;
|
|
} else if (mesh.addContact(ct)) {
|
|
contactsAdded++;
|
|
contactsChanged = true;
|
|
} else {
|
|
Serial.printf("Config Import: contact table full after %d added\n", contactsAdded);
|
|
}
|
|
}
|
|
ct_gotPubkey = ct_gotType = false;
|
|
section = IMP_CONTACTS_ARRAY;
|
|
continue;
|
|
}
|
|
if (section == IMP_RADIO || section == IMP_POSITION ||
|
|
section == IMP_OTHER || section == IMP_AUTOADD) {
|
|
section = IMP_ROOT;
|
|
continue;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Closing array bracket
|
|
if (line[0] == ']' || (line[0] == ']' && line[1] == ',')) {
|
|
if (section == IMP_CHANNELS_ARRAY || section == IMP_CONTACTS_ARRAY) {
|
|
section = IMP_ROOT;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Opening brace within an array: start new object
|
|
if (line[0] == '{' && section == IMP_CHANNELS_ARRAY) {
|
|
memset(ch_name, 0, sizeof(ch_name));
|
|
memset(ch_secret, 0, sizeof(ch_secret));
|
|
ch_gotName = ch_gotSecret = false;
|
|
section = IMP_CHANNEL_OBJ;
|
|
continue;
|
|
}
|
|
if (line[0] == '{' && section == IMP_CONTACTS_ARRAY) {
|
|
memset(&ct, 0, sizeof(ct));
|
|
ct_gotPubkey = ct_gotType = false;
|
|
ct.out_path_len = OUT_PATH_UNKNOWN;
|
|
ct.shared_secret_valid = false;
|
|
section = IMP_CONTACT_OBJ;
|
|
continue;
|
|
}
|
|
|
|
// --- Key-value parsing ---
|
|
char* val = meck_imp_parse_kv(line, key, sizeof(key));
|
|
if (!val) continue;
|
|
|
|
// Check for section-opening keys at root level
|
|
if (section == IMP_ROOT) {
|
|
if (strcmp(key, "name") == 0) {
|
|
char importName[32];
|
|
meck_imp_extract_string(importName, sizeof(importName), val);
|
|
strncpy(prefs->node_name, importName, sizeof(prefs->node_name));
|
|
prefs->node_name[31] = '\0';
|
|
prefsChanged = true;
|
|
Serial.printf("Config Import: name = %s\n", prefs->node_name);
|
|
}
|
|
else if (strcmp(key, "public_key") == 0) {
|
|
char hex[PUB_KEY_SIZE * 2 + 2];
|
|
meck_imp_extract_string(hex, sizeof(hex), val);
|
|
if (mesh::Utils::fromHex(imp_pub, PUB_KEY_SIZE, hex)) {
|
|
gotPub = true;
|
|
}
|
|
}
|
|
else if (strcmp(key, "private_key") == 0) {
|
|
char hex[PRV_KEY_SIZE * 2 + 2];
|
|
meck_imp_extract_string(hex, sizeof(hex), val);
|
|
if (mesh::Utils::fromHex(imp_prv, PRV_KEY_SIZE, hex)) {
|
|
gotPrv = true;
|
|
}
|
|
}
|
|
else if (strcmp(key, "radio_settings") == 0) {
|
|
// Value should contain '{' — section opens
|
|
if (strchr(val, '{')) section = IMP_RADIO;
|
|
}
|
|
else if (strcmp(key, "position_settings") == 0) {
|
|
if (strchr(val, '{')) section = IMP_POSITION;
|
|
}
|
|
else if (strcmp(key, "other_settings") == 0) {
|
|
if (strchr(val, '{')) section = IMP_OTHER;
|
|
}
|
|
else if (strcmp(key, "auto_add_settings") == 0) {
|
|
if (strchr(val, '{')) section = IMP_AUTOADD;
|
|
}
|
|
else if (strcmp(key, "channels") == 0) {
|
|
if (strchr(val, '[')) section = IMP_CHANNELS_ARRAY;
|
|
}
|
|
else if (strcmp(key, "contacts") == 0) {
|
|
if (strchr(val, '[')) section = IMP_CONTACTS_ARRAY;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// --- Radio settings ---
|
|
if (section == IMP_RADIO) {
|
|
if (strcmp(key, "frequency") == 0) {
|
|
double freq = atof(val);
|
|
// Auto-detect units: >3000 = kHz, <=3000 = MHz
|
|
if (freq > 3000.0) freq /= 1000.0;
|
|
prefs->freq = (float)freq;
|
|
prefsChanged = true;
|
|
}
|
|
else if (strcmp(key, "bandwidth") == 0) {
|
|
double bw = atof(val);
|
|
// Auto-detect units: >1000 = Hz, <=1000 = kHz
|
|
if (bw > 1000.0) bw /= 1000.0;
|
|
prefs->bw = (float)bw;
|
|
prefsChanged = true;
|
|
}
|
|
else if (strcmp(key, "spreading_factor") == 0) {
|
|
prefs->sf = (uint8_t)atoi(val);
|
|
prefsChanged = true;
|
|
}
|
|
else if (strcmp(key, "coding_rate") == 0) {
|
|
prefs->cr = (uint8_t)atoi(val);
|
|
prefsChanged = true;
|
|
}
|
|
else if (strcmp(key, "tx_power") == 0) {
|
|
prefs->tx_power_dbm = (uint8_t)atoi(val);
|
|
prefsChanged = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// --- Position settings ---
|
|
if (section == IMP_POSITION) {
|
|
if (strcmp(key, "latitude") == 0) {
|
|
char str[16];
|
|
meck_imp_extract_string(str, sizeof(str), val);
|
|
node_lat = atof(str);
|
|
prefsChanged = true;
|
|
}
|
|
else if (strcmp(key, "longitude") == 0) {
|
|
char str[16];
|
|
meck_imp_extract_string(str, sizeof(str), val);
|
|
node_lon = atof(str);
|
|
prefsChanged = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// --- Other settings ---
|
|
if (section == IMP_OTHER) {
|
|
if (strcmp(key, "manual_add_contacts") == 0) {
|
|
prefs->manual_add_contacts = (uint8_t)atoi(val);
|
|
prefsChanged = true;
|
|
}
|
|
else if (strcmp(key, "advert_location_policy") == 0) {
|
|
prefs->advert_loc_policy = (uint8_t)atoi(val);
|
|
prefsChanged = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// --- Auto-add settings ---
|
|
if (section == IMP_AUTOADD) {
|
|
if (strcmp(key, "auto_add_chat") == 0) {
|
|
if (strstr(val, "true")) prefs->autoadd_config |= AUTO_ADD_CHAT;
|
|
else prefs->autoadd_config &= ~AUTO_ADD_CHAT;
|
|
prefsChanged = true;
|
|
}
|
|
else if (strcmp(key, "auto_add_repeater") == 0) {
|
|
if (strstr(val, "true")) prefs->autoadd_config |= AUTO_ADD_REPEATER;
|
|
else prefs->autoadd_config &= ~AUTO_ADD_REPEATER;
|
|
prefsChanged = true;
|
|
}
|
|
else if (strcmp(key, "auto_add_room_server") == 0) {
|
|
if (strstr(val, "true")) prefs->autoadd_config |= AUTO_ADD_ROOM_SERVER;
|
|
else prefs->autoadd_config &= ~AUTO_ADD_ROOM_SERVER;
|
|
prefsChanged = true;
|
|
}
|
|
else if (strcmp(key, "auto_add_sensor") == 0) {
|
|
if (strstr(val, "true")) prefs->autoadd_config |= AUTO_ADD_SENSOR;
|
|
else prefs->autoadd_config &= ~AUTO_ADD_SENSOR;
|
|
prefsChanged = true;
|
|
}
|
|
else if (strcmp(key, "overwrite_oldest") == 0) {
|
|
if (strstr(val, "true")) prefs->autoadd_config |= AUTO_ADD_OVERWRITE_OLDEST;
|
|
else prefs->autoadd_config &= ~AUTO_ADD_OVERWRITE_OLDEST;
|
|
prefsChanged = true;
|
|
}
|
|
else if (strcmp(key, "auto_add_max_hops") == 0) {
|
|
if (strstr(val, "null")) {
|
|
prefs->autoadd_max_hops = 0;
|
|
} else {
|
|
prefs->autoadd_max_hops = (uint8_t)atoi(val);
|
|
}
|
|
prefsChanged = true;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// --- Channel object ---
|
|
if (section == IMP_CHANNEL_OBJ) {
|
|
if (strcmp(key, "name") == 0) {
|
|
meck_imp_extract_string(ch_name, sizeof(ch_name), val);
|
|
ch_gotName = true;
|
|
}
|
|
else if (strcmp(key, "secret") == 0) {
|
|
char hex[PUB_KEY_SIZE * 2 + 2];
|
|
meck_imp_extract_string(hex, sizeof(hex), val);
|
|
// Import handles both 16-byte (32 hex) and 32-byte (64 hex) secrets
|
|
int hexLen = strlen(hex);
|
|
if (hexLen >= CIPHER_KEY_SIZE * 2) {
|
|
memset(ch_secret, 0, sizeof(ch_secret));
|
|
mesh::Utils::fromHex(ch_secret, hexLen / 2, hex);
|
|
ch_gotSecret = true;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// --- Contact object ---
|
|
if (section == IMP_CONTACT_OBJ) {
|
|
if (strcmp(key, "type") == 0) {
|
|
ct.type = (uint8_t)atoi(val);
|
|
ct_gotType = true;
|
|
}
|
|
else if (strcmp(key, "name") == 0) {
|
|
meck_imp_extract_string(ct.name, sizeof(ct.name), val);
|
|
}
|
|
else if (strcmp(key, "public_key") == 0) {
|
|
char hex[PUB_KEY_SIZE * 2 + 2];
|
|
meck_imp_extract_string(hex, sizeof(hex), val);
|
|
if (mesh::Utils::fromHex(ct_pubkey, PUB_KEY_SIZE, hex)) {
|
|
ct_gotPubkey = true;
|
|
}
|
|
}
|
|
else if (strcmp(key, "flags") == 0) {
|
|
ct.flags = (uint8_t)atoi(val);
|
|
}
|
|
else if (strcmp(key, "latitude") == 0) {
|
|
char str[16];
|
|
meck_imp_extract_string(str, sizeof(str), val);
|
|
ct.gps_lat = (int32_t)(atof(str) * 1000000.0);
|
|
}
|
|
else if (strcmp(key, "longitude") == 0) {
|
|
char str[16];
|
|
meck_imp_extract_string(str, sizeof(str), val);
|
|
ct.gps_lon = (int32_t)(atof(str) * 1000000.0);
|
|
}
|
|
else if (strcmp(key, "last_advert") == 0) {
|
|
ct.last_advert_timestamp = (uint32_t)strtoul(val, NULL, 10);
|
|
}
|
|
else if (strcmp(key, "last_modified") == 0) {
|
|
ct.lastmod = (uint32_t)strtoul(val, NULL, 10);
|
|
}
|
|
// custom_name, out_path_list -- ignored
|
|
continue;
|
|
}
|
|
|
|
} // end while
|
|
|
|
f.close();
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
|
|
// --- Apply identity ---
|
|
if (gotPub && gotPrv) {
|
|
// Reconstruct LocalIdentity: readFrom expects prv_key[64] + pub_key[32]
|
|
uint8_t idBuf[PRV_KEY_SIZE + PUB_KEY_SIZE];
|
|
memcpy(idBuf, imp_prv, PRV_KEY_SIZE);
|
|
memcpy(&idBuf[PRV_KEY_SIZE], imp_pub, PUB_KEY_SIZE);
|
|
mesh.self_id.readFrom(idBuf, sizeof(idBuf));
|
|
mesh.saveMainIdentity();
|
|
identityChanged = true;
|
|
Serial.println("Config Import: identity replaced");
|
|
}
|
|
|
|
// --- Save prefs ---
|
|
if (prefsChanged) {
|
|
mesh.savePrefs();
|
|
Serial.println("Config Import: prefs saved");
|
|
}
|
|
|
|
// --- Save channels ---
|
|
if (channelsAdded > 0) {
|
|
mesh.saveChannels();
|
|
}
|
|
Serial.printf("Config Import: channels %d added, %d skipped\n",
|
|
channelsAdded, channelsSkipped);
|
|
|
|
// --- Save contacts ---
|
|
if (contactsChanged) {
|
|
mesh.saveContacts();
|
|
}
|
|
Serial.printf("Config Import: contacts %d added, %d skipped, %d total\n",
|
|
contactsAdded, contactsSkipped, (int)mesh.getNumContacts());
|
|
|
|
// --- Rename import file ---
|
|
if (SD.exists(donePath)) SD.remove(donePath);
|
|
SD.rename(importPath, donePath);
|
|
Serial.printf("Config Import: renamed to %s\n", donePath);
|
|
|
|
// If identity changed, reboot to apply cleanly
|
|
if (identityChanged) {
|
|
Serial.println("Config Import: identity changed -- rebooting in 2s...");
|
|
delay(2000);
|
|
ESP.restart();
|
|
// Does not return
|
|
}
|
|
|
|
return 1;
|
|
} |