mirror of
https://github.com/meshtastic/firmware.git
synced 2025-06-08 22:22:05 +00:00
trunk
This commit is contained in:
parent
763c80e571
commit
43aa906017
@ -9,10 +9,10 @@
|
|||||||
#include "RadioLibInterface.h"
|
#include "RadioLibInterface.h"
|
||||||
#include "buzz.h"
|
#include "buzz.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "modules/ExternalNotificationModule.h"
|
|
||||||
#include "modules/CannedMessageModule.h"
|
#include "modules/CannedMessageModule.h"
|
||||||
|
#include "modules/ExternalNotificationModule.h"
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
#ifdef ARCH_PORTDUINO
|
#ifdef ARCH_PORTDUINO
|
||||||
#include "platform/portduino/PortduinoGlue.h"
|
#include "platform/portduino/PortduinoGlue.h"
|
||||||
#endif
|
#endif
|
||||||
@ -27,7 +27,7 @@
|
|||||||
using namespace concurrency;
|
using namespace concurrency;
|
||||||
|
|
||||||
ButtonThread *buttonThread; // Declared extern in header
|
ButtonThread *buttonThread; // Declared extern in header
|
||||||
extern CannedMessageModule* cannedMessageModule;
|
extern CannedMessageModule *cannedMessageModule;
|
||||||
volatile ButtonThread::ButtonEventType ButtonThread::btnEvent = ButtonThread::BUTTON_EVENT_NONE;
|
volatile ButtonThread::ButtonEventType ButtonThread::btnEvent = ButtonThread::BUTTON_EVENT_NONE;
|
||||||
|
|
||||||
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN)
|
#if defined(BUTTON_PIN) || defined(ARCH_PORTDUINO) || defined(USERPREFS_BUTTON_PIN)
|
||||||
@ -122,7 +122,7 @@ void ButtonThread::switchPage()
|
|||||||
{
|
{
|
||||||
// Prevent screen switch if CannedMessageModule is focused and intercepting input
|
// Prevent screen switch if CannedMessageModule is focused and intercepting input
|
||||||
#if HAS_SCREEN
|
#if HAS_SCREEN
|
||||||
extern CannedMessageModule* cannedMessageModule;
|
extern CannedMessageModule *cannedMessageModule;
|
||||||
|
|
||||||
if (cannedMessageModule && cannedMessageModule->isInterceptingAndFocused()) {
|
if (cannedMessageModule && cannedMessageModule->isInterceptingAndFocused()) {
|
||||||
LOG_DEBUG("User button ignored during canned message input");
|
LOG_DEBUG("User button ignored during canned message input");
|
||||||
@ -232,15 +232,15 @@ int32_t ButtonThread::runOnce()
|
|||||||
|
|
||||||
case BUTTON_EVENT_DOUBLE_PRESSED: {
|
case BUTTON_EVENT_DOUBLE_PRESSED: {
|
||||||
LOG_BUTTON("Double press!");
|
LOG_BUTTON("Double press!");
|
||||||
|
|
||||||
#ifdef ELECROW_ThinkNode_M1
|
#ifdef ELECROW_ThinkNode_M1
|
||||||
digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW);
|
digitalWrite(PIN_EINK_EN, digitalRead(PIN_EINK_EN) == LOW);
|
||||||
break;
|
break;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Send GPS position immediately
|
// Send GPS position immediately
|
||||||
sendAdHocPosition();
|
sendAdHocPosition();
|
||||||
|
|
||||||
// Show temporary on-screen confirmation banner for 3 seconds
|
// Show temporary on-screen confirmation banner for 3 seconds
|
||||||
screen->showOverlayBanner("Ad-hoc Ping Sent", 3000);
|
screen->showOverlayBanner("Ad-hoc Ping Sent", 3000);
|
||||||
break;
|
break;
|
||||||
@ -250,21 +250,21 @@ int32_t ButtonThread::runOnce()
|
|||||||
LOG_BUTTON("Mulitipress! %hux", multipressClickCount);
|
LOG_BUTTON("Mulitipress! %hux", multipressClickCount);
|
||||||
switch (multipressClickCount) {
|
switch (multipressClickCount) {
|
||||||
#if HAS_GPS && !defined(ELECROW_ThinkNode_M1)
|
#if HAS_GPS && !defined(ELECROW_ThinkNode_M1)
|
||||||
// 3 clicks: toggle GPS
|
// 3 clicks: toggle GPS
|
||||||
case 3:
|
case 3:
|
||||||
if (!config.device.disable_triple_click && (gps != nullptr)) {
|
if (!config.device.disable_triple_click && (gps != nullptr)) {
|
||||||
gps->toggleGpsMode();
|
gps->toggleGpsMode();
|
||||||
|
|
||||||
const char* statusMsg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED)
|
const char *statusMsg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED)
|
||||||
? "GPS Enabled"
|
? "GPS Enabled"
|
||||||
: "GPS Disabled";
|
: "GPS Disabled";
|
||||||
|
|
||||||
if (screen) {
|
if (screen) {
|
||||||
screen->forceDisplay(true); // Force a new UI frame, then force an EInk update
|
screen->forceDisplay(true); // Force a new UI frame, then force an EInk update
|
||||||
screen->showOverlayBanner(statusMsg, 3000);
|
screen->showOverlayBanner(statusMsg, 3000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
break;
|
|
||||||
#elif defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2)
|
#elif defined(ELECROW_ThinkNode_M1) || defined(ELECROW_ThinkNode_M2)
|
||||||
case 3:
|
case 3:
|
||||||
LOG_INFO("3 clicks: toggle buzzer");
|
LOG_INFO("3 clicks: toggle buzzer");
|
||||||
@ -306,12 +306,12 @@ int32_t ButtonThread::runOnce()
|
|||||||
case BUTTON_EVENT_LONG_PRESSED: {
|
case BUTTON_EVENT_LONG_PRESSED: {
|
||||||
LOG_BUTTON("Long press!");
|
LOG_BUTTON("Long press!");
|
||||||
powerFSM.trigger(EVENT_PRESS);
|
powerFSM.trigger(EVENT_PRESS);
|
||||||
|
|
||||||
if (screen) {
|
if (screen) {
|
||||||
// Show shutdown message as a temporary overlay banner
|
// Show shutdown message as a temporary overlay banner
|
||||||
screen->showOverlayBanner("Shutting Down..."); // Display for 3 seconds
|
screen->showOverlayBanner("Shutting Down..."); // Display for 3 seconds
|
||||||
}
|
}
|
||||||
|
|
||||||
playBeep();
|
playBeep();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -35,19 +35,19 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#include "FSCommon.h"
|
#include "FSCommon.h"
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
#include "NodeDB.h"
|
#include "NodeDB.h"
|
||||||
|
#include "RadioLibInterface.h"
|
||||||
#include "error.h"
|
#include "error.h"
|
||||||
#include "gps/GeoCoord.h"
|
#include "gps/GeoCoord.h"
|
||||||
#include "gps/RTC.h"
|
#include "gps/RTC.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include "graphics/images.h"
|
|
||||||
#include "graphics/emotes.h"
|
#include "graphics/emotes.h"
|
||||||
|
#include "graphics/images.h"
|
||||||
#include "input/ScanAndSelect.h"
|
#include "input/ScanAndSelect.h"
|
||||||
#include "input/TouchScreenImpl1.h"
|
#include "input/TouchScreenImpl1.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "mesh-pb-constants.h"
|
#include "mesh-pb-constants.h"
|
||||||
#include "mesh/Channels.h"
|
#include "mesh/Channels.h"
|
||||||
#include "RadioLibInterface.h"
|
|
||||||
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
#include "mesh/generated/meshtastic/deviceonly.pb.h"
|
||||||
#include "meshUtils.h"
|
#include "meshUtils.h"
|
||||||
#include "modules/AdminModule.h"
|
#include "modules/AdminModule.h"
|
||||||
@ -57,7 +57,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
#include "target_specific.h"
|
#include "target_specific.h"
|
||||||
|
|
||||||
|
|
||||||
using graphics::Emote;
|
using graphics::Emote;
|
||||||
using graphics::emotes;
|
using graphics::emotes;
|
||||||
using graphics::numEmotes;
|
using graphics::numEmotes;
|
||||||
@ -132,18 +131,19 @@ static bool heartbeat = false;
|
|||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
|
|
||||||
|
|
||||||
// Start Functions to write date/time to the screen
|
// Start Functions to write date/time to the screen
|
||||||
#include <string> // Only needed if you're using std::string elsewhere
|
#include <string> // Only needed if you're using std::string elsewhere
|
||||||
|
|
||||||
bool isLeapYear(int year) {
|
bool isLeapYear(int year)
|
||||||
|
{
|
||||||
return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
|
return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
const int daysInMonth[] = { 31,28,31,30,31,30,31,31,30,31,30,31 };
|
const int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
||||||
|
|
||||||
// Fills the buffer with a formatted date/time string and returns pixel width
|
// Fills the buffer with a formatted date/time string and returns pixel width
|
||||||
int formatDateTime(char* buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay* display, bool includeTime) {
|
int formatDateTime(char *buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay *display, bool includeTime)
|
||||||
|
{
|
||||||
int sec = rtc_sec % 60;
|
int sec = rtc_sec % 60;
|
||||||
rtc_sec /= 60;
|
rtc_sec /= 60;
|
||||||
int min = rtc_sec % 60;
|
int min = rtc_sec % 60;
|
||||||
@ -165,7 +165,8 @@ int formatDateTime(char* buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay* dis
|
|||||||
int month = 0;
|
int month = 0;
|
||||||
while (month < 12) {
|
while (month < 12) {
|
||||||
int dim = daysInMonth[month];
|
int dim = daysInMonth[month];
|
||||||
if (month == 1 && isLeapYear(year)) dim++;
|
if (month == 1 && isLeapYear(year))
|
||||||
|
dim++;
|
||||||
if (rtc_sec >= (uint32_t)dim) {
|
if (rtc_sec >= (uint32_t)dim) {
|
||||||
rtc_sec -= dim;
|
rtc_sec -= dim;
|
||||||
month++;
|
month++;
|
||||||
@ -188,7 +189,6 @@ int formatDateTime(char* buf, size_t bufSize, uint32_t rtc_sec, OLEDDisplay* dis
|
|||||||
// Usage: int stringWidth = formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display);
|
// Usage: int stringWidth = formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display);
|
||||||
// End Functions to write date/time to the screen
|
// End Functions to write date/time to the screen
|
||||||
|
|
||||||
|
|
||||||
void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display)
|
void drawScaledXBitmap16x16(int x, int y, int width, int height, const uint8_t *bitmapXBM, OLEDDisplay *display)
|
||||||
{
|
{
|
||||||
for (int row = 0; row < height; row++) {
|
for (int row = 0; row < height; row++) {
|
||||||
@ -1357,7 +1357,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
}
|
}
|
||||||
for (int i = 0; i < numEmotes; ++i) {
|
for (int i = 0; i < numEmotes; ++i) {
|
||||||
const Emote &e = emotes[i];
|
const Emote &e = emotes[i];
|
||||||
if (strcmp(msg, e.label) == 0){
|
if (strcmp(msg, e.label) == 0) {
|
||||||
// Draw the header
|
// Draw the header
|
||||||
if (isInverted) {
|
if (isInverted) {
|
||||||
drawRoundedHighlight(display, x, 0, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, cornerRadius);
|
drawRoundedHighlight(display, x, 0, SCREEN_WIDTH, FONT_HEIGHT_SMALL - 1, cornerRadius);
|
||||||
@ -1881,23 +1881,26 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
|
|||||||
for (size_t i = 0; i < total; i++) {
|
for (size_t i = 0; i < total; i++) {
|
||||||
meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
meshtastic_NodeInfoLite *n = nodeDB->getMeshNodeByIndex(i);
|
||||||
// Skip nulls and ourself
|
// Skip nulls and ourself
|
||||||
if (!n || n->num == nodeDB->getNodeNum()) continue;
|
if (!n || n->num == nodeDB->getNodeNum())
|
||||||
if (n->is_favorite) favoritedNodes.push_back(n);
|
continue;
|
||||||
|
if (n->is_favorite)
|
||||||
|
favoritedNodes.push_back(n);
|
||||||
}
|
}
|
||||||
// Keep a stable, consistent display order
|
// Keep a stable, consistent display order
|
||||||
std::sort(favoritedNodes.begin(), favoritedNodes.end(),
|
std::sort(favoritedNodes.begin(), favoritedNodes.end(),
|
||||||
[](meshtastic_NodeInfoLite *a, meshtastic_NodeInfoLite *b) {
|
[](meshtastic_NodeInfoLite *a, meshtastic_NodeInfoLite *b) { return a->num < b->num; });
|
||||||
return a->num < b->num;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (favoritedNodes.empty()) return;
|
if (favoritedNodes.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
// --- Only display if index is valid ---
|
// --- Only display if index is valid ---
|
||||||
int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size());
|
int nodeIndex = state->currentFrame - (screen->frameCount - favoritedNodes.size());
|
||||||
if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size()) return;
|
if (nodeIndex < 0 || nodeIndex >= (int)favoritedNodes.size())
|
||||||
|
return;
|
||||||
|
|
||||||
meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
|
meshtastic_NodeInfoLite *node = favoritedNodes[nodeIndex];
|
||||||
if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite) return;
|
if (!node || node->num == nodeDB->getNodeNum() || !node->is_favorite)
|
||||||
|
return;
|
||||||
|
|
||||||
display->clear();
|
display->clear();
|
||||||
|
|
||||||
@ -1928,13 +1931,8 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
|
|||||||
// 4. The first line is ALWAYS at your macro position; subsequent lines use the next available macro slot.
|
// 4. The first line is ALWAYS at your macro position; subsequent lines use the next available macro slot.
|
||||||
|
|
||||||
// List of available macro Y positions in order, from top to bottom.
|
// List of available macro Y positions in order, from top to bottom.
|
||||||
const int yPositions[5] = {
|
const int yPositions[5] = {moreCompactFirstLine, moreCompactSecondLine, moreCompactThirdLine, moreCompactFourthLine,
|
||||||
moreCompactFirstLine,
|
moreCompactFifthLine};
|
||||||
moreCompactSecondLine,
|
|
||||||
moreCompactThirdLine,
|
|
||||||
moreCompactFourthLine,
|
|
||||||
moreCompactFifthLine
|
|
||||||
};
|
|
||||||
int line = 0; // which slot to use next
|
int line = 0; // which slot to use next
|
||||||
|
|
||||||
// === 1. Long Name (always try to show first) ===
|
// === 1. Long Name (always try to show first) ===
|
||||||
@ -1965,7 +1963,8 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
|
|||||||
size_t len = strlen(signalHopsStr);
|
size_t len = strlen(signalHopsStr);
|
||||||
// Decide between "1 Hop" and "N Hops"
|
// Decide between "1 Hop" and "N Hops"
|
||||||
if (haveSignal) {
|
if (haveSignal) {
|
||||||
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops"));
|
snprintf(signalHopsStr + len, sizeof(signalHopsStr) - len, " [%d %s]", node->hops_away,
|
||||||
|
(node->hops_away == 1 ? "Hop" : "Hops"));
|
||||||
} else {
|
} else {
|
||||||
snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops"));
|
snprintf(signalHopsStr, sizeof(signalHopsStr), "[%d %s]", node->hops_away, (node->hops_away == 1 ? "Hop" : "Hops"));
|
||||||
}
|
}
|
||||||
@ -1980,10 +1979,13 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
|
|||||||
if (seconds != 0 && seconds != UINT32_MAX) {
|
if (seconds != 0 && seconds != UINT32_MAX) {
|
||||||
uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
|
uint32_t minutes = seconds / 60, hours = minutes / 60, days = hours / 24;
|
||||||
// Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago"
|
// Format as "Heard: Xm ago", "Heard: Xh ago", or "Heard: Xd ago"
|
||||||
snprintf(seenStr, sizeof(seenStr),
|
snprintf(seenStr, sizeof(seenStr), (days > 365 ? " Heard: ?" : " Heard: %d%c ago"),
|
||||||
(days > 365 ? " Heard: ?" : " Heard: %d%c ago"),
|
(days ? days
|
||||||
(days ? days : hours ? hours : minutes),
|
: hours ? hours
|
||||||
(days ? 'd' : hours ? 'h' : 'm'));
|
: minutes),
|
||||||
|
(days ? 'd'
|
||||||
|
: hours ? 'h'
|
||||||
|
: 'm'));
|
||||||
}
|
}
|
||||||
if (seenStr[0] && line < 5) {
|
if (seenStr[0] && line < 5) {
|
||||||
display->drawString(x, yPositions[line++], seenStr);
|
display->drawString(x, yPositions[line++], seenStr);
|
||||||
@ -2010,7 +2012,7 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
|
|||||||
|
|
||||||
// === 5. Distance (only if both nodes have GPS position) ===
|
// === 5. Distance (only if both nodes have GPS position) ===
|
||||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
char distStr[24] = ""; // Make buffer big enough for any string
|
char distStr[24] = ""; // Make buffer big enough for any string
|
||||||
bool haveDistance = false;
|
bool haveDistance = false;
|
||||||
|
|
||||||
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) {
|
if (nodeDB->hasValidPosition(ourNode) && nodeDB->hasValidPosition(node)) {
|
||||||
@ -2022,9 +2024,7 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
|
|||||||
double dLat = (lat2 - lat1) * DEG_TO_RAD;
|
double dLat = (lat2 - lat1) * DEG_TO_RAD;
|
||||||
double dLon = (lon2 - lon1) * DEG_TO_RAD;
|
double dLon = (lon2 - lon1) * DEG_TO_RAD;
|
||||||
double a =
|
double a =
|
||||||
sin(dLat / 2) * sin(dLat / 2) +
|
sin(dLat / 2) * sin(dLat / 2) + cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) * sin(dLon / 2) * sin(dLon / 2);
|
||||||
cos(lat1 * DEG_TO_RAD) * cos(lat2 * DEG_TO_RAD) *
|
|
||||||
sin(dLon / 2) * sin(dLon / 2);
|
|
||||||
double c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
double c = 2 * atan2(sqrt(a), sqrt(1 - a));
|
||||||
double distanceKm = earthRadiusKm * c;
|
double distanceKm = earthRadiusKm * c;
|
||||||
|
|
||||||
@ -2088,19 +2088,16 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
|
|||||||
const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
|
const int16_t compassY = topY + (usableHeight / 2) + ((FONT_HEIGHT_SMALL - 1) / 2) + 2;
|
||||||
|
|
||||||
const auto &op = ourNode->position;
|
const auto &op = ourNode->position;
|
||||||
float myHeading = screen->hasHeading()
|
float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
|
||||||
? screen->getHeading() * PI / 180
|
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
||||||
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
|
||||||
screen->drawCompassNorth(display, compassX, compassY, myHeading);
|
screen->drawCompassNorth(display, compassX, compassY, myHeading);
|
||||||
|
|
||||||
const auto &p = node->position;
|
const auto &p = node->position;
|
||||||
float d = GeoCoord::latLongToMeter(
|
float d =
|
||||||
DegD(p.latitude_i), DegD(p.longitude_i),
|
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
||||||
DegD(op.latitude_i), DegD(op.longitude_i));
|
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
|
||||||
float bearing = GeoCoord::bearing(
|
if (!config.display.compass_north_top)
|
||||||
DegD(op.latitude_i), DegD(op.longitude_i),
|
bearing -= myHeading;
|
||||||
DegD(p.latitude_i), DegD(p.longitude_i));
|
|
||||||
if (!config.display.compass_north_top) bearing -= myHeading;
|
|
||||||
screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearing);
|
screen->drawNodeHeading(display, compassX, compassY, compassDiam, bearing);
|
||||||
|
|
||||||
display->drawCircle(compassX, compassY, compassRadius);
|
display->drawCircle(compassX, compassY, compassRadius);
|
||||||
@ -2115,39 +2112,39 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
|
|||||||
if (showCompass) {
|
if (showCompass) {
|
||||||
int yBelowContent = (line > 0 && line <= 5) ? (yPositions[line - 1] + FONT_HEIGHT_SMALL + 2) : moreCompactFirstLine;
|
int yBelowContent = (line > 0 && line <= 5) ? (yPositions[line - 1] + FONT_HEIGHT_SMALL + 2) : moreCompactFirstLine;
|
||||||
const int margin = 4;
|
const int margin = 4;
|
||||||
// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) -----------
|
// --------- PATCH FOR EINK NAV BAR (ONLY CHANGE BELOW) -----------
|
||||||
#if defined(USE_EINK)
|
#if defined(USE_EINK)
|
||||||
const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8;
|
const int iconSize = (SCREEN_WIDTH > 128) ? 16 : 8;
|
||||||
const int navBarHeight = iconSize + 6;
|
const int navBarHeight = iconSize + 6;
|
||||||
#else
|
#else
|
||||||
const int navBarHeight = 0;
|
const int navBarHeight = 0;
|
||||||
#endif
|
#endif
|
||||||
int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin;
|
int availableHeight = SCREEN_HEIGHT - yBelowContent - navBarHeight - margin;
|
||||||
// --------- END PATCH FOR EINK NAV BAR -----------
|
// --------- END PATCH FOR EINK NAV BAR -----------
|
||||||
|
|
||||||
if (availableHeight < FONT_HEIGHT_SMALL * 2) return;
|
if (availableHeight < FONT_HEIGHT_SMALL * 2)
|
||||||
|
return;
|
||||||
|
|
||||||
int compassRadius = availableHeight / 2;
|
int compassRadius = availableHeight / 2;
|
||||||
if (compassRadius < 8) compassRadius = 8;
|
if (compassRadius < 8)
|
||||||
if (compassRadius * 2 > SCREEN_WIDTH - 16) compassRadius = (SCREEN_WIDTH - 16) / 2;
|
compassRadius = 8;
|
||||||
|
if (compassRadius * 2 > SCREEN_WIDTH - 16)
|
||||||
|
compassRadius = (SCREEN_WIDTH - 16) / 2;
|
||||||
|
|
||||||
int compassX = x + SCREEN_WIDTH / 2;
|
int compassX = x + SCREEN_WIDTH / 2;
|
||||||
int compassY = yBelowContent + availableHeight / 2;
|
int compassY = yBelowContent + availableHeight / 2;
|
||||||
|
|
||||||
const auto &op = ourNode->position;
|
const auto &op = ourNode->position;
|
||||||
float myHeading = screen->hasHeading()
|
float myHeading = screen->hasHeading() ? screen->getHeading() * PI / 180
|
||||||
? screen->getHeading() * PI / 180
|
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
||||||
: screen->estimatedHeading(DegD(op.latitude_i), DegD(op.longitude_i));
|
|
||||||
screen->drawCompassNorth(display, compassX, compassY, myHeading);
|
screen->drawCompassNorth(display, compassX, compassY, myHeading);
|
||||||
|
|
||||||
const auto &p = node->position;
|
const auto &p = node->position;
|
||||||
float d = GeoCoord::latLongToMeter(
|
float d =
|
||||||
DegD(p.latitude_i), DegD(p.longitude_i),
|
GeoCoord::latLongToMeter(DegD(p.latitude_i), DegD(p.longitude_i), DegD(op.latitude_i), DegD(op.longitude_i));
|
||||||
DegD(op.latitude_i), DegD(op.longitude_i));
|
float bearing = GeoCoord::bearing(DegD(op.latitude_i), DegD(op.longitude_i), DegD(p.latitude_i), DegD(p.longitude_i));
|
||||||
float bearing = GeoCoord::bearing(
|
if (!config.display.compass_north_top)
|
||||||
DegD(op.latitude_i), DegD(op.longitude_i),
|
bearing -= myHeading;
|
||||||
DegD(p.latitude_i), DegD(p.longitude_i));
|
|
||||||
if (!config.display.compass_north_top) bearing -= myHeading;
|
|
||||||
screen->drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing);
|
screen->drawNodeHeading(display, compassX, compassY, compassRadius * 2, bearing);
|
||||||
|
|
||||||
display->drawCircle(compassX, compassY, compassRadius);
|
display->drawCircle(compassX, compassY, compassRadius);
|
||||||
@ -2346,9 +2343,9 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
|||||||
|
|
||||||
int totalEntries = nodeList.size();
|
int totalEntries = nodeList.size();
|
||||||
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
|
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
totalRowsAvailable -= 1;
|
totalRowsAvailable -= 1;
|
||||||
#endif
|
#endif
|
||||||
int visibleNodeRows = totalRowsAvailable;
|
int visibleNodeRows = totalRowsAvailable;
|
||||||
int totalColumns = 2;
|
int totalColumns = 2;
|
||||||
|
|
||||||
@ -2424,8 +2421,8 @@ void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
|||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
display->drawString(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nodeName);
|
display->drawString(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nodeName);
|
||||||
if (node->is_favorite){
|
if (node->is_favorite) {
|
||||||
if(SCREEN_WIDTH > 128){
|
if (SCREEN_WIDTH > 128) {
|
||||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||||
} else {
|
} else {
|
||||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||||
@ -2455,10 +2452,10 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
|||||||
|
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
|
|
||||||
display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName);
|
display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName);
|
||||||
if (node->is_favorite){
|
if (node->is_favorite) {
|
||||||
if(SCREEN_WIDTH > 128){
|
if (SCREEN_WIDTH > 128) {
|
||||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||||
} else {
|
} else {
|
||||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||||
@ -2550,8 +2547,8 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName);
|
display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName);
|
||||||
if (node->is_favorite){
|
if (node->is_favorite) {
|
||||||
if(SCREEN_WIDTH > 128){
|
if (SCREEN_WIDTH > 128) {
|
||||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||||
} else {
|
} else {
|
||||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||||
@ -2681,8 +2678,8 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName);
|
display->drawStringMaxWidth(x + ((SCREEN_WIDTH > 128) ? 6 : 2), y, nameMaxWidth, nodeName);
|
||||||
if (node->is_favorite){
|
if (node->is_favorite) {
|
||||||
if(SCREEN_WIDTH > 128){
|
if (SCREEN_WIDTH > 128) {
|
||||||
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
drawScaledXBitmap16x16(x, y + 6, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint, display);
|
||||||
} else {
|
} else {
|
||||||
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
display->drawXbm(x, y + 5, smallbulletpoint_width, smallbulletpoint_height, smallbulletpoint);
|
||||||
@ -2784,13 +2781,14 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
|||||||
rows = 5;
|
rows = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// === First Row: Region / Channel Utilization and Uptime ===
|
// === First Row: Region / Channel Utilization and Uptime ===
|
||||||
bool origBold = config.display.heading_bold;
|
bool origBold = config.display.heading_bold;
|
||||||
config.display.heading_bold = false;
|
config.display.heading_bold = false;
|
||||||
|
|
||||||
// Display Region and Channel Utilization
|
// Display Region and Channel Utilization
|
||||||
drawNodes(display, x + 1, ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)) + 2, nodeStatus, -1, false, "online");
|
drawNodes(display, x + 1,
|
||||||
|
((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)) + 2, nodeStatus,
|
||||||
|
-1, false, "online");
|
||||||
|
|
||||||
uint32_t uptime = millis() / 1000;
|
uint32_t uptime = millis() / 1000;
|
||||||
char uptimeStr[6];
|
char uptimeStr[6];
|
||||||
@ -2812,7 +2810,9 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
|||||||
|
|
||||||
char uptimeFullStr[16];
|
char uptimeFullStr[16];
|
||||||
snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr);
|
snprintf(uptimeFullStr, sizeof(uptimeFullStr), "Uptime: %s", uptimeStr);
|
||||||
display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeFullStr), ((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)), uptimeFullStr);
|
display->drawString(SCREEN_WIDTH - display->getStringWidth(uptimeFullStr),
|
||||||
|
((rows == 4) ? compactFirstLine : ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine)),
|
||||||
|
uptimeFullStr);
|
||||||
|
|
||||||
config.display.heading_bold = origBold;
|
config.display.heading_bold = origBold;
|
||||||
|
|
||||||
@ -2827,9 +2827,13 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
|||||||
} else {
|
} else {
|
||||||
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
|
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
|
||||||
}
|
}
|
||||||
display->drawString(0, ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), displayLine);
|
display->drawString(
|
||||||
|
0, ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)),
|
||||||
|
displayLine);
|
||||||
} else {
|
} else {
|
||||||
drawGPS(display, 0, ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)) + 3, gpsStatus);
|
drawGPS(display, 0,
|
||||||
|
((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)) + 3,
|
||||||
|
gpsStatus);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -2838,16 +2842,22 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
|||||||
int batV = powerStatus->getBatteryVoltageMv() / 1000;
|
int batV = powerStatus->getBatteryVoltageMv() / 1000;
|
||||||
int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10;
|
int batCv = (powerStatus->getBatteryVoltageMv() % 1000) / 10;
|
||||||
snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv);
|
snprintf(batStr, sizeof(batStr), "%01d.%02dV", batV, batCv);
|
||||||
display->drawString(x + SCREEN_WIDTH - display->getStringWidth(batStr), ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), batStr);
|
display->drawString(
|
||||||
|
x + SCREEN_WIDTH - display->getStringWidth(batStr),
|
||||||
|
((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), batStr);
|
||||||
} else {
|
} else {
|
||||||
display->drawString(x + SCREEN_WIDTH - display->getStringWidth("USB"), ((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)), String("USB"));
|
display->drawString(
|
||||||
|
x + SCREEN_WIDTH - display->getStringWidth("USB"),
|
||||||
|
((rows == 4) ? compactSecondLine : ((SCREEN_HEIGHT > 64) ? compactSecondLine : moreCompactSecondLine)),
|
||||||
|
String("USB"));
|
||||||
}
|
}
|
||||||
|
|
||||||
config.display.heading_bold = origBold;
|
config.display.heading_bold = origBold;
|
||||||
|
|
||||||
// === Third Row: Bluetooth Off (Only If Actually Off) ===
|
// === Third Row: Bluetooth Off (Only If Actually Off) ===
|
||||||
if (!config.bluetooth.enabled) {
|
if (!config.bluetooth.enabled) {
|
||||||
display->drawString(0, ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactThirdLine : moreCompactThirdLine)), "BT off");
|
display->drawString(
|
||||||
|
0, ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactThirdLine : moreCompactThirdLine)), "BT off");
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Third & Fourth Rows: Node Identity ===
|
// === Third & Fourth Rows: Node Identity ===
|
||||||
@ -2867,27 +2877,35 @@ static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, i
|
|||||||
|
|
||||||
char combinedName[50];
|
char combinedName[50];
|
||||||
snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortnameble);
|
snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortnameble);
|
||||||
if(SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10){
|
if (SCREEN_WIDTH - (display->getStringWidth(longName) + display->getStringWidth(shortnameble)) > 10) {
|
||||||
size_t len = strlen(combinedName);
|
size_t len = strlen(combinedName);
|
||||||
if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) {
|
if (len >= 3 && strcmp(combinedName + len - 3, " ()") == 0) {
|
||||||
combinedName[len - 3] = '\0'; // Remove the last three characters
|
combinedName[len - 3] = '\0'; // Remove the last three characters
|
||||||
}
|
}
|
||||||
textWidth = display->getStringWidth(combinedName);
|
textWidth = display->getStringWidth(combinedName);
|
||||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
display->drawString(nameX, ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset, combinedName);
|
display->drawString(
|
||||||
|
nameX,
|
||||||
|
((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset,
|
||||||
|
combinedName);
|
||||||
} else {
|
} else {
|
||||||
textWidth = display->getStringWidth(longName);
|
textWidth = display->getStringWidth(longName);
|
||||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
yOffset = (strcmp(shortnameble, "") == 0) ? 1 : 0;
|
yOffset = (strcmp(shortnameble, "") == 0) ? 1 : 0;
|
||||||
if(yOffset == 1){
|
if (yOffset == 1) {
|
||||||
yOffset = (SCREEN_WIDTH > 128) ? 0 : 7;
|
yOffset = (SCREEN_WIDTH > 128) ? 0 : 7;
|
||||||
}
|
}
|
||||||
display->drawString(nameX, ((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset, longName);
|
display->drawString(
|
||||||
|
nameX,
|
||||||
|
((rows == 4) ? compactThirdLine : ((SCREEN_HEIGHT > 64) ? compactFourthLine : moreCompactFourthLine)) + yOffset,
|
||||||
|
longName);
|
||||||
|
|
||||||
// === Fourth Row: ShortName Centered ===
|
// === Fourth Row: ShortName Centered ===
|
||||||
textWidth = display->getStringWidth(shortnameble);
|
textWidth = display->getStringWidth(shortnameble);
|
||||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
display->drawString(nameX, ((rows == 4) ? compactFourthLine : ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine)), shortnameble);
|
display->drawString(nameX,
|
||||||
|
((rows == 4) ? compactFourthLine : ((SCREEN_HEIGHT > 64) ? compactFifthLine : moreCompactFifthLine)),
|
||||||
|
shortnameble);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2947,14 +2965,14 @@ static void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int
|
|||||||
char freqStr[16];
|
char freqStr[16];
|
||||||
float freq = RadioLibInterface::instance->getFreq();
|
float freq = RadioLibInterface::instance->getFreq();
|
||||||
snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
|
snprintf(freqStr, sizeof(freqStr), "%.3f", freq);
|
||||||
if(config.lora.channel_num == 0){
|
if (config.lora.channel_num == 0) {
|
||||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %s", freqStr);
|
snprintf(frequencyslot, sizeof(frequencyslot), "Freq: %s", freqStr);
|
||||||
} else {
|
} else {
|
||||||
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Chan: %s (%d)", freqStr, config.lora.channel_num);
|
snprintf(frequencyslot, sizeof(frequencyslot), "Freq/Chan: %s (%d)", freqStr, config.lora.channel_num);
|
||||||
}
|
}
|
||||||
size_t len = strlen(frequencyslot);
|
size_t len = strlen(frequencyslot);
|
||||||
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
|
if (len >= 4 && strcmp(frequencyslot + len - 4, " (0)") == 0) {
|
||||||
frequencyslot[len - 4] = '\0'; // Remove the last three characters
|
frequencyslot[len - 4] = '\0'; // Remove the last three characters
|
||||||
}
|
}
|
||||||
textWidth = display->getStringWidth(frequencyslot);
|
textWidth = display->getStringWidth(frequencyslot);
|
||||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
@ -3061,9 +3079,11 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat
|
|||||||
} else {
|
} else {
|
||||||
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
|
displayLine = config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_NOT_PRESENT ? "No GPS" : "GPS off";
|
||||||
}
|
}
|
||||||
display->drawString(display->getStringWidth(Satelite_String) + 3, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), displayLine);
|
display->drawString(display->getStringWidth(Satelite_String) + 3,
|
||||||
|
((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine), displayLine);
|
||||||
} else {
|
} else {
|
||||||
drawGPS(display, display->getStringWidth(Satelite_String) + 3, ((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine) + 3, gpsStatus);
|
drawGPS(display, display->getStringWidth(Satelite_String) + 3,
|
||||||
|
((SCREEN_HEIGHT > 64) ? compactFirstLine : moreCompactFirstLine) + 3, gpsStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
config.display.heading_bold = origBold;
|
config.display.heading_bold = origBold;
|
||||||
@ -3107,7 +3127,7 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat
|
|||||||
// === Fifth Row: Date ===
|
// === Fifth Row: Date ===
|
||||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
|
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||||
char datetimeStr[25];
|
char datetimeStr[25];
|
||||||
bool showTime = false; // set to true for full datetime
|
bool showTime = false; // set to true for full datetime
|
||||||
formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime);
|
formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, showTime);
|
||||||
char fullLine[40];
|
char fullLine[40];
|
||||||
snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr);
|
snprintf(fullLine, sizeof(fullLine), " Date: %s", datetimeStr);
|
||||||
@ -3160,11 +3180,14 @@ static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiStat
|
|||||||
SCREEN_HEIGHT - yBelowContent - margin;
|
SCREEN_HEIGHT - yBelowContent - margin;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (availableHeight < FONT_HEIGHT_SMALL * 2) return;
|
if (availableHeight < FONT_HEIGHT_SMALL * 2)
|
||||||
|
return;
|
||||||
|
|
||||||
int compassRadius = availableHeight / 2;
|
int compassRadius = availableHeight / 2;
|
||||||
if (compassRadius < 8) compassRadius = 8;
|
if (compassRadius < 8)
|
||||||
if (compassRadius * 2 > SCREEN_WIDTH - 16) compassRadius = (SCREEN_WIDTH - 16) / 2;
|
compassRadius = 8;
|
||||||
|
if (compassRadius * 2 > SCREEN_WIDTH - 16)
|
||||||
|
compassRadius = (SCREEN_WIDTH - 16) / 2;
|
||||||
|
|
||||||
int compassX = x + SCREEN_WIDTH / 2;
|
int compassX = x + SCREEN_WIDTH / 2;
|
||||||
int compassY = yBelowContent + availableHeight / 2;
|
int compassY = yBelowContent + availableHeight / 2;
|
||||||
@ -3532,7 +3555,8 @@ void NavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state)
|
|||||||
const int bigOffset = useBigIcons ? 1 : 0;
|
const int bigOffset = useBigIcons ? 1 : 0;
|
||||||
|
|
||||||
const size_t totalIcons = screen->indicatorIcons.size();
|
const size_t totalIcons = screen->indicatorIcons.size();
|
||||||
if (totalIcons == 0) return;
|
if (totalIcons == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
const size_t iconsPerPage = (SCREEN_WIDTH + spacing) / (iconSize + spacing);
|
const size_t iconsPerPage = (SCREEN_WIDTH + spacing) / (iconSize + spacing);
|
||||||
const size_t currentPage = currentFrame / iconsPerPage;
|
const size_t currentPage = currentFrame / iconsPerPage;
|
||||||
@ -3628,7 +3652,7 @@ void Screen::setup()
|
|||||||
// === Set custom overlay callbacks ===
|
// === Set custom overlay callbacks ===
|
||||||
static OverlayCallback overlays[] = {
|
static OverlayCallback overlays[] = {
|
||||||
drawFunctionOverlay, // For mute/buzzer modifiers etc.
|
drawFunctionOverlay, // For mute/buzzer modifiers etc.
|
||||||
NavigationBar // Custom indicator icons for each frame
|
NavigationBar // Custom indicator icons for each frame
|
||||||
};
|
};
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
|
|
||||||
|
@ -275,7 +275,7 @@ class Screen : public concurrency::OSThread
|
|||||||
}
|
}
|
||||||
|
|
||||||
void showOverlayBanner(const String &message, uint32_t durationMs = 3000);
|
void showOverlayBanner(const String &message, uint32_t durationMs = 3000);
|
||||||
|
|
||||||
void startFirmwareUpdateScreen()
|
void startFirmwareUpdateScreen()
|
||||||
{
|
{
|
||||||
ScreenCmd cmd;
|
ScreenCmd cmd;
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
|
|
||||||
namespace graphics {
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
// =======================
|
// =======================
|
||||||
// Shared UI Helpers
|
// Shared UI Helpers
|
||||||
@ -22,11 +23,11 @@ namespace graphics {
|
|||||||
#define standardFourthLine (FONT_HEIGHT_SMALL + 1) * 4
|
#define standardFourthLine (FONT_HEIGHT_SMALL + 1) * 4
|
||||||
|
|
||||||
// More Compact line layout
|
// More Compact line layout
|
||||||
#define moreCompactFirstLine compactFirstLine
|
#define moreCompactFirstLine compactFirstLine
|
||||||
#define moreCompactSecondLine (moreCompactFirstLine + (FONT_HEIGHT_SMALL - 5))
|
#define moreCompactSecondLine (moreCompactFirstLine + (FONT_HEIGHT_SMALL - 5))
|
||||||
#define moreCompactThirdLine (moreCompactSecondLine + (FONT_HEIGHT_SMALL - 5))
|
#define moreCompactThirdLine (moreCompactSecondLine + (FONT_HEIGHT_SMALL - 5))
|
||||||
#define moreCompactFourthLine (moreCompactThirdLine + (FONT_HEIGHT_SMALL - 5))
|
#define moreCompactFourthLine (moreCompactThirdLine + (FONT_HEIGHT_SMALL - 5))
|
||||||
#define moreCompactFifthLine (moreCompactFourthLine + (FONT_HEIGHT_SMALL - 5))
|
#define moreCompactFifthLine (moreCompactFourthLine + (FONT_HEIGHT_SMALL - 5))
|
||||||
|
|
||||||
// Quick screen access
|
// Quick screen access
|
||||||
#define SCREEN_WIDTH display->getWidth()
|
#define SCREEN_WIDTH display->getWidth()
|
||||||
|
@ -1,56 +1,57 @@
|
|||||||
#include "emotes.h"
|
#include "emotes.h"
|
||||||
|
|
||||||
namespace graphics {
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
// Always define Emote list and count
|
// Always define Emote list and count
|
||||||
const Emote emotes[] = {
|
const Emote emotes[] = {
|
||||||
#ifndef EXCLUDE_EMOJI
|
#ifndef EXCLUDE_EMOJI
|
||||||
// --- Thumbs ---
|
// --- Thumbs ---
|
||||||
{"\U0001F44D", thumbup, thumbs_width, thumbs_height}, // 👍 Thumbs Up
|
{"\U0001F44D", thumbup, thumbs_width, thumbs_height}, // 👍 Thumbs Up
|
||||||
{"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // 👎 Thumbs Down
|
{"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // 👎 Thumbs Down
|
||||||
|
|
||||||
// --- Smileys (Multiple Unicode Aliases) ---
|
// --- Smileys (Multiple Unicode Aliases) ---
|
||||||
{"\U0001F60A", smiley, smiley_width, smiley_height}, // 😊 Smiling Face with Smiling Eyes
|
{"\U0001F60A", smiley, smiley_width, smiley_height}, // 😊 Smiling Face with Smiling Eyes
|
||||||
{"\U0001F600", smiley, smiley_width, smiley_height}, // 😀 Grinning Face
|
{"\U0001F600", smiley, smiley_width, smiley_height}, // 😀 Grinning Face
|
||||||
{"\U0001F642", smiley, smiley_width, smiley_height}, // 🙂 Slightly Smiling Face
|
{"\U0001F642", smiley, smiley_width, smiley_height}, // 🙂 Slightly Smiling Face
|
||||||
{"\U0001F609", smiley, smiley_width, smiley_height}, // 😉 Winking Face
|
{"\U0001F609", smiley, smiley_width, smiley_height}, // 😉 Winking Face
|
||||||
{"\U0001F601", smiley, smiley_width, smiley_height}, // 😁 Grinning Face with Smiling Eyes
|
{"\U0001F601", smiley, smiley_width, smiley_height}, // 😁 Grinning Face with Smiling Eyes
|
||||||
|
|
||||||
// --- Question/Alert ---
|
// --- Question/Alert ---
|
||||||
{"\u2753", question, question_width, question_height}, // ❓ Question Mark
|
{"\u2753", question, question_width, question_height}, // ❓ Question Mark
|
||||||
{"\u203C\uFE0F", bang, bang_width, bang_height}, // ‼️ Double Exclamation Mark
|
{"\u203C\uFE0F", bang, bang_width, bang_height}, // ‼️ Double Exclamation Mark
|
||||||
|
|
||||||
// --- Laughing Faces ---
|
// --- Laughing Faces ---
|
||||||
{"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy
|
{"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy
|
||||||
{"\U0001F923", haha, haha_width, haha_height}, // 🤣 Rolling on the Floor Laughing
|
{"\U0001F923", haha, haha_width, haha_height}, // 🤣 Rolling on the Floor Laughing
|
||||||
{"\U0001F606", haha, haha_width, haha_height}, // 😆 Smiling with Open Mouth and Closed Eyes
|
{"\U0001F606", haha, haha_width, haha_height}, // 😆 Smiling with Open Mouth and Closed Eyes
|
||||||
{"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat
|
{"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat
|
||||||
{"\U0001F604", haha, haha_width, haha_height}, // 😄 Grinning Face with Smiling Eyes
|
{"\U0001F604", haha, haha_width, haha_height}, // 😄 Grinning Face with Smiling Eyes
|
||||||
|
|
||||||
// --- Gestures and People ---
|
// --- Gestures and People ---
|
||||||
{"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height},// 👋 Waving Hand
|
{"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // 👋 Waving Hand
|
||||||
{"\U0001F920", cowboy, cowboy_width, cowboy_height}, // 🤠 Cowboy Hat Face
|
{"\U0001F920", cowboy, cowboy_width, cowboy_height}, // 🤠 Cowboy Hat Face
|
||||||
{"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // 🎧 Headphones
|
{"\U0001F3A7", deadmau5, deadmau5_width, deadmau5_height}, // 🎧 Headphones
|
||||||
|
|
||||||
// --- Weather ---
|
// --- Weather ---
|
||||||
{"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector)
|
{"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector)
|
||||||
{"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀️ Sun (with variation selector)
|
{"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀️ Sun (with variation selector)
|
||||||
{"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain
|
{"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain
|
||||||
{"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud
|
{"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud
|
||||||
{"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog
|
{"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog
|
||||||
|
|
||||||
// --- Misc Faces ---
|
// --- Misc Faces ---
|
||||||
{"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns
|
{"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns
|
||||||
|
|
||||||
// --- Hearts (Multiple Unicode Aliases) ---
|
// --- Hearts (Multiple Unicode Aliases) ---
|
||||||
{"\u2764\uFE0F", heart, heart_width, heart_height}, // ❤️ Red Heart
|
{"\u2764\uFE0F", heart, heart_width, heart_height}, // ❤️ Red Heart
|
||||||
{"\U0001F9E1", heart, heart_width, heart_height}, // 🧡 Orange Heart
|
{"\U0001F9E1", heart, heart_width, heart_height}, // 🧡 Orange Heart
|
||||||
{"\U00002763", heart, heart_width, heart_height}, // ❣ Heart Exclamation
|
{"\U00002763", heart, heart_width, heart_height}, // ❣ Heart Exclamation
|
||||||
{"\U00002764", heart, heart_width, heart_height}, // ❤ Red Heart (legacy)
|
{"\U00002764", heart, heart_width, heart_height}, // ❤ Red Heart (legacy)
|
||||||
{"\U0001F495", heart, heart_width, heart_height}, // 💕 Two Hearts
|
{"\U0001F495", heart, heart_width, heart_height}, // 💕 Two Hearts
|
||||||
{"\U0001F496", heart, heart_width, heart_height}, // 💖 Sparkling Heart
|
{"\U0001F496", heart, heart_width, heart_height}, // 💖 Sparkling Heart
|
||||||
{"\U0001F497", heart, heart_width, heart_height}, // 💗 Growing Heart
|
{"\U0001F497", heart, heart_width, heart_height}, // 💗 Growing Heart
|
||||||
{"\U0001F498", heart, heart_width, heart_height}, // 💘 Heart with Arrow
|
{"\U0001F498", heart, heart_width, heart_height}, // 💘 Heart with Arrow
|
||||||
|
|
||||||
// --- Objects ---
|
// --- Objects ---
|
||||||
{"\U0001F4A9", poo, poo_width, poo_height}, // 💩 Pile of Poo
|
{"\U0001F4A9", poo, poo_width, poo_height}, // 💩 Pile of Poo
|
||||||
@ -83,8 +84,7 @@ const unsigned char smiley[] PROGMEM = {
|
|||||||
0x01, 0x09, 0x12, 0x20, 0x01, 0x0f, 0x1e, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20,
|
0x01, 0x09, 0x12, 0x20, 0x01, 0x0f, 0x1e, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20,
|
||||||
0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x81, 0x00, 0x20, 0x20,
|
0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x01, 0x00, 0x00, 0x20, 0x81, 0x00, 0x20, 0x20,
|
||||||
0x82, 0x00, 0x20, 0x10, 0x02, 0x01, 0x10, 0x10, 0x04, 0x02, 0x08, 0x08, 0x04, 0xfc, 0x07, 0x08, 0x08, 0x00, 0x00, 0x04,
|
0x82, 0x00, 0x20, 0x10, 0x02, 0x01, 0x10, 0x10, 0x04, 0x02, 0x08, 0x08, 0x04, 0xfc, 0x07, 0x08, 0x08, 0x00, 0x00, 0x04,
|
||||||
0x10, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x01, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x01, 0x30, 0x00, 0x00, 0xfe, 0x0f, 0x00
|
0x10, 0x00, 0x00, 0x02, 0x20, 0x00, 0x00, 0x01, 0x40, 0x00, 0xc0, 0x00, 0x80, 0x01, 0x30, 0x00, 0x00, 0xfe, 0x0f, 0x00};
|
||||||
};
|
|
||||||
|
|
||||||
const unsigned char question[] PROGMEM = {
|
const unsigned char question[] PROGMEM = {
|
||||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00,
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x80, 0xFF, 0x01, 0x00, 0xC0, 0xFF, 0x07, 0x00, 0xE0, 0xFF, 0x07, 0x00,
|
||||||
@ -219,9 +219,7 @@ const unsigned char bell_icon[] PROGMEM = {
|
|||||||
0b00011000, 0b00000000, 0b00000000, 0b00000110, 0b11110000, 0b11111111, 0b11111111, 0b00000011, 0b00000000, 0b00001100,
|
0b00011000, 0b00000000, 0b00000000, 0b00000110, 0b11110000, 0b11111111, 0b11111111, 0b00000011, 0b00000000, 0b00001100,
|
||||||
0b00001100, 0b00000000, 0b00000000, 0b00011000, 0b00000110, 0b00000000, 0b00000000, 0b11111000, 0b00000111, 0b00000000,
|
0b00001100, 0b00000000, 0b00000000, 0b00011000, 0b00000110, 0b00000000, 0b00000000, 0b11111000, 0b00000111, 0b00000000,
|
||||||
0b00000000, 0b11100000, 0b00000001, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
0b00000000, 0b11100000, 0b00000001, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
||||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000
|
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000};
|
||||||
};
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
namespace graphics {
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
// === Emote List ===
|
// === Emote List ===
|
||||||
struct Emote {
|
struct Emote {
|
||||||
|
@ -241,71 +241,38 @@ const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0
|
|||||||
const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010,
|
const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010,
|
||||||
0b01000010, 0b01000010, 0b11111111, 0b00011000};
|
0b01000010, 0b01000010, 0b11111111, 0b00011000};
|
||||||
|
|
||||||
|
|
||||||
#define key_symbol_width 8
|
#define key_symbol_width 8
|
||||||
#define key_symbol_height 8
|
#define key_symbol_height 8
|
||||||
const uint8_t key_symbol[] PROGMEM = {
|
const uint8_t key_symbol[] PROGMEM = {0b00000000, 0b00000000, 0b00000110, 0b11111001,
|
||||||
0b00000000,
|
0b10101001, 0b10000110, 0b00000000, 0b00000000};
|
||||||
0b00000000,
|
|
||||||
0b00000110,
|
|
||||||
0b11111001,
|
|
||||||
0b10101001,
|
|
||||||
0b10000110,
|
|
||||||
0b00000000,
|
|
||||||
0b00000000
|
|
||||||
};
|
|
||||||
|
|
||||||
#define placeholder_width 8
|
#define placeholder_width 8
|
||||||
#define placeholder_height 8
|
#define placeholder_height 8
|
||||||
const uint8_t placeholder[] PROGMEM = {
|
const uint8_t placeholder[] PROGMEM = {0b11111111, 0b11111111, 0b11111111, 0b11111111,
|
||||||
0b11111111,
|
0b11111111, 0b11111111, 0b11111111, 0b11111111};
|
||||||
0b11111111,
|
|
||||||
0b11111111,
|
|
||||||
0b11111111,
|
|
||||||
0b11111111,
|
|
||||||
0b11111111,
|
|
||||||
0b11111111,
|
|
||||||
0b11111111
|
|
||||||
};
|
|
||||||
|
|
||||||
#define icon_node_width 8
|
#define icon_node_width 8
|
||||||
#define icon_node_height 8
|
#define icon_node_height 8
|
||||||
static const uint8_t icon_node[] PROGMEM = {
|
static const uint8_t icon_node[] PROGMEM = {
|
||||||
0x10, // #
|
0x10, // #
|
||||||
0x10, // # ← antenna
|
0x10, // # ← antenna
|
||||||
0x10, // #
|
0x10, // #
|
||||||
0xFE, // ####### ← device top
|
0xFE, // ####### ← device top
|
||||||
0x82, // # #
|
0x82, // # #
|
||||||
0xAA, // # # # # ← body with pattern
|
0xAA, // # # # # ← body with pattern
|
||||||
0x92, // # # #
|
0x92, // # # #
|
||||||
0xFE // ####### ← device base
|
0xFE // ####### ← device base
|
||||||
};
|
};
|
||||||
|
|
||||||
#define bluetoothdisabled_width 8
|
#define bluetoothdisabled_width 8
|
||||||
#define bluetoothdisabled_height 8
|
#define bluetoothdisabled_height 8
|
||||||
const uint8_t bluetoothdisabled[] PROGMEM = {
|
const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100, 0b01010100,
|
||||||
0b11101100,
|
0b01001100, 0b00000000, 0b00000000, 0b00000000};
|
||||||
0b01010100,
|
|
||||||
0b01001100,
|
|
||||||
0b01010100,
|
|
||||||
0b01001100,
|
|
||||||
0b00000000,
|
|
||||||
0b00000000,
|
|
||||||
0b00000000
|
|
||||||
};
|
|
||||||
|
|
||||||
#define smallbulletpoint_width 8
|
#define smallbulletpoint_width 8
|
||||||
#define smallbulletpoint_height 8
|
#define smallbulletpoint_height 8
|
||||||
const uint8_t smallbulletpoint[] PROGMEM = {
|
const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000,
|
||||||
0b00000011,
|
0b00000000, 0b00000000, 0b00000000, 0b00000000};
|
||||||
0b00000011,
|
|
||||||
0b00000000,
|
|
||||||
0b00000000,
|
|
||||||
0b00000000,
|
|
||||||
0b00000000,
|
|
||||||
0b00000000,
|
|
||||||
0b00000000
|
|
||||||
};
|
|
||||||
|
|
||||||
#include "img/icon.xbm"
|
#include "img/icon.xbm"
|
||||||
static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning");
|
static_assert(sizeof(icon_bits) >= 0, "Silence unused variable warning");
|
@ -1228,7 +1228,7 @@ void setup()
|
|||||||
LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset");
|
LOG_WARN("LoRa chip does not support 2.4GHz. Revert to unset");
|
||||||
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
config.lora.region = meshtastic_Config_LoRaConfig_RegionCode_UNSET;
|
||||||
nodeDB->saveToDisk(SEGMENT_CONFIG);
|
nodeDB->saveToDisk(SEGMENT_CONFIG);
|
||||||
|
|
||||||
if (!rIf->reconfigure()) {
|
if (!rIf->reconfigure()) {
|
||||||
LOG_WARN("Reconfigure failed, rebooting");
|
LOG_WARN("Reconfigure failed, rebooting");
|
||||||
screen->startAlert("Rebooting...");
|
screen->startAlert("Rebooting...");
|
||||||
|
@ -299,7 +299,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
if (node != NULL) {
|
if (node != NULL) {
|
||||||
node->is_favorite = true;
|
node->is_favorite = true;
|
||||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||||
if (screen) screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
|
if (screen)
|
||||||
|
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -309,7 +310,8 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
if (node != NULL) {
|
if (node != NULL) {
|
||||||
node->is_favorite = false;
|
node->is_favorite = false;
|
||||||
saveChanges(SEGMENT_NODEDATABASE, false);
|
saveChanges(SEGMENT_NODEDATABASE, false);
|
||||||
if (screen) screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
|
if (screen)
|
||||||
|
screen->setFrames(graphics::Screen::FOCUS_PRESERVE); // <-- Rebuild screens
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -1120,7 +1122,7 @@ void AdminModule::reboot(int32_t seconds)
|
|||||||
{
|
{
|
||||||
LOG_INFO("Reboot in %d seconds", seconds);
|
LOG_INFO("Reboot in %d seconds", seconds);
|
||||||
if (screen)
|
if (screen)
|
||||||
screen->showOverlayBanner("Rebooting...", 0); // stays on screen
|
screen->showOverlayBanner("Rebooting...", 0); // stays on screen
|
||||||
rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000);
|
rebootAtMsec = (seconds < 0) ? 0 : (millis() + seconds * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,12 +11,12 @@
|
|||||||
#include "PowerFSM.h" // needed for button bypass
|
#include "PowerFSM.h" // needed for button bypass
|
||||||
#include "SPILock.h"
|
#include "SPILock.h"
|
||||||
#include "detect/ScanI2C.h"
|
#include "detect/ScanI2C.h"
|
||||||
#include "input/ScanAndSelect.h"
|
|
||||||
#include "mesh/generated/meshtastic/cannedmessages.pb.h"
|
|
||||||
#include "graphics/images.h"
|
|
||||||
#include "modules/AdminModule.h"
|
|
||||||
#include "graphics/SharedUIDisplay.h"
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include "main.h" // for cardkb_found
|
#include "graphics/images.h"
|
||||||
|
#include "input/ScanAndSelect.h"
|
||||||
|
#include "main.h" // for cardkb_found
|
||||||
|
#include "mesh/generated/meshtastic/cannedmessages.pb.h"
|
||||||
|
#include "modules/AdminModule.h"
|
||||||
#include "modules/ExternalNotificationModule.h" // for buzzer control
|
#include "modules/ExternalNotificationModule.h" // for buzzer control
|
||||||
#if !MESHTASTIC_EXCLUDE_GPS
|
#if !MESHTASTIC_EXCLUDE_GPS
|
||||||
#include "GPS.h"
|
#include "GPS.h"
|
||||||
@ -70,7 +70,8 @@ CannedMessageModule::CannedMessageModule()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
static bool returnToCannedList = false;
|
static bool returnToCannedList = false;
|
||||||
bool hasKeyForNode(const meshtastic_NodeInfoLite* node) {
|
bool hasKeyForNode(const meshtastic_NodeInfoLite *node)
|
||||||
|
{
|
||||||
return node && node->has_user && node->user.public_key.size > 0;
|
return node && node->has_user && node->user.public_key.size > 0;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -96,7 +97,7 @@ int CannedMessageModule::splitConfiguredMessages()
|
|||||||
strncpy(this->messageStore, canned_messages.c_str(), sizeof(this->messageStore));
|
strncpy(this->messageStore, canned_messages.c_str(), sizeof(this->messageStore));
|
||||||
|
|
||||||
// Temporary array to allow for insertion
|
// Temporary array to allow for insertion
|
||||||
const char* tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0};
|
const char *tempMessages[CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT + 3] = {0};
|
||||||
int tempCount = 0;
|
int tempCount = 0;
|
||||||
|
|
||||||
// First message always starts at buffer start
|
// First message always starts at buffer start
|
||||||
@ -117,12 +118,14 @@ int CannedMessageModule::splitConfiguredMessages()
|
|||||||
// Insert "[Select Destination]" after Free Text if present, otherwise at the top
|
// Insert "[Select Destination]" after Free Text if present, otherwise at the top
|
||||||
#if defined(USE_VIRTUAL_KEYBOARD)
|
#if defined(USE_VIRTUAL_KEYBOARD)
|
||||||
// Insert at position 1 (after Free Text)
|
// Insert at position 1 (after Free Text)
|
||||||
for (int j = tempCount; j > 1; j--) tempMessages[j] = tempMessages[j - 1];
|
for (int j = tempCount; j > 1; j--)
|
||||||
|
tempMessages[j] = tempMessages[j - 1];
|
||||||
tempMessages[1] = "[Select Destination]";
|
tempMessages[1] = "[Select Destination]";
|
||||||
tempCount++;
|
tempCount++;
|
||||||
#else
|
#else
|
||||||
// Insert at position 0 (top)
|
// Insert at position 0 (top)
|
||||||
for (int j = tempCount; j > 0; j--) tempMessages[j] = tempMessages[j - 1];
|
for (int j = tempCount; j > 0; j--)
|
||||||
|
tempMessages[j] = tempMessages[j - 1];
|
||||||
tempMessages[0] = "[Select Destination]";
|
tempMessages[0] = "[Select Destination]";
|
||||||
tempCount++;
|
tempCount++;
|
||||||
#endif
|
#endif
|
||||||
@ -132,13 +135,14 @@ int CannedMessageModule::splitConfiguredMessages()
|
|||||||
|
|
||||||
// Copy to the member array
|
// Copy to the member array
|
||||||
for (int k = 0; k < tempCount; ++k) {
|
for (int k = 0; k < tempCount; ++k) {
|
||||||
this->messages[k] = (char*)tempMessages[k];
|
this->messages[k] = (char *)tempMessages[k];
|
||||||
}
|
}
|
||||||
this->messagesCount = tempCount;
|
this->messagesCount = tempCount;
|
||||||
|
|
||||||
return this->messagesCount;
|
return this->messagesCount;
|
||||||
}
|
}
|
||||||
void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char* buffer) {
|
void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer)
|
||||||
|
{
|
||||||
if (display->getWidth() > 128) {
|
if (display->getWidth() > 128) {
|
||||||
if (this->dest == NODENUM_BROADCAST) {
|
if (this->dest == NODENUM_BROADCAST) {
|
||||||
display->drawStringf(x, y, buffer, "To: Broadcast@%s", channels.getName(this->channel));
|
display->drawStringf(x, y, buffer, "To: Broadcast@%s", channels.getName(this->channel));
|
||||||
@ -154,7 +158,8 @@ void CannedMessageModule::drawHeader(OLEDDisplay *display, int16_t x, int16_t y,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CannedMessageModule::resetSearch() {
|
void CannedMessageModule::resetSearch()
|
||||||
|
{
|
||||||
LOG_INFO("Resetting search, restoring full destination list");
|
LOG_INFO("Resetting search, restoring full destination list");
|
||||||
|
|
||||||
int previousDestIndex = destIndex;
|
int previousDestIndex = destIndex;
|
||||||
@ -165,14 +170,16 @@ void CannedMessageModule::resetSearch() {
|
|||||||
// Adjust scrollIndex so previousDestIndex is still visible
|
// Adjust scrollIndex so previousDestIndex is still visible
|
||||||
int totalEntries = activeChannelIndices.size() + filteredNodes.size();
|
int totalEntries = activeChannelIndices.size() + filteredNodes.size();
|
||||||
this->visibleRows = (displayHeight - FONT_HEIGHT_SMALL * 2) / FONT_HEIGHT_SMALL;
|
this->visibleRows = (displayHeight - FONT_HEIGHT_SMALL * 2) / FONT_HEIGHT_SMALL;
|
||||||
if (this->visibleRows < 1) this->visibleRows = 1;
|
if (this->visibleRows < 1)
|
||||||
|
this->visibleRows = 1;
|
||||||
int maxScrollIndex = std::max(0, totalEntries - visibleRows);
|
int maxScrollIndex = std::max(0, totalEntries - visibleRows);
|
||||||
scrollIndex = std::min(std::max(previousDestIndex - (visibleRows / 2), 0), maxScrollIndex);
|
scrollIndex = std::min(std::max(previousDestIndex - (visibleRows / 2), 0), maxScrollIndex);
|
||||||
|
|
||||||
lastUpdateMillis = millis();
|
lastUpdateMillis = millis();
|
||||||
requestFocus();
|
requestFocus();
|
||||||
}
|
}
|
||||||
void CannedMessageModule::updateFilteredNodes() {
|
void CannedMessageModule::updateFilteredNodes()
|
||||||
|
{
|
||||||
static size_t lastNumMeshNodes = 0;
|
static size_t lastNumMeshNodes = 0;
|
||||||
static String lastSearchQuery = "";
|
static String lastSearchQuery = "";
|
||||||
|
|
||||||
@ -181,7 +188,8 @@ void CannedMessageModule::updateFilteredNodes() {
|
|||||||
lastNumMeshNodes = numMeshNodes;
|
lastNumMeshNodes = numMeshNodes;
|
||||||
|
|
||||||
// Early exit if nothing changed
|
// Early exit if nothing changed
|
||||||
if (searchQuery == lastSearchQuery && !nodesChanged) return;
|
if (searchQuery == lastSearchQuery && !nodesChanged)
|
||||||
|
return;
|
||||||
lastSearchQuery = searchQuery;
|
lastSearchQuery = searchQuery;
|
||||||
needsUpdate = false;
|
needsUpdate = false;
|
||||||
|
|
||||||
@ -196,10 +204,11 @@ void CannedMessageModule::updateFilteredNodes() {
|
|||||||
this->filteredNodes.reserve(numMeshNodes);
|
this->filteredNodes.reserve(numMeshNodes);
|
||||||
|
|
||||||
for (size_t i = 0; i < numMeshNodes; ++i) {
|
for (size_t i = 0; i < numMeshNodes; ++i) {
|
||||||
meshtastic_NodeInfoLite* node = nodeDB->getMeshNodeByIndex(i);
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||||
if (!node || node->num == myNodeNum) continue;
|
if (!node || node->num == myNodeNum)
|
||||||
|
continue;
|
||||||
|
|
||||||
const String& nodeName = node->user.long_name;
|
const String &nodeName = node->user.long_name;
|
||||||
|
|
||||||
if (searchQuery.length() == 0) {
|
if (searchQuery.length() == 0) {
|
||||||
this->filteredNodes.push_back({node, sinceLastSeen(node)});
|
this->filteredNodes.push_back({node, sinceLastSeen(node)});
|
||||||
@ -226,13 +235,13 @@ void CannedMessageModule::updateFilteredNodes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sort by favorite, then last heard
|
// Sort by favorite, then last heard
|
||||||
std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry& a, const NodeEntry& b) {
|
std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry &a, const NodeEntry &b) {
|
||||||
if (a.node->is_favorite != b.node->is_favorite)
|
if (a.node->is_favorite != b.node->is_favorite)
|
||||||
return a.node->is_favorite > b.node->is_favorite;
|
return a.node->is_favorite > b.node->is_favorite;
|
||||||
return a.lastHeard < b.lastHeard;
|
return a.lastHeard < b.lastHeard;
|
||||||
});
|
});
|
||||||
scrollIndex = 0; // Show first result at the top
|
scrollIndex = 0; // Show first result at the top
|
||||||
destIndex = 0; // Highlight the first entry
|
destIndex = 0; // Highlight the first entry
|
||||||
if (nodesChanged) {
|
if (nodesChanged) {
|
||||||
LOG_INFO("Nodes changed, forcing UI refresh.");
|
LOG_INFO("Nodes changed, forcing UI refresh.");
|
||||||
screen->forceDisplay();
|
screen->forceDisplay();
|
||||||
@ -240,24 +249,28 @@ void CannedMessageModule::updateFilteredNodes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns true if character input is currently allowed (used for search/freetext states)
|
// Returns true if character input is currently allowed (used for search/freetext states)
|
||||||
bool CannedMessageModule::isCharInputAllowed() const {
|
bool CannedMessageModule::isCharInputAllowed() const
|
||||||
return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT ||
|
{
|
||||||
runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION;
|
return runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Main input event dispatcher for CannedMessageModule.
|
* Main input event dispatcher for CannedMessageModule.
|
||||||
* Routes keyboard/button/touch input to the correct handler based on the current runState.
|
* Routes keyboard/button/touch input to the correct handler based on the current runState.
|
||||||
* Only one handler (per state) processes each event, eliminating redundancy.
|
* Only one handler (per state) processes each event, eliminating redundancy.
|
||||||
*/
|
*/
|
||||||
int CannedMessageModule::handleInputEvent(const InputEvent* event) {
|
int CannedMessageModule::handleInputEvent(const InputEvent *event)
|
||||||
|
{
|
||||||
// Allow input only from configured source (hardware/software filter)
|
// Allow input only from configured source (hardware/software filter)
|
||||||
if (!isInputSourceAllowed(event)) return 0;
|
if (!isInputSourceAllowed(event))
|
||||||
|
return 0;
|
||||||
|
|
||||||
// Global/system commands always processed (brightness, BT, GPS, shutdown, etc.)
|
// Global/system commands always processed (brightness, BT, GPS, shutdown, etc.)
|
||||||
if (handleSystemCommandInput(event)) return 1;
|
if (handleSystemCommandInput(event))
|
||||||
|
return 1;
|
||||||
|
|
||||||
// Tab key: Always allow switching between canned/destination screens
|
// Tab key: Always allow switching between canned/destination screens
|
||||||
if (event->kbchar == INPUT_BROKER_MSG_TAB && handleTabSwitch(event)) return 1;
|
if (event->kbchar == INPUT_BROKER_MSG_TAB && handleTabSwitch(event))
|
||||||
|
return 1;
|
||||||
|
|
||||||
// Matrix keypad: If matrix key, trigger action select for canned message
|
// Matrix keypad: If matrix key, trigger action select for canned message
|
||||||
if (event->inputEvent == static_cast<char>(MATRIXKEY)) {
|
if (event->inputEvent == static_cast<char>(MATRIXKEY)) {
|
||||||
@ -276,106 +289,106 @@ int CannedMessageModule::handleInputEvent(const InputEvent* event) {
|
|||||||
|
|
||||||
// Route event to handler for current UI state (no double-handling)
|
// Route event to handler for current UI state (no double-handling)
|
||||||
switch (runState) {
|
switch (runState) {
|
||||||
// Node/Channel destination selection mode: Handles character search, arrows, select, cancel, backspace
|
// Node/Channel destination selection mode: Handles character search, arrows, select, cancel, backspace
|
||||||
case CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION:
|
case CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION:
|
||||||
return handleDestinationSelectionInput(event, isUp, isDown, isSelect); // All allowed input for this state
|
return handleDestinationSelectionInput(event, isUp, isDown, isSelect); // All allowed input for this state
|
||||||
|
|
||||||
// Free text input mode: Handles character input, cancel, backspace, select, etc.
|
// Free text input mode: Handles character input, cancel, backspace, select, etc.
|
||||||
case CANNED_MESSAGE_RUN_STATE_FREETEXT:
|
case CANNED_MESSAGE_RUN_STATE_FREETEXT:
|
||||||
return handleFreeTextInput(event); // All allowed input for this state
|
return handleFreeTextInput(event); // All allowed input for this state
|
||||||
|
|
||||||
// If sending, block all input except global/system (handled above)
|
// If sending, block all input except global/system (handled above)
|
||||||
case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE:
|
case CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE:
|
||||||
return 1;
|
return 1;
|
||||||
|
|
||||||
case CANNED_MESSAGE_RUN_STATE_INACTIVE:
|
case CANNED_MESSAGE_RUN_STATE_INACTIVE:
|
||||||
if (isSelect) {
|
if (isSelect) {
|
||||||
// When inactive, call the onebutton shortpress instead. Activate module only on up/down
|
// When inactive, call the onebutton shortpress instead. Activate module only on up/down
|
||||||
powerFSM.trigger(EVENT_PRESS);
|
powerFSM.trigger(EVENT_PRESS);
|
||||||
return 1; // Let caller know we handled it
|
return 1; // Let caller know we handled it
|
||||||
}
|
}
|
||||||
// Let LEFT/RIGHT pass through so frame navigation works
|
// Let LEFT/RIGHT pass through so frame navigation works
|
||||||
if (
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT) ||
|
||||||
event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT) ||
|
event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) {
|
||||||
event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)
|
break;
|
||||||
) {
|
}
|
||||||
break;
|
// Handle UP/DOWN: activate canned message list!
|
||||||
}
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP) ||
|
||||||
// Handle UP/DOWN: activate canned message list!
|
event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN)) {
|
||||||
if (
|
// Always select the first real canned message on activation
|
||||||
event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP) ||
|
int firstRealMsgIdx = 0;
|
||||||
event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN)
|
for (int i = 0; i < messagesCount; ++i) {
|
||||||
) {
|
if (strcmp(messages[i], "[Select Destination]") != 0 && strcmp(messages[i], "[Exit]") != 0 &&
|
||||||
// Always select the first real canned message on activation
|
strcmp(messages[i], "[---- Free Text ----]") != 0) {
|
||||||
int firstRealMsgIdx = 0;
|
firstRealMsgIdx = i;
|
||||||
for (int i = 0; i < messagesCount; ++i) {
|
break;
|
||||||
if (strcmp(messages[i], "[Select Destination]") != 0 &&
|
|
||||||
strcmp(messages[i], "[Exit]") != 0 &&
|
|
||||||
strcmp(messages[i], "[---- Free Text ----]") != 0) {
|
|
||||||
firstRealMsgIdx = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
currentMessageIndex = firstRealMsgIdx;
|
|
||||||
|
|
||||||
// This triggers the canned message list
|
|
||||||
runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
|
|
||||||
requestFocus();
|
|
||||||
UIFrameEvent e;
|
|
||||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
|
||||||
notifyObservers(&e);
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
// Printable char (ASCII) opens free text compose
|
currentMessageIndex = firstRealMsgIdx;
|
||||||
if (event->kbchar >= 32 && event->kbchar <= 126) {
|
|
||||||
runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
|
|
||||||
requestFocus();
|
|
||||||
UIFrameEvent e;
|
|
||||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
|
||||||
notifyObservers(&e);
|
|
||||||
// Immediately process the input in the new state (freetext)
|
|
||||||
return handleFreeTextInput(event);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
// (Other states can be added here as needed)
|
// This triggers the canned message list
|
||||||
default:
|
runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
|
||||||
break;
|
requestFocus();
|
||||||
|
UIFrameEvent e;
|
||||||
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||||
|
notifyObservers(&e);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
// Printable char (ASCII) opens free text compose
|
||||||
|
if (event->kbchar >= 32 && event->kbchar <= 126) {
|
||||||
|
runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
|
||||||
|
requestFocus();
|
||||||
|
UIFrameEvent e;
|
||||||
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||||
|
notifyObservers(&e);
|
||||||
|
// Immediately process the input in the new state (freetext)
|
||||||
|
return handleFreeTextInput(event);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
// (Other states can be added here as needed)
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no state handler above processed the event, let the message selector try to handle it
|
// If no state handler above processed the event, let the message selector try to handle it
|
||||||
// (Handles up/down/select on canned message list, exit/return)
|
// (Handles up/down/select on canned message list, exit/return)
|
||||||
if (handleMessageSelectorInput(event, isUp, isDown, isSelect)) return 1;
|
if (handleMessageSelectorInput(event, isUp, isDown, isSelect))
|
||||||
|
return 1;
|
||||||
|
|
||||||
// Default: event not handled by canned message system, allow others to process
|
// Default: event not handled by canned message system, allow others to process
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CannedMessageModule::isInputSourceAllowed(const InputEvent* event) {
|
bool CannedMessageModule::isInputSourceAllowed(const InputEvent *event)
|
||||||
|
{
|
||||||
return strlen(moduleConfig.canned_message.allow_input_source) == 0 ||
|
return strlen(moduleConfig.canned_message.allow_input_source) == 0 ||
|
||||||
strcasecmp(moduleConfig.canned_message.allow_input_source, event->source) == 0 ||
|
strcasecmp(moduleConfig.canned_message.allow_input_source, event->source) == 0 ||
|
||||||
strcasecmp(moduleConfig.canned_message.allow_input_source, "_any") == 0;
|
strcasecmp(moduleConfig.canned_message.allow_input_source, "_any") == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CannedMessageModule::isUpEvent(const InputEvent* event) {
|
bool CannedMessageModule::isUpEvent(const InputEvent *event)
|
||||||
|
{
|
||||||
return event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP);
|
return event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP);
|
||||||
}
|
}
|
||||||
bool CannedMessageModule::isDownEvent(const InputEvent* event) {
|
bool CannedMessageModule::isDownEvent(const InputEvent *event)
|
||||||
|
{
|
||||||
return event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN);
|
return event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN);
|
||||||
}
|
}
|
||||||
bool CannedMessageModule::isSelectEvent(const InputEvent* event) {
|
bool CannedMessageModule::isSelectEvent(const InputEvent *event)
|
||||||
|
{
|
||||||
return event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT);
|
return event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CannedMessageModule::handleTabSwitch(const InputEvent* event) {
|
bool CannedMessageModule::handleTabSwitch(const InputEvent *event)
|
||||||
if (event->kbchar != 0x09) return false;
|
{
|
||||||
|
if (event->kbchar != 0x09)
|
||||||
|
return false;
|
||||||
|
|
||||||
destSelect = (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE)
|
destSelect = (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) ? CANNED_MESSAGE_DESTINATION_TYPE_NONE
|
||||||
? CANNED_MESSAGE_DESTINATION_TYPE_NONE
|
: CANNED_MESSAGE_DESTINATION_TYPE_NODE;
|
||||||
: CANNED_MESSAGE_DESTINATION_TYPE_NODE;
|
runState = (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) ? CANNED_MESSAGE_RUN_STATE_FREETEXT
|
||||||
runState = (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE)
|
: CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION;
|
||||||
? CANNED_MESSAGE_RUN_STATE_FREETEXT
|
|
||||||
: CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION;
|
|
||||||
|
|
||||||
destIndex = 0;
|
destIndex = 0;
|
||||||
scrollIndex = 0;
|
scrollIndex = 0;
|
||||||
@ -390,11 +403,11 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent* event) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int CannedMessageModule::handleDestinationSelectionInput(const InputEvent* event, bool isUp, bool isDown, bool isSelect) {
|
int CannedMessageModule::handleDestinationSelectionInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect)
|
||||||
|
{
|
||||||
static bool shouldRedraw = false;
|
static bool shouldRedraw = false;
|
||||||
|
|
||||||
if (event->kbchar >= 32 && event->kbchar <= 126 &&
|
if (event->kbchar >= 32 && event->kbchar <= 126 && !isUp && !isDown &&
|
||||||
!isUp && !isDown &&
|
|
||||||
event->inputEvent != static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT) &&
|
event->inputEvent != static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT) &&
|
||||||
event->inputEvent != static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT) &&
|
event->inputEvent != static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT) &&
|
||||||
event->inputEvent != static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) {
|
event->inputEvent != static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) {
|
||||||
@ -458,7 +471,7 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent* event
|
|||||||
} else {
|
} else {
|
||||||
int nodeIndex = destIndex - static_cast<int>(activeChannelIndices.size());
|
int nodeIndex = destIndex - static_cast<int>(activeChannelIndices.size());
|
||||||
if (nodeIndex >= 0 && nodeIndex < static_cast<int>(filteredNodes.size())) {
|
if (nodeIndex >= 0 && nodeIndex < static_cast<int>(filteredNodes.size())) {
|
||||||
meshtastic_NodeInfoLite* selectedNode = filteredNodes[nodeIndex].node;
|
meshtastic_NodeInfoLite *selectedNode = filteredNodes[nodeIndex].node;
|
||||||
if (selectedNode) {
|
if (selectedNode) {
|
||||||
dest = selectedNode->num;
|
dest = selectedNode->num;
|
||||||
channel = selectedNode->channel;
|
channel = selectedNode->channel;
|
||||||
@ -489,8 +502,10 @@ int CannedMessageModule::handleDestinationSelectionInput(const InputEvent* event
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CannedMessageModule::handleMessageSelectorInput(const InputEvent* event, bool isUp, bool isDown, bool isSelect) {
|
bool CannedMessageModule::handleMessageSelectorInput(const InputEvent *event, bool isUp, bool isDown, bool isSelect)
|
||||||
if (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) return false;
|
{
|
||||||
|
if (destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE)
|
||||||
|
return false;
|
||||||
|
|
||||||
// === Handle Cancel key: go inactive, clear UI state ===
|
// === Handle Cancel key: go inactive, clear UI state ===
|
||||||
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) {
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) {
|
||||||
@ -518,7 +533,7 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent* event, bo
|
|||||||
runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN;
|
runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN;
|
||||||
handled = true;
|
handled = true;
|
||||||
} else if (isSelect) {
|
} else if (isSelect) {
|
||||||
const char* current = messages[currentMessageIndex];
|
const char *current = messages[currentMessageIndex];
|
||||||
|
|
||||||
// === [Select Destination] triggers destination selection UI ===
|
// === [Select Destination] triggers destination selection UI ===
|
||||||
if (strcmp(current, "[Select Destination]") == 0) {
|
if (strcmp(current, "[Select Destination]") == 0) {
|
||||||
@ -579,9 +594,11 @@ bool CannedMessageModule::handleMessageSelectorInput(const InputEvent* event, bo
|
|||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) {
|
bool CannedMessageModule::handleFreeTextInput(const InputEvent *event)
|
||||||
|
{
|
||||||
// Always process only if in FREETEXT mode
|
// Always process only if in FREETEXT mode
|
||||||
if (runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) return false;
|
if (runState != CANNED_MESSAGE_RUN_STATE_FREETEXT)
|
||||||
|
return false;
|
||||||
|
|
||||||
#if defined(USE_VIRTUAL_KEYBOARD)
|
#if defined(USE_VIRTUAL_KEYBOARD)
|
||||||
// Touch input (virtual keyboard) handling
|
// Touch input (virtual keyboard) handling
|
||||||
@ -596,9 +613,9 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) {
|
|||||||
shift = !shift;
|
shift = !shift;
|
||||||
valid = true;
|
valid = true;
|
||||||
} else if (keyTapped == "⌫") {
|
} else if (keyTapped == "⌫") {
|
||||||
#ifndef RAK14014
|
#ifndef RAK14014
|
||||||
highlight = keyTapped[0];
|
highlight = keyTapped[0];
|
||||||
#endif
|
#endif
|
||||||
payload = 0x08;
|
payload = 0x08;
|
||||||
shift = false;
|
shift = false;
|
||||||
valid = true;
|
valid = true;
|
||||||
@ -608,13 +625,13 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) {
|
|||||||
charSet = (charSet == 0 ? 1 : 0);
|
charSet = (charSet == 0 ? 1 : 0);
|
||||||
valid = true;
|
valid = true;
|
||||||
} else if (keyTapped == " ") {
|
} else if (keyTapped == " ") {
|
||||||
#ifndef RAK14014
|
#ifndef RAK14014
|
||||||
highlight = keyTapped[0];
|
highlight = keyTapped[0];
|
||||||
#endif
|
#endif
|
||||||
payload = keyTapped[0];
|
payload = keyTapped[0];
|
||||||
shift = false;
|
shift = false;
|
||||||
valid = true;
|
valid = true;
|
||||||
}
|
}
|
||||||
// Touch enter/submit
|
// Touch enter/submit
|
||||||
else if (keyTapped == "↵") {
|
else if (keyTapped == "↵") {
|
||||||
runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message!
|
runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT; // Send the message!
|
||||||
@ -623,9 +640,9 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) {
|
|||||||
shift = false;
|
shift = false;
|
||||||
valid = true;
|
valid = true;
|
||||||
} else if (!keyTapped.isEmpty()) {
|
} else if (!keyTapped.isEmpty()) {
|
||||||
#ifndef RAK14014
|
#ifndef RAK14014
|
||||||
highlight = keyTapped[0];
|
highlight = keyTapped[0];
|
||||||
#endif
|
#endif
|
||||||
payload = shift ? keyTapped[0] : std::tolower(keyTapped[0]);
|
payload = shift ? keyTapped[0] : std::tolower(keyTapped[0]);
|
||||||
shift = false;
|
shift = false;
|
||||||
valid = true;
|
valid = true;
|
||||||
@ -643,11 +660,13 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) {
|
|||||||
// Confirm select (Enter)
|
// Confirm select (Enter)
|
||||||
bool isSelect = isSelectEvent(event);
|
bool isSelect = isSelectEvent(event);
|
||||||
if (isSelect) {
|
if (isSelect) {
|
||||||
LOG_DEBUG("[SELECT] handleFreeTextInput: runState=%d, dest=%u, channel=%d, freetext='%s'",
|
LOG_DEBUG("[SELECT] handleFreeTextInput: runState=%d, dest=%u, channel=%d, freetext='%s'", (int)runState, dest, channel,
|
||||||
(int)runState, dest, channel, freetext.c_str());
|
freetext.c_str());
|
||||||
if (dest == 0) dest = NODENUM_BROADCAST;
|
if (dest == 0)
|
||||||
|
dest = NODENUM_BROADCAST;
|
||||||
// Defensive: If channel isn't valid, pick the first available channel
|
// Defensive: If channel isn't valid, pick the first available channel
|
||||||
if (channel >= channels.getNumChannels()) channel = 0;
|
if (channel >= channels.getNumChannels())
|
||||||
|
channel = 0;
|
||||||
|
|
||||||
payload = CANNED_MESSAGE_RUN_STATE_FREETEXT;
|
payload = CANNED_MESSAGE_RUN_STATE_FREETEXT;
|
||||||
currentMessageIndex = -1;
|
currentMessageIndex = -1;
|
||||||
@ -706,9 +725,11 @@ bool CannedMessageModule::handleFreeTextInput(const InputEvent* event) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CannedMessageModule::handleSystemCommandInput(const InputEvent* event) {
|
bool CannedMessageModule::handleSystemCommandInput(const InputEvent *event)
|
||||||
|
{
|
||||||
// Only respond to "ANYKEY" events for system keys
|
// Only respond to "ANYKEY" events for system keys
|
||||||
if (event->inputEvent != static_cast<char>(ANYKEY)) return false;
|
if (event->inputEvent != static_cast<char>(ANYKEY))
|
||||||
|
return false;
|
||||||
|
|
||||||
// Block ALL input if an alert banner is active
|
// Block ALL input if an alert banner is active
|
||||||
extern String alertBannerMessage;
|
extern String alertBannerMessage;
|
||||||
@ -719,101 +740,114 @@ bool CannedMessageModule::handleSystemCommandInput(const InputEvent* event) {
|
|||||||
|
|
||||||
// System commands (all others fall through to return false)
|
// System commands (all others fall through to return false)
|
||||||
switch (event->kbchar) {
|
switch (event->kbchar) {
|
||||||
// Fn key symbols
|
// Fn key symbols
|
||||||
case INPUT_BROKER_MSG_FN_SYMBOL_ON:
|
case INPUT_BROKER_MSG_FN_SYMBOL_ON:
|
||||||
if (screen) screen->setFunctionSymbol("Fn");
|
if (screen)
|
||||||
return true;
|
screen->setFunctionSymbol("Fn");
|
||||||
case INPUT_BROKER_MSG_FN_SYMBOL_OFF:
|
return true;
|
||||||
if (screen) screen->removeFunctionSymbol("Fn");
|
case INPUT_BROKER_MSG_FN_SYMBOL_OFF:
|
||||||
return true;
|
if (screen)
|
||||||
// Brightness
|
screen->removeFunctionSymbol("Fn");
|
||||||
case INPUT_BROKER_MSG_BRIGHTNESS_UP:
|
return true;
|
||||||
if (screen) screen->increaseBrightness();
|
// Brightness
|
||||||
LOG_DEBUG("Increase Screen Brightness");
|
case INPUT_BROKER_MSG_BRIGHTNESS_UP:
|
||||||
return true;
|
if (screen)
|
||||||
case INPUT_BROKER_MSG_BRIGHTNESS_DOWN:
|
screen->increaseBrightness();
|
||||||
if (screen) screen->decreaseBrightness();
|
LOG_DEBUG("Increase Screen Brightness");
|
||||||
LOG_DEBUG("Decrease Screen Brightness");
|
return true;
|
||||||
return true;
|
case INPUT_BROKER_MSG_BRIGHTNESS_DOWN:
|
||||||
// Mute
|
if (screen)
|
||||||
case INPUT_BROKER_MSG_MUTE_TOGGLE:
|
screen->decreaseBrightness();
|
||||||
if (moduleConfig.external_notification.enabled && externalNotificationModule) {
|
LOG_DEBUG("Decrease Screen Brightness");
|
||||||
bool isMuted = externalNotificationModule->getMute();
|
return true;
|
||||||
externalNotificationModule->setMute(!isMuted);
|
// Mute
|
||||||
graphics::isMuted = !isMuted;
|
case INPUT_BROKER_MSG_MUTE_TOGGLE:
|
||||||
if (!isMuted)
|
if (moduleConfig.external_notification.enabled && externalNotificationModule) {
|
||||||
externalNotificationModule->stopNow();
|
bool isMuted = externalNotificationModule->getMute();
|
||||||
if (screen)
|
externalNotificationModule->setMute(!isMuted);
|
||||||
screen->showOverlayBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);
|
graphics::isMuted = !isMuted;
|
||||||
}
|
if (!isMuted)
|
||||||
return true;
|
externalNotificationModule->stopNow();
|
||||||
// Bluetooth
|
if (screen)
|
||||||
case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE:
|
screen->showOverlayBanner(isMuted ? "Notifications\nEnabled" : "Notifications\nDisabled", 3000);
|
||||||
config.bluetooth.enabled = !config.bluetooth.enabled;
|
}
|
||||||
LOG_INFO("User toggled Bluetooth");
|
return true;
|
||||||
nodeDB->saveToDisk();
|
// Bluetooth
|
||||||
#if defined(ARDUINO_ARCH_NRF52)
|
case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE:
|
||||||
if (!config.bluetooth.enabled) {
|
config.bluetooth.enabled = !config.bluetooth.enabled;
|
||||||
disableBluetooth();
|
LOG_INFO("User toggled Bluetooth");
|
||||||
if (screen) screen->showOverlayBanner("Bluetooth OFF\nRebooting", 3000);
|
nodeDB->saveToDisk();
|
||||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000;
|
#if defined(ARDUINO_ARCH_NRF52)
|
||||||
} else {
|
if (!config.bluetooth.enabled) {
|
||||||
if (screen) screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000);
|
disableBluetooth();
|
||||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
if (screen)
|
||||||
}
|
screen->showOverlayBanner("Bluetooth OFF\nRebooting", 3000);
|
||||||
#else
|
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 2000;
|
||||||
if (!config.bluetooth.enabled) {
|
} else {
|
||||||
disableBluetooth();
|
if (screen)
|
||||||
if (screen) screen->showOverlayBanner("Bluetooth OFF", 3000);
|
screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000);
|
||||||
} else {
|
|
||||||
if (screen) screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000);
|
|
||||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
return true;
|
|
||||||
// GPS
|
|
||||||
case INPUT_BROKER_MSG_GPS_TOGGLE:
|
|
||||||
#if !MESHTASTIC_EXCLUDE_GPS
|
|
||||||
if (gps) {
|
|
||||||
gps->toggleGpsMode();
|
|
||||||
const char* msg = (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED)
|
|
||||||
? "GPS Enabled" : "GPS Disabled";
|
|
||||||
if (screen) {
|
|
||||||
screen->forceDisplay();
|
|
||||||
screen->showOverlayBanner(msg, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
return true;
|
|
||||||
// Mesh ping
|
|
||||||
case INPUT_BROKER_MSG_SEND_PING:
|
|
||||||
service->refreshLocalMeshNode();
|
|
||||||
if (service->trySendPosition(NODENUM_BROADCAST, true)) {
|
|
||||||
if (screen) screen->showOverlayBanner("Position\nUpdate Sent", 3000);
|
|
||||||
} else {
|
|
||||||
if (screen) screen->showOverlayBanner("Node Info\nUpdate Sent", 3000);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
// Power control
|
|
||||||
case INPUT_BROKER_MSG_SHUTDOWN:
|
|
||||||
if (screen) screen->showOverlayBanner("Shutting down...");
|
|
||||||
nodeDB->saveToDisk();
|
|
||||||
shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000;
|
|
||||||
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
||||||
return true;
|
|
||||||
case INPUT_BROKER_MSG_REBOOT:
|
|
||||||
if (screen) screen->showOverlayBanner("Rebooting...", 0);
|
|
||||||
nodeDB->saveToDisk();
|
|
||||||
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||||
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
}
|
||||||
return true;
|
#else
|
||||||
case INPUT_BROKER_MSG_DISMISS_FRAME:
|
if (!config.bluetooth.enabled) {
|
||||||
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
disableBluetooth();
|
||||||
if (screen) screen->dismissCurrentFrame();
|
if (screen)
|
||||||
return true;
|
screen->showOverlayBanner("Bluetooth OFF", 3000);
|
||||||
// Not a system command, let other handlers process it
|
} else {
|
||||||
default:
|
if (screen)
|
||||||
return false;
|
screen->showOverlayBanner("Bluetooth ON\nRebooting", 3000);
|
||||||
|
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
// GPS
|
||||||
|
case INPUT_BROKER_MSG_GPS_TOGGLE:
|
||||||
|
#if !MESHTASTIC_EXCLUDE_GPS
|
||||||
|
if (gps) {
|
||||||
|
gps->toggleGpsMode();
|
||||||
|
const char *msg =
|
||||||
|
(config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED) ? "GPS Enabled" : "GPS Disabled";
|
||||||
|
if (screen) {
|
||||||
|
screen->forceDisplay();
|
||||||
|
screen->showOverlayBanner(msg, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
// Mesh ping
|
||||||
|
case INPUT_BROKER_MSG_SEND_PING:
|
||||||
|
service->refreshLocalMeshNode();
|
||||||
|
if (service->trySendPosition(NODENUM_BROADCAST, true)) {
|
||||||
|
if (screen)
|
||||||
|
screen->showOverlayBanner("Position\nUpdate Sent", 3000);
|
||||||
|
} else {
|
||||||
|
if (screen)
|
||||||
|
screen->showOverlayBanner("Node Info\nUpdate Sent", 3000);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
// Power control
|
||||||
|
case INPUT_BROKER_MSG_SHUTDOWN:
|
||||||
|
if (screen)
|
||||||
|
screen->showOverlayBanner("Shutting down...");
|
||||||
|
nodeDB->saveToDisk();
|
||||||
|
shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000;
|
||||||
|
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||||
|
return true;
|
||||||
|
case INPUT_BROKER_MSG_REBOOT:
|
||||||
|
if (screen)
|
||||||
|
screen->showOverlayBanner("Rebooting...", 0);
|
||||||
|
nodeDB->saveToDisk();
|
||||||
|
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
||||||
|
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||||
|
return true;
|
||||||
|
case INPUT_BROKER_MSG_DISMISS_FRAME:
|
||||||
|
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||||
|
if (screen)
|
||||||
|
screen->dismissCurrentFrame();
|
||||||
|
return true;
|
||||||
|
// Not a system command, let other handlers process it
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -834,9 +868,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
|
|||||||
memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size);
|
memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size);
|
||||||
|
|
||||||
// Optionally add bell character
|
// Optionally add bell character
|
||||||
if (moduleConfig.canned_message.send_bell &&
|
if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) {
|
||||||
p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN)
|
|
||||||
{
|
|
||||||
p->decoded.payload.bytes[p->decoded.payload.size++] = 7; // Bell
|
p->decoded.payload.bytes[p->decoded.payload.size++] = 7; // Bell
|
||||||
p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; // Null-terminate
|
p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; // Null-terminate
|
||||||
}
|
}
|
||||||
@ -845,8 +877,7 @@ void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const cha
|
|||||||
this->waitingForAck = true;
|
this->waitingForAck = true;
|
||||||
|
|
||||||
// Log outgoing message
|
// Log outgoing message
|
||||||
LOG_INFO("Send message id=%d, dest=%x, msg=%.*s",
|
LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes);
|
||||||
p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes);
|
|
||||||
|
|
||||||
// Send to mesh and phone (even if no phone connected, to track ACKs)
|
// Send to mesh and phone (even if no phone connected, to track ACKs)
|
||||||
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
service->sendToMesh(p, RX_SRC_LOCAL, true);
|
||||||
@ -862,8 +893,8 @@ bool validEvent = false;
|
|||||||
unsigned long lastUpdateMillis = 0;
|
unsigned long lastUpdateMillis = 0;
|
||||||
int32_t CannedMessageModule::runOnce()
|
int32_t CannedMessageModule::runOnce()
|
||||||
{
|
{
|
||||||
#define NODE_UPDATE_IDLE_MS 100
|
#define NODE_UPDATE_IDLE_MS 100
|
||||||
#define NODE_UPDATE_ACTIVE_MS 80
|
#define NODE_UPDATE_ACTIVE_MS 80
|
||||||
|
|
||||||
unsigned long updateThreshold = (searchQuery.length() > 0) ? NODE_UPDATE_ACTIVE_MS : NODE_UPDATE_IDLE_MS;
|
unsigned long updateThreshold = (searchQuery.length() > 0) ? NODE_UPDATE_ACTIVE_MS : NODE_UPDATE_IDLE_MS;
|
||||||
if (needsUpdate && millis() - lastUpdateMillis > updateThreshold) {
|
if (needsUpdate && millis() - lastUpdateMillis > updateThreshold) {
|
||||||
@ -882,7 +913,8 @@ int32_t CannedMessageModule::runOnce()
|
|||||||
}
|
}
|
||||||
UIFrameEvent e;
|
UIFrameEvent e;
|
||||||
if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) ||
|
if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) ||
|
||||||
(this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) {
|
(this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) ||
|
||||||
|
(this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE_SELECTION)) {
|
||||||
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||||
temporaryMessage = "";
|
temporaryMessage = "";
|
||||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||||
@ -952,8 +984,7 @@ int32_t CannedMessageModule::runOnce()
|
|||||||
// Find first actual canned message (not a special action entry)
|
// Find first actual canned message (not a special action entry)
|
||||||
int firstRealMsgIdx = 0;
|
int firstRealMsgIdx = 0;
|
||||||
for (int i = 0; i < this->messagesCount; ++i) {
|
for (int i = 0; i < this->messagesCount; ++i) {
|
||||||
if (strcmp(this->messages[i], "[Select Destination]") != 0 &&
|
if (strcmp(this->messages[i], "[Select Destination]") != 0 && strcmp(this->messages[i], "[Exit]") != 0 &&
|
||||||
strcmp(this->messages[i], "[Exit]") != 0 &&
|
|
||||||
strcmp(this->messages[i], "[---- Free Text ----]") != 0) {
|
strcmp(this->messages[i], "[---- Free Text ----]") != 0) {
|
||||||
firstRealMsgIdx = i;
|
firstRealMsgIdx = i;
|
||||||
break;
|
break;
|
||||||
@ -1021,24 +1052,23 @@ int32_t CannedMessageModule::runOnce()
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case INPUT_BROKER_MSG_TAB: // Tab key (Switch to Destination Selection Mode)
|
case INPUT_BROKER_MSG_TAB: // Tab key (Switch to Destination Selection Mode)
|
||||||
{
|
{
|
||||||
if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) {
|
if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) {
|
||||||
// Enter selection screen
|
// Enter selection screen
|
||||||
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE;
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE;
|
||||||
this->destIndex = 0; // Reset to first node/channel
|
this->destIndex = 0; // Reset to first node/channel
|
||||||
this->scrollIndex = 0; // Reset scrolling
|
this->scrollIndex = 0; // Reset scrolling
|
||||||
this->runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION;
|
this->runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION;
|
||||||
|
|
||||||
// Ensure UI updates correctly
|
// Ensure UI updates correctly
|
||||||
UIFrameEvent e;
|
UIFrameEvent e;
|
||||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||||
this->notifyObservers(&e);
|
this->notifyObservers(&e);
|
||||||
}
|
|
||||||
|
|
||||||
// If already inside the selection screen, do nothing (prevent exiting)
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
// If already inside the selection screen, do nothing (prevent exiting)
|
||||||
|
return 0;
|
||||||
|
} break;
|
||||||
case INPUT_BROKER_MSG_LEFT:
|
case INPUT_BROKER_MSG_LEFT:
|
||||||
case INPUT_BROKER_MSG_RIGHT:
|
case INPUT_BROKER_MSG_RIGHT:
|
||||||
// already handled above
|
// already handled above
|
||||||
@ -1102,7 +1132,8 @@ const char *CannedMessageModule::getMessageByIndex(int index)
|
|||||||
|
|
||||||
const char *CannedMessageModule::getNodeName(NodeNum node)
|
const char *CannedMessageModule::getNodeName(NodeNum node)
|
||||||
{
|
{
|
||||||
if (node == NODENUM_BROADCAST) return "Broadcast";
|
if (node == NODENUM_BROADCAST)
|
||||||
|
return "Broadcast";
|
||||||
|
|
||||||
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
|
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
|
||||||
if (info && info->has_user && strlen(info->user.long_name) > 0) {
|
if (info && info->has_user && strlen(info->user.long_name) > 0) {
|
||||||
@ -1378,7 +1409,7 @@ bool CannedMessageModule::interceptingKeyboardInput()
|
|||||||
|
|
||||||
void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
this->displayHeight = display->getHeight(); // Store display height for later use
|
this->displayHeight = display->getHeight(); // Store display height for later use
|
||||||
char buffer[50];
|
char buffer[50];
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
@ -1394,7 +1425,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === Destination Selection ===
|
// === Destination Selection ===
|
||||||
if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION || this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) {
|
if (this->runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION ||
|
||||||
|
this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) {
|
||||||
requestFocus();
|
requestFocus();
|
||||||
display->setColor(WHITE); // Always draw cleanly
|
display->setColor(WHITE); // Always draw cleanly
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
@ -1414,15 +1446,19 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
|||||||
int totalEntries = numActiveChannels + this->filteredNodes.size();
|
int totalEntries = numActiveChannels + this->filteredNodes.size();
|
||||||
int columns = 1;
|
int columns = 1;
|
||||||
this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4);
|
this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / (FONT_HEIGHT_SMALL - 4);
|
||||||
if (this->visibleRows < 1) this->visibleRows = 1;
|
if (this->visibleRows < 1)
|
||||||
|
this->visibleRows = 1;
|
||||||
|
|
||||||
// === Clamp scrolling ===
|
// === Clamp scrolling ===
|
||||||
if (scrollIndex > totalEntries / columns) scrollIndex = totalEntries / columns;
|
if (scrollIndex > totalEntries / columns)
|
||||||
if (scrollIndex < 0) scrollIndex = 0;
|
scrollIndex = totalEntries / columns;
|
||||||
|
if (scrollIndex < 0)
|
||||||
|
scrollIndex = 0;
|
||||||
|
|
||||||
for (int row = 0; row < visibleRows; row++) {
|
for (int row = 0; row < visibleRows; row++) {
|
||||||
int itemIndex = scrollIndex + row;
|
int itemIndex = scrollIndex + row;
|
||||||
if (itemIndex >= totalEntries) break;
|
if (itemIndex >= totalEntries)
|
||||||
|
break;
|
||||||
|
|
||||||
int xOffset = 0;
|
int xOffset = 0;
|
||||||
int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset;
|
int yOffset = row * (FONT_HEIGHT_SMALL - 4) + rowYOffset;
|
||||||
@ -1444,7 +1480,8 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entryText.length() == 0 || entryText == "Unknown") entryText = "?";
|
if (entryText.length() == 0 || entryText == "Unknown")
|
||||||
|
entryText = "?";
|
||||||
|
|
||||||
// === Highlight background (if selected) ===
|
// === Highlight background (if selected) ===
|
||||||
if (itemIndex == destIndex) {
|
if (itemIndex == destIndex) {
|
||||||
@ -1496,22 +1533,21 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
|||||||
EINK_ADD_FRAMEFLAG(display, COSMETIC);
|
EINK_ADD_FRAMEFLAG(display, COSMETIC);
|
||||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
|
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
int yOffset = y + 10;
|
int yOffset = y + 10;
|
||||||
#else
|
#else
|
||||||
display->setFont(FONT_MEDIUM);
|
display->setFont(FONT_MEDIUM);
|
||||||
int yOffset = y + 10;
|
int yOffset = y + 10;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// --- Delivery Status Message ---
|
// --- Delivery Status Message ---
|
||||||
if (this->ack) {
|
if (this->ack) {
|
||||||
if (this->lastSentNode == NODENUM_BROADCAST) {
|
if (this->lastSentNode == NODENUM_BROADCAST) {
|
||||||
snprintf(buffer, sizeof(buffer), "Broadcast Sent to\n%s", channels.getName(this->channel));
|
snprintf(buffer, sizeof(buffer), "Broadcast Sent to\n%s", channels.getName(this->channel));
|
||||||
} else if (this->lastAckHopLimit > this->lastAckHopStart) {
|
} else if (this->lastAckHopLimit > this->lastAckHopStart) {
|
||||||
snprintf(buffer, sizeof(buffer), "Delivered (%d hops)\nto %s",
|
snprintf(buffer, sizeof(buffer), "Delivered (%d hops)\nto %s", this->lastAckHopLimit - this->lastAckHopStart,
|
||||||
this->lastAckHopLimit - this->lastAckHopStart,
|
getNodeName(this->incoming));
|
||||||
getNodeName(this->incoming));
|
|
||||||
} else {
|
} else {
|
||||||
snprintf(buffer, sizeof(buffer), "Delivered\nto %s", getNodeName(this->incoming));
|
snprintf(buffer, sizeof(buffer), "Delivered\nto %s", getNodeName(this->incoming));
|
||||||
}
|
}
|
||||||
@ -1522,20 +1558,21 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
|||||||
// Draw delivery message and compute y-offset after text height
|
// Draw delivery message and compute y-offset after text height
|
||||||
int lineCount = 1;
|
int lineCount = 1;
|
||||||
for (const char *ptr = buffer; *ptr; ptr++) {
|
for (const char *ptr = buffer; *ptr; ptr++) {
|
||||||
if (*ptr == '\n') lineCount++;
|
if (*ptr == '\n')
|
||||||
|
lineCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
display->drawString(display->getWidth() / 2 + x, yOffset, buffer);
|
display->drawString(display->getWidth() / 2 + x, yOffset, buffer);
|
||||||
yOffset += lineCount * FONT_HEIGHT_MEDIUM; // only 1 line gap, no extra padding
|
yOffset += lineCount * FONT_HEIGHT_MEDIUM; // only 1 line gap, no extra padding
|
||||||
|
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
// --- SNR + RSSI Compact Line ---
|
// --- SNR + RSSI Compact Line ---
|
||||||
if (this->ack) {
|
if (this->ack) {
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
snprintf(buffer, sizeof(buffer), "SNR: %.1f dB RSSI: %d", this->lastRxSnr, this->lastRxRssi);
|
snprintf(buffer, sizeof(buffer), "SNR: %.1f dB RSSI: %d", this->lastRxSnr, this->lastRxRssi);
|
||||||
display->drawString(display->getWidth() / 2 + x, yOffset, buffer);
|
display->drawString(display->getWidth() / 2 + x, yOffset, buffer);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1566,7 +1603,7 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
|||||||
if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
||||||
requestFocus();
|
requestFocus();
|
||||||
#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
|
#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
|
||||||
EInkDynamicDisplay* einkDisplay = static_cast<EInkDynamicDisplay*>(display);
|
EInkDynamicDisplay *einkDisplay = static_cast<EInkDynamicDisplay *>(display);
|
||||||
einkDisplay->enableUnlimitedFastMode();
|
einkDisplay->enableUnlimitedFastMode();
|
||||||
#endif
|
#endif
|
||||||
#if defined(USE_VIRTUAL_KEYBOARD)
|
#if defined(USE_VIRTUAL_KEYBOARD)
|
||||||
@ -1575,25 +1612,26 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
|||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
|
|
||||||
// --- Draw node/channel header at the top ---
|
// --- Draw node/channel header at the top ---
|
||||||
drawHeader(display, x, y, buffer);
|
drawHeader(display, x, y, buffer);
|
||||||
|
|
||||||
// --- Char count right-aligned ---
|
// --- Char count right-aligned ---
|
||||||
if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) {
|
if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) {
|
||||||
uint16_t charsLeft = meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0);
|
uint16_t charsLeft =
|
||||||
|
meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0);
|
||||||
snprintf(buffer, sizeof(buffer), "%d left", charsLeft);
|
snprintf(buffer, sizeof(buffer), "%d left", charsLeft);
|
||||||
display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer);
|
display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Draw Free Text input, shifted down ---
|
// --- Draw Free Text input, shifted down ---
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(),
|
display->drawStringMaxWidth(0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(),
|
||||||
drawWithCursor(this->freetext, this->cursor));
|
drawWithCursor(this->freetext, this->cursor));
|
||||||
#endif
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Canned Messages List ===
|
// === Canned Messages List ===
|
||||||
if (this->messagesCount > 0) {
|
if (this->messagesCount > 0) {
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
@ -1607,13 +1645,12 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
|||||||
const int listYOffset = y + FONT_HEIGHT_SMALL - 3;
|
const int listYOffset = y + FONT_HEIGHT_SMALL - 3;
|
||||||
const int visibleRows = (display->getHeight() - listYOffset) / rowSpacing;
|
const int visibleRows = (display->getHeight() - listYOffset) / rowSpacing;
|
||||||
|
|
||||||
int topMsg = (messagesCount > visibleRows && currentMessageIndex >= visibleRows - 1)
|
int topMsg =
|
||||||
? currentMessageIndex - visibleRows + 2
|
(messagesCount > visibleRows && currentMessageIndex >= visibleRows - 1) ? currentMessageIndex - visibleRows + 2 : 0;
|
||||||
: 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < std::min(messagesCount, visibleRows); i++) {
|
for (int i = 0; i < std::min(messagesCount, visibleRows); i++) {
|
||||||
int lineY = listYOffset + rowSpacing * i;
|
int lineY = listYOffset + rowSpacing * i;
|
||||||
const char* msg = getMessageByIndex(topMsg + i);
|
const char *msg = getMessageByIndex(topMsg + i);
|
||||||
|
|
||||||
if ((topMsg + i) == currentMessageIndex) {
|
if ((topMsg + i) == currentMessageIndex) {
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
@ -1793,6 +1830,7 @@ String CannedMessageModule::drawWithCursor(String text, int cursor)
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool CannedMessageModule::isInterceptingAndFocused() {
|
bool CannedMessageModule::isInterceptingAndFocused()
|
||||||
|
{
|
||||||
return this->interceptingKeyboardInput();
|
return this->interceptingKeyboardInput();
|
||||||
}
|
}
|
@ -57,10 +57,9 @@ struct NodeEntry {
|
|||||||
// Main Class
|
// Main Class
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
class CannedMessageModule : public SinglePortModule,
|
class CannedMessageModule : public SinglePortModule, public Observable<const UIFrameEvent *>, private concurrency::OSThread
|
||||||
public Observable<const UIFrameEvent *>,
|
{
|
||||||
private concurrency::OSThread {
|
public:
|
||||||
public:
|
|
||||||
CannedMessageModule();
|
CannedMessageModule();
|
||||||
|
|
||||||
// === Message navigation ===
|
// === Message navigation ===
|
||||||
@ -89,19 +88,22 @@ public:
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// === Packet Interest Filter ===
|
// === Packet Interest Filter ===
|
||||||
virtual bool wantPacket(const meshtastic_MeshPacket *p) override {
|
virtual bool wantPacket(const meshtastic_MeshPacket *p) override
|
||||||
if (p->rx_rssi != 0) lastRxRssi = p->rx_rssi;
|
{
|
||||||
if (p->rx_snr > 0) lastRxSnr = p->rx_snr;
|
if (p->rx_rssi != 0)
|
||||||
|
lastRxRssi = p->rx_rssi;
|
||||||
|
if (p->rx_snr > 0)
|
||||||
|
lastRxSnr = p->rx_snr;
|
||||||
return (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) ? waitingForAck : false;
|
return (p->decoded.portnum == meshtastic_PortNum_ROUTING_APP) ? waitingForAck : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// === Thread Entry Point ===
|
// === Thread Entry Point ===
|
||||||
virtual int32_t runOnce() override;
|
virtual int32_t runOnce() override;
|
||||||
|
|
||||||
// === Transmission ===
|
// === Transmission ===
|
||||||
void sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies);
|
void sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies);
|
||||||
void drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char* buffer);
|
void drawHeader(OLEDDisplay *display, int16_t x, int16_t y, char *buffer);
|
||||||
int splitConfiguredMessages();
|
int splitConfiguredMessages();
|
||||||
int getNextIndex();
|
int getNextIndex();
|
||||||
int getPrevIndex();
|
int getPrevIndex();
|
||||||
@ -130,7 +132,7 @@ protected:
|
|||||||
bool saveProtoForModule();
|
bool saveProtoForModule();
|
||||||
void installDefaultCannedMessageModuleConfig();
|
void installDefaultCannedMessageModuleConfig();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// === Input Observers ===
|
// === Input Observers ===
|
||||||
CallbackObserver<CannedMessageModule, const InputEvent *> inputObserver =
|
CallbackObserver<CannedMessageModule, const InputEvent *> inputObserver =
|
||||||
CallbackObserver<CannedMessageModule, const InputEvent *>(this, &CannedMessageModule::handleInputEvent);
|
CallbackObserver<CannedMessageModule, const InputEvent *>(this, &CannedMessageModule::handleInputEvent);
|
||||||
@ -155,19 +157,19 @@ private:
|
|||||||
int currentMessageIndex = -1;
|
int currentMessageIndex = -1;
|
||||||
|
|
||||||
// === Routing & Acknowledgment ===
|
// === Routing & Acknowledgment ===
|
||||||
NodeNum dest = NODENUM_BROADCAST; // Destination node for outgoing messages (default: broadcast)
|
NodeNum dest = NODENUM_BROADCAST; // Destination node for outgoing messages (default: broadcast)
|
||||||
NodeNum incoming = NODENUM_BROADCAST; // Source node from which last ACK/NACK was received
|
NodeNum incoming = NODENUM_BROADCAST; // Source node from which last ACK/NACK was received
|
||||||
NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display)
|
NodeNum lastSentNode = 0; // Tracks the most recent node we sent a message to (for UI display)
|
||||||
ChannelIndex channel = 0; // Channel index used when sending a message
|
ChannelIndex channel = 0; // Channel index used when sending a message
|
||||||
|
|
||||||
bool ack = false; // True = ACK received, False = NACK or failed
|
bool ack = false; // True = ACK received, False = NACK or failed
|
||||||
bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets
|
bool waitingForAck = false; // True if we're expecting an ACK and should monitor routing packets
|
||||||
bool lastAckWasRelayed = false; // True if the ACK was relayed through intermediate nodes
|
bool lastAckWasRelayed = false; // True if the ACK was relayed through intermediate nodes
|
||||||
uint8_t lastAckHopStart = 0; // Hop start value from the received ACK packet
|
uint8_t lastAckHopStart = 0; // Hop start value from the received ACK packet
|
||||||
uint8_t lastAckHopLimit = 0; // Hop limit value from the received ACK packet
|
uint8_t lastAckHopLimit = 0; // Hop limit value from the received ACK packet
|
||||||
|
|
||||||
float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI)
|
float lastRxSnr = 0; // SNR from last received ACK (used for diagnostics/UI)
|
||||||
int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI)
|
int32_t lastRxRssi = 0; // RSSI from last received ACK (used for diagnostics/UI)
|
||||||
|
|
||||||
// === State Tracking ===
|
// === State Tracking ===
|
||||||
cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
cannedMessageModuleRunState runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||||
|
@ -11,16 +11,16 @@
|
|||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
#include "UnitConversions.h"
|
#include "UnitConversions.h"
|
||||||
|
#include "buzz.h"
|
||||||
|
#include "graphics/SharedUIDisplay.h"
|
||||||
|
#include "graphics/images.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
|
#include "modules/ExternalNotificationModule.h"
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
#include "target_specific.h"
|
#include "target_specific.h"
|
||||||
#include <OLEDDisplay.h>
|
#include <OLEDDisplay.h>
|
||||||
#include <OLEDDisplayUi.h>
|
#include <OLEDDisplayUi.h>
|
||||||
#include "graphics/SharedUIDisplay.h"
|
|
||||||
#include "graphics/images.h"
|
|
||||||
#include "buzz.h"
|
|
||||||
#include "modules/ExternalNotificationModule.h"
|
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL
|
#if !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR_EXTERNAL
|
||||||
|
|
||||||
@ -30,8 +30,9 @@
|
|||||||
#include "Sensor/RCWL9620Sensor.h"
|
#include "Sensor/RCWL9620Sensor.h"
|
||||||
#include "Sensor/nullSensor.h"
|
#include "Sensor/nullSensor.h"
|
||||||
|
|
||||||
namespace graphics {
|
namespace graphics
|
||||||
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y);
|
{
|
||||||
|
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y);
|
||||||
}
|
}
|
||||||
#if __has_include(<Adafruit_AHTX0.h>)
|
#if __has_include(<Adafruit_AHTX0.h>)
|
||||||
#include "Sensor/AHT10.h"
|
#include "Sensor/AHT10.h"
|
||||||
@ -358,9 +359,9 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
|
|||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
|
|
||||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
display->drawString(centerX, titleY, titleStr); // Centered title
|
display->drawString(centerX, titleY, titleStr); // Centered title
|
||||||
if (config.display.heading_bold)
|
if (config.display.heading_bold)
|
||||||
display->drawString(centerX + 1, titleY, titleStr); // Bold effect via 1px offset
|
display->drawString(centerX + 1, titleY, titleStr); // Bold effect via 1px offset
|
||||||
|
|
||||||
// Restore text color & alignment
|
// Restore text color & alignment
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
@ -387,9 +388,8 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
|
|||||||
const auto &m = telemetry.variant.environment_metrics;
|
const auto &m = telemetry.variant.environment_metrics;
|
||||||
|
|
||||||
// Check if any telemetry field has valid data
|
// Check if any telemetry field has valid data
|
||||||
bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 ||
|
bool hasAny = m.has_temperature || m.has_relative_humidity || m.barometric_pressure != 0 || m.iaq != 0 || m.voltage != 0 ||
|
||||||
m.iaq != 0 || m.voltage != 0 || m.current != 0 || m.lux != 0 ||
|
m.current != 0 || m.lux != 0 || m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0;
|
||||||
m.white_lux != 0 || m.weight != 0 || m.distance != 0 || m.radiation != 0;
|
|
||||||
|
|
||||||
if (!hasAny) {
|
if (!hasAny) {
|
||||||
display->drawString(x, currentY, "No Telemetry");
|
display->drawString(x, currentY, "No Telemetry");
|
||||||
@ -399,21 +399,21 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
|
|||||||
// === First line: Show sender name + time since received (left), and first metric (right) ===
|
// === First line: Show sender name + time since received (left), and first metric (right) ===
|
||||||
const char *sender = getSenderShortName(*lastMeasurementPacket);
|
const char *sender = getSenderShortName(*lastMeasurementPacket);
|
||||||
uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket);
|
uint32_t agoSecs = service->GetTimeSinceMeshPacket(lastMeasurementPacket);
|
||||||
String agoStr = (agoSecs > 864000) ? "?" :
|
String agoStr = (agoSecs > 864000) ? "?"
|
||||||
(agoSecs > 3600) ? String(agoSecs / 3600) + "h" :
|
: (agoSecs > 3600) ? String(agoSecs / 3600) + "h"
|
||||||
(agoSecs > 60) ? String(agoSecs / 60) + "m" :
|
: (agoSecs > 60) ? String(agoSecs / 60) + "m"
|
||||||
String(agoSecs) + "s";
|
: String(agoSecs) + "s";
|
||||||
|
|
||||||
String leftStr = String(sender) + " (" + agoStr + ")";
|
String leftStr = String(sender) + " (" + agoStr + ")";
|
||||||
display->drawString(x, currentY, leftStr); // Left side: who and when
|
display->drawString(x, currentY, leftStr); // Left side: who and when
|
||||||
|
|
||||||
// === Collect sensor readings as label strings (no icons) ===
|
// === Collect sensor readings as label strings (no icons) ===
|
||||||
std::vector<String> entries;
|
std::vector<String> entries;
|
||||||
|
|
||||||
if (m.has_temperature) {
|
if (m.has_temperature) {
|
||||||
String tempStr = moduleConfig.telemetry.environment_display_fahrenheit
|
String tempStr = moduleConfig.telemetry.environment_display_fahrenheit
|
||||||
? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "°F"
|
? "Tmp: " + String(UnitConversions::CelsiusToFahrenheit(m.temperature), 1) + "°F"
|
||||||
: "Tmp: " + String(m.temperature, 1) + "°C";
|
: "Tmp: " + String(m.temperature, 1) + "°C";
|
||||||
entries.push_back(tempStr);
|
entries.push_back(tempStr);
|
||||||
}
|
}
|
||||||
if (m.has_relative_humidity)
|
if (m.has_relative_humidity)
|
||||||
@ -422,21 +422,23 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
|
|||||||
entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa");
|
entries.push_back("Prss: " + String(m.barometric_pressure, 0) + " hPa");
|
||||||
if (m.iaq != 0) {
|
if (m.iaq != 0) {
|
||||||
String aqi = "IAQ: " + String(m.iaq);
|
String aqi = "IAQ: " + String(m.iaq);
|
||||||
const char *bannerMsg = nullptr; // Default: no banner
|
const char *bannerMsg = nullptr; // Default: no banner
|
||||||
|
|
||||||
if (m.iaq <= 25) aqi += " (Excellent)";
|
if (m.iaq <= 25)
|
||||||
else if (m.iaq <= 50) aqi += " (Good)";
|
aqi += " (Excellent)";
|
||||||
else if (m.iaq <= 100) aqi += " (Moderate)";
|
else if (m.iaq <= 50)
|
||||||
else if (m.iaq <= 150) aqi += " (Poor)";
|
aqi += " (Good)";
|
||||||
|
else if (m.iaq <= 100)
|
||||||
|
aqi += " (Moderate)";
|
||||||
|
else if (m.iaq <= 150)
|
||||||
|
aqi += " (Poor)";
|
||||||
else if (m.iaq <= 200) {
|
else if (m.iaq <= 200) {
|
||||||
aqi += " (Unhealthy)";
|
aqi += " (Unhealthy)";
|
||||||
bannerMsg = "Unhealthy IAQ";
|
bannerMsg = "Unhealthy IAQ";
|
||||||
}
|
} else if (m.iaq <= 300) {
|
||||||
else if (m.iaq <= 300) {
|
|
||||||
aqi += " (Very Unhealthy)";
|
aqi += " (Very Unhealthy)";
|
||||||
bannerMsg = "Very Unhealthy IAQ";
|
bannerMsg = "Very Unhealthy IAQ";
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
aqi += " (Hazardous)";
|
aqi += " (Hazardous)";
|
||||||
bannerMsg = "Hazardous IAQ";
|
bannerMsg = "Hazardous IAQ";
|
||||||
}
|
}
|
||||||
@ -480,7 +482,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
|
|||||||
String valueStr = entries.front();
|
String valueStr = entries.front();
|
||||||
int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr);
|
int rightX = SCREEN_WIDTH - display->getStringWidth(valueStr);
|
||||||
display->drawString(rightX, currentY, valueStr);
|
display->drawString(rightX, currentY, valueStr);
|
||||||
entries.erase(entries.begin()); // Remove from queue
|
entries.erase(entries.begin()); // Remove from queue
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Advance to next line for remaining telemetry entries ===
|
// === Advance to next line for remaining telemetry entries ===
|
||||||
|
@ -10,11 +10,11 @@
|
|||||||
#include "PowerTelemetry.h"
|
#include "PowerTelemetry.h"
|
||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "Router.h"
|
#include "Router.h"
|
||||||
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
#include "target_specific.h"
|
#include "target_specific.h"
|
||||||
#include "graphics/SharedUIDisplay.h"
|
|
||||||
|
|
||||||
#define FAILED_STATE_SENSOR_READ_MULTIPLIER 10
|
#define FAILED_STATE_SENSOR_READ_MULTIPLIER 10
|
||||||
#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
|
#define DISPLAY_RECEIVEID_MEASUREMENTS_ON_SCREEN true
|
||||||
@ -22,8 +22,9 @@
|
|||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
#include <Throttle.h>
|
#include <Throttle.h>
|
||||||
|
|
||||||
namespace graphics {
|
namespace graphics
|
||||||
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y);
|
{
|
||||||
|
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y);
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t PowerTelemetryModule::runOnce()
|
int32_t PowerTelemetryModule::runOnce()
|
||||||
@ -112,7 +113,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
|
|||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
|
|
||||||
graphics::drawCommonHeader(display, x, y); // Shared UI header
|
graphics::drawCommonHeader(display, x, y); // Shared UI header
|
||||||
|
|
||||||
// === Draw title (aligned with header baseline) ===
|
// === Draw title (aligned with header baseline) ===
|
||||||
const int highlightHeight = FONT_HEIGHT_SMALL - 1;
|
const int highlightHeight = FONT_HEIGHT_SMALL - 1;
|
||||||
|
@ -42,7 +42,7 @@ void powerCommandsCheck()
|
|||||||
|
|
||||||
#if defined(ARCH_ESP32) || defined(ARCH_NRF52)
|
#if defined(ARCH_ESP32) || defined(ARCH_NRF52)
|
||||||
if (shutdownAtMsec && screen) {
|
if (shutdownAtMsec && screen) {
|
||||||
screen->showOverlayBanner("Shutting Down...", 0); // stays on screen
|
screen->showOverlayBanner("Shutting Down...", 0); // stays on screen
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user