9 Commits

6 changed files with 101 additions and 23 deletions

View File

@@ -541,6 +541,46 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe
#endif
}
void MyMesh::queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char* sender, const char* text) {
// Format message the same way as onChannelMessageRecv for BLE app sync
// This allows sent messages from device keyboard to appear in the app
int i = 0;
if (app_target_ver >= 3) {
out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV_V3;
out_frame[i++] = 0; // SNR not applicable for sent messages
out_frame[i++] = 0; // reserved1
out_frame[i++] = 0; // reserved2
} else {
out_frame[i++] = RESP_CODE_CHANNEL_MSG_RECV;
}
out_frame[i++] = channel_idx;
out_frame[i++] = 0; // path_len = 0 indicates local/sent message
out_frame[i++] = TXT_TYPE_PLAIN;
memcpy(&out_frame[i], &timestamp, 4);
i += 4;
// Format as "sender: text" like the app expects
char formatted[MAX_FRAME_SIZE];
snprintf(formatted, sizeof(formatted), "%s: %s", sender, text);
int tlen = strlen(formatted);
if (i + tlen > MAX_FRAME_SIZE) {
tlen = MAX_FRAME_SIZE - i;
}
memcpy(&out_frame[i], formatted, tlen);
i += tlen;
addToOfflineQueue(out_frame, i);
// If app is connected, send push notification
if (_serial->isConnected()) {
uint8_t frame[1];
frame[0] = PUSH_CODE_MSG_WAITING;
_serial->writeFrame(frame, 1);
}
}
uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_timestamp, const uint8_t *data,
uint8_t len, uint8_t *reply) {
if (data[0] == REQ_TYPE_GET_TELEMETRY_DATA) {
@@ -1979,4 +2019,4 @@ bool MyMesh::advert() {
} else {
return false;
}
}
}

View File

@@ -8,11 +8,11 @@
#define FIRMWARE_VER_CODE 8
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "1 Feb 2026"
#define FIRMWARE_BUILD_DATE "7 Feb 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "Meck v0.6"
#define FIRMWARE_VERSION "Meck v0.6.3"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -101,6 +101,9 @@ public:
void enterCLIRescue();
int getRecentlyHeard(AdvertPath dest[], int max_num);
// Queue a sent channel message for BLE app sync
void queueSentChannelMessage(uint8_t channel_idx, uint32_t timestamp, const char* sender, const char* text);
protected:
float getAirtimeBudgetFactor() const override;
@@ -230,4 +233,4 @@ private:
AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table
};
extern MyMesh the_mesh;
extern MyMesh the_mesh;

View File

@@ -16,7 +16,7 @@
static uint8_t composeChannelIdx = 0; // Which channel to send to
static unsigned long lastComposeRefresh = 0;
static bool composeNeedsRefresh = false;
#define COMPOSE_REFRESH_INTERVAL 250 // ms between e-ink refreshes while typing
#define COMPOSE_REFRESH_INTERVAL 600 // ms between e-ink refreshes while typing (refresh takes ~650ms)
void initKeyboard();
void handleKeyboardInput();
@@ -646,8 +646,6 @@ void drawComposeScreen() {
void sendComposedMessage() {
if (composePos == 0) return;
MESH_DEBUG_PRINTLN("Sending message to channel %d: %s", composeChannelIdx, composeBuffer);
// Get the selected channel
ChannelDetails channel;
if (the_mesh.getChannel(composeChannelIdx, channel)) {
@@ -657,20 +655,21 @@ void sendComposedMessage() {
if (the_mesh.sendGroupMessage(timestamp, channel.channel,
the_mesh.getNodePrefs()->node_name,
composeBuffer, composePos)) {
MESH_DEBUG_PRINTLN("Message sent to channel %s", channel.name);
// Add the sent message to local channel history so we can see what we sent
ui_task.addSentChannelMessage(composeChannelIdx,
the_mesh.getNodePrefs()->node_name,
composeBuffer);
// Queue message for BLE app sync (so sent messages appear in companion app)
the_mesh.queueSentChannelMessage(composeChannelIdx, timestamp,
the_mesh.getNodePrefs()->node_name,
composeBuffer);
ui_task.showAlert("Sent!", 1500);
} else {
MESH_DEBUG_PRINTLN("Failed to send message");
ui_task.showAlert("Send failed!", 1500);
}
} else {
MESH_DEBUG_PRINTLN("Could not get channel %d", composeChannelIdx);
ui_task.showAlert("No channel!", 1500);
}
}

View File

@@ -104,29 +104,53 @@ class HomeScreen : public UIScreen {
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%
// Try to get accurate SOC from BQ27220 fuel gauge first
uint8_t batteryPercentage = board.getBatteryPercent();
// Fall back to voltage-based estimation if fuel gauge returns 0
if (batteryPercentage == 0 && 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;

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;

View File

@@ -46,6 +46,14 @@ void TDeckBoard::begin() {
MESH_DEBUG_PRINTLN("TDeckBoard::begin() - GPS Serial2 initialized at %d baud", GPS_BAUDRATE);
#endif
// Disable 4G modem power (only present on 4G version, not audio version)
// This turns off the red status LED on the modem module
#ifdef MODEM_POWER_EN
pinMode(MODEM_POWER_EN, OUTPUT);
digitalWrite(MODEM_POWER_EN, LOW); // Cut power to modem
MESH_DEBUG_PRINTLN("TDeckBoard::begin() - 4G modem power disabled");
#endif
// Configure user button
pinMode(PIN_USER_BTN, INPUT);