Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Scan and Select" input for Canned Messages #4365

Merged
merged 5 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/graphics/EInkDynamicDisplay.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ void EInkDynamicDisplay::hashImage()

// Sum all bytes of the image buffer together
for (uint16_t b = 0; b < (displayWidth / 8) * displayHeight; b++) {
imageHash += buffer[b];
imageHash ^= buffer[b] << b;
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/graphics/Screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "gps/RTC.h"
#include "graphics/ScreenFonts.h"
#include "graphics/images.h"
#include "input/ScanAndSelect.h"
#include "input/TouchScreenImpl1.h"
#include "main.h"
#include "mesh-pb-constants.h"
Expand Down Expand Up @@ -2291,6 +2292,11 @@ void Screen::handlePrint(const char *text)

void Screen::handleOnPress()
{
// If Canned Messages is using the "Scan and Select" input, dismiss the canned message frame when user button is pressed
// Minimize impact as a courtesy, as "scan and select" may be used as default config for some boards
if (scanAndSelectInput != nullptr && scanAndSelectInput->dismissCannedMessageFrame())
return;

// If screen was off, just wake it, otherwise advance to next frame
// If we are in a transition, the press must have bounced, drop it.
if (ui->getUiState()->frameState == FIXED) {
Expand Down
204 changes: 204 additions & 0 deletions src/input/ScanAndSelect.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
#include "configuration.h"

// Normally these input methods are protected by guarding in setupModules
// In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen class
#if HAS_SCREEN

#include "ScanAndSelect.h"
#include "modules/CannedMessageModule.h"

// Config
static const char name[] = "scanAndSelect"; // should match "allow input source" string
static constexpr uint32_t durationShortMs = 50;
static constexpr uint32_t durationLongMs = 1500;
static constexpr uint32_t durationAlertMs = 2000;

// Constructor: init base class
ScanAndSelectInput::ScanAndSelectInput() : concurrency::OSThread(name) {}

// Attempt to setup class; true if success.
// Called by setupModules method. Instance deleted if setup fails.
bool ScanAndSelectInput::init()
{
// Short circuit: Canned messages enabled?
if (!moduleConfig.canned_message.enabled)
return false;

// Short circuit: Using correct "input source"?
// Todo: protobuf enum instead of string?
if (strcasecmp(moduleConfig.canned_message.allow_input_source, name) != 0)
return false;

// Use any available inputbroker pin as the button
if (moduleConfig.canned_message.inputbroker_pin_press)
pin = moduleConfig.canned_message.inputbroker_pin_press;
else if (moduleConfig.canned_message.inputbroker_pin_a)
pin = moduleConfig.canned_message.inputbroker_pin_a;
else if (moduleConfig.canned_message.inputbroker_pin_b)
pin = moduleConfig.canned_message.inputbroker_pin_b;
else
return false; // Short circuit: no button found

// Set-up the button
pinMode(pin, INPUT_PULLUP);
attachInterrupt(pin, handleChangeInterrupt, CHANGE);

// Connect our class to the canned message module
inputBroker->registerSource(this);

LOG_INFO("Initialized 'Scan and Select' input for Canned Messages, using pin %d\n", pin);
return true; // Init succeded
}

// Runs periodically, unless sleeping between presses
int32_t ScanAndSelectInput::runOnce()
{
uint32_t now = millis();

// If: "no messages added" alert screen currently shown
if (alertingNoMessage) {
// Dismiss the alert screen several seconds after it appears
if (now > alertingSinceMs + durationAlertMs) {
alertingNoMessage = false;
screen->endAlert();
}
}

// If: Button is pressed
if (digitalRead(pin) == LOW) {
// New press
if (!held) {
downSinceMs = now;
}

// Existing press
else {
// Duration enough for long press
// Long press not yet fired (prevent repeat firing while held)
if (!longPressFired && now - downSinceMs > durationLongMs) {
longPressFired = true;
longPress();
}
}

// Record the change of state: button is down
held = true;
}

// If: Button is not pressed
else {
// Button newly released
// Long press event didn't already fire
if (held && !longPressFired) {
// Duration enough for short press
if (now - downSinceMs > durationShortMs) {
shortPress();
}
}

// Record the change of state: button is up
held = false;
longPressFired = false; // Re-Arm: allow another long press
}

// If thread's job is done, let it sleep
if (!held && !alertingNoMessage) {
Thread::canSleep = true;
return OSThread::disable();
}

// Run this method again is a few ms
return durationShortMs;
}

void ScanAndSelectInput::longPress()
{
// (If canned messages set)
if (cannedMessageModule->hasMessages()) {
// If module frame displayed already, send the current message
if (cannedMessageModule->shouldDraw())
raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT);

// Otherwise, initial long press opens the module frame
else
raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN);
}

// (If canned messages not set) tell the user
else
alertNoMessage();
}

void ScanAndSelectInput::shortPress()
{
// (If canned messages set) scroll to next message
if (cannedMessageModule->hasMessages())
raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN);

// (If canned messages not yet set) tell the user
else
alertNoMessage();
}

// Begin running runOnce at regular intervals
// Called from pin change interrupt
void ScanAndSelectInput::enableThread()
{
Thread::canSleep = false;
OSThread::enabled = true;
OSThread::setIntervalFromNow(0);
}

// Inform user (screen) that no canned messages have been added
// Automatically dismissed after several seconds
void ScanAndSelectInput::alertNoMessage()
{
alertingNoMessage = true;
alertingSinceMs = millis();

// Graphics code: the alert frame to show on screen
screen->startAlert([](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
display->setTextAlignment(TEXT_ALIGN_CENTER_BOTH);
display->setFont(FONT_SMALL);
int16_t textX = display->getWidth() / 2;
int16_t textY = display->getHeight() / 2;
display->drawString(textX + x, textY + y, "No Canned Messages");
});
}

// Remove the canned message frame from screen
// Used to dismiss the module frame when user button pressed
// Returns true if the frame was previously displayed, and has now been closed
// Return value consumed by Screen class when determining how to handle user button
bool ScanAndSelectInput::dismissCannedMessageFrame()
{
if (cannedMessageModule->shouldDraw()) {
raiseEvent(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL);
return true;
}

return false;
}

// Feed input to the canned messages module
void ScanAndSelectInput::raiseEvent(_meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar key)
{
InputEvent e;
e.source = name;
e.inputEvent = key;
notifyObservers(&e);
}

// Pin change interrupt
void ScanAndSelectInput::handleChangeInterrupt()
{
// Because we need to detect both press and release (rising and falling edge), the interrupt itself can't determine the
// action. Instead, we start up the thread and get it to read the button for us

// The instance we're referring to here is created in setupModules()
scanAndSelectInput->enableThread();
}

ScanAndSelectInput *scanAndSelectInput = nullptr; // Instantiated in setupModules method. Deleted if unused, or init() fails

#endif
50 changes: 50 additions & 0 deletions src/input/ScanAndSelect.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
A "single button" input method for Canned Messages

- Short press to cycle through messages
- Long Press to send

To use:
- set "allow input source" to "scanAndSelect"
- set the single button's GPIO as either pin A, pin B, or pin Press

Originally designed to make use of "extra" built-in button on some boards.
Non-intrusive; suitable for use as a default module config.
*/

#pragma once
#include "concurrency/OSThread.h"
#include "main.h"

// Normally these input methods are protected by guarding in setupModules
// In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen class
#if HAS_SCREEN

class ScanAndSelectInput : public Observable<const InputEvent *>, public concurrency::OSThread
{
public:
ScanAndSelectInput(); // No-op constructor, only initializes OSThread base class
bool init(); // Attempt to setup class; true if success. Instance deleted if setup fails
bool dismissCannedMessageFrame(); // Remove the canned message frame from screen. True if frame was open, and now closed.
void alertNoMessage(); // Inform user (screen) that no canned messages have been added

protected:
int32_t runOnce() override; // Runs at regular intervals, when enabled
void enableThread(); // Begin running runOnce at regular intervals
static void handleChangeInterrupt(); // Calls enableThread from pin change interrupt
void shortPress(); // Code to run when short press fires
void longPress(); // Code to run when long press fires
void raiseEvent(_meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar key); // Feed input to canned message module

bool held = false; // Have we handled a change in button state?
bool longPressFired = false; // Long press fires while button still held. This bool ensures the release is no-op
uint32_t downSinceMs = 0; // Debouncing for short press, timing for long press
uint8_t pin = -1; // Read from cannned message config during init

bool alertingNoMessage = false; // Is the "no canned messages" alert shown on screen?
uint32_t alertingSinceMs = 0; // Used to dismiss the "no canned message" alert several seconds
};

extern ScanAndSelectInput *scanAndSelectInput; // Instantiated in setupModules method. Deleted if unused, or init() fails

#endif
7 changes: 7 additions & 0 deletions src/mesh/NodeDB.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,13 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.output_ms = 100;
moduleConfig.external_notification.active = true;
#endif
#ifdef BUTTON_SECONDARY_CANNEDMESSAGES
// Use a board's second built-in button as input source for canned messages
moduleConfig.canned_message.enabled = true;
moduleConfig.canned_message.inputbroker_pin_press = BUTTON_PIN_SECONDARY;
strcpy(moduleConfig.canned_message.allow_input_source, "scanAndSelect");
#endif

moduleConfig.has_canned_message = true;

strncpy(moduleConfig.mqtt.address, default_mqtt_address, sizeof(moduleConfig.mqtt.address));
Expand Down
Loading
Loading