mirror of
https://github.com/pelgraine/Meck.git
synced 2026-08-02 23:02:30 +02:00
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:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -253,6 +253,9 @@ public:
|
||||
void saveContacts() {
|
||||
_store->saveContacts(this);
|
||||
}
|
||||
void saveMainIdentity() {
|
||||
_store->saveMainIdentity(self_id);
|
||||
}
|
||||
|
||||
private:
|
||||
void writeOKFrame();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user