mirror of
https://github.com/meshtastic/firmware.git
synced 2025-02-08 05:31:25 +00:00
"Scan and Select" input for Canned Messages (#4365)
* 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
This commit is contained in:
parent
9ec7dbd695
commit
92526fca23
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
@ -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) {
|
||||
|
204
src/input/ScanAndSelect.cpp
Normal file
204
src/input/ScanAndSelect.cpp
Normal file
@ -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
src/input/ScanAndSelect.h
Normal file
50
src/input/ScanAndSelect.h
Normal file
@ -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
|
@ -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));
|
||||
|
@ -10,6 +10,7 @@
|
||||
#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
|
||||
@ -694,9 +695,22 @@ 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)) {
|
||||
@ -931,13 +945,17 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
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) {
|
||||
// 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
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setFont(FONT_MEDIUM);
|
||||
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 {
|
||||
@ -951,17 +969,37 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
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, y + 100, buffer, snrString, this->lastRxSnr);
|
||||
display->drawStringf(display->getWidth() / 2 + x, y + 130, buffer, rssiString, this->lastRxRssi);
|
||||
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->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);
|
||||
@ -1033,11 +1071,18 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
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));
|
||||
|
@ -56,6 +56,7 @@ class CannedMessageModule : public SinglePortModule, public Observable<const UIF
|
||||
const char *getMessageByIndex(int index);
|
||||
const char *getNodeName(NodeNum node);
|
||||
bool shouldDraw();
|
||||
bool hasMessages();
|
||||
// void eventUp();
|
||||
// void eventDown();
|
||||
// void eventSelect();
|
||||
|
@ -2,6 +2,7 @@
|
||||
#if !MESHTASTIC_EXCLUDE_INPUTBROKER
|
||||
#include "input/InputBroker.h"
|
||||
#include "input/RotaryEncoderInterruptImpl1.h"
|
||||
#include "input/ScanAndSelect.h"
|
||||
#include "input/SerialKeyboardImpl.h"
|
||||
#include "input/TrackballInterruptImpl1.h"
|
||||
#include "input/UpDownInterruptImpl1.h"
|
||||
@ -144,6 +145,16 @@ void setupModules()
|
||||
delete upDownInterruptImpl1;
|
||||
upDownInterruptImpl1 = nullptr;
|
||||
}
|
||||
|
||||
#if HAS_SCREEN
|
||||
// In order to have the user button dismiss the canned message frame, this class lightly interacts with the Screen class
|
||||
scanAndSelectInput = new ScanAndSelectInput();
|
||||
if (!scanAndSelectInput->init()) {
|
||||
delete scanAndSelectInput;
|
||||
scanAndSelectInput = nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
cardKbI2cImpl = new CardKbI2cImpl();
|
||||
cardKbI2cImpl->init();
|
||||
#ifdef INPUTBROKER_MATRIX_TYPE
|
||||
|
@ -1,4 +1,6 @@
|
||||
#define BUTTON_PIN 0
|
||||
#define BUTTON_PIN_SECONDARY 21 // Second built-in button
|
||||
#define BUTTON_SECONDARY_CANNEDMESSAGES // By default, use the secondary button as canned message input
|
||||
|
||||
// I2C
|
||||
#define I2C_SDA SDA
|
||||
|
@ -1,4 +1,6 @@
|
||||
#define BUTTON_PIN 0
|
||||
#define BUTTON_PIN_SECONDARY 21 // Second built-in button
|
||||
#define BUTTON_SECONDARY_CANNEDMESSAGES // By default, use the secondary button as canned message input
|
||||
|
||||
// I2C
|
||||
#define I2C_SDA SDA
|
||||
|
Loading…
Reference in New Issue
Block a user