diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index cdf5ab1a5..c7bc0fe36 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -820,6 +820,9 @@ void Screen::setup() nodeStatusObserver.observe(&nodeStatus->onNewStatus); if (textMessagePlugin) textMessageObserver.observe(textMessagePlugin); + + // Plugins can notify screen about refresh + MeshPlugin::observeUIEvents(&uiFrameEventObserver); } void Screen::forceDisplay() @@ -1457,4 +1460,23 @@ int Screen::handleTextMessage(const MeshPacket *packet) return 0; } +int Screen::handleUIFrameEvent(const UIFrameEvent *event) +{ + if (showingNormalScreen) { + if (event->frameChanged) + { + setFrames(); // Regen the list of screens (will show new text message) + } + else if (event->needRedraw) + { + setFastFramerate(); + // TODO: We might also want switch to corresponding frame, + // but we don't know the exact frame number. + //ui.switchToFrame(0); + } + } + + return 0; +} + } // namespace graphics diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 9334fe43c..10b2b6ff7 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -40,6 +40,7 @@ class Screen #include "concurrency/OSThread.h" #include "power.h" #include +#include "mesh/MeshPlugin.h" // 0 to 255, though particular variants might define different defaults #ifndef BRIGHTNESS_DEFAULT @@ -90,6 +91,8 @@ class Screen : public concurrency::OSThread CallbackObserver(this, &Screen::handleStatusUpdate); CallbackObserver textMessageObserver = CallbackObserver(this, &Screen::handleTextMessage); + CallbackObserver uiFrameEventObserver = + CallbackObserver(this, &Screen::handleUIFrameEvent); public: Screen(uint8_t address, int sda = -1, int scl = -1); @@ -218,6 +221,7 @@ class Screen : public concurrency::OSThread int handleStatusUpdate(const meshtastic::Status *arg); int handleTextMessage(const MeshPacket *arg); + int handleUIFrameEvent(const UIFrameEvent *arg); /// Used to force (super slow) eink displays to draw critical frames void forceDisplay(); diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp new file mode 100644 index 000000000..85e5a087f --- /dev/null +++ b/src/input/InputBroker.cpp @@ -0,0 +1,18 @@ +#include "InputBroker.h" + +InputBroker *inputBroker; + +InputBroker::InputBroker() +{ +}; + +void InputBroker::registerSource(Observable *source) +{ + this->inputEventObserver.observe(source); +} + +int InputBroker::handleInputEvent(const InputEvent *event) +{ + this->notifyObservers(event); + return 0; +} \ No newline at end of file diff --git a/src/input/InputBroker.h b/src/input/InputBroker.h new file mode 100644 index 000000000..e75b0c407 --- /dev/null +++ b/src/input/InputBroker.h @@ -0,0 +1,22 @@ +#pragma once +#include "Observer.h" + +typedef struct _InputEvent { + const char* source; + char inputEvent; +} InputEvent; +class InputBroker : + public Observable +{ + CallbackObserver inputEventObserver = + CallbackObserver(this, &InputBroker::handleInputEvent); + + public: + InputBroker(); + void registerSource(Observable *source); + + protected: + int handleInputEvent(const InputEvent *event); +}; + +extern InputBroker *inputBroker; \ No newline at end of file diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp new file mode 100644 index 000000000..ba825080f --- /dev/null +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -0,0 +1,148 @@ +#include "configuration.h" +#include "RotaryEncoderInterruptBase.h" + +RotaryEncoderInterruptBase::RotaryEncoderInterruptBase( + const char *name) : + concurrency::OSThread(name) +{ + this->_originName = name; +} + +void RotaryEncoderInterruptBase::init( + uint8_t pinA, uint8_t pinB, uint8_t pinPress, + char eventCw, char eventCcw, char eventPressed, +// std::function onIntA, std::function onIntB, std::function onIntPress) : + void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()) +{ + this->_pinA = pinA; + this->_pinB = pinB; + this->_eventCw = eventCw; + this->_eventCcw = eventCcw; + this->_eventPressed = eventPressed; + + pinMode(pinPress, INPUT_PULLUP); + pinMode(this->_pinA, INPUT_PULLUP); + pinMode(this->_pinB, INPUT_PULLUP); + +// attachInterrupt(pinPress, onIntPress, RISING); + attachInterrupt(pinPress, onIntPress, RISING); + attachInterrupt(this->_pinA, onIntA, CHANGE); + attachInterrupt(this->_pinB, onIntB, CHANGE); + + this->rotaryLevelA = digitalRead(this->_pinA); + this->rotaryLevelB = digitalRead(this->_pinB); + DEBUG_MSG("Rotary initialized (%d, %d, %d)\n", + this->_pinA, this->_pinB, pinPress); +} + + +int32_t RotaryEncoderInterruptBase::runOnce() +{ + InputEvent e; + e.inputEvent = InputEventChar_NULL; + e.source = this->_originName; + + if (this->action == ROTARY_ACTION_PRESSED) + { + DEBUG_MSG("Rotary event Press\n"); + e.inputEvent = this->_eventPressed; + } + else if (this->action == ROTARY_ACTION_CW) + { + DEBUG_MSG("Rotary event CW\n"); + e.inputEvent = this->_eventCw; + } + else if (this->action == ROTARY_ACTION_CCW) + { + DEBUG_MSG("Rotary event CW\n"); + e.inputEvent = this->_eventCcw; + } + + if (e.inputEvent != InputEventChar_NULL) + { + this->notifyObservers(&e); + } + + this->action = ROTARY_ACTION_NONE; + + return 30000; // TODO: technically this can be MAX_INT +} + + +void RotaryEncoderInterruptBase::intPressHandler() +{ + this->action = ROTARY_ACTION_PRESSED; + setIntervalFromNow(20); // TODO: this modifies a non-volatile variable! +} + +void RotaryEncoderInterruptBase::intAHandler() +{ + // CW rotation (at least on most common rotary encoders) + int currentLevelA = digitalRead(this->_pinA); + if (this->rotaryLevelA == currentLevelA) + { + return; + } + this->rotaryLevelA = currentLevelA; + intHandler( + currentLevelA == HIGH, + this->rotaryLevelB, + ROTARY_ACTION_CCW, + this->rotaryStateCCW); +} + +void RotaryEncoderInterruptBase::intBHandler() +{ + // CW rotation (at least on most common rotary encoders) + int currentLevelB = digitalRead(this->_pinB); + if (this->rotaryLevelB == currentLevelB) + { + return; + } + this->rotaryLevelB = currentLevelB; + this->rotaryStateCW = intHandler( + currentLevelB == HIGH, + this->rotaryLevelA, + ROTARY_ACTION_CW, + this->rotaryStateCW); +} + +/** + * @brief Rotary action implementation. + * We assume, the following pin setup: + * A --|| + * GND --||]======== + * B --|| + * + * @return The new state for rotary pin. + */ +RotaryEncoderInterruptBaseStateType RotaryEncoderInterruptBase::intHandler( + bool actualPinRaising, + int otherPinLevel, + RotaryEncoderInterruptBaseActionType action, + RotaryEncoderInterruptBaseStateType state) +{ + RotaryEncoderInterruptBaseStateType newState = + state; + if (actualPinRaising && (otherPinLevel == LOW)) + { + if (state == ROTARY_EVENT_CLEARED) + { + newState = ROTARY_EVENT_OCCURRED; + if ((this->action != ROTARY_ACTION_PRESSED) + && (this->action != action)) + { + this->action = action; + DEBUG_MSG("Rotary action\n"); + } + } + } + else if (!actualPinRaising && (otherPinLevel == HIGH)) + { + // Logic to prevent bouncing. + newState = ROTARY_EVENT_CLEARED; + } + setIntervalFromNow(50); // TODO: this modifies a non-volatile variable! + + return newState; +} diff --git a/src/input/RotaryEncoderInterruptBase.h b/src/input/RotaryEncoderInterruptBase.h new file mode 100644 index 000000000..ae4af5262 --- /dev/null +++ b/src/input/RotaryEncoderInterruptBase.h @@ -0,0 +1,57 @@ +#pragma once + +#include "SinglePortPlugin.h" // TODO: what header file to include? +#include "InputBroker.h" + +enum RotaryEncoderInterruptBaseStateType +{ + ROTARY_EVENT_OCCURRED, + ROTARY_EVENT_CLEARED +}; + +enum RotaryEncoderInterruptBaseActionType +{ + ROTARY_ACTION_NONE, + ROTARY_ACTION_PRESSED, + ROTARY_ACTION_CW, + ROTARY_ACTION_CCW +}; + +class RotaryEncoderInterruptBase : + public Observable, + private concurrency::OSThread +{ + public: + RotaryEncoderInterruptBase( + const char *name); + void init( + uint8_t pinA, uint8_t pinB, uint8_t pinPress, + char eventCw, char eventCcw, char eventPressed, +// std::function onIntA, std::function onIntB, std::function onIntPress); + void (*onIntA)(), void (*onIntB)(), void (*onIntPress)()); + void intPressHandler(); + void intAHandler(); + void intBHandler(); + + protected: + virtual int32_t runOnce(); + RotaryEncoderInterruptBaseStateType intHandler( + bool actualPinRaising, + int otherPinLevel, + RotaryEncoderInterruptBaseActionType action, + RotaryEncoderInterruptBaseStateType state); + + volatile RotaryEncoderInterruptBaseStateType rotaryStateCW = ROTARY_EVENT_CLEARED; + volatile RotaryEncoderInterruptBaseStateType rotaryStateCCW = ROTARY_EVENT_CLEARED; + volatile int rotaryLevelA = LOW; + volatile int rotaryLevelB = LOW; + volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE; + + private: + uint8_t _pinA; + uint8_t _pinB; + char _eventCw; + char _eventCcw; + char _eventPressed; + const char *_originName; +}; \ No newline at end of file diff --git a/src/input/RotaryEncoderInterruptImpl1.cpp b/src/input/RotaryEncoderInterruptImpl1.cpp new file mode 100644 index 000000000..e0e904a0d --- /dev/null +++ b/src/input/RotaryEncoderInterruptImpl1.cpp @@ -0,0 +1,51 @@ +#include "RotaryEncoderInterruptImpl1.h" +#include "InputBroker.h" + +RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; + +RotaryEncoderInterruptImpl1::RotaryEncoderInterruptImpl1() : + RotaryEncoderInterruptBase( + "rotEnc1") +{ +} + +void RotaryEncoderInterruptImpl1::init() +{ + if (!radioConfig.preferences.rotary1_enabled) + { + // Input device is disabled. + return; + } + + uint8_t pinA = radioConfig.preferences.rotary1_pin_a; + uint8_t pinB = radioConfig.preferences.rotary1_pin_b; + uint8_t pinPress = radioConfig.preferences.rotary1_pin_press; + char eventCw = + static_cast(radioConfig.preferences.rotary1_event_cw); + char eventCcw = + static_cast(radioConfig.preferences.rotary1_event_ccw); + char eventPressed = + static_cast(radioConfig.preferences.rotary1_event_press); + + //radioConfig.preferences.ext_notification_plugin_output + RotaryEncoderInterruptBase::init( + pinA, pinB, pinPress, + eventCw, eventCcw, eventPressed, + RotaryEncoderInterruptImpl1::handleIntA, + RotaryEncoderInterruptImpl1::handleIntB, + RotaryEncoderInterruptImpl1::handleIntPressed); + inputBroker->registerSource(this); +} + +void RotaryEncoderInterruptImpl1::handleIntA() +{ + rotaryEncoderInterruptImpl1->intAHandler(); +} +void RotaryEncoderInterruptImpl1::handleIntB() +{ + rotaryEncoderInterruptImpl1->intBHandler(); +} +void RotaryEncoderInterruptImpl1::handleIntPressed() +{ + rotaryEncoderInterruptImpl1->intPressHandler(); +} diff --git a/src/input/RotaryEncoderInterruptImpl1.h b/src/input/RotaryEncoderInterruptImpl1.h new file mode 100644 index 000000000..aeafeeca7 --- /dev/null +++ b/src/input/RotaryEncoderInterruptImpl1.h @@ -0,0 +1,22 @@ +#pragma once +#include "RotaryEncoderInterruptBase.h" + +/** + * @brief The idea behind this class to have static methods for the event handlers. + * Check attachInterrupt() at RotaryEncoderInteruptBase.cpp + * Technically you can have as many rotary encoders hardver attached + * to your device as you wish, but you always need to have separate event + * handlers, thus you need to have a RotaryEncoderInterrupt implementation. + */ +class RotaryEncoderInterruptImpl1 : + public RotaryEncoderInterruptBase +{ + public: + RotaryEncoderInterruptImpl1(); + void init(); + static void handleIntA(); + static void handleIntB(); + static void handleIntPressed(); +}; + +extern RotaryEncoderInterruptImpl1 *rotaryEncoderInterruptImpl1; \ No newline at end of file diff --git a/src/mesh/MeshPlugin.cpp b/src/mesh/MeshPlugin.cpp index 85988ec7b..985a7060f 100644 --- a/src/mesh/MeshPlugin.cpp +++ b/src/mesh/MeshPlugin.cpp @@ -235,3 +235,18 @@ std::vector MeshPlugin::GetMeshPluginsWithUIFrames() } return pluginsWithUIFrames; } + +void MeshPlugin::observeUIEvents( + Observer *observer) +{ + std::vector pluginsWithUIFrames; + for (auto i = plugins->begin(); i != plugins->end(); ++i) { + auto &pi = **i; + Observable *observable = + pi.getUIFrameObservable(); + if (observable != NULL) { + DEBUG_MSG("Plugin wants a UI Frame\n"); + observer->observe(observable); + } + } +} diff --git a/src/mesh/MeshPlugin.h b/src/mesh/MeshPlugin.h index 0143814a7..b6bcf22b8 100644 --- a/src/mesh/MeshPlugin.h +++ b/src/mesh/MeshPlugin.h @@ -21,6 +21,14 @@ enum class ProcessMessage STOP = 1, }; +/* + * This struct is used by Screen to figure out whether screen frame should be updated. + */ +typedef struct _UIFrameEvent { + bool frameChanged; + bool needRedraw; +} UIFrameEvent; + /** A baseclass for any mesh "plugin". * * A plugin allows you to add new features to meshtastic device code, without needing to know messaging details. @@ -48,6 +56,7 @@ class MeshPlugin static void callPlugins(const MeshPacket &mp, RxSource src = RX_SRC_RADIO); static std::vector GetMeshPluginsWithUIFrames(); + static void observeUIEvents(Observer *observer); #ifndef NO_SCREEN virtual void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { return; } #endif @@ -119,6 +128,7 @@ class MeshPlugin * @return true if you want to be alloced a UI screen frame */ virtual bool wantUIFrame() { return false; } + virtual Observable* getUIFrameObservable() { return NULL; } MeshPacket *allocAckNak(Routing_Error err, NodeNum to, PacketId idFrom, ChannelIndex chIndex); diff --git a/src/plugins/CannedMessagePlugin.cpp b/src/plugins/CannedMessagePlugin.cpp new file mode 100644 index 000000000..ba3cb02c9 --- /dev/null +++ b/src/plugins/CannedMessagePlugin.cpp @@ -0,0 +1,280 @@ +#include "configuration.h" +#include "CannedMessagePlugin.h" +#include "MeshService.h" + +// TODO: reuse defined from Screen.cpp +#define FONT_SMALL ArialMT_Plain_10 +#define FONT_MEDIUM ArialMT_Plain_16 +#define FONT_LARGE ArialMT_Plain_24 + +// Remove Canned message screen if no action is taken for some milliseconds +#define INACTIVATE_AFTER_MS 20000 + +CannedMessagePlugin *cannedMessagePlugin; + +CannedMessagePlugin::CannedMessagePlugin() + : SinglePortPlugin("canned", PortNum_TEXT_MESSAGE_APP), + concurrency::OSThread("CannedMessagePlugin") +{ + if (radioConfig.preferences.canned_message_plugin_enabled) + { + if(this->splitConfiguredMessages() <= 0) + { + radioConfig.preferences.canned_message_plugin_enabled = false; + DEBUG_MSG("CannedMessagePlugin: No messages are configured. Plugin is disabled\n"); + return; + } + this->inputObserver.observe(inputBroker); + } +} + +/** + * @brief Items in array this->messages will be set to be pointing on the right + * starting points of the string radioConfig.preferences.canned_message_plugin_messages + * + * @return int Returns the number of messages found. + */ +int CannedMessagePlugin::splitConfiguredMessages() +{ + int messageIndex = 0; + int i = 0; + this->messages[messageIndex++] = + radioConfig.preferences.canned_message_plugin_messages; + int upTo = + strlen(radioConfig.preferences.canned_message_plugin_messages) - 1; + + while (i < upTo) + { + if (radioConfig.preferences.canned_message_plugin_messages[i] == '|') + { + // Message ending found, replace it with string-end character. + radioConfig.preferences.canned_message_plugin_messages[i] = '\0'; + DEBUG_MSG("CannedMessage %d is: '%s'\n", + messageIndex-1, this->messages[messageIndex-1]); + + if (messageIndex >= CANNED_MESSAGE_PLUGIN_MESSAGE_MAX_COUNT) + { + this->messagesCount = messageIndex; + return this->messagesCount; + } + + // Next message starts after pipe (|) just found. + this->messages[messageIndex++] = + (radioConfig.preferences.canned_message_plugin_messages + i + 1); + } + i += 1; + } + if (strlen(this->messages[messageIndex-1]) > 0) + { + DEBUG_MSG("CannedMessage %d is: '%s'\n", + messageIndex-1, this->messages[messageIndex-1]); + this->messagesCount = messageIndex; + } + else + { + this->messagesCount = messageIndex-1; + } + + return this->messagesCount; +} + +int CannedMessagePlugin::handleInputEvent(const InputEvent *event) +{ + if ( + (strlen(radioConfig.preferences.canned_message_plugin_allow_input_origin) > 0) && + (strcmp(radioConfig.preferences.canned_message_plugin_allow_input_origin, event->source) != 0) && + (strcmp(radioConfig.preferences.canned_message_plugin_allow_input_origin, "_any") != 0)) + { + // Event source is not accepted. + return 0; + } + + bool validEvent = false; + if (event->inputEvent == static_cast(InputEventChar_UP)) + { + DEBUG_MSG("Canned message event UP\n"); + this->action = CANNED_MESSAGE_ACTION_UP; + validEvent = true; + } + if (event->inputEvent == static_cast(InputEventChar_DOWN)) + { + DEBUG_MSG("Canned message event DOWN\n"); + this->action = CANNED_MESSAGE_ACTION_DOWN; + validEvent = true; + } + if (event->inputEvent == static_cast(InputEventChar_SELECT)) + { + DEBUG_MSG("Canned message event Select\n"); + this->action = CANNED_MESSAGE_ACTION_SELECT; + validEvent = true; + } + + if (validEvent) + { + // Let runOnce to be called immediately. + setIntervalFromNow(0); + } + + return 0; +} + +void CannedMessagePlugin::sendText(NodeNum dest, + const char* message, + bool wantReplies) +{ + MeshPacket *p = allocDataPacket(); + p->to = dest; + p->want_ack = true; + p->decoded.payload.size = strlen(message); + memcpy(p->decoded.payload.bytes, message, p->decoded.payload.size); + if (radioConfig.preferences.canned_message_plugin_send_bell) + { + p->decoded.payload.bytes[p->decoded.payload.size-1] = 7; // Bell character + p->decoded.payload.bytes[p->decoded.payload.size] = '\0'; // Bell character + p->decoded.payload.size++; + } + + +// PacketId prevPacketId = p->id; // In case we need it later. + + DEBUG_MSG("Sending message id=%d, msg=%.*s\n", + p->id, p->decoded.payload.size, p->decoded.payload.bytes); + + service.sendToMesh(p); +} + +int32_t CannedMessagePlugin::runOnce() +{ + if (!radioConfig.preferences.canned_message_plugin_enabled) + { + return 30000; // TODO: should return MAX_VAL + } + DEBUG_MSG("Check status\n"); + UIFrameEvent e = {false, true}; + if (this->sendingState == SENDING_STATE_ACTIVE) + { + // TODO: might have some feedback of sendig state + this->sendingState = SENDING_STATE_NONE; + e.frameChanged = true; + this->notifyObservers(&e); + } + else if ((this->action != CANNED_MESSAGE_ACTION_NONE) + && (this->currentMessageIndex == -1)) + { + this->currentMessageIndex = 0; + DEBUG_MSG("First touch.\n"); + e.frameChanged = true; + } + else if (this->action == CANNED_MESSAGE_ACTION_SELECT) + { + sendText( + NODENUM_BROADCAST, + this->messages[this->currentMessageIndex], + true); + this->sendingState = SENDING_STATE_ACTIVE; + this->currentMessageIndex = -1; + this->notifyObservers(&e); + return 2000; + } + else if (this->action == CANNED_MESSAGE_ACTION_UP) + { + this->currentMessageIndex = getPrevIndex(); + DEBUG_MSG("MOVE UP"); + } + else if (this->action == CANNED_MESSAGE_ACTION_DOWN) + { + this->currentMessageIndex = this->getNextIndex(); + DEBUG_MSG("MOVE DOWN"); + } + if (this->action != CANNED_MESSAGE_ACTION_NONE) + { + this->lastTouchMillis = millis(); + this->action = CANNED_MESSAGE_ACTION_NONE; + this->notifyObservers(&e); + return INACTIVATE_AFTER_MS; + } + if ((millis() - this->lastTouchMillis) > INACTIVATE_AFTER_MS) + { + // Reset plugin + DEBUG_MSG("Reset due the lack of activity.\n"); + e.frameChanged = true; + this->currentMessageIndex = -1; + this->sendingState = SENDING_STATE_NONE; + this->notifyObservers(&e); + } + + return 30000; // TODO: should return MAX_VAL +} + +String CannedMessagePlugin::getCurrentMessage() +{ + return this->messages[this->currentMessageIndex]; +} +String CannedMessagePlugin::getPrevMessage() +{ + return this->messages[this->getPrevIndex()]; +} +String CannedMessagePlugin::getNextMessage() +{ + return this->messages[this->getNextIndex()]; +} +bool CannedMessagePlugin::shouldDraw() +{ + if (!radioConfig.preferences.canned_message_plugin_enabled) + { + return false; + } + return (currentMessageIndex != -1) || (this->sendingState != SENDING_STATE_NONE); +} +cannedMessagePluginSendigState CannedMessagePlugin::getSendingState() +{ + return this->sendingState; +} + +int CannedMessagePlugin::getNextIndex() +{ + if (this->currentMessageIndex >= (this->messagesCount -1)) + { + return 0; + } + else + { + return this->currentMessageIndex + 1; + } +} + +int CannedMessagePlugin::getPrevIndex() +{ + if (this->currentMessageIndex <= 0) + { + return this->messagesCount - 1; + } + else + { + return this->currentMessageIndex - 1; + } +} + +void CannedMessagePlugin::drawFrame( + OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) +{ + displayedNodeNum = 0; // Not currently showing a node pane + + if (cannedMessagePlugin->getSendingState() == SENDING_STATE_NONE) + { + display->setTextAlignment(TEXT_ALIGN_LEFT); + display->setFont(FONT_SMALL); + display->drawString(0 + x, 0 + y, cannedMessagePlugin->getPrevMessage()); + display->setFont(FONT_MEDIUM); + display->drawString(0 + x, 0 + y + 8, cannedMessagePlugin->getCurrentMessage()); + display->setFont(FONT_SMALL); + display->drawString(0 + x, 0 + y + 24, cannedMessagePlugin->getNextMessage()); + } + else + { + display->setTextAlignment(TEXT_ALIGN_CENTER); + display->setFont(FONT_MEDIUM); + display->drawString(display->getWidth()/2 + x, 0 + y + 12, "Sending..."); + } +} + diff --git a/src/plugins/CannedMessagePlugin.h b/src/plugins/CannedMessagePlugin.h new file mode 100644 index 000000000..eab516b85 --- /dev/null +++ b/src/plugins/CannedMessagePlugin.h @@ -0,0 +1,69 @@ +#pragma once +#include "SinglePortPlugin.h" +#include "input/InputBroker.h" + +enum cannedMessagePluginActionType +{ + CANNED_MESSAGE_ACTION_NONE, + CANNED_MESSAGE_ACTION_SELECT, + CANNED_MESSAGE_ACTION_UP, + CANNED_MESSAGE_ACTION_DOWN +}; + +enum cannedMessagePluginSendigState +{ + SENDING_STATE_NONE, + SENDING_STATE_ACTIVE +}; + + +#define CANNED_MESSAGE_PLUGIN_MESSAGE_MAX_COUNT 50 + +class CannedMessagePlugin : + public SinglePortPlugin, + public Observable, + private concurrency::OSThread +{ + CallbackObserver inputObserver = + CallbackObserver( + this, &CannedMessagePlugin::handleInputEvent); + public: + CannedMessagePlugin(); + String getCurrentMessage(); + String getPrevMessage(); + String getNextMessage(); + bool shouldDraw(); + cannedMessagePluginSendigState getSendingState(); + void eventUp(); + void eventDown(); + void eventSelect(); + + protected: + + virtual int32_t runOnce(); + + void sendText( + NodeNum dest, + const char* message, + bool wantReplies); + + int splitConfiguredMessages(); + int getNextIndex(); + int getPrevIndex(); + + int handleInputEvent(const InputEvent *event); + virtual bool wantUIFrame() { return this->shouldDraw(); } + virtual Observable* getUIFrameObservable() { return this; } + virtual void drawFrame( + OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y); + + volatile cannedMessagePluginActionType action = CANNED_MESSAGE_ACTION_NONE; + int currentMessageIndex = -1; + cannedMessagePluginSendigState sendingState = SENDING_STATE_NONE; + + char *messages[CANNED_MESSAGE_PLUGIN_MESSAGE_MAX_COUNT]; + int messagesCount = 0; + unsigned long lastTouchMillis = 0; +}; + +extern CannedMessagePlugin *cannedMessagePlugin; diff --git a/src/plugins/Plugins.cpp b/src/plugins/Plugins.cpp index 92602c7a8..03ab1a8a0 100644 --- a/src/plugins/Plugins.cpp +++ b/src/plugins/Plugins.cpp @@ -1,4 +1,6 @@ #include "configuration.h" +#include "input/InputBroker.h" +#include "input/RotaryEncoderInterruptImpl1.h" #include "plugins/ExternalNotificationPlugin.h" #include "plugins/NodeInfoPlugin.h" #include "plugins/PositionPlugin.h" @@ -8,6 +10,7 @@ #include "plugins/TextMessagePlugin.h" #include "plugins/RoutingPlugin.h" #include "plugins/AdminPlugin.h" +#include "plugins/CannedMessagePlugin.h" #ifndef NO_ESP32 #include "plugins/esp32/SerialPlugin.h" #include "plugins/esp32/EnvironmentalMeasurementPlugin.h" @@ -20,6 +23,7 @@ */ void setupPlugins() { + inputBroker = new InputBroker(); adminPlugin = new AdminPlugin(); nodeInfoPlugin = new NodeInfoPlugin(); positionPlugin = new PositionPlugin(); @@ -30,6 +34,10 @@ void setupPlugins() new RemoteHardwarePlugin(); new ReplyPlugin(); + rotaryEncoderInterruptImpl1 = + new RotaryEncoderInterruptImpl1(); + rotaryEncoderInterruptImpl1->init(); + cannedMessagePlugin = new CannedMessagePlugin(); #ifndef NO_ESP32 // Only run on an esp32 based device.