Compare commits

..

5 Commits

Author SHA1 Message Date
pelgraine
3ac5570ebb Repeater acks for sent messages in Sent message popup now included 2026-02-08 00:27:13 +11:00
pelgraine
e194f6d307 Fixed render battery indicator so it uses the same linear mapping for the UI as the BLE app 2026-02-07 20:42:32 +11:00
pelgraine
af9f41a541 Updated version and date in mymesh 2026-02-07 16:24:10 +11:00
pelgraine
0a746cdca5 Merge branch 'main' into dev 2026-02-07 16:22:51 +11:00
pelgraine
3a5c48f440 "Battery UI changes - percentage display and icon size" 2026-02-07 16:20:33 +11:00
6 changed files with 104 additions and 20 deletions

View File

@@ -43,4 +43,6 @@ public:
virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0;
virtual void notify(UIEventType t = UIEventType::none) = 0;
virtual void loop() = 0;
};
virtual void showAlert(const char* text, int duration_millis) {}
virtual void forceRefresh() {}
};

View File

@@ -446,9 +446,37 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe
}
bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
// REVISIT: try to determine which Region (from transport_codes[1]) that Sender is indicating for replies/responses
// if unknown, fallback to finding Region from transport_codes[0], the 'scope' used by Sender
return false;
// Check if this incoming flood packet is a repeat of a message we recently sent
if (packet->payload_len >= SENT_FINGERPRINT_SIZE) {
unsigned long now = millis();
for (int i = 0; i < SENT_TRACK_SIZE; i++) {
SentMsgTrack* t = &_sent_track[i];
if (!t->active) continue;
// Expire old entries
if ((now - t->sent_millis) > SENT_TRACK_EXPIRY_MS) {
t->active = false;
continue;
}
// Compare payload fingerprint
if (memcmp(packet->payload, t->fingerprint, SENT_FINGERPRINT_SIZE) == 0) {
t->repeat_count++;
MESH_DEBUG_PRINTLN("SentTrack: heard repeat #%d (SNR=%.1f)", t->repeat_count, packet->getSNR());
#ifdef DISPLAY_CLASS
if (_ui) {
char buf[40];
snprintf(buf, sizeof(buf), "Sent! (%d)", t->repeat_count);
_ui->showAlert(buf, 2000); // show/extend alert with updated count
}
#endif
break; // found match, no need to check other entries
}
}
}
return false; // never filter — let normal processing continue
}
void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
@@ -463,6 +491,17 @@ void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, ui
}
}
void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
// Capture payload fingerprint for repeat tracking before sending
if (pkt->payload_len >= SENT_FINGERPRINT_SIZE) {
SentMsgTrack* t = &_sent_track[_sent_track_idx];
memcpy(t->fingerprint, pkt->payload, SENT_FINGERPRINT_SIZE);
t->repeat_count = 0;
t->sent_millis = millis();
t->active = true;
_sent_track_idx = (_sent_track_idx + 1) % SENT_TRACK_SIZE;
MESH_DEBUG_PRINTLN("SentTrack: captured fingerprint for channel msg");
}
// TODO: have per-channel send_scope
if (send_scope.isNull()) {
sendFlood(pkt, delay_millis);
@@ -826,6 +865,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
dirty_contacts_expiry = 0;
memset(advert_paths, 0, sizeof(advert_paths));
memset(send_scope.key, 0, sizeof(send_scope.key));
memset(_sent_track, 0, sizeof(_sent_track));
_sent_track_idx = 0;
// defaults
memset(&_prefs, 0, sizeof(_prefs));

View File

@@ -8,11 +8,11 @@
#define FIRMWARE_VER_CODE 8
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "4 Feb 2026"
#define FIRMWARE_BUILD_DATE "7 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "Meck v0.6.2"
#define FIRMWARE_VERSION "Meck v0.6.4"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -231,6 +231,19 @@ private:
#define ADVERT_PATH_TABLE_SIZE 16
AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table
// Sent message repeat tracking
#define SENT_TRACK_SIZE 4
#define SENT_FINGERPRINT_SIZE 12
#define SENT_TRACK_EXPIRY_MS 30000 // stop tracking after 30 seconds
struct SentMsgTrack {
uint8_t fingerprint[SENT_FINGERPRINT_SIZE];
uint8_t repeat_count;
unsigned long sent_millis;
bool active;
};
SentMsgTrack _sent_track[SENT_TRACK_SIZE];
int _sent_track_idx; // next slot in circular buffer
};
extern MyMesh the_mesh;

View File

@@ -103,30 +103,52 @@ class HomeScreen : public UIScreen {
AdvertPath recent[UI_RECENT_LIST_SIZE];
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
// Convert millivolts to percentage
const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V)
const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V)
int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts);
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) {
// Use voltage-based estimation to match BLE app readings
uint8_t batteryPercentage = 0;
if (batteryMilliVolts > 0) {
const int minMilliVolts = 3000;
const int maxMilliVolts = 4200;
int pct = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts);
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
batteryPercentage = (uint8_t)pct;
}
// battery icon
int iconWidth = 22;
int iconHeight = 8;
int iconX = display.width() - iconWidth - 5; // Position the icon near the top-right corner
int iconY = 0;
display.setColor(DisplayDriver::GREEN);
// battery icon dimensions (smaller to match tiny percentage text)
int iconWidth = 16;
int iconHeight = 6;
// measure percentage text width to position icon + text together at right edge
display.setTextSize(0);
char pctStr[5];
sprintf(pctStr, "%d%%", batteryPercentage);
uint16_t textWidth = display.getTextWidth(pctStr);
// layout: [icon 16px][cap 2px][gap 2px][text][margin 2px]
int totalWidth = iconWidth + 2 + 2 + textWidth + 2;
int iconX = display.width() - totalWidth;
int iconY = 0; // vertically align with node name text
// battery outline
display.drawRect(iconX, iconY, iconWidth, iconHeight);
// battery "cap"
display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 3, iconHeight / 2);
display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 2, iconHeight / 2);
// fill the battery based on the percentage
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100;
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4);
// draw percentage text after the battery cap, offset upward to center with icon
// (setCursor adds +5 internally for baseline, so compensate for the tiny font)
int textX = iconX + iconWidth + 2 + 2; // after cap + gap
int textY = iconY - 3; // offset up to vertically center with icon
display.setCursor(textX, textY);
display.print(pctStr);
display.setTextSize(1); // restore default text size
}
CayenneLPP sensors_lpp;
@@ -588,6 +610,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
void UITask::showAlert(const char* text, int duration_millis) {
strcpy(_alert, text);
_alert_expiry = millis() + duration_millis;
_next_refresh = millis() + 100; // trigger re-render to show updated text
}
void UITask::notify(UIEventType t) {

View File

@@ -75,7 +75,8 @@ public:
void gotoHomeScreen() { setCurrScreen(home); }
void gotoChannelScreen(); // Navigate to channel message screen
void showAlert(const char* text, int duration_millis);
void showAlert(const char* text, int duration_millis) override;
void forceRefresh() override { _next_refresh = 100; }
int getMsgCount() const { return _msgcount; }
bool hasDisplay() const { return _display != NULL; }
bool isButtonPressed() const;

View File

@@ -84,6 +84,10 @@ void GxEPDDisplay::startFrame(Color bkg) {
void GxEPDDisplay::setTextSize(int sz) {
display_crc.update<int>(sz);
switch(sz) {
case 0: // Tiny - built-in 6x8 pixel font
display.setFont(NULL);
display.setTextSize(1);
break;
case 1: // Small - use 9pt (was 9pt)
display.setFont(&FreeSans9pt7b);
break;