Separate Companion message history from pending inbox

Default to retained history with a separate pending count, and add persistent display.inbox CLI modes for history, pending, and unread. Preserve browsing while clients are connected and show the active USB or Bluetooth connection.

Fixes #5
This commit is contained in:
mikecarper committed 2026-09-10 21:42:57 -07:00
1 parent 96ef0ae905
commit bbfe26b01d
26 files changed
+1178 -666

No files matched your search

+5 -1
View File
@@ -119,10 +119,14 @@ public:
}
bool isBluetoothConnected() const {
return isInterfaceConnected(InterfaceType::Bluetooth);
}
bool isInterfaceConnected(InterfaceType type) const {
if (!_enabled) return false;
for (auto iface : _interfaces) {
if (iface.instance && iface.type == InterfaceType::Bluetooth
if (iface.instance && iface.type == type
&& iface.instance->isEnabled() && iface.instance->isConnected()) {
return true;
}
+1
View File
@@ -23,6 +23,7 @@ static const char* const WC_ALLOWED_SET_KEYS[] = {
"cad", "radio.rxgain", "radio.fem.rxgain", "radio.rxps", "powersaving",
"repeat",
"display.mode", "display.timeout", "display.usb.mode", "display.usb.timeout",
"display.inbox",
"advert.interval", "flood.advert.interval",
"flood.max", "flood.max.advert", "flood.max.unscoped", "loop.detect",
// MQTTPrefs (WiFi / MQTT / misc observer)
File diff suppressed because it is too large. Load diff
+29 -1
View File
@@ -84,6 +84,8 @@ class CompanionMessageHistory {
public:
struct Entry {
uint64_t heard_millis;
int queue_index;
bool unread;
int channel_idx;
char channel_name[32];
char origin[62];
@@ -133,12 +135,14 @@ public:
void add(uint64_t heard_millis, int channel_idx,
const char* channel_name, const char* origin,
const char* message) {
const char* message, int queue_index = -1) {
_head = (_head + 1) % Capacity;
if (_count < Capacity) ++_count;
Entry& entry = _entries[_head];
entry.heard_millis = heard_millis;
entry.queue_index = queue_index;
entry.unread = true;
entry.channel_idx = channel_idx;
copyField(entry.channel_name, sizeof(entry.channel_name), channel_name);
copyField(entry.origin, sizeof(entry.origin), origin);
@@ -151,6 +155,30 @@ public:
return &_entries[index];
}
// Queue positions also account for non-display frames and channel eviction.
// Downloading to an app never marks a message as read on the radio.
void queueRemoved(int index) {
if (index < 0) return;
for (size_t age = 0; age < _count; ++age) {
Entry& entry = _entries[(_head + Capacity - age) % Capacity];
if (entry.queue_index == index) entry.queue_index = -1;
else if (entry.queue_index > index) --entry.queue_index;
}
}
void markRead(const Entry* selected) {
for (size_t age = 0; age < _count; ++age) {
Entry& entry = _entries[(_head + Capacity - age) % Capacity];
if (&entry == selected) { entry.unread = false; return; }
}
}
size_t unreadCount() const {
size_t count = 0;
for (size_t age = 0; age < _count; ++age) if (newest(age)->unread) ++count;
return count;
}
static bool sameThread(const Entry& left, const Entry& right) {
if (left.channel_idx >= 0 || right.channel_idx >= 0) {
return left.channel_idx >= 0 && right.channel_idx >= 0
+10
View File
@@ -7,6 +7,15 @@ namespace mesh { namespace ui {
enum class DisplayMode : uint8_t { Off, On, Button, Pairing, ButtonPairing, Automatic };
enum class DisplayWake : uint8_t { Boot, Button, Message };
enum class DisplayInboxMode : uint8_t { History, Pending, Unread };
inline const char* displayInboxModeName(DisplayInboxMode mode) {
switch (mode) {
case DisplayInboxMode::Pending: return "pending";
case DisplayInboxMode::Unread: return "unread";
default: return "history";
}
}
struct DisplayPowerProfile {
DisplayMode mode = DisplayMode::Button;
@@ -15,6 +24,7 @@ struct DisplayPowerProfile {
struct DisplayPowerPrefs {
DisplayPowerProfile battery;
DisplayPowerProfile usb;
DisplayInboxMode inbox = DisplayInboxMode::History;
};
inline DisplayPowerPrefs& displayPowerPrefs() {
+29 -7
View File
@@ -12,12 +12,12 @@ const char* const temporary = "/display_prefs.tmp";
// Explicit bytes avoid compiler padding and preserve a small fixed format.
void encode(const DisplayPowerPrefs& prefs, uint8_t (&data)[12]) {
data[0] = 'D'; data[1] = 'P'; data[2] = 1;
data[0] = 'D'; data[1] = 'P'; data[2] = 2;
data[3] = uint8_t(prefs.battery.mode);
data[4] = prefs.battery.seconds & 255; data[5] = prefs.battery.seconds >> 8;
data[6] = uint8_t(prefs.usb.mode);
data[7] = prefs.usb.seconds & 255; data[8] = prefs.usb.seconds >> 8;
data[9] = 0; data[10] = 0; data[11] = 0;
data[9] = uint8_t(prefs.inbox); data[10] = 0; data[11] = 0;
for (unsigned i = 0; i < 11; ++i) data[11] ^= data[i];
}
bool readPrefs(const char* name, DisplayPowerPrefs& prefs) {
@@ -37,9 +37,16 @@ bool readPrefs(const char* name, DisplayPowerPrefs& prefs) {
prefs.battery.seconds = uint16_t(data[4]) | (uint16_t(data[5]) << 8);
prefs.usb.mode = DisplayMode(data[6]);
prefs.usb.seconds = uint16_t(data[7]) | (uint16_t(data[8]) << 8);
prefs.inbox = data[2] == 1 ? DisplayInboxMode::History : DisplayInboxMode(data[9]);
encode(prefs, expected);
if (data[2] == 1) {
// Version 1 reserved byte 9 and had no inbox preference.
expected[2] = 1;
expected[11] ^= 0x02 ^ 0x01;
}
return memcmp(data, expected, sizeof(data)) == 0
&& validDisplayProfile(prefs.battery) && validDisplayProfile(prefs.usb);
&& validDisplayProfile(prefs.battery) && validDisplayProfile(prefs.usb)
&& uint8_t(prefs.inbox) <= uint8_t(DisplayInboxMode::Unread);
}
bool savePrefs(const DisplayPowerPrefs& prefs) {
if (!settings_fs) return false;
@@ -106,14 +113,15 @@ void migrateLegacyDisplayTimeout(uint16_t seconds) {
savePrefs(prefs);
}
bool handleDisplayPowerCommand(const char* command, char* reply, size_t reply_size) {
bool handleDisplayPowerCommand(const char* command, char* reply, size_t reply_size,
bool inbox_supported) {
const bool get = strncmp(command, "get ", 4) == 0;
const bool set = strncmp(command, "set ", 4) == 0;
if (!get && !set) return false;
const char* key = command + 4;
const char* keys[] = {"display.mode", "display.timeout", "display.usb.mode", "display.usb.timeout"};
const char* keys[] = {"display.mode", "display.timeout", "display.usb.mode", "display.usb.timeout", "display.inbox"};
int index = -1;
for (int i = 0; i < 4; ++i) {
for (int i = 0; i < 5; ++i) {
const size_t len = strlen(keys[i]);
if (strncmp(key, keys[i], len) == 0 && (key[len] == 0 || key[len] == ' ')) {
index = i; break;
@@ -121,20 +129,34 @@ bool handleDisplayPowerCommand(const char* command, char* reply, size_t reply_si
}
if (index < 0) return false;
#ifndef MESHCORE_HAS_REAL_DISPLAY
(void)inbox_supported;
snprintf(reply, reply_size, "Error: display unsupported");
return true;
#else
auto candidate = displayPowerPrefs();
auto& profile = index < 2 ? candidate.battery : candidate.usb;
const char* value = key + strlen(keys[index]);
if (index == 4 && !inbox_supported) {
snprintf(reply, reply_size, "Error: inbox modes unsupported");
return true;
}
if (get) {
if (*value) snprintf(reply, reply_size, "Error: unexpected argument");
else if (index == 4) snprintf(reply, reply_size, "> %s", displayInboxModeName(candidate.inbox));
else if (index & 1) snprintf(reply, reply_size, "> %u", unsigned(profile.seconds));
else snprintf(reply, reply_size, "> %s", displayModeName(profile.mode));
return true;
}
while (*value == ' ') ++value;
if (index & 1) {
if (index == 4) {
unsigned mode = 0;
while (mode <= unsigned(DisplayInboxMode::Unread)
&& strcmp(value, displayInboxModeName(DisplayInboxMode(mode))) != 0) ++mode;
if (mode > unsigned(DisplayInboxMode::Unread)) {
snprintf(reply, reply_size, "Error: use history|pending|unread"); return true;
}
candidate.inbox = DisplayInboxMode(mode);
} else if (index & 1) {
uint32_t seconds = 0;
bool valid = *value != 0;
for (const char* p = value; *p && valid; ++p) {
+2 -1
View File
@@ -8,7 +8,8 @@ namespace mesh { namespace ui {
// Separate, versioned file: existing node and MQTT preference layouts stay
// readable by older firmware. Returns true when a saved profile was loaded.
bool loadDisplayPowerSettings(FILESYSTEM* fs, bool pairing_supported);
bool handleDisplayPowerCommand(const char* command, char* reply, size_t reply_size);
bool handleDisplayPowerCommand(const char* command, char* reply, size_t reply_size,
bool inbox_supported = false);
bool displayPairingSupported();
void migrateLegacyDisplayTimeout(uint16_t seconds);