mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-08-03 02:36:11 +00:00
Merge branch 'feat/on-device-room-login'
On-device room-server login with saved passwords (atomic-write persisted), self-healing on failed login, and password cleanup on contact removal.
This commit is contained in:
@@ -47,6 +47,10 @@ public:
|
|||||||
// A repeater rebroadcast of one of our channel sends was heard (seq from
|
// A repeater rebroadcast of one of our channel sends was heard (seq from
|
||||||
// lastChannelRelaySeq()) — drives the channel "relayed into mesh" marker.
|
// lastChannelRelaySeq()) — drives the channel "relayed into mesh" marker.
|
||||||
virtual void onChannelRelayed(uint32_t seq) { (void)seq; }
|
virtual void onChannelRelayed(uint32_t seq) { (void)seq; }
|
||||||
|
// Result of an on-device-UI-triggered MyMesh::sendRoomLogin() arrived.
|
||||||
|
// pub_key is the contact's key prefix (>=4 bytes valid); permissions is the
|
||||||
|
// room/repeater ACL byte (only meaningful when success is true).
|
||||||
|
virtual void onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) { (void)pub_key; (void)success; (void)permissions; }
|
||||||
// True only when a BLE central is actually bonded/connected. On a dual
|
// True only when a BLE central is actually bonded/connected. On a dual
|
||||||
// (BLE+USB) interface hasConnection() is always true (USB counts), so use
|
// (BLE+USB) interface hasConnection() is always true (USB counts), so use
|
||||||
// this for BLE-specific UI like the pairing-PIN prompt.
|
// this for BLE-specific UI like the pairing-PIN prompt.
|
||||||
|
|||||||
@@ -176,6 +176,10 @@ File DataStore::openWrite(const char* filename) {
|
|||||||
return ::openWrite(_fs, filename);
|
return ::openWrite(_fs, filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool DataStore::commitFile(const char* tmp_path, const char* final_path) {
|
||||||
|
return commitTempFile(_fs, tmp_path, final_path);
|
||||||
|
}
|
||||||
|
|
||||||
bool DataStore::removeFile(const char* filename) {
|
bool DataStore::removeFile(const char* filename) {
|
||||||
return _fs->remove(filename);
|
return _fs->remove(filename);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ public:
|
|||||||
File openRead(const char* filename);
|
File openRead(const char* filename);
|
||||||
File openRead(FILESYSTEM* fs, const char* filename);
|
File openRead(FILESYSTEM* fs, const char* filename);
|
||||||
File openWrite(const char* filename);
|
File openWrite(const char* filename);
|
||||||
|
// Atomically replace final_path with a fully-written temp file (see
|
||||||
|
// openWrite()). Use for small custom records that want the same crash-safety
|
||||||
|
// as contacts/channels: write everything to a .tmp, then commit. Returns
|
||||||
|
// false if the swap fails (the previous good file is left untouched).
|
||||||
|
bool commitFile(const char* tmp_path, const char* final_path);
|
||||||
bool removeFile(const char* filename);
|
bool removeFile(const char* filename);
|
||||||
bool removeFile(FILESYSTEM* fs, const char* filename);
|
bool removeFile(FILESYSTEM* fs, const char* filename);
|
||||||
uint32_t getStorageUsedKb() const;
|
uint32_t getStorageUsedKb() const;
|
||||||
|
|||||||
@@ -870,6 +870,24 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
|
|||||||
i += 6; // pub_key_prefix
|
i += 6; // pub_key_prefix
|
||||||
}
|
}
|
||||||
_serial->writeFrame(out_frame, i);
|
_serial->writeFrame(out_frame, i);
|
||||||
|
} else if (ui_pending_login && memcmp(&ui_pending_login, contact.id.pub_key, 4) == 0) { // check for on-device UI login response
|
||||||
|
ui_pending_login = 0;
|
||||||
|
|
||||||
|
bool success;
|
||||||
|
uint8_t permissions = 0;
|
||||||
|
if (memcmp(&data[4], "OK", 2) == 0) { // legacy Repeater login OK response
|
||||||
|
success = true;
|
||||||
|
} else if (data[4] == RESP_SERVER_LOGIN_OK) { // new login response
|
||||||
|
uint16_t keep_alive_secs = ((uint16_t)data[5]) * 16;
|
||||||
|
if (keep_alive_secs > 0) {
|
||||||
|
startConnection(contact, keep_alive_secs);
|
||||||
|
}
|
||||||
|
success = true;
|
||||||
|
permissions = data[7]; // ACL permissions
|
||||||
|
} else {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
_ui->onRoomLoginResult(contact.id.pub_key, success, permissions);
|
||||||
} else if (len > 4 && // check for status response
|
} else if (len > 4 && // check for status response
|
||||||
pending_status &&
|
pending_status &&
|
||||||
memcmp(&pending_status, contact.id.pub_key, 4) == 0 // legacy matching scheme
|
memcmp(&pending_status, contact.id.pub_key, 4) == 0 // legacy matching scheme
|
||||||
@@ -910,6 +928,104 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define ROOM_PW_FILE "/room_pw"
|
||||||
|
#define ROOM_PW_TMP "/room_pw.tmp"
|
||||||
|
#define MAX_SAVED_ROOM_PASSWORDS 16
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
struct RoomPwRec {
|
||||||
|
uint8_t key[4]; // pub-key prefix
|
||||||
|
char pw[16]; // up to 15 chars + NUL
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
|
bool MyMesh::onContactPathRecv(ContactInfo& contact, uint8_t* in_path, uint8_t in_path_len, uint8_t* out_path, uint8_t out_path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) {
|
||||||
if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 4) {
|
if (extra_type == PAYLOAD_TYPE_RESPONSE && extra_len > 4) {
|
||||||
uint32_t tag;
|
uint32_t tag;
|
||||||
@@ -1823,6 +1939,7 @@ void MyMesh::handleCmdFrame(size_t len) {
|
|||||||
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
ContactInfo *recipient = lookupContactByPubKey(pub_key, PUB_KEY_SIZE);
|
||||||
if (recipient && removeContact(*recipient)) {
|
if (recipient && removeContact(*recipient)) {
|
||||||
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE);
|
_store->deleteBlobByKey(pub_key, PUB_KEY_SIZE);
|
||||||
|
forgetRoomPassword(pub_key); // drop any saved room login -- useless without the contact
|
||||||
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY);
|
||||||
writeOKFrame();
|
writeOKFrame();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -208,10 +208,30 @@ private:
|
|||||||
bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) override { return getChannel(channel_idx, ch); }
|
bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) override { return getChannel(channel_idx, ch); }
|
||||||
|
|
||||||
void clearPendingReqs() {
|
void clearPendingReqs() {
|
||||||
pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = 0;
|
pending_login = pending_status = pending_telemetry = pending_discovery = pending_req = ui_pending_login = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
// On-device UI login to a room/repeater contact — no phone app required.
|
||||||
|
// The room server's ACL grants permission per-identity (self_id), not per
|
||||||
|
// command source, so this reuses the same sendLogin() the BLE CMD_SEND_LOGIN
|
||||||
|
// path uses; the async result lands in onContactResponse() and is pushed to
|
||||||
|
// the UI via AbstractUITask::onRoomLoginResult().
|
||||||
|
bool sendRoomLogin(const ContactInfo& contact, const char* password) {
|
||||||
|
uint32_t est_timeout;
|
||||||
|
if (sendLogin(contact, password, est_timeout) == MSG_SEND_FAILED) return false;
|
||||||
|
clearPendingReqs();
|
||||||
|
memcpy(&ui_pending_login, contact.id.pub_key, 4); // match this in onContactResponse()
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On-device-saved room/repeater login passwords, persisted to flash (own
|
||||||
|
// small file, independent of /contacts3) so a room that's already been
|
||||||
|
// logged into doesn't need its password retyped after reboot.
|
||||||
|
bool saveRoomPassword(const uint8_t* pub_key, const char* password);
|
||||||
|
bool getRoomPassword(const uint8_t* pub_key, char* out_password, uint8_t max_len);
|
||||||
|
void forgetRoomPassword(const uint8_t* pub_key);
|
||||||
|
|
||||||
void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); }
|
void savePrefs() { _store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon); }
|
||||||
void saveRTCTime() { _store->saveRTCTime(); }
|
void saveRTCTime() { _store->saveRTCTime(); }
|
||||||
DataStore* getDataStore() const { return _store; }
|
DataStore* getDataStore() const { return _store; }
|
||||||
@@ -299,6 +319,7 @@ private:
|
|||||||
DataStore* _store;
|
DataStore* _store;
|
||||||
NodePrefs _prefs;
|
NodePrefs _prefs;
|
||||||
uint32_t pending_login;
|
uint32_t pending_login;
|
||||||
|
uint32_t ui_pending_login; // like pending_login, but triggered by on-device UI instead of BLE/USB app
|
||||||
uint32_t pending_status;
|
uint32_t pending_status;
|
||||||
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
|
uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ
|
||||||
uint32_t pending_req; // pending _BINARY_REQ
|
uint32_t pending_req; // pending _BINARY_REQ
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class QuickMsgScreen : public UIScreen {
|
|||||||
uint16_t _sorted[MAX_CONTACTS];
|
uint16_t _sorted[MAX_CONTACTS];
|
||||||
ContactInfo _sel_contact;
|
ContactInfo _sel_contact;
|
||||||
bool _room_mode; // true = picking a room server, false = picking a DM contact
|
bool _room_mode; // true = picking a room server, false = picking a DM contact
|
||||||
|
bool _login_mode; // true while KEYBOARD is collecting a room-login password
|
||||||
|
|
||||||
// CHANNEL_PICK
|
// CHANNEL_PICK
|
||||||
int _channel_sel, _channel_scroll;
|
int _channel_sel, _channel_scroll;
|
||||||
@@ -662,7 +663,7 @@ class QuickMsgScreen : public UIScreen {
|
|||||||
public:
|
public:
|
||||||
QuickMsgScreen(UITask* task, KeyboardWidget* kb)
|
QuickMsgScreen(UITask* task, KeyboardWidget* kb)
|
||||||
: _task(task), _kb(kb), _phase(MODE_SELECT), _mode_sel(0),
|
: _task(task), _kb(kb), _phase(MODE_SELECT), _mode_sel(0),
|
||||||
_contact_sel(0), _contact_scroll(0), _num_contacts(0), _room_mode(false),
|
_contact_sel(0), _contact_scroll(0), _num_contacts(0), _room_mode(false), _login_mode(false),
|
||||||
_channel_sel(0), _channel_scroll(0), _num_channels(0),
|
_channel_sel(0), _channel_scroll(0), _num_channels(0),
|
||||||
_sel_channel_idx(0), _sending_to_channel(false),
|
_sel_channel_idx(0), _sending_to_channel(false),
|
||||||
_msg_sel(0), _msg_scroll(0), _active_msg_count(0),
|
_msg_sel(0), _msg_scroll(0), _active_msg_count(0),
|
||||||
@@ -749,6 +750,62 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rooms successfully logged in to this power-on session. RAM-only — the
|
||||||
|
// server's ACL (see ClientACL) is the real permission store and survives
|
||||||
|
// reboot on its own, but the device has no way to query it, so this is just
|
||||||
|
// a local memo to skip re-prompting for a password already entered this
|
||||||
|
// session when the same room is picked again.
|
||||||
|
static const int ROOM_LOGIN_TABLE_SIZE = 8;
|
||||||
|
uint8_t _room_login_prefix[ROOM_LOGIN_TABLE_SIZE][4];
|
||||||
|
int _room_login_head = 0, _room_login_count = 0;
|
||||||
|
|
||||||
|
bool isRoomLoggedIn(const uint8_t* pub_key) const {
|
||||||
|
for (int i = 0; i < _room_login_count; i++)
|
||||||
|
if (memcmp(_room_login_prefix[i], pub_key, 4) == 0) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void markRoomLoggedIn(const uint8_t* pub_key) {
|
||||||
|
if (isRoomLoggedIn(pub_key)) return;
|
||||||
|
int pos;
|
||||||
|
if (_room_login_count < ROOM_LOGIN_TABLE_SIZE) {
|
||||||
|
pos = (_room_login_head + _room_login_count) % ROOM_LOGIN_TABLE_SIZE;
|
||||||
|
_room_login_count++;
|
||||||
|
} else {
|
||||||
|
pos = _room_login_head;
|
||||||
|
_room_login_head = (_room_login_head + 1) % ROOM_LOGIN_TABLE_SIZE;
|
||||||
|
}
|
||||||
|
memcpy(_room_login_prefix[pos], pub_key, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password of the room-login attempt currently in flight -- set right
|
||||||
|
// before sendRoomLogin(), read back in onRoomLoginResult() so a successful
|
||||||
|
// attempt can be persisted (see MyMesh::saveRoomPassword()).
|
||||||
|
char _login_pw[16];
|
||||||
|
|
||||||
|
void startRoomLogin(const char* password) {
|
||||||
|
strncpy(_login_pw, password, sizeof(_login_pw) - 1);
|
||||||
|
_login_pw[sizeof(_login_pw) - 1] = 0;
|
||||||
|
bool sent = the_mesh.sendRoomLogin(_sel_contact, password);
|
||||||
|
_task->showAlert(sent ? "Logging in..." : "Login failed", sent ? 1000 : 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result of an on-device sendRoomLogin() (MyMesh::onContactResponse(), routed
|
||||||
|
// via AbstractUITask::onRoomLoginResult()). Surfaces as a transient alert.
|
||||||
|
void onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) {
|
||||||
|
(void)permissions;
|
||||||
|
if (success) {
|
||||||
|
markRoomLoggedIn(pub_key);
|
||||||
|
the_mesh.saveRoomPassword(pub_key, _login_pw);
|
||||||
|
} else {
|
||||||
|
// Saved password (if any) no longer works -- forget it so the next
|
||||||
|
// ENTER on this room falls back to a manual prompt instead of
|
||||||
|
// silently retrying the same bad password forever.
|
||||||
|
the_mesh.forgetRoomPassword(pub_key);
|
||||||
|
}
|
||||||
|
_task->showAlert(success ? "Login OK" : "Login failed", 1200);
|
||||||
|
}
|
||||||
|
|
||||||
// Look up a contact by 4-byte pub_key prefix (as stored in DmHistEntry).
|
// Look up a contact by 4-byte pub_key prefix (as stored in DmHistEntry).
|
||||||
bool contactByPrefix(const uint8_t* prefix, ContactInfo& out) const {
|
bool contactByPrefix(const uint8_t* prefix, ContactInfo& out) const {
|
||||||
int total = the_mesh.getNumContacts();
|
int total = the_mesh.getNumContacts();
|
||||||
@@ -832,6 +889,7 @@ public:
|
|||||||
_sending_to_channel = false;
|
_sending_to_channel = false;
|
||||||
|
|
||||||
_room_mode = false;
|
_room_mode = false;
|
||||||
|
_login_mode = false;
|
||||||
buildContactList();
|
buildContactList();
|
||||||
buildChannelList();
|
buildChannelList();
|
||||||
|
|
||||||
@@ -1443,6 +1501,17 @@ public:
|
|||||||
} else if (_phase == CONTACT_PICK) {
|
} else if (_phase == CONTACT_PICK) {
|
||||||
// Context menu consumes all input while open
|
// Context menu consumes all input while open
|
||||||
if (_ctx_menu.active) {
|
if (_ctx_menu.active) {
|
||||||
|
if (_room_mode) {
|
||||||
|
auto res = _ctx_menu.handleInput(c);
|
||||||
|
if (res == PopupMenu::SELECTED && _num_contacts > 0) {
|
||||||
|
if (the_mesh.getContactByIdx(_sorted[_contact_sel], _sel_contact)) {
|
||||||
|
_login_mode = true;
|
||||||
|
_kb->begin("", 15); // room/repeater password: max 15 chars
|
||||||
|
_phase = KEYBOARD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// LEFT/RIGHT cycle Notif/Melody in-place (menu stays open).
|
// LEFT/RIGHT cycle Notif/Melody in-place (menu stays open).
|
||||||
if (!_pin_picker_active && _num_contacts > 0) {
|
if (!_pin_picker_active && _num_contacts > 0) {
|
||||||
bool left = (c == KEY_LEFT || c == KEY_PREV);
|
bool left = (c == KEY_LEFT || c == KEY_PREV);
|
||||||
@@ -1545,6 +1614,22 @@ public:
|
|||||||
if (c == KEY_ENTER && _num_contacts > 0) {
|
if (c == KEY_ENTER && _num_contacts > 0) {
|
||||||
if (the_mesh.getContactByIdx(_sorted[_contact_sel], _sel_contact)) {
|
if (the_mesh.getContactByIdx(_sorted[_contact_sel], _sel_contact)) {
|
||||||
if (_pick_target) { commitPickTargetDM(_sel_contact); return true; }
|
if (_pick_target) { commitPickTargetDM(_sel_contact); return true; }
|
||||||
|
if (_room_mode && !isRoomLoggedIn(_sel_contact.id.pub_key)) {
|
||||||
|
// Posting to a room requires a login handshake first (even with a
|
||||||
|
// blank password) — go straight to the password prompt instead of
|
||||||
|
// a history view that would silently fail to send.
|
||||||
|
char saved_pw[sizeof(_login_pw)];
|
||||||
|
if (the_mesh.getRoomPassword(_sel_contact.id.pub_key, saved_pw, sizeof(saved_pw))) {
|
||||||
|
// Logged in to this room before, on an earlier boot -- retry
|
||||||
|
// with the remembered password instead of prompting again.
|
||||||
|
startRoomLogin(saved_pw);
|
||||||
|
} else {
|
||||||
|
_login_mode = true;
|
||||||
|
_kb->begin("", 15); // room/repeater password: max 15 chars
|
||||||
|
_phase = KEYBOARD;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
_task->clearDMUnread(_sel_contact.id.pub_key);
|
_task->clearDMUnread(_sel_contact.id.pub_key);
|
||||||
_dm_hist_sel = -1;
|
_dm_hist_sel = -1;
|
||||||
_dm_hist_scroll = 0;
|
_dm_hist_scroll = 0;
|
||||||
@@ -1554,6 +1639,11 @@ public:
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (c == KEY_CONTEXT_MENU && _num_contacts > 0 && _room_mode) {
|
||||||
|
_ctx_menu.begin("Room options", 1);
|
||||||
|
_ctx_menu.addItem("Login...");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (c == KEY_CONTEXT_MENU && _num_contacts > 0 && !_room_mode) {
|
if (c == KEY_CONTEXT_MENU && _num_contacts > 0 && !_room_mode) {
|
||||||
static const char* NOTIF_LABELS[] = { "default", "OFF", "ON" };
|
static const char* NOTIF_LABELS[] = { "default", "OFF", "ON" };
|
||||||
ContactInfo ci;
|
ContactInfo ci;
|
||||||
@@ -1831,6 +1921,20 @@ public:
|
|||||||
|
|
||||||
} else if (_phase == KEYBOARD) {
|
} else if (_phase == KEYBOARD) {
|
||||||
auto res = _kb->handleInput(c);
|
auto res = _kb->handleInput(c);
|
||||||
|
if (_login_mode) {
|
||||||
|
if (res == KeyboardWidget::CANCELLED) {
|
||||||
|
_login_mode = false;
|
||||||
|
_phase = CONTACT_PICK;
|
||||||
|
} else if (res == KeyboardWidget::DONE) {
|
||||||
|
// Blank password is valid (guest/no-password rooms) — unlike normal
|
||||||
|
// message text, an empty submit here is a deliberate "log in with no
|
||||||
|
// password" attempt, so it isn't suppressed like an empty message is.
|
||||||
|
_login_mode = false;
|
||||||
|
startRoomLogin(_kb->buf);
|
||||||
|
_phase = CONTACT_PICK;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (res == KeyboardWidget::CANCELLED) {
|
if (res == KeyboardWidget::CANCELLED) {
|
||||||
if (_share_mode) { _share_mode = false; _task->gotoHomeScreen(); }
|
if (_share_mode) { _share_mode = false; _task->gotoHomeScreen(); }
|
||||||
else { _phase = MSG_PICK; }
|
else { _phase = MSG_PICK; }
|
||||||
|
|||||||
@@ -1645,6 +1645,15 @@ void UITask::onChannelRelayed(uint32_t seq) {
|
|||||||
((QuickMsgScreen*)quick_msg)->markChannelRelayed(seq);
|
((QuickMsgScreen*)quick_msg)->markChannelRelayed(seq);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void UITask::onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) {
|
||||||
|
((QuickMsgScreen*)quick_msg)->onRoomLoginResult(pub_key, success, permissions);
|
||||||
|
// Unlike the keypress-driven showAlert() calls elsewhere, this fires from a
|
||||||
|
// background mesh response with no keypress to schedule a redraw — without
|
||||||
|
// forcing one, the alert's short expiry can lapse before the next scheduled
|
||||||
|
// refresh ever draws it.
|
||||||
|
_next_refresh = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void UITask::addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, uint32_t sender_timestamp) {
|
void UITask::addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, uint32_t sender_timestamp) {
|
||||||
((QuickMsgScreen*)quick_msg)->addDMMsg(pub_key, outgoing, text, sender_timestamp);
|
((QuickMsgScreen*)quick_msg)->addDMMsg(pub_key, outgoing, text, sender_timestamp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ public:
|
|||||||
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, uint32_t sender_timestamp = 0) override;
|
void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, uint32_t sender_timestamp = 0) override;
|
||||||
void onMsgAck(uint32_t ack_crc) override;
|
void onMsgAck(uint32_t ack_crc) override;
|
||||||
void onChannelRelayed(uint32_t seq) override;
|
void onChannelRelayed(uint32_t seq) override;
|
||||||
|
void onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) override;
|
||||||
int getDMUnreadTotal() const;
|
int getDMUnreadTotal() const;
|
||||||
int getMsgCount() const { return _msgcount; }
|
int getMsgCount() const { return _msgcount; }
|
||||||
int getChannelUnreadCount() const;
|
int getChannelUnreadCount() const;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
- **Trail auto-pause** — recording **freezes on stops** (banking elapsed time and breaking the map line across the idle gap) and **resumes on movement** without ending the session; the home-screen blink keeps going while paused.
|
- **Trail auto-pause** — recording **freezes on stops** (banking elapsed time and breaking the map line across the idle gap) and **resumes on movement** without ending the session; the home-screen blink keeps going while paused.
|
||||||
- **Collapsible Tools** — tools are grouped into fold-in-place **Location / Comms / System** sections, the same model as Settings (Tools always opens folded to the section list), and the home carousel now uses **page-indicator icons** instead of dots.
|
- **Collapsible Tools** — tools are grouped into fold-in-place **Location / Comms / System** sections, the same model as Settings (Tools always opens folded to the section list), and the home carousel now uses **page-indicator icons** instead of dots.
|
||||||
- **Waypoint coordinate editor** — add a waypoint by scroll-editing its latitude/longitude digit by digit.
|
- **Waypoint coordinate editor** — add a waypoint by scroll-editing its latitude/longitude digit by digit.
|
||||||
|
- **On-device room login with saved passwords** — log in to a room server straight from the device (no phone app): pick the room and the password prompt appears automatically (a blank password works for open rooms), or re-login any time via the room's **context-menu "Login…"**. The password is **remembered across reboots**, so a room you've used before logs back in without retyping; a failed login (e.g. the server's password changed) forgets the stale password so the next attempt prompts again. Saved passwords are written with the same atomic, crash-safe persistence as contacts and channels.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user