mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-23 03:00:56 +00:00
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.
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
#include "RTC.h"
|
||||
|
||||
#include "MeshService.h"
|
||||
#include "Router.h"
|
||||
#include "airtime.h"
|
||||
#include "main.h"
|
||||
#include "power.h"
|
||||
@@ -31,6 +32,12 @@ InkHUD::MenuApplet::MenuApplet() : concurrency::OSThread("MenuApplet")
|
||||
if (settings->optionalMenuItems.backlight) {
|
||||
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()
|
||||
@@ -65,6 +72,10 @@ void InkHUD::MenuApplet::onForeground()
|
||||
|
||||
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:
|
||||
// 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
|
||||
@@ -153,6 +164,16 @@ void InkHUD::MenuApplet::execute(MenuItem item)
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL);
|
||||
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:
|
||||
inkhud->rotate();
|
||||
break;
|
||||
@@ -260,9 +281,11 @@ void InkHUD::MenuApplet::showPage(MenuPage page)
|
||||
break;
|
||||
|
||||
case SEND:
|
||||
items.push_back(MenuItem("Ping", MenuAction::SEND_PING, MenuPage::EXIT));
|
||||
// Todo: canned messages
|
||||
items.push_back(MenuItem("Exit", MenuPage::EXIT));
|
||||
populateSendPage();
|
||||
break;
|
||||
|
||||
case CANNEDMESSAGE_RECIPIENT:
|
||||
populateRecipientPage();
|
||||
break;
|
||||
|
||||
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()
|
||||
{
|
||||
// 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.
|
||||
// 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.
|
||||
@@ -619,4 +750,37 @@ uint16_t InkHUD::MenuApplet::getSystemInfoPanelHeight()
|
||||
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
|
||||
Reference in New Issue
Block a user