mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-17 16:22:48 +00:00
Compare commits
113 Commits
v2.7.0.195
...
split-noti
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
550c0796eb | ||
|
|
93d101d11a | ||
|
|
fc2fd5ebff | ||
|
|
7265b5e6c6 | ||
|
|
bf5c9f6263 | ||
|
|
1736db6b56 | ||
|
|
3a5dc870e0 | ||
|
|
11d307c609 | ||
|
|
c090a7f6d5 | ||
|
|
c8bfb61c8d | ||
|
|
c144bd03dc | ||
|
|
dbc67973c6 | ||
|
|
3dd77ace85 | ||
|
|
e1b1e35a27 | ||
|
|
18098fb1cb | ||
|
|
b1e3353ceb | ||
|
|
8fb1e0f874 | ||
|
|
667ff17fdb | ||
|
|
42c1967e7b | ||
|
|
7512673b09 | ||
|
|
94258cfd1c | ||
|
|
7c297eff8f | ||
|
|
c178396e20 | ||
|
|
caf4c3919c | ||
|
|
7d09bd981a | ||
|
|
f046c1a68a | ||
|
|
3870d81bf6 | ||
|
|
a7dcf580ad | ||
|
|
61f81ac758 | ||
|
|
9446f07c4d | ||
|
|
94904cb6a7 | ||
|
|
646b370411 | ||
|
|
b6bcee18b5 | ||
|
|
20988aa4fa | ||
|
|
cab6707ca0 | ||
|
|
46ac9841d6 | ||
|
|
88ab198e0f | ||
|
|
0c948a3fc0 | ||
|
|
17456d0618 | ||
|
|
a395448170 | ||
|
|
e6ba326876 | ||
|
|
ecfaf3a095 | ||
|
|
a6cc4ab3fe | ||
|
|
d411fd99f0 | ||
|
|
819f5a2fde | ||
|
|
ca34fe9a90 | ||
|
|
137e7183c7 | ||
|
|
54fa39b2e9 | ||
|
|
eca240373a | ||
|
|
0b1703a51a | ||
|
|
653f6c2a85 | ||
|
|
7a285cf221 | ||
|
|
cea5cd171a | ||
|
|
c5e3bc841e | ||
|
|
ca7d2d7482 | ||
|
|
7af31a88c0 | ||
|
|
9f53df4f2e | ||
|
|
485fc7639e | ||
|
|
34f3800e2b | ||
|
|
a3ed75c5c9 | ||
|
|
088143dbf3 | ||
|
|
fecf80c39b | ||
|
|
7ef8067b87 | ||
|
|
91bcf072a0 | ||
|
|
9de5d170bf | ||
|
|
4802cef3ca | ||
|
|
3d28086f68 | ||
|
|
232d601b14 | ||
|
|
36ee2cfda0 | ||
|
|
56c1ba037a | ||
|
|
ae9c062dc9 | ||
|
|
6c5b947ad5 | ||
|
|
f9bf7a1010 | ||
|
|
c35610b04d | ||
|
|
0df1d49220 | ||
|
|
0ba3170dfe | ||
|
|
94b9684981 | ||
|
|
e0918ea448 | ||
|
|
4c0517c6f2 | ||
|
|
07cd16d2df | ||
|
|
a33672db4f | ||
|
|
38896198f2 | ||
|
|
6088ab49eb | ||
|
|
7f8acf5658 | ||
|
|
99176a8388 | ||
|
|
30e0972de5 | ||
|
|
6bd600a878 | ||
|
|
2f31ee5b6e | ||
|
|
6a91741209 | ||
|
|
b55e763b29 | ||
|
|
60acba877e | ||
|
|
221988c665 | ||
|
|
850d957931 | ||
|
|
012f88e56f | ||
|
|
83248ce0d0 | ||
|
|
bdc1df9f5c | ||
|
|
2de08bebdc | ||
|
|
d3e56ea084 | ||
|
|
2f37204df2 | ||
|
|
0808f5215f | ||
|
|
247e05bb10 | ||
|
|
791377b76b | ||
|
|
53d28f3a3a | ||
|
|
4308bbc156 | ||
|
|
574cbe55c0 | ||
|
|
ce1480df98 | ||
|
|
0108ad7992 | ||
|
|
f11b49863d | ||
|
|
5ca5ee2846 | ||
|
|
e1df4e19e5 | ||
|
|
766189212c | ||
|
|
8ba98ae873 | ||
|
|
7a38368494 |
@@ -6,6 +6,6 @@ Lora:
|
|||||||
IRQ: 16
|
IRQ: 16
|
||||||
Busy: 20
|
Busy: 20
|
||||||
Reset: 24
|
Reset: 24
|
||||||
TXen: 13
|
RXen: 12
|
||||||
DIO2_AS_RF_SWITCH: true
|
DIO2_AS_RF_SWITCH: true
|
||||||
DIO3_TCXO_VOLTAGE: true
|
DIO3_TCXO_VOLTAGE: true
|
||||||
|
|||||||
@@ -87,6 +87,9 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
|
<release version="2.7.1" date="2025-06-21">
|
||||||
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.1</url>
|
||||||
|
</release>
|
||||||
<release version="2.7.0" date="2025-06-20">
|
<release version="2.7.0" date="2025-06-20">
|
||||||
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.0</url>
|
<url type="details">https://github.com/meshtastic/firmware/releases?q=tag%3Av2.7.0</url>
|
||||||
</release>
|
</release>
|
||||||
|
|||||||
7
debian/changelog
vendored
7
debian/changelog
vendored
@@ -1,4 +1,4 @@
|
|||||||
meshtasticd (2.7.0.0) UNRELEASED; urgency=medium
|
meshtasticd (2.7.1.0) UNRELEASED; urgency=medium
|
||||||
|
|
||||||
[ Austin Lane ]
|
[ Austin Lane ]
|
||||||
* Initial packaging
|
* Initial packaging
|
||||||
@@ -22,4 +22,7 @@ meshtasticd (2.7.0.0) UNRELEASED; urgency=medium
|
|||||||
[ ]
|
[ ]
|
||||||
* GitHub Actions Automatic version bump
|
* GitHub Actions Automatic version bump
|
||||||
|
|
||||||
-- <github-actions[bot]@users.noreply.github.com> Mon, 16 Jun 2025 02:10:49 +0000
|
[ ]
|
||||||
|
* GitHub Actions Automatic version bump
|
||||||
|
|
||||||
|
-- <github-actions[bot]@users.noreply.github.com> Sat, 21 Jun 2025 15:51:49 +0000
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ build_flags = -Wno-missing-field-initializers
|
|||||||
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
-DMESHTASTIC_EXCLUDE_HEALTH_TELEMETRY=1
|
||||||
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
-DMESHTASTIC_EXCLUDE_POWERSTRESS=1 ; exclude power stress test module from main firmware
|
||||||
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
-DMESHTASTIC_EXCLUDE_GENERIC_THREAD_MODULE=1
|
||||||
|
-D MAX_THREADS=40 ; As we've split modules, we have more threads to manage
|
||||||
#-DBUILD_EPOCH=$UNIX_TIME
|
#-DBUILD_EPOCH=$UNIX_TIME
|
||||||
#-D OLED_PL=1
|
#-D OLED_PL=1
|
||||||
|
|
||||||
@@ -103,12 +104,12 @@ lib_deps =
|
|||||||
[radiolib_base]
|
[radiolib_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
|
# renovate: datasource=custom.pio depName=RadioLib packageName=jgromes/library/RadioLib
|
||||||
jgromes/RadioLib@7.1.2
|
jgromes/RadioLib@7.2.0
|
||||||
|
|
||||||
[device-ui_base]
|
[device-ui_base]
|
||||||
lib_deps =
|
lib_deps =
|
||||||
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
# renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master
|
||||||
https://github.com/meshtastic/device-ui/archive/d99edaf43775c9b235aab20521b034c99e04e4a8.zip
|
https://github.com/meshtastic/device-ui/archive/cdc6e5bdeedb8293d10e4a02be6ca64e95a7c515.zip
|
||||||
|
|
||||||
; Common libs for environmental measurements in telemetry module
|
; Common libs for environmental measurements in telemetry module
|
||||||
[environmental_base]
|
[environmental_base]
|
||||||
|
|||||||
Submodule protobufs updated: 6791138f0b...386fa53c15
@@ -26,7 +26,7 @@
|
|||||||
#ifndef SLEEP_TIME
|
#ifndef SLEEP_TIME
|
||||||
#define SLEEP_TIME 30
|
#define SLEEP_TIME 30
|
||||||
#endif
|
#endif
|
||||||
#if EXCLUDE_POWER_FSM
|
#if MESHTASTIC_EXCLUDE_POWER_FSM
|
||||||
FakeFsm powerFSM;
|
FakeFsm powerFSM;
|
||||||
void PowerFSM_setup(){};
|
void PowerFSM_setup(){};
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
#define EVENT_SHUTDOWN 16 // force a full shutdown now (not just sleep)
|
#define EVENT_SHUTDOWN 16 // force a full shutdown now (not just sleep)
|
||||||
#define EVENT_INPUT 17 // input broker wants something, we need to wake up and enable screen
|
#define EVENT_INPUT 17 // input broker wants something, we need to wake up and enable screen
|
||||||
|
|
||||||
#if EXCLUDE_POWER_FSM
|
#if MESHTASTIC_EXCLUDE_POWER_FSM
|
||||||
class FakeFsm
|
class FakeFsm
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class PowerFSMThread : public OSThread
|
|||||||
protected:
|
protected:
|
||||||
int32_t runOnce() override
|
int32_t runOnce() override
|
||||||
{
|
{
|
||||||
#if !EXCLUDE_POWER_FSM
|
#if !MESHTASTIC_EXCLUDE_POWER_FSM
|
||||||
powerFSM.run_machine();
|
powerFSM.run_machine();
|
||||||
|
|
||||||
/// If we are in power state we force the CPU to wake every 10ms to check for serial characters (we don't yet wake
|
/// If we are in power state we force the CPU to wake every 10ms to check for serial characters (we don't yet wake
|
||||||
|
|||||||
@@ -352,8 +352,8 @@ void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16
|
|||||||
for (uint16_t i = 0; i < len; i += 16) {
|
for (uint16_t i = 0; i < len; i += 16) {
|
||||||
if (i % 128 == 0)
|
if (i % 128 == 0)
|
||||||
log(logLevel, " +------------------------------------------------+ +----------------+");
|
log(logLevel, " +------------------------------------------------+ +----------------+");
|
||||||
char s[] = "| | | |\n";
|
char s[] = " | | | |\n";
|
||||||
uint8_t ix = 1, iy = 52;
|
uint8_t ix = 5, iy = 56;
|
||||||
for (uint8_t j = 0; j < 16; j++) {
|
for (uint8_t j = 0; j < 16; j++) {
|
||||||
if (i + j < len) {
|
if (i + j < len) {
|
||||||
uint8_t c = buf[i + j];
|
uint8_t c = buf[i + j];
|
||||||
@@ -367,10 +367,8 @@ void RedirectablePrint::hexDump(const char *logLevel, unsigned char *buf, uint16
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
uint8_t index = i / 16;
|
uint8_t index = i / 16;
|
||||||
if (i < 256)
|
sprintf(s, "%03x", index);
|
||||||
log(logLevel, " ");
|
s[3] = '.';
|
||||||
log(logLevel, "%02x", index);
|
|
||||||
log(logLevel, ".");
|
|
||||||
log(logLevel, s);
|
log(logLevel, s);
|
||||||
}
|
}
|
||||||
log(logLevel, " +------------------------------------------------+ +----------------+");
|
log(logLevel, " +------------------------------------------------+ +----------------+");
|
||||||
@@ -393,4 +391,4 @@ std::string RedirectablePrint::mt_sprintf(const std::string fmt_str, ...)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return std::string(formatted.get());
|
return std::string(formatted.get());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#include "TimeFormatters.h"
|
#include "TimeFormatters.h"
|
||||||
#include "draw/ClockRenderer.h"
|
#include "draw/ClockRenderer.h"
|
||||||
#include "draw/DebugRenderer.h"
|
#include "draw/DebugRenderer.h"
|
||||||
|
#include "draw/MenuHandler.h"
|
||||||
#include "draw/MessageRenderer.h"
|
#include "draw/MessageRenderer.h"
|
||||||
#include "draw/NodeListRenderer.h"
|
#include "draw/NodeListRenderer.h"
|
||||||
#include "draw/NotificationRenderer.h"
|
#include "draw/NotificationRenderer.h"
|
||||||
@@ -58,7 +59,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
#include "mesh/Channels.h"
|
#include "mesh/Channels.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/ExternalNotificationModule.h"
|
#include "modules/ExternalNotificationModule.h"
|
||||||
#include "modules/TextMessageModule.h"
|
#include "modules/TextMessageModule.h"
|
||||||
#include "modules/WaypointModule.h"
|
#include "modules/WaypointModule.h"
|
||||||
@@ -136,13 +136,17 @@ extern bool hasUnreadMessage;
|
|||||||
// The banner appears in the center of the screen and disappears after the specified duration
|
// The banner appears in the center of the screen and disappears after the specified duration
|
||||||
|
|
||||||
// Called to trigger a banner with custom message and duration
|
// Called to trigger a banner with custom message and duration
|
||||||
void Screen::showOverlayBanner(const char *message, uint32_t durationMs, uint8_t options, std::function<void(int)> bannerCallback,
|
void Screen::showOverlayBanner(const char *message, uint32_t durationMs, const char **optionsArrayPtr, uint8_t options,
|
||||||
int8_t InitialSelected)
|
std::function<void(int)> bannerCallback, int8_t InitialSelected)
|
||||||
{
|
{
|
||||||
|
#ifdef USE_EINK
|
||||||
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||||
|
#endif
|
||||||
// Store the message and set the expiration timestamp
|
// Store the message and set the expiration timestamp
|
||||||
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
||||||
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
||||||
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||||
|
NotificationRenderer::optionsArrayPtr = optionsArrayPtr;
|
||||||
NotificationRenderer::alertBannerOptions = options;
|
NotificationRenderer::alertBannerOptions = options;
|
||||||
NotificationRenderer::alertBannerCallback = bannerCallback;
|
NotificationRenderer::alertBannerCallback = bannerCallback;
|
||||||
NotificationRenderer::curSelected = InitialSelected;
|
NotificationRenderer::curSelected = InitialSelected;
|
||||||
@@ -153,6 +157,24 @@ void Screen::showOverlayBanner(const char *message, uint32_t durationMs, uint8_t
|
|||||||
ui->update();
|
ui->update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called to trigger a banner with custom message and duration
|
||||||
|
void Screen::showNodePicker(const char *message, uint32_t durationMs, std::function<void(int)> bannerCallback)
|
||||||
|
{
|
||||||
|
#ifdef USE_EINK
|
||||||
|
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Skip full refresh for all overlay menus
|
||||||
|
#endif
|
||||||
|
// Store the message and set the expiration timestamp
|
||||||
|
strncpy(NotificationRenderer::alertBannerMessage, message, 255);
|
||||||
|
NotificationRenderer::alertBannerMessage[255] = '\0'; // Ensure null termination
|
||||||
|
NotificationRenderer::alertBannerUntil = (durationMs == 0) ? 0 : millis() + durationMs;
|
||||||
|
NotificationRenderer::alertBannerCallback = bannerCallback;
|
||||||
|
NotificationRenderer::pauseBanner = false;
|
||||||
|
static OverlayCallback overlays[] = {graphics::UIRenderer::drawNavigationBar, NotificationRenderer::drawAlertBannerOverlay};
|
||||||
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
|
setFastFramerate(); // Draw ASAP
|
||||||
|
ui->update();
|
||||||
|
}
|
||||||
|
|
||||||
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
uint8_t module_frame;
|
uint8_t module_frame;
|
||||||
@@ -414,8 +436,7 @@ void Screen::setup()
|
|||||||
|
|
||||||
// === Set custom overlay callbacks ===
|
// === Set custom overlay callbacks ===
|
||||||
static OverlayCallback overlays[] = {
|
static OverlayCallback overlays[] = {
|
||||||
graphics::UIRenderer::drawFunctionOverlay, // For mute/buzzer modifiers etc.
|
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
|
||||||
graphics::UIRenderer::drawNavigationBar // Custom indicator icons for each frame
|
|
||||||
};
|
};
|
||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
|
|
||||||
@@ -472,6 +493,7 @@ void Screen::setup()
|
|||||||
|
|
||||||
// === Turn on display and trigger first draw ===
|
// === Turn on display and trigger first draw ===
|
||||||
handleSetOn(true);
|
handleSetOn(true);
|
||||||
|
determineResolution(dispdev->height(), dispdev->width());
|
||||||
ui->update();
|
ui->update();
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
ui->update(); // Some SSD1306 clones drop the first draw, so run twice
|
ui->update(); // Some SSD1306 clones drop the first draw, so run twice
|
||||||
@@ -558,6 +580,7 @@ int32_t Screen::runOnce()
|
|||||||
if (displayHeight == 0) {
|
if (displayHeight == 0) {
|
||||||
displayHeight = dispdev->getHeight();
|
displayHeight = dispdev->getHeight();
|
||||||
}
|
}
|
||||||
|
menuHandler::handleMenuSwitch();
|
||||||
|
|
||||||
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
|
// Show boot screen for first logo_timeout seconds, then switch to normal operation.
|
||||||
// serialSinceMsec adjusts for additional serial wait time during nRF52 bootup
|
// serialSinceMsec adjusts for additional serial wait time during nRF52 bootup
|
||||||
@@ -586,7 +609,7 @@ int32_t Screen::runOnce()
|
|||||||
|
|
||||||
#ifndef DISABLE_WELCOME_UNSET
|
#ifndef DISABLE_WELCOME_UNSET
|
||||||
if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
if (!NotificationRenderer::isOverlayBannerShowing() && config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
||||||
LoraRegionPicker(0);
|
menuHandler::LoraRegionPicker(0);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
|
if (!NotificationRenderer::isOverlayBannerShowing() && rebootAtMsec != 0) {
|
||||||
@@ -769,32 +792,6 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
indicatorIcons.clear();
|
indicatorIcons.clear();
|
||||||
|
|
||||||
size_t numframes = 0;
|
size_t numframes = 0;
|
||||||
moduleFrames = MeshModule::GetMeshModulesWithUIFrames();
|
|
||||||
LOG_DEBUG("Show %d module frames", moduleFrames.size());
|
|
||||||
|
|
||||||
// put all of the module frames first.
|
|
||||||
// this is a little bit of a dirty hack; since we're going to call
|
|
||||||
// the same drawModuleFrame handler here for all of these module frames
|
|
||||||
// and then we'll just assume that the state->currentFrame value
|
|
||||||
// is the same offset into the moduleFrames vector
|
|
||||||
// so that we can invoke the module's callback
|
|
||||||
for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) {
|
|
||||||
// Draw the module frame, using the hack described above
|
|
||||||
normalFrames[numframes] = drawModuleFrame;
|
|
||||||
|
|
||||||
// Check if the module being drawn has requested focus
|
|
||||||
// We will honor this request later, if setFrames was triggered by a UIFrameEvent
|
|
||||||
MeshModule *m = *i;
|
|
||||||
if (m->isRequestingFocus())
|
|
||||||
fsi.positions.focusedModule = numframes;
|
|
||||||
if (m == waypointModule)
|
|
||||||
fsi.positions.waypoint = numframes;
|
|
||||||
|
|
||||||
indicatorIcons.push_back(icon_module);
|
|
||||||
numframes++;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DEBUG("Added modules. numframes: %d", numframes);
|
|
||||||
|
|
||||||
// If we have a critical fault, show it first
|
// If we have a critical fault, show it first
|
||||||
fsi.positions.fault = numframes;
|
fsi.positions.fault = numframes;
|
||||||
@@ -808,7 +805,7 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
fsi.positions.clock = numframes;
|
fsi.positions.clock = numframes;
|
||||||
normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame
|
normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame
|
||||||
: &graphics::ClockRenderer::drawAnalogClockFrame;
|
: &graphics::ClockRenderer::drawAnalogClockFrame;
|
||||||
indicatorIcons.push_back(icon_clock);
|
indicatorIcons.push_back(digital_icon_clock);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Declare this early so it’s available in FOCUS_PRESERVE block
|
// Declare this early so it’s available in FOCUS_PRESERVE block
|
||||||
@@ -823,22 +820,27 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
indicatorIcons.push_back(icon_mail);
|
indicatorIcons.push_back(icon_mail);
|
||||||
|
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
|
fsi.positions.nodelist = numframes;
|
||||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawDynamicNodeListScreen;
|
||||||
indicatorIcons.push_back(icon_nodes);
|
indicatorIcons.push_back(icon_nodes);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Show detailed node views only on E-Ink builds
|
// Show detailed node views only on E-Ink builds
|
||||||
#ifdef USE_EINK
|
#ifdef USE_EINK
|
||||||
|
fsi.positions.nodelist_lastheard = numframes;
|
||||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawLastHeardScreen;
|
||||||
indicatorIcons.push_back(icon_nodes);
|
indicatorIcons.push_back(icon_nodes);
|
||||||
|
|
||||||
|
fsi.positions.nodelist_hopsignal = numframes;
|
||||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawHopSignalScreen;
|
||||||
indicatorIcons.push_back(icon_signal);
|
indicatorIcons.push_back(icon_signal);
|
||||||
|
|
||||||
|
fsi.positions.nodelist_distance = numframes;
|
||||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawDistanceScreen;
|
||||||
indicatorIcons.push_back(icon_distance);
|
indicatorIcons.push_back(icon_distance);
|
||||||
#endif
|
#endif
|
||||||
#if HAS_GPS
|
#if HAS_GPS
|
||||||
|
fsi.positions.nodelist_bearings = numframes;
|
||||||
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
|
normalFrames[numframes++] = graphics::NodeListRenderer::drawNodeListWithCompasses;
|
||||||
indicatorIcons.push_back(icon_list);
|
indicatorIcons.push_back(icon_list);
|
||||||
|
|
||||||
@@ -858,8 +860,9 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
}
|
}
|
||||||
#if !defined(DISPLAY_CLOCK_FRAME)
|
#if !defined(DISPLAY_CLOCK_FRAME)
|
||||||
fsi.positions.clock = numframes;
|
fsi.positions.clock = numframes;
|
||||||
normalFrames[numframes++] = graphics::ClockRenderer::drawDigitalClockFrame;
|
normalFrames[numframes++] = graphics::ClockRenderer::digitalWatchFace ? graphics::ClockRenderer::drawDigitalClockFrame
|
||||||
indicatorIcons.push_back(icon_clock);
|
: graphics::ClockRenderer::drawAnalogClockFrame;
|
||||||
|
indicatorIcons.push_back(digital_icon_clock);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// We don't show the node info of our node (if we have it yet - we should)
|
// We don't show the node info of our node (if we have it yet - we should)
|
||||||
@@ -886,6 +889,36 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Beware of what changes you make in this code!
|
||||||
|
// We pass numfames into GetMeshModulesWithUIFrames() which is highly important!
|
||||||
|
// Inside of that callback, goes over to MeshModule.cpp and we run
|
||||||
|
// modulesWithUIFrames.resize(startIndex, nullptr), to insert nullptr
|
||||||
|
// entries until we're ready to start building the matching entries.
|
||||||
|
// We are doing our best to keep the normalFrames vector
|
||||||
|
// and the moduleFrames vector in lock step.
|
||||||
|
moduleFrames = MeshModule::GetMeshModulesWithUIFrames(numframes);
|
||||||
|
LOG_DEBUG("Show %d module frames", moduleFrames.size());
|
||||||
|
|
||||||
|
for (auto i = moduleFrames.begin(); i != moduleFrames.end(); ++i) {
|
||||||
|
// Draw the module frame, using the hack described above
|
||||||
|
if (*i != nullptr) {
|
||||||
|
normalFrames[numframes] = drawModuleFrame;
|
||||||
|
|
||||||
|
// Check if the module being drawn has requested focus
|
||||||
|
// We will honor this request later, if setFrames was triggered by a UIFrameEvent
|
||||||
|
MeshModule *m = *i;
|
||||||
|
if (m && m->isRequestingFocus())
|
||||||
|
fsi.positions.focusedModule = numframes;
|
||||||
|
if (m && m == waypointModule)
|
||||||
|
fsi.positions.waypoint = numframes;
|
||||||
|
|
||||||
|
indicatorIcons.push_back(icon_module);
|
||||||
|
numframes++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("Added modules. numframes: %d", numframes);
|
||||||
|
|
||||||
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
|
fsi.frameCount = numframes; // Total framecount is used to apply FOCUS_PRESERVE
|
||||||
this->frameCount = numframes; // ✅ Save frame count for use in custom overlay
|
this->frameCount = numframes; // ✅ Save frame count for use in custom overlay
|
||||||
LOG_DEBUG("Finished build frames. numframes: %d", numframes);
|
LOG_DEBUG("Finished build frames. numframes: %d", numframes);
|
||||||
@@ -917,6 +950,11 @@ void Screen::setFrames(FrameFocus focus)
|
|||||||
// If no module requested focus, will show the first frame instead
|
// If no module requested focus, will show the first frame instead
|
||||||
ui->switchToFrame(fsi.positions.focusedModule);
|
ui->switchToFrame(fsi.positions.focusedModule);
|
||||||
break;
|
break;
|
||||||
|
case FOCUS_CLOCK:
|
||||||
|
// Whichever frame was marked by MeshModule::requestFocus(), if any
|
||||||
|
// If no module requested focus, will show the first frame instead
|
||||||
|
ui->switchToFrame(fsi.positions.clock);
|
||||||
|
break;
|
||||||
|
|
||||||
case FOCUS_PRESERVE:
|
case FOCUS_PRESERVE:
|
||||||
// No more adjustment — force stay on same index
|
// No more adjustment — force stay on same index
|
||||||
@@ -1205,6 +1243,8 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
ui->setOverlays(overlays, sizeof(overlays) / sizeof(overlays[0]));
|
||||||
setFastFramerate(); // Draw ASAP
|
setFastFramerate(); // Draw ASAP
|
||||||
ui->update();
|
ui->update();
|
||||||
|
|
||||||
|
menuHandler::handleMenuSwitch();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
@@ -1230,7 +1270,7 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
// Ask any MeshModules if they're handling keyboard input right now
|
// Ask any MeshModules if they're handling keyboard input right now
|
||||||
bool inputIntercepted = false;
|
bool inputIntercepted = false;
|
||||||
for (MeshModule *module : moduleFrames) {
|
for (MeshModule *module : moduleFrames) {
|
||||||
if (module->interceptingKeyboardInput())
|
if (module && module->interceptingKeyboardInput())
|
||||||
inputIntercepted = true;
|
inputIntercepted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1242,128 +1282,36 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
showNextFrame();
|
showNextFrame();
|
||||||
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
} else if (event->inputEvent == INPUT_BROKER_SELECT) {
|
||||||
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
if (this->ui->getUiState()->currentFrame == framesetInfo.positions.home) {
|
||||||
const char *banner_message;
|
menuHandler::homeBaseMenu();
|
||||||
int options;
|
|
||||||
if (kb_found) {
|
|
||||||
banner_message = "Action?\nBack\nSleep Screen\nNew Preset Msg\nNew Freetext Msg";
|
|
||||||
options = 4;
|
|
||||||
} else {
|
|
||||||
banner_message = "Action?\nBack\nSleep Screen\nNew Preset Msg";
|
|
||||||
options = 3;
|
|
||||||
}
|
|
||||||
showOverlayBanner(banner_message, 30000, options, [](int selected) -> void {
|
|
||||||
if (selected == 1) {
|
|
||||||
screen->setOn(false);
|
|
||||||
} else if (selected == 2) {
|
|
||||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
|
||||||
} else if (selected == 3) {
|
|
||||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
#if HAS_TFT
|
#if HAS_TFT
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
|
||||||
showOverlayBanner("Switch to MUI?\nYes\nNo", 30000, 2, [](int selected) -> void {
|
menuHandler::switchToMUIMenu();
|
||||||
if (selected == 0) {
|
|
||||||
config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
|
|
||||||
config.bluetooth.enabled = false;
|
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
|
||||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
#else
|
#else
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.memory) {
|
||||||
showOverlayBanner(
|
menuHandler::BuzzerModeMenu();
|
||||||
"Beeps Mode\nAll Enabled\nDisabled\nNotifications\nSystem Only", 30000, 4,
|
|
||||||
[](int selected) -> void {
|
|
||||||
config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
|
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
|
||||||
},
|
|
||||||
config.device.buzzer_mode);
|
|
||||||
#endif
|
#endif
|
||||||
#if HAS_GPS
|
#if HAS_GPS
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.gps && gps) {
|
||||||
showOverlayBanner(
|
menuHandler::positionBaseMenu();
|
||||||
"Toggle GPS\nBack\nEnabled\nDisabled", 30000, 3,
|
|
||||||
[](int selected) -> void {
|
|
||||||
if (selected == 1) {
|
|
||||||
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED;
|
|
||||||
playGPSEnableBeep();
|
|
||||||
gps->enable();
|
|
||||||
} else if (selected == 2) {
|
|
||||||
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED;
|
|
||||||
playGPSDisableBeep();
|
|
||||||
gps->disable();
|
|
||||||
}
|
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
|
||||||
},
|
|
||||||
config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1
|
|
||||||
: 2); // set inital selection
|
|
||||||
#endif
|
#endif
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.clock) {
|
||||||
TZPicker();
|
menuHandler::clockMenu();
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.lora) {
|
||||||
LoraRegionPicker();
|
menuHandler::LoraRegionPicker();
|
||||||
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.textMessage &&
|
||||||
devicestate.rx_text_message.from) {
|
devicestate.rx_text_message.from) {
|
||||||
const char *banner_message;
|
menuHandler::messageResponseMenu();
|
||||||
int options;
|
|
||||||
if (kb_found) {
|
|
||||||
banner_message = "Message Action?\nBack\nDismiss\nReply via Preset\nReply via Freetext";
|
|
||||||
options = 4;
|
|
||||||
} else {
|
|
||||||
banner_message = "Message Action?\nBack\nDismiss\nReply via Preset";
|
|
||||||
options = 3;
|
|
||||||
}
|
|
||||||
#ifdef HAS_I2S
|
|
||||||
banner_message = "Message Action?\nBack\nDismiss\nReply via Preset\nReply via Freetext\nRead Aloud";
|
|
||||||
options = 5;
|
|
||||||
#endif
|
|
||||||
showOverlayBanner(banner_message, 30000, options, [](int selected) -> void {
|
|
||||||
if (selected == 1) {
|
|
||||||
screen->dismissCurrentFrame();
|
|
||||||
} else if (selected == 2) {
|
|
||||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
|
||||||
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST,
|
|
||||||
devicestate.rx_text_message.channel);
|
|
||||||
} else {
|
|
||||||
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
|
|
||||||
}
|
|
||||||
} else if (selected == 3) {
|
|
||||||
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
|
||||||
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST,
|
|
||||||
devicestate.rx_text_message.channel);
|
|
||||||
} else {
|
|
||||||
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#ifdef HAS_I2S
|
|
||||||
else if (selected == 4) {
|
|
||||||
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
|
|
||||||
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
|
|
||||||
|
|
||||||
audioThread->readAloud(msg);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
});
|
|
||||||
} else if (framesetInfo.positions.firstFavorite != 255 &&
|
} else if (framesetInfo.positions.firstFavorite != 255 &&
|
||||||
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
this->ui->getUiState()->currentFrame >= framesetInfo.positions.firstFavorite &&
|
||||||
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
|
this->ui->getUiState()->currentFrame <= framesetInfo.positions.lastFavorite) {
|
||||||
const char *banner_message;
|
menuHandler::favoriteBaseMenu();
|
||||||
int options;
|
} else if (this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist ||
|
||||||
if (kb_found) {
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_lastheard ||
|
||||||
banner_message = "Message Node?\nCancel\nNew Preset Msg\nNew Freetext Msg";
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
|
||||||
options = 3;
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_distance ||
|
||||||
} else {
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_hopsignal ||
|
||||||
banner_message = "Message Node?\nCancel\nConfirm";
|
this->ui->getUiState()->currentFrame == framesetInfo.positions.nodelist_bearings) {
|
||||||
options = 2;
|
menuHandler::nodeListMenu();
|
||||||
}
|
|
||||||
showOverlayBanner(banner_message, 30000, options, [](int selected) -> void {
|
|
||||||
if (selected == 1) {
|
|
||||||
cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
|
|
||||||
} else if (selected == 2) {
|
|
||||||
cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else if (event->inputEvent == INPUT_BROKER_BACK) {
|
} else if (event->inputEvent == INPUT_BROKER_BACK) {
|
||||||
showPrevFrame();
|
showPrevFrame();
|
||||||
@@ -1376,12 +1324,13 @@ int Screen::handleInputEvent(const InputEvent *event)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Screen::handleAdminMessage(const meshtastic_AdminMessage *arg)
|
int Screen::handleAdminMessage(AdminModule_ObserverData *arg)
|
||||||
{
|
{
|
||||||
switch (arg->which_payload_variant) {
|
switch (arg->request->which_payload_variant) {
|
||||||
// Node removed manually (i.e. via app)
|
// Node removed manually (i.e. via app)
|
||||||
case meshtastic_AdminMessage_remove_by_nodenum_tag:
|
case meshtastic_AdminMessage_remove_by_nodenum_tag:
|
||||||
setFrames(FOCUS_PRESERVE);
|
setFrames(FOCUS_PRESERVE);
|
||||||
|
*arg->result = AdminMessageHandleResult::HANDLED;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Default no-op, in case the admin message observable gets used by other classes in future
|
// Default no-op, in case the admin message observable gets used by other classes in future
|
||||||
@@ -1396,95 +1345,6 @@ bool Screen::isOverlayBannerShowing()
|
|||||||
return NotificationRenderer::isOverlayBannerShowing();
|
return NotificationRenderer::isOverlayBannerShowing();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Screen::LoraRegionPicker(uint32_t duration)
|
|
||||||
{
|
|
||||||
showOverlayBanner(
|
|
||||||
"Set the LoRa "
|
|
||||||
"region\nBack\nUS\nEU_433\nEU_868\nCN\nJP\nANZ\nKR\nTW\nRU\nIN\nNZ_865\nTH\nLORA_24\nUA_433\nUA_868\nMY_433\nMY_"
|
|
||||||
"919\nSG_"
|
|
||||||
"923\nPH_433\nPH_868\nPH_915\nANZ_433",
|
|
||||||
duration, 23,
|
|
||||||
[](int selected) -> void {
|
|
||||||
if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) {
|
|
||||||
config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected);
|
|
||||||
// This is needed as we wait til picking the LoRa region to generate keys for the first time.
|
|
||||||
if (!owner.is_licensed) {
|
|
||||||
bool keygenSuccess = false;
|
|
||||||
if (config.security.private_key.size == 32) {
|
|
||||||
// public key is derived from private, so this will always have the same result.
|
|
||||||
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
|
||||||
keygenSuccess = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_INFO("Generate new PKI keys");
|
|
||||||
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
|
||||||
keygenSuccess = true;
|
|
||||||
}
|
|
||||||
if (keygenSuccess) {
|
|
||||||
config.security.public_key.size = 32;
|
|
||||||
config.security.private_key.size = 32;
|
|
||||||
owner.public_key.size = 32;
|
|
||||||
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
config.lora.tx_enabled = true;
|
|
||||||
initRegion();
|
|
||||||
if (myRegion->dutyCycle < 100) {
|
|
||||||
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
|
|
||||||
}
|
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
|
||||||
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
0);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Screen::TZPicker()
|
|
||||||
{
|
|
||||||
showOverlayBanner(
|
|
||||||
"Pick "
|
|
||||||
"Timezone\nBack\nUS/Hawaii\nUS/Alaska\nUS/Pacific\nUS/Mountain\nUS/Central\nUS/Eastern\nUTC\nEU/Western\nEU/"
|
|
||||||
"Central\nEU/Eastern\nAsia/Kolkata\nAsia/Hong_Kong\nAU/AWST\nAU/ACST\nAU/AEST\nPacific/NZ",
|
|
||||||
30000, 17, [](int selected) -> void {
|
|
||||||
if (selected == 1) { // Hawaii
|
|
||||||
strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 2) { // Alaska
|
|
||||||
strncpy(config.device.tzdef, "AKST9AKDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 3) { // Pacific
|
|
||||||
strncpy(config.device.tzdef, "PST8PDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 4) { // Mountain
|
|
||||||
strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 5) { // Central
|
|
||||||
strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 6) { // Eastern
|
|
||||||
strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 7) { // UTC
|
|
||||||
strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 8) { // EU/Western
|
|
||||||
strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 9) { // EU/Central
|
|
||||||
strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 10) { // EU/Eastern
|
|
||||||
strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 11) { // Asia/Kolkata
|
|
||||||
strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 12) { // China
|
|
||||||
strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 13) { // AU/AWST
|
|
||||||
strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 14) { // AU/ACST
|
|
||||||
strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 15) { // AU/AEST
|
|
||||||
strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
|
|
||||||
} else if (selected == 16) { // NZ
|
|
||||||
strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef));
|
|
||||||
}
|
|
||||||
|
|
||||||
setenv("TZ", config.device.tzdef, 1);
|
|
||||||
service->reloadConfig(SEGMENT_CONFIG);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class Screen
|
|||||||
FOCUS_FAULT,
|
FOCUS_FAULT,
|
||||||
FOCUS_TEXTMESSAGE,
|
FOCUS_TEXTMESSAGE,
|
||||||
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
||||||
|
FOCUS_CLOCK,
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
||||||
@@ -38,8 +39,8 @@ class Screen
|
|||||||
void setFunctionSymbol(std::string) {}
|
void setFunctionSymbol(std::string) {}
|
||||||
void removeFunctionSymbol(std::string) {}
|
void removeFunctionSymbol(std::string) {}
|
||||||
void startAlert(const char *) {}
|
void startAlert(const char *) {}
|
||||||
void showOverlayBanner(const char *message, uint32_t durationMs = 3000, uint8_t options = 0,
|
void showOverlayBanner(const char *message, uint32_t durationMs = 3000, const char **optionsArrayPtr = nullptr,
|
||||||
std::function<void(int)> bannerCallback = NULL, int8_t InitialSelected = 0)
|
uint8_t options = 0, std::function<void(int)> bannerCallback = NULL, int8_t InitialSelected = 0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
void setFrames(FrameFocus focus) {}
|
void setFrames(FrameFocus focus) {}
|
||||||
@@ -78,6 +79,7 @@ class Screen
|
|||||||
#include "concurrency/OSThread.h"
|
#include "concurrency/OSThread.h"
|
||||||
#include "input/InputBroker.h"
|
#include "input/InputBroker.h"
|
||||||
#include "mesh/MeshModule.h"
|
#include "mesh/MeshModule.h"
|
||||||
|
#include "modules/AdminModule.h"
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -193,8 +195,8 @@ class Screen : public concurrency::OSThread
|
|||||||
CallbackObserver<Screen, const UIFrameEvent *>(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules
|
CallbackObserver<Screen, const UIFrameEvent *>(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules
|
||||||
CallbackObserver<Screen, const InputEvent *> inputObserver =
|
CallbackObserver<Screen, const InputEvent *> inputObserver =
|
||||||
CallbackObserver<Screen, const InputEvent *>(this, &Screen::handleInputEvent);
|
CallbackObserver<Screen, const InputEvent *>(this, &Screen::handleInputEvent);
|
||||||
CallbackObserver<Screen, const meshtastic_AdminMessage *> adminMessageObserver =
|
CallbackObserver<Screen, AdminModule_ObserverData *> adminMessageObserver =
|
||||||
CallbackObserver<Screen, const meshtastic_AdminMessage *>(this, &Screen::handleAdminMessage);
|
CallbackObserver<Screen, AdminModule_ObserverData *>(this, &Screen::handleAdminMessage);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
||||||
@@ -208,6 +210,7 @@ class Screen : public concurrency::OSThread
|
|||||||
FOCUS_FAULT,
|
FOCUS_FAULT,
|
||||||
FOCUS_TEXTMESSAGE,
|
FOCUS_TEXTMESSAGE,
|
||||||
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
FOCUS_MODULE, // Note: target module should call requestFocus(), otherwise no info about which module to focus
|
||||||
|
FOCUS_CLOCK,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Regenerate the normal set of frames, focusing a specific frame if requested
|
// Regenerate the normal set of frames, focusing a specific frame if requested
|
||||||
@@ -285,8 +288,10 @@ class Screen : public concurrency::OSThread
|
|||||||
enqueueCmd(cmd);
|
enqueueCmd(cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
void showOverlayBanner(const char *message, uint32_t durationMs = 3000, uint8_t options = 0,
|
void showOverlayBanner(const char *message, uint32_t durationMs = 3000, const char **optionsArrayPtr = nullptr,
|
||||||
std::function<void(int)> bannerCallback = NULL, int8_t InitialSelected = 0);
|
uint8_t options = 0, std::function<void(int)> bannerCallback = NULL, int8_t InitialSelected = 0);
|
||||||
|
|
||||||
|
void showNodePicker(const char *message, uint32_t durationMs, std::function<void(int)> bannerCallback);
|
||||||
|
|
||||||
void startFirmwareUpdateScreen()
|
void startFirmwareUpdateScreen()
|
||||||
{
|
{
|
||||||
@@ -544,7 +549,7 @@ class Screen : public concurrency::OSThread
|
|||||||
int handleTextMessage(const meshtastic_MeshPacket *arg);
|
int handleTextMessage(const meshtastic_MeshPacket *arg);
|
||||||
int handleUIFrameEvent(const UIFrameEvent *arg);
|
int handleUIFrameEvent(const UIFrameEvent *arg);
|
||||||
int handleInputEvent(const InputEvent *arg);
|
int handleInputEvent(const InputEvent *arg);
|
||||||
int handleAdminMessage(const meshtastic_AdminMessage *arg);
|
int handleAdminMessage(AdminModule_ObserverData *arg);
|
||||||
|
|
||||||
/// Used to force (super slow) eink displays to draw critical frames
|
/// Used to force (super slow) eink displays to draw critical frames
|
||||||
void forceDisplay(bool forceUiUpdate = false);
|
void forceDisplay(bool forceUiUpdate = false);
|
||||||
@@ -601,8 +606,6 @@ class Screen : public concurrency::OSThread
|
|||||||
void handleShowNextFrame();
|
void handleShowNextFrame();
|
||||||
void handleShowPrevFrame();
|
void handleShowPrevFrame();
|
||||||
void handleStartFirmwareUpdateScreen();
|
void handleStartFirmwareUpdateScreen();
|
||||||
void TZPicker();
|
|
||||||
void LoraRegionPicker(uint32_t duration = 30000);
|
|
||||||
|
|
||||||
// Info collected by setFrames method.
|
// Info collected by setFrames method.
|
||||||
// Index location of specific frames.
|
// Index location of specific frames.
|
||||||
@@ -611,7 +614,6 @@ class Screen : public concurrency::OSThread
|
|||||||
struct FramesetInfo {
|
struct FramesetInfo {
|
||||||
struct FramePositions {
|
struct FramePositions {
|
||||||
uint8_t fault = 255;
|
uint8_t fault = 255;
|
||||||
uint8_t textMessage = 255;
|
|
||||||
uint8_t waypoint = 255;
|
uint8_t waypoint = 255;
|
||||||
uint8_t focusedModule = 255;
|
uint8_t focusedModule = 255;
|
||||||
uint8_t log = 255;
|
uint8_t log = 255;
|
||||||
@@ -621,6 +623,12 @@ class Screen : public concurrency::OSThread
|
|||||||
uint8_t memory = 255;
|
uint8_t memory = 255;
|
||||||
uint8_t gps = 255;
|
uint8_t gps = 255;
|
||||||
uint8_t home = 255;
|
uint8_t home = 255;
|
||||||
|
uint8_t textMessage = 255;
|
||||||
|
uint8_t nodelist = 255;
|
||||||
|
uint8_t nodelist_lastheard = 255;
|
||||||
|
uint8_t nodelist_hopsignal = 255;
|
||||||
|
uint8_t nodelist_distance = 255;
|
||||||
|
uint8_t nodelist_bearings = 255;
|
||||||
uint8_t clock = 255;
|
uint8_t clock = 255;
|
||||||
uint8_t firstFavorite = 255;
|
uint8_t firstFavorite = 255;
|
||||||
uint8_t lastFavorite = 255;
|
uint8_t lastFavorite = 255;
|
||||||
@@ -678,5 +686,6 @@ class Screen : public concurrency::OSThread
|
|||||||
// Extern declarations for function symbols used in UIRenderer
|
// Extern declarations for function symbols used in UIRenderer
|
||||||
extern std::vector<std::string> functionSymbol;
|
extern std::vector<std::string> functionSymbol;
|
||||||
extern std::string functionSymbolString;
|
extern std::string functionSymbolString;
|
||||||
|
extern graphics::Screen *screen;
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -10,9 +10,22 @@
|
|||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
|
|
||||||
|
void determineResolution(int16_t screenheight, int16_t screenwidth)
|
||||||
|
{
|
||||||
|
if (screenwidth > 128) {
|
||||||
|
isHighResolution = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case for Heltec Wireless Tracker v1.1
|
||||||
|
if (screenwidth == 160 && screenheight == 80) {
|
||||||
|
isHighResolution = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === Shared External State ===
|
// === Shared External State ===
|
||||||
bool hasUnreadMessage = false;
|
bool hasUnreadMessage = false;
|
||||||
bool isMuted = false;
|
bool isMuted = false;
|
||||||
|
bool isHighResolution = false;
|
||||||
|
|
||||||
// === Internal State ===
|
// === Internal State ===
|
||||||
bool isBoltVisibleShared = true;
|
bool isBoltVisibleShared = true;
|
||||||
@@ -40,7 +53,7 @@ void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w,
|
|||||||
// *************************
|
// *************************
|
||||||
// * Common Header Drawing *
|
// * Common Header Drawing *
|
||||||
// *************************
|
// *************************
|
||||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr)
|
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only)
|
||||||
{
|
{
|
||||||
constexpr int HEADER_OFFSET_Y = 1;
|
constexpr int HEADER_OFFSET_Y = 1;
|
||||||
y += HEADER_OFFSET_Y;
|
y += HEADER_OFFSET_Y;
|
||||||
@@ -56,34 +69,40 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
const int screenW = display->getWidth();
|
const int screenW = display->getWidth();
|
||||||
const int screenH = display->getHeight();
|
const int screenH = display->getHeight();
|
||||||
|
|
||||||
const bool useBigIcons = (screenW > 128);
|
if (!battery_only) {
|
||||||
|
// === Inverted Header Background ===
|
||||||
// === Inverted Header Background ===
|
if (isInverted) {
|
||||||
if (isInverted) {
|
display->setColor(BLACK);
|
||||||
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||||
display->setColor(BLACK);
|
display->setColor(WHITE);
|
||||||
} else {
|
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
display->fillRect(0, 0, screenW, highlightHeight + 3);
|
|
||||||
display->setColor(WHITE);
|
|
||||||
if (screenW > 128) {
|
|
||||||
display->drawLine(0, 20, screenW, 20);
|
|
||||||
} else {
|
} else {
|
||||||
display->drawLine(0, 14, screenW, 14);
|
display->setColor(BLACK);
|
||||||
|
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
if (isHighResolution) {
|
||||||
|
display->drawLine(0, 20, screenW, 20);
|
||||||
|
} else {
|
||||||
|
display->drawLine(0, 14, screenW, 14);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// === Screen Title ===
|
// === Screen Title ===
|
||||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
display->drawString(SCREEN_WIDTH / 2, y, titleStr);
|
display->drawString(SCREEN_WIDTH / 2, y, titleStr);
|
||||||
if (config.display.heading_bold) {
|
if (config.display.heading_bold) {
|
||||||
display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr);
|
display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
|
||||||
// === Battery State ===
|
// === Battery State ===
|
||||||
int chargePercent = powerStatus->getBatteryChargePercent();
|
int chargePercent = powerStatus->getBatteryChargePercent();
|
||||||
bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue;
|
bool isCharging = powerStatus->getIsCharging() == meshtastic::OptionalBool::OptTrue;
|
||||||
|
if (chargePercent == 100) {
|
||||||
|
isCharging = false;
|
||||||
|
}
|
||||||
uint32_t now = millis();
|
uint32_t now = millis();
|
||||||
|
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
@@ -93,20 +112,22 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
bool useHorizontalBattery = (screenW > 128 && screenW >= screenH);
|
bool useHorizontalBattery = (isHighResolution && screenW >= screenH);
|
||||||
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
||||||
|
|
||||||
// === Battery Icons ===
|
// === Battery Icons ===
|
||||||
if (useHorizontalBattery) {
|
if (useHorizontalBattery) {
|
||||||
int batteryX = 2;
|
int batteryX = 2;
|
||||||
int batteryY = HEADER_OFFSET_Y + 2;
|
int batteryY = HEADER_OFFSET_Y + 3;
|
||||||
display->drawXbm(batteryX, batteryY, 29, 15, batteryBitmap_h);
|
display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom);
|
||||||
|
display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top);
|
||||||
if (isCharging && isBoltVisibleShared)
|
if (isCharging && isBoltVisibleShared)
|
||||||
display->drawXbm(batteryX + 9, batteryY + 1, 9, 13, lightning_bolt_h);
|
display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h);
|
||||||
else {
|
else {
|
||||||
display->drawXbm(batteryX + 8, batteryY, 12, 15, batteryBitmap_sidegaps_h);
|
display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY);
|
||||||
int fillWidth = 24 * chargePercent / 100;
|
display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12);
|
||||||
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 13);
|
int fillWidth = 14 * chargePercent / 100;
|
||||||
|
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
int batteryX = 1;
|
int batteryX = 1;
|
||||||
@@ -129,12 +150,8 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
char chargeStr[4];
|
char chargeStr[4];
|
||||||
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
|
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
|
||||||
int chargeNumWidth = display->getStringWidth(chargeStr);
|
int chargeNumWidth = display->getStringWidth(chargeStr);
|
||||||
const int batteryOffset = useHorizontalBattery ? 28 : 6;
|
const int batteryOffset = useHorizontalBattery ? 19 : 9;
|
||||||
#ifdef USE_EINK
|
const int percentX = x + batteryOffset;
|
||||||
const int percentX = x + xOffset + batteryOffset - 2;
|
|
||||||
#else
|
|
||||||
const int percentX = x + xOffset + batteryOffset;
|
|
||||||
#endif
|
|
||||||
display->drawString(percentX, textY, chargeStr);
|
display->drawString(percentX, textY, chargeStr);
|
||||||
display->drawString(percentX + chargeNumWidth - 1, textY, "%");
|
display->drawString(percentX + chargeNumWidth - 1, textY, "%");
|
||||||
if (isBold) {
|
if (isBold) {
|
||||||
@@ -148,7 +165,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
int timeStrWidth = display->getStringWidth("12:34"); // Default alignment
|
int timeStrWidth = display->getStringWidth("12:34"); // Default alignment
|
||||||
int timeX = screenW - xOffset - timeStrWidth + 4;
|
int timeX = screenW - xOffset - timeStrWidth + 4;
|
||||||
|
|
||||||
if (rtc_sec > 0) {
|
if (rtc_sec > 0 && !battery_only) {
|
||||||
// === Build Time String ===
|
// === Build Time String ===
|
||||||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||||
int hour = hms / SEC_PER_HOUR;
|
int hour = hms / SEC_PER_HOUR;
|
||||||
@@ -164,7 +181,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
}
|
}
|
||||||
|
|
||||||
timeStrWidth = display->getStringWidth(timeStr);
|
timeStrWidth = display->getStringWidth(timeStr);
|
||||||
timeX = screenW - xOffset - timeStrWidth + 4;
|
timeX = screenW - xOffset - timeStrWidth + 3;
|
||||||
|
|
||||||
// === Show Mail or Mute Icon to the Left of Time ===
|
// === Show Mail or Mute Icon to the Left of Time ===
|
||||||
int iconRightEdge = timeX - 1;
|
int iconRightEdge = timeX - 1;
|
||||||
@@ -217,7 +234,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||||||
}
|
}
|
||||||
} else if (isMuted) {
|
} else if (isMuted) {
|
||||||
if (useBigIcons) {
|
if (isHighResolution) {
|
||||||
int iconX = iconRightEdge - mute_symbol_big_width;
|
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||||
|
|
||||||
@@ -281,7 +298,7 @@ void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *ti
|
|||||||
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||||||
}
|
}
|
||||||
} else if (isMuted) {
|
} else if (isMuted) {
|
||||||
if (useBigIcons) {
|
if (isHighResolution) {
|
||||||
int iconX = iconRightEdge - mute_symbol_big_width;
|
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||||
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
|
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
|
||||||
@@ -300,7 +317,7 @@ const int *getTextPositions(OLEDDisplay *display)
|
|||||||
{
|
{
|
||||||
static int textPositions[7]; // Static array that persists beyond function scope
|
static int textPositions[7]; // Static array that persists beyond function scope
|
||||||
|
|
||||||
if (display->getHeight() > 64) {
|
if (isHighResolution) {
|
||||||
textPositions[0] = textZeroLine;
|
textPositions[0] = textZeroLine;
|
||||||
textPositions[1] = textFirstLine_medium;
|
textPositions[1] = textFirstLine_medium;
|
||||||
textPositions[2] = textSecondLine_medium;
|
textPositions[2] = textSecondLine_medium;
|
||||||
|
|||||||
@@ -41,12 +41,14 @@ namespace graphics
|
|||||||
// Shared state (declare inside namespace)
|
// Shared state (declare inside namespace)
|
||||||
extern bool hasUnreadMessage;
|
extern bool hasUnreadMessage;
|
||||||
extern bool isMuted;
|
extern bool isMuted;
|
||||||
|
extern bool isHighResolution;
|
||||||
|
void determineResolution(int16_t screenheight, int16_t screenwidth);
|
||||||
|
|
||||||
// Rounded highlight (used for inverted headers)
|
// Rounded highlight (used for inverted headers)
|
||||||
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
|
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
|
||||||
|
|
||||||
// Shared battery/time/mail header
|
// Shared battery/time/mail header
|
||||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "");
|
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool battery_only = false);
|
||||||
|
|
||||||
const int *getTextPositions(OLEDDisplay *display);
|
const int *getTextPositions(OLEDDisplay *display);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace graphics
|
|||||||
|
|
||||||
namespace ClockRenderer
|
namespace ClockRenderer
|
||||||
{
|
{
|
||||||
|
bool digitalWatchFace = true;
|
||||||
|
|
||||||
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
||||||
{
|
{
|
||||||
@@ -146,6 +147,7 @@ void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int heig
|
|||||||
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
|
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale)
|
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode, float scale)
|
||||||
{
|
{
|
||||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||||
@@ -179,21 +181,22 @@ void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool
|
|||||||
drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
drawVerticalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
// Draw a digital clock
|
// Draw a digital clock
|
||||||
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
display->clear();
|
display->clear();
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
int line = 1;
|
int line = 1;
|
||||||
|
// === Set Title, Blank for Clock
|
||||||
|
const char *titleStr = "";
|
||||||
|
// === Header ===
|
||||||
|
graphics::drawCommonHeader(display, x, y, titleStr, true);
|
||||||
|
|
||||||
#ifdef T_WATCH_S3
|
#ifdef T_WATCH_S3
|
||||||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||||
graphics::ClockRenderer::drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2);
|
graphics::ClockRenderer::drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36,
|
|
||||||
graphics::ClockRenderer::digitalWatchFace, 1);
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||||
@@ -230,7 +233,7 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
|||||||
float scale = 1.5;
|
float scale = 1.5;
|
||||||
#else
|
#else
|
||||||
float scale = 0.75;
|
float scale = 0.75;
|
||||||
if (SCREEN_WIDTH > 128) {
|
if (isHighResolution) {
|
||||||
scale = 1.5;
|
scale = 1.5;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -276,17 +279,17 @@ void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int1
|
|||||||
|
|
||||||
// draw seconds string
|
// draw seconds string
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
int xOffset = (SCREEN_WIDTH > 128) ? 0 : -1;
|
int xOffset = (isHighResolution) ? 0 : -1;
|
||||||
if (hour >= 10) {
|
if (hour >= 10) {
|
||||||
xOffset += (SCREEN_WIDTH > 128) ? 32 : 18;
|
xOffset += (isHighResolution) ? 32 : 18;
|
||||||
}
|
}
|
||||||
int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1;
|
int yOffset = (isHighResolution) ? 3 : 1;
|
||||||
if (config.display.use_12h_clock) {
|
if (config.display.use_12h_clock) {
|
||||||
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2,
|
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - yOffset - 2,
|
||||||
isPM ? "pm" : "am");
|
isPM ? "pm" : "am");
|
||||||
}
|
}
|
||||||
#ifndef USE_EINK
|
#ifndef USE_EINK
|
||||||
xOffset = (SCREEN_WIDTH > 128) ? 18 : 10;
|
xOffset = (isHighResolution) ? 18 : 10;
|
||||||
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
|
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - yOffset,
|
||||||
secondString);
|
secondString);
|
||||||
#endif
|
#endif
|
||||||
@@ -301,31 +304,30 @@ void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y)
|
|||||||
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
|
// === Set Title, Blank for Clock
|
||||||
|
const char *titleStr = "";
|
||||||
|
// === Header ===
|
||||||
|
graphics::drawCommonHeader(display, x, y, titleStr, true);
|
||||||
|
|
||||||
graphics::UIRenderer::drawBattery(display, x, y + 7, imgBattery, powerStatus);
|
|
||||||
|
|
||||||
if (powerStatus->getHasBattery()) {
|
|
||||||
char batteryPercent[8];
|
|
||||||
snprintf(batteryPercent, sizeof(batteryPercent), "%d%%", powerStatus->getBatteryChargePercent());
|
|
||||||
|
|
||||||
display->setFont(FONT_SMALL);
|
|
||||||
|
|
||||||
display->drawString(x + 20, y + 2, batteryPercent);
|
|
||||||
}
|
|
||||||
#ifdef T_WATCH_S3
|
#ifdef T_WATCH_S3
|
||||||
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
if (nimbleBluetooth && nimbleBluetooth->isConnected()) {
|
||||||
drawBluetoothConnectedIcon(display, display->getWidth() - 18, y + 2);
|
drawBluetoothConnectedIcon(display, display->getWidth() - 18, display->getHeight() - 14);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
drawWatchFaceToggleButton(display, display->getWidth() - 36, display->getHeight() - 36,
|
|
||||||
graphics::ClockRenderer::digitalWatchFace, 1);
|
|
||||||
|
|
||||||
// clock face center coordinates
|
// clock face center coordinates
|
||||||
int16_t centerX = display->getWidth() / 2;
|
int16_t centerX = display->getWidth() / 2;
|
||||||
int16_t centerY = display->getHeight() / 2;
|
int16_t centerY = display->getHeight() / 2;
|
||||||
|
|
||||||
// clock face radius
|
// clock face radius
|
||||||
int16_t radius = (display->getWidth() / 2) * 0.8;
|
int16_t radius = 0;
|
||||||
|
if (display->getHeight() < display->getWidth()) {
|
||||||
|
radius = (display->getHeight() / 2) * 0.9;
|
||||||
|
} else {
|
||||||
|
radius = (display->getWidth() / 2) * 0.9;
|
||||||
|
}
|
||||||
|
#ifdef T_WATCH_S3
|
||||||
|
radius = (display->getWidth() / 2) * 0.8;
|
||||||
|
#endif
|
||||||
|
|
||||||
// noon (0 deg) coordinates (outermost circle)
|
// noon (0 deg) coordinates (outermost circle)
|
||||||
int16_t noonX = centerX;
|
int16_t noonX = centerX;
|
||||||
@@ -338,10 +340,16 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
int16_t tickMarkOuterNoonY = secondHandNoonY;
|
int16_t tickMarkOuterNoonY = secondHandNoonY;
|
||||||
|
|
||||||
// seconds tick mark inner y coordinate; (second nested circle)
|
// seconds tick mark inner y coordinate; (second nested circle)
|
||||||
double secondsTickMarkInnerNoonY = (double)noonY + 8;
|
double secondsTickMarkInnerNoonY = (double)noonY + 4;
|
||||||
|
if (isHighResolution) {
|
||||||
|
secondsTickMarkInnerNoonY = (double)noonY + 8;
|
||||||
|
}
|
||||||
|
|
||||||
// hours tick mark inner y coordinate; (third nested circle)
|
// hours tick mark inner y coordinate; (third nested circle)
|
||||||
double hoursTickMarkInnerNoonY = (double)noonY + 16;
|
double hoursTickMarkInnerNoonY = (double)noonY + 6;
|
||||||
|
if (isHighResolution) {
|
||||||
|
hoursTickMarkInnerNoonY = (double)noonY + 16;
|
||||||
|
}
|
||||||
|
|
||||||
// minute hand y coordinate
|
// minute hand y coordinate
|
||||||
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
|
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
|
||||||
@@ -350,7 +358,10 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
int16_t hourStringNoonY = minuteHandNoonY + 18;
|
int16_t hourStringNoonY = minuteHandNoonY + 18;
|
||||||
|
|
||||||
// hour hand radius and y coordinate
|
// hour hand radius and y coordinate
|
||||||
int16_t hourHandRadius = radius * 0.55;
|
int16_t hourHandRadius = radius * 0.35;
|
||||||
|
if (isHighResolution) {
|
||||||
|
int16_t hourHandRadius = radius * 0.55;
|
||||||
|
}
|
||||||
int16_t hourHandNoonY = centerY - hourHandRadius;
|
int16_t hourHandNoonY = centerY - hourHandRadius;
|
||||||
|
|
||||||
display->setColor(OLEDDISPLAY_COLOR::WHITE);
|
display->setColor(OLEDDISPLAY_COLOR::WHITE);
|
||||||
@@ -366,7 +377,20 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
int minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||||
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
int second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||||
|
|
||||||
hour = hour > 12 ? hour - 12 : hour;
|
bool isPM = hour >= 12;
|
||||||
|
if (config.display.use_12h_clock) {
|
||||||
|
bool isPM = hour >= 12;
|
||||||
|
display->setFont(FONT_SMALL);
|
||||||
|
int yOffset = isHighResolution ? 1 : 0;
|
||||||
|
#ifdef USE_EINK
|
||||||
|
yOffset += 3;
|
||||||
|
#endif
|
||||||
|
display->drawString(centerX - (display->getStringWidth(isPM ? "pm" : "am") / 2), centerY + yOffset,
|
||||||
|
isPM ? "pm" : "am");
|
||||||
|
}
|
||||||
|
hour %= 12;
|
||||||
|
if (hour == 0)
|
||||||
|
hour = 12;
|
||||||
|
|
||||||
int16_t degreesPerHour = 30;
|
int16_t degreesPerHour = 30;
|
||||||
int16_t degreesPerMinuteOrSecond = 6;
|
int16_t degreesPerMinuteOrSecond = 6;
|
||||||
@@ -443,16 +467,32 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset;
|
double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset;
|
||||||
double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset;
|
double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset;
|
||||||
|
|
||||||
|
#ifdef T_WATCH_S3
|
||||||
// draw hour number
|
// draw hour number
|
||||||
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||||
|
#else
|
||||||
|
#ifdef USE_EINK
|
||||||
|
if (isHighResolution) {
|
||||||
|
// draw hour number
|
||||||
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (isHighResolution && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) {
|
||||||
|
// draw hour number
|
||||||
|
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
if (angle % degreesPerMinuteOrSecond == 0) {
|
if (angle % degreesPerMinuteOrSecond == 0) {
|
||||||
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
|
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
|
||||||
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
|
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
|
||||||
|
|
||||||
// draw minute tick mark
|
if (isHighResolution) {
|
||||||
display->drawLine(startX, startY, endX, endY);
|
// draw minute tick mark
|
||||||
|
display->drawLine(startX, startY, endX, endY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,9 +501,10 @@ void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
|
|
||||||
// draw minute hand
|
// draw minute hand
|
||||||
display->drawLine(centerX, centerY, minuteX, minuteY);
|
display->drawLine(centerX, centerY, minuteX, minuteY);
|
||||||
|
#ifndef USE_EINK
|
||||||
// draw second hand
|
// draw second hand
|
||||||
display->drawLine(centerX, centerY, secondX, secondY);
|
display->drawLine(centerX, centerY, secondX, secondY);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class Screen;
|
|||||||
namespace ClockRenderer
|
namespace ClockRenderer
|
||||||
{
|
{
|
||||||
// Whether we are showing the digital watch face or the analog one
|
// Whether we are showing the digital watch face or the analog one
|
||||||
static bool digitalWatchFace = true;
|
extern bool digitalWatchFace;
|
||||||
|
|
||||||
// Clock frame functions
|
// Clock frame functions
|
||||||
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
@@ -25,7 +25,7 @@ void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int he
|
|||||||
void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height);
|
void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height);
|
||||||
|
|
||||||
// UI elements for clock displays
|
// UI elements for clock displays
|
||||||
void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1);
|
// void drawWatchFaceToggleButton(OLEDDisplay *display, int16_t x, int16_t y, bool digitalMode = true, float scale = 1);
|
||||||
void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y);
|
void drawBluetoothConnectedIcon(OLEDDisplay *display, int16_t x, int16_t y);
|
||||||
|
|
||||||
} // namespace ClockRenderer
|
} // namespace ClockRenderer
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
#include "gps/GeoCoord.h"
|
#include "gps/GeoCoord.h"
|
||||||
#include "graphics/ScreenFonts.h"
|
#include "graphics/ScreenFonts.h"
|
||||||
|
#include "graphics/SharedUIDisplay.h"
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
namespace graphics
|
namespace graphics
|
||||||
@@ -45,7 +46,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
|
|||||||
// This could draw a "N" indicator or north arrow
|
// This could draw a "N" indicator or north arrow
|
||||||
// For now, we'll draw a simple north indicator
|
// For now, we'll draw a simple north indicator
|
||||||
// const float radius = 17.0f;
|
// const float radius = 17.0f;
|
||||||
if (display->width() > 128) {
|
if (isHighResolution) {
|
||||||
radius += 4;
|
radius += 4;
|
||||||
}
|
}
|
||||||
Point north(0, -radius);
|
Point north(0, -radius);
|
||||||
@@ -55,7 +56,7 @@ void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY,
|
|||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
if (display->width() > 128) {
|
if (isHighResolution) {
|
||||||
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
||||||
} else {
|
} else {
|
||||||
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
||||||
|
|||||||
@@ -67,21 +67,6 @@ void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16
|
|||||||
|
|
||||||
char channelStr[20];
|
char channelStr[20];
|
||||||
snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex()));
|
snprintf(channelStr, sizeof(channelStr), "#%s", channels.getName(channels.getPrimaryIndex()));
|
||||||
|
|
||||||
// Display power status
|
|
||||||
if (powerStatus->getHasBattery()) {
|
|
||||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
|
||||||
UIRenderer::drawBattery(display, x, y + 2, imgBattery, powerStatus);
|
|
||||||
} else {
|
|
||||||
UIRenderer::drawBattery(display, x + 1, y + 3, imgBattery, powerStatus);
|
|
||||||
}
|
|
||||||
} else if (powerStatus->knowsUSB()) {
|
|
||||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
|
||||||
display->drawFastImage(x, y + 2, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower);
|
|
||||||
} else {
|
|
||||||
display->drawFastImage(x + 1, y + 3, 16, 8, powerStatus->getHasUSB() ? imgUSB : imgPower);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Display nodes status
|
// Display nodes status
|
||||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_DEFAULT) {
|
||||||
UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus);
|
UIRenderer::drawNodes(display, x + (SCREEN_WIDTH * 0.25), y + 2, nodeStatus);
|
||||||
@@ -393,7 +378,7 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
int line = 1;
|
int line = 1;
|
||||||
|
|
||||||
// === Set Title
|
// === Set Title
|
||||||
const char *titleStr = (SCREEN_WIDTH > 128) ? "LoRa Info" : "LoRa";
|
const char *titleStr = (isHighResolution) ? "LoRa Info" : "LoRa";
|
||||||
|
|
||||||
// === Header ===
|
// === Header ===
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
@@ -444,12 +429,12 @@ void drawLoRaFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
char chUtilPercentage[10];
|
char chUtilPercentage[10];
|
||||||
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
|
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
|
||||||
|
|
||||||
int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
|
int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
|
||||||
int chUtil_y = getTextPositions(display)[line] + 3;
|
int chUtil_y = getTextPositions(display)[line] + 3;
|
||||||
|
|
||||||
int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50;
|
int chutil_bar_width = (isHighResolution) ? 100 : 50;
|
||||||
int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7;
|
int chutil_bar_height = (isHighResolution) ? 12 : 7;
|
||||||
int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3;
|
int extraoffset = (isHighResolution) ? 6 : 3;
|
||||||
int chutil_percent = airTime->channelUtilizationPercent();
|
int chutil_percent = airTime->channelUtilizationPercent();
|
||||||
|
|
||||||
int centerofscreen = SCREEN_WIDTH / 2;
|
int centerofscreen = SCREEN_WIDTH / 2;
|
||||||
@@ -516,7 +501,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
int line = 1;
|
int line = 1;
|
||||||
const int barHeight = 6;
|
const int barHeight = 6;
|
||||||
const int labelX = x;
|
const int labelX = x;
|
||||||
const int barsOffset = (SCREEN_WIDTH > 128) ? 24 : 0;
|
const int barsOffset = (isHighResolution) ? 24 : 0;
|
||||||
const int barX = x + 40 + barsOffset;
|
const int barX = x + 40 + barsOffset;
|
||||||
|
|
||||||
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
|
auto drawUsageRow = [&](const char *label, uint32_t used, uint32_t total, bool isHeap = false) {
|
||||||
@@ -526,7 +511,7 @@ void drawMemoryUsage(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x,
|
|||||||
int percent = (used * 100) / total;
|
int percent = (used * 100) / total;
|
||||||
|
|
||||||
char combinedStr[24];
|
char combinedStr[24];
|
||||||
if (SCREEN_WIDTH > 128) {
|
if (isHighResolution) {
|
||||||
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024,
|
snprintf(combinedStr, sizeof(combinedStr), "%s%3d%% %u/%uKB", (percent > 80) ? "! " : "", percent, used / 1024,
|
||||||
total / 1024);
|
total / 1024);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
457
src/graphics/draw/MenuHandler.cpp
Normal file
457
src/graphics/draw/MenuHandler.cpp
Normal file
@@ -0,0 +1,457 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
#if HAS_SCREEN
|
||||||
|
#include "ClockRenderer.h"
|
||||||
|
#include "GPS.h"
|
||||||
|
#include "MenuHandler.h"
|
||||||
|
#include "MeshRadio.h"
|
||||||
|
#include "MeshService.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
|
#include "buzz.h"
|
||||||
|
#include "graphics/Screen.h"
|
||||||
|
#include "graphics/draw/UIRenderer.h"
|
||||||
|
#include "main.h"
|
||||||
|
#include "modules/AdminModule.h"
|
||||||
|
#include "modules/CannedMessageModule.h"
|
||||||
|
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
menuHandler::screenMenus menuHandler::menuQueue = menu_none;
|
||||||
|
|
||||||
|
void menuHandler::LoraRegionPicker(uint32_t duration)
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back",
|
||||||
|
"US",
|
||||||
|
"EU_433",
|
||||||
|
"EU_868",
|
||||||
|
"CN",
|
||||||
|
"JP",
|
||||||
|
"ANZ",
|
||||||
|
"KR",
|
||||||
|
"TW",
|
||||||
|
"RU",
|
||||||
|
"IN",
|
||||||
|
"NZ_865",
|
||||||
|
"TH",
|
||||||
|
"LORA_24",
|
||||||
|
"UA_433",
|
||||||
|
"UA_868",
|
||||||
|
"MY_433",
|
||||||
|
"MY_"
|
||||||
|
"919",
|
||||||
|
"SG_"
|
||||||
|
"923",
|
||||||
|
"PH_433",
|
||||||
|
"PH_868",
|
||||||
|
"PH_915",
|
||||||
|
"ANZ_433"};
|
||||||
|
screen->showOverlayBanner(
|
||||||
|
"Set the LoRa region", duration, optionsArray, 23,
|
||||||
|
[](int selected) -> void {
|
||||||
|
if (selected != 0 && config.lora.region != _meshtastic_Config_LoRaConfig_RegionCode(selected)) {
|
||||||
|
config.lora.region = _meshtastic_Config_LoRaConfig_RegionCode(selected);
|
||||||
|
// This is needed as we wait til picking the LoRa region to generate keys for the first time.
|
||||||
|
if (!owner.is_licensed) {
|
||||||
|
bool keygenSuccess = false;
|
||||||
|
if (config.security.private_key.size == 32) {
|
||||||
|
// public key is derived from private, so this will always have the same result.
|
||||||
|
if (crypto->regeneratePublicKey(config.security.public_key.bytes, config.security.private_key.bytes)) {
|
||||||
|
keygenSuccess = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_INFO("Generate new PKI keys");
|
||||||
|
crypto->generateKeyPair(config.security.public_key.bytes, config.security.private_key.bytes);
|
||||||
|
keygenSuccess = true;
|
||||||
|
}
|
||||||
|
if (keygenSuccess) {
|
||||||
|
config.security.public_key.size = 32;
|
||||||
|
config.security.private_key.size = 32;
|
||||||
|
owner.public_key.size = 32;
|
||||||
|
memcpy(owner.public_key.bytes, config.security.public_key.bytes, 32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.lora.tx_enabled = true;
|
||||||
|
initRegion();
|
||||||
|
if (myRegion->dutyCycle < 100) {
|
||||||
|
config.lora.ignore_mqtt = true; // Ignore MQTT by default if region has a duty cycle limit
|
||||||
|
}
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::TwelveHourPicker()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "12-hour", "24-hour"};
|
||||||
|
screen->showOverlayBanner("Time Format", 30000, optionsArray, 3, [](int selected) -> void {
|
||||||
|
if (selected == 0) {
|
||||||
|
menuHandler::menuQueue = menuHandler::clock_menu;
|
||||||
|
} else if (selected == 1) {
|
||||||
|
config.display.use_12h_clock = true;
|
||||||
|
} else {
|
||||||
|
config.display.use_12h_clock = false;
|
||||||
|
}
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::ClockFacePicker()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "Digital", "Analog"};
|
||||||
|
screen->showOverlayBanner("Which Face?", 30000, optionsArray, 3, [](int selected) -> void {
|
||||||
|
if (selected == 0) {
|
||||||
|
menuHandler::menuQueue = menuHandler::clock_menu;
|
||||||
|
} else if (selected == 1) {
|
||||||
|
graphics::ClockRenderer::digitalWatchFace = true;
|
||||||
|
screen->setFrames(Screen::FOCUS_CLOCK);
|
||||||
|
} else {
|
||||||
|
graphics::ClockRenderer::digitalWatchFace = false;
|
||||||
|
screen->setFrames(Screen::FOCUS_CLOCK);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::TZPicker()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back",
|
||||||
|
"US/Hawaii",
|
||||||
|
"US/Alaska",
|
||||||
|
"US/Pacific",
|
||||||
|
"US/Arizona",
|
||||||
|
"US/Mountain",
|
||||||
|
"US/Central",
|
||||||
|
"US/Eastern",
|
||||||
|
"UTC",
|
||||||
|
"EU/Western",
|
||||||
|
"EU/"
|
||||||
|
"Central",
|
||||||
|
"EU/Eastern",
|
||||||
|
"Asia/Kolkata",
|
||||||
|
"Asia/Hong_Kong",
|
||||||
|
"AU/AWST",
|
||||||
|
"AU/ACST",
|
||||||
|
"AU/AEST",
|
||||||
|
"Pacific/NZ"};
|
||||||
|
screen->showOverlayBanner("Pick Timezone", 30000, optionsArray, 17, [](int selected) -> void {
|
||||||
|
if (selected == 0) {
|
||||||
|
menuHandler::menuQueue = menuHandler::clock_menu;
|
||||||
|
} else if (selected == 1) { // Hawaii
|
||||||
|
strncpy(config.device.tzdef, "HST10", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 2) { // Alaska
|
||||||
|
strncpy(config.device.tzdef, "AKST9AKDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 3) { // Pacific
|
||||||
|
strncpy(config.device.tzdef, "PST8PDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 4) { // Arizona
|
||||||
|
strncpy(config.device.tzdef, "MST7", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 5) { // Mountain
|
||||||
|
strncpy(config.device.tzdef, "MST7MDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 6) { // Central
|
||||||
|
strncpy(config.device.tzdef, "CST6CDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 7) { // Eastern
|
||||||
|
strncpy(config.device.tzdef, "EST5EDT,M3.2.0,M11.1.0", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 8) { // UTC
|
||||||
|
strncpy(config.device.tzdef, "UTC", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 9) { // EU/Western
|
||||||
|
strncpy(config.device.tzdef, "GMT0BST,M3.5.0/1,M10.5.0", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 10) { // EU/Central
|
||||||
|
strncpy(config.device.tzdef, "CET-1CEST,M3.5.0,M10.5.0/3", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 11) { // EU/Eastern
|
||||||
|
strncpy(config.device.tzdef, "EET-2EEST,M3.5.0/3,M10.5.0/4", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 12) { // Asia/Kolkata
|
||||||
|
strncpy(config.device.tzdef, "IST-5:30", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 13) { // China
|
||||||
|
strncpy(config.device.tzdef, "HKT-8", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 14) { // AU/AWST
|
||||||
|
strncpy(config.device.tzdef, "AWST-8", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 15) { // AU/ACST
|
||||||
|
strncpy(config.device.tzdef, "ACST-9:30ACDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 16) { // AU/AEST
|
||||||
|
strncpy(config.device.tzdef, "AEST-10AEDT,M10.1.0,M4.1.0/3", sizeof(config.device.tzdef));
|
||||||
|
} else if (selected == 17) { // NZ
|
||||||
|
strncpy(config.device.tzdef, "NZST-12NZDT,M9.5.0,M4.1.0/3", sizeof(config.device.tzdef));
|
||||||
|
}
|
||||||
|
if (selected != 0) {
|
||||||
|
setenv("TZ", config.device.tzdef, 1);
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::clockMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "Clock Face", "Time Format", "Timezone"};
|
||||||
|
screen->showOverlayBanner("Clock Action", 30000, optionsArray, 4, [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
menuHandler::menuQueue = menuHandler::clock_face_picker;
|
||||||
|
screen->setInterval(0);
|
||||||
|
runASAP = true;
|
||||||
|
} else if (selected == 2) {
|
||||||
|
menuHandler::menuQueue = menuHandler::twelve_hour_picker;
|
||||||
|
screen->setInterval(0);
|
||||||
|
runASAP = true;
|
||||||
|
} else if (selected == 3) {
|
||||||
|
menuHandler::menuQueue = menuHandler::TZ_picker;
|
||||||
|
screen->setInterval(0);
|
||||||
|
runASAP = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::messageResponseMenu()
|
||||||
|
{
|
||||||
|
|
||||||
|
static const char **optionsArrayPtr;
|
||||||
|
int options;
|
||||||
|
if (kb_found) {
|
||||||
|
static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset", "Reply via Freetext"};
|
||||||
|
optionsArrayPtr = optionsArray;
|
||||||
|
options = 4;
|
||||||
|
} else {
|
||||||
|
static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset"};
|
||||||
|
optionsArrayPtr = optionsArray;
|
||||||
|
options = 3;
|
||||||
|
}
|
||||||
|
#ifdef HAS_I2S
|
||||||
|
static const char *optionsArray[] = {"Back", "Dismiss", "Reply via Preset", "Reply via Freetext", "Read Aloud"};
|
||||||
|
optionsArrayPtr = optionsArray;
|
||||||
|
options = 5;
|
||||||
|
#endif
|
||||||
|
screen->showOverlayBanner("Message Action", 30000, optionsArrayPtr, options, [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
screen->dismissCurrentFrame();
|
||||||
|
} else if (selected == 2) {
|
||||||
|
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||||
|
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||||
|
} else {
|
||||||
|
cannedMessageModule->LaunchWithDestination(devicestate.rx_text_message.from);
|
||||||
|
}
|
||||||
|
} else if (selected == 3) {
|
||||||
|
if (devicestate.rx_text_message.to == NODENUM_BROADCAST) {
|
||||||
|
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST, devicestate.rx_text_message.channel);
|
||||||
|
} else {
|
||||||
|
cannedMessageModule->LaunchFreetextWithDestination(devicestate.rx_text_message.from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef HAS_I2S
|
||||||
|
else if (selected == 4) {
|
||||||
|
const meshtastic_MeshPacket &mp = devicestate.rx_text_message;
|
||||||
|
const char *msg = reinterpret_cast<const char *>(mp.decoded.payload.bytes);
|
||||||
|
|
||||||
|
audioThread->readAloud(msg);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::homeBaseMenu()
|
||||||
|
{
|
||||||
|
int options;
|
||||||
|
static const char **optionsArrayPtr;
|
||||||
|
|
||||||
|
if (kb_found) {
|
||||||
|
#ifdef PIN_EINK_EN
|
||||||
|
static const char *optionsArray[] = {"Back", "Toggle Backlight", "Send Position", "New Preset Msg", "New Freetext Msg"};
|
||||||
|
#else
|
||||||
|
static const char *optionsArray[] = {"Back", "Sleep Screen", "Send Position", "New Preset Msg", "New Freetext Msg"};
|
||||||
|
#endif
|
||||||
|
optionsArrayPtr = optionsArray;
|
||||||
|
options = 5;
|
||||||
|
} else {
|
||||||
|
#ifdef PIN_EINK_EN
|
||||||
|
static const char *optionsArray[] = {"Back", "Toggle Backlight", "Send Position", "New Preset Msg"};
|
||||||
|
#else
|
||||||
|
static const char *optionsArray[] = {"Back", "Sleep Screen", "Send Position", "New Preset Msg"};
|
||||||
|
#endif
|
||||||
|
optionsArrayPtr = optionsArray;
|
||||||
|
options = 4;
|
||||||
|
}
|
||||||
|
screen->showOverlayBanner("Home Action", 30000, optionsArrayPtr, options, [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
#ifdef PIN_EINK_EN
|
||||||
|
if (digitalRead(PIN_EINK_EN) == HIGH) {
|
||||||
|
digitalWrite(PIN_EINK_EN, LOW);
|
||||||
|
} else {
|
||||||
|
digitalWrite(PIN_EINK_EN, HIGH);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
screen->setOn(false);
|
||||||
|
#endif
|
||||||
|
} else if (selected == 2) {
|
||||||
|
service->refreshLocalMeshNode();
|
||||||
|
if (service->trySendPosition(NODENUM_BROADCAST, true)) {
|
||||||
|
LOG_INFO("Position Sent");
|
||||||
|
} else {
|
||||||
|
LOG_INFO("Node Info Sent");
|
||||||
|
}
|
||||||
|
} else if (selected == 3) {
|
||||||
|
cannedMessageModule->LaunchWithDestination(NODENUM_BROADCAST);
|
||||||
|
} else if (selected == 4) {
|
||||||
|
cannedMessageModule->LaunchFreetextWithDestination(NODENUM_BROADCAST);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::favoriteBaseMenu()
|
||||||
|
{
|
||||||
|
int options;
|
||||||
|
static const char **optionsArrayPtr;
|
||||||
|
|
||||||
|
if (kb_found) {
|
||||||
|
static const char *optionsArray[] = {"Back", "New Preset Msg", "New Freetext Msg"};
|
||||||
|
optionsArrayPtr = optionsArray;
|
||||||
|
options = 3;
|
||||||
|
} else {
|
||||||
|
static const char *optionsArray[] = {"Back", "New Preset Msg"};
|
||||||
|
optionsArrayPtr = optionsArray;
|
||||||
|
options = 2;
|
||||||
|
}
|
||||||
|
screen->showOverlayBanner("Favorites Action", 30000, optionsArrayPtr, options, [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
cannedMessageModule->LaunchWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||||
|
} else if (selected == 2) {
|
||||||
|
cannedMessageModule->LaunchFreetextWithDestination(graphics::UIRenderer::currentFavoriteNodeNum);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::positionBaseMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "GPS Toggle", "Compass Point"};
|
||||||
|
screen->showOverlayBanner("Position Action", 30000, optionsArray, 3, [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
menuQueue = gps_toggle_menu;
|
||||||
|
} else if (selected == 2) {
|
||||||
|
menuQueue = compass_point_north_menu;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::nodeListMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "Reset NodeDB"};
|
||||||
|
screen->showOverlayBanner("Node Action", 30000, optionsArray, 2, [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
menuQueue = reset_node_db_menu;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::resetNodeDBMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "Confirm"};
|
||||||
|
screen->showOverlayBanner("Confirm Reset NodeDB", 30000, optionsArray, 2, [](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
disableBluetooth();
|
||||||
|
LOG_INFO("Initiate node-db reset");
|
||||||
|
nodeDB->resetNodes();
|
||||||
|
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::compassNorthMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "Points Up", "Dynamic"};
|
||||||
|
screen->showOverlayBanner("Needle Direction?", 30000, optionsArray, 3, [](int selected) -> void {
|
||||||
|
if (selected == 1 && config.display.compass_north_top != true) {
|
||||||
|
config.display.compass_north_top = true;
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
screen->setFrames();
|
||||||
|
} else if (selected == 2 && config.display.compass_north_top != false) {
|
||||||
|
config.display.compass_north_top = false;
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
screen->setFrames();
|
||||||
|
} else if (selected == 0) {
|
||||||
|
menuQueue = position_base_menu;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::GPSToggleMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Back", "Enabled", "Disabled"};
|
||||||
|
screen->showOverlayBanner(
|
||||||
|
"Toggle GPS", 30000, optionsArray, 3,
|
||||||
|
[](int selected) -> void {
|
||||||
|
if (selected == 1) {
|
||||||
|
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_ENABLED;
|
||||||
|
playGPSEnableBeep();
|
||||||
|
gps->enable();
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
} else if (selected == 2) {
|
||||||
|
config.position.gps_mode = meshtastic_Config_PositionConfig_GpsMode_DISABLED;
|
||||||
|
playGPSDisableBeep();
|
||||||
|
gps->disable();
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
} else {
|
||||||
|
menuQueue = position_base_menu;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED ? 1 : 2); // set inital selection
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::BuzzerModeMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"All Enabled", "Disabled", "Notifications", "System Only"};
|
||||||
|
screen->showOverlayBanner(
|
||||||
|
"Beep Action", 30000, optionsArray, 4,
|
||||||
|
[](int selected) -> void {
|
||||||
|
config.device.buzzer_mode = (meshtastic_Config_DeviceConfig_BuzzerMode)selected;
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
},
|
||||||
|
config.device.buzzer_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::switchToMUIMenu()
|
||||||
|
{
|
||||||
|
static const char *optionsArray[] = {"Yes", "No"};
|
||||||
|
screen->showOverlayBanner("Switch to MUI?", 30000, optionsArray, 2, [](int selected) -> void {
|
||||||
|
if (selected == 0) {
|
||||||
|
config.display.displaymode = meshtastic_Config_DisplayConfig_DisplayMode_COLOR;
|
||||||
|
config.bluetooth.enabled = false;
|
||||||
|
service->reloadConfig(SEGMENT_CONFIG);
|
||||||
|
rebootAtMsec = (millis() + DEFAULT_REBOOT_SECONDS * 1000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void menuHandler::handleMenuSwitch()
|
||||||
|
{
|
||||||
|
switch (menuQueue) {
|
||||||
|
case menu_none:
|
||||||
|
break;
|
||||||
|
case lora_picker:
|
||||||
|
LoraRegionPicker();
|
||||||
|
break;
|
||||||
|
case TZ_picker:
|
||||||
|
TZPicker();
|
||||||
|
break;
|
||||||
|
case twelve_hour_picker:
|
||||||
|
TwelveHourPicker();
|
||||||
|
break;
|
||||||
|
case clock_face_picker:
|
||||||
|
ClockFacePicker();
|
||||||
|
break;
|
||||||
|
case clock_menu:
|
||||||
|
clockMenu();
|
||||||
|
break;
|
||||||
|
case position_base_menu:
|
||||||
|
positionBaseMenu();
|
||||||
|
break;
|
||||||
|
case gps_toggle_menu:
|
||||||
|
GPSToggleMenu();
|
||||||
|
break;
|
||||||
|
case compass_point_north_menu:
|
||||||
|
compassNorthMenu();
|
||||||
|
break;
|
||||||
|
case reset_node_db_menu:
|
||||||
|
resetNodeDBMenu();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
menuQueue = menu_none;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
|
|
||||||
|
#endif
|
||||||
40
src/graphics/draw/MenuHandler.h
Normal file
40
src/graphics/draw/MenuHandler.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
#include "configuration.h"
|
||||||
|
namespace graphics
|
||||||
|
{
|
||||||
|
|
||||||
|
class menuHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum screenMenus {
|
||||||
|
menu_none,
|
||||||
|
lora_picker,
|
||||||
|
TZ_picker,
|
||||||
|
twelve_hour_picker,
|
||||||
|
clock_face_picker,
|
||||||
|
clock_menu,
|
||||||
|
position_base_menu,
|
||||||
|
gps_toggle_menu,
|
||||||
|
compass_point_north_menu,
|
||||||
|
reset_node_db_menu
|
||||||
|
};
|
||||||
|
static screenMenus menuQueue;
|
||||||
|
|
||||||
|
static void LoraRegionPicker(uint32_t duration = 30000);
|
||||||
|
static void handleMenuSwitch();
|
||||||
|
static void clockMenu();
|
||||||
|
static void TZPicker();
|
||||||
|
static void TwelveHourPicker();
|
||||||
|
static void ClockFacePicker();
|
||||||
|
static void messageResponseMenu();
|
||||||
|
static void homeBaseMenu();
|
||||||
|
static void favoriteBaseMenu();
|
||||||
|
static void positionBaseMenu();
|
||||||
|
static void compassNorthMenu();
|
||||||
|
static void GPSToggleMenu();
|
||||||
|
static void BuzzerModeMenu();
|
||||||
|
static void switchToMUIMenu();
|
||||||
|
static void nodeListMenu();
|
||||||
|
static void resetNodeDBMenu();
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace graphics
|
||||||
@@ -56,6 +56,11 @@ namespace graphics
|
|||||||
namespace MessageRenderer
|
namespace MessageRenderer
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// Simple cache based on text hash
|
||||||
|
static size_t cachedKey = 0;
|
||||||
|
static std::vector<std::string> cachedLines;
|
||||||
|
static std::vector<int> cachedHeights;
|
||||||
|
|
||||||
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
|
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
|
||||||
{
|
{
|
||||||
int cursorX = x;
|
int cursorX = x;
|
||||||
@@ -225,6 +230,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
sender);
|
sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint32_t now = millis();
|
||||||
#ifndef EXCLUDE_EMOJI
|
#ifndef EXCLUDE_EMOJI
|
||||||
// === Bounce animation setup ===
|
// === Bounce animation setup ===
|
||||||
static uint32_t lastBounceTime = 0;
|
static uint32_t lastBounceTime = 0;
|
||||||
@@ -232,7 +238,6 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
const int bounceRange = 2; // Max pixels to bounce up/down
|
const int bounceRange = 2; // Max pixels to bounce up/down
|
||||||
const int bounceInterval = 10; // How quickly to change bounce direction (ms)
|
const int bounceInterval = 10; // How quickly to change bounce direction (ms)
|
||||||
|
|
||||||
uint32_t now = millis();
|
|
||||||
if (now - lastBounceTime >= bounceInterval) {
|
if (now - lastBounceTime >= bounceInterval) {
|
||||||
lastBounceTime = now;
|
lastBounceTime = now;
|
||||||
bounceY = (bounceY + 1) % (bounceRange * 2);
|
bounceY = (bounceY + 1) % (bounceRange * 2);
|
||||||
@@ -246,82 +251,51 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
display->drawString(x + 4, headerY, headerStr);
|
display->drawString(x + 4, headerY, headerStr);
|
||||||
|
|
||||||
// Draw separator (same as scroll version)
|
// Draw separator (same as scroll version)
|
||||||
for (int separatorX = 0; separatorX <= (display->getStringWidth(headerStr) + 3); separatorX += 2) {
|
for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) {
|
||||||
display->setPixel(separatorX, headerY + ((SCREEN_WIDTH > 128) ? 19 : 13));
|
display->setPixel(separatorX, headerY + ((isHighResolution) ? 19 : 13));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Center the emote below the header line + separator + nav
|
// Center the emote below the header line + separator + nav
|
||||||
int remainingHeight = SCREEN_HEIGHT - (headerY + FONT_HEIGHT_SMALL) - navHeight;
|
int remainingHeight = SCREEN_HEIGHT - (headerY + FONT_HEIGHT_SMALL) - navHeight;
|
||||||
int emoteY = headerY + FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange;
|
int emoteY = headerY + 6 + FONT_HEIGHT_SMALL + (remainingHeight - e.height) / 2 + bounceY - bounceRange;
|
||||||
display->drawXbm((SCREEN_WIDTH - e.width) / 2, emoteY, e.width, e.height, e.bitmap);
|
display->drawXbm((SCREEN_WIDTH - e.width) / 2, emoteY, e.width, e.height, e.bitmap);
|
||||||
|
|
||||||
|
// Draw header at the end to sort out overlapping elements
|
||||||
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
// === Generate the cache key ===
|
||||||
|
size_t currentKey = (size_t)mp.from;
|
||||||
|
currentKey ^= ((size_t)mp.to << 8);
|
||||||
|
currentKey ^= ((size_t)mp.rx_time << 16);
|
||||||
|
currentKey ^= ((size_t)mp.id << 24);
|
||||||
|
|
||||||
// === Word-wrap and build line list ===
|
if (cachedKey != currentKey) {
|
||||||
std::vector<std::string> lines;
|
LOG_INFO("Message cache key is misssed cachedKey=0x%0x, currentKey=0x%x", cachedKey, currentKey);
|
||||||
lines.push_back(std::string(headerStr)); // Header line is always first
|
|
||||||
|
|
||||||
std::string line, word;
|
// Cache miss - regenerate lines and heights
|
||||||
for (int i = 0; messageBuf[i]; ++i) {
|
cachedLines = generateLines(display, headerStr, messageBuf, textWidth);
|
||||||
char ch = messageBuf[i];
|
cachedHeights = calculateLineHeights(cachedLines, emotes);
|
||||||
if (ch == '\n') {
|
cachedKey = currentKey;
|
||||||
if (!word.empty())
|
} else {
|
||||||
line += word;
|
// Cache hit but update the header line with current time information
|
||||||
if (!line.empty())
|
cachedLines[0] = std::string(headerStr);
|
||||||
lines.push_back(line);
|
// The header always has a fixed height since it doesn't contain emotes
|
||||||
line.clear();
|
// As per calculateLineHeights logic for lines without emotes:
|
||||||
word.clear();
|
cachedHeights[0] = FONT_HEIGHT_SMALL - 2;
|
||||||
} else if (ch == ' ') {
|
if (cachedHeights[0] < 8)
|
||||||
line += word + ' ';
|
cachedHeights[0] = 8; // minimum safety
|
||||||
word.clear();
|
|
||||||
} else {
|
|
||||||
word += ch;
|
|
||||||
std::string test = line + word;
|
|
||||||
if (display->getStringWidth(test.c_str()) > textWidth) {
|
|
||||||
if (!line.empty())
|
|
||||||
lines.push_back(line);
|
|
||||||
line = word;
|
|
||||||
word.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (!word.empty())
|
|
||||||
line += word;
|
|
||||||
if (!line.empty())
|
|
||||||
lines.push_back(line);
|
|
||||||
|
|
||||||
// === Scrolling logic ===
|
// === Scrolling logic ===
|
||||||
std::vector<int> rowHeights;
|
|
||||||
|
|
||||||
for (const auto &_line : lines) {
|
|
||||||
int lineHeight = FONT_HEIGHT_SMALL;
|
|
||||||
bool hasEmote = false;
|
|
||||||
|
|
||||||
for (int i = 0; i < numEmotes; ++i) {
|
|
||||||
const Emote &e = emotes[i];
|
|
||||||
if (_line.find(e.label) != std::string::npos) {
|
|
||||||
lineHeight = std::max(lineHeight, e.height);
|
|
||||||
hasEmote = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply tighter spacing if no emotes on this line
|
|
||||||
if (!hasEmote) {
|
|
||||||
lineHeight -= 2; // reduce by 2px for tighter spacing
|
|
||||||
if (lineHeight < 8)
|
|
||||||
lineHeight = 8; // minimum safety
|
|
||||||
}
|
|
||||||
|
|
||||||
rowHeights.push_back(lineHeight);
|
|
||||||
}
|
|
||||||
int totalHeight = 0;
|
int totalHeight = 0;
|
||||||
for (size_t i = 1; i < rowHeights.size(); ++i) {
|
for (size_t i = 1; i < cachedHeights.size(); ++i) {
|
||||||
totalHeight += rowHeights[i];
|
totalHeight += cachedHeights[i];
|
||||||
}
|
}
|
||||||
int usableScrollHeight = usableHeight - rowHeights[0]; // remove header height
|
int usableScrollHeight = usableHeight - cachedHeights[0]; // remove header height
|
||||||
int scrollStop = std::max(0, totalHeight - usableScrollHeight + rowHeights.back());
|
int scrollStop = std::max(0, totalHeight - usableScrollHeight + cachedHeights.back());
|
||||||
|
|
||||||
static float scrollY = 0.0f;
|
static float scrollY = 0.0f;
|
||||||
static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0;
|
static uint32_t lastTime = 0, scrollStartDelay = 0, pauseStart = 0;
|
||||||
@@ -363,28 +337,109 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
|||||||
|
|
||||||
int scrollOffset = static_cast<int>(scrollY);
|
int scrollOffset = static_cast<int>(scrollY);
|
||||||
int yOffset = -scrollOffset + getTextPositions(display)[1];
|
int yOffset = -scrollOffset + getTextPositions(display)[1];
|
||||||
for (int separatorX = 0; separatorX <= (display->getStringWidth(headerStr) + 3); separatorX += 2) {
|
for (int separatorX = 1; separatorX <= (display->getStringWidth(headerStr) + 2); separatorX += 2) {
|
||||||
display->setPixel(separatorX, yOffset + ((SCREEN_WIDTH > 128) ? 19 : 13));
|
display->setPixel(separatorX, yOffset + ((isHighResolution) ? 19 : 13));
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Render visible lines ===
|
// === Render visible lines ===
|
||||||
|
renderMessageContent(display, cachedLines, cachedHeights, x, yOffset, scrollBottom, emotes, numEmotes, isInverted, isBold);
|
||||||
|
|
||||||
|
// Draw header at the end to sort out overlapping elements
|
||||||
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth)
|
||||||
|
{
|
||||||
|
std::vector<std::string> lines;
|
||||||
|
lines.push_back(std::string(headerStr)); // Header line is always first
|
||||||
|
|
||||||
|
std::string line, word;
|
||||||
|
for (int i = 0; messageBuf[i]; ++i) {
|
||||||
|
char ch = messageBuf[i];
|
||||||
|
if ((unsigned char)messageBuf[i] == 0xE2 && (unsigned char)messageBuf[i + 1] == 0x80 &&
|
||||||
|
(unsigned char)messageBuf[i + 2] == 0x99) {
|
||||||
|
ch = '\''; // plain apostrophe
|
||||||
|
i += 2; // skip over the extra UTF-8 bytes
|
||||||
|
}
|
||||||
|
if (ch == '\n') {
|
||||||
|
if (!word.empty())
|
||||||
|
line += word;
|
||||||
|
if (!line.empty())
|
||||||
|
lines.push_back(line);
|
||||||
|
line.clear();
|
||||||
|
word.clear();
|
||||||
|
} else if (ch == ' ') {
|
||||||
|
line += word + ' ';
|
||||||
|
word.clear();
|
||||||
|
} else {
|
||||||
|
word += ch;
|
||||||
|
std::string test = line + word;
|
||||||
|
// Keep these lines for diagnostics
|
||||||
|
// LOG_INFO("Char: '%c' (0x%02X)", ch, (unsigned char)ch);
|
||||||
|
// LOG_INFO("Current String: %s", test.c_str());
|
||||||
|
if (display->getStringWidth(test.c_str()) > textWidth) {
|
||||||
|
if (!line.empty())
|
||||||
|
lines.push_back(line);
|
||||||
|
line = word;
|
||||||
|
word.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!word.empty())
|
||||||
|
line += word;
|
||||||
|
if (!line.empty())
|
||||||
|
lines.push_back(line);
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes)
|
||||||
|
{
|
||||||
|
std::vector<int> rowHeights;
|
||||||
|
|
||||||
|
for (const auto &_line : lines) {
|
||||||
|
int lineHeight = FONT_HEIGHT_SMALL;
|
||||||
|
bool hasEmote = false;
|
||||||
|
|
||||||
|
for (int i = 0; i < numEmotes; ++i) {
|
||||||
|
const Emote &e = emotes[i];
|
||||||
|
if (_line.find(e.label) != std::string::npos) {
|
||||||
|
lineHeight = std::max(lineHeight, e.height);
|
||||||
|
hasEmote = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply tighter spacing if no emotes on this line
|
||||||
|
if (!hasEmote) {
|
||||||
|
lineHeight -= 2; // reduce by 2px for tighter spacing
|
||||||
|
if (lineHeight < 8)
|
||||||
|
lineHeight = 8; // minimum safety
|
||||||
|
}
|
||||||
|
|
||||||
|
rowHeights.push_back(lineHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rowHeights;
|
||||||
|
}
|
||||||
|
|
||||||
|
void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &lines, const std::vector<int> &rowHeights, int x,
|
||||||
|
int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold)
|
||||||
|
{
|
||||||
for (size_t i = 0; i < lines.size(); ++i) {
|
for (size_t i = 0; i < lines.size(); ++i) {
|
||||||
int lineY = yOffset;
|
int lineY = yOffset;
|
||||||
for (size_t j = 0; j < i; ++j)
|
for (size_t j = 0; j < i; ++j)
|
||||||
lineY += rowHeights[j];
|
lineY += rowHeights[j];
|
||||||
if (lineY > -rowHeights[i] && lineY < scrollBottom) {
|
if (lineY > -rowHeights[i] && lineY < scrollBottom) {
|
||||||
if (i == 0 && isInverted) {
|
if (i == 0 && isInverted) {
|
||||||
display->drawString(x + 3, lineY, lines[i].c_str());
|
display->drawString(x, lineY, lines[i].c_str());
|
||||||
if (isBold)
|
if (isBold)
|
||||||
display->drawString(x + 4, lineY, lines[i].c_str());
|
display->drawString(x, lineY, lines[i].c_str());
|
||||||
} else {
|
} else {
|
||||||
drawStringWithEmotes(display, x, lineY, lines[i], emotes, numEmotes);
|
drawStringWithEmotes(display, x, lineY, lines[i], emotes, numEmotes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw header at the end to sort out overlapping elements
|
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace MessageRenderer
|
} // namespace MessageRenderer
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
#include "OLEDDisplay.h"
|
#include "OLEDDisplay.h"
|
||||||
#include "OLEDDisplayUi.h"
|
#include "OLEDDisplayUi.h"
|
||||||
#include "graphics/emotes.h"
|
#include "graphics/emotes.h"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
@@ -14,5 +16,15 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
|
|||||||
/// Draws the text message frame for displaying received messages
|
/// Draws the text message frame for displaying received messages
|
||||||
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|
||||||
|
// Function to generate lines with word wrapping
|
||||||
|
std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth);
|
||||||
|
|
||||||
|
// Function to calculate heights for each line
|
||||||
|
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes);
|
||||||
|
|
||||||
|
// Function to render the message content
|
||||||
|
void renderMessageContent(OLEDDisplay *display, const std::vector<std::string> &lines, const std::vector<int> &rowHeights, int x,
|
||||||
|
int yOffset, int scrollBottom, const Emote *emotes, int numEmotes, bool isInverted, bool isBold);
|
||||||
|
|
||||||
} // namespace MessageRenderer
|
} // namespace MessageRenderer
|
||||||
} // namespace graphics
|
} // namespace graphics
|
||||||
|
|||||||
@@ -80,7 +80,11 @@ const char *getCurrentModeTitle(int screenWidth)
|
|||||||
case MODE_LAST_HEARD:
|
case MODE_LAST_HEARD:
|
||||||
return "Last Heard";
|
return "Last Heard";
|
||||||
case MODE_HOP_SIGNAL:
|
case MODE_HOP_SIGNAL:
|
||||||
return (screenWidth > 128) ? "Hops/Signal" : "Hops/Sig";
|
#ifdef USE_EINK
|
||||||
|
return "Hops/Sig";
|
||||||
|
#else
|
||||||
|
return (isHighResolution) ? "Hops/Signal" : "Hops/Sig";
|
||||||
|
#endif
|
||||||
case MODE_DISTANCE:
|
case MODE_DISTANCE:
|
||||||
return "Distance";
|
return "Distance";
|
||||||
default:
|
default:
|
||||||
@@ -109,35 +113,6 @@ int calculateMaxScroll(int totalEntries, int visibleRows)
|
|||||||
return std::max(0, (totalEntries - 1) / (visibleRows * 2));
|
return std::max(0, (totalEntries - 1) / (visibleRows * 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
void retrieveAndSortNodes(std::vector<NodeEntry> &nodeList)
|
|
||||||
{
|
|
||||||
size_t numNodes = nodeDB->getNumMeshNodes();
|
|
||||||
for (size_t i = 0; i < numNodes; i++) {
|
|
||||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
|
||||||
if (!node || node->num == nodeDB->getNodeNum())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
NodeEntry entry;
|
|
||||||
entry.node = node;
|
|
||||||
entry.sortValue = sinceLastSeen(node);
|
|
||||||
|
|
||||||
nodeList.push_back(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort nodes: favorites first, then by last heard (most recent first)
|
|
||||||
std::sort(nodeList.begin(), nodeList.end(), [](const NodeEntry &a, const NodeEntry &b) {
|
|
||||||
bool aFav = a.node->is_favorite;
|
|
||||||
bool bFav = b.node->is_favorite;
|
|
||||||
if (aFav != bFav)
|
|
||||||
return aFav;
|
|
||||||
if (a.sortValue == 0 || a.sortValue == UINT32_MAX)
|
|
||||||
return false;
|
|
||||||
if (b.sortValue == 0 || b.sortValue == UINT32_MAX)
|
|
||||||
return true;
|
|
||||||
return a.sortValue < b.sortValue;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
|
void drawColumnSeparator(OLEDDisplay *display, int16_t x, int16_t yStart, int16_t yEnd)
|
||||||
{
|
{
|
||||||
int columnWidth = display->getWidth() / 2;
|
int columnWidth = display->getWidth() / 2;
|
||||||
@@ -170,7 +145,7 @@ void drawScrollbar(OLEDDisplay *display, int visibleNodeRows, int totalEntries,
|
|||||||
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||||
{
|
{
|
||||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||||
int timeOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
|
int timeOffset = (isHighResolution) ? (isLeftCol ? 7 : 10) : (isLeftCol ? 3 : 7);
|
||||||
|
|
||||||
const char *nodeName = getSafeNodeName(node);
|
const char *nodeName = getSafeNodeName(node);
|
||||||
|
|
||||||
@@ -191,9 +166,9 @@ 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 : 3), y, nodeName);
|
display->drawString(x + ((isHighResolution) ? 6 : 3), y, nodeName);
|
||||||
if (node->is_favorite) {
|
if (node->is_favorite) {
|
||||||
if (SCREEN_WIDTH > 128) {
|
if (isHighResolution) {
|
||||||
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);
|
||||||
@@ -212,8 +187,8 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
|||||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||||
|
|
||||||
int nameMaxWidth = columnWidth - 25;
|
int nameMaxWidth = columnWidth - 25;
|
||||||
int barsOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
|
int barsOffset = (isHighResolution) ? (isLeftCol ? 20 : 24) : (isLeftCol ? 15 : 19);
|
||||||
int hopOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
|
int hopOffset = (isHighResolution) ? (isLeftCol ? 21 : 29) : (isLeftCol ? 13 : 17);
|
||||||
|
|
||||||
int barsXOffset = columnWidth - barsOffset;
|
int barsXOffset = columnWidth - barsOffset;
|
||||||
|
|
||||||
@@ -222,9 +197,9 @@ 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 : 3), y, nameMaxWidth, nodeName);
|
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||||
if (node->is_favorite) {
|
if (node->is_favorite) {
|
||||||
if (SCREEN_WIDTH > 128) {
|
if (isHighResolution) {
|
||||||
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);
|
||||||
@@ -259,7 +234,7 @@ void drawEntryHopSignal(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int
|
|||||||
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth)
|
||||||
{
|
{
|
||||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||||
int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||||
|
|
||||||
const char *nodeName = getSafeNodeName(node);
|
const char *nodeName = getSafeNodeName(node);
|
||||||
char distStr[10] = "";
|
char distStr[10] = "";
|
||||||
@@ -314,9 +289,9 @@ 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 : 3), y, nameMaxWidth, nodeName);
|
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||||
if (node->is_favorite) {
|
if (node->is_favorite) {
|
||||||
if (SCREEN_WIDTH > 128) {
|
if (isHighResolution) {
|
||||||
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);
|
||||||
@@ -324,8 +299,8 @@ void drawNodeDistance(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (strlen(distStr) > 0) {
|
if (strlen(distStr) > 0) {
|
||||||
int offset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
|
int offset = (isHighResolution) ? (isLeftCol ? 7 : 10) // Offset for Wide Screens (Left Column:Right Column)
|
||||||
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
|
: (isLeftCol ? 4 : 7); // Offset for Narrow Screens (Left Column:Right Column)
|
||||||
int rightEdge = x + columnWidth - offset;
|
int rightEdge = x + columnWidth - offset;
|
||||||
int textWidth = display->getStringWidth(distStr);
|
int textWidth = display->getStringWidth(distStr);
|
||||||
display->drawString(rightEdge - textWidth, y, distStr);
|
display->drawString(rightEdge - textWidth, y, distStr);
|
||||||
@@ -354,15 +329,15 @@ void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||||
|
|
||||||
// Adjust max text width depending on column and screen width
|
// Adjust max text width depending on column and screen width
|
||||||
int nameMaxWidth = columnWidth - (SCREEN_WIDTH > 128 ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
int nameMaxWidth = columnWidth - (isHighResolution ? (isLeftCol ? 25 : 28) : (isLeftCol ? 20 : 22));
|
||||||
|
|
||||||
const char *nodeName = getSafeNodeName(node);
|
const char *nodeName = getSafeNodeName(node);
|
||||||
|
|
||||||
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 : 3), y, nameMaxWidth, nodeName);
|
display->drawStringMaxWidth(x + ((isHighResolution) ? 6 : 3), y, nameMaxWidth, nodeName);
|
||||||
if (node->is_favorite) {
|
if (node->is_favorite) {
|
||||||
if (SCREEN_WIDTH > 128) {
|
if (isHighResolution) {
|
||||||
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);
|
||||||
@@ -377,7 +352,7 @@ void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
bool isLeftCol = (x < SCREEN_WIDTH / 2);
|
||||||
int arrowXOffset = (SCREEN_WIDTH > 128) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
|
int arrowXOffset = (isHighResolution) ? (isLeftCol ? 22 : 24) : (isLeftCol ? 12 : 18);
|
||||||
|
|
||||||
int centerX = x + columnWidth - arrowXOffset;
|
int centerX = x + columnWidth - arrowXOffset;
|
||||||
int centerY = y + FONT_HEIGHT_SMALL / 2;
|
int centerY = y + FONT_HEIGHT_SMALL / 2;
|
||||||
@@ -436,19 +411,16 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
|||||||
// Space below header
|
// Space below header
|
||||||
y += COMMON_HEADER_HEIGHT;
|
y += COMMON_HEADER_HEIGHT;
|
||||||
|
|
||||||
// Fetch and display sorted node list
|
int totalEntries = nodeDB->getNumMeshNodes();
|
||||||
std::vector<NodeEntry> nodeList;
|
|
||||||
retrieveAndSortNodes(nodeList);
|
|
||||||
|
|
||||||
int totalEntries = nodeList.size();
|
|
||||||
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
|
int totalRowsAvailable = (display->getHeight() - y) / rowYOffset;
|
||||||
#ifdef USE_EINK
|
|
||||||
totalRowsAvailable -= 1;
|
|
||||||
#endif
|
|
||||||
int visibleNodeRows = totalRowsAvailable;
|
int visibleNodeRows = totalRowsAvailable;
|
||||||
int totalColumns = 2;
|
int totalColumns = 2;
|
||||||
|
|
||||||
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
|
int startIndex = scrollIndex * visibleNodeRows * totalColumns;
|
||||||
|
if (nodeDB->getMeshNodeByIndex(startIndex)->num == nodeDB->getNodeNum()) {
|
||||||
|
startIndex++; // skip own node
|
||||||
|
}
|
||||||
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
|
int endIndex = std::min(startIndex + visibleNodeRows * totalColumns, totalEntries);
|
||||||
|
|
||||||
int yOffset = 0;
|
int yOffset = 0;
|
||||||
@@ -460,10 +432,10 @@ void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t
|
|||||||
for (int i = startIndex; i < endIndex; ++i) {
|
for (int i = startIndex; i < endIndex; ++i) {
|
||||||
int xPos = x + (col * columnWidth);
|
int xPos = x + (col * columnWidth);
|
||||||
int yPos = y + yOffset;
|
int yPos = y + yOffset;
|
||||||
renderer(display, nodeList[i].node, xPos, yPos, columnWidth);
|
renderer(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth);
|
||||||
|
|
||||||
if (extras) {
|
if (extras) {
|
||||||
extras(display, nodeList[i].node, xPos, yPos, columnWidth, heading, lat, lon);
|
extras(display, nodeDB->getMeshNodeByIndex(i), xPos, yPos, columnWidth, heading, lat, lon);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
lastNodeY = std::max(lastNodeY, yPos + FONT_HEIGHT_SMALL);
|
||||||
@@ -533,7 +505,12 @@ void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_
|
|||||||
|
|
||||||
void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
void drawHopSignalScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||||
{
|
{
|
||||||
|
#ifdef USE_EINK
|
||||||
|
const char *title = "Hops/Sig";
|
||||||
|
#else
|
||||||
|
|
||||||
const char *title = "Hops/Signal";
|
const char *title = "Hops/Signal";
|
||||||
|
#endif
|
||||||
drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal);
|
drawNodeListScreen(display, state, x, y, title, drawEntryHopSignal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,6 @@ namespace NodeListRenderer
|
|||||||
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int);
|
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int);
|
||||||
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double);
|
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double);
|
||||||
|
|
||||||
// Node entry structure
|
|
||||||
struct NodeEntry {
|
|
||||||
meshtastic_NodeInfoLite *node;
|
|
||||||
uint32_t sortValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Node list mode enumeration
|
// Node list mode enumeration
|
||||||
enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 };
|
enum NodeListMode { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_DISTANCE = 2, MODE_COUNT = 3 };
|
||||||
|
|
||||||
@@ -57,7 +51,6 @@ void drawNodeListWithCompasses(OLEDDisplay *display, OLEDDisplayUiState *state,
|
|||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
const char *getCurrentModeTitle(int screenWidth);
|
const char *getCurrentModeTitle(int screenWidth);
|
||||||
void retrieveAndSortNodes(std::vector<NodeEntry> &nodeList);
|
|
||||||
const char *getSafeNodeName(meshtastic_NodeInfoLite *node);
|
const char *getSafeNodeName(meshtastic_NodeInfoLite *node);
|
||||||
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
|
void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields);
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ int8_t NotificationRenderer::curSelected = 0;
|
|||||||
char NotificationRenderer::alertBannerMessage[256] = {0};
|
char NotificationRenderer::alertBannerMessage[256] = {0};
|
||||||
uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever
|
uint32_t NotificationRenderer::alertBannerUntil = 0; // 0 is a special case meaning forever
|
||||||
uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options
|
uint8_t NotificationRenderer::alertBannerOptions = 0; // last x lines are seelctable options
|
||||||
|
const char **NotificationRenderer::optionsArrayPtr = nullptr;
|
||||||
std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
|
std::function<void(int)> NotificationRenderer::alertBannerCallback = NULL;
|
||||||
bool NotificationRenderer::pauseBanner = false;
|
bool NotificationRenderer::pauseBanner = false;
|
||||||
|
|
||||||
@@ -56,60 +57,51 @@ void NotificationRenderer::drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiStat
|
|||||||
|
|
||||||
void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
|
void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
|
||||||
{
|
{
|
||||||
// Exit if no message is active or duration has passed
|
if (!isOverlayBannerShowing() || pauseBanner)
|
||||||
if (!isOverlayBannerShowing())
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (pauseBanner)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// === Layout Configuration ===
|
// === Layout Configuration ===
|
||||||
constexpr uint16_t padding = 5; // Padding around text inside the box
|
constexpr uint16_t vPadding = 2;
|
||||||
constexpr uint16_t vPadding = 2; // Padding around text inside the box
|
|
||||||
constexpr uint8_t lineSpacing = 1; // Extra space between lines
|
|
||||||
|
|
||||||
// Search the message to determine if we need the bell added
|
|
||||||
bool needs_bell = (strstr(alertBannerMessage, "Alert Received") != nullptr);
|
|
||||||
|
|
||||||
uint8_t firstOption = 0;
|
|
||||||
uint8_t firstOptionToShow = 0;
|
|
||||||
|
|
||||||
// Setup font and alignment
|
// Setup font and alignment
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT); // We will manually center per line
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
const int MAX_LINES = 24;
|
|
||||||
|
|
||||||
|
uint16_t optionWidths[alertBannerOptions] = {0};
|
||||||
uint16_t maxWidth = 0;
|
uint16_t maxWidth = 0;
|
||||||
uint16_t arrowsWidth = display->getStringWidth("> <", 4, true);
|
uint16_t arrowsWidth = display->getStringWidth("> <", 4, true);
|
||||||
uint16_t lineWidths[MAX_LINES] = {0};
|
uint16_t lineWidths[MAX_LINES] = {0};
|
||||||
uint16_t lineLengths[MAX_LINES] = {0};
|
uint16_t lineLengths[MAX_LINES] = {0};
|
||||||
char *lineStarts[MAX_LINES + 1];
|
const char *lineStarts[MAX_LINES + 1] = {0};
|
||||||
uint16_t lineCount = 0;
|
uint16_t lineCount = 0;
|
||||||
char lineBuffer[40] = {0};
|
char lineBuffer[40] = {0};
|
||||||
// pointer to the terminating null
|
|
||||||
|
// Parse lines
|
||||||
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
char *alertEnd = alertBannerMessage + strnlen(alertBannerMessage, sizeof(alertBannerMessage));
|
||||||
lineStarts[lineCount] = alertBannerMessage;
|
lineStarts[lineCount] = alertBannerMessage;
|
||||||
|
|
||||||
// loop through lines finding \n characters
|
|
||||||
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
while ((lineCount < MAX_LINES) && (lineStarts[lineCount] < alertEnd)) {
|
||||||
lineStarts[lineCount + 1] = std::find(lineStarts[lineCount], alertEnd, '\n');
|
lineStarts[lineCount + 1] = std::find((char *)lineStarts[lineCount], alertEnd, '\n');
|
||||||
lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount];
|
lineLengths[lineCount] = lineStarts[lineCount + 1] - lineStarts[lineCount];
|
||||||
if (lineStarts[lineCount + 1][0] == '\n') {
|
if (lineStarts[lineCount + 1][0] == '\n')
|
||||||
lineStarts[lineCount + 1] += 1; // Move the start pointer beyond the \n
|
lineStarts[lineCount + 1] += 1;
|
||||||
}
|
|
||||||
lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true);
|
lineWidths[lineCount] = display->getStringWidth(lineStarts[lineCount], lineLengths[lineCount], true);
|
||||||
if (lineWidths[lineCount] > maxWidth) {
|
if (lineWidths[lineCount] > maxWidth)
|
||||||
maxWidth = lineWidths[lineCount];
|
maxWidth = lineWidths[lineCount];
|
||||||
}
|
|
||||||
if (alertBannerOptions > 0 && lineCount > 0 && lineWidths[lineCount] + arrowsWidth > maxWidth) {
|
|
||||||
maxWidth = lineWidths[lineCount] + arrowsWidth;
|
|
||||||
}
|
|
||||||
lineCount++;
|
lineCount++;
|
||||||
// if we are doing a selection, add extra width for arrows
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Measure option widths
|
||||||
|
for (int i = 0; i < alertBannerOptions; i++) {
|
||||||
|
optionWidths[i] = display->getStringWidth(optionsArrayPtr[i], strlen(optionsArrayPtr[i]), true);
|
||||||
|
if (optionWidths[i] > maxWidth)
|
||||||
|
maxWidth = optionWidths[i];
|
||||||
|
if (optionWidths[i] + arrowsWidth > maxWidth)
|
||||||
|
maxWidth = optionWidths[i] + arrowsWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle input
|
||||||
if (alertBannerOptions > 0) {
|
if (alertBannerOptions > 0) {
|
||||||
// respond to input
|
|
||||||
if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) {
|
if (inEvent == INPUT_BROKER_UP || inEvent == INPUT_BROKER_ALT_PRESS) {
|
||||||
curSelected--;
|
curSelected--;
|
||||||
} else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) {
|
} else if (inEvent == INPUT_BROKER_DOWN || inEvent == INPUT_BROKER_USER_PRESS) {
|
||||||
@@ -120,113 +112,176 @@ void NotificationRenderer::drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisp
|
|||||||
} else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
|
} else if ((inEvent == INPUT_BROKER_CANCEL || inEvent == INPUT_BROKER_ALT_LONG) && alertBannerUntil != 0) {
|
||||||
alertBannerMessage[0] = '\0';
|
alertBannerMessage[0] = '\0';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (curSelected == -1)
|
if (curSelected == -1)
|
||||||
curSelected = alertBannerOptions - 1;
|
curSelected = alertBannerOptions - 1;
|
||||||
if (curSelected == alertBannerOptions)
|
if (curSelected == alertBannerOptions)
|
||||||
curSelected = 0;
|
curSelected = 0;
|
||||||
// compare number of options to number of lines
|
} else {
|
||||||
if (lineCount < alertBannerOptions)
|
|
||||||
return;
|
|
||||||
firstOption = lineCount - alertBannerOptions;
|
|
||||||
if (curSelected > 1 && alertBannerOptions > 3) {
|
|
||||||
firstOptionToShow = curSelected + firstOption - 1;
|
|
||||||
// put the selected option in the middle
|
|
||||||
} else {
|
|
||||||
firstOptionToShow = firstOption;
|
|
||||||
}
|
|
||||||
} else { // not in an alert with a callback
|
|
||||||
// TODO: check that at least a second has passed since the alert started
|
|
||||||
if (inEvent == INPUT_BROKER_SELECT || inEvent == INPUT_BROKER_ALT_LONG || inEvent == INPUT_BROKER_CANCEL) {
|
if (inEvent == INPUT_BROKER_SELECT || inEvent == INPUT_BROKER_ALT_LONG || inEvent == INPUT_BROKER_CANCEL) {
|
||||||
alertBannerMessage[0] = '\0'; // end the alert early
|
alertBannerMessage[0] = '\0';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inEvent = INPUT_BROKER_NONE;
|
inEvent = INPUT_BROKER_NONE;
|
||||||
if (alertBannerMessage[0] == '\0')
|
if (alertBannerMessage[0] == '\0')
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// set width from longest line
|
uint16_t totalLines = lineCount + alertBannerOptions;
|
||||||
uint16_t boxWidth = padding * 2 + maxWidth;
|
|
||||||
if (needs_bell) {
|
uint16_t screenHeight = display->height();
|
||||||
if (SCREEN_WIDTH > 128 && boxWidth <= 150) {
|
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||||
boxWidth += 26;
|
uint8_t visibleTotalLines = std::min<uint8_t>(totalLines, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||||
}
|
uint8_t linesShown = lineCount;
|
||||||
if (SCREEN_WIDTH <= 128 && boxWidth <= 100) {
|
const char *linePointers[visibleTotalLines]; // this is sort of a dynamic allocation
|
||||||
boxWidth += 20;
|
|
||||||
|
// copy the linestarts to display to the linePointers holder
|
||||||
|
for (int i = 0; i < lineCount; i++) {
|
||||||
|
linePointers[i] = lineStarts[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t firstOptionToShow = 0;
|
||||||
|
if (alertBannerOptions > 0) {
|
||||||
|
if (curSelected > 1 && alertBannerOptions > visibleTotalLines - lineCount) {
|
||||||
|
if (curSelected > alertBannerOptions - visibleTotalLines + lineCount)
|
||||||
|
firstOptionToShow = alertBannerOptions - visibleTotalLines + lineCount;
|
||||||
|
else
|
||||||
|
firstOptionToShow = curSelected - 1;
|
||||||
|
} else {
|
||||||
|
firstOptionToShow = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// calculate max lines on screen? for now it's 4
|
|
||||||
// set height from line count
|
for (int i = firstOptionToShow; i < alertBannerOptions && linesShown < visibleTotalLines; i++, linesShown++) {
|
||||||
uint16_t boxHeight;
|
if (i == curSelected) {
|
||||||
if (lineCount <= 4) {
|
strncpy(lineBuffer, "> ", 3);
|
||||||
boxHeight = vPadding * 2 + lineCount * FONT_HEIGHT_SMALL + (lineCount - 1) * lineSpacing;
|
strncpy(lineBuffer + 2, optionsArrayPtr[i], 36);
|
||||||
|
strncpy(lineBuffer + strlen(optionsArrayPtr[i]) + 2, " <", 3);
|
||||||
|
lineBuffer[39] = '\0';
|
||||||
|
linePointers[linesShown] = lineBuffer;
|
||||||
|
} else {
|
||||||
|
linePointers[linesShown] = optionsArrayPtr[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (alertBannerOptions > 0) {
|
||||||
|
drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow, maxWidth);
|
||||||
} else {
|
} else {
|
||||||
boxHeight = vPadding * 2 + 4 * FONT_HEIGHT_SMALL + 4 * lineSpacing;
|
drawNotificationBox(display, state, linePointers, totalLines, firstOptionToShow);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NotificationRenderer::drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[],
|
||||||
|
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth)
|
||||||
|
{
|
||||||
|
bool is_picker = false;
|
||||||
|
uint16_t lineCount = 0;
|
||||||
|
// === Layout Configuration ===
|
||||||
|
constexpr uint16_t hPadding = 5;
|
||||||
|
constexpr uint16_t vPadding = 2;
|
||||||
|
bool needs_bell = false;
|
||||||
|
uint16_t lineWidths[totalLines] = {0};
|
||||||
|
|
||||||
|
if (maxWidth != 0)
|
||||||
|
is_picker = true;
|
||||||
|
// seelction box
|
||||||
|
|
||||||
|
while (lineCount < totalLines) {
|
||||||
|
if (lines[lineCount] != nullptr) {
|
||||||
|
lineWidths[lineCount] = display->getStringWidth(lines[lineCount], strlen(lines[lineCount]), true);
|
||||||
|
if (!is_picker) {
|
||||||
|
needs_bell |= (strstr(alertBannerMessage, "Alert Received") != nullptr);
|
||||||
|
if (lineWidths[lineCount] > maxWidth)
|
||||||
|
maxWidth = lineWidths[lineCount];
|
||||||
|
}
|
||||||
|
lineCount++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// count lines
|
||||||
|
|
||||||
|
uint16_t boxWidth = hPadding * 2 + maxWidth;
|
||||||
|
if (needs_bell) {
|
||||||
|
if (isHighResolution && boxWidth <= 150)
|
||||||
|
boxWidth += 26;
|
||||||
|
if (!isHighResolution && boxWidth <= 100)
|
||||||
|
boxWidth += 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t screenHeight = display->height();
|
||||||
|
uint8_t effectiveLineHeight = FONT_HEIGHT_SMALL - 3;
|
||||||
|
uint8_t visibleTotalLines = std::min<uint8_t>(lineCount, (screenHeight - vPadding * 2) / effectiveLineHeight);
|
||||||
|
uint16_t contentHeight = visibleTotalLines * effectiveLineHeight;
|
||||||
|
uint16_t boxHeight = contentHeight + vPadding * 2;
|
||||||
|
|
||||||
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
|
int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
|
||||||
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
int16_t boxTop = (display->height() / 2) - (boxHeight / 2);
|
||||||
// === Draw background box ===
|
|
||||||
|
// === Draw Box ===
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Slightly oversized box
|
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2);
|
||||||
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1); // Top Line
|
display->fillRect(boxLeft, boxTop - 2, boxWidth, 1);
|
||||||
display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1); // Bottom Line
|
display->fillRect(boxLeft, boxTop + boxHeight + 1, boxWidth, 1);
|
||||||
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight); // Left Line
|
display->fillRect(boxLeft - 2, boxTop, 1, boxHeight);
|
||||||
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight); // Right Line
|
display->fillRect(boxLeft + boxWidth + 1, boxTop, 1, boxHeight);
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight); // Border
|
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
|
||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
display->fillRect(boxLeft, boxTop, 1, 1); // Top Left
|
display->fillRect(boxLeft, boxTop, 1, 1);
|
||||||
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1); // Top Right
|
display->fillRect(boxLeft + boxWidth - 1, boxTop, 1, 1);
|
||||||
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1); // Bottom Left
|
display->fillRect(boxLeft, boxTop + boxHeight - 1, 1, 1);
|
||||||
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1); // Bottom Right
|
display->fillRect(boxLeft + boxWidth - 1, boxTop + boxHeight - 1, 1, 1);
|
||||||
display->setColor(WHITE);
|
display->setColor(WHITE);
|
||||||
|
|
||||||
// === Draw each line centered in the box ===
|
// === Draw Content ===
|
||||||
int16_t lineY = boxTop + vPadding;
|
int16_t lineY = boxTop + vPadding;
|
||||||
|
|
||||||
for (int i = 0; i < lineCount; i++) {
|
for (int i = 0; i < lineCount; i++) {
|
||||||
// is this line selected?
|
|
||||||
// if so, start the buffer with -> and strncpy to the 4th location
|
|
||||||
if (i < lineCount - alertBannerOptions || alertBannerOptions == 0) {
|
|
||||||
strncpy(lineBuffer, lineStarts[i], 40);
|
|
||||||
if (lineLengths[i] > 39)
|
|
||||||
lineBuffer[39] = '\0';
|
|
||||||
else
|
|
||||||
lineBuffer[lineLengths[i]] = '\0';
|
|
||||||
} else if (i >= firstOptionToShow && i < firstOptionToShow + 3) {
|
|
||||||
if (i == curSelected + firstOption) {
|
|
||||||
if (lineLengths[i] > 35)
|
|
||||||
lineLengths[i] = 35;
|
|
||||||
strncpy(lineBuffer, "> ", 3);
|
|
||||||
strncpy(lineBuffer + 2, lineStarts[i], 36);
|
|
||||||
strncpy(lineBuffer + lineLengths[i] + 2, " <", 3);
|
|
||||||
lineLengths[i] += 4;
|
|
||||||
lineWidths[i] += display->getStringWidth("> <", 4, true);
|
|
||||||
if (lineLengths[i] > 35)
|
|
||||||
lineBuffer[39] = '\0';
|
|
||||||
else
|
|
||||||
lineBuffer[lineLengths[i]] = '\0';
|
|
||||||
} else {
|
|
||||||
strncpy(lineBuffer, lineStarts[i], 40);
|
|
||||||
if (lineLengths[i] > 39)
|
|
||||||
lineBuffer[39] = '\0';
|
|
||||||
else
|
|
||||||
lineBuffer[lineLengths[i]] = '\0';
|
|
||||||
}
|
|
||||||
} else { // add break for the additional lines
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
|
int16_t textX = boxLeft + (boxWidth - lineWidths[i]) / 2;
|
||||||
|
|
||||||
if (needs_bell && i == 0) {
|
if (needs_bell && i == 0) {
|
||||||
int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2;
|
int bellY = lineY + (FONT_HEIGHT_SMALL - 8) / 2;
|
||||||
display->drawXbm(textX - 10, bellY, 8, 8, bell_alert);
|
display->drawXbm(textX - 10, bellY, 8, 8, bell_alert);
|
||||||
display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert);
|
display->drawXbm(textX + lineWidths[i] + 2, bellY, 8, 8, bell_alert);
|
||||||
}
|
}
|
||||||
|
|
||||||
display->drawString(textX, lineY, lineBuffer);
|
// Determine if this is a pop-up or a pick list
|
||||||
lineY += FONT_HEIGHT_SMALL + lineSpacing;
|
if (alertBannerOptions > 0 && i == 0) {
|
||||||
|
// Pick List
|
||||||
|
display->setColor(WHITE);
|
||||||
|
int background_yOffset = 1;
|
||||||
|
// Determine if we have low hanging characters
|
||||||
|
if (strchr(lines[i], 'p') || strchr(lines[i], 'g') || strchr(lines[i], 'y') || strchr(lines[i], 'j')) {
|
||||||
|
background_yOffset = -1;
|
||||||
|
}
|
||||||
|
display->fillRect(boxLeft, boxTop + 1, boxWidth, effectiveLineHeight - background_yOffset);
|
||||||
|
display->setColor(BLACK);
|
||||||
|
int yOffset = 3;
|
||||||
|
display->drawString(textX, lineY - yOffset, lines[i]);
|
||||||
|
display->setColor(WHITE);
|
||||||
|
lineY += (effectiveLineHeight - 2 - background_yOffset);
|
||||||
|
} else {
|
||||||
|
// Pop-up
|
||||||
|
LOG_WARN("x%u y%u %s", textX, lineY, lines[i]);
|
||||||
|
display->drawString(textX, lineY, lines[i]);
|
||||||
|
lineY += (effectiveLineHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Scroll Bar (Thicker, inside box, not over title) ===
|
||||||
|
if (totalLines > visibleTotalLines) {
|
||||||
|
const uint8_t scrollBarWidth = 5;
|
||||||
|
const uint8_t scrollPadding = 2;
|
||||||
|
|
||||||
|
int16_t scrollBarX = boxLeft + boxWidth - scrollBarWidth - 2;
|
||||||
|
int16_t scrollBarY = boxTop + vPadding + effectiveLineHeight; // start after title line
|
||||||
|
uint16_t scrollBarHeight = boxHeight - vPadding * 2 - effectiveLineHeight;
|
||||||
|
|
||||||
|
float ratio = (float)visibleTotalLines / totalLines;
|
||||||
|
uint16_t indicatorHeight = std::max((int)(scrollBarHeight * ratio), 4);
|
||||||
|
float scrollRatio = (float)(firstOptionToShow + lineCount - visibleTotalLines) / (totalLines - visibleTotalLines);
|
||||||
|
uint16_t indicatorY = scrollBarY + scrollRatio * (scrollBarHeight - indicatorHeight);
|
||||||
|
|
||||||
|
display->drawRect(scrollBarX, scrollBarY, scrollBarWidth, scrollBarHeight);
|
||||||
|
display->fillRect(scrollBarX + 1, indicatorY, scrollBarWidth - 2, indicatorHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "OLEDDisplay.h"
|
#include "OLEDDisplay.h"
|
||||||
#include "OLEDDisplayUi.h"
|
#include "OLEDDisplayUi.h"
|
||||||
|
#define MAX_LINES 5
|
||||||
|
|
||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
@@ -12,13 +13,17 @@ class NotificationRenderer
|
|||||||
static char inEvent;
|
static char inEvent;
|
||||||
static int8_t curSelected;
|
static int8_t curSelected;
|
||||||
static char alertBannerMessage[256];
|
static char alertBannerMessage[256];
|
||||||
static uint32_t alertBannerUntil; // 0 is a special case meaning forever
|
static uint32_t alertBannerUntil; // 0 is a special case meaning forever
|
||||||
|
static const char **optionsArrayPtr;
|
||||||
static uint8_t alertBannerOptions; // last x lines are seelctable options
|
static uint8_t alertBannerOptions; // last x lines are seelctable options
|
||||||
static std::function<void(int)> alertBannerCallback;
|
static std::function<void(int)> alertBannerCallback;
|
||||||
|
|
||||||
static bool pauseBanner;
|
static bool pauseBanner;
|
||||||
|
|
||||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
|
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||||
|
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);
|
||||||
|
|
||||||
static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||||
|
|||||||
@@ -18,6 +18,32 @@
|
|||||||
#include <RTC.h>
|
#include <RTC.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
|
||||||
|
bool isAllowedPunctuation(char c)
|
||||||
|
{
|
||||||
|
const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ ";
|
||||||
|
return allowed.find(c) != std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string sanitizeString(const std::string &input)
|
||||||
|
{
|
||||||
|
std::string output;
|
||||||
|
bool inReplacement = false;
|
||||||
|
|
||||||
|
for (char c : input) {
|
||||||
|
if (std::isalnum(static_cast<unsigned char>(c)) || isAllowedPunctuation(c)) {
|
||||||
|
output += c;
|
||||||
|
inReplacement = false;
|
||||||
|
} else {
|
||||||
|
if (!inReplacement) {
|
||||||
|
output += 0xbf; // ISO-8859-1 for inverted question mark
|
||||||
|
inReplacement = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
#if !MESHTASTIC_EXCLUDE_GPS
|
#if !MESHTASTIC_EXCLUDE_GPS
|
||||||
|
|
||||||
// External variables
|
// External variables
|
||||||
@@ -38,7 +64,7 @@ NodeNum UIRenderer::currentFavoriteNodeNum = 0;
|
|||||||
void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps)
|
void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gps)
|
||||||
{
|
{
|
||||||
// Draw satellite image
|
// Draw satellite image
|
||||||
if (SCREEN_WIDTH > 128) {
|
if (isHighResolution) {
|
||||||
NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display);
|
NodeListRenderer::drawScaledXBitmap16x16(x, y - 2, imgSatellite_width, imgSatellite_height, imgSatellite, display);
|
||||||
} else {
|
} else {
|
||||||
display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite);
|
display->drawXbm(x + 1, y + 1, imgSatellite_width, imgSatellite_height, imgSatellite);
|
||||||
@@ -58,7 +84,7 @@ void UIRenderer::drawGps(OLEDDisplay *display, int16_t x, int16_t y, const mesht
|
|||||||
} else {
|
} else {
|
||||||
snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites());
|
snprintf(textString, sizeof(textString), "%u sats", gps->getNumSatellites());
|
||||||
}
|
}
|
||||||
if (SCREEN_WIDTH > 128) {
|
if (isHighResolution) {
|
||||||
display->drawString(x + 18, y, textString);
|
display->drawString(x + 18, y, textString);
|
||||||
} else {
|
} else {
|
||||||
display->drawString(x + 11, y, textString);
|
display->drawString(x + 11, y, textString);
|
||||||
@@ -163,46 +189,6 @@ void UIRenderer::drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void UIRenderer::drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer,
|
|
||||||
const meshtastic::PowerStatus *powerStatus)
|
|
||||||
{
|
|
||||||
static const uint8_t powerBar[3] = {0x81, 0xBD, 0xBD};
|
|
||||||
static const uint8_t lightning[8] = {0xA1, 0xA1, 0xA5, 0xAD, 0xB5, 0xA5, 0x85, 0x85};
|
|
||||||
|
|
||||||
// Clear the bar area inside the battery image
|
|
||||||
for (int i = 1; i < 14; i++) {
|
|
||||||
imgBuffer[i] = 0x81;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill with lightning or power bars
|
|
||||||
if (powerStatus->getIsCharging()) {
|
|
||||||
memcpy(imgBuffer + 3, lightning, 8);
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < 4; i++) {
|
|
||||||
if (powerStatus->getBatteryChargePercent() >= 25 * i)
|
|
||||||
memcpy(imgBuffer + 1 + (i * 3), powerBar, 3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slightly more conservative scaling based on screen width
|
|
||||||
int scale = 1;
|
|
||||||
|
|
||||||
if (SCREEN_WIDTH >= 200)
|
|
||||||
scale = 2;
|
|
||||||
if (SCREEN_WIDTH >= 300)
|
|
||||||
scale = 2; // Do NOT go higher than 2
|
|
||||||
|
|
||||||
// Draw scaled battery image (16 columns × 8 rows)
|
|
||||||
for (int col = 0; col < 16; col++) {
|
|
||||||
uint8_t colBits = imgBuffer[col];
|
|
||||||
for (int row = 0; row < 8; row++) {
|
|
||||||
if (colBits & (1 << row)) {
|
|
||||||
display->fillRect(x + col * scale, y + row * scale, scale, scale);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw nodes status
|
// Draw nodes status
|
||||||
void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset,
|
void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset,
|
||||||
bool show_total, String additional_words)
|
bool show_total, String additional_words)
|
||||||
@@ -221,19 +207,19 @@ void UIRenderer::drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const mes
|
|||||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
|
defined(ST7789_CS) || defined(USE_ST7789) || defined(ILI9488_CS) || defined(HX8357_CS)) && \
|
||||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||||
|
|
||||||
if (SCREEN_WIDTH > 128) {
|
if (isHighResolution) {
|
||||||
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
|
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
|
||||||
} else {
|
} else {
|
||||||
display->drawFastImage(x, y + 3, 8, 8, imgUser);
|
display->drawFastImage(x, y + 3, 8, 8, imgUser);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
if (SCREEN_WIDTH > 128) {
|
if (isHighResolution) {
|
||||||
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
|
NodeListRenderer::drawScaledXBitmap16x16(x, y - 1, 8, 8, imgUser, display);
|
||||||
} else {
|
} else {
|
||||||
display->drawFastImage(x, y + 1, 8, 8, imgUser);
|
display->drawFastImage(x, y + 1, 8, 8, imgUser);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
int string_offset = (SCREEN_WIDTH > 128) ? 9 : 0;
|
int string_offset = (isHighResolution) ? 9 : 0;
|
||||||
display->drawString(x + 10 + string_offset, y - 2, usersString);
|
display->drawString(x + 10 + string_offset, y - 2, usersString);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,12 +279,14 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
|||||||
|
|
||||||
// List of available macro Y positions in order, from top to bottom.
|
// List of available macro Y positions in order, from top to bottom.
|
||||||
int line = 1; // which slot to use next
|
int line = 1; // which slot to use next
|
||||||
|
std::string usernameStr;
|
||||||
|
|
||||||
// === 1. Long Name (always try to show first) ===
|
// === 1. Long Name (always try to show first) ===
|
||||||
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
|
const char *username = (node->has_user && node->user.long_name[0]) ? node->user.long_name : nullptr;
|
||||||
if (username && line < 5) {
|
if (username) {
|
||||||
|
usernameStr = sanitizeString(username); // Sanitize the incoming long_name just in case
|
||||||
// Print node's long name (e.g. "Backpack Node")
|
// Print node's long name (e.g. "Backpack Node")
|
||||||
display->drawString(x, getTextPositions(display)[line++], username);
|
display->drawString(x, getTextPositions(display)[line++], usernameStr.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 2. Signal and Hops (combined on one line, if available) ===
|
// === 2. Signal and Hops (combined on one line, if available) ===
|
||||||
@@ -476,7 +464,7 @@ void UIRenderer::drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *st
|
|||||||
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 = (isHighResolution) ? 16 : 8;
|
||||||
const int navBarHeight = iconSize + 6;
|
const int navBarHeight = iconSize + 6;
|
||||||
#else
|
#else
|
||||||
const int navBarHeight = 0;
|
const int navBarHeight = 0;
|
||||||
@@ -570,15 +558,15 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
|||||||
} 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";
|
||||||
}
|
}
|
||||||
int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1;
|
int yOffset = (isHighResolution) ? 3 : 1;
|
||||||
if (SCREEN_WIDTH > 128) {
|
if (isHighResolution) {
|
||||||
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
|
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
|
||||||
imgSatellite_height, imgSatellite, display);
|
imgSatellite_height, imgSatellite, display);
|
||||||
} else {
|
} else {
|
||||||
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
|
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
|
||||||
imgSatellite);
|
imgSatellite);
|
||||||
}
|
}
|
||||||
int xOffset = (SCREEN_WIDTH > 128) ? 6 : 0;
|
int xOffset = (isHighResolution) ? 6 : 0;
|
||||||
display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine);
|
display->drawString(x + 11 + xOffset, getTextPositions(display)[line], displayLine);
|
||||||
} else {
|
} else {
|
||||||
UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus);
|
UIRenderer::drawGps(display, 0, getTextPositions(display)[line], gpsStatus);
|
||||||
@@ -602,17 +590,17 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
|||||||
char chUtilPercentage[10];
|
char chUtilPercentage[10];
|
||||||
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
|
snprintf(chUtilPercentage, sizeof(chUtilPercentage), "%2.0f%%", airTime->channelUtilizationPercent());
|
||||||
|
|
||||||
int chUtil_x = (SCREEN_WIDTH > 128) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
|
int chUtil_x = (isHighResolution) ? display->getStringWidth(chUtil) + 10 : display->getStringWidth(chUtil) + 5;
|
||||||
int chUtil_y = getTextPositions(display)[line] + 3;
|
int chUtil_y = getTextPositions(display)[line] + 3;
|
||||||
|
|
||||||
int chutil_bar_width = (SCREEN_WIDTH > 128) ? 100 : 50;
|
int chutil_bar_width = (isHighResolution) ? 100 : 50;
|
||||||
if (!config.bluetooth.enabled) {
|
if (!config.bluetooth.enabled) {
|
||||||
chutil_bar_width = (SCREEN_WIDTH > 128) ? 80 : 40;
|
chutil_bar_width = (isHighResolution) ? 80 : 40;
|
||||||
}
|
}
|
||||||
int chutil_bar_height = (SCREEN_WIDTH > 128) ? 12 : 7;
|
int chutil_bar_height = (isHighResolution) ? 12 : 7;
|
||||||
int extraoffset = (SCREEN_WIDTH > 128) ? 6 : 3;
|
int extraoffset = (isHighResolution) ? 6 : 3;
|
||||||
if (!config.bluetooth.enabled) {
|
if (!config.bluetooth.enabled) {
|
||||||
extraoffset = (SCREEN_WIDTH > 128) ? 6 : 1;
|
extraoffset = (isHighResolution) ? 6 : 1;
|
||||||
}
|
}
|
||||||
int chutil_percent = airTime->channelUtilizationPercent();
|
int chutil_percent = airTime->channelUtilizationPercent();
|
||||||
|
|
||||||
@@ -672,21 +660,20 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
|||||||
// === Fourth & Fifth Rows: Node Identity ===
|
// === Fourth & Fifth Rows: Node Identity ===
|
||||||
int textWidth = 0;
|
int textWidth = 0;
|
||||||
int nameX = 0;
|
int nameX = 0;
|
||||||
int yOffset = (SCREEN_WIDTH > 128) ? 0 : 5;
|
int yOffset = (isHighResolution) ? 0 : 5;
|
||||||
const char *longName = nullptr;
|
const char *longName = nullptr;
|
||||||
|
std::string longNameStr;
|
||||||
|
|
||||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||||
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) {
|
if (ourNode && ourNode->has_user && strlen(ourNode->user.long_name) > 0) {
|
||||||
longName = ourNode->user.long_name;
|
longNameStr = sanitizeString(ourNode->user.long_name);
|
||||||
}
|
}
|
||||||
uint8_t dmac[6];
|
|
||||||
char shortnameble[35];
|
char shortnameble[35];
|
||||||
getMacAddr(dmac);
|
|
||||||
snprintf(screen->ourId, sizeof(screen->ourId), "%02x%02x", dmac[4], dmac[5]);
|
|
||||||
snprintf(shortnameble, sizeof(shortnameble), "%s",
|
snprintf(shortnameble, sizeof(shortnameble), "%s",
|
||||||
graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
|
graphics::UIRenderer::haveGlyphs(owner.short_name) ? owner.short_name : "");
|
||||||
|
|
||||||
char combinedName[50];
|
char combinedName[50];
|
||||||
snprintf(combinedName, sizeof(combinedName), "%s (%s)", longName, shortnameble);
|
snprintf(combinedName, sizeof(combinedName), "%s (%s)", longNameStr.empty() ? "" : longNameStr.c_str(), 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) {
|
||||||
@@ -700,7 +687,7 @@ void UIRenderer::drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *sta
|
|||||||
// === LongName Centered ===
|
// === LongName Centered ===
|
||||||
textWidth = display->getStringWidth(longName);
|
textWidth = display->getStringWidth(longName);
|
||||||
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
nameX = (SCREEN_WIDTH - textWidth) / 2;
|
||||||
display->drawString(nameX, getTextPositions(display)[line++], longName);
|
display->drawString(nameX, getTextPositions(display)[line++], longNameStr.c_str());
|
||||||
|
|
||||||
// === ShortName Centered ===
|
// === ShortName Centered ===
|
||||||
textWidth = display->getStringWidth(shortnameble);
|
textWidth = display->getStringWidth(shortnameble);
|
||||||
@@ -808,44 +795,42 @@ void UIRenderer::drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState
|
|||||||
{
|
{
|
||||||
LOG_DEBUG("Draw screensaver overlay");
|
LOG_DEBUG("Draw screensaver overlay");
|
||||||
|
|
||||||
EINK_ADD_FRAMEFLAG(display, COSMETIC); // Take the opportunity for a full-refresh
|
EINK_ADD_FRAMEFLAG(display, COSMETIC); // Full refresh for screensaver
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
display->setFont(FONT_SMALL);
|
display->setFont(FONT_SMALL);
|
||||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||||
const char *pauseText = "Screen Paused";
|
const char *pauseText = "Screen Paused";
|
||||||
const char *idText = owner.short_name;
|
const char *idText = owner.short_name;
|
||||||
const bool useId = haveGlyphs(idText); // This bool is used to hide the idText box if we can't render the short name
|
const bool useId = haveGlyphs(idText);
|
||||||
constexpr uint16_t padding = 5;
|
constexpr uint8_t padding = 2;
|
||||||
constexpr uint8_t dividerGap = 1;
|
constexpr uint8_t dividerGap = 1;
|
||||||
constexpr uint8_t imprecision = 5; // How far the box origins can drift from center. Combat burn-in.
|
|
||||||
|
|
||||||
// Dimensions
|
// Text widths
|
||||||
const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true); // "true": handle utf8 chars
|
const uint16_t idTextWidth = display->getStringWidth(idText, strlen(idText), true);
|
||||||
const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText));
|
const uint16_t pauseTextWidth = display->getStringWidth(pauseText, strlen(pauseText));
|
||||||
const uint16_t boxWidth = padding + (useId ? idTextWidth + padding + padding : 0) + pauseTextWidth + padding;
|
const uint16_t boxWidth = padding + (useId ? idTextWidth + padding : 0) + pauseTextWidth + padding;
|
||||||
const uint16_t boxHeight = padding + FONT_HEIGHT_SMALL + padding;
|
const uint16_t boxHeight = FONT_HEIGHT_SMALL + (padding * 2);
|
||||||
|
|
||||||
// Position
|
// Flush with bottom
|
||||||
const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2) + random(-imprecision, imprecision + 1);
|
const int16_t boxLeft = (display->width() / 2) - (boxWidth / 2);
|
||||||
// const int16_t boxRight = boxLeft + boxWidth - 1;
|
const int16_t boxTop = display->height() - boxHeight;
|
||||||
const int16_t boxTop = (display->height() / 2) - (boxHeight / 2 + random(-imprecision, imprecision + 1));
|
const int16_t boxBottom = display->height() - 1;
|
||||||
const int16_t boxBottom = boxTop + boxHeight - 1;
|
|
||||||
const int16_t idTextLeft = boxLeft + padding;
|
const int16_t idTextLeft = boxLeft + padding;
|
||||||
const int16_t idTextTop = boxTop + padding;
|
const int16_t idTextTop = boxTop + padding;
|
||||||
const int16_t pauseTextLeft = boxLeft + (useId ? padding + idTextWidth + padding : 0) + padding;
|
const int16_t pauseTextLeft = boxLeft + (useId ? idTextWidth + (padding * 2) : 0) + padding;
|
||||||
const int16_t pauseTextTop = boxTop + padding;
|
const int16_t pauseTextTop = boxTop + padding;
|
||||||
const int16_t dividerX = boxLeft + padding + idTextWidth + padding;
|
const int16_t dividerX = boxLeft + padding + idTextWidth + padding;
|
||||||
const int16_t dividerTop = boxTop + 1 + dividerGap;
|
const int16_t dividerTop = boxTop + dividerGap;
|
||||||
const int16_t dividerBottom = boxBottom - 1 - dividerGap;
|
const int16_t dividerBottom = boxBottom - dividerGap;
|
||||||
|
|
||||||
// Draw: box
|
// Draw: box
|
||||||
display->setColor(EINK_WHITE);
|
display->setColor(EINK_WHITE);
|
||||||
display->fillRect(boxLeft - 1, boxTop - 1, boxWidth + 2, boxHeight + 2); // Clear a slightly oversized area for the box
|
display->fillRect(boxLeft, boxTop, boxWidth, boxHeight);
|
||||||
display->setColor(EINK_BLACK);
|
display->setColor(EINK_BLACK);
|
||||||
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
|
display->drawRect(boxLeft, boxTop, boxWidth, boxHeight);
|
||||||
|
|
||||||
// Draw: Text
|
// Draw: text
|
||||||
if (useId)
|
if (useId)
|
||||||
display->drawString(idTextLeft, idTextTop, idText);
|
display->drawString(idTextLeft, idTextTop, idText);
|
||||||
display->drawString(pauseTextLeft, pauseTextTop, pauseText);
|
display->drawString(pauseTextLeft, pauseTextTop, pauseText);
|
||||||
@@ -920,15 +905,15 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU
|
|||||||
} 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";
|
||||||
}
|
}
|
||||||
int yOffset = (SCREEN_WIDTH > 128) ? 3 : 1;
|
int yOffset = (isHighResolution) ? 3 : 1;
|
||||||
if (SCREEN_WIDTH > 128) {
|
if (isHighResolution) {
|
||||||
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
|
NodeListRenderer::drawScaledXBitmap16x16(x, getTextPositions(display)[line] + yOffset - 5, imgSatellite_width,
|
||||||
imgSatellite_height, imgSatellite, display);
|
imgSatellite_height, imgSatellite, display);
|
||||||
} else {
|
} else {
|
||||||
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
|
display->drawXbm(x + 1, getTextPositions(display)[line] + yOffset, imgSatellite_width, imgSatellite_height,
|
||||||
imgSatellite);
|
imgSatellite);
|
||||||
}
|
}
|
||||||
int xOffset = (SCREEN_WIDTH > 128) ? 6 : 0;
|
int xOffset = (isHighResolution) ? 6 : 0;
|
||||||
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
|
display->drawString(x + 11 + xOffset, getTextPositions(display)[line++], displayLine);
|
||||||
} else {
|
} else {
|
||||||
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
|
UIRenderer::drawGps(display, 0, getTextPositions(display)[line++], gpsStatus);
|
||||||
@@ -1114,18 +1099,6 @@ void UIRenderer::drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *sta
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Function overlay for showing mute/buzzer modifiers etc.
|
|
||||||
void UIRenderer::drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state)
|
|
||||||
{
|
|
||||||
// LOG_DEBUG("Draw function overlay");
|
|
||||||
if (functionSymbol.begin() != functionSymbol.end()) {
|
|
||||||
char buf[64];
|
|
||||||
display->setFont(FONT_SMALL);
|
|
||||||
snprintf(buf, sizeof(buf), "%s", functionSymbolString.c_str());
|
|
||||||
display->drawString(SCREEN_WIDTH - display->getStringWidth(buf), SCREEN_HEIGHT - FONT_HEIGHT_SMALL, buf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation bar overlay implementation
|
// Navigation bar overlay implementation
|
||||||
static int8_t lastFrameIndex = -1;
|
static int8_t lastFrameIndex = -1;
|
||||||
static uint32_t lastFrameChangeTime = 0;
|
static uint32_t lastFrameChangeTime = 0;
|
||||||
@@ -1141,10 +1114,9 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
|
|||||||
lastFrameChangeTime = millis();
|
lastFrameChangeTime = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool useBigIcons = (SCREEN_WIDTH > 128);
|
const int iconSize = isHighResolution ? 16 : 8;
|
||||||
const int iconSize = useBigIcons ? 16 : 8;
|
const int spacing = isHighResolution ? 8 : 4;
|
||||||
const int spacing = useBigIcons ? 8 : 4;
|
const int bigOffset = isHighResolution ? 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)
|
if (totalIcons == 0)
|
||||||
@@ -1158,14 +1130,35 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
|
|||||||
const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing;
|
const int totalWidth = (pageEnd - pageStart) * iconSize + (pageEnd - pageStart - 1) * spacing;
|
||||||
const int xStart = (SCREEN_WIDTH - totalWidth) / 2;
|
const int xStart = (SCREEN_WIDTH - totalWidth) / 2;
|
||||||
|
|
||||||
// Only show bar briefly after switching frames (unless on E-Ink)
|
// Only show bar briefly after switching frames
|
||||||
|
static uint32_t navBarLastShown = 0;
|
||||||
|
static bool cosmeticRefreshDone = false;
|
||||||
|
|
||||||
|
bool navBarVisible = millis() - lastFrameChangeTime <= ICON_DISPLAY_DURATION_MS;
|
||||||
|
int y = navBarVisible ? (SCREEN_HEIGHT - iconSize - 1) : SCREEN_HEIGHT;
|
||||||
|
|
||||||
#if defined(USE_EINK)
|
#if defined(USE_EINK)
|
||||||
int y = SCREEN_HEIGHT - iconSize - 1;
|
static bool navBarPrevVisible = false;
|
||||||
#else
|
|
||||||
int y = SCREEN_HEIGHT - iconSize - 1;
|
if (navBarVisible && !navBarPrevVisible) {
|
||||||
if (millis() - lastFrameChangeTime > ICON_DISPLAY_DURATION_MS) {
|
EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when showing nav bar
|
||||||
y = SCREEN_HEIGHT;
|
cosmeticRefreshDone = false;
|
||||||
|
navBarLastShown = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!navBarVisible && navBarPrevVisible) {
|
||||||
|
EINK_ADD_FRAMEFLAG(display, DEMAND_FAST); // Fast refresh when hiding nav bar
|
||||||
|
navBarLastShown = millis(); // Mark when it disappeared
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!navBarVisible && navBarLastShown != 0 && !cosmeticRefreshDone) {
|
||||||
|
if (millis() - navBarLastShown > 10000) { // 10s after hidden
|
||||||
|
EINK_ADD_FRAMEFLAG(display, COSMETIC); // One-time ghost cleanup
|
||||||
|
cosmeticRefreshDone = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navBarPrevVisible = navBarVisible;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Pre-calculate bounding rect
|
// Pre-calculate bounding rect
|
||||||
@@ -1191,7 +1184,7 @@ void UIRenderer::drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *sta
|
|||||||
display->setColor(BLACK);
|
display->setColor(BLACK);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (useBigIcons) {
|
if (isHighResolution) {
|
||||||
NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display);
|
NodeListRenderer::drawScaledXBitmap16x16(x, y, 8, 8, icon, display);
|
||||||
} else {
|
} else {
|
||||||
display->drawXbm(x, y, iconSize, iconSize, icon);
|
display->drawXbm(x, y, iconSize, iconSize, icon);
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ class UIRenderer
|
|||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
// Common UI elements
|
// Common UI elements
|
||||||
static void drawBattery(OLEDDisplay *display, int16_t x, int16_t y, uint8_t *imgBuffer,
|
|
||||||
const meshtastic::PowerStatus *powerStatus);
|
|
||||||
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus,
|
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus,
|
||||||
int node_offset = 0, bool show_total = true, String additional_words = "");
|
int node_offset = 0, bool show_total = true, String additional_words = "");
|
||||||
|
|
||||||
@@ -49,9 +47,6 @@ class UIRenderer
|
|||||||
// Overlay and special screens
|
// Overlay and special screens
|
||||||
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text);
|
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text);
|
||||||
|
|
||||||
// Function overlay for showing mute/buzzer modifiers etc.
|
|
||||||
static void drawFunctionOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
|
||||||
|
|
||||||
// Navigation bar overlay
|
// Navigation bar overlay
|
||||||
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,6 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03
|
|||||||
0xf3, 0x3f, 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x03, 0x33, 0xff, 0x33,
|
0xf3, 0x3f, 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x03, 0x33, 0xff, 0x33,
|
||||||
0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f};
|
0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f};
|
||||||
|
|
||||||
// This image definition is here instead of images.h because it's modified dynamically by the drawBattery function
|
|
||||||
static uint8_t imgBattery[16] = {0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0xE7, 0x3C};
|
|
||||||
|
|
||||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \
|
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || ARCH_PORTDUINO) && \
|
||||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||||
@@ -45,19 +42,15 @@ const uint8_t imgSF[] PROGMEM = {0xd2, 0xb7, 0xad, 0xbb, 0x92, 0x01, 0xfd, 0xfd,
|
|||||||
|
|
||||||
// === Horizontal battery ===
|
// === Horizontal battery ===
|
||||||
// Basic battery design and all related pieces
|
// Basic battery design and all related pieces
|
||||||
const unsigned char batteryBitmap_h[] PROGMEM = {
|
const unsigned char batteryBitmap_h_bottom[] PROGMEM = {
|
||||||
0b11111110, 0b00000000, 0b11110000, 0b00000111, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000,
|
0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001,
|
||||||
0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000,
|
0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
||||||
0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000,
|
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00011110, 0b00000000};
|
||||||
0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000, 0b00000001, 0b00000000, 0b00000000, 0b00011000,
|
|
||||||
0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b00000001, 0b00000000,
|
|
||||||
0b00000000, 0b00001000, 0b00000001, 0b00000000, 0b00000000, 0b00001000, 0b11111110, 0b00000000, 0b11110000, 0b00000111};
|
|
||||||
|
|
||||||
// This is the left and right bars for the fill in
|
const unsigned char batteryBitmap_h_top[] PROGMEM = {
|
||||||
const unsigned char batteryBitmap_sidegaps_h[] PROGMEM = {
|
0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000,
|
||||||
0b11111111, 0b00001111, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000,
|
||||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
|
0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b00111100, 0b00000000};
|
||||||
0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b11111111, 0b00001111};
|
|
||||||
|
|
||||||
// Lightning Bolt
|
// Lightning Bolt
|
||||||
const unsigned char lightning_bolt_h[] PROGMEM = {
|
const unsigned char lightning_bolt_h[] PROGMEM = {
|
||||||
@@ -280,11 +273,16 @@ const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100,
|
|||||||
const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000,
|
const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000,
|
||||||
0b00000000, 0b00000000, 0b00000000, 0b00000000};
|
0b00000000, 0b00000000, 0b00000000, 0b00000000};
|
||||||
|
|
||||||
// Clock
|
// Digital Clock
|
||||||
#define icon_clock_width 8
|
#define digital_icon_clock_width 8
|
||||||
#define icon_clock_height 8
|
#define digital_icon_clock_height 8
|
||||||
const uint8_t icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001,
|
const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001,
|
||||||
0b10010001, 0b10000001, 0b01000010, 0b00111100};
|
0b10010001, 0b10000001, 0b01000010, 0b00111100};
|
||||||
|
// Analog Clock
|
||||||
|
#define analog_icon_clock_width 8
|
||||||
|
#define analog_icon_clock_height 8
|
||||||
|
const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000,
|
||||||
|
0b00100100, 0b01000010, 0b01000010, 0b11111111};
|
||||||
|
|
||||||
#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");
|
||||||
@@ -19,6 +19,8 @@ namespace NicheGraphics::InkHUD
|
|||||||
enum MenuAction {
|
enum MenuAction {
|
||||||
NO_ACTION,
|
NO_ACTION,
|
||||||
SEND_PING,
|
SEND_PING,
|
||||||
|
STORE_CANNEDMESSAGE_SELECTION,
|
||||||
|
SEND_CANNEDMESSAGE,
|
||||||
SHUTDOWN,
|
SHUTDOWN,
|
||||||
NEXT_TILE,
|
NEXT_TILE,
|
||||||
TOGGLE_BACKLIGHT,
|
TOGGLE_BACKLIGHT,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
|
|
||||||
#include "MeshService.h"
|
#include "MeshService.h"
|
||||||
|
#include "Router.h"
|
||||||
#include "airtime.h"
|
#include "airtime.h"
|
||||||
#include "main.h"
|
#include "main.h"
|
||||||
#include "power.h"
|
#include "power.h"
|
||||||
@@ -31,6 +32,12 @@ InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet")
|
|||||||
if (settings->optionalMenuItems.backlight) {
|
if (settings->optionalMenuItems.backlight) {
|
||||||
backlight = Drivers::LatchingBacklight::getInstance();
|
backlight = Drivers::LatchingBacklight::getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize the Canned Message store
|
||||||
|
// This is a shared nicheGraphics component
|
||||||
|
// - handles loading & parsing the canned messages
|
||||||
|
// - handles setting / getting of canned messages via apps (Client API Admin Messages)
|
||||||
|
cm.store = CannedMessageStore::getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InkHUD::MenuApplet::onForeground()
|
void InkHUD::MenuApplet::onForeground()
|
||||||
@@ -65,6 +72,10 @@ void InkHUD::MenuApplet::onForeground()
|
|||||||
|
|
||||||
void InkHUD::MenuApplet::onBackground()
|
void InkHUD::MenuApplet::onBackground()
|
||||||
{
|
{
|
||||||
|
// Discard any data we generated while selecting a canned message
|
||||||
|
// Frees heap mem
|
||||||
|
freeCannedMessageResources();
|
||||||
|
|
||||||
// If device has a backlight which isn't controlled by aux button:
|
// If device has a backlight which isn't controlled by aux button:
|
||||||
// Item in options submenu allows keeping backlight on after menu is closed
|
// Item in options submenu allows keeping backlight on after menu is closed
|
||||||
// If this item is deselected we will turn backlight off again, now that menu is closing
|
// If this item is deselected we will turn backlight off again, now that menu is closing
|
||||||
@@ -153,6 +164,16 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
|||||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL);
|
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case STORE_CANNEDMESSAGE_SELECTION:
|
||||||
|
cm.selectedMessageItem = &cm.messageItems.at(cursor - 1); // Minus one: offset for the initial "Send Ping" entry
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SEND_CANNEDMESSAGE:
|
||||||
|
cm.selectedRecipientItem = &cm.recipientItems.at(cursor);
|
||||||
|
sendText(cm.selectedRecipientItem->dest, cm.selectedRecipientItem->channelIndex, cm.selectedMessageItem->rawText.c_str());
|
||||||
|
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL); // Next refresh should be FULL. Lots of button pressing to get here
|
||||||
|
break;
|
||||||
|
|
||||||
case ROTATE:
|
case ROTATE:
|
||||||
inkhud->rotate();
|
inkhud->rotate();
|
||||||
break;
|
break;
|
||||||
@@ -260,9 +281,11 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case SEND:
|
case SEND:
|
||||||
items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT));
|
populateSendPage();
|
||||||
// Todo: canned messages
|
break;
|
||||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
|
||||||
|
case CANNEDMESSAGE_RECIPIENT:
|
||||||
|
populateRecipientPage();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case OPTIONS:
|
case OPTIONS:
|
||||||
@@ -497,6 +520,8 @@ void InkHUD::MenuApplet::populateAutoshowPage()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create MenuItem entries to select our definition of "Recent"
|
||||||
|
// Controls how long data will remain in any "Recents" flavored applets
|
||||||
void InkHUD::MenuApplet::populateRecentsPage()
|
void InkHUD::MenuApplet::populateRecentsPage()
|
||||||
{
|
{
|
||||||
// How many values are shown for use to choose from
|
// How many values are shown for use to choose from
|
||||||
@@ -510,6 +535,112 @@ void InkHUD::MenuApplet::populateRecentsPage()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MenuItem entries for the "send" page
|
||||||
|
// Dynamically creates menu items based on available canned messages
|
||||||
|
void InkHUD::MenuApplet::populateSendPage()
|
||||||
|
{
|
||||||
|
// Position / NodeInfo packet
|
||||||
|
items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT));
|
||||||
|
|
||||||
|
// One menu item for each canned message
|
||||||
|
uint8_t count = cm.store->size();
|
||||||
|
for (uint8_t i = 0; i < count; i++) {
|
||||||
|
// Gather the information for this item
|
||||||
|
CannedMessages::MessageItem messageItem;
|
||||||
|
messageItem.rawText = cm.store->at(i);
|
||||||
|
messageItem.label = parse(messageItem.rawText);
|
||||||
|
|
||||||
|
// Store the item (until the menu closes)
|
||||||
|
cm.messageItems.push_back(messageItem);
|
||||||
|
|
||||||
|
// Create a menu item
|
||||||
|
const char *itemText = cm.messageItems.back().label.c_str();
|
||||||
|
items.push_back(MenuItem(itemText, MenuAction::STORE_CANNEDMESSAGE_SELECTION, MenuPage::CANNEDMESSAGE_RECIPIENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamically create MenuItem entries for possible canned message destinations
|
||||||
|
// All available channels are shown
|
||||||
|
// Favorite nodes are shown, provided we don't have an *excessive* amount
|
||||||
|
void InkHUD::MenuApplet::populateRecipientPage()
|
||||||
|
{
|
||||||
|
// Create recipient data (and menu items) for any channels
|
||||||
|
// --------------------------------------------------------
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < MAX_NUM_CHANNELS; i++) {
|
||||||
|
// Get the channel, and check if it's enabled
|
||||||
|
meshtastic_Channel &channel = channels.getByIndex(i);
|
||||||
|
if (!channel.has_settings || channel.role == meshtastic_Channel_Role_DISABLED)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CannedMessages::RecipientItem r;
|
||||||
|
|
||||||
|
// Set index
|
||||||
|
r.channelIndex = channel.index;
|
||||||
|
|
||||||
|
// Set a label for the menu item
|
||||||
|
r.label = "Ch " + to_string(i) + ": ";
|
||||||
|
if (channel.role == meshtastic_Channel_Role_PRIMARY)
|
||||||
|
r.label += "Primary";
|
||||||
|
else
|
||||||
|
r.label += parse(channel.settings.name);
|
||||||
|
|
||||||
|
// Add to the list of recipients
|
||||||
|
cm.recipientItems.push_back(r);
|
||||||
|
|
||||||
|
// Add a menu item for this recipient
|
||||||
|
const char *itemText = cm.recipientItems.back().label.c_str();
|
||||||
|
items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create recipient data (and menu items) for favorite nodes
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
|
||||||
|
uint32_t nodeCount = nodeDB->getNumMeshNodes();
|
||||||
|
uint32_t favoriteCount = 0;
|
||||||
|
|
||||||
|
// Count favorites
|
||||||
|
for (uint32_t i = 0; i < nodeCount; i++) {
|
||||||
|
if (nodeDB->getMeshNodeByIndex(i)->is_favorite)
|
||||||
|
favoriteCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add favorites if the number is reasonable
|
||||||
|
// Don't want some monstrous list that takes 100 clicks to reach exit
|
||||||
|
if (favoriteCount < 20) {
|
||||||
|
for (uint32_t i = 0; i < nodeCount; i++) {
|
||||||
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||||
|
|
||||||
|
// Skip node if not a favorite
|
||||||
|
if (!node->is_favorite)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CannedMessages::RecipientItem r;
|
||||||
|
|
||||||
|
r.dest = node->num;
|
||||||
|
r.channelIndex = nodeDB->getMeshNodeChannel(node->num); // Channel index only relevant if encrypted DM not possible(?)
|
||||||
|
|
||||||
|
// Set a label for the menu item
|
||||||
|
r.label = "DM: ";
|
||||||
|
if (node->has_user)
|
||||||
|
r.label += parse(node->user.long_name);
|
||||||
|
else
|
||||||
|
r.label += hexifyNodeNum(node->num); // Unsure if it's possible to favorite a node without NodeInfo?
|
||||||
|
|
||||||
|
// Add to the list of recipients
|
||||||
|
cm.recipientItems.push_back(r);
|
||||||
|
|
||||||
|
// Add a menu item for this recipient
|
||||||
|
const char *itemText = cm.recipientItems.back().label.c_str();
|
||||||
|
items.push_back(MenuItem(itemText, SEND_CANNEDMESSAGE, MenuPage::EXIT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||||
|
}
|
||||||
|
|
||||||
// Renders the panel shown at the top of the root menu.
|
// Renders the panel shown at the top of the root menu.
|
||||||
// Displays the clock, and several other pieces of instantaneous system info,
|
// Displays the clock, and several other pieces of instantaneous system info,
|
||||||
// which we'd prefer not to have displayed in a normal applet, as they update too frequently.
|
// which we'd prefer not to have displayed in a normal applet, as they update too frequently.
|
||||||
@@ -619,4 +750,37 @@ uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight()
|
|||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send a text message to the mesh
|
||||||
|
// Used to send our canned messages
|
||||||
|
void InkHUD::MenuApplet::sendText(NodeNum dest, ChannelIndex channel, const char *message)
|
||||||
|
{
|
||||||
|
meshtastic_MeshPacket *p = router->allocForSending();
|
||||||
|
p->decoded.portnum = meshtastic_PortNum_TEXT_MESSAGE_APP;
|
||||||
|
p->to = dest;
|
||||||
|
p->channel = channel;
|
||||||
|
p->want_ack = true;
|
||||||
|
p->decoded.payload.size = strlen(message);
|
||||||
|
memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size);
|
||||||
|
|
||||||
|
// Tack on a bell character if requested
|
||||||
|
if (moduleConfig.canned_message.send_bell && p->decoded.payload.size < meshtastic_Constants_DATA_PAYLOAD_LEN) {
|
||||||
|
p->decoded.payload.bytes[p->decoded.payload.size] = 7; // Bell character
|
||||||
|
p->decoded.payload.bytes[p->decoded.payload.size + 1] = '\0'; // Append Null Terminator
|
||||||
|
p->decoded.payload.size++;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("Send message id=%d, dest=%x, msg=%.*s", p->id, p->to, p->decoded.payload.size, p->decoded.payload.bytes);
|
||||||
|
|
||||||
|
service->sendToMesh(p, RX_SRC_LOCAL, true); // Send to mesh, cc to phone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free up any heap mmemory we'd used while selecting / sending canned messages
|
||||||
|
void InkHUD::MenuApplet::freeCannedMessageResources()
|
||||||
|
{
|
||||||
|
cm.selectedMessageItem = nullptr;
|
||||||
|
cm.selectedRecipientItem = nullptr;
|
||||||
|
cm.messageItems.clear();
|
||||||
|
cm.recipientItems.clear();
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -6,10 +6,12 @@
|
|||||||
#include "graphics/niche/InkHUD/InkHUD.h"
|
#include "graphics/niche/InkHUD/InkHUD.h"
|
||||||
#include "graphics/niche/InkHUD/Persistence.h"
|
#include "graphics/niche/InkHUD/Persistence.h"
|
||||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||||
|
#include "graphics/niche/Utils/CannedMessageStore.h"
|
||||||
|
|
||||||
#include "./MenuItem.h"
|
#include "./MenuItem.h"
|
||||||
#include "./MenuPage.h"
|
#include "./MenuPage.h"
|
||||||
|
|
||||||
|
#include "Channels.h"
|
||||||
#include "concurrency/OSThread.h"
|
#include "concurrency/OSThread.h"
|
||||||
|
|
||||||
namespace NicheGraphics::InkHUD
|
namespace NicheGraphics::InkHUD
|
||||||
@@ -36,12 +38,18 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
|||||||
|
|
||||||
void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any
|
void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any
|
||||||
void showPage(MenuPage page); // Load and display a MenuPage
|
void showPage(MenuPage page); // Load and display a MenuPage
|
||||||
|
|
||||||
|
void populateSendPage(); // Dynamically create MenuItems including canned messages
|
||||||
|
void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message
|
||||||
void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets
|
void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets
|
||||||
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
|
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
|
||||||
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
||||||
|
|
||||||
uint16_t getSystemInfoPanelHeight();
|
uint16_t getSystemInfoPanelHeight();
|
||||||
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
|
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
|
||||||
uint16_t *height = nullptr); // Info panel at top of root menu
|
uint16_t *height = nullptr); // Info panel at top of root menu
|
||||||
|
void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh
|
||||||
|
void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data
|
||||||
|
|
||||||
MenuPage currentPage = MenuPage::ROOT;
|
MenuPage currentPage = MenuPage::ROOT;
|
||||||
uint8_t cursor = 0; // Which menu item is currently highlighted
|
uint8_t cursor = 0; // Which menu item is currently highlighted
|
||||||
@@ -51,6 +59,37 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
|
|||||||
|
|
||||||
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
|
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
|
||||||
|
|
||||||
|
// Data for selecting and sending canned messages via the menu
|
||||||
|
// Placed into a sub-class for organization only
|
||||||
|
class CannedMessages
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Share NicheGraphics component
|
||||||
|
// Handles loading, getting, setting
|
||||||
|
CannedMessageStore *store;
|
||||||
|
|
||||||
|
// One canned message
|
||||||
|
// Links the menu item to the true message text
|
||||||
|
struct MessageItem {
|
||||||
|
std::string label; // Shown in menu. Prefixed, and UTF-8 chars parsed
|
||||||
|
std::string rawText; // The message which will be sent, if this item is selected
|
||||||
|
} *selectedMessageItem;
|
||||||
|
|
||||||
|
// One possible destination for a canned message
|
||||||
|
// Links the menu item to the intended recipient
|
||||||
|
// May represent either broadcast or DM
|
||||||
|
struct RecipientItem {
|
||||||
|
std::string label; // Shown in menu
|
||||||
|
NodeNum dest = NODENUM_BROADCAST;
|
||||||
|
uint8_t channelIndex = 0;
|
||||||
|
} *selectedRecipientItem;
|
||||||
|
|
||||||
|
// These lists are generated when the menu page is populated
|
||||||
|
// Cleared onBackground (when MenuApplet closes)
|
||||||
|
std::vector<MessageItem> messageItems;
|
||||||
|
std::vector<RecipientItem> recipientItems;
|
||||||
|
} cm;
|
||||||
|
|
||||||
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
|
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ namespace NicheGraphics::InkHUD
|
|||||||
enum MenuPage : uint8_t {
|
enum MenuPage : uint8_t {
|
||||||
ROOT, // Initial menu page
|
ROOT, // Initial menu page
|
||||||
SEND,
|
SEND,
|
||||||
|
CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message
|
||||||
OPTIONS,
|
OPTIONS,
|
||||||
APPLETS,
|
APPLETS,
|
||||||
AUTOSHOW,
|
AUTOSHOW,
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ using namespace NicheGraphics;
|
|||||||
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
|
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
|
||||||
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
|
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
|
||||||
|
|
||||||
InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) : channelIndex(channelIndex)
|
InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex)
|
||||||
|
: SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex)
|
||||||
{
|
{
|
||||||
// Create the message store
|
// Create the message store
|
||||||
// Will shortly attempt to load messages from RAM, if applet is active
|
// Will shortly attempt to load messages from RAM, if applet is active
|
||||||
@@ -69,9 +70,8 @@ void InkHUD::ThreadedMessageApplet::onRender()
|
|||||||
|
|
||||||
// Grab data for message
|
// Grab data for message
|
||||||
MessageStore::Message &m = store->messages.at(i);
|
MessageStore::Message &m = store->messages.at(i);
|
||||||
bool outgoing = (m.sender == 0);
|
bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message
|
||||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender);
|
std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message
|
||||||
std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message
|
|
||||||
|
|
||||||
// Cache bottom Y of message text
|
// Cache bottom Y of message text
|
||||||
// - Used when drawing vertical line alongside
|
// - Used when drawing vertical line alongside
|
||||||
@@ -171,54 +171,54 @@ void InkHUD::ThreadedMessageApplet::onRender()
|
|||||||
void InkHUD::ThreadedMessageApplet::onActivate()
|
void InkHUD::ThreadedMessageApplet::onActivate()
|
||||||
{
|
{
|
||||||
loadMessagesFromFlash();
|
loadMessagesFromFlash();
|
||||||
textMessageObserver.observe(textMessageModule); // Begin handling any new text messages with onReceiveTextMessage
|
loopbackOk = true; // Allow us to handle messages generated on the node (canned messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Code which runs when the applet stop running
|
// Code which runs when the applet stop running
|
||||||
// This might be happen at shutdown, or if user disables the applet at run-time
|
// This might be at shutdown, or if the user disables the applet at run-time, via the menu
|
||||||
void InkHUD::ThreadedMessageApplet::onDeactivate()
|
void InkHUD::ThreadedMessageApplet::onDeactivate()
|
||||||
{
|
{
|
||||||
textMessageObserver.unobserve(textMessageModule); // Stop handling any new text messages with onReceiveTextMessage
|
loopbackOk = false; // Slightly reduce our impact if the applet is disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle new text messages
|
// Handle new text messages
|
||||||
// These might be incoming, from the mesh, or outgoing from phone
|
// These might be incoming, from the mesh, or outgoing from phone
|
||||||
// Each instance of the ThreadMessageApplet will only listen on one specific channel
|
// Each instance of the ThreadMessageApplet will only listen on one specific channel
|
||||||
// Method should return 0, to indicate general success to TextMessageModule
|
ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_MeshPacket &mp)
|
||||||
int InkHUD::ThreadedMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
|
||||||
{
|
{
|
||||||
// Abort if applet fully deactivated
|
// Abort if applet fully deactivated
|
||||||
// Already handled by onActivate and onDeactivate, but good practice for all applets
|
|
||||||
if (!isActive())
|
if (!isActive())
|
||||||
return 0;
|
return ProcessMessage::CONTINUE;
|
||||||
|
|
||||||
// Abort if wrong channel
|
// Abort if wrong channel
|
||||||
if (p->channel != this->channelIndex)
|
if (mp.channel != this->channelIndex)
|
||||||
return 0;
|
return ProcessMessage::CONTINUE;
|
||||||
|
|
||||||
// Abort if message was a DM
|
// Abort if message was a DM
|
||||||
if (p->to != NODENUM_BROADCAST)
|
if (mp.to != NODENUM_BROADCAST)
|
||||||
return 0;
|
return ProcessMessage::CONTINUE;
|
||||||
|
|
||||||
// Extract info into our slimmed-down "StoredMessage" type
|
// Extract info into our slimmed-down "StoredMessage" type
|
||||||
MessageStore::Message newMessage;
|
MessageStore::Message newMessage;
|
||||||
newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
||||||
newMessage.sender = p->from;
|
newMessage.sender = mp.from;
|
||||||
newMessage.channelIndex = p->channel;
|
newMessage.channelIndex = mp.channel;
|
||||||
newMessage.text = std::string(&p->decoded.payload.bytes[0], &p->decoded.payload.bytes[p->decoded.payload.size]);
|
newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size);
|
||||||
|
|
||||||
// Store newest message at front
|
// Store newest message at front
|
||||||
// These records are used when rendering, and also stored in flash at shutdown
|
// These records are used when rendering, and also stored in flash at shutdown
|
||||||
store->messages.push_front(newMessage);
|
store->messages.push_front(newMessage);
|
||||||
|
|
||||||
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
|
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
|
||||||
if (getFrom(p) != nodeDB->getNodeNum())
|
if (getFrom(&mp) != nodeDB->getNodeNum())
|
||||||
requestAutoshow();
|
requestAutoshow();
|
||||||
|
|
||||||
// Redraw the applet, perhaps.
|
// Redraw the applet, perhaps.
|
||||||
requestUpdate(); // Want to update display, if applet is foreground
|
requestUpdate(); // Want to update display, if applet is foreground
|
||||||
|
|
||||||
return 0;
|
// Tell Module API to continue informing other firmware components about this message
|
||||||
|
// We're not the only component which is interested in new text messages
|
||||||
|
return ProcessMessage::CONTINUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't show notifications for text messages broadcast to our channel, when the applet is displayed
|
// Don't show notifications for text messages broadcast to our channel, when the applet is displayed
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace NicheGraphics::InkHUD
|
|||||||
|
|
||||||
class Applet;
|
class Applet;
|
||||||
|
|
||||||
class ThreadedMessageApplet : public Applet
|
class ThreadedMessageApplet : public Applet, public SinglePortModule
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit ThreadedMessageApplet(uint8_t channelIndex);
|
explicit ThreadedMessageApplet(uint8_t channelIndex);
|
||||||
@@ -41,16 +41,11 @@ class ThreadedMessageApplet : public Applet
|
|||||||
void onActivate() override;
|
void onActivate() override;
|
||||||
void onDeactivate() override;
|
void onDeactivate() override;
|
||||||
void onShutdown() override;
|
void onShutdown() override;
|
||||||
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
|
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||||
|
|
||||||
bool approveNotification(Notification &n) override; // Which notifications to suppress
|
bool approveNotification(Notification &n) override; // Which notifications to suppress
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Used to register our text message callback
|
|
||||||
CallbackObserver<ThreadedMessageApplet, const meshtastic_MeshPacket *> textMessageObserver =
|
|
||||||
CallbackObserver<ThreadedMessageApplet, const meshtastic_MeshPacket *>(this,
|
|
||||||
&ThreadedMessageApplet::onReceiveTextMessage);
|
|
||||||
|
|
||||||
void saveMessagesToFlash();
|
void saveMessagesToFlash();
|
||||||
void loadMessagesFromFlash();
|
void loadMessagesFromFlash();
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,13 @@
|
|||||||
|
|
||||||
#include "RTC.h"
|
#include "RTC.h"
|
||||||
#include "buzz.h"
|
#include "buzz.h"
|
||||||
#include "modules/AdminModule.h"
|
|
||||||
#include "modules/ExternalNotificationModule.h"
|
#include "modules/ExternalNotificationModule.h"
|
||||||
#include "modules/TextMessageModule.h"
|
#include "modules/TextMessageModule.h"
|
||||||
#include "sleep.h"
|
#include "sleep.h"
|
||||||
|
|
||||||
#include "./Applet.h"
|
#include "./Applet.h"
|
||||||
#include "./SystemApplet.h"
|
#include "./SystemApplet.h"
|
||||||
#include "graphics/niche/FlashData.h"
|
#include "graphics/niche/Utils/FlashData.h"
|
||||||
|
|
||||||
using namespace NicheGraphics;
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ void InkHUD::Events::begin()
|
|||||||
rebootObserver.observe(¬ifyReboot);
|
rebootObserver.observe(¬ifyReboot);
|
||||||
textMessageObserver.observe(textMessageModule);
|
textMessageObserver.observe(textMessageModule);
|
||||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||||
adminMessageObserver.observe(adminModule);
|
adminMessageObserver.observe((Observable<AdminModule_ObserverData *> *)adminModule);
|
||||||
#endif
|
#endif
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
lightSleepObserver.observe(¬ifyLightSleep);
|
lightSleepObserver.observe(¬ifyLightSleep);
|
||||||
@@ -193,14 +192,15 @@ int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet)
|
|||||||
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
|
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
|
||||||
}
|
}
|
||||||
|
|
||||||
int InkHUD::Events::onAdminMessage(const meshtastic_AdminMessage *message)
|
int InkHUD::Events::onAdminMessage(AdminModule_ObserverData *data)
|
||||||
{
|
{
|
||||||
switch (message->which_payload_variant) {
|
switch (data->request->which_payload_variant) {
|
||||||
// Factory reset
|
// Factory reset
|
||||||
// Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data.
|
// Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data.
|
||||||
case meshtastic_AdminMessage_factory_reset_device_tag:
|
case meshtastic_AdminMessage_factory_reset_device_tag:
|
||||||
case meshtastic_AdminMessage_factory_reset_config_tag:
|
case meshtastic_AdminMessage_factory_reset_config_tag:
|
||||||
eraseOnReboot = true;
|
eraseOnReboot = true;
|
||||||
|
*data->result = AdminMessageHandleResult::HANDLED;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ however this class handles general events which concern InkHUD as a whole, e.g.
|
|||||||
|
|
||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
|
||||||
#include "Observer.h"
|
#include "modules/AdminModule.h"
|
||||||
|
|
||||||
#include "./InkHUD.h"
|
#include "./InkHUD.h"
|
||||||
#include "./Persistence.h"
|
#include "./Persistence.h"
|
||||||
@@ -33,7 +33,7 @@ class Events
|
|||||||
int beforeDeepSleep(void *unused); // Prepare for shutdown
|
int beforeDeepSleep(void *unused); // Prepare for shutdown
|
||||||
int beforeReboot(void *unused); // Prepare for reboot
|
int beforeReboot(void *unused); // Prepare for reboot
|
||||||
int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message
|
int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message
|
||||||
int onAdminMessage(const meshtastic_AdminMessage *message); // Handle incoming admin messages
|
int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
int beforeLightSleep(void *unused); // Prepare for light sleep
|
int beforeLightSleep(void *unused); // Prepare for light sleep
|
||||||
#endif
|
#endif
|
||||||
@@ -54,8 +54,8 @@ class Events
|
|||||||
CallbackObserver<Events, const meshtastic_MeshPacket *>(this, &Events::onReceiveTextMessage);
|
CallbackObserver<Events, const meshtastic_MeshPacket *>(this, &Events::onReceiveTextMessage);
|
||||||
|
|
||||||
// Get notified of incoming admin messages, and handle any which are relevant to InkHUD
|
// Get notified of incoming admin messages, and handle any which are relevant to InkHUD
|
||||||
CallbackObserver<Events, const meshtastic_AdminMessage *> adminMessageObserver =
|
CallbackObserver<Events, AdminModule_ObserverData *> adminMessageObserver =
|
||||||
CallbackObserver<Events, const meshtastic_AdminMessage *>(this, &Events::onAdminMessage);
|
CallbackObserver<Events, AdminModule_ObserverData *>(this, &Events::onAdminMessage);
|
||||||
|
|
||||||
#ifdef ARCH_ESP32
|
#ifdef ARCH_ESP32
|
||||||
// Get notified when the system is entering light sleep
|
// Get notified when the system is entering light sleep
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ The save / load mechanism is a shared NicheGraphics feature.
|
|||||||
#include "configuration.h"
|
#include "configuration.h"
|
||||||
|
|
||||||
#include "./InkHUD.h"
|
#include "./InkHUD.h"
|
||||||
#include "graphics/niche/FlashData.h"
|
|
||||||
#include "graphics/niche/InkHUD/MessageStore.h"
|
#include "graphics/niche/InkHUD/MessageStore.h"
|
||||||
|
#include "graphics/niche/Utils/FlashData.h"
|
||||||
|
|
||||||
namespace NicheGraphics::InkHUD
|
namespace NicheGraphics::InkHUD
|
||||||
{
|
{
|
||||||
|
|||||||
163
src/graphics/niche/Utils/CannedMessageStore.cpp
Normal file
163
src/graphics/niche/Utils/CannedMessageStore.cpp
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
#include "./CannedMessageStore.h"
|
||||||
|
|
||||||
|
#include "FSCommon.h"
|
||||||
|
#include "NodeDB.h"
|
||||||
|
#include "SPILock.h"
|
||||||
|
#include "generated/meshtastic/cannedmessages.pb.h"
|
||||||
|
|
||||||
|
using namespace NicheGraphics;
|
||||||
|
|
||||||
|
// Location of the file which stores the canned messages on flash
|
||||||
|
static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto";
|
||||||
|
|
||||||
|
CannedMessageStore::CannedMessageStore()
|
||||||
|
{
|
||||||
|
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||||
|
adminMessageObserver.observe(adminModule);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Load & parse messages from flash
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get access to (or create) the singleton instance of this class
|
||||||
|
CannedMessageStore *CannedMessageStore::getInstance()
|
||||||
|
{
|
||||||
|
// Instantiate the class the first time this method is called
|
||||||
|
static CannedMessageStore *const singletonInstance = new CannedMessageStore;
|
||||||
|
|
||||||
|
return singletonInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access canned messages by index
|
||||||
|
// Consumer should check CannedMessageStore::size to avoid accessing out of bounds
|
||||||
|
const std::string &CannedMessageStore::at(uint8_t i)
|
||||||
|
{
|
||||||
|
assert(i < messages.size());
|
||||||
|
return messages.at(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Number of canned message strings available
|
||||||
|
uint8_t CannedMessageStore::size()
|
||||||
|
{
|
||||||
|
return messages.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load canned message data from flash, and parse into the individual strings
|
||||||
|
void CannedMessageStore::load()
|
||||||
|
{
|
||||||
|
// In case we're reloading
|
||||||
|
messages.clear();
|
||||||
|
|
||||||
|
// Attempt to load the bulk canned message data from flash
|
||||||
|
meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig;
|
||||||
|
LoadFileResult result = nodeDB->loadProto("/prefs/cannedConf.proto", meshtastic_CannedMessageModuleConfig_size,
|
||||||
|
sizeof(meshtastic_CannedMessageModuleConfig),
|
||||||
|
&meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig);
|
||||||
|
|
||||||
|
// Abort if nothing to load
|
||||||
|
if (result != LoadFileResult::LOAD_SUCCESS || strlen(cannedMessageModuleConfig.messages) == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Split into individual canned messages
|
||||||
|
// These are concatenated when stored in flash, using '|' as a delimiter
|
||||||
|
std::string s;
|
||||||
|
for (char c : cannedMessageModuleConfig.messages) { // Character by character
|
||||||
|
|
||||||
|
// If found end of a string
|
||||||
|
if (c == '|' || c == '\0') {
|
||||||
|
// Copy into the vector (if non-empty)
|
||||||
|
if (!s.empty())
|
||||||
|
messages.push_back(s);
|
||||||
|
|
||||||
|
// Reset the string builder
|
||||||
|
s.clear();
|
||||||
|
|
||||||
|
// End of data, all strings processed
|
||||||
|
if (c == 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, append char (continue building string)
|
||||||
|
else
|
||||||
|
s.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle incoming admin messages
|
||||||
|
// We get these as an observer of AdminModule
|
||||||
|
// It's our responsibility to handle setting and getting of canned messages via the client API
|
||||||
|
// Ordinarily, this would be handled by the CannedMessageModule, but it is bound to Screen.cpp, so not suitable for NicheGraphics
|
||||||
|
int CannedMessageStore::onAdminMessage(AdminModule_ObserverData *data)
|
||||||
|
{
|
||||||
|
switch (data->request->which_payload_variant) {
|
||||||
|
|
||||||
|
// Client API changing the canned messages
|
||||||
|
case meshtastic_AdminMessage_set_canned_message_module_messages_tag:
|
||||||
|
handleSet(data->request);
|
||||||
|
*data->result = AdminMessageHandleResult::HANDLED;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Client API wants to know the current canned messages
|
||||||
|
case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag:
|
||||||
|
handleGet(data->response);
|
||||||
|
*data->result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client API changing the canned messages
|
||||||
|
void CannedMessageStore::handleSet(const meshtastic_AdminMessage *request)
|
||||||
|
{
|
||||||
|
// Copy into the correct struct (for writing to flash as protobuf)
|
||||||
|
meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig;
|
||||||
|
strncpy(cannedMessageModuleConfig.messages, request->set_canned_message_module_messages,
|
||||||
|
sizeof(cannedMessageModuleConfig.messages));
|
||||||
|
|
||||||
|
// Ensure the directory exists
|
||||||
|
#ifdef FSCom
|
||||||
|
spiLock->lock();
|
||||||
|
FSCom.mkdir("/prefs");
|
||||||
|
spiLock->unlock();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Write to flash
|
||||||
|
nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size,
|
||||||
|
&meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig);
|
||||||
|
|
||||||
|
// Reload from flash, to update the canned messages in RAM
|
||||||
|
// (This is a lazy way to handle it)
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client API wants to know the current canned messages
|
||||||
|
// We're reconstructing the monolithic canned message string from our copy of the messages in RAM
|
||||||
|
// Lazy, but more convenient that reloading the monolithic string from flash just for this
|
||||||
|
void CannedMessageStore::handleGet(meshtastic_AdminMessage *response)
|
||||||
|
{
|
||||||
|
// Merge the canned messages back into the delimited format expected
|
||||||
|
std::string merged;
|
||||||
|
if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0
|
||||||
|
merged.reserve(201);
|
||||||
|
for (std::string &s : messages) {
|
||||||
|
merged += s;
|
||||||
|
merged += '|';
|
||||||
|
}
|
||||||
|
merged.pop_back(); // Drop the final delimiter (loop added one too many)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place the data into the response
|
||||||
|
// This response is scoped to AdminModule::handleReceivedProtobuf
|
||||||
|
// We were passed reference to it via the observable
|
||||||
|
response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag;
|
||||||
|
strncpy(response->get_canned_message_module_messages_response, merged.c_str(), strlen(merged.c_str()) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
54
src/graphics/niche/Utils/CannedMessageStore.h
Normal file
54
src/graphics/niche/Utils/CannedMessageStore.h
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
Re-usable NicheGraphics tool
|
||||||
|
|
||||||
|
Makes canned message data accessible to any NicheGraphics UI.
|
||||||
|
- handles loading & parsing from flash
|
||||||
|
- handles the admin messages for setting & getting canned messages via client API (phone apps, etc)
|
||||||
|
|
||||||
|
The original CannedMessageModule class is bound to Screen.cpp,
|
||||||
|
making it incompatible with the NicheGraphics framework, which suppresses Screen.cpp
|
||||||
|
|
||||||
|
This implementation aims to be self-contained.
|
||||||
|
The necessary interaction with the AdminModule is done as an observer.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "configuration.h"
|
||||||
|
|
||||||
|
#include "modules/AdminModule.h"
|
||||||
|
|
||||||
|
namespace NicheGraphics
|
||||||
|
{
|
||||||
|
|
||||||
|
class CannedMessageStore
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static CannedMessageStore *getInstance(); // Create or get the singleton instance
|
||||||
|
const std::string &at(uint8_t i); // Get canned message at index
|
||||||
|
uint8_t size(); // Get total number of canned messages
|
||||||
|
|
||||||
|
int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages
|
||||||
|
|
||||||
|
private:
|
||||||
|
CannedMessageStore(); // Constructor made private: force use of CannedMessageStore::instance()
|
||||||
|
|
||||||
|
void load(); // Load from flash, and parse
|
||||||
|
|
||||||
|
void handleSet(const meshtastic_AdminMessage *request); // Client API changing the canned messages
|
||||||
|
void handleGet(meshtastic_AdminMessage *response); // Client API wants to know current canned messages
|
||||||
|
|
||||||
|
std::vector<std::string> messages;
|
||||||
|
|
||||||
|
// Get notified of incoming admin messages, to get / set canned messages
|
||||||
|
CallbackObserver<CannedMessageStore, AdminModule_ObserverData *> adminMessageObserver =
|
||||||
|
CallbackObserver<CannedMessageStore, AdminModule_ObserverData *>(this, &CannedMessageStore::onAdminMessage);
|
||||||
|
};
|
||||||
|
|
||||||
|
}; // namespace NicheGraphics
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -266,6 +266,11 @@ int32_t ButtonThread::runOnce()
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// doesn't handle BUTTON_EVENT_PRESSED_SCREEN BUTTON_EVENT_TOUCH_LONG_PRESSED BUTTON_EVENT_COMBO_SHORT_LONG
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
btnEvent = BUTTON_EVENT_NONE;
|
btnEvent = BUTTON_EVENT_NONE;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef
|
|||||||
this->_pinUp = pinUp;
|
this->_pinUp = pinUp;
|
||||||
this->_pinLeft = pinLeft;
|
this->_pinLeft = pinLeft;
|
||||||
this->_pinRight = pinRight;
|
this->_pinRight = pinRight;
|
||||||
|
this->_pinPress = pinPress;
|
||||||
this->_eventDown = eventDown;
|
this->_eventDown = eventDown;
|
||||||
this->_eventUp = eventUp;
|
this->_eventUp = eventUp;
|
||||||
this->_eventLeft = eventLeft;
|
this->_eventLeft = eventLeft;
|
||||||
@@ -20,23 +21,23 @@ void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLef
|
|||||||
|
|
||||||
if (pinPress != 255) {
|
if (pinPress != 255) {
|
||||||
pinMode(pinPress, INPUT_PULLUP);
|
pinMode(pinPress, INPUT_PULLUP);
|
||||||
attachInterrupt(pinPress, onIntPress, RISING);
|
attachInterrupt(pinPress, onIntPress, TB_DIRECTION);
|
||||||
}
|
}
|
||||||
if (this->_pinDown != 255) {
|
if (this->_pinDown != 255) {
|
||||||
pinMode(this->_pinDown, INPUT_PULLUP);
|
pinMode(this->_pinDown, INPUT_PULLUP);
|
||||||
attachInterrupt(this->_pinDown, onIntDown, RISING);
|
attachInterrupt(this->_pinDown, onIntDown, TB_DIRECTION);
|
||||||
}
|
}
|
||||||
if (this->_pinUp != 255) {
|
if (this->_pinUp != 255) {
|
||||||
pinMode(this->_pinUp, INPUT_PULLUP);
|
pinMode(this->_pinUp, INPUT_PULLUP);
|
||||||
attachInterrupt(this->_pinUp, onIntUp, RISING);
|
attachInterrupt(this->_pinUp, onIntUp, TB_DIRECTION);
|
||||||
}
|
}
|
||||||
if (this->_pinLeft != 255) {
|
if (this->_pinLeft != 255) {
|
||||||
pinMode(this->_pinLeft, INPUT_PULLUP);
|
pinMode(this->_pinLeft, INPUT_PULLUP);
|
||||||
attachInterrupt(this->_pinLeft, onIntLeft, RISING);
|
attachInterrupt(this->_pinLeft, onIntLeft, TB_DIRECTION);
|
||||||
}
|
}
|
||||||
if (this->_pinRight != 255) {
|
if (this->_pinRight != 255) {
|
||||||
pinMode(this->_pinRight, INPUT_PULLUP);
|
pinMode(this->_pinRight, INPUT_PULLUP);
|
||||||
attachInterrupt(this->_pinRight, onIntRight, RISING);
|
attachInterrupt(this->_pinRight, onIntRight, TB_DIRECTION);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight,
|
LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight,
|
||||||
@@ -67,19 +68,19 @@ int32_t TrackballInterruptBase::runOnce()
|
|||||||
e.inputEvent = this->_eventRight;
|
e.inputEvent = this->_eventRight;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
if (this->action == TB_ACTION_PRESSED) {
|
if (this->action == TB_ACTION_PRESSED && !digitalRead(_pinPress)) {
|
||||||
// LOG_DEBUG("Trackball event Press");
|
// LOG_DEBUG("Trackball event Press");
|
||||||
e.inputEvent = this->_eventPressed;
|
e.inputEvent = this->_eventPressed;
|
||||||
} else if (this->action == TB_ACTION_UP) {
|
} else if (this->action == TB_ACTION_UP && !digitalRead(_pinUp)) {
|
||||||
// LOG_DEBUG("Trackball event UP");
|
// LOG_DEBUG("Trackball event UP");
|
||||||
e.inputEvent = this->_eventUp;
|
e.inputEvent = this->_eventUp;
|
||||||
} else if (this->action == TB_ACTION_DOWN) {
|
} else if (this->action == TB_ACTION_DOWN && !digitalRead(_pinDown)) {
|
||||||
// LOG_DEBUG("Trackball event DOWN");
|
// LOG_DEBUG("Trackball event DOWN");
|
||||||
e.inputEvent = this->_eventDown;
|
e.inputEvent = this->_eventDown;
|
||||||
} else if (this->action == TB_ACTION_LEFT) {
|
} else if (this->action == TB_ACTION_LEFT && !digitalRead(_pinLeft)) {
|
||||||
// LOG_DEBUG("Trackball event LEFT");
|
// LOG_DEBUG("Trackball event LEFT");
|
||||||
e.inputEvent = this->_eventLeft;
|
e.inputEvent = this->_eventLeft;
|
||||||
} else if (this->action == TB_ACTION_RIGHT) {
|
} else if (this->action == TB_ACTION_RIGHT && !digitalRead(_pinRight)) {
|
||||||
// LOG_DEBUG("Trackball event RIGHT");
|
// LOG_DEBUG("Trackball event RIGHT");
|
||||||
e.inputEvent = this->_eventRight;
|
e.inputEvent = this->_eventRight;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
#include "InputBroker.h"
|
#include "InputBroker.h"
|
||||||
#include "mesh/NodeDB.h"
|
#include "mesh/NodeDB.h"
|
||||||
|
|
||||||
|
#ifndef TB_DIRECTION
|
||||||
|
#define TB_DIRECTION RISING
|
||||||
|
#endif
|
||||||
|
|
||||||
class TrackballInterruptBase : public Observable<const InputEvent *>, public concurrency::OSThread
|
class TrackballInterruptBase : public Observable<const InputEvent *>, public concurrency::OSThread
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -16,6 +20,7 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
|
|||||||
void intUpHandler();
|
void intUpHandler();
|
||||||
void intLeftHandler();
|
void intLeftHandler();
|
||||||
void intRightHandler();
|
void intRightHandler();
|
||||||
|
uint32_t lastTime = 0;
|
||||||
|
|
||||||
virtual int32_t runOnce() override;
|
virtual int32_t runOnce() override;
|
||||||
|
|
||||||
@@ -28,14 +33,15 @@ class TrackballInterruptBase : public Observable<const InputEvent *>, public con
|
|||||||
TB_ACTION_LEFT,
|
TB_ACTION_LEFT,
|
||||||
TB_ACTION_RIGHT
|
TB_ACTION_RIGHT
|
||||||
};
|
};
|
||||||
|
|
||||||
volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE;
|
|
||||||
|
|
||||||
private:
|
|
||||||
uint8_t _pinDown = 0;
|
uint8_t _pinDown = 0;
|
||||||
uint8_t _pinUp = 0;
|
uint8_t _pinUp = 0;
|
||||||
uint8_t _pinLeft = 0;
|
uint8_t _pinLeft = 0;
|
||||||
uint8_t _pinRight = 0;
|
uint8_t _pinRight = 0;
|
||||||
|
uint8_t _pinPress = 0;
|
||||||
|
|
||||||
|
volatile TrackballInterruptBaseActionType action = TB_ACTION_NONE;
|
||||||
|
|
||||||
|
private:
|
||||||
input_broker_event _eventDown = INPUT_BROKER_NONE;
|
input_broker_event _eventDown = INPUT_BROKER_NONE;
|
||||||
input_broker_event _eventUp = INPUT_BROKER_NONE;
|
input_broker_event _eventUp = INPUT_BROKER_NONE;
|
||||||
input_broker_event _eventLeft = INPUT_BROKER_NONE;
|
input_broker_event _eventLeft = INPUT_BROKER_NONE;
|
||||||
|
|||||||
@@ -23,21 +23,41 @@ void TrackballInterruptImpl1::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLe
|
|||||||
|
|
||||||
void TrackballInterruptImpl1::handleIntDown()
|
void TrackballInterruptImpl1::handleIntDown()
|
||||||
{
|
{
|
||||||
trackballInterruptImpl1->intDownHandler();
|
if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
|
||||||
|
trackballInterruptImpl1->lastTime = millis();
|
||||||
|
trackballInterruptImpl1->intDownHandler();
|
||||||
|
trackballInterruptImpl1->setIntervalFromNow(20);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
void TrackballInterruptImpl1::handleIntUp()
|
void TrackballInterruptImpl1::handleIntUp()
|
||||||
{
|
{
|
||||||
trackballInterruptImpl1->intUpHandler();
|
if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
|
||||||
|
trackballInterruptImpl1->lastTime = millis();
|
||||||
|
trackballInterruptImpl1->intUpHandler();
|
||||||
|
trackballInterruptImpl1->setIntervalFromNow(20);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
void TrackballInterruptImpl1::handleIntLeft()
|
void TrackballInterruptImpl1::handleIntLeft()
|
||||||
{
|
{
|
||||||
trackballInterruptImpl1->intLeftHandler();
|
if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
|
||||||
|
trackballInterruptImpl1->lastTime = millis();
|
||||||
|
trackballInterruptImpl1->intLeftHandler();
|
||||||
|
trackballInterruptImpl1->setIntervalFromNow(20);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
void TrackballInterruptImpl1::handleIntRight()
|
void TrackballInterruptImpl1::handleIntRight()
|
||||||
{
|
{
|
||||||
trackballInterruptImpl1->intRightHandler();
|
if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
|
||||||
|
trackballInterruptImpl1->lastTime = millis();
|
||||||
|
trackballInterruptImpl1->intRightHandler();
|
||||||
|
trackballInterruptImpl1->setIntervalFromNow(20);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
void TrackballInterruptImpl1::handleIntPressed()
|
void TrackballInterruptImpl1::handleIntPressed()
|
||||||
{
|
{
|
||||||
trackballInterruptImpl1->intPressHandler();
|
if (TB_DIRECTION == RISING || millis() > trackballInterruptImpl1->lastTime + 10) {
|
||||||
|
trackballInterruptImpl1->lastTime = millis();
|
||||||
|
trackballInterruptImpl1->intPressHandler();
|
||||||
|
trackballInterruptImpl1->setIntervalFromNow(20);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ UpDownInterruptBase::UpDownInterruptBase(const char *name) : concurrency::OSThre
|
|||||||
|
|
||||||
void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown,
|
void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown,
|
||||||
input_broker_event eventUp, input_broker_event eventPressed, void (*onIntDown)(),
|
input_broker_event eventUp, input_broker_event eventPressed, void (*onIntDown)(),
|
||||||
void (*onIntUp)(), void (*onIntPress)())
|
void (*onIntUp)(), void (*onIntPress)(), unsigned long updownDebounceMs)
|
||||||
{
|
{
|
||||||
this->_pinDown = pinDown;
|
this->_pinDown = pinDown;
|
||||||
this->_pinUp = pinUp;
|
this->_pinUp = pinUp;
|
||||||
@@ -33,16 +33,25 @@ int32_t UpDownInterruptBase::runOnce()
|
|||||||
{
|
{
|
||||||
InputEvent e;
|
InputEvent e;
|
||||||
e.inputEvent = INPUT_BROKER_NONE;
|
e.inputEvent = INPUT_BROKER_NONE;
|
||||||
|
unsigned long now = millis();
|
||||||
if (this->action == UPDOWN_ACTION_PRESSED) {
|
if (this->action == UPDOWN_ACTION_PRESSED) {
|
||||||
LOG_DEBUG("GPIO event Press");
|
if (now - lastPressKeyTime >= pressDebounceMs) {
|
||||||
e.inputEvent = this->_eventPressed;
|
lastPressKeyTime = now;
|
||||||
|
LOG_DEBUG("GPIO event Press");
|
||||||
|
e.inputEvent = this->_eventPressed;
|
||||||
|
}
|
||||||
} else if (this->action == UPDOWN_ACTION_UP) {
|
} else if (this->action == UPDOWN_ACTION_UP) {
|
||||||
LOG_DEBUG("GPIO event Up");
|
if (now - lastUpKeyTime >= updownDebounceMs) {
|
||||||
e.inputEvent = this->_eventUp;
|
lastUpKeyTime = now;
|
||||||
|
LOG_DEBUG("GPIO event Up");
|
||||||
|
e.inputEvent = this->_eventUp;
|
||||||
|
}
|
||||||
} else if (this->action == UPDOWN_ACTION_DOWN) {
|
} else if (this->action == UPDOWN_ACTION_DOWN) {
|
||||||
LOG_DEBUG("GPIO event Down");
|
if (now - lastDownKeyTime >= updownDebounceMs) {
|
||||||
e.inputEvent = this->_eventDown;
|
lastDownKeyTime = now;
|
||||||
|
LOG_DEBUG("GPIO event Down");
|
||||||
|
e.inputEvent = this->_eventDown;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.inputEvent != INPUT_BROKER_NONE) {
|
if (e.inputEvent != INPUT_BROKER_NONE) {
|
||||||
@@ -52,7 +61,6 @@ int32_t UpDownInterruptBase::runOnce()
|
|||||||
}
|
}
|
||||||
|
|
||||||
this->action = UPDOWN_ACTION_NONE;
|
this->action = UPDOWN_ACTION_NONE;
|
||||||
|
|
||||||
return 100;
|
return 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ class UpDownInterruptBase : public Observable<const InputEvent *>, public concur
|
|||||||
public:
|
public:
|
||||||
explicit UpDownInterruptBase(const char *name);
|
explicit UpDownInterruptBase(const char *name);
|
||||||
void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp,
|
void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, input_broker_event eventDown, input_broker_event eventUp,
|
||||||
input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)());
|
input_broker_event eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntPress)(),
|
||||||
|
unsigned long updownDebounceMs = 50);
|
||||||
void intPressHandler();
|
void intPressHandler();
|
||||||
void intDownHandler();
|
void intDownHandler();
|
||||||
void intUpHandler();
|
void intUpHandler();
|
||||||
@@ -27,4 +28,10 @@ class UpDownInterruptBase : public Observable<const InputEvent *>, public concur
|
|||||||
input_broker_event _eventUp = INPUT_BROKER_NONE;
|
input_broker_event _eventUp = INPUT_BROKER_NONE;
|
||||||
input_broker_event _eventPressed = INPUT_BROKER_NONE;
|
input_broker_event _eventPressed = INPUT_BROKER_NONE;
|
||||||
const char *_originName;
|
const char *_originName;
|
||||||
|
|
||||||
|
unsigned long lastUpKeyTime = 0;
|
||||||
|
unsigned long lastDownKeyTime = 0;
|
||||||
|
unsigned long lastPressKeyTime = 0;
|
||||||
|
unsigned long updownDebounceMs = 50;
|
||||||
|
const unsigned long pressDebounceMs = 200;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1422,7 +1422,7 @@ extern meshtastic_DeviceMetadata getDeviceMetadata()
|
|||||||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AUDIO_CONFIG;
|
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AUDIO_CONFIG;
|
||||||
#endif
|
#endif
|
||||||
// Option to explicitly include canned messages for edge cases, e.g. niche graphics
|
// Option to explicitly include canned messages for edge cases, e.g. niche graphics
|
||||||
#if (!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES
|
#if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
|
||||||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG;
|
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG;
|
||||||
#endif
|
#endif
|
||||||
#if NO_EXT_GPIO
|
#if NO_EXT_GPIO
|
||||||
|
|||||||
@@ -61,12 +61,17 @@ class Default
|
|||||||
throttlingFactor = 0.04;
|
throttlingFactor = 0.04;
|
||||||
else if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST)
|
else if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_MEDIUM_FAST)
|
||||||
throttlingFactor = 0.02;
|
throttlingFactor = 0.02;
|
||||||
else if (config.lora.use_preset && config.lora.modem_preset == meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW)
|
|
||||||
throttlingFactor = 0.01;
|
|
||||||
else if (config.lora.use_preset &&
|
else if (config.lora.use_preset &&
|
||||||
IS_ONE_OF(config.lora.modem_preset, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
IS_ONE_OF(config.lora.modem_preset, meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST,
|
||||||
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO))
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_TURBO,
|
||||||
return 1.0; // Don't bother throttling for highest bandwidth presets
|
meshtastic_Config_LoRaConfig_ModemPreset_SHORT_SLOW))
|
||||||
|
throttlingFactor = 0.01;
|
||||||
|
|
||||||
|
#if USERPREFS_EVENT_MODE
|
||||||
|
// If we are in event mode, scale down the throttling factor
|
||||||
|
throttlingFactor = 0.04;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Scaling up traffic based on number of nodes over 40
|
// Scaling up traffic based on number of nodes over 40
|
||||||
int nodesOverForty = (numOnlineNodes - 40);
|
int nodesOverForty = (numOnlineNodes - 40);
|
||||||
return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by 0.075 (default)
|
return 1.0 + (nodesOverForty * throttlingFactor); // Each number of online node scales by 0.075 (default)
|
||||||
|
|||||||
@@ -244,10 +244,13 @@ void setReplyTo(meshtastic_MeshPacket *p, const meshtastic_MeshPacket &to)
|
|||||||
p->decoded.request_id = to.id;
|
p->decoded.request_id = to.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<MeshModule *> MeshModule::GetMeshModulesWithUIFrames()
|
std::vector<MeshModule *> MeshModule::GetMeshModulesWithUIFrames(int startIndex)
|
||||||
{
|
{
|
||||||
|
|
||||||
std::vector<MeshModule *> modulesWithUIFrames;
|
std::vector<MeshModule *> modulesWithUIFrames;
|
||||||
|
|
||||||
|
// Fill with nullptr up to startIndex
|
||||||
|
modulesWithUIFrames.resize(startIndex, nullptr);
|
||||||
|
|
||||||
if (modules) {
|
if (modules) {
|
||||||
for (auto i = modules->begin(); i != modules->end(); ++i) {
|
for (auto i = modules->begin(); i != modules->end(); ++i) {
|
||||||
auto &pi = **i;
|
auto &pi = **i;
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class MeshModule
|
|||||||
*/
|
*/
|
||||||
static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO);
|
static void callModules(meshtastic_MeshPacket &mp, RxSource src = RX_SRC_RADIO);
|
||||||
|
|
||||||
static std::vector<MeshModule *> GetMeshModulesWithUIFrames();
|
static std::vector<MeshModule *> GetMeshModulesWithUIFrames(int startIndex);
|
||||||
static void observeUIEvents(Observer<const UIFrameEvent *> *observer);
|
static void observeUIEvents(Observer<const UIFrameEvent *> *observer);
|
||||||
static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp,
|
static AdminMessageHandleResult handleAdminMessageForAllModules(const meshtastic_MeshPacket &mp,
|
||||||
meshtastic_AdminMessage *request,
|
meshtastic_AdminMessage *request,
|
||||||
|
|||||||
@@ -850,10 +850,12 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role)
|
|||||||
if (role == meshtastic_Config_DeviceConfig_Role_ROUTER) {
|
if (role == meshtastic_Config_DeviceConfig_Role_ROUTER) {
|
||||||
initConfigIntervals();
|
initConfigIntervals();
|
||||||
initModuleConfigIntervals();
|
initModuleConfigIntervals();
|
||||||
|
moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs;
|
||||||
config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY;
|
config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_CORE_PORTNUMS_ONLY;
|
||||||
owner.has_is_unmessagable = true;
|
owner.has_is_unmessagable = true;
|
||||||
owner.is_unmessagable = true;
|
owner.is_unmessagable = true;
|
||||||
} else if (role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) {
|
} else if (role == meshtastic_Config_DeviceConfig_Role_ROUTER_LATE) {
|
||||||
|
moduleConfig.telemetry.device_update_interval = ONE_DAY;
|
||||||
owner.has_is_unmessagable = true;
|
owner.has_is_unmessagable = true;
|
||||||
owner.is_unmessagable = true;
|
owner.is_unmessagable = true;
|
||||||
} else if (role == meshtastic_Config_DeviceConfig_Role_REPEATER) {
|
} else if (role == meshtastic_Config_DeviceConfig_Role_REPEATER) {
|
||||||
@@ -864,6 +866,7 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role)
|
|||||||
} else if (role == meshtastic_Config_DeviceConfig_Role_SENSOR) {
|
} else if (role == meshtastic_Config_DeviceConfig_Role_SENSOR) {
|
||||||
owner.has_is_unmessagable = true;
|
owner.has_is_unmessagable = true;
|
||||||
owner.is_unmessagable = true;
|
owner.is_unmessagable = true;
|
||||||
|
moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs;
|
||||||
moduleConfig.telemetry.environment_measurement_enabled = true;
|
moduleConfig.telemetry.environment_measurement_enabled = true;
|
||||||
moduleConfig.telemetry.environment_update_interval = 300;
|
moduleConfig.telemetry.environment_update_interval = 300;
|
||||||
} else if (role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) {
|
} else if (role == meshtastic_Config_DeviceConfig_Role_LOST_AND_FOUND) {
|
||||||
@@ -881,6 +884,7 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role)
|
|||||||
} else if (role == meshtastic_Config_DeviceConfig_Role_TRACKER) {
|
} else if (role == meshtastic_Config_DeviceConfig_Role_TRACKER) {
|
||||||
owner.has_is_unmessagable = true;
|
owner.has_is_unmessagable = true;
|
||||||
owner.is_unmessagable = true;
|
owner.is_unmessagable = true;
|
||||||
|
moduleConfig.telemetry.device_update_interval = default_telemetry_broadcast_interval_secs;
|
||||||
} else if (role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) {
|
} else if (role == meshtastic_Config_DeviceConfig_Role_TAK_TRACKER) {
|
||||||
owner.has_is_unmessagable = true;
|
owner.has_is_unmessagable = true;
|
||||||
owner.is_unmessagable = true;
|
owner.is_unmessagable = true;
|
||||||
@@ -910,7 +914,11 @@ void NodeDB::installRoleDefaults(meshtastic_Config_DeviceConfig_Role role)
|
|||||||
void NodeDB::initModuleConfigIntervals()
|
void NodeDB::initModuleConfigIntervals()
|
||||||
{
|
{
|
||||||
// Zero out telemetry intervals so that they coalesce to defaults in Default.h
|
// Zero out telemetry intervals so that they coalesce to defaults in Default.h
|
||||||
moduleConfig.telemetry.device_update_interval = 0;
|
#ifdef USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL
|
||||||
|
moduleConfig.telemetry.device_update_interval = USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL;
|
||||||
|
#else
|
||||||
|
moduleConfig.telemetry.device_update_interval = UINT32_MAX;
|
||||||
|
#endif
|
||||||
moduleConfig.telemetry.environment_update_interval = 0;
|
moduleConfig.telemetry.environment_update_interval = 0;
|
||||||
moduleConfig.telemetry.air_quality_interval = 0;
|
moduleConfig.telemetry.air_quality_interval = 0;
|
||||||
moduleConfig.telemetry.power_update_interval = 0;
|
moduleConfig.telemetry.power_update_interval = 0;
|
||||||
@@ -1558,6 +1566,7 @@ void NodeDB::addFromContact(meshtastic_SharedContact contact)
|
|||||||
// Mark the node's key as manually verified to indicate trustworthiness.
|
// Mark the node's key as manually verified to indicate trustworthiness.
|
||||||
updateGUIforNode = info;
|
updateGUIforNode = info;
|
||||||
// powerFSM.trigger(EVENT_NODEDB_UPDATED); This event has been retired
|
// powerFSM.trigger(EVENT_NODEDB_UPDATED); This event has been retired
|
||||||
|
sortMeshDB();
|
||||||
notifyObservers(true); // Force an update whether or not our node counts have changed
|
notifyObservers(true); // Force an update whether or not our node counts have changed
|
||||||
}
|
}
|
||||||
saveNodeDatabaseToDisk();
|
saveNodeDatabaseToDisk();
|
||||||
@@ -1661,6 +1670,31 @@ void NodeDB::updateFrom(const meshtastic_MeshPacket &mp)
|
|||||||
info->has_hops_away = true;
|
info->has_hops_away = true;
|
||||||
info->hops_away = mp.hop_start - mp.hop_limit;
|
info->hops_away = mp.hop_start - mp.hop_limit;
|
||||||
}
|
}
|
||||||
|
sortMeshDB();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NodeDB::sortMeshDB()
|
||||||
|
{
|
||||||
|
if (!Throttle::isWithinTimespanMs(lastSort, 1000 * 5)) {
|
||||||
|
lastSort = millis();
|
||||||
|
std::sort(meshNodes->begin(), meshNodes->end(), [](const meshtastic_NodeInfoLite &a, const meshtastic_NodeInfoLite &b) {
|
||||||
|
if (a.num == myNodeInfo.my_node_num) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (b.num == myNodeInfo.my_node_num) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool aFav = a.is_favorite;
|
||||||
|
bool bFav = b.is_favorite;
|
||||||
|
if (aFav != bFav)
|
||||||
|
return aFav;
|
||||||
|
if (a.last_heard == 0 || a.last_heard == UINT32_MAX)
|
||||||
|
return false;
|
||||||
|
if (b.last_heard == 0 || b.last_heard == UINT32_MAX)
|
||||||
|
return true;
|
||||||
|
return a.last_heard > b.last_heard;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -282,6 +282,7 @@ class NodeDB
|
|||||||
bool duplicateWarned = false;
|
bool duplicateWarned = false;
|
||||||
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash
|
uint32_t lastNodeDbSave = 0; // when we last saved our db to flash
|
||||||
uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually
|
uint32_t lastBackupAttempt = 0; // when we last tried a backup automatically or manually
|
||||||
|
uint32_t lastSort = 0; // When last sorted the nodeDB
|
||||||
/// Find a node in our DB, create an empty NodeInfoLite if missing
|
/// Find a node in our DB, create an empty NodeInfoLite if missing
|
||||||
meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n);
|
meshtastic_NodeInfoLite *getOrCreateMeshNode(NodeNum n);
|
||||||
|
|
||||||
@@ -310,6 +311,7 @@ class NodeDB
|
|||||||
bool saveChannelsToDisk();
|
bool saveChannelsToDisk();
|
||||||
bool saveDeviceStateToDisk();
|
bool saveDeviceStateToDisk();
|
||||||
bool saveNodeDatabaseToDisk();
|
bool saveNodeDatabaseToDisk();
|
||||||
|
void sortMeshDB();
|
||||||
};
|
};
|
||||||
|
|
||||||
extern NodeDB *nodeDB;
|
extern NodeDB *nodeDB;
|
||||||
|
|||||||
@@ -91,7 +91,9 @@ typedef enum _meshtastic_TelemetrySensorType {
|
|||||||
/* MAX17261 lipo battery gauge */
|
/* MAX17261 lipo battery gauge */
|
||||||
meshtastic_TelemetrySensorType_MAX17261 = 38,
|
meshtastic_TelemetrySensorType_MAX17261 = 38,
|
||||||
/* PCT2075 Temperature Sensor */
|
/* PCT2075 Temperature Sensor */
|
||||||
meshtastic_TelemetrySensorType_PCT2075 = 39
|
meshtastic_TelemetrySensorType_PCT2075 = 39,
|
||||||
|
/* ADS1X15 ADC */
|
||||||
|
meshtastic_TelemetrySensorType_ADS1X15 = 40
|
||||||
} meshtastic_TelemetrySensorType;
|
} meshtastic_TelemetrySensorType;
|
||||||
|
|
||||||
/* Struct definitions */
|
/* Struct definitions */
|
||||||
@@ -206,6 +208,36 @@ typedef struct _meshtastic_PowerMetrics {
|
|||||||
/* Current (Ch3) */
|
/* Current (Ch3) */
|
||||||
bool has_ch3_current;
|
bool has_ch3_current;
|
||||||
float ch3_current;
|
float ch3_current;
|
||||||
|
/* Voltage (Ch4) */
|
||||||
|
bool has_ch4_voltage;
|
||||||
|
float ch4_voltage;
|
||||||
|
/* Current (Ch4) */
|
||||||
|
bool has_ch4_current;
|
||||||
|
float ch4_current;
|
||||||
|
/* Voltage (Ch5) */
|
||||||
|
bool has_ch5_voltage;
|
||||||
|
float ch5_voltage;
|
||||||
|
/* Current (Ch5) */
|
||||||
|
bool has_ch5_current;
|
||||||
|
float ch5_current;
|
||||||
|
/* Voltage (Ch6) */
|
||||||
|
bool has_ch6_voltage;
|
||||||
|
float ch6_voltage;
|
||||||
|
/* Current (Ch6) */
|
||||||
|
bool has_ch6_current;
|
||||||
|
float ch6_current;
|
||||||
|
/* Voltage (Ch7) */
|
||||||
|
bool has_ch7_voltage;
|
||||||
|
float ch7_voltage;
|
||||||
|
/* Current (Ch7) */
|
||||||
|
bool has_ch7_current;
|
||||||
|
float ch7_current;
|
||||||
|
/* Voltage (Ch8) */
|
||||||
|
bool has_ch8_voltage;
|
||||||
|
float ch8_voltage;
|
||||||
|
/* Current (Ch8) */
|
||||||
|
bool has_ch8_current;
|
||||||
|
float ch8_current;
|
||||||
} meshtastic_PowerMetrics;
|
} meshtastic_PowerMetrics;
|
||||||
|
|
||||||
/* Air quality metrics */
|
/* Air quality metrics */
|
||||||
@@ -360,8 +392,8 @@ extern "C" {
|
|||||||
|
|
||||||
/* Helper constants for enums */
|
/* Helper constants for enums */
|
||||||
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
|
#define _meshtastic_TelemetrySensorType_MIN meshtastic_TelemetrySensorType_SENSOR_UNSET
|
||||||
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_PCT2075
|
#define _meshtastic_TelemetrySensorType_MAX meshtastic_TelemetrySensorType_ADS1X15
|
||||||
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_PCT2075+1))
|
#define _meshtastic_TelemetrySensorType_ARRAYSIZE ((meshtastic_TelemetrySensorType)(meshtastic_TelemetrySensorType_ADS1X15+1))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -376,7 +408,7 @@ extern "C" {
|
|||||||
/* Initializer values for message structs */
|
/* Initializer values for message structs */
|
||||||
#define meshtastic_DeviceMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0}
|
#define meshtastic_DeviceMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||||
#define meshtastic_EnvironmentMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
#define meshtastic_EnvironmentMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||||
#define meshtastic_PowerMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
#define meshtastic_PowerMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||||
#define meshtastic_AirQualityMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
#define meshtastic_AirQualityMetrics_init_default {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||||
#define meshtastic_LocalStats_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
#define meshtastic_LocalStats_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_HealthMetrics_init_default {false, 0, false, 0, false, 0}
|
#define meshtastic_HealthMetrics_init_default {false, 0, false, 0, false, 0}
|
||||||
@@ -385,7 +417,7 @@ extern "C" {
|
|||||||
#define meshtastic_Nau7802Config_init_default {0, 0}
|
#define meshtastic_Nau7802Config_init_default {0, 0}
|
||||||
#define meshtastic_DeviceMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0}
|
#define meshtastic_DeviceMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||||
#define meshtastic_EnvironmentMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
#define meshtastic_EnvironmentMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||||
#define meshtastic_PowerMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
#define meshtastic_PowerMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||||
#define meshtastic_AirQualityMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
#define meshtastic_AirQualityMetrics_init_zero {false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0, false, 0}
|
||||||
#define meshtastic_LocalStats_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
#define meshtastic_LocalStats_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
#define meshtastic_HealthMetrics_init_zero {false, 0, false, 0, false, 0}
|
#define meshtastic_HealthMetrics_init_zero {false, 0, false, 0, false, 0}
|
||||||
@@ -427,6 +459,16 @@ extern "C" {
|
|||||||
#define meshtastic_PowerMetrics_ch2_current_tag 4
|
#define meshtastic_PowerMetrics_ch2_current_tag 4
|
||||||
#define meshtastic_PowerMetrics_ch3_voltage_tag 5
|
#define meshtastic_PowerMetrics_ch3_voltage_tag 5
|
||||||
#define meshtastic_PowerMetrics_ch3_current_tag 6
|
#define meshtastic_PowerMetrics_ch3_current_tag 6
|
||||||
|
#define meshtastic_PowerMetrics_ch4_voltage_tag 7
|
||||||
|
#define meshtastic_PowerMetrics_ch4_current_tag 8
|
||||||
|
#define meshtastic_PowerMetrics_ch5_voltage_tag 9
|
||||||
|
#define meshtastic_PowerMetrics_ch5_current_tag 10
|
||||||
|
#define meshtastic_PowerMetrics_ch6_voltage_tag 11
|
||||||
|
#define meshtastic_PowerMetrics_ch6_current_tag 12
|
||||||
|
#define meshtastic_PowerMetrics_ch7_voltage_tag 13
|
||||||
|
#define meshtastic_PowerMetrics_ch7_current_tag 14
|
||||||
|
#define meshtastic_PowerMetrics_ch8_voltage_tag 15
|
||||||
|
#define meshtastic_PowerMetrics_ch8_current_tag 16
|
||||||
#define meshtastic_AirQualityMetrics_pm10_standard_tag 1
|
#define meshtastic_AirQualityMetrics_pm10_standard_tag 1
|
||||||
#define meshtastic_AirQualityMetrics_pm25_standard_tag 2
|
#define meshtastic_AirQualityMetrics_pm25_standard_tag 2
|
||||||
#define meshtastic_AirQualityMetrics_pm100_standard_tag 3
|
#define meshtastic_AirQualityMetrics_pm100_standard_tag 3
|
||||||
@@ -518,7 +560,17 @@ X(a, STATIC, OPTIONAL, FLOAT, ch1_current, 2) \
|
|||||||
X(a, STATIC, OPTIONAL, FLOAT, ch2_voltage, 3) \
|
X(a, STATIC, OPTIONAL, FLOAT, ch2_voltage, 3) \
|
||||||
X(a, STATIC, OPTIONAL, FLOAT, ch2_current, 4) \
|
X(a, STATIC, OPTIONAL, FLOAT, ch2_current, 4) \
|
||||||
X(a, STATIC, OPTIONAL, FLOAT, ch3_voltage, 5) \
|
X(a, STATIC, OPTIONAL, FLOAT, ch3_voltage, 5) \
|
||||||
X(a, STATIC, OPTIONAL, FLOAT, ch3_current, 6)
|
X(a, STATIC, OPTIONAL, FLOAT, ch3_current, 6) \
|
||||||
|
X(a, STATIC, OPTIONAL, FLOAT, ch4_voltage, 7) \
|
||||||
|
X(a, STATIC, OPTIONAL, FLOAT, ch4_current, 8) \
|
||||||
|
X(a, STATIC, OPTIONAL, FLOAT, ch5_voltage, 9) \
|
||||||
|
X(a, STATIC, OPTIONAL, FLOAT, ch5_current, 10) \
|
||||||
|
X(a, STATIC, OPTIONAL, FLOAT, ch6_voltage, 11) \
|
||||||
|
X(a, STATIC, OPTIONAL, FLOAT, ch6_current, 12) \
|
||||||
|
X(a, STATIC, OPTIONAL, FLOAT, ch7_voltage, 13) \
|
||||||
|
X(a, STATIC, OPTIONAL, FLOAT, ch7_current, 14) \
|
||||||
|
X(a, STATIC, OPTIONAL, FLOAT, ch8_voltage, 15) \
|
||||||
|
X(a, STATIC, OPTIONAL, FLOAT, ch8_current, 16)
|
||||||
#define meshtastic_PowerMetrics_CALLBACK NULL
|
#define meshtastic_PowerMetrics_CALLBACK NULL
|
||||||
#define meshtastic_PowerMetrics_DEFAULT NULL
|
#define meshtastic_PowerMetrics_DEFAULT NULL
|
||||||
|
|
||||||
@@ -631,7 +683,7 @@ extern const pb_msgdesc_t meshtastic_Nau7802Config_msg;
|
|||||||
#define meshtastic_HostMetrics_size 264
|
#define meshtastic_HostMetrics_size 264
|
||||||
#define meshtastic_LocalStats_size 72
|
#define meshtastic_LocalStats_size 72
|
||||||
#define meshtastic_Nau7802Config_size 16
|
#define meshtastic_Nau7802Config_size 16
|
||||||
#define meshtastic_PowerMetrics_size 30
|
#define meshtastic_PowerMetrics_size 81
|
||||||
#define meshtastic_Telemetry_size 272
|
#define meshtastic_Telemetry_size 272
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
@@ -470,22 +470,38 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
|||||||
setPassKey(&res);
|
setPassKey(&res);
|
||||||
myReply = allocDataProtobuf(res);
|
myReply = allocDataProtobuf(res);
|
||||||
} else if (mp.decoded.want_response) {
|
} else if (mp.decoded.want_response) {
|
||||||
LOG_DEBUG("Did not responded to a request that wanted a respond. req.variant=%d", r->which_payload_variant);
|
LOG_DEBUG("Module API did not respond to admin message. req.variant=%d", r->which_payload_variant);
|
||||||
} else if (handleResult != AdminMessageHandleResult::HANDLED) {
|
} else if (handleResult != AdminMessageHandleResult::HANDLED) {
|
||||||
// Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages
|
// Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages
|
||||||
LOG_DEBUG("Ignore irrelevant admin %d", r->which_payload_variant);
|
LOG_DEBUG("Module API did not handle admin message %d", r->which_payload_variant);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Allow any observers (e.g. the UI) to handle/respond
|
||||||
|
AdminMessageHandleResult observerResult = AdminMessageHandleResult::NOT_HANDLED;
|
||||||
|
meshtastic_AdminMessage observerResponse = meshtastic_AdminMessage_init_default;
|
||||||
|
AdminModule_ObserverData observerData = {
|
||||||
|
.request = r,
|
||||||
|
.response = &observerResponse,
|
||||||
|
.result = &observerResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
notifyObservers(&observerData);
|
||||||
|
|
||||||
|
if (observerResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) {
|
||||||
|
setPassKey(&observerResponse);
|
||||||
|
myReply = allocDataProtobuf(observerResponse);
|
||||||
|
LOG_DEBUG("Observer responded to admin message");
|
||||||
|
} else if (observerResult == AdminMessageHandleResult::HANDLED) {
|
||||||
|
LOG_DEBUG("Observer handled admin message");
|
||||||
|
}
|
||||||
|
|
||||||
// If asked for a response and it is not yet set, generate an 'ACK' response
|
// If asked for a response and it is not yet set, generate an 'ACK' response
|
||||||
if (mp.decoded.want_response && !myReply) {
|
if (mp.decoded.want_response && !myReply) {
|
||||||
myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp);
|
myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow any observers (e.g. the UI) to respond to this event
|
|
||||||
notifyObservers(r);
|
|
||||||
|
|
||||||
return handled;
|
return handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1137,7 +1153,7 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r
|
|||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
conn.has_serial = true; // No serial-less devices
|
conn.has_serial = true; // No serial-less devices
|
||||||
#if !EXCLUDE_POWER_FSM
|
#if !MESHTASTIC_EXCLUDE_POWER_FSM
|
||||||
conn.serial.is_connected = powerFSM.getState() == &stateSERIAL;
|
conn.serial.is_connected = powerFSM.getState() == &stateSERIAL;
|
||||||
#else
|
#else
|
||||||
conn.serial.is_connected = powerFSM.getState();
|
conn.serial.is_connected = powerFSM.getState();
|
||||||
|
|||||||
@@ -6,10 +6,19 @@
|
|||||||
#include "mesh/wifi/WiFiAPClient.h"
|
#include "mesh/wifi/WiFiAPClient.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Datatype passed to Observers by AdminModule, to allow external handling of admin messages
|
||||||
|
*/
|
||||||
|
struct AdminModule_ObserverData {
|
||||||
|
const meshtastic_AdminMessage *request;
|
||||||
|
meshtastic_AdminMessage *response;
|
||||||
|
AdminMessageHandleResult *result;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Admin module for admin messages
|
* Admin module for admin messages
|
||||||
*/
|
*/
|
||||||
class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Observable<const meshtastic_AdminMessage *>
|
class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Observable<AdminModule_ObserverData *>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
/** Constructor
|
/** Constructor
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ int CannedMessageModule::splitConfiguredMessages()
|
|||||||
}
|
}
|
||||||
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 (graphics::isHighResolution) {
|
||||||
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));
|
||||||
} else {
|
} else {
|
||||||
@@ -245,12 +245,15 @@ void CannedMessageModule::updateDestinationSelectionList()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* As the nodeDB is sorted, can skip this step
|
||||||
// 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 && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) {
|
if (nodesChanged && runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION) {
|
||||||
@@ -387,6 +390,7 @@ bool CannedMessageModule::handleTabSwitch(const InputEvent *event)
|
|||||||
// RESTORE THIS!
|
// RESTORE THIS!
|
||||||
if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION)
|
if (runState == CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION)
|
||||||
updateDestinationSelectionList();
|
updateDestinationSelectionList();
|
||||||
|
requestFocus();
|
||||||
|
|
||||||
UIFrameEvent e;
|
UIFrameEvent e;
|
||||||
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
||||||
@@ -986,6 +990,7 @@ int32_t CannedMessageModule::runOnce()
|
|||||||
default:
|
default:
|
||||||
// Only insert ASCII printable characters (32–126)
|
// Only insert ASCII printable characters (32–126)
|
||||||
if (this->payload >= 32 && this->payload <= 126) {
|
if (this->payload >= 32 && this->payload <= 126) {
|
||||||
|
requestFocus();
|
||||||
if (this->cursor == this->freetext.length()) {
|
if (this->cursor == this->freetext.length()) {
|
||||||
this->freetext += (char)this->payload;
|
this->freetext += (char)this->payload;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -79,10 +79,10 @@ bool KeyVerificationModule::handleReceivedProtobuf(const meshtastic_MeshPacket &
|
|||||||
memset(message, 0, sizeof(message));
|
memset(message, 0, sizeof(message));
|
||||||
sprintf(message, "Verification: \n");
|
sprintf(message, "Verification: \n");
|
||||||
generateVerificationCode(message + 15);
|
generateVerificationCode(message + 15);
|
||||||
sprintf(message + 24, "\nACCEPT\nREJECT");
|
static const char *optionsArray[] = {"ACCEPT", "REJECT"};
|
||||||
LOG_INFO("Hash1 matches!");
|
LOG_INFO("Hash1 matches!");
|
||||||
if (screen) {
|
if (screen) {
|
||||||
screen->showOverlayBanner(message, 30000, 2, [=](int selected) {
|
screen->showOverlayBanner(message, 30000, optionsArray, 2, [=](int selected) {
|
||||||
if (selected == 0) {
|
if (selected == 0) {
|
||||||
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
auto remoteNodePtr = nodeDB->getMeshNode(currentRemoteNode);
|
||||||
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
remoteNodePtr->bitfield |= NODEINFO_BITFIELD_IS_KEY_MANUALLY_VERIFIED_MASK;
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
|
|||||||
case INPUT_BROKER_SEND_PING:
|
case INPUT_BROKER_SEND_PING:
|
||||||
service->refreshLocalMeshNode();
|
service->refreshLocalMeshNode();
|
||||||
if (service->trySendPosition(NODENUM_BROADCAST, true)) {
|
if (service->trySendPosition(NODENUM_BROADCAST, true)) {
|
||||||
IF_SCREEN(screen->showOverlayBanner("Position\nUpdate Sent", 3000));
|
IF_SCREEN(screen->showOverlayBanner("Position\nSent", 3000));
|
||||||
} else {
|
} else {
|
||||||
IF_SCREEN(screen->showOverlayBanner("Node Info\nUpdate Sent", 3000));
|
IF_SCREEN(screen->showOverlayBanner("Node Info\nSent", 3000));
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
// Power control
|
// Power control
|
||||||
@@ -113,6 +113,10 @@ int SystemCommandsModule::handleInputEvent(const InputEvent *event)
|
|||||||
shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000;
|
shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000;
|
||||||
// runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
// runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// No other input events handled here
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr);
|
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only);
|
||||||
}
|
}
|
||||||
#if __has_include(<Adafruit_AHTX0.h>)
|
#if __has_include(<Adafruit_AHTX0.h>)
|
||||||
#include "Sensor/AHT10.h"
|
#include "Sensor/AHT10.h"
|
||||||
@@ -358,7 +358,7 @@ void EnvironmentTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiSt
|
|||||||
int line = 1;
|
int line = 1;
|
||||||
|
|
||||||
// === Set Title
|
// === Set Title
|
||||||
const char *titleStr = (SCREEN_WIDTH > 128) ? "Environment" : "Env.";
|
const char *titleStr = (graphics::isHighResolution) ? "Environment" : "Env.";
|
||||||
|
|
||||||
// === Header ===
|
// === Header ===
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
namespace graphics
|
namespace graphics
|
||||||
{
|
{
|
||||||
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr);
|
extern void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool battery_only);
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t PowerTelemetryModule::runOnce()
|
int32_t PowerTelemetryModule::runOnce()
|
||||||
@@ -115,7 +115,7 @@ void PowerTelemetryModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *s
|
|||||||
int line = 1;
|
int line = 1;
|
||||||
|
|
||||||
// === Set Title
|
// === Set Title
|
||||||
const char *titleStr = (SCREEN_WIDTH > 128) ? "Power Telem." : "Power";
|
const char *titleStr = (graphics::isHighResolution) ? "Power Telem." : "Power";
|
||||||
|
|
||||||
// === Header ===
|
// === Header ===
|
||||||
graphics::drawCommonHeader(display, x, y, titleStr);
|
graphics::drawCommonHeader(display, x, y, titleStr);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
inline const std::unordered_map<std::string, std::string> configProducts = {{"MESHTOAD", "lora-usb-meshtoad-e22.yaml"},
|
inline const std::unordered_map<std::string, std::string> configProducts = {{"MESHTOAD", "lora-usb-meshtoad-e22.yaml"},
|
||||||
{"MESHSTICK", "lora-meshstick-1262.yaml"},
|
{"MESHSTICK", "lora-meshstick-1262.yaml"},
|
||||||
{"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"},
|
{"MESHADV-PI", "lora-MeshAdv-900M30S.yaml"},
|
||||||
{"MESHADV-MINI", "lora-MeshAdv-Mini-900M22S.yaml"},
|
{"MeshAdv Mini", "lora-MeshAdv-Mini-900M22S.yaml"},
|
||||||
{"POWERPI", "lora-MeshAdv-900M30S.yaml"}};
|
{"POWERPI", "lora-MeshAdv-900M30S.yaml"}};
|
||||||
|
|
||||||
enum configNames {
|
enum configNames {
|
||||||
|
|||||||
@@ -79,7 +79,8 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
|||||||
msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity);
|
msgPayload["relative_humidity"] = new JSONValue(decoded->variant.environment_metrics.relative_humidity);
|
||||||
}
|
}
|
||||||
if (decoded->variant.environment_metrics.has_barometric_pressure) {
|
if (decoded->variant.environment_metrics.has_barometric_pressure) {
|
||||||
msgPayload["barometric_pressure"] = new JSONValue(decoded->variant.environment_metrics.barometric_pressure);
|
msgPayload["barometric_pressure"] =
|
||||||
|
new JSONValue(decoded->variant.environment_metrics.barometric_pressure);
|
||||||
}
|
}
|
||||||
if (decoded->variant.environment_metrics.has_gas_resistance) {
|
if (decoded->variant.environment_metrics.has_gas_resistance) {
|
||||||
msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance);
|
msgPayload["gas_resistance"] = new JSONValue(decoded->variant.environment_metrics.gas_resistance);
|
||||||
@@ -125,13 +126,16 @@ std::string MeshPacketSerializer::JsonSerialize(const meshtastic_MeshPacket *mp,
|
|||||||
msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard);
|
msgPayload["pm100"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_standard);
|
||||||
}
|
}
|
||||||
if (decoded->variant.air_quality_metrics.has_pm10_environmental) {
|
if (decoded->variant.air_quality_metrics.has_pm10_environmental) {
|
||||||
msgPayload["pm10_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental);
|
msgPayload["pm10_e"] =
|
||||||
|
new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm10_environmental);
|
||||||
}
|
}
|
||||||
if (decoded->variant.air_quality_metrics.has_pm25_environmental) {
|
if (decoded->variant.air_quality_metrics.has_pm25_environmental) {
|
||||||
msgPayload["pm25_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental);
|
msgPayload["pm25_e"] =
|
||||||
|
new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm25_environmental);
|
||||||
}
|
}
|
||||||
if (decoded->variant.air_quality_metrics.has_pm100_environmental) {
|
if (decoded->variant.air_quality_metrics.has_pm100_environmental) {
|
||||||
msgPayload["pm100_e"] = new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental);
|
msgPayload["pm100_e"] =
|
||||||
|
new JSONValue((unsigned int)decoded->variant.air_quality_metrics.pm100_environmental);
|
||||||
}
|
}
|
||||||
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
|
} else if (decoded->which_variant == meshtastic_Telemetry_power_metrics_tag) {
|
||||||
if (decoded->variant.power_metrics.has_ch1_voltage) {
|
if (decoded->variant.power_metrics.has_ch1_voltage) {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
// "USERPREFS_CONFIG_SMART_POSITION_ENABLED": "false",
|
// "USERPREFS_CONFIG_SMART_POSITION_ENABLED": "false",
|
||||||
// "USERPREFS_CONFIG_GPS_UPDATE_INTERVAL": "600",
|
// "USERPREFS_CONFIG_GPS_UPDATE_INTERVAL": "600",
|
||||||
// "USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL": "1800",
|
// "USERPREFS_CONFIG_POSITION_BROADCAST_INTERVAL": "1800",
|
||||||
|
// "USERPREFS_CONFIG_DEVICE_TELEM_UPDATE_INTERVAL": "900", // Device telemetry update interval in seconds
|
||||||
// "USERPREFS_LORACONFIG_CHANNEL_NUM": "31",
|
// "USERPREFS_LORACONFIG_CHANNEL_NUM": "31",
|
||||||
// "USERPREFS_LORACONFIG_MODEM_PRESET": "meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST",
|
// "USERPREFS_LORACONFIG_MODEM_PRESET": "meshtastic_Config_LoRaConfig_ModemPreset_SHORT_FAST",
|
||||||
// "USERPREFS_USE_ADMIN_KEY_0": "{ 0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a, 0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c }",
|
// "USERPREFS_USE_ADMIN_KEY_0": "{ 0xcd, 0xc0, 0xb4, 0x3c, 0x53, 0x24, 0xdf, 0x13, 0xca, 0x5a, 0xa6, 0x0c, 0x0d, 0xec, 0x85, 0x5a, 0x4c, 0xf6, 0x1a, 0x96, 0x04, 0x1a, 0x3e, 0xfc, 0xbb, 0x8e, 0x33, 0x71, 0xe5, 0xfc, 0xff, 0x3c }",
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ build_flags =
|
|||||||
${inkhud.build_flags}
|
${inkhud.build_flags}
|
||||||
-I variants/heltec_vision_master_e213
|
-I variants/heltec_vision_master_e213
|
||||||
-D HELTEC_VISION_MASTER_E213
|
-D HELTEC_VISION_MASTER_E213
|
||||||
-D MAX_THREADS=40 ; Required if used with WiFi
|
|
||||||
lib_deps =
|
lib_deps =
|
||||||
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
|
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
|
||||||
${esp32s3_base.lib_deps}
|
${esp32s3_base.lib_deps}
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ build_flags =
|
|||||||
${inkhud.build_flags}
|
${inkhud.build_flags}
|
||||||
-I variants/heltec_vision_master_e290
|
-I variants/heltec_vision_master_e290
|
||||||
-D HELTEC_VISION_MASTER_E290
|
-D HELTEC_VISION_MASTER_E290
|
||||||
-D MAX_THREADS=40 ; Required if used with WiFi
|
|
||||||
lib_deps =
|
lib_deps =
|
||||||
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
|
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
|
||||||
${esp32s3_base.lib_deps}
|
${esp32s3_base.lib_deps}
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ build_flags =
|
|||||||
${inkhud.build_flags}
|
${inkhud.build_flags}
|
||||||
-I variants/heltec_wireless_paper
|
-I variants/heltec_wireless_paper
|
||||||
-D HELTEC_WIRELESS_PAPER
|
-D HELTEC_WIRELESS_PAPER
|
||||||
-D MAX_THREADS=40 ; Required if used with WiFi
|
|
||||||
lib_deps =
|
lib_deps =
|
||||||
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
|
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
|
||||||
${esp32s3_base.lib_deps}
|
${esp32s3_base.lib_deps}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ build_flags = ${esp32s3_base.build_flags}
|
|||||||
-D USE_LOG_DEBUG
|
-D USE_LOG_DEBUG
|
||||||
-D LOG_DEBUG_INC=\"DebugConfiguration.h\"
|
-D LOG_DEBUG_INC=\"DebugConfiguration.h\"
|
||||||
-D RADIOLIB_SPI_PARANOID=0
|
-D RADIOLIB_SPI_PARANOID=0
|
||||||
-D MAX_THREADS=40
|
|
||||||
-D HAS_SCREEN=0
|
-D HAS_SCREEN=0
|
||||||
-D HAS_TFT=1
|
-D HAS_TFT=1
|
||||||
-D USE_PIN_BUZZER
|
-D USE_PIN_BUZZER
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ lib_deps =
|
|||||||
${native_base.lib_deps}
|
${native_base.lib_deps}
|
||||||
${device-ui_base.lib_deps}
|
${device-ui_base.lib_deps}
|
||||||
build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunction-sections -fdata-sections -Wl,--gc-sections
|
build_flags = ${native_base.build_flags} -Os -lX11 -linput -lxkbcommon -ffunction-sections -fdata-sections -Wl,--gc-sections
|
||||||
-D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
|
|
||||||
-D RAM_SIZE=16384
|
-D RAM_SIZE=16384
|
||||||
-D USE_X11=1
|
-D USE_X11=1
|
||||||
-D HAS_TFT=1
|
-D HAS_TFT=1
|
||||||
@@ -51,7 +50,6 @@ lib_deps =
|
|||||||
${device-ui_base.lib_deps}
|
${device-ui_base.lib_deps}
|
||||||
board_level = extra
|
board_level = extra
|
||||||
build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections -Wl,--gc-sections
|
build_flags = ${native_base.build_flags} -Os -ffunction-sections -fdata-sections -Wl,--gc-sections
|
||||||
-D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
|
|
||||||
-D RAM_SIZE=8192
|
-D RAM_SIZE=8192
|
||||||
-D USE_FRAMEBUFFER=1
|
-D USE_FRAMEBUFFER=1
|
||||||
-D LV_COLOR_DEPTH=32
|
-D LV_COLOR_DEPTH=32
|
||||||
@@ -81,7 +79,6 @@ lib_deps =
|
|||||||
${device-ui_base.lib_deps}
|
${device-ui_base.lib_deps}
|
||||||
board_level = extra
|
board_level = extra
|
||||||
build_flags = ${native_base.build_flags} -O0 -fsanitize=address -lX11 -linput -lxkbcommon
|
build_flags = ${native_base.build_flags} -O0 -fsanitize=address -lX11 -linput -lxkbcommon
|
||||||
-D MESHTASTIC_EXCLUDE_CANNEDMESSAGES=1
|
|
||||||
-D DEBUG_HEAP
|
-D DEBUG_HEAP
|
||||||
-D RAM_SIZE=16384
|
-D RAM_SIZE=16384
|
||||||
-D USE_X11=1
|
-D USE_X11=1
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ static const uint8_t A7 = PIN_A7;
|
|||||||
#define ADC_RESOLUTION 14
|
#define ADC_RESOLUTION 14
|
||||||
|
|
||||||
// Other pins
|
// Other pins
|
||||||
#define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT
|
#define WB_I2C1_SDA (13) // SENSOR_SLOT IO_SLOT
|
||||||
#define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT
|
#define WB_I2C1_SCL (14) // SENSOR_SLOT IO_SLOT
|
||||||
|
|
||||||
#define PIN_AREF (2)
|
#define PIN_AREF (2)
|
||||||
#define PIN_NFC1 (9)
|
#define PIN_NFC1 (9)
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ static const uint8_t SCL = PIN_WIRE_SCL;
|
|||||||
#define TB_LEFT 27
|
#define TB_LEFT 27
|
||||||
#define TB_RIGHT 28
|
#define TB_RIGHT 28
|
||||||
#define TB_PRESS 29
|
#define TB_PRESS 29
|
||||||
|
#define TB_DIRECTION FALLING
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
// Compatibility Definitions
|
// Compatibility Definitions
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ upload_protocol = esptool
|
|||||||
build_flags = ${esp32s3_base.build_flags}
|
build_flags = ${esp32s3_base.build_flags}
|
||||||
-DT_DECK
|
-DT_DECK
|
||||||
-DBOARD_HAS_PSRAM
|
-DBOARD_HAS_PSRAM
|
||||||
-DMAX_THREADS=40
|
|
||||||
-DGPS_POWER_TOGGLE
|
-DGPS_POWER_TOGGLE
|
||||||
-Ivariants/t-deck
|
-Ivariants/t-deck
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ build_flags =
|
|||||||
${inkhud.build_flags}
|
${inkhud.build_flags}
|
||||||
-I variants/tlora_t3s3_epaper
|
-I variants/tlora_t3s3_epaper
|
||||||
-D TLORA_T3S3_EPAPER
|
-D TLORA_T3S3_EPAPER
|
||||||
-D MAX_THREADS=40 ; Required if used with WiFi
|
|
||||||
lib_deps =
|
lib_deps =
|
||||||
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
|
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
|
||||||
${esp32s3_base.lib_deps}
|
${esp32s3_base.lib_deps}
|
||||||
@@ -57,8 +57,7 @@
|
|||||||
#define LED_PIN 13 // the red part of the RGB LED
|
#define LED_PIN 13 // the red part of the RGB LED
|
||||||
#define LED_STATE_ON 0 // State when LED is lit
|
#define LED_STATE_ON 0 // State when LED is lit
|
||||||
|
|
||||||
#define ALT_BUTTON_PIN 21 // Button 3 - square - top button in landscape mode
|
#define ALT_BUTTON_PIN 21 // Button 3 - square - top button in landscape mode
|
||||||
#define BUTTON_NEED_PULLUP2 TB_UP
|
|
||||||
#define BUTTON_PIN 0 // Circle button
|
#define BUTTON_PIN 0 // Circle button
|
||||||
#define BUTTON_NEED_PULLUP // we do need a helping hand up
|
#define BUTTON_NEED_PULLUP // we do need a helping hand up
|
||||||
#define CANCEL_BUTTON_PIN 45 // Button 1 - triangle - bottom button in landscape mode
|
#define CANCEL_BUTTON_PIN 45 // Button 1 - triangle - bottom button in landscape mode
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[VERSION]
|
[VERSION]
|
||||||
major = 2
|
major = 2
|
||||||
minor = 7
|
minor = 7
|
||||||
build = 0
|
build = 1
|
||||||
|
|||||||
Reference in New Issue
Block a user