mirror of
https://github.com/meshtastic/firmware.git
synced 2026-01-16 14:57:41 +00:00
Merge branch 'master' into usbhost
This commit is contained in:
260
src/input/ExpressLRSFiveWay.cpp
Normal file
260
src/input/ExpressLRSFiveWay.cpp
Normal file
@@ -0,0 +1,260 @@
|
||||
|
||||
#include "ExpressLRSFiveWay.h"
|
||||
|
||||
#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE
|
||||
|
||||
static const char inputSourceName[] = "ExpressLRS5Way"; // should match "allow input source" string
|
||||
|
||||
/**
|
||||
* @brief Calculate fuzz: half the distance to the next nearest neighbor for each joystick position.
|
||||
*
|
||||
* The goal is to avoid collisions between joystick positions while still maintaining
|
||||
* the widest tolerance for the analog value.
|
||||
*
|
||||
* Example: {10,50,800,1000,300,1600}
|
||||
* If we just choose the minimum difference for this array the value would
|
||||
* be 40/2 = 20.
|
||||
*
|
||||
* 20 does not leave enough room for the joystick position using 1600 which
|
||||
* could have a +-100 offset.
|
||||
*
|
||||
* Example Fuzz values: {20, 20, 100, 100, 125, 300} now the fuzz for the 1600
|
||||
* position is 300 instead of 20
|
||||
*/
|
||||
void ExpressLRSFiveWay::calcFuzzValues()
|
||||
{
|
||||
for (unsigned int i = 0; i < N_JOY_ADC_VALUES; i++) {
|
||||
uint16_t closestDist = 0xffff;
|
||||
uint16_t ival = joyAdcValues[i];
|
||||
// Find the closest value to ival
|
||||
for (unsigned int j = 0; j < N_JOY_ADC_VALUES; j++) {
|
||||
// Don't compare value with itself
|
||||
if (j == i)
|
||||
continue;
|
||||
uint16_t jval = joyAdcValues[j];
|
||||
if (jval < ival && (ival - jval < closestDist))
|
||||
closestDist = ival - jval;
|
||||
if (jval > ival && (jval - ival < closestDist))
|
||||
closestDist = jval - ival;
|
||||
} // for j
|
||||
|
||||
// And the fuzz is half the distance to the closest value
|
||||
fuzzValues[i] = closestDist / 2;
|
||||
// DBG("joy%u=%u f=%u, ", i, ival, fuzzValues[i]);
|
||||
} // for i
|
||||
}
|
||||
|
||||
int ExpressLRSFiveWay::readKey()
|
||||
{
|
||||
uint16_t value = analogRead(PIN_JOYSTICK);
|
||||
|
||||
constexpr uint8_t IDX_TO_INPUT[N_JOY_ADC_VALUES - 1] = {UP, DOWN, LEFT, RIGHT, OK};
|
||||
for (unsigned int i = 0; i < N_JOY_ADC_VALUES - 1; ++i) {
|
||||
if (value < (joyAdcValues[i] + fuzzValues[i]) && value > (joyAdcValues[i] - fuzzValues[i]))
|
||||
return IDX_TO_INPUT[i];
|
||||
}
|
||||
return NO_PRESS;
|
||||
}
|
||||
|
||||
ExpressLRSFiveWay::ExpressLRSFiveWay() : concurrency::OSThread(inputSourceName)
|
||||
{
|
||||
// ExpressLRS: init values
|
||||
isLongPressed = false;
|
||||
keyInProcess = NO_PRESS;
|
||||
keyDownStart = 0;
|
||||
|
||||
// Express LRS: calculate the threshold for interpreting ADC values as various buttons
|
||||
calcFuzzValues();
|
||||
|
||||
// Meshtastic: register with canned messages
|
||||
inputBroker->registerSource(this);
|
||||
}
|
||||
|
||||
// ExpressLRS: interpret reading as key events
|
||||
void ExpressLRSFiveWay::update(int *keyValue, bool *keyLongPressed)
|
||||
{
|
||||
*keyValue = NO_PRESS;
|
||||
|
||||
int newKey = readKey();
|
||||
uint32_t now = millis();
|
||||
if (keyInProcess == NO_PRESS) {
|
||||
// New key down
|
||||
if (newKey != NO_PRESS) {
|
||||
keyDownStart = now;
|
||||
// DBGLN("down=%u", newKey);
|
||||
}
|
||||
} else {
|
||||
// if key released
|
||||
if (newKey == NO_PRESS) {
|
||||
// DBGLN("up=%u", keyInProcess);
|
||||
if (!isLongPressed) {
|
||||
if ((now - keyDownStart) > KEY_DEBOUNCE_MS) {
|
||||
*keyValue = keyInProcess;
|
||||
*keyLongPressed = false;
|
||||
}
|
||||
}
|
||||
isLongPressed = false;
|
||||
}
|
||||
// else if the key has changed while down, reset state for next go-around
|
||||
else if (newKey != keyInProcess) {
|
||||
newKey = NO_PRESS;
|
||||
}
|
||||
// else still pressing, waiting for long if not already signaled
|
||||
else if (!isLongPressed) {
|
||||
if ((now - keyDownStart) > KEY_LONG_PRESS_MS) {
|
||||
*keyValue = keyInProcess;
|
||||
*keyLongPressed = true;
|
||||
isLongPressed = true;
|
||||
}
|
||||
}
|
||||
} // if keyInProcess != NO_PRESS
|
||||
|
||||
keyInProcess = newKey;
|
||||
}
|
||||
|
||||
// Meshtastic: runs at regular intervals
|
||||
int32_t ExpressLRSFiveWay::runOnce()
|
||||
{
|
||||
uint32_t now = millis();
|
||||
|
||||
// Dismiss any alert frames after 2 seconds
|
||||
// Feedback for GPS toggle / adhoc ping
|
||||
if (alerting && now > alertingSinceMs + 2000) {
|
||||
alerting = false;
|
||||
screen->endAlert();
|
||||
}
|
||||
|
||||
// Get key events from ExpressLRS code
|
||||
int keyValue;
|
||||
bool longPressed;
|
||||
update(&keyValue, &longPressed);
|
||||
|
||||
// Do something about this key press
|
||||
determineAction((KeyType)keyValue, longPressed ? LONG : SHORT);
|
||||
|
||||
// If there has been recent key activity, poll the joystick slightly more frequently
|
||||
if (now < keyDownStart + (20 * 1000UL)) // Within last 20 seconds
|
||||
return 100;
|
||||
|
||||
// Otherwise, poll slightly less often
|
||||
// Too many missed pressed if much slower than 250ms
|
||||
return 250;
|
||||
}
|
||||
|
||||
// Determine what action to take when a button press is detected
|
||||
// Written verbose for easier remapping by user
|
||||
void ExpressLRSFiveWay::determineAction(KeyType key, PressLength length)
|
||||
{
|
||||
switch (key) {
|
||||
case LEFT:
|
||||
if (inCannedMessageMenu()) // If in canned message menu
|
||||
sendKey(CANCEL); // exit the menu (press imaginary cancel key)
|
||||
else
|
||||
sendKey(LEFT);
|
||||
break;
|
||||
|
||||
case RIGHT:
|
||||
if (inCannedMessageMenu()) // If in canned message menu:
|
||||
sendKey(CANCEL); // exit the menu (press imaginary cancel key)
|
||||
else
|
||||
sendKey(RIGHT);
|
||||
break;
|
||||
|
||||
case UP:
|
||||
if (length == LONG)
|
||||
toggleGPS();
|
||||
else
|
||||
sendKey(UP);
|
||||
break;
|
||||
|
||||
case DOWN:
|
||||
if (length == LONG)
|
||||
sendAdhocPing();
|
||||
else
|
||||
sendKey(DOWN);
|
||||
break;
|
||||
|
||||
case OK:
|
||||
if (length == LONG)
|
||||
shutdown();
|
||||
else
|
||||
click(); // Use instead of sendKey(OK). Works better when canned message module disabled
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Feed input to the canned messages module
|
||||
void ExpressLRSFiveWay::sendKey(KeyType key)
|
||||
{
|
||||
InputEvent e;
|
||||
e.source = inputSourceName;
|
||||
e.inputEvent = key;
|
||||
notifyObservers(&e);
|
||||
}
|
||||
|
||||
// Enable or Disable a connected GPS
|
||||
// Contained as one method for easier remapping of buttons by user
|
||||
void ExpressLRSFiveWay::toggleGPS()
|
||||
{
|
||||
#if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS
|
||||
if (!config.device.disable_triple_click && (gps != nullptr)) {
|
||||
gps->toggleGpsMode();
|
||||
screen->startAlert("GPS Toggled");
|
||||
alerting = true;
|
||||
alertingSinceMs = millis();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Send either node-info or position, on demand
|
||||
// Contained as one method for easier remapping of buttons by user
|
||||
void ExpressLRSFiveWay::sendAdhocPing()
|
||||
{
|
||||
service->refreshLocalMeshNode();
|
||||
bool sentPosition = service->trySendPosition(NODENUM_BROADCAST, true);
|
||||
|
||||
// Show custom alert frame, with multi-line centering
|
||||
screen->startAlert([sentPosition](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
|
||||
uint16_t x_offset = display->width() / 2;
|
||||
uint16_t y_offset = 26; // Same constant as the default startAlert frame
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setFont(FONT_MEDIUM);
|
||||
display->drawString(x_offset + x, y_offset + y, "Sent ad-hoc");
|
||||
display->drawString(x_offset + x, y_offset + FONT_HEIGHT_MEDIUM + y, sentPosition ? "position" : "nodeinfo");
|
||||
});
|
||||
|
||||
alerting = true;
|
||||
alertingSinceMs = millis();
|
||||
}
|
||||
|
||||
// Shutdown the node (enter deep-sleep)
|
||||
// Contained as one method for easier remapping of buttons by user
|
||||
void ExpressLRSFiveWay::shutdown()
|
||||
{
|
||||
LOG_INFO("Shutdown from long press\n");
|
||||
powerFSM.trigger(EVENT_PRESS);
|
||||
screen->startAlert("Shutting down...");
|
||||
// Don't set alerting = true. We don't want to auto-dismiss this alert.
|
||||
|
||||
playShutdownMelody(); // In case user adds a buzzer
|
||||
|
||||
shutdownAtMsec = millis() + 3000;
|
||||
}
|
||||
|
||||
// Emulate user button, or canned message SELECT
|
||||
// This is necessary as canned message module doesn't translate SELECT to user button presses if the module is disabled
|
||||
// Contained as one method for easier remapping of buttons by user
|
||||
void ExpressLRSFiveWay::click()
|
||||
{
|
||||
if (!moduleConfig.canned_message.enabled)
|
||||
powerFSM.trigger(EVENT_PRESS);
|
||||
else
|
||||
sendKey(OK);
|
||||
}
|
||||
|
||||
ExpressLRSFiveWay *expressLRSFiveWayInput = nullptr;
|
||||
|
||||
#endif
|
||||
85
src/input/ExpressLRSFiveWay.h
Normal file
85
src/input/ExpressLRSFiveWay.h
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
Input source for Radio Master Bandit Nano, and similar hardware.
|
||||
Devices have a 5-button "resistor ladder" style joystick, read by ADC.
|
||||
These devices do not use the ADC to monitor input voltage.
|
||||
|
||||
Much of this code taken directly from ExpressLRS FiveWayButton class:
|
||||
https://github.com/ExpressLRS/ExpressLRS/tree/d9f56f8bd6f9f7144d5f01caaca766383e1e0950/src/lib/SCREEN/FiveWayButton
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef INPUTBROKER_EXPRESSLRSFIVEWAY_TYPE
|
||||
|
||||
#include <esp_adc_cal.h>
|
||||
#include <soc/adc_channel.h>
|
||||
|
||||
#include "InputBroker.h"
|
||||
#include "MeshService.h" // For adhoc ping action
|
||||
#include "buzz.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "graphics/Screen.h" // Feedback for adhoc ping / toggle GPS
|
||||
#include "main.h"
|
||||
#include "modules/CannedMessageModule.h"
|
||||
|
||||
#if HAS_GPS && !MESHTASTIC_EXCLUDE_GPS
|
||||
#include "GPS.h" // For toggle GPS action
|
||||
#endif
|
||||
|
||||
class ExpressLRSFiveWay : public Observable<const InputEvent *>, public concurrency::OSThread
|
||||
{
|
||||
private:
|
||||
// Number of values in JOY_ADC_VALUES, if defined
|
||||
// These must be ADC readings for {UP, DOWN, LEFT, RIGHT, ENTER, IDLE}
|
||||
static constexpr size_t N_JOY_ADC_VALUES = 6;
|
||||
static constexpr uint32_t KEY_DEBOUNCE_MS = 25;
|
||||
static constexpr uint32_t KEY_LONG_PRESS_MS = 3000; // How many milliseconds to hold key for a long press
|
||||
|
||||
// This merged an enum used by the ExpressLRS code, with meshtastic canned message values
|
||||
// Key names are kept simple, to allow user customizaton
|
||||
typedef enum {
|
||||
UP = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP,
|
||||
DOWN = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN,
|
||||
LEFT = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT,
|
||||
RIGHT = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT,
|
||||
OK = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT,
|
||||
CANCEL = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL,
|
||||
NO_PRESS = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE
|
||||
} KeyType;
|
||||
|
||||
typedef enum { SHORT, LONG } PressLength;
|
||||
|
||||
// From ExpressLRS
|
||||
int keyInProcess;
|
||||
uint32_t keyDownStart;
|
||||
bool isLongPressed;
|
||||
const uint16_t joyAdcValues[N_JOY_ADC_VALUES] = {JOYSTICK_ADC_VALS};
|
||||
uint16_t fuzzValues[N_JOY_ADC_VALUES];
|
||||
void calcFuzzValues();
|
||||
int readKey();
|
||||
void update(int *keyValue, bool *keyLongPressed);
|
||||
|
||||
// Meshtastic code
|
||||
void determineAction(KeyType key, PressLength length);
|
||||
void sendKey(KeyType key);
|
||||
inline bool inCannedMessageMenu() { return cannedMessageModule->shouldDraw(); }
|
||||
int32_t runOnce() override;
|
||||
|
||||
// Simplified Meshtastic actions, for easier remapping by user
|
||||
void toggleGPS();
|
||||
void sendAdhocPing();
|
||||
void shutdown();
|
||||
void click();
|
||||
|
||||
bool alerting = false; // Is the screen showing an alert frame? Feedback for GPS toggle / adhoc ping actions
|
||||
uint32_t alertingSinceMs = 0; // When did screen begin showing an alert frame? Used to auto-dismiss
|
||||
|
||||
public:
|
||||
ExpressLRSFiveWay();
|
||||
};
|
||||
|
||||
extern ExpressLRSFiveWay *expressLRSFiveWayInput;
|
||||
|
||||
#endif
|
||||
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
|
||||
170
src/input/SerialKeyboard.cpp
Normal file
170
src/input/SerialKeyboard.cpp
Normal file
@@ -0,0 +1,170 @@
|
||||
#include "SerialKeyboard.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef INPUTBROKER_SERIAL_TYPE
|
||||
#define CANNED_MESSAGE_MODULE_ENABLE 1 // in case it's not set in the variant file
|
||||
|
||||
#if INPUTBROKER_SERIAL_TYPE == 1 // It's a Chatter
|
||||
// 3 SHIFT level (lower case, upper case, numbers), up to 4 repeated presses, button number
|
||||
unsigned char KeyMap[3][4][10] = {{{'.', 'a', 'd', 'g', 'j', 'm', 'p', 't', 'w', ' '},
|
||||
{',', 'b', 'e', 'h', 'k', 'n', 'q', 'u', 'x', ' '},
|
||||
{'?', 'c', 'f', 'i', 'l', 'o', 'r', 'v', 'y', ' '},
|
||||
{'1', '2', '3', '4', '5', '6', 's', '8', 'z', ' '}}, // low case
|
||||
{{'!', 'A', 'D', 'G', 'J', 'M', 'P', 'T', 'W', ' '},
|
||||
{'+', 'B', 'E', 'H', 'K', 'N', 'Q', 'U', 'X', ' '},
|
||||
{'-', 'C', 'F', 'I', 'L', 'O', 'R', 'V', 'Y', ' '},
|
||||
{'1', '2', '3', '4', '5', '6', 'S', '8', 'Z', ' '}}, // upper case
|
||||
{{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'},
|
||||
{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'},
|
||||
{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'},
|
||||
{'1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}}}; // numbers
|
||||
|
||||
#endif
|
||||
|
||||
SerialKeyboard::SerialKeyboard(const char *name) : concurrency::OSThread(name)
|
||||
{
|
||||
this->_originName = name;
|
||||
}
|
||||
|
||||
void SerialKeyboard::erase()
|
||||
{
|
||||
InputEvent e;
|
||||
e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK;
|
||||
e.kbchar = 0x08;
|
||||
e.source = this->_originName;
|
||||
this->notifyObservers(&e);
|
||||
}
|
||||
|
||||
int32_t SerialKeyboard::runOnce()
|
||||
{
|
||||
if (!INPUTBROKER_SERIAL_TYPE) {
|
||||
// Input device is not requested.
|
||||
return disable();
|
||||
}
|
||||
|
||||
if (firstTime) {
|
||||
// This is the first time the OSThread library has called this function, so do port setup
|
||||
firstTime = 0;
|
||||
pinMode(KB_LOAD, OUTPUT);
|
||||
pinMode(KB_CLK, OUTPUT);
|
||||
pinMode(KB_DATA, INPUT);
|
||||
digitalWrite(KB_LOAD, HIGH);
|
||||
digitalWrite(KB_CLK, LOW);
|
||||
prevKeys = 0b1111111111111111;
|
||||
LOG_DEBUG("Serial Keyboard setup\n");
|
||||
}
|
||||
|
||||
if (INPUTBROKER_SERIAL_TYPE == 1) { // Chatter V1.0 & V2.0 keypads
|
||||
// scan for keypresses
|
||||
// Write pulse to load pin
|
||||
digitalWrite(KB_LOAD, LOW);
|
||||
delayMicroseconds(5);
|
||||
digitalWrite(KB_LOAD, HIGH);
|
||||
delayMicroseconds(5);
|
||||
|
||||
// Get data from 74HC165
|
||||
byte shiftRegister1 = shiftIn(KB_DATA, KB_CLK, LSBFIRST);
|
||||
byte shiftRegister2 = shiftIn(KB_DATA, KB_CLK, LSBFIRST);
|
||||
|
||||
keys = (shiftRegister1 << 8) + shiftRegister2;
|
||||
|
||||
// Print to serial monitor
|
||||
// Serial.print (shiftRegister1, BIN);
|
||||
// Serial.print ("X");
|
||||
// Serial.println (shiftRegister2, BIN);
|
||||
|
||||
if (millis() - lastPressTime > 500) {
|
||||
quickPress = 0;
|
||||
}
|
||||
|
||||
if (keys < prevKeys) { // a new key has been pressed (and not released), doesn't works for multiple presses at once but
|
||||
// shouldn't be a limitation
|
||||
InputEvent e;
|
||||
e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE;
|
||||
e.source = this->_originName;
|
||||
// SELECT OR SEND OR CANCEL EVENT
|
||||
if (!(shiftRegister2 & (1 << 3))) {
|
||||
e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP;
|
||||
} else if (!(shiftRegister2 & (1 << 2))) {
|
||||
e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT;
|
||||
e.kbchar = 0xb7;
|
||||
} else if (!(shiftRegister2 & (1 << 1))) {
|
||||
e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT;
|
||||
} else if (!(shiftRegister2 & (1 << 0))) {
|
||||
e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL;
|
||||
}
|
||||
|
||||
// TEXT INPUT EVENT
|
||||
else if (!(shiftRegister1 & (1 << 4))) {
|
||||
keyPressed = 0;
|
||||
} else if (!(shiftRegister1 & (1 << 3))) {
|
||||
keyPressed = 1;
|
||||
} else if (!(shiftRegister2 & (1 << 4))) {
|
||||
keyPressed = 2;
|
||||
} else if (!(shiftRegister1 & (1 << 5))) {
|
||||
keyPressed = 3;
|
||||
} else if (!(shiftRegister1 & (1 << 2))) {
|
||||
keyPressed = 4;
|
||||
} else if (!(shiftRegister2 & (1 << 5))) {
|
||||
keyPressed = 5;
|
||||
} else if (!(shiftRegister1 & (1 << 6))) {
|
||||
keyPressed = 6;
|
||||
} else if (!(shiftRegister1 & (1 << 1))) {
|
||||
keyPressed = 7;
|
||||
} else if (!(shiftRegister2 & (1 << 6))) {
|
||||
keyPressed = 8;
|
||||
} else if (!(shiftRegister1 & (1 << 0))) {
|
||||
keyPressed = 9;
|
||||
}
|
||||
// BACKSPACE or TAB
|
||||
else if (!(shiftRegister1 & (1 << 7))) {
|
||||
if (shift == 0 || shift == 2) { // BACKSPACE
|
||||
e.inputEvent = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_BACK;
|
||||
e.kbchar = 0x08;
|
||||
} else { // shift = 1 => TAB
|
||||
e.inputEvent = ANYKEY;
|
||||
e.kbchar = 0x09;
|
||||
}
|
||||
}
|
||||
// SHIFT
|
||||
else if (!(shiftRegister2 & (1 << 7))) {
|
||||
keyPressed = 10;
|
||||
}
|
||||
|
||||
if (keyPressed < 11) {
|
||||
if (keyPressed == lastKeyPressed && millis() - lastPressTime < 500) {
|
||||
quickPress += 1;
|
||||
if (quickPress > 3) {
|
||||
quickPress = 0;
|
||||
}
|
||||
}
|
||||
if (keyPressed != lastKeyPressed) {
|
||||
quickPress = 0;
|
||||
}
|
||||
if (keyPressed < 10) { // if it's a letter
|
||||
if (keyPressed == lastKeyPressed && millis() - lastPressTime < 500) {
|
||||
erase();
|
||||
}
|
||||
e.inputEvent = ANYKEY;
|
||||
e.kbchar = char(KeyMap[shift][quickPress][keyPressed]);
|
||||
} else { // then it's shift
|
||||
shift += 1;
|
||||
if (shift > 2) {
|
||||
shift = 0;
|
||||
}
|
||||
}
|
||||
lastPressTime = millis();
|
||||
lastKeyPressed = keyPressed;
|
||||
keyPressed = 13;
|
||||
}
|
||||
|
||||
if (e.inputEvent != meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE) {
|
||||
this->notifyObservers(&e);
|
||||
}
|
||||
}
|
||||
prevKeys = keys;
|
||||
}
|
||||
return 50;
|
||||
}
|
||||
|
||||
#endif // INPUTBROKER_SERIAL_TYPE
|
||||
25
src/input/SerialKeyboard.h
Normal file
25
src/input/SerialKeyboard.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "InputBroker.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
|
||||
class SerialKeyboard : public Observable<const InputEvent *>, public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
explicit SerialKeyboard(const char *name);
|
||||
|
||||
protected:
|
||||
virtual int32_t runOnce() override;
|
||||
void erase();
|
||||
|
||||
private:
|
||||
const char *_originName;
|
||||
bool firstTime = 1;
|
||||
int prevKeys = 0;
|
||||
int keys = 0;
|
||||
int shift = 0;
|
||||
int keyPressed = 13;
|
||||
int lastKeyPressed = 13;
|
||||
int quickPress = 0;
|
||||
unsigned long lastPressTime = 0;
|
||||
};
|
||||
21
src/input/SerialKeyboardImpl.cpp
Normal file
21
src/input/SerialKeyboardImpl.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "SerialKeyboardImpl.h"
|
||||
#include "InputBroker.h"
|
||||
#include "configuration.h"
|
||||
|
||||
#ifdef INPUTBROKER_SERIAL_TYPE
|
||||
|
||||
SerialKeyboardImpl *aSerialKeyboardImpl;
|
||||
|
||||
SerialKeyboardImpl::SerialKeyboardImpl() : SerialKeyboard("serialKB") {}
|
||||
|
||||
void SerialKeyboardImpl::init()
|
||||
{
|
||||
if (!INPUTBROKER_SERIAL_TYPE) {
|
||||
disable();
|
||||
return;
|
||||
}
|
||||
|
||||
inputBroker->registerSource(this);
|
||||
}
|
||||
|
||||
#endif // INPUTBROKER_SERIAL_TYPE
|
||||
19
src/input/SerialKeyboardImpl.h
Normal file
19
src/input/SerialKeyboardImpl.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
#include "SerialKeyboard.h"
|
||||
#include "main.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 SerialKeyboardImpl : public SerialKeyboard
|
||||
{
|
||||
public:
|
||||
SerialKeyboardImpl();
|
||||
void init();
|
||||
};
|
||||
|
||||
extern SerialKeyboardImpl *aSerialKeyboardImpl;
|
||||
Reference in New Issue
Block a user