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:
1 parent
96ef0ae905
commit
bbfe26b01d
26 files changed
+1178
-666
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
+574
-572
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user