mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-22 10:42:49 +00:00
Introducing Default screen Nodelist (Last head nodes) Distance Screen Compass screen Hops and Signal Screen Improved node receipient list using navigation bar and search bar for canned messages
1587 lines
65 KiB
C++
1587 lines
65 KiB
C++
#include "configuration.h"
|
|
#if ARCH_PORTDUINO
|
|
#include "PortduinoGlue.h"
|
|
#endif
|
|
#if HAS_SCREEN
|
|
#include "CannedMessageModule.h"
|
|
#include "Channels.h"
|
|
#include "FSCommon.h"
|
|
#include "MeshService.h"
|
|
#include "NodeDB.h"
|
|
#include "PowerFSM.h" // needed for button bypass
|
|
#include "SPILock.h"
|
|
#include "detect/ScanI2C.h"
|
|
#include "input/ScanAndSelect.h"
|
|
#include "mesh/generated/meshtastic/cannedmessages.pb.h"
|
|
#include "modules/AdminModule.h"
|
|
|
|
#include "main.h" // for cardkb_found
|
|
#include "modules/ExternalNotificationModule.h" // for buzzer control
|
|
#if !MESHTASTIC_EXCLUDE_GPS
|
|
#include "GPS.h"
|
|
#endif
|
|
#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
|
|
#include "graphics/EInkDynamicDisplay.h" // To select between full and fast refresh on E-Ink displays
|
|
#endif
|
|
|
|
#ifndef INPUTBROKER_MATRIX_TYPE
|
|
#define INPUTBROKER_MATRIX_TYPE 0
|
|
#endif
|
|
|
|
#include "graphics/ScreenFonts.h"
|
|
#include <Throttle.h>
|
|
|
|
// Remove Canned message screen if no action is taken for some milliseconds
|
|
#define INACTIVATE_AFTER_MS 20000
|
|
|
|
extern ScanI2C::DeviceAddress cardkb_found;
|
|
|
|
static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto";
|
|
|
|
meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig;
|
|
|
|
CannedMessageModule *cannedMessageModule;
|
|
|
|
CannedMessageModule::CannedMessageModule()
|
|
: SinglePortModule("canned", meshtastic_PortNum_TEXT_MESSAGE_APP), concurrency::OSThread("CannedMessage")
|
|
{
|
|
if (moduleConfig.canned_message.enabled || CANNED_MESSAGE_MODULE_ENABLE) {
|
|
this->loadProtoForModule();
|
|
if ((this->splitConfiguredMessages() <= 0) && (cardkb_found.address == 0x00) && !INPUTBROKER_MATRIX_TYPE &&
|
|
!CANNED_MESSAGE_MODULE_ENABLE) {
|
|
LOG_INFO("CannedMessageModule: No messages are configured. Module is disabled");
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED;
|
|
disable();
|
|
} else {
|
|
LOG_INFO("CannedMessageModule is enabled");
|
|
|
|
// T-Watch interface currently has no way to select destination type, so default to 'node'
|
|
#if defined(USE_VIRTUAL_KEYBOARD)
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE;
|
|
#endif
|
|
|
|
this->inputObserver.observe(inputBroker);
|
|
}
|
|
} else {
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED;
|
|
disable();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Items in array this->messages will be set to be pointing on the right
|
|
* starting points of the string this->messageStore
|
|
*
|
|
* @return int Returns the number of messages found.
|
|
*/
|
|
// FIXME: This is just one set of messages now
|
|
int CannedMessageModule::splitConfiguredMessages()
|
|
{
|
|
int messageIndex = 0;
|
|
int i = 0;
|
|
|
|
String canned_messages = cannedMessageModuleConfig.messages;
|
|
|
|
#if defined(USE_VIRTUAL_KEYBOARD)
|
|
String separator = canned_messages.length() ? "|" : "";
|
|
|
|
canned_messages = "[---- Free Text ----]" + separator + canned_messages;
|
|
#endif
|
|
|
|
// collect all the message parts
|
|
strncpy(this->messageStore, canned_messages.c_str(), sizeof(this->messageStore));
|
|
|
|
// The first message points to the beginning of the store.
|
|
this->messages[messageIndex++] = this->messageStore;
|
|
int upTo = strlen(this->messageStore) - 1;
|
|
|
|
while (i < upTo) {
|
|
if (this->messageStore[i] == '|') {
|
|
// Message ending found, replace it with string-end character.
|
|
this->messageStore[i] = '\0';
|
|
|
|
// hit our max messages, bail
|
|
if (messageIndex >= CANNED_MESSAGE_MODULE_MESSAGE_MAX_COUNT) {
|
|
this->messagesCount = messageIndex;
|
|
return this->messagesCount;
|
|
}
|
|
|
|
// Next message starts after pipe (|) just found.
|
|
this->messages[messageIndex++] = (this->messageStore + i + 1);
|
|
}
|
|
i += 1;
|
|
}
|
|
if (strlen(this->messages[messageIndex - 1]) > 0) {
|
|
// We have a last message.
|
|
LOG_DEBUG("CannedMessage %d is: '%s'", messageIndex - 1, this->messages[messageIndex - 1]);
|
|
this->messagesCount = messageIndex;
|
|
} else {
|
|
this->messagesCount = messageIndex - 1;
|
|
}
|
|
|
|
return this->messagesCount;
|
|
}
|
|
void CannedMessageModule::resetSearch() {
|
|
LOG_INFO("Resetting search, restoring full destination list");
|
|
updateFilteredNodes(); // Reload all nodes and channels
|
|
requestFocus();
|
|
}
|
|
void CannedMessageModule::updateFilteredNodes() {
|
|
static size_t lastNumMeshNodes = 0; // Track the last known node count
|
|
static String lastSearchQuery = ""; // Track last search query
|
|
|
|
size_t numMeshNodes = nodeDB->getNumMeshNodes();
|
|
|
|
// If the number of nodes has changed, force an update
|
|
bool nodesChanged = (numMeshNodes != lastNumMeshNodes);
|
|
lastNumMeshNodes = numMeshNodes;
|
|
|
|
// Also check if search query changed
|
|
if (searchQuery == lastSearchQuery && !nodesChanged) return;
|
|
|
|
lastSearchQuery = searchQuery;
|
|
needsUpdate = false;
|
|
|
|
this->filteredNodes.clear();
|
|
this->activeChannelIndices.clear();
|
|
|
|
NodeNum myNodeNum = nodeDB->getNodeNum();
|
|
|
|
for (size_t i = 0; i < numMeshNodes; i++) {
|
|
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
|
if (!node || node->num == myNodeNum) continue;
|
|
|
|
String nodeName = node->user.long_name;
|
|
String lowerNodeName = nodeName;
|
|
String lowerSearchQuery = searchQuery;
|
|
|
|
lowerNodeName.toLowerCase();
|
|
lowerSearchQuery.toLowerCase();
|
|
|
|
if (searchQuery.length() == 0 || lowerNodeName.indexOf(lowerSearchQuery) != -1) {
|
|
this->filteredNodes.push_back({node, sinceLastSeen(node)});
|
|
}
|
|
}
|
|
|
|
// Populate active channels
|
|
this->activeChannelIndices.clear();
|
|
std::vector<String> seenChannels;
|
|
for (uint8_t i = 0; i < channels.getNumChannels(); i++) {
|
|
String channelName = channels.getName(i);
|
|
if (channelName.length() > 0 && std::find(seenChannels.begin(), seenChannels.end(), channelName) == seenChannels.end()) {
|
|
this->activeChannelIndices.push_back(i);
|
|
seenChannels.push_back(channelName);
|
|
}
|
|
}
|
|
|
|
// Sort nodes by favorite status and last seen time
|
|
std::sort(this->filteredNodes.begin(), this->filteredNodes.end(), [](const NodeEntry &a, const NodeEntry &b) {
|
|
if (a.node->is_favorite != b.node->is_favorite) {
|
|
return a.node->is_favorite > b.node->is_favorite; // Favorited nodes first
|
|
}
|
|
return a.lastHeard < b.lastHeard; // Otherwise, sort by last heard (oldest first)
|
|
});
|
|
|
|
// 🔹 If nodes have changed, refresh the screen
|
|
if (nodesChanged) {
|
|
LOG_INFO("Nodes changed, forcing UI refresh.");
|
|
screen->forceDisplay();
|
|
}
|
|
}
|
|
|
|
int CannedMessageModule::handleInputEvent(const InputEvent *event)
|
|
{
|
|
if ((strlen(moduleConfig.canned_message.allow_input_source) > 0) &&
|
|
(strcasecmp(moduleConfig.canned_message.allow_input_source, event->source) != 0) &&
|
|
(strcasecmp(moduleConfig.canned_message.allow_input_source, "_any") != 0)) {
|
|
// Event source is not accepted.
|
|
// Event only accepted if source matches the configured one, or
|
|
// the configured one is "_any" (or if there is no configured
|
|
// source at all)
|
|
return 0;
|
|
}
|
|
if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) {
|
|
return 0; // Ignore input while sending
|
|
}
|
|
static int lastDestIndex = -1; // Cache the last index
|
|
bool selectionChanged = false; // Track if UI needs redrawing
|
|
|
|
bool isUp = event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP);
|
|
bool isDown = event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN);
|
|
|
|
if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) {
|
|
if (event->kbchar >= 32 && event->kbchar <= 126) {
|
|
this->searchQuery += event->kbchar;
|
|
return 0;
|
|
}
|
|
|
|
size_t numMeshNodes = this->filteredNodes.size();
|
|
int totalEntries = numMeshNodes + this->activeChannelIndices.size();
|
|
int columns = 2;
|
|
int totalRows = (totalEntries + columns - 1) / columns;
|
|
int maxScrollIndex = std::max(0, totalRows - this->visibleRows);
|
|
scrollIndex = std::max(0, std::min(scrollIndex, maxScrollIndex));
|
|
|
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK)) {
|
|
if (this->searchQuery.length() > 0) {
|
|
this->searchQuery.remove(this->searchQuery.length() - 1);
|
|
}
|
|
if (this->searchQuery.length() == 0) {
|
|
resetSearch(); // Function to restore all destinations
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
bool needsRedraw = false;
|
|
|
|
// 🔼 UP Navigation in Node Selection
|
|
if (isUp) {
|
|
if ((this->destIndex / columns) <= scrollIndex) {
|
|
if (scrollIndex > 0) {
|
|
scrollIndex--;
|
|
needsRedraw = true;
|
|
}
|
|
} else if (this->destIndex >= columns) {
|
|
this->destIndex -= columns;
|
|
}
|
|
}
|
|
|
|
// 🔽 DOWN Navigation in Node Selection
|
|
if (isDown) {
|
|
if ((this->destIndex / columns) >= (scrollIndex + this->visibleRows - 1)) {
|
|
if (scrollIndex < maxScrollIndex) {
|
|
scrollIndex++;
|
|
needsRedraw = true;
|
|
}
|
|
} else if (this->destIndex + columns < totalEntries) {
|
|
this->destIndex += columns;
|
|
}
|
|
}
|
|
|
|
|
|
// ◀ LEFT Navigation (Wrap to previous row OR last row)
|
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) {
|
|
if (this->destIndex % columns == 0) {
|
|
if (this->destIndex >= columns) {
|
|
this->destIndex = this->destIndex - columns + (columns - 1);
|
|
} else {
|
|
int lastRowStart = ((totalEntries - 1) / columns) * columns;
|
|
this->destIndex = std::min(lastRowStart + (columns - 1), totalEntries - 1);
|
|
}
|
|
} else {
|
|
this->destIndex--;
|
|
}
|
|
}
|
|
|
|
// ▶ RIGHT Navigation (Wrap to next row OR first row)
|
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) {
|
|
int nextIndex = this->destIndex + 1;
|
|
if ((this->destIndex + 1) % columns == 0 || nextIndex >= totalEntries) {
|
|
if (this->destIndex + columns < totalEntries) {
|
|
this->destIndex = this->destIndex + columns - (columns - 1);
|
|
} else {
|
|
this->destIndex = 0;
|
|
}
|
|
} else {
|
|
this->destIndex++;
|
|
}
|
|
}
|
|
|
|
if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NODE) {
|
|
if (isUp && this->messagesCount > 0) {
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP;
|
|
return 0;
|
|
}
|
|
if (isDown && this->messagesCount > 0) {
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN;
|
|
return 0;
|
|
}
|
|
}
|
|
// Only refresh UI when needed
|
|
if (needsRedraw) {
|
|
screen->forceDisplay();
|
|
}
|
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) {
|
|
if (this->destIndex < static_cast<int>(this->activeChannelIndices.size())) {
|
|
this->dest = NODENUM_BROADCAST;
|
|
this->channel = this->activeChannelIndices[this->destIndex];
|
|
} else {
|
|
int nodeIndex = this->destIndex - static_cast<int>(this->activeChannelIndices.size());
|
|
if (nodeIndex >= 0 && nodeIndex < static_cast<int>(this->filteredNodes.size())) {
|
|
meshtastic_NodeInfoLite *selectedNode = this->filteredNodes[nodeIndex].node;
|
|
if (selectedNode) {
|
|
this->dest = selectedNode->num;
|
|
this->channel = selectedNode->channel;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ✅ Now correctly switches to FreeText screen with selected node/channel
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
screen->forceDisplay();
|
|
return 0;
|
|
}
|
|
|
|
// Handle Cancel (ESC)
|
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) {
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE; // Ensure return to main screen
|
|
this->searchQuery = "";
|
|
|
|
UIFrameEvent e;
|
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
|
this->notifyObservers(&e);
|
|
|
|
screen->forceDisplay();
|
|
return 0; // 🚀 Prevents input from affecting canned messages
|
|
}
|
|
|
|
return 0; // 🚀 FINAL EARLY EXIT: Stops the function from continuing into canned message handling
|
|
}
|
|
// If we reach here, we are NOT in Select Destination mode.
|
|
// The remaining logic is for canned message handling.
|
|
bool validEvent = false;
|
|
if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NODE) {
|
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP)) {
|
|
if (this->messagesCount > 0) {
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_UP;
|
|
validEvent = true;
|
|
}
|
|
}
|
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN)) {
|
|
if (this->messagesCount > 0) {
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN;
|
|
validEvent = true;
|
|
}
|
|
}
|
|
}
|
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) {
|
|
|
|
#if defined(USE_VIRTUAL_KEYBOARD)
|
|
if (this->currentMessageIndex == 0) {
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
|
|
|
|
requestFocus(); // Tell Screen::setFrames to move to our module's frame, next time it runs
|
|
UIFrameEvent e;
|
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen
|
|
this->notifyObservers(&e);
|
|
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
// when inactive, call the onebutton shortpress instead. Activate Module only on up/down
|
|
if ((this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED)) {
|
|
powerFSM.trigger(EVENT_PRESS);
|
|
} else {
|
|
this->payload = this->runState;
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT;
|
|
validEvent = true;
|
|
}
|
|
}
|
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL)) {
|
|
// If in Node Selection Mode, exit and return to FreeText Mode
|
|
if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) {
|
|
updateFilteredNodes(); // Ensure the filtered node list is refreshed before selecting
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
|
|
return 0;
|
|
}
|
|
|
|
// Default behavior for Cancel in other modes
|
|
UIFrameEvent e;
|
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen
|
|
this->currentMessageIndex = -1;
|
|
|
|
#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD)
|
|
this->freetext = ""; // clear freetext
|
|
this->cursor = 0;
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
#endif
|
|
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
this->notifyObservers(&e);
|
|
screen->forceDisplay(); // Ensure the UI updates properly
|
|
return 0;
|
|
}
|
|
if ((event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK)) ||
|
|
(event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) ||
|
|
(event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT))) {
|
|
|
|
#if defined(USE_VIRTUAL_KEYBOARD)
|
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) {
|
|
this->payload = INPUT_BROKER_MSG_LEFT;
|
|
} else if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) {
|
|
this->payload = INPUT_BROKER_MSG_RIGHT;
|
|
}
|
|
#else
|
|
// tweak for left/right events generated via trackball/touch with empty kbchar
|
|
if (!event->kbchar) {
|
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) {
|
|
this->payload = INPUT_BROKER_MSG_LEFT;
|
|
} else if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) {
|
|
this->payload = INPUT_BROKER_MSG_RIGHT;
|
|
}
|
|
} else {
|
|
// pass the pressed key
|
|
this->payload = event->kbchar;
|
|
}
|
|
#endif
|
|
|
|
this->lastTouchMillis = millis();
|
|
validEvent = true;
|
|
}
|
|
if (event->inputEvent == static_cast<char>(ANYKEY)) {
|
|
// when inactive, this will switch to the freetext mode
|
|
if ((this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) ||
|
|
(this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED)) {
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_FREETEXT;
|
|
}
|
|
|
|
validEvent = false; // If key is normal than it will be set to true.
|
|
|
|
// Run modifier key code below, (doesnt inturrupt typing or reset to start screen page)
|
|
switch (event->kbchar) {
|
|
case INPUT_BROKER_MSG_BRIGHTNESS_UP: // make screen brighter
|
|
if (screen)
|
|
screen->increaseBrightness();
|
|
LOG_DEBUG("Increase Screen Brightness");
|
|
break;
|
|
case INPUT_BROKER_MSG_BRIGHTNESS_DOWN: // make screen dimmer
|
|
if (screen)
|
|
screen->decreaseBrightness();
|
|
LOG_DEBUG("Decrease Screen Brightness");
|
|
break;
|
|
case INPUT_BROKER_MSG_FN_SYMBOL_ON: // draw modifier (function) symbol
|
|
if (screen)
|
|
screen->setFunctionSymbol("Fn");
|
|
break;
|
|
case INPUT_BROKER_MSG_FN_SYMBOL_OFF: // remove modifier (function) symbol
|
|
if (screen)
|
|
screen->removeFunctionSymbol("Fn");
|
|
break;
|
|
// mute (switch off/toggle) external notifications on fn+m
|
|
case INPUT_BROKER_MSG_MUTE_TOGGLE:
|
|
if (moduleConfig.external_notification.enabled == true) {
|
|
if (externalNotificationModule->getMute()) {
|
|
externalNotificationModule->setMute(false);
|
|
showTemporaryMessage("Notifications \nEnabled");
|
|
if (screen)
|
|
screen->removeFunctionSymbol("M"); // remove the mute symbol from the bottom right corner
|
|
} else {
|
|
externalNotificationModule->stopNow(); // this will turn off all GPIO and sounds and idle the loop
|
|
externalNotificationModule->setMute(true);
|
|
showTemporaryMessage("Notifications \nDisabled");
|
|
if (screen)
|
|
screen->setFunctionSymbol("M"); // add the mute symbol to the bottom right corner
|
|
}
|
|
}
|
|
break;
|
|
case INPUT_BROKER_MSG_GPS_TOGGLE: // toggle GPS like triple press does
|
|
#if !MESHTASTIC_EXCLUDE_GPS
|
|
if (gps != nullptr) {
|
|
gps->toggleGpsMode();
|
|
}
|
|
if (screen)
|
|
screen->forceDisplay();
|
|
showTemporaryMessage("GPS Toggled");
|
|
#endif
|
|
break;
|
|
case INPUT_BROKER_MSG_BLUETOOTH_TOGGLE: // toggle Bluetooth on/off
|
|
if (config.bluetooth.enabled == true) {
|
|
config.bluetooth.enabled = false;
|
|
LOG_INFO("User toggled Bluetooth");
|
|
nodeDB->saveToDisk();
|
|
disableBluetooth();
|
|
showTemporaryMessage("Bluetooth OFF");
|
|
} else if (config.bluetooth.enabled == false) {
|
|
config.bluetooth.enabled = true;
|
|
LOG_INFO("User toggled Bluetooth");
|
|
nodeDB->saveToDisk();
|
|
rebootAtMsec = millis() + 2000;
|
|
showTemporaryMessage("Bluetooth ON\nReboot");
|
|
}
|
|
break;
|
|
case INPUT_BROKER_MSG_SEND_PING: // fn+space send network ping like double press does
|
|
service->refreshLocalMeshNode();
|
|
if (service->trySendPosition(NODENUM_BROADCAST, true)) {
|
|
showTemporaryMessage("Position \nUpdate Sent");
|
|
} else {
|
|
showTemporaryMessage("Node Info \nUpdate Sent");
|
|
}
|
|
break;
|
|
case INPUT_BROKER_MSG_DISMISS_FRAME: // fn+del: dismiss screen frames like text or waypoint
|
|
// Avoid opening the canned message screen frame
|
|
// We're only handling the keypress here by convention, this has nothing to do with canned messages
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
// Attempt to close whatever frame is currently shown on display
|
|
screen->dismissCurrentFrame();
|
|
return 0;
|
|
default:
|
|
// pass the pressed key
|
|
// LOG_DEBUG("Canned message ANYKEY (%x)", event->kbchar);
|
|
this->payload = event->kbchar;
|
|
this->lastTouchMillis = millis();
|
|
validEvent = true;
|
|
break;
|
|
}
|
|
if (screen && (event->kbchar != INPUT_BROKER_MSG_FN_SYMBOL_ON)) {
|
|
screen->removeFunctionSymbol("Fn"); // remove modifier (function) symbol
|
|
}
|
|
}
|
|
|
|
#if defined(USE_VIRTUAL_KEYBOARD)
|
|
if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
|
String keyTapped = keyForCoordinates(event->touchX, event->touchY);
|
|
|
|
if (keyTapped == "⇧") {
|
|
this->highlight = -1;
|
|
|
|
this->payload = 0x00;
|
|
|
|
validEvent = true;
|
|
|
|
this->shift = !this->shift;
|
|
} else if (keyTapped == "⌫") {
|
|
#ifndef RAK14014
|
|
this->highlight = keyTapped[0];
|
|
#endif
|
|
|
|
this->payload = 0x08;
|
|
|
|
validEvent = true;
|
|
|
|
this->shift = false;
|
|
} else if (keyTapped == "123" || keyTapped == "ABC") {
|
|
this->highlight = -1;
|
|
|
|
this->payload = 0x00;
|
|
|
|
this->charSet = this->charSet == 0 ? 1 : 0;
|
|
|
|
validEvent = true;
|
|
} else if (keyTapped == " ") {
|
|
#ifndef RAK14014
|
|
this->highlight = keyTapped[0];
|
|
#endif
|
|
|
|
this->payload = keyTapped[0];
|
|
|
|
validEvent = true;
|
|
|
|
this->shift = false;
|
|
} else if (keyTapped == "↵") {
|
|
this->highlight = 0x00;
|
|
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT;
|
|
|
|
this->payload = CANNED_MESSAGE_RUN_STATE_FREETEXT;
|
|
|
|
this->currentMessageIndex = event->kbchar - 1;
|
|
|
|
validEvent = true;
|
|
|
|
this->shift = false;
|
|
} else if (keyTapped != "") {
|
|
#ifndef RAK14014
|
|
this->highlight = keyTapped[0];
|
|
#endif
|
|
|
|
this->payload = this->shift ? keyTapped[0] : std::tolower(keyTapped[0]);
|
|
|
|
validEvent = true;
|
|
|
|
this->shift = false;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (event->inputEvent == static_cast<char>(MATRIXKEY)) {
|
|
// this will send the text immediately on matrix press
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_SELECT;
|
|
this->payload = MATRIXKEY;
|
|
this->currentMessageIndex = event->kbchar - 1;
|
|
this->lastTouchMillis = millis();
|
|
validEvent = true;
|
|
}
|
|
|
|
if (validEvent) {
|
|
requestFocus(); // Tell Screen::setFrames to move to our module's frame, next time it runs
|
|
|
|
// Let runOnce to be called immediately.
|
|
if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) {
|
|
setIntervalFromNow(0); // on fast keypresses, this isn't fast enough.
|
|
} else {
|
|
runOnce();
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void CannedMessageModule::sendText(NodeNum dest, ChannelIndex channel, const char *message, bool wantReplies)
|
|
{
|
|
meshtastic_MeshPacket *p = allocDataPacket();
|
|
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);
|
|
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'; // Bell character
|
|
p->decoded.payload.size++;
|
|
}
|
|
|
|
// Only receive routing messages when expecting ACK for a canned message
|
|
// Prevents the canned message module from regenerating the screen's frameset at unexpected times,
|
|
// or raising a UIFrameEvent before another module has the chance
|
|
this->waitingForAck = true;
|
|
|
|
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. Even if there's no phone connected, this stores the message to match ACKs
|
|
}
|
|
|
|
int32_t CannedMessageModule::runOnce()
|
|
{
|
|
updateFilteredNodes();
|
|
if (((!moduleConfig.canned_message.enabled) && !CANNED_MESSAGE_MODULE_ENABLE) ||
|
|
(this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) {
|
|
temporaryMessage = "";
|
|
return INT32_MAX;
|
|
}
|
|
// LOG_DEBUG("Check status");
|
|
UIFrameEvent e;
|
|
if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) ||
|
|
(this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) || (this->runState == CANNED_MESSAGE_RUN_STATE_MESSAGE)) {
|
|
// TODO: might have some feedback of sending state
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
temporaryMessage = "";
|
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen
|
|
this->currentMessageIndex = -1;
|
|
this->freetext = ""; // clear freetext
|
|
this->cursor = 0;
|
|
|
|
#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(SENSECAP_INDICATOR)
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
#endif
|
|
|
|
this->notifyObservers(&e);
|
|
} else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) &&
|
|
!Throttle::isWithinTimespanMs(this->lastTouchMillis, INACTIVATE_AFTER_MS)) {
|
|
// Reset module
|
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen
|
|
this->currentMessageIndex = -1;
|
|
this->freetext = ""; // clear freetext
|
|
this->cursor = 0;
|
|
|
|
#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD)
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
#endif
|
|
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
this->notifyObservers(&e);
|
|
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_SELECT) {
|
|
if (this->payload == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
|
if (this->freetext.length() > 0) {
|
|
sendText(this->dest, this->channel, this->freetext.c_str(), true);
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
|
|
} else {
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
}
|
|
} else {
|
|
if ((this->messagesCount > this->currentMessageIndex) && (strlen(this->messages[this->currentMessageIndex]) > 0)) {
|
|
if (strcmp(this->messages[this->currentMessageIndex], "~") == 0) {
|
|
powerFSM.trigger(EVENT_PRESS);
|
|
return INT32_MAX;
|
|
} else {
|
|
#if defined(USE_VIRTUAL_KEYBOARD)
|
|
sendText(this->dest, indexChannels[this->channel], this->messages[this->currentMessageIndex], true);
|
|
#else
|
|
sendText(this->dest, this->channel, this->messages[this->currentMessageIndex], true);
|
|
#endif
|
|
}
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
|
|
} else {
|
|
// LOG_DEBUG("Reset message is empty");
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
}
|
|
}
|
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen
|
|
this->currentMessageIndex = -1;
|
|
this->freetext = ""; // clear freetext
|
|
this->cursor = 0;
|
|
|
|
#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD)
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
#endif
|
|
|
|
this->notifyObservers(&e);
|
|
return 2000;
|
|
} else if ((this->runState != CANNED_MESSAGE_RUN_STATE_FREETEXT) && (this->currentMessageIndex == -1)) {
|
|
this->currentMessageIndex = 0;
|
|
LOG_DEBUG("First touch (%d):%s", this->currentMessageIndex, this->getCurrentMessage());
|
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
|
|
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_UP) {
|
|
if (this->messagesCount > 0) {
|
|
this->currentMessageIndex = getPrevIndex();
|
|
this->freetext = ""; // clear freetext
|
|
this->cursor = 0;
|
|
|
|
#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD)
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
#endif
|
|
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
|
|
LOG_DEBUG("MOVE UP (%d):%s", this->currentMessageIndex, this->getCurrentMessage());
|
|
}
|
|
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTION_DOWN) {
|
|
if (this->messagesCount > 0) {
|
|
this->currentMessageIndex = this->getNextIndex();
|
|
this->freetext = ""; // clear freetext
|
|
this->cursor = 0;
|
|
|
|
#if !defined(T_WATCH_S3) && !defined(RAK14014) && !defined(USE_VIRTUAL_KEYBOARD)
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
#endif
|
|
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
|
|
LOG_DEBUG("MOVE DOWN (%d):%s", this->currentMessageIndex, this->getCurrentMessage());
|
|
}
|
|
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) {
|
|
switch (this->payload) {
|
|
case INPUT_BROKER_MSG_LEFT:
|
|
if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) {
|
|
size_t numMeshNodes = nodeDB->getNumMeshNodes();
|
|
if (this->dest == NODENUM_BROADCAST) {
|
|
this->dest = nodeDB->getNodeNum();
|
|
}
|
|
for (unsigned int i = 0; i < numMeshNodes; i++) {
|
|
if (nodeDB->getMeshNodeByIndex(i)->num == this->dest) {
|
|
this->dest =
|
|
(i > 0) ? nodeDB->getMeshNodeByIndex(i - 1)->num : nodeDB->getMeshNodeByIndex(numMeshNodes - 1)->num;
|
|
break;
|
|
}
|
|
}
|
|
if (this->dest == nodeDB->getNodeNum()) {
|
|
this->dest = NODENUM_BROADCAST;
|
|
}
|
|
} else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) {
|
|
for (unsigned int i = 0; i < channels.getNumChannels(); i++) {
|
|
if ((channels.getByIndex(i).role == meshtastic_Channel_Role_SECONDARY) ||
|
|
(channels.getByIndex(i).role == meshtastic_Channel_Role_PRIMARY)) {
|
|
indexChannels[numChannels] = i;
|
|
numChannels++;
|
|
}
|
|
}
|
|
if (this->channel == 0) {
|
|
this->channel = numChannels - 1;
|
|
} else {
|
|
this->channel--;
|
|
}
|
|
} else {
|
|
if (this->cursor > 0) {
|
|
this->cursor--;
|
|
}
|
|
}
|
|
break;
|
|
case INPUT_BROKER_MSG_RIGHT:
|
|
if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) {
|
|
size_t numMeshNodes = nodeDB->getNumMeshNodes();
|
|
if (this->dest == NODENUM_BROADCAST) {
|
|
this->dest = nodeDB->getNodeNum();
|
|
}
|
|
for (unsigned int i = 0; i < numMeshNodes; i++) {
|
|
if (nodeDB->getMeshNodeByIndex(i)->num == this->dest) {
|
|
this->dest =
|
|
(i < numMeshNodes - 1) ? nodeDB->getMeshNodeByIndex(i + 1)->num : nodeDB->getMeshNodeByIndex(0)->num;
|
|
break;
|
|
}
|
|
}
|
|
if (this->dest == nodeDB->getNodeNum()) {
|
|
this->dest = NODENUM_BROADCAST;
|
|
}
|
|
} else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) {
|
|
for (unsigned int i = 0; i < channels.getNumChannels(); i++) {
|
|
if ((channels.getByIndex(i).role == meshtastic_Channel_Role_SECONDARY) ||
|
|
(channels.getByIndex(i).role == meshtastic_Channel_Role_PRIMARY)) {
|
|
indexChannels[numChannels] = i;
|
|
numChannels++;
|
|
}
|
|
}
|
|
if (this->channel == numChannels - 1) {
|
|
this->channel = 0;
|
|
} else {
|
|
this->channel++;
|
|
}
|
|
} else {
|
|
if (this->cursor < this->freetext.length()) {
|
|
this->cursor++;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen
|
|
switch (this->payload) { // code below all trigger the freetext window (where you type to send a message) or reset the
|
|
// display back to the default window
|
|
case 0x08: // backspace
|
|
if (this->freetext.length() > 0 && this->highlight == 0x00) {
|
|
if (this->cursor == this->freetext.length()) {
|
|
this->freetext = this->freetext.substring(0, this->freetext.length() - 1);
|
|
} else {
|
|
this->freetext = this->freetext.substring(0, this->cursor - 1) +
|
|
this->freetext.substring(this->cursor, this->freetext.length());
|
|
}
|
|
this->cursor--;
|
|
}
|
|
break;
|
|
case 0x09: // Tab key (Switch to Destination Selection Mode)
|
|
{
|
|
if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) {
|
|
// Enter selection screen
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE;
|
|
this->destIndex = 0; // Reset to first node/channel
|
|
this->scrollIndex = 0; // Reset scrolling
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_DESTINATION_SELECTION;
|
|
|
|
// Ensure UI updates correctly
|
|
UIFrameEvent e;
|
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET;
|
|
this->notifyObservers(&e);
|
|
}
|
|
|
|
// If already inside the selection screen, do nothing (prevent exiting)
|
|
return 0;
|
|
}
|
|
break;
|
|
case INPUT_BROKER_MSG_LEFT:
|
|
case INPUT_BROKER_MSG_RIGHT:
|
|
// already handled above
|
|
break;
|
|
// handle fn+s for shutdown
|
|
case INPUT_BROKER_MSG_SHUTDOWN:
|
|
if (screen)
|
|
screen->startAlert("Shutting down...");
|
|
shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000;
|
|
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
break;
|
|
// and fn+r for reboot
|
|
case INPUT_BROKER_MSG_REBOOT:
|
|
if (screen)
|
|
screen->startAlert("Rebooting...");
|
|
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
|
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
break;
|
|
default:
|
|
if (this->highlight != 0x00) {
|
|
break;
|
|
}
|
|
|
|
if (this->cursor == this->freetext.length()) {
|
|
this->freetext += this->payload;
|
|
} else {
|
|
this->freetext =
|
|
this->freetext.substring(0, this->cursor) + this->payload + this->freetext.substring(this->cursor);
|
|
}
|
|
|
|
this->cursor += 1;
|
|
|
|
uint16_t maxChars = meshtastic_Constants_DATA_PAYLOAD_LEN - (moduleConfig.canned_message.send_bell ? 1 : 0);
|
|
if (this->freetext.length() > maxChars) {
|
|
this->cursor = maxChars;
|
|
this->freetext = this->freetext.substring(0, maxChars);
|
|
}
|
|
break;
|
|
}
|
|
if (screen)
|
|
screen->removeFunctionSymbol("Fn");
|
|
}
|
|
|
|
this->lastTouchMillis = millis();
|
|
this->notifyObservers(&e);
|
|
return INACTIVATE_AFTER_MS;
|
|
}
|
|
|
|
if (this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) {
|
|
this->lastTouchMillis = millis();
|
|
this->notifyObservers(&e);
|
|
return INACTIVATE_AFTER_MS;
|
|
}
|
|
|
|
return INT32_MAX;
|
|
}
|
|
|
|
const char *CannedMessageModule::getCurrentMessage()
|
|
{
|
|
return this->messages[this->currentMessageIndex];
|
|
}
|
|
const char *CannedMessageModule::getPrevMessage()
|
|
{
|
|
return this->messages[this->getPrevIndex()];
|
|
}
|
|
const char *CannedMessageModule::getNextMessage()
|
|
{
|
|
return this->messages[this->getNextIndex()];
|
|
}
|
|
const char *CannedMessageModule::getMessageByIndex(int index)
|
|
{
|
|
return (index >= 0 && index < this->messagesCount) ? this->messages[index] : "";
|
|
}
|
|
|
|
const char *CannedMessageModule::getNodeName(NodeNum node)
|
|
{
|
|
if (node == NODENUM_BROADCAST) {
|
|
return "Broadcast";
|
|
} else {
|
|
meshtastic_NodeInfoLite *info = nodeDB->getMeshNode(node);
|
|
if (info != NULL) {
|
|
return info->user.long_name;
|
|
} else {
|
|
return "Unknown";
|
|
}
|
|
}
|
|
}
|
|
|
|
bool CannedMessageModule::shouldDraw()
|
|
{
|
|
if (!moduleConfig.canned_message.enabled && !CANNED_MESSAGE_MODULE_ENABLE) {
|
|
return false;
|
|
}
|
|
|
|
// If using "scan and select" input, don't draw the module frame just to say "disabled"
|
|
// The scanAndSelectInput class will draw its own temporary alert for user, when the input button is pressed
|
|
else if (scanAndSelectInput != nullptr && !hasMessages())
|
|
return false;
|
|
|
|
return (currentMessageIndex != -1) || (this->runState != CANNED_MESSAGE_RUN_STATE_INACTIVE);
|
|
}
|
|
|
|
// Has the user defined any canned messages?
|
|
// Expose publicly whether canned message module is ready for use
|
|
bool CannedMessageModule::hasMessages()
|
|
{
|
|
return (this->messagesCount > 0);
|
|
}
|
|
|
|
int CannedMessageModule::getNextIndex()
|
|
{
|
|
if (this->currentMessageIndex >= (this->messagesCount - 1)) {
|
|
return 0;
|
|
} else {
|
|
return this->currentMessageIndex + 1;
|
|
}
|
|
}
|
|
|
|
int CannedMessageModule::getPrevIndex()
|
|
{
|
|
if (this->currentMessageIndex <= 0) {
|
|
return this->messagesCount - 1;
|
|
} else {
|
|
return this->currentMessageIndex - 1;
|
|
}
|
|
}
|
|
void CannedMessageModule::showTemporaryMessage(const String &message)
|
|
{
|
|
temporaryMessage = message;
|
|
UIFrameEvent e;
|
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen
|
|
notifyObservers(&e);
|
|
runState = CANNED_MESSAGE_RUN_STATE_MESSAGE;
|
|
// run this loop again in 2 seconds, next iteration will clear the display
|
|
setIntervalFromNow(2000);
|
|
}
|
|
|
|
#if defined(USE_VIRTUAL_KEYBOARD)
|
|
|
|
String CannedMessageModule::keyForCoordinates(uint x, uint y)
|
|
{
|
|
int outerSize = *(&this->keyboard[this->charSet] + 1) - this->keyboard[this->charSet];
|
|
|
|
for (int8_t outerIndex = 0; outerIndex < outerSize; outerIndex++) {
|
|
int innerSize = *(&this->keyboard[this->charSet][outerIndex] + 1) - this->keyboard[this->charSet][outerIndex];
|
|
|
|
for (int8_t innerIndex = 0; innerIndex < innerSize; innerIndex++) {
|
|
Letter letter = this->keyboard[this->charSet][outerIndex][innerIndex];
|
|
|
|
if (x > letter.rectX && x < (letter.rectX + letter.rectWidth) && y > letter.rectY &&
|
|
y < (letter.rectY + letter.rectHeight)) {
|
|
return letter.character;
|
|
}
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
void CannedMessageModule::drawKeyboard(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
|
{
|
|
int outerSize = *(&this->keyboard[this->charSet] + 1) - this->keyboard[this->charSet];
|
|
|
|
int xOffset = 0;
|
|
|
|
int yOffset = 56;
|
|
|
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
|
|
|
display->setFont(FONT_SMALL);
|
|
|
|
display->setColor(OLEDDISPLAY_COLOR::WHITE);
|
|
|
|
display->drawStringMaxWidth(0, 0, display->getWidth(),
|
|
cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor));
|
|
|
|
display->setFont(FONT_MEDIUM);
|
|
|
|
int cellHeight = round((display->height() - 64) / outerSize);
|
|
|
|
int yCorrection = 8;
|
|
|
|
for (int8_t outerIndex = 0; outerIndex < outerSize; outerIndex++) {
|
|
yOffset += outerIndex > 0 ? cellHeight : 0;
|
|
|
|
int innerSizeBound = *(&this->keyboard[this->charSet][outerIndex] + 1) - this->keyboard[this->charSet][outerIndex];
|
|
|
|
int innerSize = 0;
|
|
|
|
for (int8_t innerIndex = 0; innerIndex < innerSizeBound; innerIndex++) {
|
|
if (this->keyboard[this->charSet][outerIndex][innerIndex].character != "") {
|
|
innerSize++;
|
|
}
|
|
}
|
|
|
|
int cellWidth = display->width() / innerSize;
|
|
|
|
for (int8_t innerIndex = 0; innerIndex < innerSize; innerIndex++) {
|
|
xOffset += innerIndex > 0 ? cellWidth : 0;
|
|
|
|
Letter letter = this->keyboard[this->charSet][outerIndex][innerIndex];
|
|
|
|
Letter updatedLetter = {letter.character, letter.width, xOffset, yOffset, cellWidth, cellHeight};
|
|
|
|
#ifdef RAK14014 // Optimize the touch range of the virtual keyboard in the bottom row
|
|
if (outerIndex == outerSize - 1) {
|
|
updatedLetter.rectHeight = 240 - yOffset;
|
|
}
|
|
#endif
|
|
this->keyboard[this->charSet][outerIndex][innerIndex] = updatedLetter;
|
|
|
|
float characterOffset = ((cellWidth / 2) - (letter.width / 2));
|
|
|
|
if (letter.character == "⇧") {
|
|
if (this->shift) {
|
|
display->fillRect(xOffset, yOffset, cellWidth, cellHeight);
|
|
|
|
display->setColor(OLEDDISPLAY_COLOR::BLACK);
|
|
|
|
drawShiftIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2);
|
|
|
|
display->setColor(OLEDDISPLAY_COLOR::WHITE);
|
|
} else {
|
|
display->drawRect(xOffset, yOffset, cellWidth, cellHeight);
|
|
|
|
drawShiftIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2);
|
|
}
|
|
} else if (letter.character == "⌫") {
|
|
if (this->highlight == letter.character[0]) {
|
|
display->fillRect(xOffset, yOffset, cellWidth, cellHeight);
|
|
|
|
display->setColor(OLEDDISPLAY_COLOR::BLACK);
|
|
|
|
drawBackspaceIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2);
|
|
|
|
display->setColor(OLEDDISPLAY_COLOR::WHITE);
|
|
|
|
setIntervalFromNow(0);
|
|
} else {
|
|
display->drawRect(xOffset, yOffset, cellWidth, cellHeight);
|
|
|
|
drawBackspaceIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.2);
|
|
}
|
|
} else if (letter.character == "↵") {
|
|
display->drawRect(xOffset, yOffset, cellWidth, cellHeight);
|
|
|
|
drawEnterIcon(display, xOffset + characterOffset, yOffset + yCorrection + 5, 1.7);
|
|
} else {
|
|
if (this->highlight == letter.character[0]) {
|
|
display->fillRect(xOffset, yOffset, cellWidth, cellHeight);
|
|
|
|
display->setColor(OLEDDISPLAY_COLOR::BLACK);
|
|
|
|
display->drawString(xOffset + characterOffset, yOffset + yCorrection,
|
|
letter.character == " " ? "space" : letter.character);
|
|
|
|
display->setColor(OLEDDISPLAY_COLOR::WHITE);
|
|
|
|
setIntervalFromNow(0);
|
|
} else {
|
|
display->drawRect(xOffset, yOffset, cellWidth, cellHeight);
|
|
|
|
display->drawString(xOffset + characterOffset, yOffset + yCorrection,
|
|
letter.character == " " ? "space" : letter.character);
|
|
}
|
|
}
|
|
}
|
|
|
|
xOffset = 0;
|
|
}
|
|
|
|
this->highlight = 0x00;
|
|
}
|
|
|
|
void CannedMessageModule::drawShiftIcon(OLEDDisplay *display, int x, int y, float scale)
|
|
{
|
|
PointStruct shiftIcon[10] = {{8, 0}, {15, 7}, {15, 8}, {12, 8}, {12, 12}, {4, 12}, {4, 8}, {1, 8}, {1, 7}, {8, 0}};
|
|
|
|
int size = 10;
|
|
|
|
for (int i = 0; i < size - 1; i++) {
|
|
int x0 = x + (shiftIcon[i].x * scale);
|
|
int y0 = y + (shiftIcon[i].y * scale);
|
|
int x1 = x + (shiftIcon[i + 1].x * scale);
|
|
int y1 = y + (shiftIcon[i + 1].y * scale);
|
|
|
|
display->drawLine(x0, y0, x1, y1);
|
|
}
|
|
}
|
|
|
|
void CannedMessageModule::drawBackspaceIcon(OLEDDisplay *display, int x, int y, float scale)
|
|
{
|
|
PointStruct backspaceIcon[6] = {{0, 7}, {5, 2}, {15, 2}, {15, 12}, {5, 12}, {0, 7}};
|
|
|
|
int size = 6;
|
|
|
|
for (int i = 0; i < size - 1; i++) {
|
|
int x0 = x + (backspaceIcon[i].x * scale);
|
|
int y0 = y + (backspaceIcon[i].y * scale);
|
|
int x1 = x + (backspaceIcon[i + 1].x * scale);
|
|
int y1 = y + (backspaceIcon[i + 1].y * scale);
|
|
|
|
display->drawLine(x0, y0, x1, y1);
|
|
}
|
|
|
|
PointStruct backspaceIconX[4] = {{7, 4}, {13, 10}, {7, 10}, {13, 4}};
|
|
|
|
size = 4;
|
|
|
|
for (int i = 0; i < size - 1; i++) {
|
|
int x0 = x + (backspaceIconX[i].x * scale);
|
|
int y0 = y + (backspaceIconX[i].y * scale);
|
|
int x1 = x + (backspaceIconX[i + 1].x * scale);
|
|
int y1 = y + (backspaceIconX[i + 1].y * scale);
|
|
|
|
display->drawLine(x0, y0, x1, y1);
|
|
}
|
|
}
|
|
|
|
void CannedMessageModule::drawEnterIcon(OLEDDisplay *display, int x, int y, float scale)
|
|
{
|
|
PointStruct enterIcon[6] = {{0, 7}, {4, 3}, {4, 11}, {0, 7}, {15, 7}, {15, 0}};
|
|
|
|
int size = 6;
|
|
|
|
for (int i = 0; i < size - 1; i++) {
|
|
int x0 = x + (enterIcon[i].x * scale);
|
|
int y0 = y + (enterIcon[i].y * scale);
|
|
int x1 = x + (enterIcon[i + 1].x * scale);
|
|
int y1 = y + (enterIcon[i + 1].y * scale);
|
|
|
|
display->drawLine(x0, y0, x1, y1);
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
// Indicate to screen class that module is handling keyboard input specially (at certain times)
|
|
// This prevents the left & right keys being used for nav. between screen frames during text entry.
|
|
bool CannedMessageModule::interceptingKeyboardInput()
|
|
{
|
|
switch (runState) {
|
|
case CANNED_MESSAGE_RUN_STATE_DISABLED:
|
|
case CANNED_MESSAGE_RUN_STATE_INACTIVE:
|
|
return false;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
#if !HAS_TFT
|
|
void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
|
{
|
|
this->displayHeight = display->getHeight(); // Store display height for later use
|
|
char buffer[50];
|
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
|
display->setFont(FONT_SMALL);
|
|
|
|
if (temporaryMessage.length() != 0) {
|
|
requestFocus(); // Tell Screen::setFrames to move to our module's frame
|
|
LOG_DEBUG("Draw temporary message: %s", temporaryMessage.c_str());
|
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
|
display->setFont(FONT_MEDIUM);
|
|
display->drawString(display->getWidth() / 2 + x, 0 + y + 12, temporaryMessage);
|
|
} else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) {
|
|
requestFocus(); // Tell Screen::setFrames to move to our module's frame
|
|
EINK_ADD_FRAMEFLAG(display, COSMETIC); // Clean after this popup. Layout makes ghosting particularly obvious
|
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
|
|
|
#ifdef USE_EINK
|
|
display->setFont(FONT_SMALL); // No chunky text
|
|
#else
|
|
display->setFont(FONT_MEDIUM); // Chunky text
|
|
#endif
|
|
|
|
String displayString;
|
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
|
if (this->ack) {
|
|
displayString = "Delivered to\n%s";
|
|
} else {
|
|
displayString = "Delivery failed\nto %s";
|
|
}
|
|
display->drawStringf(display->getWidth() / 2 + x, 0 + y + 12, buffer, displayString,
|
|
cannedMessageModule->getNodeName(this->incoming));
|
|
|
|
display->setFont(FONT_SMALL);
|
|
|
|
String snrString = "Last Rx SNR: %f";
|
|
String rssiString = "Last Rx RSSI: %d";
|
|
|
|
// Don't bother drawing snr and rssi for tiny displays
|
|
if (display->getHeight() > 100) {
|
|
|
|
// Original implementation used constants of y = 100 and y = 130. Shrink this if screen is *slightly* small
|
|
int16_t snrY = 100;
|
|
int16_t rssiY = 130;
|
|
|
|
// If dislay is *slighly* too small for the original consants, squish up a bit
|
|
if (display->getHeight() < rssiY + FONT_HEIGHT_SMALL) {
|
|
snrY = display->getHeight() - ((1.5) * FONT_HEIGHT_SMALL);
|
|
rssiY = display->getHeight() - ((2.5) * FONT_HEIGHT_SMALL);
|
|
}
|
|
|
|
if (this->ack) {
|
|
display->drawStringf(display->getWidth() / 2 + x, snrY + y, buffer, snrString, this->lastRxSnr);
|
|
display->drawStringf(display->getWidth() / 2 + x, rssiY + y, buffer, rssiString, this->lastRxRssi);
|
|
}
|
|
}
|
|
} else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) {
|
|
// E-Ink: clean the screen *after* this pop-up
|
|
EINK_ADD_FRAMEFLAG(display, COSMETIC);
|
|
|
|
requestFocus(); // Tell Screen::setFrames to move to our module's frame
|
|
|
|
#ifdef USE_EINK
|
|
display->setFont(FONT_SMALL); // No chunky text
|
|
#else
|
|
display->setFont(FONT_MEDIUM); // Chunky text
|
|
#endif
|
|
|
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
|
display->drawString(display->getWidth() / 2 + x, 0 + y + 12, "Sending...");
|
|
} else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) {
|
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
|
display->setFont(FONT_SMALL);
|
|
display->drawString(10 + x, 0 + y + FONT_HEIGHT_SMALL, "Canned Message\nModule disabled.");
|
|
} else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) {
|
|
requestFocus();
|
|
updateFilteredNodes();
|
|
display->clear();
|
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
|
display->setFont(FONT_SMALL);
|
|
|
|
int titleY = 2;
|
|
String titleText = "Select Destination";
|
|
titleText += searchQuery.length() > 0 ? " [" + searchQuery + "]" : " [ ]";
|
|
display->drawString(display->getWidth() / 2 - display->getStringWidth(titleText) / 2, titleY, titleText);
|
|
|
|
int rowYOffset = titleY + FONT_HEIGHT_SMALL; // Adjusted for search box spacing
|
|
int numActiveChannels = this->activeChannelIndices.size();
|
|
int totalEntries = numActiveChannels + this->filteredNodes.size();
|
|
int columns = 2;
|
|
this->visibleRows = (display->getHeight() - (titleY + FONT_HEIGHT_SMALL)) / FONT_HEIGHT_SMALL;
|
|
if (this->visibleRows < 1) this->visibleRows = 1;
|
|
|
|
// Ensure scrolling within bounds
|
|
if (scrollIndex > totalEntries / columns) scrollIndex = totalEntries / columns;
|
|
if (scrollIndex < 0) scrollIndex = 0;
|
|
|
|
for (int row = 0; row < visibleRows; row++) {
|
|
int itemIndex = (scrollIndex + row) * columns;
|
|
for (int col = 0; col < columns; col++) {
|
|
if (itemIndex >= totalEntries) break;
|
|
|
|
int xOffset = col * (display->getWidth() / columns);
|
|
int yOffset = row * FONT_HEIGHT_SMALL + rowYOffset;
|
|
String entryText;
|
|
|
|
// Draw Channels First
|
|
if (itemIndex < numActiveChannels) {
|
|
uint8_t channelIndex = this->activeChannelIndices[itemIndex];
|
|
entryText = String("@") + String(channels.getName(channelIndex));
|
|
}
|
|
// Then Draw Nodes
|
|
else {
|
|
int nodeIndex = itemIndex - numActiveChannels;
|
|
if (nodeIndex >= 0 && nodeIndex < static_cast<int>(this->filteredNodes.size())) {
|
|
meshtastic_NodeInfoLite *node = this->filteredNodes[nodeIndex].node;
|
|
entryText = node ? (node->is_favorite ? "* " + String(node->user.long_name) : String(node->user.long_name)) : "?";
|
|
}
|
|
}
|
|
|
|
// Prevent Empty Names
|
|
if (entryText.length() == 0 || entryText == "Unknown") entryText = "?";
|
|
|
|
// Trim if Too Long
|
|
while (display->getStringWidth(entryText + "-") > (display->getWidth() / columns - 4)) {
|
|
entryText = entryText.substring(0, entryText.length() - 1);
|
|
}
|
|
|
|
// Highlight Selection
|
|
if (itemIndex == destIndex) {
|
|
display->fillRect(xOffset, yOffset, display->getStringWidth(entryText) + 4, FONT_HEIGHT_SMALL + 2);
|
|
display->setColor(BLACK);
|
|
}
|
|
display->drawString(xOffset + 2, yOffset, entryText);
|
|
display->setColor(WHITE);
|
|
itemIndex++;
|
|
}
|
|
}
|
|
if (totalEntries > visibleRows * columns) {
|
|
display->drawRect(display->getWidth() - 6, rowYOffset, 4, visibleRows * FONT_HEIGHT_SMALL);
|
|
int totalPages = (totalEntries + columns - 1) / columns;
|
|
int scrollHeight = (visibleRows * FONT_HEIGHT_SMALL * visibleRows) / (totalPages);
|
|
int scrollPos = rowYOffset + ((visibleRows * FONT_HEIGHT_SMALL) * scrollIndex) / totalPages;
|
|
display->fillRect(display->getWidth() - 6, scrollPos, 4, scrollHeight);
|
|
}
|
|
screen->forceDisplay();
|
|
} else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
|
requestFocus(); // Tell Screen::setFrames to move to our module's frame
|
|
#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
|
|
EInkDynamicDisplay* einkDisplay = static_cast<EInkDynamicDisplay*>(display);
|
|
einkDisplay->enableUnlimitedFastMode(); // Enable unlimited fast refresh while typing
|
|
#endif
|
|
|
|
#if defined(USE_VIRTUAL_KEYBOARD)
|
|
drawKeyboard(display, state, 0, 0);
|
|
#else
|
|
|
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
|
display->setFont(FONT_SMALL);
|
|
if (this->destSelect != CANNED_MESSAGE_DESTINATION_TYPE_NONE) {
|
|
display->fillRect(0 + x, 0 + y, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
|
|
display->setColor(BLACK);
|
|
}
|
|
switch (this->destSelect) {
|
|
case CANNED_MESSAGE_DESTINATION_TYPE_NODE:
|
|
display->drawStringf(1 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest),
|
|
channels.getName(this->channel));
|
|
LOG_INFO("Displaying recipient: Node=%s (ID=%d)", cannedMessageModule->getNodeName(this->dest), this->dest);
|
|
display->drawStringf(0 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest),
|
|
channels.getName(this->channel));
|
|
break;
|
|
case CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL:
|
|
display->drawStringf(1 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest),
|
|
channels.getName(this->channel));
|
|
display->drawStringf(0 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest),
|
|
channels.getName(this->channel));
|
|
break;
|
|
default:
|
|
if (display->getWidth() > 128) {
|
|
display->drawStringf(0 + x, 0 + y, buffer, "To: %s@%s", cannedMessageModule->getNodeName(this->dest),
|
|
channels.getName(this->channel));
|
|
} else {
|
|
display->drawStringf(0 + x, 0 + y, buffer, "To: %.5s@%.5s", cannedMessageModule->getNodeName(this->dest),
|
|
channels.getName(this->channel));
|
|
}
|
|
break;
|
|
}
|
|
// used chars right aligned, only when not editing the destination
|
|
if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NONE) {
|
|
uint16_t charsLeft =
|
|
meshtastic_Constants_DATA_PAYLOAD_LEN - this->freetext.length() - (moduleConfig.canned_message.send_bell ? 1 : 0);
|
|
snprintf(buffer, sizeof(buffer), "%d left", charsLeft);
|
|
display->drawString(x + display->getWidth() - display->getStringWidth(buffer), y + 0, buffer);
|
|
}
|
|
display->setColor(WHITE);
|
|
display->drawStringMaxWidth(
|
|
0 + x, 0 + y + FONT_HEIGHT_SMALL, x + display->getWidth(),
|
|
cannedMessageModule->drawWithCursor(cannedMessageModule->freetext, cannedMessageModule->cursor));
|
|
#endif
|
|
} else {
|
|
if (this->messagesCount > 0) {
|
|
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
|
display->setFont(FONT_SMALL);
|
|
display->drawStringf(0 + x, 0 + y, buffer, "To: %s", cannedMessageModule->getNodeName(this->dest));
|
|
int lines = (display->getHeight() / FONT_HEIGHT_SMALL) - 1;
|
|
if (lines == 3) {
|
|
// static (old) behavior for small displays
|
|
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL, cannedMessageModule->getPrevMessage());
|
|
display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), y + FONT_HEIGHT_SMALL);
|
|
display->setColor(BLACK);
|
|
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, cannedMessageModule->getCurrentMessage());
|
|
display->setColor(WHITE);
|
|
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 3, cannedMessageModule->getNextMessage());
|
|
} else {
|
|
// use entire display height for larger displays
|
|
int topMsg = (messagesCount > lines && currentMessageIndex >= lines - 1) ? currentMessageIndex - lines + 2 : 0;
|
|
for (int i = 0; i < std::min(messagesCount, lines); i++) {
|
|
if (i == currentMessageIndex - topMsg) {
|
|
#ifdef USE_EINK
|
|
// Avoid drawing solid black with fillRect: harder to clear for E-Ink
|
|
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), ">");
|
|
display->drawString(12 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1),
|
|
cannedMessageModule->getCurrentMessage());
|
|
#else
|
|
display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), x + display->getWidth(),
|
|
y + FONT_HEIGHT_SMALL);
|
|
display->setColor(BLACK);
|
|
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), cannedMessageModule->getCurrentMessage());
|
|
display->setColor(WHITE);
|
|
#endif
|
|
} else {
|
|
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1),
|
|
cannedMessageModule->getMessageByIndex(topMsg + i));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif //! HAS_TFT
|
|
|
|
ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
|
|
{
|
|
if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP && waitingForAck) {
|
|
// look for a request_id
|
|
if (mp.decoded.request_id != 0) {
|
|
UIFrameEvent e;
|
|
e.action = UIFrameEvent::Action::REGENERATE_FRAMESET; // We want to change the list of frames shown on-screen
|
|
requestFocus(); // Tell Screen::setFrames that our module's frame should be shown, even if not "first" in the frameset
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED;
|
|
this->incoming = service->getNodenumFromRequestId(mp.decoded.request_id);
|
|
meshtastic_Routing decoded = meshtastic_Routing_init_default;
|
|
pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, meshtastic_Routing_fields, &decoded);
|
|
this->ack = decoded.error_reason == meshtastic_Routing_Error_NONE;
|
|
waitingForAck = false; // No longer want routing packets
|
|
this->notifyObservers(&e);
|
|
// run the next time 2 seconds later
|
|
setIntervalFromNow(2000);
|
|
}
|
|
}
|
|
|
|
return ProcessMessage::CONTINUE;
|
|
}
|
|
|
|
void CannedMessageModule::loadProtoForModule()
|
|
{
|
|
if (nodeDB->loadProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size,
|
|
sizeof(meshtastic_CannedMessageModuleConfig), &meshtastic_CannedMessageModuleConfig_msg,
|
|
&cannedMessageModuleConfig) != LoadFileResult::LOAD_SUCCESS) {
|
|
installDefaultCannedMessageModuleConfig();
|
|
}
|
|
}
|
|
/**
|
|
* @brief Save the module config to file.
|
|
*
|
|
* @return true On success.
|
|
* @return false On error.
|
|
*/
|
|
bool CannedMessageModule::saveProtoForModule()
|
|
{
|
|
bool okay = true;
|
|
|
|
#ifdef FSCom
|
|
spiLock->lock();
|
|
FSCom.mkdir("/prefs");
|
|
spiLock->unlock();
|
|
#endif
|
|
|
|
okay &= nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size,
|
|
&meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig);
|
|
|
|
return okay;
|
|
}
|
|
|
|
/**
|
|
* @brief Fill configuration with default values.
|
|
*/
|
|
void CannedMessageModule::installDefaultCannedMessageModuleConfig()
|
|
{
|
|
memset(cannedMessageModuleConfig.messages, 0, sizeof(cannedMessageModuleConfig.messages));
|
|
}
|
|
|
|
/**
|
|
* @brief An admin message arrived to AdminModule. We are asked whether we want to handle that.
|
|
*
|
|
* @param mp The mesh packet arrived.
|
|
* @param request The AdminMessage request extracted from the packet.
|
|
* @param response The prepared response
|
|
* @return AdminMessageHandleResult HANDLED if message was handled
|
|
* HANDLED_WITH_RESULT if a result is also prepared.
|
|
*/
|
|
AdminMessageHandleResult CannedMessageModule::handleAdminMessageForModule(const meshtastic_MeshPacket &mp,
|
|
meshtastic_AdminMessage *request,
|
|
meshtastic_AdminMessage *response)
|
|
{
|
|
AdminMessageHandleResult result;
|
|
|
|
switch (request->which_payload_variant) {
|
|
case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag:
|
|
LOG_DEBUG("Client getting radio canned messages");
|
|
this->handleGetCannedMessageModuleMessages(mp, response);
|
|
result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE;
|
|
break;
|
|
|
|
case meshtastic_AdminMessage_set_canned_message_module_messages_tag:
|
|
LOG_DEBUG("Client getting radio canned messages");
|
|
this->handleSetCannedMessageModuleMessages(request->set_canned_message_module_messages);
|
|
result = AdminMessageHandleResult::HANDLED;
|
|
break;
|
|
|
|
default:
|
|
result = AdminMessageHandleResult::NOT_HANDLED;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void CannedMessageModule::handleGetCannedMessageModuleMessages(const meshtastic_MeshPacket &req,
|
|
meshtastic_AdminMessage *response)
|
|
{
|
|
LOG_DEBUG("*** handleGetCannedMessageModuleMessages");
|
|
if (req.decoded.want_response) {
|
|
response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag;
|
|
strncpy(response->get_canned_message_module_messages_response, cannedMessageModuleConfig.messages,
|
|
sizeof(response->get_canned_message_module_messages_response));
|
|
} // Don't send anything if not instructed to. Better than asserting.
|
|
}
|
|
|
|
void CannedMessageModule::handleSetCannedMessageModuleMessages(const char *from_msg)
|
|
{
|
|
int changed = 0;
|
|
|
|
if (*from_msg) {
|
|
changed |= strcmp(cannedMessageModuleConfig.messages, from_msg);
|
|
strncpy(cannedMessageModuleConfig.messages, from_msg, sizeof(cannedMessageModuleConfig.messages));
|
|
LOG_DEBUG("*** from_msg.text:%s", from_msg);
|
|
}
|
|
|
|
if (changed) {
|
|
this->saveProtoForModule();
|
|
}
|
|
}
|
|
|
|
String CannedMessageModule::drawWithCursor(String text, int cursor)
|
|
{
|
|
String result = text.substring(0, cursor) + "_" + text.substring(cursor);
|
|
return result;
|
|
}
|
|
|
|
#endif
|