diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp
index f381df854..c082c485d 100644
--- a/src/graphics/Screen.cpp
+++ b/src/graphics/Screen.cpp
@@ -59,7 +59,6 @@ along with this program. If not, see .
#include "mesh/Channels.h"
#include "mesh/generated/meshtastic/deviceonly.pb.h"
#include "meshUtils.h"
-#include "modules/AdminModule.h"
#include "modules/ExternalNotificationModule.h"
#include "modules/TextMessageModule.h"
#include "modules/WaypointModule.h"
@@ -1308,12 +1307,13 @@ int Screen::handleInputEvent(const InputEvent *event)
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)
case meshtastic_AdminMessage_remove_by_nodenum_tag:
setFrames(FOCUS_PRESERVE);
+ *arg->result = AdminMessageHandleResult::HANDLED;
break;
// Default no-op, in case the admin message observable gets used by other classes in future
diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h
index 21f1a0322..99ade0a6b 100644
--- a/src/graphics/Screen.h
+++ b/src/graphics/Screen.h
@@ -79,6 +79,7 @@ class Screen
#include "concurrency/OSThread.h"
#include "input/InputBroker.h"
#include "mesh/MeshModule.h"
+#include "modules/AdminModule.h"
#include "power.h"
#include
#include
@@ -194,8 +195,8 @@ class Screen : public concurrency::OSThread
CallbackObserver(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules
CallbackObserver inputObserver =
CallbackObserver(this, &Screen::handleInputEvent);
- CallbackObserver adminMessageObserver =
- CallbackObserver(this, &Screen::handleAdminMessage);
+ CallbackObserver adminMessageObserver =
+ CallbackObserver(this, &Screen::handleAdminMessage);
public:
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
@@ -546,7 +547,7 @@ class Screen : public concurrency::OSThread
int handleTextMessage(const meshtastic_MeshPacket *arg);
int handleUIFrameEvent(const UIFrameEvent *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
void forceDisplay(bool forceUiUpdate = false);
diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h
index f162aa385..f42b9dc2c 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h
+++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuAction.h
@@ -19,6 +19,8 @@ namespace NicheGraphics::InkHUD
enum MenuAction {
NO_ACTION,
SEND_PING,
+ STORE_CANNEDMESSAGE_SELECTION,
+ SEND_CANNEDMESSAGE,
SHUTDOWN,
NEXT_TILE,
TOGGLE_BACKLIGHT,
diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
index 9fdfad8ee..69965972f 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
+++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.cpp
@@ -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
\ No newline at end of file
diff --git a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h
index d9297c8ed..4c974672a 100644
--- a/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h
+++ b/src/graphics/niche/InkHUD/Applets/System/Menu/MenuApplet.h
@@ -6,10 +6,12 @@
#include "graphics/niche/InkHUD/InkHUD.h"
#include "graphics/niche/InkHUD/Persistence.h"
#include "graphics/niche/InkHUD/SystemApplet.h"
+#include "graphics/niche/Utils/CannedMessageStore.h"
#include "./MenuItem.h"
#include "./MenuPage.h"
+#include "Channels.h"
#include "concurrency/OSThread.h"
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 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 populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
+
uint16_t getSystemInfoPanelHeight();
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;
uint8_t cursor = 0; // Which menu item is currently highlighted
@@ -51,6 +59,37 @@ class MenuApplet : public SystemApplet, public concurrency::OSThread
std::vector