mirror of
https://github.com/meshtastic/firmware.git
synced 2026-09-22 06:15:25 -04:00
* 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>
336 lines
9.4 KiB
C++
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
|