mirror of
https://github.com/meshtastic/firmware.git
synced 2025-06-25 22:20:27 +00:00
Canned Messages via InkHUD menu (#7096)
Some checks are pending
CI / setup (check) (push) Waiting to run
CI / setup (esp32) (push) Waiting to run
CI / setup (esp32c3) (push) Waiting to run
CI / setup (esp32c6) (push) Waiting to run
CI / setup (esp32s3) (push) Waiting to run
CI / setup (nrf52840) (push) Waiting to run
CI / setup (rp2040) (push) Waiting to run
CI / setup (stm32) (push) Waiting to run
CI / check (push) Blocked by required conditions
CI / build-esp32 (push) Blocked by required conditions
CI / build-esp32-s3 (push) Blocked by required conditions
CI / build-esp32-c3 (push) Blocked by required conditions
CI / build-esp32-c6 (push) Blocked by required conditions
CI / build-nrf52 (push) Blocked by required conditions
CI / build-rpi2040 (push) Blocked by required conditions
CI / build-stm32 (push) Blocked by required conditions
CI / build-debian-src (push) Waiting to run
CI / package-pio-deps-native-tft (push) Waiting to run
CI / test-native (push) Waiting to run
CI / docker-deb-amd64 (push) Waiting to run
CI / docker-deb-amd64-tft (push) Waiting to run
CI / docker-alp-amd64 (push) Waiting to run
CI / docker-alp-amd64-tft (push) Waiting to run
CI / docker-deb-arm64 (push) Waiting to run
CI / docker-deb-armv7 (push) Waiting to run
CI / gather-artifacts (esp32) (push) Blocked by required conditions
CI / gather-artifacts (esp32c3) (push) Blocked by required conditions
CI / gather-artifacts (esp32c6) (push) Blocked by required conditions
CI / gather-artifacts (esp32s3) (push) Blocked by required conditions
CI / gather-artifacts (nrf52840) (push) Blocked by required conditions
CI / gather-artifacts (rp2040) (push) Blocked by required conditions
CI / gather-artifacts (stm32) (push) Blocked by required conditions
CI / release-artifacts (push) Blocked by required conditions
CI / release-firmware (esp32) (push) Blocked by required conditions
CI / release-firmware (esp32c3) (push) Blocked by required conditions
CI / release-firmware (esp32c6) (push) Blocked by required conditions
CI / release-firmware (esp32s3) (push) Blocked by required conditions
CI / release-firmware (nrf52840) (push) Blocked by required conditions
CI / release-firmware (rp2040) (push) Blocked by required conditions
CI / release-firmware (stm32) (push) Blocked by required conditions
CI / publish-firmware (push) Blocked by required conditions
Some checks are pending
CI / setup (check) (push) Waiting to run
CI / setup (esp32) (push) Waiting to run
CI / setup (esp32c3) (push) Waiting to run
CI / setup (esp32c6) (push) Waiting to run
CI / setup (esp32s3) (push) Waiting to run
CI / setup (nrf52840) (push) Waiting to run
CI / setup (rp2040) (push) Waiting to run
CI / setup (stm32) (push) Waiting to run
CI / check (push) Blocked by required conditions
CI / build-esp32 (push) Blocked by required conditions
CI / build-esp32-s3 (push) Blocked by required conditions
CI / build-esp32-c3 (push) Blocked by required conditions
CI / build-esp32-c6 (push) Blocked by required conditions
CI / build-nrf52 (push) Blocked by required conditions
CI / build-rpi2040 (push) Blocked by required conditions
CI / build-stm32 (push) Blocked by required conditions
CI / build-debian-src (push) Waiting to run
CI / package-pio-deps-native-tft (push) Waiting to run
CI / test-native (push) Waiting to run
CI / docker-deb-amd64 (push) Waiting to run
CI / docker-deb-amd64-tft (push) Waiting to run
CI / docker-alp-amd64 (push) Waiting to run
CI / docker-alp-amd64-tft (push) Waiting to run
CI / docker-deb-arm64 (push) Waiting to run
CI / docker-deb-armv7 (push) Waiting to run
CI / gather-artifacts (esp32) (push) Blocked by required conditions
CI / gather-artifacts (esp32c3) (push) Blocked by required conditions
CI / gather-artifacts (esp32c6) (push) Blocked by required conditions
CI / gather-artifacts (esp32s3) (push) Blocked by required conditions
CI / gather-artifacts (nrf52840) (push) Blocked by required conditions
CI / gather-artifacts (rp2040) (push) Blocked by required conditions
CI / gather-artifacts (stm32) (push) Blocked by required conditions
CI / release-artifacts (push) Blocked by required conditions
CI / release-firmware (esp32) (push) Blocked by required conditions
CI / release-firmware (esp32c3) (push) Blocked by required conditions
CI / release-firmware (esp32c6) (push) Blocked by required conditions
CI / release-firmware (esp32s3) (push) Blocked by required conditions
CI / release-firmware (nrf52840) (push) Blocked by required conditions
CI / release-firmware (rp2040) (push) Blocked by required conditions
CI / release-firmware (stm32) (push) Blocked by required conditions
CI / publish-firmware (push) Blocked by required conditions
* 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:
parent
91bcf072a0
commit
ecfaf3a095
@ -58,7 +58,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#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"
|
||||
@ -1377,12 +1376,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
|
||||
|
@ -78,6 +78,7 @@ class Screen
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "input/InputBroker.h"
|
||||
#include "mesh/MeshModule.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "power.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@ -193,8 +194,8 @@ class Screen : public concurrency::OSThread
|
||||
CallbackObserver<Screen, const UIFrameEvent *>(this, &Screen::handleUIFrameEvent); // Sent by Mesh Modules
|
||||
CallbackObserver<Screen, const InputEvent *> inputObserver =
|
||||
CallbackObserver<Screen, const InputEvent *>(this, &Screen::handleInputEvent);
|
||||
CallbackObserver<Screen, const meshtastic_AdminMessage *> adminMessageObserver =
|
||||
CallbackObserver<Screen, const meshtastic_AdminMessage *>(this, &Screen::handleAdminMessage);
|
||||
CallbackObserver<Screen, AdminModule_ObserverData *> adminMessageObserver =
|
||||
CallbackObserver<Screen, AdminModule_ObserverData *>(this, &Screen::handleAdminMessage);
|
||||
|
||||
public:
|
||||
explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY);
|
||||
@ -544,7 +545,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);
|
||||
|
@ -19,6 +19,8 @@ namespace NicheGraphics::InkHUD
|
||||
enum MenuAction {
|
||||
NO_ACTION,
|
||||
SEND_PING,
|
||||
STORE_CANNEDMESSAGE_SELECTION,
|
||||
SEND_CANNEDMESSAGE,
|
||||
SHUTDOWN,
|
||||
NEXT_TILE,
|
||||
TOGGLE_BACKLIGHT,
|
||||
|
@ -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
|
@ -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<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
|
||||
|
||||
// Data for selecting and sending canned messages via the menu
|
||||
// Placed into a sub-class for organization only
|
||||
class CannedMessages
|
||||
{
|
||||
public:
|
||||
// Share NicheGraphics component
|
||||
// Handles loading, getting, setting
|
||||
CannedMessageStore *store;
|
||||
|
||||
// One canned message
|
||||
// Links the menu item to the true message text
|
||||
struct MessageItem {
|
||||
std::string label; // Shown in menu. Prefixed, and UTF-8 chars parsed
|
||||
std::string rawText; // The message which will be sent, if this item is selected
|
||||
} *selectedMessageItem;
|
||||
|
||||
// One possible destination for a canned message
|
||||
// Links the menu item to the intended recipient
|
||||
// May represent either broadcast or DM
|
||||
struct RecipientItem {
|
||||
std::string label; // Shown in menu
|
||||
NodeNum dest = NODENUM_BROADCAST;
|
||||
uint8_t channelIndex = 0;
|
||||
} *selectedRecipientItem;
|
||||
|
||||
// These lists are generated when the menu page is populated
|
||||
// Cleared onBackground (when MenuApplet closes)
|
||||
std::vector<MessageItem> messageItems;
|
||||
std::vector<RecipientItem> recipientItems;
|
||||
} cm;
|
||||
|
||||
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
|
||||
};
|
||||
|
||||
|
@ -18,6 +18,7 @@ namespace NicheGraphics::InkHUD
|
||||
enum MenuPage : uint8_t {
|
||||
ROOT, // Initial menu page
|
||||
SEND,
|
||||
CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message
|
||||
OPTIONS,
|
||||
APPLETS,
|
||||
AUTOSHOW,
|
||||
|
@ -13,7 +13,8 @@ using namespace NicheGraphics;
|
||||
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
|
||||
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
|
||||
|
||||
InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex) : channelIndex(channelIndex)
|
||||
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
|
||||
@ -69,9 +70,8 @@ void InkHUD::ThreadedMessageApplet::onRender()
|
||||
|
||||
// Grab data for message
|
||||
MessageStore::Message &m = store->messages.at(i);
|
||||
bool outgoing = (m.sender == 0);
|
||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender);
|
||||
std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message
|
||||
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
|
||||
@ -171,54 +171,54 @@ void InkHUD::ThreadedMessageApplet::onRender()
|
||||
void InkHUD::ThreadedMessageApplet::onActivate()
|
||||
{
|
||||
loadMessagesFromFlash();
|
||||
textMessageObserver.observe(textMessageModule); // Begin handling any new text messages with onReceiveTextMessage
|
||||
loopbackOk = true; // Allow us to handle messages generated on the node (canned messages)
|
||||
}
|
||||
|
||||
// Code which runs when the applet stop running
|
||||
// This might be happen at shutdown, or if user disables the applet at run-time
|
||||
// This might be at shutdown, or if the user disables the applet at run-time, via the menu
|
||||
void InkHUD::ThreadedMessageApplet::onDeactivate()
|
||||
{
|
||||
textMessageObserver.unobserve(textMessageModule); // Stop handling any new text messages with onReceiveTextMessage
|
||||
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
|
||||
// Method should return 0, to indicate general success to TextMessageModule
|
||||
int InkHUD::ThreadedMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
||||
ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
// Abort if applet fully deactivated
|
||||
// Already handled by onActivate and onDeactivate, but good practice for all applets
|
||||
if (!isActive())
|
||||
return 0;
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Abort if wrong channel
|
||||
if (p->channel != this->channelIndex)
|
||||
return 0;
|
||||
if (mp.channel != this->channelIndex)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Abort if message was a DM
|
||||
if (p->to != NODENUM_BROADCAST)
|
||||
return 0;
|
||||
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 = p->from;
|
||||
newMessage.channelIndex = p->channel;
|
||||
newMessage.text = std::string(&p->decoded.payload.bytes[0], &p->decoded.payload.bytes[p->decoded.payload.size]);
|
||||
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(p) != nodeDB->getNodeNum())
|
||||
if (getFrom(&mp) != nodeDB->getNodeNum())
|
||||
requestAutoshow();
|
||||
|
||||
// Redraw the applet, perhaps.
|
||||
requestUpdate(); // Want to update display, if applet is foreground
|
||||
|
||||
return 0;
|
||||
// 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
|
||||
|
@ -30,7 +30,7 @@ namespace NicheGraphics::InkHUD
|
||||
|
||||
class Applet;
|
||||
|
||||
class ThreadedMessageApplet : public Applet
|
||||
class ThreadedMessageApplet : public Applet, public SinglePortModule
|
||||
{
|
||||
public:
|
||||
explicit ThreadedMessageApplet(uint8_t channelIndex);
|
||||
@ -41,16 +41,11 @@ class ThreadedMessageApplet : public Applet
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
void onShutdown() override;
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
bool approveNotification(Notification &n) override; // Which notifications to suppress
|
||||
|
||||
protected:
|
||||
// Used to register our text message callback
|
||||
CallbackObserver<ThreadedMessageApplet, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<ThreadedMessageApplet, const meshtastic_MeshPacket *>(this,
|
||||
&ThreadedMessageApplet::onReceiveTextMessage);
|
||||
|
||||
void saveMessagesToFlash();
|
||||
void loadMessagesFromFlash();
|
||||
|
||||
|
@ -4,14 +4,13 @@
|
||||
|
||||
#include "RTC.h"
|
||||
#include "buzz.h"
|
||||
#include "modules/AdminModule.h"
|
||||
#include "modules/ExternalNotificationModule.h"
|
||||
#include "modules/TextMessageModule.h"
|
||||
#include "sleep.h"
|
||||
|
||||
#include "./Applet.h"
|
||||
#include "./SystemApplet.h"
|
||||
#include "graphics/niche/FlashData.h"
|
||||
#include "graphics/niche/Utils/FlashData.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
@ -30,7 +29,7 @@ void InkHUD::Events::begin()
|
||||
rebootObserver.observe(¬ifyReboot);
|
||||
textMessageObserver.observe(textMessageModule);
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
adminMessageObserver.observe(adminModule);
|
||||
adminMessageObserver.observe((Observable<AdminModule_ObserverData *> *)adminModule);
|
||||
#endif
|
||||
#ifdef ARCH_ESP32
|
||||
lightSleepObserver.observe(¬ifyLightSleep);
|
||||
@ -193,14 +192,15 @@ int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet)
|
||||
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
|
||||
}
|
||||
|
||||
int InkHUD::Events::onAdminMessage(const meshtastic_AdminMessage *message)
|
||||
int InkHUD::Events::onAdminMessage(AdminModule_ObserverData *data)
|
||||
{
|
||||
switch (message->which_payload_variant) {
|
||||
switch (data->request->which_payload_variant) {
|
||||
// Factory reset
|
||||
// Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data.
|
||||
case meshtastic_AdminMessage_factory_reset_device_tag:
|
||||
case meshtastic_AdminMessage_factory_reset_config_tag:
|
||||
eraseOnReboot = true;
|
||||
*data->result = AdminMessageHandleResult::HANDLED;
|
||||
break;
|
||||
|
||||
default:
|
||||
|
@ -13,7 +13,7 @@ however this class handles general events which concern InkHUD as a whole, e.g.
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "Observer.h"
|
||||
#include "modules/AdminModule.h"
|
||||
|
||||
#include "./InkHUD.h"
|
||||
#include "./Persistence.h"
|
||||
@ -33,7 +33,7 @@ class Events
|
||||
int beforeDeepSleep(void *unused); // Prepare for shutdown
|
||||
int beforeReboot(void *unused); // Prepare for reboot
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message
|
||||
int onAdminMessage(const meshtastic_AdminMessage *message); // Handle incoming admin messages
|
||||
int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages
|
||||
#ifdef ARCH_ESP32
|
||||
int beforeLightSleep(void *unused); // Prepare for light sleep
|
||||
#endif
|
||||
@ -54,8 +54,8 @@ class Events
|
||||
CallbackObserver<Events, const meshtastic_MeshPacket *>(this, &Events::onReceiveTextMessage);
|
||||
|
||||
// Get notified of incoming admin messages, and handle any which are relevant to InkHUD
|
||||
CallbackObserver<Events, const meshtastic_AdminMessage *> adminMessageObserver =
|
||||
CallbackObserver<Events, const meshtastic_AdminMessage *>(this, &Events::onAdminMessage);
|
||||
CallbackObserver<Events, AdminModule_ObserverData *> adminMessageObserver =
|
||||
CallbackObserver<Events, AdminModule_ObserverData *>(this, &Events::onAdminMessage);
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Get notified when the system is entering light sleep
|
||||
|
@ -15,8 +15,8 @@ The save / load mechanism is a shared NicheGraphics feature.
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./InkHUD.h"
|
||||
#include "graphics/niche/FlashData.h"
|
||||
#include "graphics/niche/InkHUD/MessageStore.h"
|
||||
#include "graphics/niche/Utils/FlashData.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
|
163
src/graphics/niche/Utils/CannedMessageStore.cpp
Normal file
163
src/graphics/niche/Utils/CannedMessageStore.cpp
Normal file
@ -0,0 +1,163 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "./CannedMessageStore.h"
|
||||
|
||||
#include "FSCommon.h"
|
||||
#include "NodeDB.h"
|
||||
#include "SPILock.h"
|
||||
#include "generated/meshtastic/cannedmessages.pb.h"
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
// Location of the file which stores the canned messages on flash
|
||||
static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto";
|
||||
|
||||
CannedMessageStore::CannedMessageStore()
|
||||
{
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
adminMessageObserver.observe(adminModule);
|
||||
#endif
|
||||
|
||||
// Load & parse messages from flash
|
||||
load();
|
||||
}
|
||||
|
||||
// Get access to (or create) the singleton instance of this class
|
||||
CannedMessageStore *CannedMessageStore::getInstance()
|
||||
{
|
||||
// Instantiate the class the first time this method is called
|
||||
static CannedMessageStore *const singletonInstance = new CannedMessageStore;
|
||||
|
||||
return singletonInstance;
|
||||
}
|
||||
|
||||
// Access canned messages by index
|
||||
// Consumer should check CannedMessageStore::size to avoid accessing out of bounds
|
||||
const std::string &CannedMessageStore::at(uint8_t i)
|
||||
{
|
||||
assert(i < messages.size());
|
||||
return messages.at(i);
|
||||
}
|
||||
|
||||
// Number of canned message strings available
|
||||
uint8_t CannedMessageStore::size()
|
||||
{
|
||||
return messages.size();
|
||||
}
|
||||
|
||||
// Load canned message data from flash, and parse into the individual strings
|
||||
void CannedMessageStore::load()
|
||||
{
|
||||
// In case we're reloading
|
||||
messages.clear();
|
||||
|
||||
// Attempt to load the bulk canned message data from flash
|
||||
meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig;
|
||||
LoadFileResult result = nodeDB->loadProto("/prefs/cannedConf.proto", meshtastic_CannedMessageModuleConfig_size,
|
||||
sizeof(meshtastic_CannedMessageModuleConfig),
|
||||
&meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig);
|
||||
|
||||
// Abort if nothing to load
|
||||
if (result != LoadFileResult::LOAD_SUCCESS || strlen(cannedMessageModuleConfig.messages) == 0)
|
||||
return;
|
||||
|
||||
// Split into individual canned messages
|
||||
// These are concatenated when stored in flash, using '|' as a delimiter
|
||||
std::string s;
|
||||
for (char c : cannedMessageModuleConfig.messages) { // Character by character
|
||||
|
||||
// If found end of a string
|
||||
if (c == '|' || c == '\0') {
|
||||
// Copy into the vector (if non-empty)
|
||||
if (!s.empty())
|
||||
messages.push_back(s);
|
||||
|
||||
// Reset the string builder
|
||||
s.clear();
|
||||
|
||||
// End of data, all strings processed
|
||||
if (c == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
// Otherwise, append char (continue building string)
|
||||
else
|
||||
s.push_back(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle incoming admin messages
|
||||
// We get these as an observer of AdminModule
|
||||
// It's our responsibility to handle setting and getting of canned messages via the client API
|
||||
// Ordinarily, this would be handled by the CannedMessageModule, but it is bound to Screen.cpp, so not suitable for NicheGraphics
|
||||
int CannedMessageStore::onAdminMessage(AdminModule_ObserverData *data)
|
||||
{
|
||||
switch (data->request->which_payload_variant) {
|
||||
|
||||
// Client API changing the canned messages
|
||||
case meshtastic_AdminMessage_set_canned_message_module_messages_tag:
|
||||
handleSet(data->request);
|
||||
*data->result = AdminMessageHandleResult::HANDLED;
|
||||
break;
|
||||
|
||||
// Client API wants to know the current canned messages
|
||||
case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag:
|
||||
handleGet(data->response);
|
||||
*data->result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
|
||||
}
|
||||
|
||||
// Client API changing the canned messages
|
||||
void CannedMessageStore::handleSet(const meshtastic_AdminMessage *request)
|
||||
{
|
||||
// Copy into the correct struct (for writing to flash as protobuf)
|
||||
meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig;
|
||||
strncpy(cannedMessageModuleConfig.messages, request->set_canned_message_module_messages,
|
||||
sizeof(cannedMessageModuleConfig.messages));
|
||||
|
||||
// Ensure the directory exists
|
||||
#ifdef FSCom
|
||||
spiLock->lock();
|
||||
FSCom.mkdir("/prefs");
|
||||
spiLock->unlock();
|
||||
#endif
|
||||
|
||||
// Write to flash
|
||||
nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size,
|
||||
&meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig);
|
||||
|
||||
// Reload from flash, to update the canned messages in RAM
|
||||
// (This is a lazy way to handle it)
|
||||
load();
|
||||
}
|
||||
|
||||
// Client API wants to know the current canned messages
|
||||
// We're reconstructing the monolithic canned message string from our copy of the messages in RAM
|
||||
// Lazy, but more convenient that reloading the monolithic string from flash just for this
|
||||
void CannedMessageStore::handleGet(meshtastic_AdminMessage *response)
|
||||
{
|
||||
// Merge the canned messages back into the delimited format expected
|
||||
std::string merged;
|
||||
if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0
|
||||
merged.reserve(201);
|
||||
for (std::string &s : messages) {
|
||||
merged += s;
|
||||
merged += '|';
|
||||
}
|
||||
merged.pop_back(); // Drop the final delimiter (loop added one too many)
|
||||
}
|
||||
|
||||
// Place the data into the response
|
||||
// This response is scoped to AdminModule::handleReceivedProtobuf
|
||||
// We were passed reference to it via the observable
|
||||
response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag;
|
||||
strncpy(response->get_canned_message_module_messages_response, merged.c_str(), strlen(merged.c_str()) + 1);
|
||||
}
|
||||
|
||||
#endif
|
54
src/graphics/niche/Utils/CannedMessageStore.h
Normal file
54
src/graphics/niche/Utils/CannedMessageStore.h
Normal file
@ -0,0 +1,54 @@
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
/*
|
||||
|
||||
Re-usable NicheGraphics tool
|
||||
|
||||
Makes canned message data accessible to any NicheGraphics UI.
|
||||
- handles loading & parsing from flash
|
||||
- handles the admin messages for setting & getting canned messages via client API (phone apps, etc)
|
||||
|
||||
The original CannedMessageModule class is bound to Screen.cpp,
|
||||
making it incompatible with the NicheGraphics framework, which suppresses Screen.cpp
|
||||
|
||||
This implementation aims to be self-contained.
|
||||
The necessary interaction with the AdminModule is done as an observer.
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "modules/AdminModule.h"
|
||||
|
||||
namespace NicheGraphics
|
||||
{
|
||||
|
||||
class CannedMessageStore
|
||||
{
|
||||
public:
|
||||
static CannedMessageStore *getInstance(); // Create or get the singleton instance
|
||||
const std::string &at(uint8_t i); // Get canned message at index
|
||||
uint8_t size(); // Get total number of canned messages
|
||||
|
||||
int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages
|
||||
|
||||
private:
|
||||
CannedMessageStore(); // Constructor made private: force use of CannedMessageStore::instance()
|
||||
|
||||
void load(); // Load from flash, and parse
|
||||
|
||||
void handleSet(const meshtastic_AdminMessage *request); // Client API changing the canned messages
|
||||
void handleGet(meshtastic_AdminMessage *response); // Client API wants to know current canned messages
|
||||
|
||||
std::vector<std::string> messages;
|
||||
|
||||
// Get notified of incoming admin messages, to get / set canned messages
|
||||
CallbackObserver<CannedMessageStore, AdminModule_ObserverData *> adminMessageObserver =
|
||||
CallbackObserver<CannedMessageStore, AdminModule_ObserverData *>(this, &CannedMessageStore::onAdminMessage);
|
||||
};
|
||||
|
||||
}; // namespace NicheGraphics
|
||||
|
||||
#endif
|
@ -1422,7 +1422,7 @@ extern meshtastic_DeviceMetadata getDeviceMetadata()
|
||||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_AUDIO_CONFIG;
|
||||
#endif
|
||||
// Option to explicitly include canned messages for edge cases, e.g. niche graphics
|
||||
#if (!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES
|
||||
#if ((!HAS_SCREEN || NO_EXT_GPIO) || MESHTASTIC_EXCLUDE_CANNEDMESSAGES) && !defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
|
||||
deviceMetadata.excluded_modules |= meshtastic_ExcludedModules_CANNEDMSG_CONFIG;
|
||||
#endif
|
||||
#if NO_EXT_GPIO
|
||||
|
@ -470,22 +470,38 @@ bool AdminModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, meshta
|
||||
setPassKey(&res);
|
||||
myReply = allocDataProtobuf(res);
|
||||
} else if (mp.decoded.want_response) {
|
||||
LOG_DEBUG("Did not responded to a request that wanted a respond. req.variant=%d", r->which_payload_variant);
|
||||
LOG_DEBUG("Module API did not respond to admin message. req.variant=%d", r->which_payload_variant);
|
||||
} else if (handleResult != AdminMessageHandleResult::HANDLED) {
|
||||
// Probably a message sent by us or sent to our local node. FIXME, we should avoid scanning these messages
|
||||
LOG_DEBUG("Ignore irrelevant admin %d", r->which_payload_variant);
|
||||
LOG_DEBUG("Module API did not handle admin message %d", r->which_payload_variant);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Allow any observers (e.g. the UI) to handle/respond
|
||||
AdminMessageHandleResult observerResult = AdminMessageHandleResult::NOT_HANDLED;
|
||||
meshtastic_AdminMessage observerResponse = meshtastic_AdminMessage_init_default;
|
||||
AdminModule_ObserverData observerData = {
|
||||
.request = r,
|
||||
.response = &observerResponse,
|
||||
.result = &observerResult,
|
||||
};
|
||||
|
||||
notifyObservers(&observerData);
|
||||
|
||||
if (observerResult == AdminMessageHandleResult::HANDLED_WITH_RESPONSE) {
|
||||
setPassKey(&observerResponse);
|
||||
myReply = allocDataProtobuf(observerResponse);
|
||||
LOG_DEBUG("Observer responded to admin message");
|
||||
} else if (observerResult == AdminMessageHandleResult::HANDLED) {
|
||||
LOG_DEBUG("Observer handled admin message");
|
||||
}
|
||||
|
||||
// If asked for a response and it is not yet set, generate an 'ACK' response
|
||||
if (mp.decoded.want_response && !myReply) {
|
||||
myReply = allocErrorResponse(meshtastic_Routing_Error_NONE, &mp);
|
||||
}
|
||||
|
||||
// Allow any observers (e.g. the UI) to respond to this event
|
||||
notifyObservers(r);
|
||||
|
||||
return handled;
|
||||
}
|
||||
|
||||
|
@ -6,10 +6,19 @@
|
||||
#include "mesh/wifi/WiFiAPClient.h"
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Datatype passed to Observers by AdminModule, to allow external handling of admin messages
|
||||
*/
|
||||
struct AdminModule_ObserverData {
|
||||
const meshtastic_AdminMessage *request;
|
||||
meshtastic_AdminMessage *response;
|
||||
AdminMessageHandleResult *result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Admin module for admin messages
|
||||
*/
|
||||
class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Observable<const meshtastic_AdminMessage *>
|
||||
class AdminModule : public ProtobufModule<meshtastic_AdminMessage>, public Observable<AdminModule_ObserverData *>
|
||||
{
|
||||
public:
|
||||
/** Constructor
|
||||
|
Loading…
Reference in New Issue
Block a user