From 2a8d4598d4e5544bbede4b775b4d6a924a4ca7ec Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Thu, 24 Sep 2026 22:54:40 +0200 Subject: [PATCH] refactor: port upstream MyMesh::Listener; MyMesh no longer calls the UI directly Ports upstream's "Abstract UI overhaul" (PR #3431 + follow-ups) with identical names/signatures. MyMesh talks to the UI only through MyMesh::Listener (setListener()); a second block holds Solo extensions (own-send mirroring, relay echoes, room login/admin replies, [LOC] shares, contact/channel removal, bot device actions, requestShutdown), all no-op by default. AbstractUITask becomes the Listener and carries the glue MyMesh used to run for the UI (display filter, room-post labels, notifications, connection polling). BLE PIN randomisation moves to main.cpp as upstream. Also fixes ui-orig's stale advertReceived event on buzzer boards. Co-Authored-By: Claude Opus 5.5 --- docs/development/ui-core.md | 20 ++- examples/companion_radio/AbstractUITask.h | 171 +++++++++----------- examples/companion_radio/MyMesh.cpp | 167 ++++++++----------- examples/companion_radio/MyMesh.h | 103 ++++++++++-- examples/companion_radio/MyMeshBot.h | 26 +-- examples/companion_radio/main.cpp | 42 +++-- examples/companion_radio/ui-new/UITask.cpp | 3 +- examples/companion_radio/ui-orig/UITask.cpp | 4 +- examples/companion_radio/ui-tiny/UITask.cpp | 1 + 9 files changed, 305 insertions(+), 232 deletions(-) diff --git a/docs/development/ui-core.md b/docs/development/ui-core.md index 380a70b6..f55a7598 100644 --- a/docs/development/ui-core.md +++ b/docs/development/ui-core.md @@ -62,10 +62,19 @@ interleaved. Classified: ### 1. `UiCore` owns the mesh-facing interface -`UiCore` implements `AbstractUITask`. `MyMesh` keeps calling exactly the same -methods; they land in the Core, which updates models/engines and emits events. -The frontend no longer implements `AbstractUITask` at all. This is the single -point that makes both frontends receive identical behaviour. +`MyMesh` talks to the UI only through `MyMesh::Listener` (set with +`setListener()`), ported from upstream's "Abstract UI overhaul" (PR #3431 and +follow-ups `3caf033d`, `5c3d9281`, `30dd723c`, `b3b17025`, `64434c53`) with +identical names/signatures. A second block in the same interface holds this +fork's extensions (own-send mirroring, relay echoes, room login/admin replies, +`[LOC]` shares, contact/channel removal, bot device actions, +`requestShutdown`), each defaulting to a no-op. + +Today `AbstractUITask` is the Listener and carries the glue `MyMesh` used to +run for the UI (display filter, room-post labelling, notifications). In the +target design `UiCore` becomes the Listener instead: it updates models/engines +and emits events, and the frontends no longer implement it at all. This is the +single point that makes both frontends receive identical behaviour. ### 2. Engines @@ -140,7 +149,8 @@ consumes keys only. Each step keeps `WioTrackerL1_companion_solo_dual` behaviour identical and is checked in the sim plus on L1 hardware before the next one. -1. **Skeleton.** `examples/companion_radio/ui-core/`; add it to every solo env's `build_src_filter` / include path. Move `MessageHistory` + DM unread tracking into it; `ui-new` uses them through the Core. +0. **Listener boundary** ✅ (upstream `MyMesh::Listener` ported; `MyMesh` has no UI calls left). +1. **Skeleton.** `examples/companion_radio/ui-core/`, compiled through a single unity `.cpp` inside each frontend directory so the 66 variant `platformio.ini` files that already build `ui-new` need no changes; add it to every solo env's `build_src_filter` / include path. Move `MessageHistory` + DM unread tracking into it; `ui-new` uses them through the Core. 2. **Engines, one per commit.** Clock tools → ping → course-over-ground → live share → locator → trail → notifications. `UITask` shrinks to screen management + drawing. 3. **Flip the interface.** `UiCore` implements `AbstractUITask`; `ui-new`'s `UITask` becomes a frontend fed by events. 4. **Settings schema.** Convert `SettingsScreen` section by section. diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index 070ebf9b..48772780 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -5,7 +5,8 @@ #include #include #include -#include // MAX_TEXT_LEN, for addOwnChannelMsg() below +#include // MAX_TEXT_LEN +#include // TXT_TYPE_* #include #ifdef PIN_BUZZER @@ -13,6 +14,7 @@ #endif #include "NodePrefs.h" +#include "MyMesh.h" enum class UIEventType { none, @@ -24,16 +26,30 @@ enum class UIEventType { ack }; -class AbstractUITask { +// Every on-device UI (ui-new / ui-orig / ui-tiny) is MyMesh's Listener. The +// glue MyMesh used to run for the UI itself -- which message types show on +// screen, how a room post is labelled, which notification plays -- lives here +// now, so each UI variant keeps receiving the same newMsg()/addDMMsg()/ +// notify() calls it always did. +class AbstractUITask : public MyMesh::Listener { protected: mesh::MainBoard* _board; BaseSerialInterface* _serial; bool _connected; + int _queue_len = 0; // last onQueueSizeChanged() -- newMsg()'s msgcount AbstractUITask(mesh::MainBoard* board, BaseSerialInterface* serial) : _board(board), _serial(serial) { _connected = false; } + // hasConnection() means "a BLE companion app is connected". Not isConnected() + // (a dual interface hardcodes that true as a USB send-fallback). Drives the BT + // status indicator, pairing PIN, and the GPX-export collision warning — all + // BLE-specific. The Auto buzzer mute / message-wake use isClientConnected() + // (BLE *or* an open USB port) directly instead. Each UI calls this at the top + // of its loop() (MyMesh::loop() used to push it at the end of its own). + void pollConnection() { setHasConnection(_serial->isBLEConnected()); } + public: void setHasConnection(bool connected) { bool prev = _connected; @@ -45,37 +61,6 @@ public: // An end-to-end ACK (CRC) arrived for one of our sent messages — drives the // DM delivery-status marker. Default no-op for UIs that don't track it. virtual void onMsgAck(uint32_t ack_crc) { (void)ack_crc; } - // A repeater rebroadcast of one of our channel sends was heard (seq from - // lastChannelRelaySeq()) — drives the channel "relayed into mesh" marker. - // May fire once per distinct repeater within earshot for the same seq; - // repeater_hash/hash_size (when given) is that repeater's path hash, so the - // UI can list every repeater that confirmed, not just "was it heard at all". - virtual void onChannelRelayed(uint32_t seq, const uint8_t* repeater_hash = nullptr, uint8_t hash_size = 0) { - (void)seq; (void)repeater_hash; (void)hash_size; - } - // 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; } - // Text reply to an on-device-UI-triggered MyMesh::sendAdminCommand() arrived - // (see AdminScreen). pub_key is the contact's key prefix (>=4 bytes valid). - virtual void onAdminReply(const uint8_t* pub_key, const char* text) { (void)pub_key; (void)text; } - // Bot action commands (!gps/!buzz, see MyMesh::botCommandReply) -- device - // state changes triggered remotely, gated by the bot_actions_* prefs. - // Default no-op so UI variants that don't wire these up just ignore them. - virtual void botSetGPS(bool on) { (void)on; } - virtual void botBuzz(int seconds) { (void)seconds; } - // !gpio1..!gpio4 (idx 1-4). botSetGPIO returns false if the pin isn't - // currently configured as an Output (or the board has none) -- lets the - // bot reply distinguish "set" from "ignored". botGetGPIO returns false if - // the pin is Off/unsupported; on true, fills is_output (current direction) - // and value (live level). - virtual bool botSetGPIO(int idx, bool on) { (void)idx; (void)on; return false; } - virtual bool botGetGPIO(int idx, bool& is_output, bool& value) { (void)idx; (void)is_output; (void)value; return false; } - // Analog read for pins that support it (GPIO1/GPIO2 on Wio Tracker L1 -- - // the nRF52840's AIN0/AIN5). Returns false if the pin isn't in Analog mode - // or doesn't support it; on true, fills millivolts with the reading. - virtual bool botGetGPIOAnalog(int idx, int& millivolts) { (void)idx; (void)millivolts; return false; } // True only when a BLE central is actually bonded/connected. On a dual // (BLE+USB) interface hasConnection() is always true (USB counts), so use // this for BLE-specific UI like the pairing-PIN prompt. @@ -90,70 +75,72 @@ public: virtual void msgRead(int msgcount) = 0; virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount, uint8_t contact_type = 0, const uint8_t* pub_key = nullptr) = 0; virtual void notify(UIEventType t = UIEventType::none) = 0; - // Returns the new entry's ring position (see MessageHistory::addChannelMsg), - // or -1 on a UI variant that doesn't track history (default no-op below) -- - // callers that need it (to then arm a relay-echo tracker) should check for - // that instead of assuming a valid position. path/path_len (packed - // (hash_size-1)<<6|hop_count, same as mesh::Packet::path_len) is the hop - // route this incoming post actually took -- nullptr/0 when not known (e.g. - // this is our own outgoing post). own_message: our own post (e.g. mirrored - // from an app-originated send) -- never counted unread, unlike a genuinely - // incoming post the device's own UI doesn't happen to be showing right now. - virtual int addChannelMsg(uint8_t channel_idx, const char* text, uint32_t timestamp = 0, - const uint8_t* path = nullptr, uint8_t path_len = 0, - bool own_message = false) { return -1; } - // Convenience wrapper around addChannelMsg() for mirroring a channel post - // this device just sent itself (bot trigger/command reply, a !gps fix - // result, an app-originated send) into the on-device history. Always frames - // it with the literal "Me: " prefix -- the convention MessagesScreen uses - // (see its bubble-side check) to tell an outgoing post from an incoming - // one -- and always passes own_message=true, so a caller can't reintroduce - // the bug this replaced: three separate MyMeshBot.h call sites used to - // build ": " instead, which rendered the reply as an incoming - // bubble from a stranger who happened to share the device's own name. - // text_len < 0 (default) means text is null-terminated; otherwise only the - // first text_len bytes are used (a source buffer isn't always guaranteed - // to be null-terminated, e.g. the app-originated mirror). - int addOwnChannelMsg(uint8_t channel_idx, const char* text, int text_len = -1, uint32_t timestamp = 0) { - char buf[MAX_TEXT_LEN + 8]; // "Me: "(4) + text(MAX_TEXT_LEN) + margin - if (text_len < 0) snprintf(buf, sizeof(buf), "Me: %s", text); - else snprintf(buf, sizeof(buf), "Me: %.*s", text_len, text); - return addChannelMsg(channel_idx, buf, timestamp, nullptr, 0, true); - } - // Arms the "relayed into mesh" tracker (a heard repeater rebroadcast) on the - // entry at ring position pos, e.g. right after addChannelMsg for a channel - // send this device just originated. seq: MyMesh::lastChannelRelaySeq(). - virtual void armChannelRelay(int pos, uint32_t seq) {} - // ack_tag/ack_deadline_ms/resends: pending-ACK tracking for an outgoing DM - // (0 = none, e.g. incoming or "no ack expected") -- see MessageHistory::addDMMsg. - // path/path_len: the hop route an incoming DM actually took (nullptr/0 for - // outgoing -- a DM's delivery confirmation is the ack_tag above, not a path). - virtual void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, uint32_t sender_timestamp = 0, - uint32_t ack_tag = 0, uint32_t ack_deadline_ms = 0, uint8_t resends = 0, - const uint8_t* path = nullptr, uint8_t path_len = 0) {} - // A node shared its current position via a [LOC] message. pub_key is the - // sender's key prefix for a verified DM share, or null for a channel share - // (keyed by name, best-effort). Default no-op so UI variants opt in. - virtual void onSharedLocation(const uint8_t* pub_key, const char* name, - int32_t lat_1e6, int32_t lon_1e6, - uint32_t ts, bool verified) {} - // A contact is gone — removed explicitly (companion app / CLI command) or - // silently auto-evicted to make room when the contact table is full. Lets - // UI state that references contacts by pubkey (favourite slots, the - // Locator/Live Share target) drop a reference that would otherwise dangle. - // Default no-op. - virtual void onContactRemoved(const uint8_t* pub_key) {} - // A channel slot was cleared (companion app set it to an empty secret). - // Drop any setting that referenced it by index — otherwise a new channel - // added later at the same slot would silently inherit the old one's bot/ - // share target or notification melody. Default no-op. - virtual void onChannelRemoved(uint8_t channel_idx) {} // Single choke point for every controlled power-down (low-battery auto-off, - // long-press power-off, and now every board.reboot() caller too): flush + // long-press power-off, and every board.reboot() caller too): flush // prefs/RTC/contacts/trail before the board actually goes down or restarts, // so no exit path can silently skip a pending write. restart=true reboots, // false powers off. Every UI variant (ui-new/ui-tiny/ui-orig) implements // this the same way -- see each's UITask::shutdown(). virtual void shutdown(bool restart = false) = 0; virtual void loop() = 0; + + // ---- MyMesh::Listener ---- + void onQueueSizeChanged(int msgcount) override { + _queue_len = msgcount; + msgRead(msgcount); // only acts on 0 (queue drained by the app) in every UI + } + void onACKRecv(uint32_t ack_crc) override { onMsgAck(ack_crc); } + void onAdvertHeard(bool was_flood) override { + notify(was_flood ? UIEventType::advertReceivedFlood : UIEventType::advertReceivedZeroHop); + } + bool requestShutdown(bool restart) override { shutdown(restart); return true; } + + void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) override { + onMessageRecvEx(pkt, from, txt_type, sender_timestamp, nullptr, 0, text); + } + void onMessageRecvEx(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, + const uint8_t* extra, int extra_len, const char* text) override { + // we only want to show text messages on display, not cli data + if (!(txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN)) return; + uint8_t path_len = pkt->isRouteFlood() ? pkt->path_len : 0xFF; + newMsg(path_len, from.name, text, _queue_len, from.type, from.id.pub_key); + notify(from.type == ADV_TYPE_ROOM ? UIEventType::roomMessage : UIEventType::contactMessage); + // Add to the on-device conversation history. Room servers (ADV_TYPE_ROOM) are + // viewed through the same history list as chat contacts (keyed by the server's + // pubkey), so their posts must be stored too — otherwise an incoming room + // message fires the notification and reaches the app via the offline queue but + // never shows when the room is opened directly on the device. + if (from.type == ADV_TYPE_CHAT) { + addDMMsg(from.id.pub_key, false, text, sender_timestamp, 0, 0, 0, pkt->path, (uint8_t)pkt->path_len); + } else if (from.type == ADV_TYPE_ROOM) { + // A room carries many guests, so prefix the post with its author so the UI + // can attribute each line. The signed message's `extra` holds the sender's + // pubkey prefix; resolve it to a contact name, falling back to a short hex. + char labeled[MAX_TEXT_LEN + 40]; // room text + "Sender: " (history store truncates) + if (extra && extra_len >= 4) { + ContactInfo* sc = the_mesh.lookupContactByPubKey(extra, extra_len); + if (sc && sc->name[0]) + snprintf(labeled, sizeof(labeled), "%s: %s", sc->name, text); + else + snprintf(labeled, sizeof(labeled), "%02X%02X: %s", extra[0], extra[1], text); + } else { + snprintf(labeled, sizeof(labeled), "%s", text); + } + addDMMsg(from.id.pub_key, false, labeled, sender_timestamp, 0, 0, 0, pkt->path, (uint8_t)pkt->path_len); + } + } + + // Upstream-shaped entry point without the channel slot: MyMesh itself always + // calls the Ex variant below, so this only serves a caller that doesn't know + // the slot -- show and notify, but there's no history ring to file it under. + void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) override { + notify(UIEventType::channelMessage); + newMsg(pkt->isRouteFlood() ? pkt->path_len : 0xFF, channel_details.name, text, _queue_len, 0); + } + void onChannelMessageRecvEx(mesh::Packet *pkt, uint8_t channel_idx, ChannelDetails& channel_details, + uint32_t timestamp, const char* text) override { + addChannelMsg(channel_idx, text, timestamp, pkt->path, (uint8_t)pkt->path_len); + notify(UIEventType::channelMessage); + newMsg(pkt->isRouteFlood() ? pkt->path_len : 0xFF, channel_details.name, text, _queue_len, 0); + } }; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 35178ead..489900c3 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -6,10 +6,10 @@ #include // needed for PlatformIO #include -#ifdef DISPLAY_CLASS -#include "helpers/ui/DisplayDriver.h" -#include "UITask.h" #include +#if defined(DISPLAY_CLASS) && defined(ENABLE_SCREENSHOT) +#include "helpers/ui/DisplayDriver.h" +#include "UITask.h" // screenshot debug command reads the UI's display #endif #define CMD_APP_START 1 @@ -373,7 +373,7 @@ uint8_t MyMesh::getAutoAddMaxHops() const { void MyMesh::onContactOverwrite(const uint8_t* pub_key) { _store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); // delete from storage - if (_ui) _ui->onContactRemoved(pub_key); // same cleanup as an explicit CMD_REMOVE_CONTACT + if (_listener) _listener->onContactRemoved(pub_key); // same cleanup as an explicit CMD_REMOVE_CONTACT if (_serial->isConnected()) { out_frame[0] = PUSH_CODE_CONTACT_DELETED; memcpy(&out_frame[1], pub_key, PUB_KEY_SIZE); @@ -389,7 +389,7 @@ void MyMesh::onContactsFull() { } void MyMesh::onDiscoveredAdvert(bool was_flood) { - if (_ui) _ui->notify(was_flood ? UIEventType::advertReceivedFlood : UIEventType::advertReceivedZeroHop); + if (_listener) _listener->onAdvertHeard(was_flood); } void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { @@ -424,6 +424,8 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path p->path_len = mesh::Packet::copyPath(p->path, path, path_len); } + if (_listener) _listener->onDiscoveredContact(contact, is_new, path_len, path); + if (!is_new) dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); // only schedule lazy write for contacts that are in contacts[] } @@ -465,7 +467,7 @@ bool MyMesh::deleteContactByKey(const uint8_t* pub_key) { if (!recipient || !removeContact(*recipient)) return false; _store->deleteBlobByKey(pub_key, PUB_KEY_SIZE); forgetRoomPassword(pub_key); - if (_ui) _ui->onContactRemoved(pub_key); + if (_listener) _listener->onContactRemoved(pub_key); dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); return true; } @@ -608,37 +610,12 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe _serial->writeFrame(frame, 1); } -#ifdef DISPLAY_CLASS - // we only want to show text messages on display, not cli data - bool should_display = txt_type == TXT_TYPE_PLAIN || txt_type == TXT_TYPE_SIGNED_PLAIN; - if (should_display && _ui) { - _ui->newMsg(path_len, from.name, text, offline_queue_len, from.type, from.id.pub_key); - _ui->notify(from.type == ADV_TYPE_ROOM ? UIEventType::roomMessage : UIEventType::contactMessage); - // Add to the on-device conversation history. Room servers (ADV_TYPE_ROOM) are - // viewed through the same history list as chat contacts (keyed by the server's - // pubkey), so their posts must be stored too — otherwise an incoming room - // message fires the notification and reaches the app via the offline queue but - // never shows when the room is opened directly on the device. - if (from.type == ADV_TYPE_CHAT) { - _ui->addDMMsg(from.id.pub_key, false, text, sender_timestamp, 0, 0, 0, pkt->path, (uint8_t)pkt->path_len); - } else if (from.type == ADV_TYPE_ROOM) { - // A room carries many guests, so prefix the post with its author so the UI - // can attribute each line. The signed message's `extra` holds the sender's - // pubkey prefix; resolve it to a contact name, falling back to a short hex. - char labeled[MAX_TEXT_LEN + 40]; // room text + "Sender: " (history store truncates) - if (extra && extra_len >= 4) { - ContactInfo* sc = lookupContactByPubKey(extra, extra_len); - if (sc && sc->name[0]) - snprintf(labeled, sizeof(labeled), "%s: %s", sc->name, text); - else - snprintf(labeled, sizeof(labeled), "%02X%02X: %s", extra[0], extra[1], text); - } else { - snprintf(labeled, sizeof(labeled), "%s", text); - } - _ui->addDMMsg(from.id.pub_key, false, labeled, sender_timestamp, 0, 0, 0, pkt->path, (uint8_t)pkt->path_len); - } + if (_listener) { + // Queue size first: the UI caps its room-unread count against it when the + // message itself arrives (upstream sends it after; order is harmless there). + _listener->onQueueSizeChanged(offline_queue_len); + _listener->onMessageRecvEx(pkt, from, txt_type, sender_timestamp, extra, extra_len, text); } -#endif } bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) { @@ -668,7 +645,7 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) { // DIFFERENT repeater's independent echo of this same send can still // match here too -- onChannelRelayed()/markChannelRelayed() append each // additionally heard repeater instead of just flipping a single flag. - if (_ui) { + if (_listener) { uint8_t hash_size = packet->getPathHashSize(); uint8_t hop_count = packet->getPathHashCount(); // The repeater we just heard directly is always the LAST hop appended @@ -676,7 +653,7 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) { // one within our own earshot, regardless of how many further hops // this same packet may go on to take beyond it. const uint8_t* repeater_hash = hop_count > 0 ? &packet->path[(hop_count - 1) * hash_size] : nullptr; - _ui->onChannelRelayed(s.seq, repeater_hash, repeater_hash ? hash_size : 0); + _listener->onChannelRelayed(s.seq, repeater_hash, repeater_hash ? hash_size : 0); } break; } @@ -933,8 +910,8 @@ void MyMesh::onMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uint32_t // Live position share: a verified DM, so key the track by the sender's pubkey. int32_t loc_lat, loc_lon; - if (_ui && geo::parseLocShare(text, loc_lat, loc_lon)) { - _ui->onSharedLocation(from.id.pub_key, from.name, loc_lat, loc_lon, sender_timestamp, true); + if (_listener && geo::parseLocShare(text, loc_lat, loc_lon)) { + _listener->onSharedLocation(from.id.pub_key, from.name, loc_lat, loc_lon, sender_timestamp, true); } // hop count of the received message. getPathHashCount() (low 6 bits of path_len) @@ -954,9 +931,9 @@ void MyMesh::onCommandDataRecv(const ContactInfo &from, mesh::Packet *pkt, uint3 // terminal), also hand the reply straight to the UI -- queueMessage() above // never displays TXT_TYPE_CLI_DATA on-device (see should_display), since that // path also serves the app's terminal, which must keep working unaffected. - if (_ui && ui_pending_admin_reply && memcmp(&ui_pending_admin_reply, from.id.pub_key, 4) == 0) { + if (_listener && ui_pending_admin_reply && memcmp(&ui_pending_admin_reply, from.id.pub_key, 4) == 0) { ui_pending_admin_reply = 0; - _ui->onAdminReply(from.id.pub_key, text); + _listener->onAdminReply(from.id.pub_key, text); } } @@ -972,10 +949,10 @@ void MyMesh::onSignedMessageRecv(const ContactInfo &from, mesh::Packet *pkt, uin // resolve that 4-byte prefix to a contact name and track by name. Unverified: // we only hold a 4-byte prefix here, not the full pubkey LiveTrack keys on. int32_t loc_lat, loc_lon; - if (_ui && geo::parseLocShare(text, loc_lat, loc_lon)) { + if (_listener && geo::parseLocShare(text, loc_lat, loc_lon)) { ContactInfo* sc = sender_prefix ? lookupContactByPubKey(sender_prefix, 4) : nullptr; const char* who = (sc && sc->name[0]) ? sc->name : from.name; - _ui->onSharedLocation(nullptr, who, loc_lat, loc_lon, sender_timestamp, false); + _listener->onSharedLocation(nullptr, who, loc_lat, loc_lon, sender_timestamp, false); } // Room-server auto-reply bot — only ever fires for the room server contact @@ -1030,28 +1007,26 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _serial->writeFrame(frame, 1); } -#ifdef DISPLAY_CLASS - if (_ui) _ui->addChannelMsg(channel_idx, text, timestamp, pkt->path, (uint8_t)pkt->path_len); - if (_ui) _ui->notify(UIEventType::channelMessage); - const char *channel_name = "Unknown"; - ChannelDetails channel_details; - if (getChannel(channel_idx, channel_details)) { - channel_name = channel_details.name; + if (_listener) { + ChannelDetails channel_details; + if (!getChannel(channel_idx, channel_details)) { + strcpy(channel_details.name, "Unknown"); + } + _listener->onQueueSizeChanged(offline_queue_len); // first -- see queueMessage() + _listener->onChannelMessageRecvEx(pkt, channel_idx, channel_details, timestamp, text); } - if (_ui) _ui->newMsg(path_len, channel_name, text, offline_queue_len, 0); // Live position share on a channel. The sender's identity here is only the // unsigned "name: msg" prefix (no pubkey), so track it by name — best-effort // and unverified. parseLocShare requires an explicit [LOC] tag, so ordinary // chatter is ignored. int32_t loc_lat, loc_lon; - if (_ui && geo::parseLocShare(text, loc_lat, loc_lon)) { + if (_listener && geo::parseLocShare(text, loc_lat, loc_lon)) { char sender[32]; const char* msg; botChannelSenderSplit(text, sender, sizeof(sender), &msg); - _ui->onSharedLocation(nullptr, sender, loc_lat, loc_lon, timestamp, false); + _listener->onSharedLocation(nullptr, sender, loc_lat, loc_lon, timestamp, false); } -#endif // hop count for !hops (see onMessageRecv); not the wire path_len above. uint8_t ch_hops = pkt->getPathHashCount(); @@ -1097,6 +1072,9 @@ void MyMesh::onChannelDataRecv(const mesh::GroupChannel &channel, mesh::Packet * frame[0] = PUSH_CODE_MSG_WAITING; // send push 'tickle' _serial->writeFrame(frame, 1); } + if (_listener) { + _listener->onChannelDataRecv(pkt, channel, data_type, data, data_len); + } } uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, @@ -1144,6 +1122,8 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim memcpy(&reply[4], telemetry.getBuffer(), tlen); return 4 + tlen; } + } else if (_listener) { + return _listener->onUnhandledRequest(contact, sender_timestamp, data, len, reply); } return 0; // unknown } @@ -1210,7 +1190,7 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, } else { success = false; } - _ui->onRoomLoginResult(contact.id.pub_key, success, permissions); + if (_listener) _listener->onRoomLoginResult(contact.id.pub_key, success, permissions); } else if (len > 4 && // check for status response pending_status && memcmp(&pending_status, contact.id.pub_key, 4) == 0 // legacy matching scheme @@ -1248,6 +1228,8 @@ void MyMesh::onContactResponse(const ContactInfo &contact, const uint8_t *data, memcpy(&out_frame[i], &data[4], len - 4); i += (len - 4); _serial->writeFrame(out_frame, i); + } else if (_listener && len > 4) { + _listener->onUnhandledResponse(contact, tag, &data[4], len - 4); } } @@ -1532,7 +1514,7 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { memcpy(r.pub_key, pub_key, PUB_KEY_SIZE); r.timestamp = getRTCClock()->getCurrentTime(); } - if (_ui) _ui->notify(packet->isRouteFlood() ? UIEventType::advertReceivedFlood : UIEventType::advertReceivedZeroHop); + if (_listener) _listener->onAdvertHeard(packet->isRouteFlood()); return; // our discover — don't forward to BLE app } } @@ -1564,6 +1546,8 @@ void MyMesh::onControlDataRecv(mesh::Packet *packet) { } else { MESH_DEBUG_PRINTLN("onControlDataRecv(), data received while app offline"); } + + if (_listener) _listener->onControlDataRecv(packet); } void MyMesh::onRawDataRecv(mesh::Packet *packet) { @@ -1584,6 +1568,9 @@ void MyMesh::onRawDataRecv(mesh::Packet *packet) { } else { MESH_DEBUG_PRINTLN("onRawDataRecv(), data received while app offline"); } + if (_listener) { + _listener->onRawDataRecv(packet); + } } void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, uint8_t flags, @@ -1657,6 +1644,9 @@ void MyMesh::onTraceRecv(mesh::Packet *packet, uint32_t tag, uint32_t auth_code, } else { MESH_DEBUG_PRINTLN("onTraceRecv(), data received while app offline"); } + if (_listener) { + _listener->onTraceRecv(packet, tag, auth_code, flags, path_snrs, path_hashes, path_len); + } } uint32_t MyMesh::calcFloodTimeoutMillisFor(uint32_t pkt_airtime_millis) const { @@ -1784,10 +1774,10 @@ void MyMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { // device UI registers in BaseChatMesh's own ack table instead, so gating on // `mine` here would leave every on-device DM stuck at ✗. The UI matches the // crc against its own pending tag, so an unrelated/overheard ACK is ignored. - if (_ui) _ui->onMsgAck(ack_crc); + if (_listener) _listener->onACKRecv(ack_crc); } -MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui) +MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store) // Sized to match simple_repeater's pool (32), not the old client-only 16: with the // on-device Repeater toggle, queued retransmits (adverts/channel flood from // neighbours) can now hold packet-pool slots for their retransmit delay window. A @@ -1795,7 +1785,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe // silently drops every incoming packet — DMs and channels included — until a slot // frees up. : BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(32), tables), - _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui), _iter(0) { + _serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _listener(NULL), _iter(0) { _iter_started = false; _cli_rescue = false; for (int i = 0; i < RELAY_RING; i++) _relay[i].pending = false; @@ -1891,7 +1881,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe #endif } -void MyMesh::begin(bool has_display) { +void MyMesh::begin() { BaseChatMesh::begin(); if (!_store->loadMainIdentity(self_id)) { @@ -1950,24 +1940,9 @@ void MyMesh::begin(bool has_display) { _prefs.gps_enabled = constrain(_prefs.gps_enabled, 0, 1); // Ensure boolean 0 or 1 _prefs.gps_interval = constrain(_prefs.gps_interval, 0, 86400); // Max 24 hours -#ifdef BLE_PIN_CODE // 123456 by default - if (_prefs.ble_pin == 0) { -#ifdef DISPLAY_CLASS - if (has_display && BLE_PIN_CODE == 123456) { - StdRNG rng; - _active_ble_pin = rng.nextInt(100000, 999999); // random pin each session - } else { - _active_ble_pin = BLE_PIN_CODE; // otherwise static pin - } -#else - _active_ble_pin = BLE_PIN_CODE; // otherwise static pin -#endif - } else { - _active_ble_pin = _prefs.ble_pin; - } -#else - _active_ble_pin = 0; -#endif + // The session PIN (random when a display can show it) is resolved by + // main.cpp, which knows whether there is one -- see resolveBLEPin(). + _active_ble_pin = _prefs.ble_pin; resetContacts(); _store->loadContacts(this); @@ -2025,6 +2000,9 @@ NodePrefs *MyMesh::getNodePrefs() { uint32_t MyMesh::getBLEPin() { return _active_ble_pin; } +void MyMesh::setBLEPin(uint32_t active_pin) { + _active_ble_pin = active_pin; +} struct FreqRange { uint32_t lower_freq, upper_freq; @@ -2067,8 +2045,8 @@ bool MyMesh::setChannelLocal(uint8_t idx, const ChannelDetails& ch) { // An all-zero secret is this codebase's "empty slot" sentinel (same check // loadChannels()/saveChannels() use) -- drop anything that referenced it by // index, the same way onContactRemoved() does for contacts. - if (_ui && isAllZero(ch.channel.secret, sizeof(ch.channel.secret))) - _ui->onChannelRemoved(idx); + if (_listener && isAllZero(ch.channel.secret, sizeof(ch.channel.secret))) + _listener->onChannelRemoved(idx); return true; } @@ -2185,9 +2163,9 @@ void MyMesh::handleCmdFrame(size_t len) { // uses); resends stays 0 -- the app already owns its own resend/retry // decision, so this only drives the on-screen status, never a second, // independent auto-resend from the device itself. - if (_ui && txt_type == TXT_TYPE_PLAIN) { + if (_listener && txt_type == TXT_TYPE_PLAIN) { uint32_t ack_deadline_ms = expected_ack ? (millis() + est_timeout + 4000) : 0; - _ui->addDMMsg(recipient->id.pub_key, true, text, msg_timestamp, expected_ack, ack_deadline_ms, 0); + _listener->addDMMsg(recipient->id.pub_key, true, text, msg_timestamp, expected_ack, ack_deadline_ms, 0); } #endif } @@ -2450,9 +2428,7 @@ void MyMesh::handleCmdFrame(size_t len) { int out_len; if ((out_len = getFromOfflineQueue(out_frame)) > 0) { _serial->writeFrame(out_frame, out_len); -#ifdef DISPLAY_CLASS - if (_ui) _ui->msgRead(offline_queue_len); -#endif + if (_listener) _listener->onQueueSizeChanged(offline_queue_len); } else { out_frame[0] = RESP_CODE_NO_MORE_MESSAGES; _serial->writeFrame(out_frame, 1); @@ -2563,9 +2539,7 @@ void MyMesh::handleCmdFrame(size_t len) { writeOKFrame(); } } else if (cmd_frame[0] == CMD_REBOOT && memcmp(&cmd_frame[1], "reboot", 6) == 0) { - if (_ui) { - _ui->shutdown(true); - } else { + if (!(_listener && _listener->requestShutdown(true))) { flushDirtyContacts(); savePrefs(); board.reboot(); @@ -3150,7 +3124,7 @@ void MyMesh::handleCmdFrame(size_t len) { #ifdef ENABLE_SCREENSHOT void MyMesh::handleScreenshotRequest() { #ifdef DISPLAY_CLASS - UITask* ui_task = static_cast(getUITask()); + UITask* ui_task = static_cast(getListener()); if (!ui_task || !ui_task->hasDisplay()) { writeErrFrame(ERR_CODE_UNSUPPORTED_CMD); return; @@ -3387,9 +3361,7 @@ void MyMesh::checkCLIRescueCmd() { } } else if (strcmp(cli_command, "reboot") == 0) { - if (_ui) { - _ui->shutdown(true); - } else { + if (!(_listener && _listener->requestShutdown(true))) { flushDirtyContacts(); savePrefs(); // flush any on-device setting change not yet persisted -- see UITask::shutdown()'s comment board.reboot(); // doesn't return @@ -3470,15 +3442,6 @@ void MyMesh::loop() { if (pkt) sendZeroHop(pkt); _next_auto_advert_ms = futureMillis(_prefs.advert_auto_interval_sec * 1000UL); } - -#ifdef DISPLAY_CLASS - // hasConnection() means "a BLE companion app is connected". Not isConnected() - // (a dual interface hardcodes that true as a USB send-fallback). Drives the BT - // status indicator, pairing PIN, and the GPX-export collision warning — all - // BLE-specific. The Auto buzzer mute / message-wake use isClientConnected() - // (BLE *or* an open USB port) directly instead. - if (_ui) _ui->setHasConnection(_serial->isBLEConnected()); -#endif } bool MyMesh::advert() { diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 83aac909..e68477df 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -2,7 +2,6 @@ #include #include -#include "AbstractUITask.h" #include #include "Features.h" // FEAT_RX_POWERSAVE, used by MyMesh.cpp @@ -107,14 +106,101 @@ struct DiscoverResult { class MyMesh : public BaseChatMesh, public DataStoreHost { public: - MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL); + // Everything MyMesh tells the on-device UI goes through this one interface + // (set with setListener()); MyMesh itself has no UI concepts. The first + // block mirrors upstream meshcore-dev/MeshCore's MyMesh::Listener (PR #3431 + // and follow-ups) with identical names/signatures, so upstream merges stay + // mechanical. The second block is this fork's extensions -- events and + // device controls upstream doesn't have (yet). Every method there defaults + // to a no-op, so an upstream-style listener still works unchanged. + class Listener { + public: + // ---- upstream MyMesh::Listener ---- + virtual void onMessageRecv(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, const char* text) = 0; + virtual void onChannelMessageRecv(mesh::Packet *pkt, ChannelDetails& channel_details, const char* text) = 0; + virtual void onQueueSizeChanged(int msgcount) = 0; + virtual void onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path_len, const uint8_t* path) { } + virtual void onControlDataRecv(const mesh::Packet* pkt) { } + virtual void onChannelDataRecv(mesh::Packet *pkt, const mesh::GroupChannel &channel, uint16_t data_type, + const uint8_t *data, size_t data_len) { } + virtual void onACKRecv(uint32_t ack_crc) { } + virtual uint8_t onUnhandledRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data, + uint8_t len, uint8_t *reply) { return 0; /* unknown request type */ } + virtual void onUnhandledResponse(const ContactInfo &from, uint32_t tag, const uint8_t* data, uint8_t len) { } + virtual void onTraceRecv(mesh::Packet *pkt, uint32_t tag, uint32_t auth_code, uint8_t flags, + const uint8_t *path_snrs, const uint8_t *path_hashes, uint8_t path_len) { } + virtual void onRawDataRecv(mesh::Packet *pkt) { } + virtual ~Listener() { } - void begin(bool has_display); + // ---- Solo fork extensions ---- + // onMessageRecv() plus the signed-message author prefix (a room post's + // real author; extra_len 0 otherwise). MyMesh calls this one; the + // default forwards to the upstream method. + virtual void onMessageRecvEx(mesh::Packet *pkt, const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, + const uint8_t* extra, int extra_len, const char* text) { + onMessageRecv(pkt, from, txt_type, sender_timestamp, text); + } + // onChannelMessageRecv() plus the channel slot and sender timestamp. + virtual void onChannelMessageRecvEx(mesh::Packet *pkt, uint8_t channel_idx, ChannelDetails& channel_details, + uint32_t timestamp, const char* text) { + onChannelMessageRecv(pkt, channel_details, text); + } + // Any advert heard (contact or discover response) -- sound/notify hook. + virtual void onAdvertHeard(bool was_flood) { } + // Our own channel send (app, bot) to mirror into on-device history. + // Returns the history ring position, or -1 if none is kept. + virtual int addChannelMsg(uint8_t channel_idx, const char* text, uint32_t timestamp = 0, + const uint8_t* path = nullptr, uint8_t path_len = 0, + bool own_message = false) { return -1; } + // Mirror of our own channel post, framed "Me: " (see MessagesScreen). + // text_len < 0: text is null-terminated; otherwise only text_len bytes. + int addOwnChannelMsg(uint8_t channel_idx, const char* text, int text_len = -1, uint32_t timestamp = 0) { + char buf[MAX_TEXT_LEN + 8]; // "Me: "(4) + text(MAX_TEXT_LEN) + margin + if (text_len < 0) snprintf(buf, sizeof(buf), "Me: %s", text); + else snprintf(buf, sizeof(buf), "Me: %.*s", text_len, text); + return addChannelMsg(channel_idx, buf, timestamp, nullptr, 0, true); + } + // Arm the "relayed into mesh" tracker on ring position pos (seq: + // lastChannelRelaySeq()), and report each repeater echo heard for it. + virtual void armChannelRelay(int pos, uint32_t seq) { } + virtual void onChannelRelayed(uint32_t seq, const uint8_t* repeater_hash = nullptr, uint8_t hash_size = 0) { } + // DM history entry: incoming (path = route taken) or our own outgoing + // send from the app/bot (ack_tag/ack_deadline_ms drive the marker). + virtual void addDMMsg(const uint8_t* pub_key, bool outgoing, const char* text, uint32_t sender_timestamp = 0, + uint32_t ack_tag = 0, uint32_t ack_deadline_ms = 0, uint8_t resends = 0, + const uint8_t* path = nullptr, uint8_t path_len = 0) { } + // Results of on-device-UI-initiated requests (sendRoomLogin/sendAdminCommand). + virtual void onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) { } + virtual void onAdminReply(const uint8_t* pub_key, const char* text) { } + // A [LOC] share (pub_key set = verified DM; null = channel/room, by name). + virtual void onSharedLocation(const uint8_t* pub_key, const char* name, + int32_t lat_1e6, int32_t lon_1e6, uint32_t ts, bool verified) { } + // A contact / channel slot is gone -- drop references to it. + virtual void onContactRemoved(const uint8_t* pub_key) { } + virtual void onChannelRemoved(uint8_t channel_idx) { } + // Remote bot device actions (!gps/!buzz/!gpioN), gated by bot_actions_* prefs. + virtual void botSetGPS(bool on) { } + virtual void botBuzz(int seconds) { } + virtual bool botSetGPIO(int idx, bool on) { return false; } + virtual bool botGetGPIO(int idx, bool& is_output, bool& value) { return false; } + virtual bool botGetGPIOAnalog(int idx, int& millivolts) { return false; } + // Controlled reboot/power-off (CLI/app "reboot"): the listener flushes + // its state and restarts. Returns false if it doesn't handle it, and + // MyMesh then flushes and reboots by itself. + virtual bool requestShutdown(bool restart) { return false; } + }; + + MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); + + void begin(); + void setListener(Listener* listener) { _listener = listener; } + Listener* getListener() const { return _listener; } void startInterface(BaseSerialInterface &serial); const char *getNodeName(); NodePrefs *getNodePrefs(); uint32_t getBLEPin(); + void setBLEPin(uint32_t active_pin); void loop(); void handleCmdFrame(size_t len); @@ -176,7 +262,6 @@ public: void clearPingResult(uint32_t tag); PingResult* getPingResult(uint32_t tag); PingCallback getPingCallback() const { return _ping_callback; } - AbstractUITask* getUITask() const { return _ui; } protected: float getAirtimeBudgetFactor() const override; @@ -257,9 +342,9 @@ public: // GroupChannel&, ...) already calls trackRelaySend() unconditionally, so // lastChannelRelaySeq() is already the seq for the send just made. int mirrorOwnChannelMsg(uint8_t channel_idx, const char* text, int text_len = -1, uint32_t timestamp = 0) { - if (!_ui) return -1; - int pos = _ui->addOwnChannelMsg(channel_idx, text, text_len, timestamp); - if (pos >= 0) _ui->armChannelRelay(pos, lastChannelRelaySeq()); + if (!_listener) return -1; + int pos = _listener->addOwnChannelMsg(channel_idx, text, text_len, timestamp); + if (pos >= 0) _listener->armChannelRelay(pos, lastChannelRelaySeq()); return pos; } private: @@ -279,7 +364,7 @@ public: // 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(). + // the UI via Listener::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(); @@ -521,7 +606,7 @@ private: uint32_t pending_telemetry, pending_discovery; // pending _TELEMETRY_REQ uint32_t pending_req; // pending _BINARY_REQ BaseSerialInterface *_serial; - AbstractUITask* _ui; + Listener* _listener; ContactsIterator _iter; uint32_t _iter_filter_since; diff --git a/examples/companion_radio/MyMeshBot.h b/examples/companion_radio/MyMeshBot.h index 36f6fc5b..10486cec 100644 --- a/examples/companion_radio/MyMeshBot.h +++ b/examples/companion_radio/MyMeshBot.h @@ -154,7 +154,7 @@ void MyMesh::tryBotReplyDM(const ContactInfo& from, const char* text, uint8_t ho botDmRecord(from.id.pub_key); _bot_reply_count++; #ifdef DISPLAY_CLASS - if (_ui) _ui->addDMMsg(from.id.pub_key, true, expanded); + if (_listener) _listener->addDMMsg(from.id.pub_key, true, expanded); #endif } } @@ -243,7 +243,7 @@ void MyMesh::tryBotReplyRoom(const ContactInfo& from, const uint8_t* sender_pref _bot_last_room_reply_ms = millis(); _bot_reply_count++; #ifdef DISPLAY_CLASS - if (_ui) _ui->addDMMsg(from.id.pub_key, true, expanded); + if (_listener) _listener->addDMMsg(from.id.pub_key, true, expanded); #endif } } @@ -334,7 +334,7 @@ bool MyMesh::botCommandReply(const char* cmd, const char* arg, const char* arg2, bool off = !strcmp(arg, "off"); if (!on && !off) { snprintf(out, out_len, "gpio%d: on|off?", idx); return true; } bool is_out = false, val = false; - if (_ui && _ui->botGetGPIO(idx, is_out, val) && is_out) { + if (_listener && _listener->botGetGPIO(idx, is_out, val) && is_out) { _bot_gpio_action[idx - 1] = on ? 1 : 0; // deferred -- see applyPendingBotActions() snprintf(out, out_len, "gpio%d: %s", idx, on ? "on" : "off"); } else { @@ -344,9 +344,9 @@ bool MyMesh::botCommandReply(const char* cmd, const char* arg, const char* arg2, } int mv = 0; bool is_out = false, val = false; - if (_ui && _ui->botGetGPIOAnalog(idx, mv)) { + if (_listener && _listener->botGetGPIOAnalog(idx, mv)) { snprintf(out, out_len, "gpio%d: %dmV", idx, mv); - } else if (_ui && _ui->botGetGPIO(idx, is_out, val)) { + } else if (_listener && _listener->botGetGPIO(idx, is_out, val)) { snprintf(out, out_len, "gpio%d: %s %s", idx, is_out ? "out" : "in", val ? "on" : "off"); } else { snprintf(out, out_len, "gpio%d: off", idx); @@ -457,7 +457,7 @@ bool MyMesh::tryBotCommand(const ContactInfo& from, const char* text, uint8_t ho botDmRecord(from.id.pub_key); _bot_reply_count++; #ifdef DISPLAY_CLASS - if (_ui) _ui->addDMMsg(from.id.pub_key, true, out); + if (_listener) _listener->addDMMsg(from.id.pub_key, true, out); #endif if (_locfix_requested) startLocFix(LOCFIX_DEST_CONTACT, from.id.pub_key, 0); applyPendingBotActions(); @@ -524,7 +524,7 @@ bool MyMesh::tryBotRoomCommand(const ContactInfo& from, const uint8_t* sender_pr _bot_last_room_reply_ms = millis(); _bot_reply_count++; #ifdef DISPLAY_CLASS - if (_ui) _ui->addDMMsg(from.id.pub_key, true, out); + if (_listener) _listener->addDMMsg(from.id.pub_key, true, out); #endif if (_locfix_requested) startLocFix(LOCFIX_DEST_CONTACT, from.id.pub_key, 0); applyPendingBotActions(); @@ -541,11 +541,11 @@ bool MyMesh::tryBotRoomCommand(const ContactInfo& from, const uint8_t* sender_pr // MyMesh.h). !gps fix's startLocFix() is armed separately by the caller (it // needs the destination, which this function doesn't have). void MyMesh::applyPendingBotActions() { - if (_bot_gps_action_pending && _ui) _ui->botSetGPS(_bot_gps_action_on); - if (_bot_buzz_action_secs > 0 && _ui) _ui->botBuzz(_bot_buzz_action_secs); + if (_bot_gps_action_pending && _listener) _listener->botSetGPS(_bot_gps_action_on); + if (_bot_buzz_action_secs > 0 && _listener) _listener->botBuzz(_bot_buzz_action_secs); if (_bot_advert_action_pending) advert(); for (int i = 0; i < 4; i++) { - if (_bot_gpio_action[i] >= 0 && _ui) _ui->botSetGPIO(i + 1, _bot_gpio_action[i] != 0); + if (_bot_gpio_action[i] >= 0 && _listener) _listener->botSetGPIO(i + 1, _bot_gpio_action[i] != 0); } } @@ -578,7 +578,7 @@ void MyMesh::startLocFix(uint8_t dest_type, const uint8_t* pub_key, uint8_t chan _loc_fix.averaging_until_ms = 0; _loc_fix.deadline_ms = futureMillis(_locfix_requested_timeout_ms); _loc_fix.gps_was_on = (_prefs.gps_enabled != 0); - if (!_loc_fix.gps_was_on && _ui) _ui->botSetGPS(true); + if (!_loc_fix.gps_was_on && _listener) _listener->botSetGPS(true); } // !gps fix state machine, ticked every MyMesh::loop() while _loc_fix.active. @@ -627,7 +627,7 @@ void MyMesh::tickLocFix() { } sendLocFixResult(msg); - if (!_loc_fix.gps_was_on && _ui) _ui->botSetGPS(false); + if (!_loc_fix.gps_was_on && _listener) _listener->botSetGPS(false); _loc_fix.active = false; } @@ -659,7 +659,7 @@ void MyMesh::sendLocFixResult(const char* msg) { else botDmRecord(c->id.pub_key); _bot_reply_count++; #ifdef DISPLAY_CLASS - if (_ui) _ui->addDMMsg(c->id.pub_key, true, msg); + if (_listener) _listener->addDMMsg(c->id.pub_key, true, msg); #endif } } diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 8573e50d..e1eb191b 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -135,11 +135,7 @@ static uint32_t _atoi(const char* sp) { StdRNG fast_rng; SimpleMeshTables tables; -MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store - #ifdef DISPLAY_CLASS - , &ui_task - #endif -); +MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store); /* END GLOBAL OBJECTS */ @@ -147,6 +143,29 @@ void halt() { while (1) ; } +// Session BLE PIN, resolved after the_mesh.begin() has loaded prefs and before +// the serial interface starts advertising with it. A saved PIN wins; otherwise +// the default 123456 becomes a random per-session PIN when there's a display to +// show it on (moved here from MyMesh::begin(), as upstream did -- MyMesh has no +// display concept). +static void resolveBLEPin(bool has_display) { +#ifdef BLE_PIN_CODE // 123456 by default + if (the_mesh.getNodePrefs()->ble_pin == 0) { + if (has_display && BLE_PIN_CODE == 123456) { + StdRNG rng; + the_mesh.setBLEPin(rng.nextInt(100000, 999999)); // random pin each session + } else { + the_mesh.setBLEPin(BLE_PIN_CODE); // otherwise static pin + } + } else { + the_mesh.setBLEPin(the_mesh.getNodePrefs()->ble_pin); + } +#else + (void)has_display; + the_mesh.setBLEPin(0); +#endif +} + /* WIFI RECONNECT TRACKERS */ #if defined(ESP32) && defined(WIFI_SSID) bool wifi_needs_reconnect = false; @@ -193,7 +212,8 @@ void setup() { #endif #endif store.begin(); - the_mesh.begin( + the_mesh.begin(); + resolveBLEPin( #ifdef DISPLAY_CLASS disp != NULL #else @@ -212,7 +232,8 @@ void setup() { #elif defined(RP2040_PLATFORM) LittleFS.begin(); store.begin(); - the_mesh.begin( + the_mesh.begin(); + resolveBLEPin( #ifdef DISPLAY_CLASS disp != NULL #else @@ -238,7 +259,8 @@ void setup() { #elif defined(ESP32) SPIFFS.begin(true); store.begin(); - the_mesh.begin( + the_mesh.begin(); + resolveBLEPin( #ifdef DISPLAY_CLASS disp != NULL #else @@ -277,7 +299,8 @@ void setup() { #elif defined(SIM_PLATFORM) // sim_fs already exists/mkdir'd itself in its constructor above. store.begin(); - the_mesh.begin( + the_mesh.begin(); + resolveBLEPin( #ifdef DISPLAY_CLASS disp != NULL #else @@ -303,6 +326,7 @@ void setup() { if (disp && the_mesh.getNodePrefs()) disp->setBrightness(the_mesh.getNodePrefs()->display_brightness); ui_task.begin(disp, &sensors, the_mesh.getNodePrefs()); // still want to pass this in as dependency, as prefs might be moved + the_mesh.setListener(&ui_task); #ifdef DISPLAY_HAS_BUSY_PUMP if (disp) disp->setBusyPumpFn(pumpRadioDuringDisplayBusyWait, nullptr); #endif diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 0146ef4c..64a69473 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -1843,7 +1843,7 @@ void UITask::handlePingResult(uint32_t tag, int16_t snr_out_x4, int16_t snr_back // Static ping callback (for MyMesh) static void onPingResult(uint32_t tag, int16_t snr_out_x4, int16_t snr_back_x4, uint32_t rtt_ms) { - AbstractUITask* ui = the_mesh.getUITask(); + MyMesh::Listener* ui = the_mesh.getListener(); if (ui) { UITask* task = static_cast(ui); task->handlePingResult(tag, snr_out_x4, snr_back_x4, rtt_ms); @@ -2642,6 +2642,7 @@ void UITask::pollHallSensor() { } void UITask::loop() { + pollConnection(); // BLE link state -> hasConnection() (see AbstractUITask) // Background delivery: resend pending on-device DMs whose ACK timed out, and // finalise the ✗ marker — runs regardless of which screen is active. ((MessagesScreen*)messages_screen)->tickDmResends(); diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index a4dae5cf..ec4037c0 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -105,7 +105,8 @@ switch(t){ buzzer.play("ack:d=32,o=8,b=120:c"); break; case UIEventType::roomMessage: - case UIEventType::advertReceived: + case UIEventType::advertReceivedFlood: + case UIEventType::advertReceivedZeroHop: case UIEventType::none: default: break; @@ -314,6 +315,7 @@ void UITask::shutdown(bool restart){ } void UITask::loop() { + pollConnection(); // BLE link state -> hasConnection() (see AbstractUITask) #ifdef PIN_USER_BTN if (_userButton) { _userButton->update(); diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 22fbe401..a89b0150 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -581,6 +581,7 @@ bool UITask::isButtonPressed() const { } void UITask::loop() { + pollConnection(); // BLE link state -> hasConnection() (see AbstractUITask) char c = 0; #if UI_HAS_JOYSTICK int ev = user_btn.check();