mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-01 10:21:59 +00:00
Contacts and rooms, the scope list, discovery and ping, APC and screenshots move out of MyMesh.cpp to headers included at its bottom, as MyMeshBot.h and MyMeshCli.h are, so upstream merges meet less of our code. The feature matrix gains the 4.26" in its layout tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
221 lines
8.6 KiB
C
221 lines
8.6 KiB
C
#pragma once
|
|
// Contacts and rooms from the UI: add / delete / favourite, telemetry
|
|
// permissions, path reset, factory reset, stale-contact pruning, room passwords.
|
|
// Not part of upstream MyMesh.cpp: included at its bottom, after all class
|
|
// definitions (as MyMeshBot.h and MyMeshCli.h).
|
|
|
|
bool MyMesh::addDiscoveredContact(const uint8_t* pub_key, const char* name, uint8_t type) {
|
|
if (lookupContactByPubKey(pub_key, PUB_KEY_SIZE)) return true; // already a contact
|
|
ContactInfo c;
|
|
memset(&c, 0, sizeof(c));
|
|
c.id = mesh::Identity(pub_key);
|
|
strncpy(c.name, name ? name : "", sizeof(c.name) - 1);
|
|
c.name[sizeof(c.name) - 1] = '\0';
|
|
c.type = type;
|
|
c.out_path_len = OUT_PATH_UNKNOWN; // no path yet → flood until one is learned
|
|
c.lastmod = getRTCClock()->getCurrentTime();
|
|
c.shared_secret_valid = false;
|
|
if (!addContact(c)) return false; // contacts table full
|
|
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
|
return true;
|
|
}
|
|
|
|
// Full delete sequence shared by CMD_REMOVE_CONTACT and the on-device Nearby menu:
|
|
// drop the contact, its persisted blob and any saved room login, run the UI cleanup
|
|
// (favourite slot / Locator / Live Share target), then schedule the lazy write.
|
|
bool MyMesh::deleteContactByKey(const uint8_t* pub_key) {
|
|
ContactInfo* recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
|
if (!recipient || !removeContact(*recipient)) return false;
|
|
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE);
|
|
forgetRoomPassword(pub_key);
|
|
if (_listener) _listener->onContactRemoved(pub_key);
|
|
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
|
return true;
|
|
}
|
|
|
|
// lastmod is bumped so the app's next 'since'-filtered contact sync picks the
|
|
// change up; the write itself is lazy, like every other contact edit.
|
|
bool MyMesh::setContactFavourite(const uint8_t* pub_key, bool fav) {
|
|
ContactInfo* c = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
|
if (!c) return false;
|
|
if (fav) c->flags |= 0x01; else c->flags &= ~0x01;
|
|
c->lastmod = getRTCClock()->getCurrentTime();
|
|
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
|
return true;
|
|
}
|
|
|
|
bool MyMesh::setContactTelemetry(const uint8_t* pub_key, uint8_t perms) {
|
|
ContactInfo* c = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
|
if (!c) return false;
|
|
c->flags = (c->flags & ~0x0E) | ((perms & 0x07) << 1);
|
|
c->lastmod = getRTCClock()->getCurrentTime();
|
|
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
|
return true;
|
|
}
|
|
|
|
bool MyMesh::factoryReset() {
|
|
bool was_on = _serial && _serial->isEnabled();
|
|
if (_serial) {
|
|
MESH_DEBUG_PRINTLN("Factory reset: disabling serial interface to prevent reconnects (BLE/WiFi)");
|
|
_serial->disable();
|
|
}
|
|
if (_store->formatFileSystem()) return true;
|
|
if (was_on) _serial->enable(); // nothing erased: the link as it was
|
|
return false;
|
|
}
|
|
|
|
// lastmod bumped (unlike CMD_RESET_PATH, which the app sent itself) so the
|
|
// app's copy of the contact drops the old path too.
|
|
bool MyMesh::resetContactPath(const uint8_t* pub_key) {
|
|
ContactInfo* c = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
|
if (!c) return false;
|
|
c->out_path_len = OUT_PATH_UNKNOWN;
|
|
c->lastmod = getRTCClock()->getCurrentTime();
|
|
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
|
return true;
|
|
}
|
|
|
|
// Shared staleness test for the Settings > Contacts "Prune now" sweep.
|
|
//
|
|
// Caveats on lastmod, deliberately accepted: it's "last touched", not purely
|
|
// "last heard over the air" -- setContactFavourite() above bumps it, and the
|
|
// app can write it outright (CMD_IMPORT_CONTACT / the contact-update handler).
|
|
// Every one of those pushes a contact *away* from being pruned, never towards
|
|
// it, so the error only ever errs on the side of keeping data.
|
|
//
|
|
// Favourites are always exempt; a contact never actually heard from
|
|
// (lastmod==0, shouldn't happen for a real saved contact, but a defensive
|
|
// check costs nothing) or whose lastmod reads ahead of "now" (clock skew, or
|
|
// an unset RTC after a battery pull) is left alone rather than guessed at
|
|
// either way.
|
|
static bool contactIsStale(const ContactInfo& ci, uint32_t now, uint32_t threshold_secs) {
|
|
if (ci.flags & 0x01) return false; // favourite bit, same one setContactFavourite() writes
|
|
if (ci.lastmod == 0 || now < ci.lastmod) return false;
|
|
return (now - ci.lastmod) >= threshold_secs;
|
|
}
|
|
|
|
// Threshold in seconds for the configured expiry index, or 0 when expiry is
|
|
// Off -- the one place the NodePrefs table is read, so count and prune can't
|
|
// disagree about which contacts are in scope.
|
|
uint32_t MyMesh::staleContactThresholdSecs() const {
|
|
return (uint32_t)NodePrefs::contactExpiryDays(_prefs.contact_expiry_idx) * 86400UL;
|
|
}
|
|
|
|
int MyMesh::countStaleContacts() {
|
|
uint32_t threshold_secs = staleContactThresholdSecs();
|
|
if (threshold_secs == 0) return 0;
|
|
uint32_t now = getRTCClock()->getCurrentTime();
|
|
int count = 0;
|
|
int n = getNumContacts();
|
|
for (int i = 0; i < n; i++) {
|
|
ContactInfo ci;
|
|
if (getContactByIdx(MAX_ANON_CONTACTS + i, ci) && contactIsStale(ci, now, threshold_secs)) count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
int MyMesh::pruneStaleContacts() {
|
|
uint32_t threshold_secs = staleContactThresholdSecs();
|
|
if (threshold_secs == 0) return 0;
|
|
uint32_t now = getRTCClock()->getCurrentTime();
|
|
int removed = 0;
|
|
// Walk backwards: removeContact() compacts the array by shifting everything
|
|
// *after* the removed slot down one, so entries at lower indices keep their
|
|
// positions and a descending scan never revisits or skips one. (Forwards
|
|
// would need a restart after every delete.)
|
|
for (int i = getNumContacts() - 1; i >= 0; i--) {
|
|
ContactInfo ci;
|
|
if (getContactByIdx(MAX_ANON_CONTACTS + i, ci) && contactIsStale(ci, now, threshold_secs)) {
|
|
if (deleteContactByKey(ci.id.pub_key)) removed++;
|
|
}
|
|
}
|
|
return removed;
|
|
}
|
|
|
|
bool MyMesh::getRoomPassword(const uint8_t* pub_key, char* out_password, uint8_t max_len) {
|
|
File f = _store->openRead(ROOM_PW_FILE);
|
|
if (!f) return false;
|
|
|
|
RoomPwRec rec;
|
|
bool found = false;
|
|
while (f.read((uint8_t *)&rec, sizeof(rec)) == sizeof(rec)) {
|
|
if (memcmp(rec.key, pub_key, 4) == 0) {
|
|
strncpy(out_password, rec.pw, max_len - 1);
|
|
out_password[max_len - 1] = 0;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
f.close();
|
|
return found;
|
|
}
|
|
|
|
bool MyMesh::saveRoomPassword(const uint8_t* pub_key, const char* password) {
|
|
// The table is tiny (<= MAX_SAVED_ROOM_PASSWORDS * 20 bytes), so just load
|
|
// it whole, update/append/evict in RAM, then rewrite -- simpler and just
|
|
// as crash-safe as a record seek given how rarely this runs (once per new
|
|
// room login).
|
|
RoomPwRec recs[MAX_SAVED_ROOM_PASSWORDS];
|
|
int count = 0;
|
|
File rf = _store->openRead(ROOM_PW_FILE);
|
|
if (rf) {
|
|
RoomPwRec rec;
|
|
while (count < MAX_SAVED_ROOM_PASSWORDS && rf.read((uint8_t *)&rec, sizeof(rec)) == sizeof(rec)) {
|
|
if (memcmp(rec.key, pub_key, 4) != 0) { // drop stale entry for this key -- replaced below
|
|
recs[count++] = rec;
|
|
}
|
|
}
|
|
rf.close();
|
|
}
|
|
|
|
RoomPwRec new_rec;
|
|
memcpy(new_rec.key, pub_key, 4);
|
|
strncpy(new_rec.pw, password, sizeof(new_rec.pw) - 1);
|
|
new_rec.pw[sizeof(new_rec.pw) - 1] = 0;
|
|
|
|
if (count < MAX_SAVED_ROOM_PASSWORDS) {
|
|
recs[count++] = new_rec;
|
|
} else { // table full and not already present -- evict oldest (front)
|
|
memmove(&recs[0], &recs[1], sizeof(RoomPwRec) * (MAX_SAVED_ROOM_PASSWORDS - 1));
|
|
recs[MAX_SAVED_ROOM_PASSWORDS - 1] = new_rec;
|
|
}
|
|
|
|
// Write to a temp file and atomically swap it over /room_pw, so an
|
|
// interrupted save leaves the previous good table intact rather than a
|
|
// truncated mix (mirrors how contacts/channels are persisted).
|
|
File wf = _store->openWrite(ROOM_PW_TMP);
|
|
if (!wf) return false;
|
|
size_t want = sizeof(RoomPwRec) * count;
|
|
bool ok = (wf.write((uint8_t *)recs, want) == want);
|
|
wf.close();
|
|
if (!ok) { _store->removeFile(ROOM_PW_TMP); return false; } // keep previous good file
|
|
return _store->commitFile(ROOM_PW_TMP, ROOM_PW_FILE);
|
|
}
|
|
|
|
void MyMesh::forgetRoomPassword(const uint8_t* pub_key) {
|
|
RoomPwRec recs[MAX_SAVED_ROOM_PASSWORDS];
|
|
int count = 0;
|
|
File rf = _store->openRead(ROOM_PW_FILE);
|
|
if (!rf) return;
|
|
|
|
RoomPwRec rec;
|
|
bool removed = false;
|
|
while (count < MAX_SAVED_ROOM_PASSWORDS && rf.read((uint8_t *)&rec, sizeof(rec)) == sizeof(rec)) {
|
|
if (memcmp(rec.key, pub_key, 4) == 0) {
|
|
removed = true;
|
|
} else {
|
|
recs[count++] = rec;
|
|
}
|
|
}
|
|
rf.close();
|
|
if (!removed) return; // nothing to do, avoid a pointless rewrite
|
|
|
|
File wf = _store->openWrite(ROOM_PW_TMP);
|
|
if (!wf) return;
|
|
size_t want = sizeof(RoomPwRec) * count;
|
|
bool ok = (wf.write((uint8_t *)recs, want) == want);
|
|
wf.close();
|
|
if (!ok) { _store->removeFile(ROOM_PW_TMP); return; } // keep previous good file
|
|
_store->commitFile(ROOM_PW_TMP, ROOM_PW_FILE);
|
|
}
|