Files
firmware/src/graphics/niche/InkHUD/Applets/System/Notification/NotificationApplet.cpp
T
cd6ac90f7e Add waypoint & geofence support with notifications for BaseUI and InkHUD (#10920)
* Implement GeofenceModule for waypoint crossing notifications and integrate with existing modules

* Waypoint Applet Initial Support on InkHUD

* undo tile change

* Update screen when Waypoint shows or dissapears

* Merge branch 'develop' into waypoint-geofence

* Geofence on InkHUD

* Update MapTile.h

* Update WaypointStore.cpp

* Notifications

* remove GF from waypoint screen

* Prevent Focus from closing the notifiaction banner

* Trunk fix

* cleanup

* undo merge conflix mistake

* Waypoint screen on BaseUI

* Focus preserve fix

* UI bugs

* Allow Inkhud to remove waypoint

* Respect Locked Waypoints

* Trunk fix

* Update WaypointStore.cpp

* Use 8-digit hex formatting for waypoint IDs.

0x%x was inconsistent with the repo's own convention (0x%08x for 32-bit IDs, used elsewhere in this file). Fixed here and in two other spots I found with the same issue (WaypointModule.cpp, GeofenceModule.cpp).

* Update ExternalNotificationModule.cpp

* Reject invalid surrogate codepoints in waypoint icon rendering

* Update WaypointModule.cpp

* Update WaypointStore.cpp

* Update WaypointStore.cpp

* Update WaypointStore.cpp

* trunk fix

* fix warnings

* power.h rename to Power.h

* Update Power.h

* Fix executable bit on bin/lint-ifdef-complexity.sh

Lost during a prior merge from develop (Windows checkout doesn't
preserve file mode), causing "execve failed: Permission denied" in
the Trunk Check Runner CI job. develop has this file at 100755;
restoring that here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Update README.md

* Clean up waypoint and geofence integration

* Minimize waypoint and geofence implementation

* removed unnecessary gating

* Geofence alert

* trunk fix

* Update test_main.cpp

* Update WaypointStore.cpp

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 16:17:28 +00:00

336 lines
9.4 KiB
C++

#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./NotificationApplet.h"
#include "./Notification.h"
#include "MessageStore.h"
#include "graphics/niche/InkHUD/Applets/Bases/Map/MapApplet.h"
#include "graphics/niche/InkHUD/Persistence.h"
#if !MESHTASTIC_EXCLUDE_WAYPOINT
#include "modules/GeofenceModule.h"
#include <cstring>
#endif
#include "meshUtils.h"
#include "modules/TextMessageModule.h"
#include "gps/RTC.h"
using namespace NicheGraphics;
InkHUD::NotificationApplet::NotificationApplet()
{
textMessageObserver.observe(textMessageModule);
#if !MESHTASTIC_EXCLUDE_WAYPOINT
if (geofenceModule)
geofenceObserver.observe(geofenceModule);
#endif
}
void InkHUD::NotificationApplet::showNotification(const Notification &n)
{
assert(isActive());
if (!settings->optionalFeatures.notifications)
return;
dismiss();
hasNotification = true;
currentNotification = n;
if (isApproved()) {
bringToForeground();
inkhud->forceUpdate();
} else
hasNotification = false;
}
// Collect meta-info about the text message, and ask for approval for the notification
// No need to save the message itself; we can use the cached InkHUD::latestMessage data during render()
int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
{
// System applets are always active
assert(isActive());
// Abort if this is an outgoing message
if (getFrom(p) == nodeDB->getNodeNum())
return 0;
Notification n;
n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
// Gather info: in-channel message
if (isBroadcast(p->to)) {
n.type = Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
n.channel = p->channel;
}
// Gather info: DM
else {
n.type = Notification::Type::NOTIFICATION_MESSAGE_DIRECT;
n.sender = p->from;
}
showNotification(n);
// Return zero: no issues here, carry on notifying other observers!
return 0;
}
#if !MESHTASTIC_EXCLUDE_WAYPOINT
int InkHUD::NotificationApplet::onGeofenceEvent(const GeofenceNotificationEvent *event)
{
assert(isActive());
if (!event)
return 0;
Notification n;
n.type = Notification::Type::NOTIFICATION_GEOFENCE;
n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true);
n.geofenceWaypointId = event->waypointId;
strncpy(n.geofenceName, event->geofenceName, sizeof(n.geofenceName) - 1);
n.geofenceName[sizeof(n.geofenceName) - 1] = '\0';
strncpy(n.geofenceNodeName, event->nodeName, sizeof(n.geofenceNodeName) - 1);
n.geofenceNodeName[sizeof(n.geofenceNodeName) - 1] = '\0';
n.geofenceEntered = event->entered;
showNotification(n);
return 0;
}
#endif
void InkHUD::NotificationApplet::onRender(bool full)
{
// Clear the region beneath the tile
// Most applets are drawing onto an empty frame buffer and don't need to do this
// We do need to do this with the battery though, as it is an "overlay"
fillRect(0, 0, width(), height(), WHITE);
// Padding (horizontal)
const uint16_t padW = 4;
// Main border
drawRect(0, 0, width(), height(), BLACK);
// drawRect(1, 1, width() - 2, height() - 2, BLACK);
// Timestamp (potentially)
// ====================
std::string ts = getTimeString(currentNotification.timestamp);
uint16_t tsW = 0;
int16_t divX = 0;
// Timestamp available
if (ts.length() > 0) {
tsW = getTextWidth(ts);
divX = padW + tsW + padW;
hatchRegion(0, 0, divX, height(), 2, BLACK); // Fill with a dark background
drawLine(divX, 0, divX, height() - 1, BLACK); // Draw divider between timestamp and main text
setCrop(1, 1, divX - 1, height() - 2);
// Drop shadow
setTextColor(WHITE);
printThick(padW + (tsW / 2), height() / 2, ts, 4, 4);
// Bold text
setTextColor(BLACK);
printThick(padW + (tsW / 2), height() / 2, ts, 2, 1);
}
// Main text
// =====================
// Background fill
// - medium dark (1/3)
hatchRegion(divX, 0, width() - divX - 1, height(), 3, BLACK);
uint16_t availableWidth = width() - divX - padW;
std::string text = getNotificationText(availableWidth);
int16_t textM = divX + padW + (getTextWidth(text) / 2);
// Restrict area for printing
// - don't overlap border, or divider
setCrop(divX + 1, 1, (width() - (divX + 1) - 1), height() - 2);
// Drop shadow
// - thick white text
setTextColor(WHITE);
printThick(textM, height() / 2, text, 4, 4);
// Main text
// - faux bold: double width
setTextColor(BLACK);
printThick(textM, height() / 2, text, 2, 1);
}
void InkHUD::NotificationApplet::onForeground()
{
handleInput = true; // Intercept the button input for our applet, so we can dismiss the notification
}
void InkHUD::NotificationApplet::onBackground()
{
handleInput = false;
inkhud->forceUpdate(EInk::UpdateTypes::FULL, true);
}
void InkHUD::NotificationApplet::onButtonShortPress()
{
if (currentNotification.type == Notification::Type::NOTIFICATION_GEOFENCE)
openGeofenceOnMap();
else
dismiss();
}
void InkHUD::NotificationApplet::onButtonLongPress()
{
dismiss();
}
void InkHUD::NotificationApplet::onExitShort()
{
dismiss();
}
void InkHUD::NotificationApplet::onExitLong()
{
dismiss();
}
void InkHUD::NotificationApplet::onNavUp()
{
dismiss();
}
void InkHUD::NotificationApplet::onNavDown()
{
dismiss();
}
void InkHUD::NotificationApplet::onNavLeft()
{
dismiss();
}
void InkHUD::NotificationApplet::onNavRight()
{
if (currentNotification.type == Notification::Type::NOTIFICATION_GEOFENCE)
openGeofenceOnMap();
else
dismiss();
}
void InkHUD::NotificationApplet::openGeofenceOnMap()
{
for (uint8_t i = 0; i < inkhud->userApplets.size(); ++i) {
Applet *applet = inkhud->userApplets.at(i);
MapApplet *map = applet ? applet->asMapApplet() : nullptr;
if (!map || !applet->isActive() || !map->focusWaypoint(currentNotification.geofenceWaypointId))
continue;
dismiss();
inkhud->showApplet(i);
return;
}
dismiss();
}
// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification
// Called internally when we first get a "notifiable event", and then again before render,
// in case autoshow swapped which applet was displayed
bool InkHUD::NotificationApplet::isApproved()
{
// Instead of an assert
if (!hasNotification) {
LOG_WARN("No notif to approve");
return false;
}
// Ask all visible user applets for approval
for (Applet *ua : inkhud->userApplets) {
if (ua->isForeground() && !ua->approveNotification(currentNotification))
return false;
}
return true;
}
// Mark that the notification should no-longer be rendered
// In addition to calling thing method, code needs to request a re-render of all applets
void InkHUD::NotificationApplet::dismiss()
{
sendToBackground();
hasNotification = false;
// Not requesting update directly from this method,
// as it is used to dismiss notifications which have been made redundant by autoshow settings, before they are ever drawn
}
// Get a string for the main body text of a notification
// Formatted to suit screen width
// Takes info from InkHUD::currentNotification
std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvailable)
{
assert(hasNotification);
std::string text;
// Text message
// ==============
if (IS_ONE_OF(currentNotification.type, Notification::Type::NOTIFICATION_MESSAGE_DIRECT,
Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) {
// Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently
bool msgIsBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
// Pick source of message
const StoredMessage *message =
msgIsBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
if (!message->sender || !messageStore.isMessageVisible(*message))
return parse(text);
// Find info about the sender
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);
// Leading tag (channel vs. DM)
text += msgIsBroadcast ? "From:" : "DM: ";
// Sender id
if (nodeInfoLiteHasUser(node))
text += parseShortName(node);
else
text += hexifyNodeNum(message->sender);
// Check if text fits
// - use a longer string, if we have the space
if (getTextWidth(text) < widthAvailable * 0.5) {
text.clear();
// Leading tag (channel vs. DM)
text += msgIsBroadcast ? "Msg from " : "DM from ";
// Sender id
if (nodeInfoLiteHasUser(node))
text += parseShortName(node);
else
text += hexifyNodeNum(message->sender);
text += ": ";
text += MessageStore::getText(*message);
}
}
else if (currentNotification.type == Notification::Type::NOTIFICATION_GEOFENCE) {
text += currentNotification.geofenceNodeName;
text += currentNotification.geofenceEntered ? " IN " : " OUT ";
text += currentNotification.geofenceName;
}
// Parse any non-ascii characters and return
return parse(text);
}
#endif