Skip to content

Abstract UI overhaul - #3431

Open
ripplebiz wants to merge 5 commits into
devfrom
abstract-ui-overhaul
Open

ripplebiz wants to merge 5 commits into
devfrom
abstract-ui-overhaul

Conversation

@ripplebiz

@ripplebiz ripplebiz commented Sep 17, 2026

Copy link
Copy Markdown
Member

The companion UI code has been getting slapped around for a long time, and has become a bit of a mess, so this PR is attempting to re-structure and cleanup some of the dependencies, and also start paving the way for UI forks which are trying to get more ambitious.

The main structural change is to remove ALL UI concepts from the MyMesh class. This involves the introduction of the MyMesh::Listener interface, so that an external component (eg. the UI) can register as a listener for various events like receiving a contact message, etc.

AbstractUITask has been refactored to be a MyMesh::Listener, so all the UI implementations can just override various interface methods to manage more advanced in-firmware functionality. Have a look at the refactored DISCOVER feature in /ui-new on generally how other standalone UI features should be structured.

This was an opt-out UI feature by defining the UI_NO_DISCOVER_SCREEN, but that is now refactored to UI_DISCOVER_SCREEN (must be zero if you Don't want that feature).

Towards Standalone UI

There is still a fair ways to go to properly abstracting things to support a standalone/companion hybrid firmware. There are a LOT of forks out there which already 'try' to do this, but are quite messy, and have to resort to various contortions primarily because: The companion firmware was NOT deigned to be for standalone devices.

This refactor is a step towards getting to this kind of hybrid firmware, but some things need to be pointed out, like Request/Response handling. The companion still has to manage handling requests/responses which are originated by the app and only IF unhandled, passes responses to the onUnhandledResponse() Listener method. So, in your fork, if you want to introduce a standalone feature, like login, then you need to initiate the sending of the request using methods in the BaseChatMesh class, and save the 32-bit 'tag' yourself, and check for that tag in onUnhandledResponse().

If a fork wants to automatically respond the some Request packet (like how the companion responds to Telemetry requests), then you need to override the onUnhandledRequest() method. This must place the response packet data payload in the 'reply' buffer, and then return the byte length of the response data (or 0 if not handled).

@liamcottle

liamcottle commented Sep 17, 2026

Copy link
Copy Markdown
Member

I've tested this PR with companion_radio_ble firmware on these devices.

  • Heltec v3
  • Wio Tracker L1 Pro

Looks to be working as expected. Tested message queue counters incrementing/decrementing while connected/disconnected, and the existing discover UI to find nearby repeaters is still working.

The wio-e5-mini_repeater test is failing for an unrelated issue.

MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui=NULL);
class Listener {
public:
virtual void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) = 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit lossy for clients that want to do more with the messages than previewing. Could we also get mesh::Packet* and extra/extra_len here? With this interface, I think standaolne uis would still need to tap into mymesh.

class Listener {
public:
virtual void onMessageRecv(const ContactInfo &from, uint8_t txt_type, uint32_t sender_timestamp, uint8_t path_len, const char* text) = 0;
virtual void onChannelMessageRecv(ChannelDetails& channel_details, uint8_t path_len, const char* text) = 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment for this one. Having the pkt would be useful

@weebl2000 weebl2000 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good - a nit here and there


void UITask::onControlDataRecv(const mesh::Packet* packet) {
#if UI_DISCOVER_SCREEN
if (packet->payload_len >= 12 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleDiscoverResponse reads payload[6..13]memcpy(d->pubkey_prefix, &packet->payload[6], 8) and lookupContactByPubKey(&packet->payload[6], 8) — so this needs payload_len >= 14, not >= 12. simple_repeater/MyMesh.cpp builds the prefix-only response as 6 + 8 = 14 bytes and validates >= 6 + PUB_KEY_SIZE on its own receive path.

With a truncated or hand-crafted 12-13 byte DISCOVER_RESP whose tag matches, the discovery screen lists an entry whose last 1-2 prefix bytes are stale data left in the shared payload buffer by the previous packet. Not memory-unsafe (still inside payload[MAX_PACKET_PAYLOAD]), just garbage shown to the user.

Worth noting the >= 12 was inherited from the old code, but the old check was dead — payload[0] & 0xF0 != CTL_TYPE_NODE_DISCOVER_RESP parses as payload[0] & 1 by C precedence, so the whole condition was broken. Nice catch adding the parens; this is the first release where the length guard actually runs.

Suggested change
if (packet->payload_len >= 12 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP) {
if (packet->payload_len >= 14 && (packet->payload[0] & 0xF0) == CTL_TYPE_NODE_DISCOVER_RESP) {

Comment on lines +218 to +225
disc_nodes_count = 0;
mesh::Packet* req = the_mesh.createControlData(cmd_bytes, sizeof(cmd_bytes));
if (req) {
the_mesh.sendZeroHop(req);
discovery_req_time = millis();
return true;
}
return false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resetting only once the packet is actually allocated keeps the old results on screen when the send fails.

Suggested change
disc_nodes_count = 0;
mesh::Packet* req = the_mesh.createControlData(cmd_bytes, sizeof(cmd_bytes));
if (req) {
the_mesh.sendZeroHop(req);
discovery_req_time = millis();
return true;
}
return false;
mesh::Packet* req = the_mesh.createControlData(cmd_bytes, sizeof(cmd_bytes));
if (req) {
disc_nodes_count = 0;
the_mesh.sendZeroHop(req);
discovery_req_time = millis();
return true;
}
return false;

virtual void onUnhandledResponse(const ContactInfo &from, uint32_t tag, const uint8_t* data, uint8_t len) { }
virtual void onTraceRecv(mesh::Packet *packet, 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 *packet) { }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Listener is a new public extension point aimed at third-party forks; a fork that heap-allocates its listener and deletes through a MyMesh::Listener* hits UB. Nothing in-tree breaks today (both UITask instances are file-scope globals), but one vtable slot removes the trap.

Suggested change
virtual void onRawDataRecv(mesh::Packet *packet) { }
virtual void onRawDataRecv(mesh::Packet *packet) { }
virtual ~Listener() { }

memcpy(&out_frame[i], &data[4], len - 4);
i += (len - 4);
_serial->writeFrame(out_frame, i);
} else if (_listener && len > 4) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} else if (_listener && len > 4) {
} else if (_listener && len >= 4) {

Comment on lines +25 to +27
#ifndef UI_DISCOVER_SCREEN
#define UI_DISCOVER_SCREEN 1
#endif

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add guard to make the migration fail loud if not set.

Suggested change
#ifndef UI_DISCOVER_SCREEN
#define UI_DISCOVER_SCREEN 1
#endif
#ifdef UI_NO_DISCOVER_SCREEN
#error "UI_NO_DISCOVER_SCREEN is obsolete - use -D UI_DISCOVER_SCREEN=0 instead"
#endif
#ifndef UI_DISCOVER_SCREEN
#define UI_DISCOVER_SCREEN 1
#endif

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants