Files
firmware/src/graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.cpp
todd-herbert ecfaf3a095 Canned Messages via InkHUD menu (#7096)
* Allow observers to respond to AdminMessage requests
Ground work for CannedMessage getters and setters

* Enable CannedMessage config in apps for InkHUD devices

* Migrate the InkHUD::Events AdminModule observer
Use the new AdminModule_ObserverData struct

* Bare-bones NicheGraphics util to access canned messages
Handles loading and parsing. Handle admin messages for setting and getting.

* Send canned messages via on-screen menu

* Change ThreadedMessageApplet from Observer to Module API
Allows us to intercept locally generated packets ('loopbackOK = true'), to handle outgoing canned messages.

* Fix: crash getting empty canned message string via Client API

* Move file into Utils subdir

* Move an include statement from .cpp to .h

* Limit strncpy size of dest, not source
Wasn't critical in ths specific case, but definitely a mistake.
2025-06-25 06:04:18 -05:00

268 lines
9.5 KiB
C++

#ifdef MESHTASTIC_INCLUDE_INKHUD
#include "./ThreadedMessageApplet.h"
#include "RTC.h"
#include "mesh/NodeDB.h"
using namespace NicheGraphics;
// Hard limits on how much message data to write to flash
// Avoid filling the storage if something goes wrong
// Normal usage should be well below this size
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex)
: SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex)
{
// Create the message store
// Will shortly attempt to load messages from RAM, if applet is active
// Label (filename in flash) is set from channel index
store = new MessageStore("ch" + to_string(channelIndex));
}
void InkHUD::ThreadedMessageApplet::onRender()
{
// =============
// Draw a header
// =============
// Header text
std::string headerText;
headerText += "Channel ";
headerText += to_string(channelIndex);
headerText += ": ";
if (channels.isDefaultChannel(channelIndex))
headerText += "Public";
else
headerText += channels.getByIndex(channelIndex).settings.name;
// Draw a "standard" applet header
drawHeader(headerText);
// Y position for divider
const int16_t dividerY = Applet::getHeaderHeight() - 1;
// ==================
// Draw each message
// ==================
// Restrict drawing area
// - don't overdraw the header
// - small gap below divider
setCrop(0, dividerY + 2, width(), height() - (dividerY + 2));
// Set padding
// - separates text from the vertical line which marks its edge
constexpr uint16_t padW = 2;
constexpr int16_t msgL = padW;
const int16_t msgR = (width() - 1) - padW;
const uint16_t msgW = (msgR - msgL) + 1;
int16_t msgB = height() - 1; // Vertical cursor for drawing. Messages are bottom-aligned to this value.
uint8_t i = 0; // Index of stored message
// Loop over messages
// - until no messages left, or
// - until no part of message fits on screen
while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) {
// Grab data for message
MessageStore::Message &m = store->messages.at(i);
bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message
std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message
// Cache bottom Y of message text
// - Used when drawing vertical line alongside
const int16_t dotsB = msgB;
// Get dimensions for message text
uint16_t bodyH = getWrappedTextHeight(msgL, msgW, bodyText);
int16_t bodyT = msgB - bodyH;
// Print message
// - if incoming
if (!outgoing)
printWrapped(msgL, bodyT, msgW, bodyText);
// Print message
// - if outgoing
else {
if (getTextWidth(bodyText) < width()) // If short,
printAt(msgR, bodyT, bodyText, RIGHT); // print right align
else // If long,
printWrapped(msgL, bodyT, msgW, bodyText); // need printWrapped(), which doesn't support right align
}
// Move cursor up
// - above message text
msgB -= bodyH;
msgB -= getFont().lineHeight() * 0.2; // Padding between message and header
// Compose info string
// - shortname, if possible, or "me"
// - time received, if possible
std::string info;
if (outgoing)
info += "Me";
else {
// Check if sender is node db
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender);
if (sender)
info += parseShortName(sender); // Handle any unprintable chars in short name
else
info += hexifyNodeNum(m.sender); // No node info at all. Print the node num
}
std::string timeString = getTimeString(m.timestamp);
if (timeString.length() > 0) {
info += " - ";
info += timeString;
}
// Print the info string
// - Faux bold: printed twice, shifted horizontally by one px
printAt(outgoing ? msgR : msgL, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM);
printAt(outgoing ? msgR - 1 : msgL + 1, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM);
// Underline the info string
const int16_t divY = msgB;
int16_t divL;
int16_t divR;
if (!outgoing) {
// Left side - incoming
divL = msgL;
divR = getTextWidth(info) + getFont().lineHeight() / 2;
} else {
// Right side - outgoing
divR = msgR;
divL = divR - getTextWidth(info) - getFont().lineHeight() / 2;
}
for (int16_t x = divL; x <= divR; x += 2)
drawPixel(x, divY, BLACK);
// Move cursor up: above info string
msgB -= fontSmall.lineHeight();
// Vertical line alongside message
for (int16_t y = msgB; y < dotsB; y += 1)
drawPixel(outgoing ? width() - 1 : 0, y, BLACK);
// Move cursor up: padding before next message
msgB -= fontSmall.lineHeight() * 0.5;
i++;
} // End of loop: drawing each message
// Fade effect:
// Area immediately below the divider. Overdraw with sparse white lines.
// Make text appear to pass behind the header
hatchRegion(0, dividerY + 1, width(), fontSmall.lineHeight() / 3, 2, WHITE);
// If we've run out of screen to draw messages, we can drop any leftover data from the queue
// Those messages have been pushed off the screen-top by newer ones
while (i < store->messages.size())
store->messages.pop_back();
}
// Code which runs when the applet begins running
// This might happen at boot, or if user enables the applet at run-time, via the menu
void InkHUD::ThreadedMessageApplet::onActivate()
{
loadMessagesFromFlash();
loopbackOk = true; // Allow us to handle messages generated on the node (canned messages)
}
// Code which runs when the applet stop running
// This might be at shutdown, or if the user disables the applet at run-time, via the menu
void InkHUD::ThreadedMessageApplet::onDeactivate()
{
loopbackOk = false; // Slightly reduce our impact if the applet is disabled
}
// Handle new text messages
// These might be incoming, from the mesh, or outgoing from phone
// Each instance of the ThreadMessageApplet will only listen on one specific channel
ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_MeshPacket &mp)
{
// Abort if applet fully deactivated
if (!isActive())
return ProcessMessage::CONTINUE;
// Abort if wrong channel
if (mp.channel != this->channelIndex)
return ProcessMessage::CONTINUE;
// Abort if message was a DM
if (mp.to != NODENUM_BROADCAST)
return ProcessMessage::CONTINUE;
// Extract info into our slimmed-down "StoredMessage" type
MessageStore::Message newMessage;
newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
newMessage.sender = mp.from;
newMessage.channelIndex = mp.channel;
newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size);
// Store newest message at front
// These records are used when rendering, and also stored in flash at shutdown
store->messages.push_front(newMessage);
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
if (getFrom(&mp) != nodeDB->getNodeNum())
requestAutoshow();
// Redraw the applet, perhaps.
requestUpdate(); // Want to update display, if applet is foreground
// 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
bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n)
{
if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST && n.getChannel() == channelIndex)
return false;
// None of our business. Allow the notification.
else
return true;
}
// Save several recent messages to flash
// Stores the contents of ThreadedMessageApplet::messages
// Just enough messages to fill the display
// Messages are packed "back-to-back", to minimize blocks of flash used
void InkHUD::ThreadedMessageApplet::saveMessagesToFlash()
{
// Create a label (will become the filename in flash)
std::string label = "ch" + to_string(channelIndex);
store->saveToFlash();
}
// Load recent messages to flash
// Fills ThreadedMessageApplet::messages with previous messages
// Just enough messages have been stored to cover the display
void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash()
{
// Create a label (will become the filename in flash)
std::string label = "ch" + to_string(channelIndex);
store->loadFromFlash();
}
// Code to run when device is shutting down
// This is in addition to any onDeactivate() code, which will also run
// Todo: implement before a reboot also
void InkHUD::ThreadedMessageApplet::onShutdown()
{
// Save our current set of messages to flash, provided the applet isn't disabled
if (isActive())
saveMessagesToFlash();
}
#endif