diff --git a/examples/companion_radio/MeckExport.h b/examples/companion_radio/MeckExport.h new file mode 100644 index 00000000..3c3ff1af --- /dev/null +++ b/examples/companion_radio/MeckExport.h @@ -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 +#include +#include + +#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; +} \ No newline at end of file diff --git a/examples/companion_radio/MeckImport.h b/examples/companion_radio/MeckImport.h new file mode 100644 index 00000000..ce96f578 --- /dev/null +++ b/examples/companion_radio/MeckImport.h @@ -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 +#include +#include + +// 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; +} \ No newline at end of file diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 53a26f39..821639e3 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -253,6 +253,9 @@ public: void saveContacts() { _store->saveContacts(this); } + void saveMainIdentity() { + _store->saveMainIdentity(self_id); + } private: void writeOKFrame(); diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 80c00024..fe2e04ec 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -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; } diff --git a/examples/companion_radio/ui-new/Emojisprites.h b/examples/companion_radio/ui-new/Emojisprites.h index af5e41ec..9cad73e2 100644 --- a/examples/companion_radio/ui-new/Emojisprites.h +++ b/examples/companion_radio/ui-new/Emojisprites.h @@ -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 #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) { diff --git a/examples/companion_radio/ui-new/Settingsscreen.h b/examples/companion_radio/ui-new/Settingsscreen.h index 27e1d583..b5714050 100644 --- a/examples/companion_radio/ui-new/Settingsscreen.h +++ b/examples/companion_radio/ui-new/Settingsscreen.h @@ -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; diff --git a/examples/companion_radio/ui-new/emojipicker.h b/examples/companion_radio/ui-new/emojipicker.h index d05f1f2a..9713e2f0 100644 --- a/examples/companion_radio/ui-new/emojipicker.h +++ b/examples/companion_radio/ui-new/emojipicker.h @@ -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 @@ -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 { diff --git a/variants/lilygo_tdeck_pro/platformio.ini b/variants/lilygo_tdeck_pro/platformio.ini index 3f344da6..66dc0b37 100644 --- a/variants/lilygo_tdeck_pro/platformio.ini +++ b/variants/lilygo_tdeck_pro/platformio.ini @@ -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} + - @@ -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} + + @@ -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} + - @@ -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} + -