mirror of
https://github.com/meshtastic/firmware.git
synced 2025-12-20 17:52:35 +00:00
This enables the node and channel selection to be accessed by pressing the tab shortcut and then swiping between nodes or pressing tab again to change channels. (To access the tab function look at my other pull request https://github.com/meshtastic/firmware/pull/3668) Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
769 lines
33 KiB
C++
769 lines
33 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 "detect/ScanI2C.h"
|
|
#include "mesh/generated/meshtastic/cannedmessages.pb.h"
|
|
|
|
#include "main.h" // for cardkb_found
|
|
#include "modules/ExternalNotificationModule.h" // for buzzer control
|
|
#if !MESHTASTIC_EXCLUDE_GPS
|
|
#include "GPS.h"
|
|
#endif
|
|
|
|
#ifndef INPUTBROKER_MATRIX_TYPE
|
|
#define INPUTBROKER_MATRIX_TYPE 0
|
|
#endif
|
|
|
|
#include "graphics/ScreenFonts.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("CannedMessageModule")
|
|
{
|
|
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\n");
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_DISABLED;
|
|
disable();
|
|
} else {
|
|
LOG_INFO("CannedMessageModule is enabled\n");
|
|
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;
|
|
|
|
// collect all the message parts
|
|
strncpy(this->messageStore, cannedMessageModuleConfig.messages, 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';
|
|
LOG_DEBUG("CannedMessage %d is: '%s'\n", messageIndex - 1, this->messages[messageIndex - 1]);
|
|
|
|
// 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'\n", messageIndex - 1, this->messages[messageIndex - 1]);
|
|
this->messagesCount = messageIndex;
|
|
} else {
|
|
this->messagesCount = messageIndex - 1;
|
|
}
|
|
|
|
return this->messagesCount;
|
|
}
|
|
|
|
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
|
|
}
|
|
bool validEvent = false;
|
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP)) {
|
|
if (this->messagesCount > 0) {
|
|
// LOG_DEBUG("Canned message event UP\n");
|
|
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) {
|
|
// LOG_DEBUG("Canned message event DOWN\n");
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_ACTION_DOWN;
|
|
validEvent = true;
|
|
}
|
|
}
|
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT)) {
|
|
LOG_DEBUG("Canned message event Select\n");
|
|
// 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)) {
|
|
LOG_DEBUG("Canned message event Cancel\n");
|
|
UIFrameEvent e = {false, true};
|
|
e.frameChanged = true;
|
|
this->currentMessageIndex = -1;
|
|
this->freetext = ""; // clear freetext
|
|
this->cursor = 0;
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
this->notifyObservers(&e);
|
|
}
|
|
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))) {
|
|
// LOG_DEBUG("Canned message event (%x)\n", event->kbchar);
|
|
// 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 = 0xb4;
|
|
// this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE;
|
|
} else if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) {
|
|
this->payload = 0xb7;
|
|
// this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE;
|
|
}
|
|
} else {
|
|
// pass the pressed key
|
|
this->payload = event->kbchar;
|
|
}
|
|
this->lastTouchMillis = millis();
|
|
validEvent = true;
|
|
}
|
|
if (event->inputEvent == static_cast<char>(ANYKEY)) {
|
|
LOG_DEBUG("Canned message event any key pressed\n");
|
|
// 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;
|
|
}
|
|
// pass the pressed key
|
|
// LOG_DEBUG("Canned message ANYKEY (%x)\n", event->kbchar);
|
|
this->payload = event->kbchar;
|
|
this->lastTouchMillis = millis();
|
|
validEvent = true;
|
|
}
|
|
if (event->inputEvent == static_cast<char>(MATRIXKEY)) {
|
|
LOG_DEBUG("Canned message event Matrix key pressed\n");
|
|
// 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) {
|
|
// 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++;
|
|
}
|
|
|
|
LOG_INFO("Sending message id=%d, dest=%x, msg=%.*s\n", 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()
|
|
{
|
|
if (((!moduleConfig.canned_message.enabled) && !CANNED_MESSAGE_MODULE_ENABLE) ||
|
|
(this->runState == CANNED_MESSAGE_RUN_STATE_DISABLED) || (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) {
|
|
return INT32_MAX;
|
|
}
|
|
// LOG_DEBUG("Check status\n");
|
|
UIFrameEvent e = {false, true};
|
|
if ((this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) ||
|
|
(this->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED)) {
|
|
// TODO: might have some feedback of sendig state
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
e.frameChanged = true;
|
|
this->currentMessageIndex = -1;
|
|
this->freetext = ""; // clear freetext
|
|
this->cursor = 0;
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
this->notifyObservers(&e);
|
|
} else if (((this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) || (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT)) &&
|
|
((millis() - this->lastTouchMillis) > INACTIVATE_AFTER_MS)) {
|
|
// Reset module
|
|
LOG_DEBUG("Reset due to lack of activity.\n");
|
|
e.frameChanged = true;
|
|
this->currentMessageIndex = -1;
|
|
this->freetext = ""; // clear freetext
|
|
this->cursor = 0;
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
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, indexChannels[this->channel], this->freetext.c_str(), true);
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
|
|
} else {
|
|
LOG_DEBUG("Reset message is empty.\n");
|
|
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 {
|
|
sendText(NODENUM_BROADCAST, channels.getPrimaryIndex(), this->messages[this->currentMessageIndex], true);
|
|
}
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
|
|
} else {
|
|
LOG_DEBUG("Reset message is empty.\n");
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
}
|
|
}
|
|
e.frameChanged = true;
|
|
this->currentMessageIndex = -1;
|
|
this->freetext = ""; // clear freetext
|
|
this->cursor = 0;
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
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\n", this->currentMessageIndex, this->getCurrentMessage());
|
|
e.frameChanged = true;
|
|
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;
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
|
|
LOG_DEBUG("MOVE UP (%d):%s\n", 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;
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE;
|
|
LOG_DEBUG("MOVE DOWN (%d):%s\n", this->currentMessageIndex, this->getCurrentMessage());
|
|
}
|
|
} else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) {
|
|
switch (this->payload) {
|
|
case 0xb4: // 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 0xb7: // 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.frameChanged = true;
|
|
switch (this->payload) {
|
|
case 0x08: // backspace
|
|
if (this->freetext.length() > 0) {
|
|
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
|
|
case 0x91: // alt+t for T-Deck that doesn't have a tab key
|
|
if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL) {
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
} else if (this->destSelect == CANNED_MESSAGE_DESTINATION_TYPE_NODE) {
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_CHANNEL;
|
|
} else {
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NODE;
|
|
}
|
|
break;
|
|
case 0xb4: // left
|
|
case 0xb7: // right
|
|
// already handled above
|
|
break;
|
|
// handle fn+s for shutdown
|
|
case 0x9b:
|
|
if (screen)
|
|
screen->startShutdownScreen();
|
|
shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000;
|
|
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
break;
|
|
// and fn+r for reboot
|
|
case 0x90:
|
|
if (screen)
|
|
screen->startRebootScreen();
|
|
rebootAtMsec = millis() + DEFAULT_REBOOT_SECONDS * 1000;
|
|
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
break;
|
|
case 0x9e: // toggle GPS like triple press does
|
|
if (gps != nullptr) {
|
|
gps->toggleGpsMode();
|
|
}
|
|
if (screen)
|
|
screen->forceDisplay();
|
|
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
break;
|
|
|
|
// mute (switch off/toggle) external notifications on fn+m
|
|
case 0xac:
|
|
if (moduleConfig.external_notification.enabled == true) {
|
|
if (externalNotificationModule->getMute()) {
|
|
externalNotificationModule->setMute(false);
|
|
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
} else {
|
|
externalNotificationModule->stopNow(); // this will turn off all GPIO and sounds and idle the loop
|
|
externalNotificationModule->setMute(true);
|
|
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
}
|
|
}
|
|
break;
|
|
case 0xaf: // fn+space send network ping like double press does
|
|
service.refreshLocalMeshNode();
|
|
service.sendNetworkPing(NODENUM_BROADCAST, true);
|
|
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
break;
|
|
default:
|
|
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;
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
return (currentMessageIndex != -1) || (this->runState != CANNED_MESSAGE_RUN_STATE_INACTIVE);
|
|
}
|
|
|
|
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::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
|
{
|
|
char buffer[50];
|
|
|
|
if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_ACK_NACK_RECEIVED) {
|
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
|
display->setFont(FONT_MEDIUM);
|
|
String displayString;
|
|
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));
|
|
} else if (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) {
|
|
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
|
display->setFont(FONT_MEDIUM);
|
|
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 (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
|
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(indexChannels[this->channel]));
|
|
display->drawStringf(0 + x, 0 + y, buffer, "To: >%s<@%s", cannedMessageModule->getNodeName(this->dest),
|
|
channels.getName(indexChannels[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(indexChannels[this->channel]));
|
|
display->drawStringf(0 + x, 0 + y, buffer, "To: %s@>%s<", cannedMessageModule->getNodeName(this->dest),
|
|
channels.getName(indexChannels[this->channel]));
|
|
break;
|
|
default:
|
|
if (display->getWidth() > 128) {
|
|
display->drawStringf(0 + x, 0 + y, buffer, "To: %s@%s", cannedMessageModule->getNodeName(this->dest),
|
|
channels.getName(indexChannels[this->channel]));
|
|
} else {
|
|
display->drawStringf(0 + x, 0 + y, buffer, "To: %.5s@%.5s", cannedMessageModule->getNodeName(this->dest),
|
|
channels.getName(indexChannels[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));
|
|
} 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) {
|
|
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);
|
|
} else {
|
|
display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1),
|
|
cannedMessageModule->getMessageByIndex(topMsg + i));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ProcessMessage CannedMessageModule::handleReceived(const meshtastic_MeshPacket &mp)
|
|
{
|
|
if (mp.decoded.portnum == meshtastic_PortNum_ROUTING_APP) {
|
|
// look for a request_id
|
|
if (mp.decoded.request_id != 0) {
|
|
UIFrameEvent e = {false, true};
|
|
e.frameChanged = true;
|
|
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;
|
|
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::SUCCESS) {
|
|
installDefaultCannedMessageModuleConfig();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Save the module config to file.
|
|
*
|
|
* @return true On success.
|
|
* @return false On error.
|
|
*/
|
|
bool CannedMessageModule::saveProtoForModule()
|
|
{
|
|
bool okay = true;
|
|
|
|
#ifdef FS
|
|
FS.mkdir("/prefs");
|
|
#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 is getting radio canned messages\n");
|
|
this->handleGetCannedMessageModuleMessages(mp, response);
|
|
result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE;
|
|
break;
|
|
|
|
case meshtastic_AdminMessage_set_canned_message_module_messages_tag:
|
|
LOG_DEBUG("Client is setting radio canned messages\n");
|
|
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\n");
|
|
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\n", 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 |