mirror of
https://github.com/pelgraine/Meck.git
synced 2026-06-16 08:01:02 +02:00
264 lines
9.4 KiB
C
264 lines
9.4 KiB
C
#pragma once
|
|
// ---------------------------------------------------------------------------
|
|
// MeckExport.h -- Full config export to MeshCore-app-compatible JSON on SD.
|
|
//
|
|
// Writes a timestamped JSON file to /meshcore/ containing any combination of:
|
|
// - Identity (public + private key)
|
|
// - Radio/device settings (frequency, BW, SF, CR, TX power, position, auto-add)
|
|
// - Channels (name + 16-byte secret)
|
|
// - Contacts (same format as existing exportContactsJSON)
|
|
//
|
|
// Sections are selected via a bitmask of MECK_EXPORT_* flags.
|
|
// The output format is compatible with the MeshCore companion app config export.
|
|
//
|
|
// Usage from main.cpp:
|
|
// int result = meckExportConfig(the_mesh, MECK_EXPORT_ALL,
|
|
// sensors.node_lat, sensors.node_lon,
|
|
// rtc_clock, sdCardReady);
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#include <SD.h>
|
|
#include <helpers/ContactInfo.h>
|
|
#include <helpers/ChannelDetails.h>
|
|
|
|
#define MECK_EXPORT_IDENTITY 0x01
|
|
#define MECK_EXPORT_CHANNELS 0x02
|
|
#define MECK_EXPORT_CONTACTS 0x04
|
|
#define MECK_EXPORT_RADIO 0x08
|
|
#define MECK_EXPORT_AUTOADD 0x10
|
|
#define MECK_EXPORT_ALL 0x1F
|
|
|
|
// Fallback defines (primary definitions live in SettingsScreen.h / ChannelScreen.h)
|
|
#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
|
|
|
|
// JSON-escape a string in-place into dest (handles backslash and double-quote).
|
|
// Returns length of escaped string.
|
|
static int meck_json_escape(char* dest, int destSize, const char* src) {
|
|
int wi = 0;
|
|
for (int ri = 0; src[ri] && wi < destSize - 2; ri++) {
|
|
if (src[ri] == '"' || src[ri] == '\\') dest[wi++] = '\\';
|
|
dest[wi++] = src[ri];
|
|
}
|
|
dest[wi] = '\0';
|
|
return wi;
|
|
}
|
|
|
|
// Export device config to a timestamped JSON file on SD card.
|
|
// flags: bitmask of MECK_EXPORT_* sections to include
|
|
// node_lat: device latitude (double, from sensors.node_lat)
|
|
// node_lon: device longitude (double, from sensors.node_lon)
|
|
// clock: RTC clock for timestamp
|
|
// sdReady: whether SD card is mounted
|
|
// outPath: if non-NULL, receives the output filepath (up to outPathSize chars)
|
|
//
|
|
// Returns number of contacts written (0 if contacts not selected), or -1 on error.
|
|
static int meckExportConfig(MyMesh& mesh, uint8_t flags,
|
|
double node_lat, double node_lon,
|
|
mesh::RTCClock& clock, bool sdReady,
|
|
char* outPath = nullptr, int outPathSize = 0) {
|
|
if (!sdReady) {
|
|
Serial.println("Config Export: SD card not ready");
|
|
return -1;
|
|
}
|
|
|
|
if (!SD.exists("/meshcore")) SD.mkdir("/meshcore");
|
|
|
|
// Build timestamped filename
|
|
char jsonPath[64];
|
|
NodePrefs* prefs = mesh.getNodePrefs();
|
|
uint32_t epoch = clock.getCurrentTime();
|
|
int8_t utcOff = prefs->utc_offset_hours;
|
|
time_t localEpoch = (time_t)epoch + (utcOff * 3600);
|
|
struct tm tmBuf;
|
|
gmtime_r(&localEpoch, &tmBuf);
|
|
snprintf(jsonPath, sizeof(jsonPath),
|
|
"/meshcore/meshcore_config_%04d%02d%02d_%02d%02d.json",
|
|
tmBuf.tm_year + 1900, tmBuf.tm_mon + 1, tmBuf.tm_mday,
|
|
tmBuf.tm_hour, tmBuf.tm_min);
|
|
|
|
// Copy filepath to caller if requested
|
|
if (outPath && outPathSize > 0) {
|
|
strncpy(outPath, jsonPath, outPathSize - 1);
|
|
outPath[outPathSize - 1] = '\0';
|
|
}
|
|
|
|
File f = SD.open(jsonPath, "w", true);
|
|
if (!f) {
|
|
Serial.printf("Config Export: failed to open %s\n", jsonPath);
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
return -1;
|
|
}
|
|
|
|
// Track whether we need a comma before the next top-level key
|
|
bool needComma = false;
|
|
|
|
f.print("{\n");
|
|
|
|
// --- Name (always emitted) ---
|
|
{
|
|
char safeName[80];
|
|
meck_json_escape(safeName, sizeof(safeName), prefs->node_name);
|
|
f.printf(" \"name\": \"%s\"", safeName);
|
|
needComma = true;
|
|
}
|
|
|
|
// --- Identity ---
|
|
if (flags & MECK_EXPORT_IDENTITY) {
|
|
// pub_key is public on Identity base class
|
|
char pubHex[PUB_KEY_SIZE * 2 + 1];
|
|
mesh::Utils::toHex(pubHex, mesh.self_id.pub_key, PUB_KEY_SIZE);
|
|
|
|
// prv_key is private -- extract via writeTo(buffer)
|
|
// writeTo writes: prv_key[64] then pub_key[32] = 96 bytes
|
|
uint8_t idBuf[PRV_KEY_SIZE + PUB_KEY_SIZE];
|
|
size_t idLen = mesh.self_id.writeTo(idBuf, sizeof(idBuf));
|
|
|
|
char prvHex[PRV_KEY_SIZE * 2 + 1];
|
|
prvHex[0] = '\0';
|
|
if (idLen >= PRV_KEY_SIZE) {
|
|
mesh::Utils::toHex(prvHex, idBuf, PRV_KEY_SIZE);
|
|
}
|
|
|
|
if (needComma) f.print(",\n"); needComma = true;
|
|
f.printf(" \"public_key\": \"%s\",\n", pubHex);
|
|
f.printf(" \"private_key\": \"%s\"", prvHex);
|
|
}
|
|
|
|
// --- Radio / device settings ---
|
|
if (flags & MECK_EXPORT_RADIO) {
|
|
// Radio settings -- convert to MeshCore app units (freq kHz, BW Hz)
|
|
if (needComma) f.print(",\n"); needComma = true;
|
|
f.print(" \"radio_settings\": {\n");
|
|
f.printf(" \"frequency\": %lu,\n", (unsigned long)(prefs->freq * 1000.0f + 0.5f));
|
|
f.printf(" \"bandwidth\": %lu,\n", (unsigned long)(prefs->bw * 1000.0f + 0.5f));
|
|
f.printf(" \"spreading_factor\": %d,\n", prefs->sf);
|
|
f.printf(" \"coding_rate\": %d,\n", prefs->cr);
|
|
f.printf(" \"tx_power\": %d\n", prefs->tx_power_dbm);
|
|
f.print(" }");
|
|
|
|
// Position settings
|
|
f.print(",\n \"position_settings\": {\n");
|
|
char latStr[16], lonStr[16];
|
|
snprintf(latStr, sizeof(latStr), "%.6f", node_lat);
|
|
snprintf(lonStr, sizeof(lonStr), "%.6f", node_lon);
|
|
f.printf(" \"latitude\": \"%s\",\n", latStr);
|
|
f.printf(" \"longitude\": \"%s\"\n", lonStr);
|
|
f.print(" }");
|
|
}
|
|
|
|
// --- Contact auto-add preferences ---
|
|
if (flags & MECK_EXPORT_AUTOADD) {
|
|
if (needComma) f.print(",\n"); needComma = true;
|
|
f.print(" \"other_settings\": {\n");
|
|
f.printf(" \"manual_add_contacts\": %d,\n", prefs->manual_add_contacts);
|
|
f.printf(" \"advert_location_policy\": %d\n", prefs->advert_loc_policy);
|
|
f.print(" }");
|
|
|
|
f.print(",\n \"auto_add_settings\": {\n");
|
|
f.printf(" \"auto_add_chat\": %s,\n",
|
|
(prefs->autoadd_config & AUTO_ADD_CHAT) ? "true" : "false");
|
|
f.printf(" \"auto_add_repeater\": %s,\n",
|
|
(prefs->autoadd_config & AUTO_ADD_REPEATER) ? "true" : "false");
|
|
f.printf(" \"auto_add_room_server\": %s,\n",
|
|
(prefs->autoadd_config & AUTO_ADD_ROOM_SERVER) ? "true" : "false");
|
|
f.printf(" \"auto_add_sensor\": %s,\n",
|
|
(prefs->autoadd_config & AUTO_ADD_SENSOR) ? "true" : "false");
|
|
f.printf(" \"overwrite_oldest\": %s,\n",
|
|
(prefs->autoadd_config & AUTO_ADD_OVERWRITE_OLDEST) ? "true" : "false");
|
|
if (prefs->autoadd_max_hops == 0) {
|
|
f.print(" \"auto_add_max_hops\": null\n");
|
|
} else {
|
|
f.printf(" \"auto_add_max_hops\": %d\n", prefs->autoadd_max_hops);
|
|
}
|
|
f.print(" }");
|
|
}
|
|
|
|
// --- Channels ---
|
|
if (flags & MECK_EXPORT_CHANNELS) {
|
|
if (needComma) f.print(",\n"); needComma = true;
|
|
f.print(" \"channels\": [\n");
|
|
|
|
int chWritten = 0;
|
|
ChannelDetails ch;
|
|
for (uint8_t i = 0; i < MAX_GROUP_CHANNELS; i++) {
|
|
if (!mesh.getChannel(i, ch) || ch.name[0] == '\0') continue;
|
|
|
|
if (chWritten > 0) f.print(",\n");
|
|
|
|
char safeName[80];
|
|
meck_json_escape(safeName, sizeof(safeName), ch.name);
|
|
|
|
// Export first CIPHER_KEY_SIZE (16) bytes of secret as hex
|
|
char secHex[CIPHER_KEY_SIZE * 2 + 1];
|
|
mesh::Utils::toHex(secHex, ch.channel.secret, CIPHER_KEY_SIZE);
|
|
|
|
f.print(" {\n");
|
|
f.printf(" \"name\": \"%s\",\n", safeName);
|
|
f.printf(" \"secret\": \"%s\"\n", secHex);
|
|
f.print(" }");
|
|
chWritten++;
|
|
}
|
|
|
|
f.print("\n ]");
|
|
Serial.printf("Config Export: %d channels\n", chWritten);
|
|
}
|
|
|
|
// --- Contacts ---
|
|
int contactsWritten = 0;
|
|
if (flags & MECK_EXPORT_CONTACTS) {
|
|
if (needComma) f.print(",\n"); needComma = true;
|
|
f.print(" \"contacts\": [\n");
|
|
|
|
uint32_t total = mesh.getNumContacts();
|
|
for (uint32_t i = 0; i < total; i++) {
|
|
ContactInfo c;
|
|
if (!mesh.getContactByIdx(i, c)) continue;
|
|
|
|
if (contactsWritten > 0) f.print(",\n");
|
|
|
|
char hexKey[PUB_KEY_SIZE * 2 + 1];
|
|
mesh::Utils::toHex(hexKey, c.id.pub_key, PUB_KEY_SIZE);
|
|
|
|
char latStr[16], lonStr[16];
|
|
snprintf(latStr, sizeof(latStr), "%.6f", (double)c.gps_lat / 1000000.0);
|
|
snprintf(lonStr, sizeof(lonStr), "%.6f", (double)c.gps_lon / 1000000.0);
|
|
|
|
char safeName[80];
|
|
meck_json_escape(safeName, sizeof(safeName), c.name);
|
|
|
|
f.print(" {\n");
|
|
f.printf(" \"type\": %d,\n", c.type);
|
|
f.printf(" \"name\": \"%s\",\n", safeName);
|
|
f.printf(" \"custom_name\": null,\n");
|
|
f.printf(" \"public_key\": \"%s\",\n", hexKey);
|
|
f.printf(" \"flags\": %d,\n", c.flags);
|
|
f.printf(" \"latitude\": \"%s\",\n", latStr);
|
|
f.printf(" \"longitude\": \"%s\",\n", lonStr);
|
|
f.printf(" \"last_advert\": %lu,\n", (unsigned long)c.last_advert_timestamp);
|
|
f.printf(" \"last_modified\": %lu,\n", (unsigned long)c.lastmod);
|
|
f.printf(" \"out_path_list\": null\n");
|
|
f.print(" }");
|
|
|
|
contactsWritten++;
|
|
}
|
|
|
|
f.print("\n ]");
|
|
}
|
|
|
|
f.print("\n}\n");
|
|
f.close();
|
|
digitalWrite(SDCARD_CS, HIGH);
|
|
|
|
Serial.printf("Config Export: wrote %s (flags=0x%02X, %d contacts)\n",
|
|
jsonPath, flags, contactsWritten);
|
|
return contactsWritten;
|
|
} |