Incorporate PR 2044 and 2141; tdpro alarm screen - needs 44khz mp3 for sounds

This commit is contained in:
pelgraine
2026-03-25 19:57:35 +11:00
parent 60dcd6a89e
commit 5dda0b686e
10 changed files with 1326 additions and 19 deletions

View File

@@ -560,12 +560,12 @@ void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, ui
recipient.name, delay_millis, _prefs.path_hash_mode, _prefs.path_hash_mode + 1);
// TODO: dynamic send_scope, depending on recipient and current 'home' Region
if (send_scope.isNull()) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
sendFlood(pkt, delay_millis, getPathHashSize());
} else {
uint16_t codes[2];
codes[0] = send_scope.calcTransportCode(pkt);
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
sendFlood(pkt, codes, delay_millis, getPathHashSize());
}
}
void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
@@ -582,12 +582,12 @@ void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pk
// TODO: have per-channel send_scope
if (send_scope.isNull()) {
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
sendFlood(pkt, delay_millis, getPathHashSize());
} else {
uint16_t codes[2];
codes[0] = send_scope.calcTransportCode(pkt);
codes[1] = 0; // REVISIT: set to 'home' Region, for sender/return region?
sendFlood(pkt, codes, delay_millis, _prefs.path_hash_mode + 1);
sendFlood(pkt, codes, delay_millis, getPathHashSize());
}
}
@@ -1490,7 +1490,7 @@ void MyMesh::handleCmdFrame(size_t len) {
if (pkt) {
if (len >= 2 && cmd_frame[1] == 1) { // optional param (1 = flood, 0 = zero hop)
unsigned long delay_millis = 0;
sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1);
sendFlood(pkt, delay_millis, getPathHashSize());
} else {
sendZeroHop(pkt);
}

View File

@@ -8,11 +8,11 @@
#define FIRMWARE_VER_CODE 10
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "23 March 2026"
#define FIRMWARE_BUILD_DATE "25 March 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "Meck v1.3"
#define FIRMWARE_VERSION "Meck v1.4"
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
@@ -150,6 +150,7 @@ protected:
uint8_t getAutoAddMaxHops() const override;
bool filterRecvFloodPacket(mesh::Packet* packet) override;
uint8_t getPathHashSize() const override { return _prefs.path_hash_mode + 1; }
void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override;
void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0) override;

View File

@@ -1771,6 +1771,19 @@ void setup() {
MESH_DEBUG_PRINTLN("setup() - BLE disabled at boot (standalone mode)");
#endif
// Alarm clock: create at boot so config is loaded, background alarm check
// works from first loop(), and the bell indicator is visible immediately.
// Audio object is NOT created here — lazy-init when alarm fires or user opens player.
#ifdef MECK_AUDIO_VARIANT
{
AlarmScreen* alarmScr = new AlarmScreen(&ui_task);
alarmScr->setSDReady(sdCardReady);
// Audio pointer set later when needed (fireAlarm or 'k'/'p' key)
ui_task.setAlarmScreen(alarmScr);
Serial.printf("ALARM: Boot init, %d alarms enabled\n", alarmScr->enabledCount());
}
#endif
Serial.printf("setup() complete — free heap: %d, largest block: %d\n",
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
MESH_DEBUG_PRINTLN("=== setup() - COMPLETE ===");
@@ -1907,6 +1920,61 @@ void loop() {
}
#endif
// Alarm clock: background alarm check + audio tick
#if defined(LilyGo_TDeck_Pro) && defined(MECK_AUDIO_VARIANT)
{
AlarmScreen* alarmScr = (AlarmScreen*)ui_task.getAlarmScreen();
if (alarmScr) {
// Service alarm audio decode (like audiobook audioTick)
alarmScr->alarmAudioTick();
if (alarmScr->isAlarmAudioActive()) {
cpuPower.setBoost();
}
// Periodic alarm check (~every 10 seconds)
static unsigned long lastAlarmCheck = 0;
if (millis() - lastAlarmCheck > ALARM_CHECK_INTERVAL_MS) {
lastAlarmCheck = millis();
uint32_t rtcNow = the_mesh.getRTCClock()->getCurrentTime();
int fireSlot = alarmScr->checkAlarms(rtcNow, the_mesh.getNodePrefs()->utc_offset_hours);
if (fireSlot >= 0 && !alarmScr->isRinging()) {
// If audiobook is playing, the alarm will take over the shared Audio*
// object. The audiobook auto-saves bookmarks every 30s, so at most
// 30s of position is lost. User can resume from audiobook player after.
AudiobookPlayerScreen* abPlayer =
(AudiobookPlayerScreen*)ui_task.getAudiobookScreen();
if (abPlayer && abPlayer->isAudioActive()) {
Serial.println("ALARM: Audiobook active — alarm taking over Audio");
}
// Ensure Audio object is shared
if (!audio) audio = new Audio();
alarmScr->setAudio(audio);
// Fire the alarm
alarmScr->fireAlarm(fireSlot);
alarmScr->setLastFiredEpoch(fireSlot, rtcNow);
// Let audio buffer fill before e-ink refresh blocks SPI
for (int i = 0; i < 50; i++) {
alarmScr->alarmAudioTick();
delay(2);
}
// Switch UI to alarm screen (ringing mode)
ui_task.gotoAlarmScreen();
// Wake display if asleep
ui_task.keepAlive();
ui_task.forceRefresh();
Serial.printf("ALARM: Fired slot %d, switched to ringing screen\n", fireSlot);
}
}
}
}
#endif
// SMS: poll for incoming messages from modem
#ifdef HAS_4G_MODEM
{
@@ -2505,6 +2573,23 @@ void handleKeyboardInput() {
Serial.printf("handleKeyboardInput: key='%c' (0x%02X) composeMode=%d\n",
key >= 32 ? key : '?', key, composeMode);
// Alarm ringing: ANY key dismisses (highest priority after lock screen)
#ifdef MECK_AUDIO_VARIANT
{
AlarmScreen* alarmScr = (AlarmScreen*)ui_task.getAlarmScreen();
if (alarmScr && alarmScr->isRinging()) {
if (key == 'z') {
alarmScr->handleInput('z'); // Snooze
} else {
alarmScr->dismiss(); // Any other key = dismiss
}
ui_task.gotoHomeScreen();
ui_task.forceRefresh();
return; // Consume the key
}
}
#endif
if (composeMode) {
// Emoji picker sub-mode
if (emojiPickerMode) {
@@ -3097,6 +3182,23 @@ void handleKeyboardInput() {
break;
#endif
#ifdef MECK_AUDIO_VARIANT
case 'k':
// Open alarm clock (screen created at boot; just ensure Audio* is available)
Serial.println("Opening alarm clock");
if (!audio) {
Serial.printf("Alarm: lazy init Audio - free heap: %d, largest block: %d\n",
ESP.getFreeHeap(), ESP.getMaxAllocHeap());
audio = new Audio();
}
{
AlarmScreen* alarmScr = (AlarmScreen*)ui_task.getAlarmScreen();
if (alarmScr) alarmScr->setAudio(audio);
}
ui_task.gotoAlarmScreen();
break;
#endif
#ifdef HAS_4G_MODEM
case 't':
// Open SMS (4G variant only)
@@ -3201,6 +3303,9 @@ void handleKeyboardInput() {
|| ui_task.isOnWebReader()
#endif
|| ui_task.isOnMapScreen()
#ifdef MECK_AUDIO_VARIANT
|| ui_task.isOnAlarmScreen()
#endif
) {
ui_task.injectKey('s'); // Pass directly for scrolling
} else {
@@ -3217,6 +3322,9 @@ void handleKeyboardInput() {
|| ui_task.isOnWebReader()
#endif
|| ui_task.isOnMapScreen()
#ifdef MECK_AUDIO_VARIANT
|| ui_task.isOnAlarmScreen()
#endif
) {
ui_task.injectKey('w'); // Pass directly for scrolling
} else {
@@ -3227,7 +3335,11 @@ void handleKeyboardInput() {
case 'a':
// Navigate left or switch channel (on channel screen)
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()) {
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()
#ifdef MECK_AUDIO_VARIANT
|| ui_task.isOnAlarmScreen()
#endif
) {
ui_task.injectKey('a'); // Pass directly for channel/contacts switching
} else {
Serial.println("Nav: Previous");
@@ -3237,7 +3349,11 @@ void handleKeyboardInput() {
case 'd':
// Navigate right or switch channel (on channel screen)
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()) {
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen() || ui_task.isOnMapScreen()
#ifdef MECK_AUDIO_VARIANT
|| ui_task.isOnAlarmScreen()
#endif
) {
ui_task.injectKey('d'); // Pass directly for channel/contacts switching
} else {
Serial.println("Nav: Next");
@@ -3515,6 +3631,24 @@ void handleKeyboardInput() {
ui_task.gotoContactsScreen();
break;
}
// Alarm screen: Q/backspace routing depends on sub-mode
#ifdef MECK_AUDIO_VARIANT
if (ui_task.isOnAlarmScreen()) {
AlarmScreen* alarmScr = (AlarmScreen*)ui_task.getAlarmScreen();
if (alarmScr && alarmScr->isRinging()) {
alarmScr->dismiss();
ui_task.gotoHomeScreen();
} else if (alarmScr && alarmScr->getMode() != AlarmScreen::ALARM_LIST) {
// In edit/picker/digit mode — pass to screen (Q = back to list, backspace = delete)
ui_task.injectKey(key);
} else {
// On alarm list — go home
Serial.println("Nav: Alarm -> Home");
ui_task.gotoHomeScreen();
}
break;
}
#endif
// Last Heard: Q goes back to home
if (ui_task.isOnLastHeardScreen()) {
Serial.println("Nav: Last Heard -> Home");
@@ -3557,6 +3691,13 @@ void handleKeyboardInput() {
ui_task.injectKey(key);
break;
}
#ifdef MECK_AUDIO_VARIANT
// Pass unhandled keys to alarm screen (digits for time entry, o for toggle)
if (ui_task.isOnAlarmScreen()) {
ui_task.injectKey(key);
break;
}
#endif
Serial.printf("Unhandled key in normal mode: '%c' (0x%02X)\n", key, key);
break;
}

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@
#include "MapScreen.h"
#endif
#include "target.h"
#if defined(LilyGo_T5S3_EPaper_Pro)
#if defined(LilyGo_T5S3_EPaper_Pro) || defined(MECK_AUDIO_VARIANT)
#include "HomeIcons.h"
#endif
#if defined(WIFI_SSID) || defined(MECK_WIFI_COMPANION)
@@ -221,6 +221,25 @@ void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts,
display.print(">>");
display.setTextSize(1); // restore
}
// ---- Alarm enabled indicator ----
// Shows a small bell icon to the left of the audio indicator
// (or battery icon if no audio playing) when any alarm is enabled.
void renderAlarmIndicator(DisplayDriver& display, int batteryLeftX) {
AlarmScreen* alarmScr = (AlarmScreen*)_task->getAlarmScreen();
if (!alarmScr || alarmScr->enabledCount() == 0) return;
// Calculate X: shift left past audio indicator if it's showing
int rightEdge = batteryLeftX;
if (_task->isAudioPlayingInBackground()) {
display.setTextSize(0);
rightEdge = rightEdge - display.getTextWidth(">>") - 2;
}
display.setColor(DisplayDriver::GREEN);
int x = rightEdge - BELL_ICON_W - 2;
display.drawXbm(x, 1, icon_bell_small, BELL_ICON_W, BELL_ICON_H);
}
#endif
CayenneLPP sensors_lpp;
@@ -297,6 +316,9 @@ public:
// audio background playback indicator (>> icon next to battery)
renderAudioIndicator(display, battLeftX);
// alarm enabled indicator (AL icon, left of audio or battery)
renderAlarmIndicator(display, battLeftX);
#else
renderBatteryIndicator(display, _task->getBattMilliVolts());
#endif
@@ -458,10 +480,14 @@ public:
display.drawTextCentered(display.width() / 2, y, "[T] Phone [B] Browser ");
#elif defined(HAS_4G_MODEM)
display.drawTextCentered(display.width() / 2, y, "[T] Phone [F] Discover ");
#elif defined(MECK_AUDIO_VARIANT) && defined(MECK_WEB_READER)
display.drawTextCentered(display.width() / 2, y, "[P] Audiobooks [B] Browser ");
#elif defined(MECK_AUDIO_VARIANT)
display.drawTextCentered(display.width() / 2, y, "[P] Audiobooks [F] Discover ");
display.drawTextCentered(display.width() / 2, y, "[P] Audiobooks [K] Alarm ");
y += 10;
#ifdef MECK_WEB_READER
display.drawTextCentered(display.width() / 2, y, "[B] Browser [F] Discover ");
#else
display.drawTextCentered(display.width() / 2, y, "[F] Discover ");
#endif
#elif defined(MECK_WEB_READER)
display.drawTextCentered(display.width() / 2, y, "[B] Browser ");
#else
@@ -1208,6 +1234,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
lock_screen = new LockScreen(this, &rtc_clock, node_prefs);
#endif
audiobook_screen = nullptr; // Created and assigned from main.cpp if audio hardware present
#ifdef MECK_AUDIO_VARIANT
alarm_screen = nullptr; // Created and assigned from main.cpp if audio hardware present
#endif
#ifdef HAS_4G_MODEM
sms_screen = new SMSScreen(this);
#endif
@@ -2509,6 +2538,22 @@ void UITask::gotoAudiobookPlayer() {
#endif
}
#ifdef MECK_AUDIO_VARIANT
void UITask::gotoAlarmScreen() {
if (alarm_screen == nullptr) return;
AlarmScreen* alarmScr = (AlarmScreen*)alarm_screen;
if (_display != NULL) {
alarmScr->enter(*_display);
}
setCurrScreen(alarm_screen);
if (_display != NULL && !_display->isOn()) {
_display->turnOn();
}
_auto_off = millis() + AUTO_OFF_MILLIS;
_next_refresh = 100;
}
#endif
#ifdef HAS_4G_MODEM
void UITask::gotoSMSScreen() {
SMSScreen* smsScr = (SMSScreen*)sms_screen;

View File

@@ -30,6 +30,10 @@
#include "WebReaderScreen.h"
#endif
#ifdef MECK_AUDIO_VARIANT
#include "AlarmScreen.h"
#endif
#if defined(LilyGo_T5S3_EPaper_Pro)
#include "VirtualKeyboard.h"
#endif
@@ -82,6 +86,9 @@ class UITask : public AbstractUITask {
UIScreen* notes_screen; // Notes editor screen
UIScreen* settings_screen; // Settings/onboarding screen
UIScreen* audiobook_screen; // Audiobook player screen (null if not available)
#ifdef MECK_AUDIO_VARIANT
UIScreen* alarm_screen; // Alarm clock screen (audio variant only)
#endif
#ifdef HAS_4G_MODEM
UIScreen* sms_screen; // SMS messaging screen (4G variant only)
#endif
@@ -172,6 +179,9 @@ public:
void gotoSettingsScreen(); // Navigate to settings
void gotoOnboarding(); // Navigate to settings in onboarding mode
void gotoAudiobookPlayer(); // Navigate to audiobook player
#ifdef MECK_AUDIO_VARIANT
void gotoAlarmScreen(); // Navigate to alarm clock
#endif
void gotoRepeaterAdmin(int contactIdx); // Navigate to repeater admin
void gotoRepeaterAdminDirect(int contactIdx); // Auto-login admin (L key from conversation)
void gotoDiscoveryScreen(); // Navigate to node discovery scan
@@ -221,6 +231,9 @@ public:
bool isOnNotesScreen() const { return curr == notes_screen; }
bool isOnSettingsScreen() const { return curr == settings_screen; }
bool isOnAudiobookPlayer() const { return curr == audiobook_screen; }
#ifdef MECK_AUDIO_VARIANT
bool isOnAlarmScreen() const { return curr == alarm_screen; }
#endif
bool isOnRepeaterAdmin() const { return curr == repeater_admin; }
bool isOnDiscoveryScreen() const { return curr == discovery_screen; }
bool isOnLastHeardScreen() const { return curr == last_heard_screen; }
@@ -288,6 +301,10 @@ public:
UIScreen* getSettingsScreen() const { return settings_screen; }
UIScreen* getAudiobookScreen() const { return audiobook_screen; }
void setAudiobookScreen(UIScreen* s) { audiobook_screen = s; }
#ifdef MECK_AUDIO_VARIANT
UIScreen* getAlarmScreen() const { return alarm_screen; }
void setAlarmScreen(UIScreen* s) { alarm_screen = s; }
#endif
UIScreen* getRepeaterAdminScreen() const { return repeater_admin; }
UIScreen* getDiscoveryScreen() const { return discovery_screen; }
UIScreen* getLastHeardScreen() const { return last_heard_screen; }

View File

@@ -46,4 +46,18 @@ static const uint8_t icon_notepad[] PROGMEM = {
static const uint8_t icon_search[] PROGMEM = {
0x3C,0x00, 0x42,0x00, 0x81,0x00, 0x81,0x00, 0x81,0x00, 0x42,0x00,
0x3C,0x00, 0x03,0x00, 0x01,0x80, 0x00,0xC0, 0x00,0x40, 0x00,0x00,
};
// ⏰ Alarm Clock (AlarmScreen) — 12x12 home tile icon
static const uint8_t icon_alarm[] PROGMEM = {
0x40,0x40, 0x9E,0x20, 0x20,0x80, 0x44,0x40, 0x44,0x40, 0x46,0x40,
0x40,0x40, 0x20,0x80, 0x1F,0x00, 0x00,0x00, 0x20,0x40, 0x40,0x20,
};
// 🔔 Bell — 7x8 status bar indicator (alarm enabled)
// MSB-first, 1 byte per row
#define BELL_ICON_W 7
#define BELL_ICON_H 8
static const uint8_t icon_bell_small[] PROGMEM = {
0x10, 0x38, 0x7C, 0x7C, 0x7C, 0xFE, 0x00, 0x10,
};

View File

@@ -36,7 +36,7 @@ uint32_t Dispatcher::getCADFailRetryDelay() const {
return 200;
}
uint32_t Dispatcher::getCADFailMaxDuration() const {
return 4000; // 4 seconds
return 6000; // 6 seconds
}
void Dispatcher::loop() {
@@ -273,12 +273,16 @@ void Dispatcher::checkSend() {
outbound_start = _ms->getMillis();
bool success = _radio->startSendRaw(raw, len);
if (!success) {
MESH_DEBUG_PRINTLN("%s Dispatcher::loop(): ERROR: send start failed!", getLogDateTime());
MESH_DEBUG_PRINTLN("%s Dispatcher::checkSend(): ERROR: send start failed!", getLogDateTime());
logTxFail(outbound, outbound->getRawLength());
releasePacket(outbound); // return to pool
// re-queue instead of dropping so the packet gets another chance
int retry_delay = getCADFailRetryDelay();
unsigned long retry_time = futureMillis(retry_delay);
_mgr->queueOutbound(outbound, 0, retry_time);
outbound = NULL;
next_tx_time = retry_time;
return;
}
outbound_expiry = futureMillis(max_airtime);

View File

@@ -10,10 +10,10 @@
#endif
void BaseChatMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis) {
sendFlood(pkt, delay_millis);
sendFlood(pkt, delay_millis, getPathHashSize());
}
void BaseChatMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) {
sendFlood(pkt, delay_millis);
sendFlood(pkt, delay_millis, getPathHashSize());
}
mesh::Packet* BaseChatMesh::createSelfAdvert(const char* name) {

View File

@@ -130,6 +130,7 @@ protected:
virtual void onContactResponse(const ContactInfo& contact, const uint8_t* data, uint8_t len) = 0;
virtual void handleReturnPathRetry(const ContactInfo& contact, const uint8_t* path, uint8_t path_len);
virtual uint8_t getPathHashSize() const = 0;
virtual void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0);
virtual void sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis=0);