add 100 and :D emoji; update firmware version to 1.11;; config json import and export on device to SD now supported

This commit is contained in:
pelgraine
2026-05-22 10:08:24 +10:00
parent 4aae5c9352
commit 47a7f2f9d1
8 changed files with 1211 additions and 159 deletions
+264
View File
@@ -0,0 +1,264 @@
#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;
}
+522
View File
@@ -0,0 +1,522 @@
#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;
}
+3
View File
@@ -253,6 +253,9 @@ public:
void saveContacts() {
_store->saveContacts(this);
}
void saveMainIdentity() {
_store->saveMainIdentity(self_id);
}
private:
void writeOKFrame();
+55
View File
@@ -23,6 +23,8 @@
#include "ContactsScreen.h"
#include "ChannelScreen.h"
#include "ChannelPickerScreen.h"
#include "MeckExport.h"
#include "MeckImport.h"
#include "SettingsScreen.h"
#include "RepeaterAdminScreen.h"
#include "DiscoveryScreen.h"
@@ -699,6 +701,8 @@
#include "ContactsScreen.h"
#include "ChannelScreen.h"
#include "ChannelPickerScreen.h"
#include "MeckExport.h"
#include "MeckImport.h"
#include "SettingsScreen.h"
#include "RepeaterAdminScreen.h"
#include "DiscoveryScreen.h"
@@ -2136,6 +2140,20 @@ void setup() {
);
MESH_DEBUG_PRINTLN("setup() - the_mesh.begin() done");
// Boot-time config import: check for /meshcore/import.json on SD
#ifdef HAS_SDCARD
if (sdCardReady) {
int importResult = meckImportConfig(the_mesh,
sensors.node_lat, sensors.node_lon,
sdCardReady);
if (importResult > 0) {
MESH_DEBUG_PRINTLN("setup() - config imported from SD");
}
// importResult == 0 means no file found (normal), -1 means error (logged inside)
// If identity was changed, meckImportConfig already rebooted before returning.
}
#endif
#ifdef WIFI_SSID
MESH_DEBUG_PRINTLN("setup() - WiFi mode (compile-time credentials)");
WiFi.begin(WIFI_SSID, WIFI_PWD);
@@ -4536,6 +4554,43 @@ void handleKeyboardInput() {
// All other keys → settings screen via injectKey
ui_task.injectKey(key);
// Check for export/import requests from the settings screen
#ifdef HAS_SDCARD
if (settings->isExportRequested()) {
settings->clearExportRequest();
uint8_t flags = settings->getExportFlags();
if (flags == 0) {
ui_task.showAlert("No sections selected", 1500);
} else {
char exportedPath[64];
int result = meckExportConfig(the_mesh, flags,
sensors.node_lat, sensors.node_lon,
rtc_clock, sdCardReady,
exportedPath, sizeof(exportedPath));
if (result >= 0) {
char buf[96];
snprintf(buf, sizeof(buf), "Exported to %s", exportedPath);
ui_task.showAlert(buf, 3500);
} else {
ui_task.showAlert("Export failed (SD?)", 2000);
}
}
}
if (settings->isImportRequested()) {
settings->clearImportRequest();
int added = meckImportConfig(the_mesh,
sensors.node_lat, sensors.node_lon,
sdCardReady);
if (added > 0) {
ui_task.showAlert("Config imported!", 2500);
} else if (added == 0) {
ui_task.showAlert("No import.json found", 2000);
} else {
ui_task.showAlert("Import failed", 2000);
}
}
#endif
return;
}
+96 -78
View File
@@ -3,7 +3,7 @@
// Emoji sprites for e-ink display - dual size
// Large (12x12) for compose/picker, Small (10x10) for channel view
// MSB-first, 2 bytes per row
// 77 total emoji: joy/thumbsup/frown first, then 43 original, then 19 new, then 11 newest, then 1 latest
// 79 total emoji: joy/thumbsup/frown first, then 43 original, then 19 new, then 11 newest, then 1 latest, then 2 added (hundred_points, grinning)
#include <stdint.h>
#ifdef ESP32
@@ -15,11 +15,11 @@
#define EMOJI_SM_W 10
#define EMOJI_SM_H 10
#define EMOJI_COUNT 77
#define EMOJI_COUNT 79
// Escape codes in 0x80+ range - safe from keyboard ASCII (32-126)
#define EMOJI_ESCAPE_START 0x80
#define EMOJI_ESCAPE_END 0xCC // 0x80 + 76
#define EMOJI_ESCAPE_END 0xCE // 0x80 + 78
#define EMOJI_PAD_BYTE 0x7F // DEL, not typeable (key < 127 guard)
// ======== LARGE 12x12 SPRITES ========
@@ -160,6 +160,14 @@ static const uint8_t emoji_lg_lizard[] PROGMEM = {
static const uint8_t emoji_lg_zany_face[] PROGMEM = {
0x1F,0x80, 0x20,0x40, 0x59,0x20, 0x58,0xA0, 0x40,0x20, 0x40,0x20, 0x4F,0x20, 0x50,0xA0, 0x20,0x40, 0x1F,0x80, 0x00,0x00, 0x00,0x00,
};
// [NEW] hundred_points 💯
static const uint8_t emoji_lg_hundred_points[] PROGMEM = {
0x00,0x00, 0x5D,0xC0, 0xD5,0x40, 0x55,0x40, 0x55,0x40, 0x5D,0xC0, 0x00,0x00, 0x00,0x00, 0xFF,0xE0, 0x00,0x00, 0xFF,0xE0, 0x00,0x00,
};
// [NEW] grinning 😁
static const uint8_t emoji_lg_grinning[] PROGMEM = {
0x1F,0x80, 0x20,0x40, 0x48,0xA0, 0x55,0x60, 0x80,0x10, 0x9F,0x90, 0x90,0x90, 0x4F,0x20, 0x20,0x40, 0x1F,0x80, 0x00,0x00, 0x00,0x00,
};
// [32] kangaroo
static const uint8_t emoji_lg_kangaroo[] PROGMEM = {
0x0E,0x00, 0x1F,0x00, 0x1F,0x00, 0x0E,0x00, 0x0F,0x00, 0x07,0x80, 0x47,0x80, 0x65,0x80, 0x3C,0x80, 0x18,0x80, 0x10,0xC0, 0x18,0xF0,
@@ -337,7 +345,8 @@ static const uint8_t emoji_lg_beer[] PROGMEM = {
static const uint8_t* const EMOJI_SPRITES_LG[] PROGMEM = {
// Faces/emotion first
emoji_lg_joy, emoji_lg_frown, emoji_lg_loudly_crying,
emoji_lg_grimace, emoji_lg_zany_face, emoji_lg_cowboy,
emoji_lg_grimace, emoji_lg_zany_face, emoji_lg_hundred_points,
emoji_lg_grinning, emoji_lg_cowboy,
// Thumbsup + heart
emoji_lg_thumbsup, emoji_lg_heart,
// Everything else in original relative order
@@ -459,6 +468,12 @@ static const uint8_t emoji_sm_lizard[] PROGMEM = {
static const uint8_t emoji_sm_zany_face[] PROGMEM = {
0x3F,0x00, 0x60,0x80, 0x72,0x80, 0x40,0x80, 0x40,0x80, 0x5E,0x80, 0x61,0x80, 0x3F,0x00, 0x00,0x00, 0x00,0x00,
};
static const uint8_t emoji_sm_hundred_points[] PROGMEM = {
0xDD,0xC0, 0x55,0x40, 0x55,0x40, 0x5D,0xC0, 0x00,0x00, 0x00,0x00, 0xFF,0x80, 0x00,0x00, 0xFF,0x80, 0x00,0x00,
};
static const uint8_t emoji_sm_grinning[] PROGMEM = {
0x3F,0x00, 0x61,0x80, 0xF3,0xC0, 0x80,0x40, 0xBF,0x40, 0x9E,0x40, 0x40,0x80, 0x3F,0x00, 0x00,0x00, 0x00,0x00,
};
static const uint8_t emoji_sm_kangaroo[] PROGMEM = {
0x1C,0x00, 0x3E,0x00, 0x1C,0x00, 0x1E,0x00, 0x0F,0x00, 0x4F,0x00, 0x6B,0x00, 0x39,0x00, 0x31,0x00, 0x31,0xC0,
};
@@ -629,7 +644,8 @@ static const uint8_t emoji_sm_beer[] PROGMEM = {
static const uint8_t* const EMOJI_SPRITES_SM[] PROGMEM = {
// Faces/emotion first
emoji_sm_joy, emoji_sm_frown, emoji_sm_loudly_crying,
emoji_sm_grimace, emoji_sm_zany_face, emoji_sm_cowboy,
emoji_sm_grimace, emoji_sm_zany_face, emoji_sm_hundred_points,
emoji_sm_grinning, emoji_sm_cowboy,
// Thumbsup + heart
emoji_sm_thumbsup, emoji_sm_heart,
// Everything else in original relative order
@@ -663,80 +679,82 @@ static const EmojiCodepoint EMOJI_CODEPOINTS[EMOJI_COUNT] = {
{ 0x1F62D, 0x0000, 0x82 }, // loudly_crying
{ 0x1F62C, 0x0000, 0x83 }, // grimace
{ 0x1F92A, 0x0000, 0x84 }, // zany_face
{ 0x1F920, 0x0000, 0x85 }, // cowboy
{ 0x1F4AF, 0x0000, 0x85 }, // hundred_points
{ 0x1F601, 0x0000, 0x86 }, // grinning
{ 0x1F920, 0x0000, 0x87 }, // cowboy
// Thumbsup + heart
{ 0x1F44D, 0x0000, 0x86 }, // thumbsup
{ 0x2665, 0x0000, 0x87 }, // heart
{ 0x1F44D, 0x0000, 0x88 }, // thumbsup
{ 0x2665, 0x0000, 0x89 }, // heart
// Everything else in original relative order
{ 0x1F6DC, 0x0000, 0x88 }, // wireless
{ 0x267E, 0x0000, 0x89 }, // infinity
{ 0x1F996, 0x0000, 0x8A }, // trex
{ 0x2620, 0x0000, 0x8B }, // skull
{ 0x271D, 0x0000, 0x8C }, // cross
{ 0x26A1, 0x0000, 0x8D }, // lightning
{ 0x1F3A9, 0x0000, 0x8E }, // tophat
{ 0x1F3CD, 0x0000, 0x8F }, // motorcycle
{ 0x1F331, 0x0000, 0x90 }, // seedling
{ 0x1F1E6, 0x1F1FA, 0x91 }, // flag_au
{ 0x2602, 0x0000, 0x92 }, // umbrella
{ 0x1F9FF, 0x0000, 0x93 }, // nazar
{ 0x1F30F, 0x0000, 0x94 }, // globe
{ 0x2622, 0x0000, 0x95 }, // radioactive
{ 0x1F404, 0x0000, 0x96 }, // cow
{ 0x1F47D, 0x0000, 0x97 }, // alien
{ 0x1F47E, 0x0000, 0x98 }, // invader
{ 0x1F5E1, 0x0000, 0x99 }, // dagger
{ 0x26F0, 0x0000, 0x9A }, // mountain
{ 0x1F51A, 0x0000, 0x9B }, // end_arrow
{ 0x2B55, 0x0000, 0x9C }, // hollow_circle
{ 0x1F409, 0x0000, 0x9D }, // dragon
{ 0x1F310, 0x0000, 0x9E }, // globe_meridians
{ 0x1F346, 0x0000, 0x9F }, // eggplant
{ 0x1F6E1, 0x0000, 0xA0 }, // shield
{ 0x1F97D, 0x0000, 0xA1 }, // goggles
{ 0x1F98E, 0x0000, 0xA2 }, // lizard
{ 0x1F998, 0x0000, 0xA3 }, // kangaroo
{ 0x1FAB6, 0x0000, 0xA4 }, // feather
{ 0x1F506, 0x0000, 0xA5 }, // bright
{ 0x303D, 0x0000, 0xA6 }, // part_alt
{ 0x1F6E5, 0x0000, 0xA7 }, // motorboat
{ 0x1F030, 0x0000, 0xA8 }, // domino
{ 0x1F4E1, 0x0000, 0xA9 }, // satellite
{ 0x1F6C3, 0x0000, 0xAA }, // customs
{ 0x1F6DE, 0x0000, 0xAB }, // wheel
{ 0x1F428, 0x0000, 0xAC }, // koala
{ 0x1F39B, 0x0000, 0xAD }, // control_knobs
{ 0x1F351, 0x0000, 0xAE }, // peach
{ 0x1F3CE, 0x0000, 0xAF }, // racing_car
{ 0x1F42D, 0x0000, 0xB0 }, // mouse
{ 0x1F344, 0x0000, 0xB1 }, // mushroom
{ 0x2623, 0x0000, 0xB2 }, // biohazard
{ 0x1F43C, 0x0000, 0xB3 }, // panda
{ 0x1F4A2, 0x0000, 0xB4 }, // anger
{ 0x1F432, 0x0000, 0xB5 }, // dragon_face
{ 0x1F4DF, 0x0000, 0xB6 }, // pager
{ 0x1F41D, 0x0000, 0xB7 }, // bee
{ 0x1F4A1, 0x0000, 0xB8 }, // bulb
{ 0x1F431, 0x0000, 0xB9 }, // cat
{ 0x269C, 0x0000, 0xBA }, // fleur
{ 0x1F314, 0x0000, 0xBB }, // moon
{ 0x2615, 0x0000, 0xBC }, // coffee
{ 0x1F9B7, 0x0000, 0xBD }, // tooth
{ 0x1F968, 0x0000, 0xBE }, // pretzel
{ 0x1F9EE, 0x0000, 0xBF }, // abacus
{ 0x1F5FF, 0x0000, 0xC0 }, // moai
{ 0x1F481, 0x0000, 0xC1 }, // tipping
{ 0x1F994, 0x0000, 0xC2 }, // hedgehog
{ 0x2666, 0x0000, 0xC3 }, // diamond_suit
{ 0x2660, 0x0000, 0xC4 }, // spade_suit
{ 0x1F355, 0x0000, 0xC5 }, // pizza
{ 0x1F340, 0x0000, 0xC6 }, // four_leaf_clover
{ 0x2601, 0x0000, 0xC7 }, // cloud
{ 0x1F680, 0x0000, 0xC8 }, // rocket
{ 0x1F6C2, 0x0000, 0xC9 }, // passport_control
{ 0x2733, 0x0000, 0xCA }, // eight_spoked_asterisk
{ 0x1F4F6, 0x0000, 0xCB }, // signal_strength
{ 0x1F37A, 0x0000, 0xCC }, // beer
{ 0x1F6DC, 0x0000, 0x8A }, // wireless
{ 0x267E, 0x0000, 0x8B }, // infinity
{ 0x1F996, 0x0000, 0x8C }, // trex
{ 0x2620, 0x0000, 0x8D }, // skull
{ 0x271D, 0x0000, 0x8E }, // cross
{ 0x26A1, 0x0000, 0x8F }, // lightning
{ 0x1F3A9, 0x0000, 0x90 }, // tophat
{ 0x1F3CD, 0x0000, 0x91 }, // motorcycle
{ 0x1F331, 0x0000, 0x92 }, // seedling
{ 0x1F1E6, 0x1F1FA, 0x93 }, // flag_au
{ 0x2602, 0x0000, 0x94 }, // umbrella
{ 0x1F9FF, 0x0000, 0x95 }, // nazar
{ 0x1F30F, 0x0000, 0x96 }, // globe
{ 0x2622, 0x0000, 0x97 }, // radioactive
{ 0x1F404, 0x0000, 0x98 }, // cow
{ 0x1F47D, 0x0000, 0x99 }, // alien
{ 0x1F47E, 0x0000, 0x9A }, // invader
{ 0x1F5E1, 0x0000, 0x9B }, // dagger
{ 0x26F0, 0x0000, 0x9C }, // mountain
{ 0x1F51A, 0x0000, 0x9D }, // end_arrow
{ 0x2B55, 0x0000, 0x9E }, // hollow_circle
{ 0x1F409, 0x0000, 0x9F }, // dragon
{ 0x1F310, 0x0000, 0xA0 }, // globe_meridians
{ 0x1F346, 0x0000, 0xA1 }, // eggplant
{ 0x1F6E1, 0x0000, 0xA2 }, // shield
{ 0x1F97D, 0x0000, 0xA3 }, // goggles
{ 0x1F98E, 0x0000, 0xA4 }, // lizard
{ 0x1F998, 0x0000, 0xA5 }, // kangaroo
{ 0x1FAB6, 0x0000, 0xA6 }, // feather
{ 0x1F506, 0x0000, 0xA7 }, // bright
{ 0x303D, 0x0000, 0xA8 }, // part_alt
{ 0x1F6E5, 0x0000, 0xA9 }, // motorboat
{ 0x1F030, 0x0000, 0xAA }, // domino
{ 0x1F4E1, 0x0000, 0xAB }, // satellite
{ 0x1F6C3, 0x0000, 0xAC }, // customs
{ 0x1F6DE, 0x0000, 0xAD }, // wheel
{ 0x1F428, 0x0000, 0xAE }, // koala
{ 0x1F39B, 0x0000, 0xAF }, // control_knobs
{ 0x1F351, 0x0000, 0xB0 }, // peach
{ 0x1F3CE, 0x0000, 0xB1 }, // racing_car
{ 0x1F42D, 0x0000, 0xB2 }, // mouse
{ 0x1F344, 0x0000, 0xB3 }, // mushroom
{ 0x2623, 0x0000, 0xB4 }, // biohazard
{ 0x1F43C, 0x0000, 0xB5 }, // panda
{ 0x1F4A2, 0x0000, 0xB6 }, // anger
{ 0x1F432, 0x0000, 0xB7 }, // dragon_face
{ 0x1F4DF, 0x0000, 0xB8 }, // pager
{ 0x1F41D, 0x0000, 0xB9 }, // bee
{ 0x1F4A1, 0x0000, 0xBA }, // bulb
{ 0x1F431, 0x0000, 0xBB }, // cat
{ 0x269C, 0x0000, 0xBC }, // fleur
{ 0x1F314, 0x0000, 0xBD }, // moon
{ 0x2615, 0x0000, 0xBE }, // coffee
{ 0x1F9B7, 0x0000, 0xBF }, // tooth
{ 0x1F968, 0x0000, 0xC0 }, // pretzel
{ 0x1F9EE, 0x0000, 0xC1 }, // abacus
{ 0x1F5FF, 0x0000, 0xC2 }, // moai
{ 0x1F481, 0x0000, 0xC3 }, // tipping
{ 0x1F994, 0x0000, 0xC4 }, // hedgehog
{ 0x2666, 0x0000, 0xC5 }, // diamond_suit
{ 0x2660, 0x0000, 0xC6 }, // spade_suit
{ 0x1F355, 0x0000, 0xC7 }, // pizza
{ 0x1F340, 0x0000, 0xC8 }, // four_leaf_clover
{ 0x2601, 0x0000, 0xC9 }, // cloud
{ 0x1F680, 0x0000, 0xCA }, // rocket
{ 0x1F6C2, 0x0000, 0xCB }, // passport_control
{ 0x2733, 0x0000, 0xCC }, // eight_spoked_asterisk
{ 0x1F4F6, 0x0000, 0xCD }, // signal_strength
{ 0x1F37A, 0x0000, 0xCE }, // beer
};
// ---- Helper functions ----
@@ -746,7 +764,7 @@ static const EmojiCodepoint EMOJI_CODEPOINTS[EMOJI_COUNT] = {
struct EmojiAlias { uint32_t cp; uint8_t escape; };
#define EMOJI_ALIAS_COUNT 1
static const EmojiAlias EMOJI_ALIASES[EMOJI_ALIAS_COUNT] = {
{ 0x1F08E, 0xA8 }, // domino tile (MWD node signifier) -> domino sprite
{ 0x1F08E, 0xAA }, // domino tile (MWD node signifier) -> domino sprite
};
static uint32_t emojiDecodeUtf8(const uint8_t* s, int remaining, int* bytes_consumed) {
@@ -46,15 +46,19 @@ extern MyMesh the_mesh;
// ---------------------------------------------------------------------------
// Auto-add config bitmask (mirrored from MyMesh.cpp for UI access)
// ---------------------------------------------------------------------------
#ifndef AUTO_ADD_OVERWRITE_OLDEST
#define AUTO_ADD_OVERWRITE_OLDEST (1 << 0) // 0x01 - overwrite oldest non-favourite when full
#define AUTO_ADD_CHAT (1 << 1) // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
#define AUTO_ADD_REPEATER (1 << 2) // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
#define AUTO_ADD_ROOM_SERVER (1 << 3) // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
#define AUTO_ADD_SENSOR (1 << 4) // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
#endif
// All type bits combined (excludes overwrite flag)
#ifndef AUTO_ADD_ALL_TYPES
#define AUTO_ADD_ALL_TYPES (AUTO_ADD_CHAT | AUTO_ADD_REPEATER | \
AUTO_ADD_ROOM_SERVER | AUTO_ADD_SENSOR)
#endif
// Contact mode indices for picker
#define CONTACT_MODE_AUTO_ALL 0 // Add all contacts automatically
@@ -62,6 +66,18 @@ extern MyMesh the_mesh;
#define CONTACT_MODE_MANUAL 2 // No auto-add, companion app only
#define CONTACT_MODE_COUNT 3
// ---------------------------------------------------------------------------
// Export section flags (must match MeckExport.h)
// ---------------------------------------------------------------------------
#ifndef MECK_EXPORT_IDENTITY
#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
#endif
// ---------------------------------------------------------------------------
// Radio presets (shared with Serial CLI in MyMesh.cpp)
// ---------------------------------------------------------------------------
@@ -149,6 +165,17 @@ enum SettingsRowType : uint8_t {
ROW_CH_HEADER, // "--- Channels ---" separator
ROW_CHANNEL, // A channel entry (dynamic, index stored separately)
ROW_ADD_CHANNEL, // "+ Add Hashtag Channel"
#ifdef HAS_SDCARD
ROW_EXPORT_IMPORT_SUBMENU, // Folder row: "Export/Import >>"
ROW_EXPORT_TO_SD, // "Export to SD >>" (enters flags sub-screen)
ROW_IMPORT_FROM_SD, // "Import from SD" action
ROW_EXPORT_IDENTITY, // Checkbox: include identity in export
ROW_EXPORT_RADIO, // Checkbox: include radio settings
ROW_EXPORT_CHANNELS, // Checkbox: include channels
ROW_EXPORT_CONTACTS, // Checkbox: include contacts
ROW_EXPORT_AUTOADD, // Checkbox: include auto-add preferences (sub-item of contacts)
ROW_EXPORT_NOW, // ">> Export Now" action trigger
#endif
ROW_INFO_HEADER, // "--- Info ---" separator
#ifdef MECK_OTA_UPDATE
ROW_OTA_TOOLS_SUBMENU, // Folder row → enters OTA Tools sub-screen
@@ -193,6 +220,10 @@ enum SubScreen : uint8_t {
#ifdef MECK_OTA_UPDATE
SUB_OTA_TOOLS, // OTA Tools sub-screen (FW update + File Manager)
#endif
#ifdef HAS_SDCARD
SUB_EXPORT_IMPORT, // Export/Import menu
SUB_EXPORT_FLAGS, // Export checkboxes + trigger
#endif
};
#ifdef MECK_OTA_UPDATE
@@ -218,13 +249,13 @@ enum FmPhase : uint8_t {
// Max rows in the settings list (increased for contact sub-toggles + WiFi)
#if defined(HAS_4G_MODEM) && defined(MECK_WIFI_COMPANION)
#define SETTINGS_MAX_ROWS 57 // Extra rows for IMEI, Carrier, APN, contacts, WiFi, scope
#define SETTINGS_MAX_ROWS 63 // Extra rows for IMEI, Carrier, APN, contacts, WiFi, scope, export
#elif defined(HAS_4G_MODEM)
#define SETTINGS_MAX_ROWS 55 // Extra rows for IMEI, Carrier, APN + contacts + scope
#define SETTINGS_MAX_ROWS 61 // Extra rows for IMEI, Carrier, APN + contacts + scope + export
#elif defined(MECK_WIFI_COMPANION)
#define SETTINGS_MAX_ROWS 51 // Extra rows for contacts + WiFi + scope
#define SETTINGS_MAX_ROWS 57 // Extra rows for contacts + WiFi + scope + export
#else
#define SETTINGS_MAX_ROWS 49 // Contacts section + scope
#define SETTINGS_MAX_ROWS 55 // Contacts section + scope + export
#endif
#define SETTINGS_TEXT_BUF 33 // 32 chars + null
@@ -269,6 +300,12 @@ private:
// Sub-screen navigation
SubScreen _subScreen;
int _savedTopCursor; // cursor position to restore when leaving sub-screen
#ifdef HAS_SDCARD
int _savedExportCursor; // cursor in SUB_EXPORT_IMPORT when entering SUB_EXPORT_FLAGS
uint8_t _exportFlags; // bitmask of MECK_EXPORT_* flags for export checkboxes
bool _exportRequested; // set by key handler, cleared by main.cpp after calling export
bool _importRequested; // set by key handler, cleared by main.cpp after calling import
#endif
// Dirty flag for radio params — prompt to apply
bool _radioChanged;
@@ -402,6 +439,20 @@ private:
addRow(ROW_FW_UPDATE);
addRow(ROW_SD_FILE_MGR);
#endif
#ifdef HAS_SDCARD
} else if (_subScreen == SUB_EXPORT_IMPORT) {
// --- Export/Import sub-screen ---
addRow(ROW_EXPORT_TO_SD);
addRow(ROW_IMPORT_FROM_SD);
} else if (_subScreen == SUB_EXPORT_FLAGS) {
// --- Export checkboxes + trigger ---
addRow(ROW_EXPORT_IDENTITY);
addRow(ROW_EXPORT_RADIO);
addRow(ROW_EXPORT_CHANNELS);
addRow(ROW_EXPORT_CONTACTS);
addRow(ROW_EXPORT_AUTOADD);
addRow(ROW_EXPORT_NOW);
#endif
} else {
// --- Top-level settings list ---
addRow(ROW_NAME);
@@ -443,6 +494,9 @@ private:
#ifdef MECK_OTA_UPDATE
addRow(ROW_OTA_TOOLS_SUBMENU);
#endif
#ifdef HAS_SDCARD
addRow(ROW_EXPORT_IMPORT_SUBMENU);
#endif
// Info section (stays at top level)
addRow(ROW_INFO_HEADER);
@@ -596,6 +650,12 @@ public:
_onboarding(false), _subScreen(SUB_NONE), _savedTopCursor(0),
_radioChanged(false), _needsTextVKB(false) {
memset(_editBuf, 0, sizeof(_editBuf));
#ifdef HAS_SDCARD
_savedExportCursor = 0;
_exportFlags = 0x1F; // MECK_EXPORT_ALL
_exportRequested = false;
_importRequested = false;
#endif
#ifdef MECK_OTA_UPDATE
_otaServer = nullptr;
_otaPhase = OTA_PHASE_CONFIRM;
@@ -620,6 +680,12 @@ public:
_cursor = 0;
_scrollTop = 0;
_radioChanged = false;
#ifdef HAS_SDCARD
_savedExportCursor = 0;
_exportFlags = 0x1F; // MECK_EXPORT_ALL
_exportRequested = false;
_importRequested = false;
#endif
#ifdef HAS_4G_MODEM
_modemEnabled = ModemManager::loadEnabledConfig();
#endif
@@ -810,6 +876,15 @@ public:
handleInput('\r');
}
// Export/Import request flags — checked and cleared by main.cpp
#ifdef HAS_SDCARD
bool isExportRequested() const { return _exportRequested; }
uint8_t getExportFlags() const { return _exportFlags; }
void clearExportRequest() { _exportRequested = false; }
bool isImportRequested() const { return _importRequested; }
void clearImportRequest() { _importRequested = false; }
#endif
// ---------------------------------------------------------------------------
// OTA firmware update
// ---------------------------------------------------------------------------
@@ -1878,6 +1953,57 @@ public:
display.print("Channels >>");
break;
#ifdef HAS_SDCARD
case ROW_EXPORT_IMPORT_SUBMENU:
display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::GREEN);
display.print("Export/Import >>");
break;
case ROW_EXPORT_TO_SD:
display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::GREEN);
display.print("Export to SD >>");
break;
case ROW_IMPORT_FROM_SD:
display.print("Import from SD");
break;
case ROW_EXPORT_IDENTITY:
snprintf(tmp, sizeof(tmp), " [%c] Identity",
(_exportFlags & MECK_EXPORT_IDENTITY) ? 'X' : ' ');
display.print(tmp);
break;
case ROW_EXPORT_RADIO:
snprintf(tmp, sizeof(tmp), " [%c] Radio Settings",
(_exportFlags & MECK_EXPORT_RADIO) ? 'X' : ' ');
display.print(tmp);
break;
case ROW_EXPORT_CHANNELS:
snprintf(tmp, sizeof(tmp), " [%c] Channels",
(_exportFlags & MECK_EXPORT_CHANNELS) ? 'X' : ' ');
display.print(tmp);
break;
case ROW_EXPORT_CONTACTS:
snprintf(tmp, sizeof(tmp), " [%c] Contacts",
(_exportFlags & MECK_EXPORT_CONTACTS) ? 'X' : ' ');
display.print(tmp);
break;
case ROW_EXPORT_AUTOADD:
snprintf(tmp, sizeof(tmp), " [%c] Auto-Add Prefs",
(_exportFlags & MECK_EXPORT_AUTOADD) ? 'X' : ' ');
display.print(tmp);
break;
case ROW_EXPORT_NOW:
display.setColor(selected ? DisplayDriver::DARK : DisplayDriver::GREEN);
display.print(">> Export Now");
break;
#endif
// --- Contacts section ---
case ROW_CONTACT_HEADER:
display.setColor(DisplayDriver::YELLOW);
@@ -3410,6 +3536,56 @@ public:
startFileMgr();
break;
#endif
#ifdef HAS_SDCARD
case ROW_EXPORT_IMPORT_SUBMENU:
_savedTopCursor = _cursor;
_subScreen = SUB_EXPORT_IMPORT;
_cursor = 0;
_scrollTop = 0;
rebuildRows();
Serial.println("Settings: entered Export/Import sub-screen");
break;
case ROW_EXPORT_TO_SD:
_savedExportCursor = _cursor;
_subScreen = SUB_EXPORT_FLAGS;
_cursor = 0;
_scrollTop = 0;
rebuildRows();
Serial.println("Settings: entered Export flags sub-screen");
break;
case ROW_IMPORT_FROM_SD:
_importRequested = true;
Serial.println("Settings: import requested");
break;
case ROW_EXPORT_IDENTITY:
_exportFlags ^= MECK_EXPORT_IDENTITY;
break;
case ROW_EXPORT_RADIO:
_exportFlags ^= MECK_EXPORT_RADIO;
break;
case ROW_EXPORT_CHANNELS:
_exportFlags ^= MECK_EXPORT_CHANNELS;
break;
case ROW_EXPORT_CONTACTS:
_exportFlags ^= MECK_EXPORT_CONTACTS;
break;
case ROW_EXPORT_AUTOADD:
_exportFlags ^= MECK_EXPORT_AUTOADD;
break;
case ROW_EXPORT_NOW:
if (_exportFlags == 0) {
Serial.println("Settings: export requested but no sections selected");
} else {
_exportRequested = true;
Serial.printf("Settings: export requested (flags=0x%02X)\n", _exportFlags);
}
break;
#endif
case ROW_CHANNEL: {
// Enter on a channel row → edit its region scope
uint8_t chIdx = _rows[_cursor].param;
@@ -3482,6 +3658,18 @@ public:
// Q: back -- if in sub-screen, return to top level; else exit settings
if (c == 'q' || c == 'Q') {
#ifdef HAS_SDCARD
if (_subScreen == SUB_EXPORT_FLAGS) {
// Return to Export/Import sub-screen
_subScreen = SUB_EXPORT_IMPORT;
rebuildRows();
_cursor = _savedExportCursor;
if (_cursor >= _numRows) _cursor = _numRows - 1;
skipNonSelectable(1);
Serial.println("Settings: back to Export/Import");
return true;
}
#endif
if (_subScreen != SUB_NONE) {
// Return to top-level settings list
_subScreen = SUB_NONE;
+75 -73
View File
@@ -1,7 +1,7 @@
#pragma once
// Emoji Picker with scrolling grid and scroll bar
// 5 columns, 4 visible rows, scrollable through all 76 emoji
// 5 columns, 4 visible rows, scrollable through all 79 emoji
// WASD navigation, Enter to select, $/Q/Backspace to cancel
#include <helpers/ui/DisplayDriver.h>
@@ -18,80 +18,82 @@ static const char* EMOJI_LABELS[EMOJI_COUNT] = {
"Cry", // 2 loudly_crying
"Grim", // 3 grimace
"Zany", // 4 zany_face
"Cowb", // 5 cowboy
"100", // 5 hundred_points
"Grin", // 6 grinning
"Cowb", // 7 cowboy
// Thumbsup + heart
"Like", // 6 thumbsup
"Love", // 7 heart
"Like", // 8 thumbsup
"Love", // 9 heart
// Everything else
"WiFi", // 8 wireless
"Inf", // 9 infinity
"Rex", // 10 trex
"Skul", // 11 skull
"Cros", // 12 cross
"Bolt", // 13 lightning
"Hat", // 14 tophat
"Moto", // 15 motorcycle
"Leaf", // 16 seedling
"AU", // 17 flag_au
"Umbr", // 18 umbrella
"Eye", // 19 nazar
"Glob", // 20 globe
"Rad", // 21 radioactive
"Cow", // 22 cow
"ET", // 23 alien
"Inv", // 24 invader
"Dagr", // 25 dagger
"Mtn", // 26 mountain
"End", // 27 end_arrow
"Ring", // 28 hollow_circle
"Drag", // 29 dragon
"Web", // 30 globe_meridians
"Eggp", // 31 eggplant
"Shld", // 32 shield
"Gogl", // 33 goggles
"Lzrd", // 34 lizard
"Roo", // 35 kangaroo
"Fthr", // 36 feather
"Sun", // 37 bright
"Wave", // 38 part_alt
"Boat", // 39 motorboat
"Domi", // 40 domino
"Dish", // 41 satellite
"Pass", // 42 customs
"Whl", // 43 wheel
"Koal", // 44 koala
"Knob", // 45 control_knobs
"Pch", // 46 peach
"Race", // 47 racing_car
"Mous", // 48 mouse
"Shrm", // 49 mushroom
"Bio", // 50 biohazard
"Pnda", // 51 panda
"Bang", // 52 anger
"DrgF", // 53 dragon_face
"Pagr", // 54 pager
"Bee", // 55 bee
"Bulb", // 56 bulb
"Cat", // 57 cat
"Flur", // 58 fleur
"Moon", // 59 moon
"Cafe", // 60 coffee
"Toth", // 61 tooth
"Prtz", // 62 pretzel
"Abac", // 63 abacus
"Moai", // 64 moai
"Hiii", // 65 tipping
"Hedg", // 66 hedgehog
"Diam", // 67 diamond_suit
"Spde", // 68 spade_suit
"Piza", // 69 pizza
"Luck", // 70 four_leaf_clover
"Cld", // 71 cloud
"Rckt", // 72 rocket
"HFC", // 73 passport_control
"Star", // 74 eight_spoked_asterisk
"Sig", // 75 signal_strength
"Beer", // 76 beer
"WiFi", // 10 wireless
"Inf", // 11 infinity
"Rex", // 12 trex
"Skul", // 13 skull
"Cros", // 14 cross
"Bolt", // 15 lightning
"Hat", // 16 tophat
"Moto", // 17 motorcycle
"Leaf", // 18 seedling
"AU", // 19 flag_au
"Umbr", // 20 umbrella
"Eye", // 21 nazar
"Glob", // 22 globe
"Rad", // 23 radioactive
"Cow", // 24 cow
"ET", // 25 alien
"Inv", // 26 invader
"Dagr", // 27 dagger
"Mtn", // 28 mountain
"End", // 29 end_arrow
"Ring", // 30 hollow_circle
"Drag", // 31 dragon
"Web", // 32 globe_meridians
"Eggp", // 33 eggplant
"Shld", // 34 shield
"Gogl", // 35 goggles
"Lzrd", // 36 lizard
"Roo", // 37 kangaroo
"Fthr", // 38 feather
"Sun", // 39 bright
"Wave", // 40 part_alt
"Boat", // 41 motorboat
"Domi", // 42 domino
"Dish", // 43 satellite
"Pass", // 44 customs
"Whl", // 45 wheel
"Koal", // 46 koala
"Knob", // 47 control_knobs
"Pch", // 48 peach
"Race", // 49 racing_car
"Mous", // 50 mouse
"Shrm", // 51 mushroom
"Bio", // 52 biohazard
"Pnda", // 53 panda
"Bang", // 54 anger
"DrgF", // 55 dragon_face
"Pagr", // 56 pager
"Bee", // 57 bee
"Bulb", // 58 bulb
"Cat", // 59 cat
"Flur", // 60 fleur
"Moon", // 61 moon
"Cafe", // 62 coffee
"Toth", // 63 tooth
"Prtz", // 64 pretzel
"Abac", // 65 abacus
"Moai", // 66 moai
"Hiii", // 67 tipping
"Hedg", // 68 hedgehog
"Diam", // 69 diamond_suit
"Spde", // 70 spade_suit
"Piza", // 71 pizza
"Luck", // 72 four_leaf_clover
"Cld", // 73 cloud
"Rckt", // 74 rocket
"HFC", // 75 passport_control
"Star", // 76 eight_spoked_asterisk
"Sig", // 77 signal_strength
"Beer", // 78 beer
};
struct EmojiPicker {
+4 -4
View File
@@ -159,7 +159,7 @@ build_flags =
-D MECK_AUDIO_VARIANT
-D MECK_WEB_READER=1
-D MECK_OTA_UPDATE=1
-D FIRMWARE_VERSION='"Meck v1.10.WiFi"'
-D FIRMWARE_VERSION='"Meck v1.11.WiFi"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
-<helpers/esp32/SerialBLEInterface.cpp>
@@ -226,7 +226,7 @@ build_flags =
-D HAS_4G_MODEM=1
-D MECK_WEB_READER=1
-D MECK_OTA_UPDATE=1
-D FIRMWARE_VERSION='"Meck v1.10.4G"'
-D FIRMWARE_VERSION='"Meck v1.11.4G"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
@@ -262,7 +262,7 @@ build_flags =
-D HAS_4G_MODEM=1
-D MECK_WEB_READER=1
-D MECK_OTA_UPDATE=1
-D FIRMWARE_VERSION='"Meck v1.10.4G.WiFi"'
-D FIRMWARE_VERSION='"Meck v1.11.4G.WiFi"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
-<helpers/esp32/SerialBLEInterface.cpp>
@@ -296,7 +296,7 @@ build_flags =
-D HAS_4G_MODEM=1
-D MECK_WEB_READER=1
-D MECK_OTA_UPDATE=1
-D FIRMWARE_VERSION='"Meck v1.10.4G.SA"'
-D FIRMWARE_VERSION='"Meck v1.11.4G.SA"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
-<helpers/esp32/SerialBLEInterface.cpp>