* Add "Scan and Select" input method for canned messages * Adapt canned message drawing if USE_EINK * Indicate current selection with indent rather than inverse text * Avoid large text on "sending" and delivery report pop-ups * Fit SNR and RSSI details on screen * Change hash function which detects changes in E-Ink images The old function struggled to distingush between images on the canned-message frame, failing to update when scrolling between messages. No real justification for the new algorithm, other than "it works" and doesn't seem "too expensive". For context, this function runs once a second. * Use canned messages (scan and select) by default with HT-VME213 and HT-VME290 * Guard for HAS_SCREEN
1223 lines
49 KiB
C++
1223 lines
49 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 "input/ScanAndSelect.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
|
|
#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"
|
|
|
|
// 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");
|
|
|
|
// T-Watch interface currently has no way to select destination type, so default to 'node'
|
|
#if defined(T_WATCH_S3) || defined(RAK14014)
|
|
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 messages = cannedMessageModuleConfig.messages;
|
|
|
|
#if defined(T_WATCH_S3) || defined(RAK14014)
|
|
String separator = messages.length() ? "|" : "";
|
|
|
|
messages = "[---- Free Text ----]" + separator + messages;
|
|
#endif
|
|
|
|
// collect all the message parts
|
|
strncpy(this->messageStore, 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'\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) {
|
|
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(T_WATCH_S3) || defined(RAK14014)
|
|
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)) {
|
|
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)
|
|
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);
|
|
}
|
|
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(T_WATCH_S3) || defined(RAK14014)
|
|
if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) {
|
|
this->payload = 0xb4;
|
|
} else if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) {
|
|
this->payload = 0xb7;
|
|
}
|
|
#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 = 0xb4;
|
|
} else if (event->inputEvent == static_cast<char>(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) {
|
|
this->payload = 0xb7;
|
|
}
|
|
} 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 0x11: // make screen brighter
|
|
if (screen)
|
|
screen->increaseBrightness();
|
|
LOG_DEBUG("increasing Screen Brightness\n");
|
|
break;
|
|
case 0x12: // make screen dimmer
|
|
if (screen)
|
|
screen->decreaseBrightness();
|
|
LOG_DEBUG("Decreasing Screen Brightness\n");
|
|
break;
|
|
case 0xf1: // draw modifier (function) symbal
|
|
if (screen)
|
|
screen->setFunctionSymbal("Fn");
|
|
break;
|
|
case 0xf2: // remove modifier (function) symbal
|
|
if (screen)
|
|
screen->removeFunctionSymbal("Fn");
|
|
break;
|
|
// mute (switch off/toggle) external notifications on fn+m
|
|
case 0xac:
|
|
if (moduleConfig.external_notification.enabled == true) {
|
|
if (externalNotificationModule->getMute()) {
|
|
externalNotificationModule->setMute(false);
|
|
showTemporaryMessage("Notifications \nEnabled");
|
|
if (screen)
|
|
screen->removeFunctionSymbal("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->setFunctionSymbal("M"); // add the mute symbol to the bottom right corner
|
|
}
|
|
}
|
|
break;
|
|
case 0x9e: // 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 0xaf: // 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;
|
|
default:
|
|
// pass the pressed key
|
|
// LOG_DEBUG("Canned message ANYKEY (%x)\n", event->kbchar);
|
|
this->payload = event->kbchar;
|
|
this->lastTouchMillis = millis();
|
|
validEvent = true;
|
|
break;
|
|
}
|
|
if (screen && (event->kbchar != 0xf1)) {
|
|
screen->removeFunctionSymbal("Fn"); // remove modifier (function) symbal
|
|
}
|
|
}
|
|
|
|
#if defined(T_WATCH_S3) || defined(RAK14014)
|
|
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 == "⌫") {
|
|
this->highlight = keyTapped[0];
|
|
|
|
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 == " ") {
|
|
this->highlight = keyTapped[0];
|
|
|
|
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 != "") {
|
|
this->highlight = keyTapped[0];
|
|
|
|
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("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)) {
|
|
temporaryMessage = "";
|
|
return INT32_MAX;
|
|
}
|
|
// LOG_DEBUG("Check status\n");
|
|
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)
|
|
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)) &&
|
|
((millis() - 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)
|
|
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, indexChannels[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(T_WATCH_S3) || defined(RAK14014)
|
|
sendText(this->dest, indexChannels[this->channel], this->messages[this->currentMessageIndex], true);
|
|
#else
|
|
sendText(NODENUM_BROADCAST, channels.getPrimaryIndex(), this->messages[this->currentMessageIndex], true);
|
|
#endif
|
|
}
|
|
this->runState = CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE;
|
|
} else {
|
|
// LOG_DEBUG("Reset message is empty.\n");
|
|
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)
|
|
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\n", 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)
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
#endif
|
|
|
|
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;
|
|
|
|
#if !defined(T_WATCH_S3) && !defined(RAK14014)
|
|
this->destSelect = CANNED_MESSAGE_DESTINATION_TYPE_NONE;
|
|
#endif
|
|
|
|
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.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
|
|
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->startAlert("Shutting down...");
|
|
shutdownAtMsec = millis() + DEFAULT_SHUTDOWN_SECONDS * 1000;
|
|
runState = CANNED_MESSAGE_RUN_STATE_INACTIVE;
|
|
break;
|
|
// and fn+r for reboot
|
|
case 0x90:
|
|
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->removeFunctionSymbal("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(T_WATCH_S3) || defined(RAK14014)
|
|
|
|
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};
|
|
|
|
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
|
|
|
|
void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
|
{
|
|
char buffer[50];
|
|
|
|
if (temporaryMessage.length() != 0) {
|
|
requestFocus(); // Tell Screen::setFrames to move to our module's frame
|
|
LOG_DEBUG("Drawing 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
|
|
|
|
#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) {
|
|
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 (cannedMessageModule->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) {
|
|
requestFocus(); // Tell Screen::setFrames to move to our module's frame
|
|
#if defined(T_WATCH_S3) || defined(RAK14014)
|
|
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(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));
|
|
#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));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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 |