Add Elecrow ThinkNode M8 board support (thinknode_m8) (#11226)

* Add Elecrow ThinkNode M8 variant scaffold (thinknode_m8)

nRF52840 + SX1262 + 2.4" e-paper + ATGM336H-5NR32 GPS.
All pins resolved from ThinkNode_M8_V0.3.sch; cross-checked
against meshtastic/firmware#9181 (Elecrow V0.1 reference).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add Elecrow ThinkNode M8 board support (nRF52840/SX1262, 1.54in e-ink, ATGM336H GNSS, SC7A20, EC04 encoder)

* Address review: keep the stored backlight level out of blanking, match only the SC7A20 WHO_AM_I byte, and transfer detents atomically

* Use std::atomic for the press-and-turn detent counter so native builds compile

* Drop the ThinkNode M8 LED_BUILTIN redefinition that warned on every translation unit

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Göttgens
2026-07-30 15:59:35 +00:00
committed by GitHub
co-authored by GitHub claude[bot] <41898282+claude[bot]@users.noreply.github.com> Claude Sonnet 4.6
parent 84322af4e0
commit 597f6767b5
22 changed files with 641 additions and 33 deletions
+1 -1
View File
@@ -566,7 +566,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
// technically speaking this should work for all(?) NRF52 boards
// but needs testing across multiple devices. NRF52 USB would not even work if
// VBUS was not properly connected and detected by the CPU
#elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO)
#elif defined(MUZI_BASE) || defined(PROMICRO_DIY_TCXO) || defined(ELECROW_ThinkNode_M8)
return powerHAL_isVBUSConnected();
#endif
return getBattVoltage() > chargingVolt;
+3 -3
View File
@@ -37,9 +37,9 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
{
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX,
ICM20948, QMA6100P, BMM150, BMI270, ICM42607P, ISM330DHCX};
return firstOfOrNONE(12, types);
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, SC7A20, BMA423, LSM6DS3, BMX160, STK8BAXX,
ICM20948, BMM150, BMI270, ICM42607P, ISM330DHCX, QMA6100P};
return firstOfOrNONE(13, types);
}
ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const
+1
View File
@@ -46,6 +46,7 @@ class ScanI2C
QMA6100P,
MPU6050,
LIS3DH,
SC7A20, // LIS3DH register map, different WHO_AM_I
BMA423,
BQ24295,
LSM6DS3,
+4
View File
@@ -691,6 +691,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize)
if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333
type = LIS3DH;
logFoundDevice("LIS3DH", (uint8_t)addr.address);
} else if ((registerValue & 0xFF00) == 0x1100) {
// Silan SC7A20: LIS3DH register map, but answers 0x11 here.
type = SC7A20;
logFoundDevice("SC7A20", (uint8_t)addr.address);
} else {
type = BMA423;
logFoundDevice("BMA423", (uint8_t)addr.address);
+71
View File
@@ -0,0 +1,71 @@
#include "graphics/Backlight.h"
#if HAS_PWM_BACKLIGHT
#include "mesh/NodeDB.h"
namespace graphics
{
namespace
{
bool pinConfigured = false;
// Level restored when the backlight is switched back on after being toggled off.
uint8_t lastOnLevel = PWM_BACKLIGHT_DEFAULT;
void drive(uint8_t level)
{
if (!pinConfigured) {
pinMode(PIN_PWM_BACKLIGHT, OUTPUT);
pinConfigured = true;
}
analogWrite(PIN_PWM_BACKLIGHT, level);
}
} // namespace
void backlightSet(uint8_t level)
{
if (level > 0)
lastOnLevel = level;
uiconfig.screen_brightness = level;
drive(level);
}
uint8_t backlightGet()
{
return uiconfig.screen_brightness;
}
void backlightOn()
{
drive(uiconfig.screen_brightness);
}
void backlightOff()
{
drive(0);
}
void backlightToggle()
{
backlightSet(uiconfig.screen_brightness > 0 ? 0 : lastOnLevel);
}
void backlightStepUp()
{
uint16_t raised = (uint16_t)uiconfig.screen_brightness + PWM_BACKLIGHT_STEP;
backlightSet(raised > PWM_BACKLIGHT_MAX ? PWM_BACKLIGHT_MAX : (uint8_t)raised);
}
void backlightStepDown()
{
// Leave an off backlight off; otherwise clamp at the minimum.
if (uiconfig.screen_brightness == 0)
return;
backlightSet(uiconfig.screen_brightness <= PWM_BACKLIGHT_MIN + PWM_BACKLIGHT_STEP
? PWM_BACKLIGHT_MIN
: uiconfig.screen_brightness - PWM_BACKLIGHT_STEP);
}
} // namespace graphics
#endif // HAS_PWM_BACKLIGHT
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include "configuration.h"
// PWM backlight control. A variant opts in by defining PIN_PWM_BACKLIGHT, optionally with
// PWM_BACKLIGHT_DEFAULT, _MIN, _MAX and _STEP. Levels are 0..255 in uiconfig.screen_brightness.
#if defined(PIN_PWM_BACKLIGHT)
#define HAS_PWM_BACKLIGHT 1
#else
#define HAS_PWM_BACKLIGHT 0
#endif
#if HAS_PWM_BACKLIGHT
#ifndef PWM_BACKLIGHT_DEFAULT
#define PWM_BACKLIGHT_DEFAULT 128
#endif
#ifndef PWM_BACKLIGHT_MIN
#define PWM_BACKLIGHT_MIN 8
#endif
#ifndef PWM_BACKLIGHT_MAX
#define PWM_BACKLIGHT_MAX 248
#endif
#ifndef PWM_BACKLIGHT_STEP
#define PWM_BACKLIGHT_STEP 20
#endif
namespace graphics
{
void backlightSet(uint8_t level);
uint8_t backlightGet();
void backlightOn(); // drive the stored level
void backlightOff(); // drive 0, leaving the stored level alone
void backlightToggle();
void backlightStepUp();
void backlightStepDown();
} // namespace graphics
#endif // HAS_PWM_BACKLIGHT
+4 -2
View File
@@ -1,4 +1,5 @@
#include "configuration.h"
#include "graphics/Backlight.h"
#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY)
#include "EInkDisplay2.h"
@@ -158,13 +159,14 @@ bool EInkDisplay::connect()
#endif
#endif
#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) || defined(TTGO_T_ECHO_PLUS)
#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) || defined(TTGO_T_ECHO_PLUS) || \
defined(ELECROW_ThinkNode_M8)
{
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
adafruitDisplay->init();
#if defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE)
#if defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) || defined(ELECROW_ThinkNode_M8)
adafruitDisplay->setRotation(4);
#else
adafruitDisplay->setRotation(3);
+29 -8
View File
@@ -61,6 +61,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "error.h"
#include "gps/GeoCoord.h"
#include "gps/RTC.h"
#include "graphics/Backlight.h"
#include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TFTPalette.h"
@@ -691,7 +692,9 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
dispdev->displayOn();
#endif
#ifdef PIN_EINK_EN
#if HAS_PWM_BACKLIGHT
graphics::backlightOn();
#elif defined(PIN_EINK_EN)
if (uiconfig.screen_brightness == 1)
digitalWrite(PIN_EINK_EN, HIGH);
#elif defined(PCA_PIN_EINK_EN)
@@ -751,7 +754,9 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
drawLockdownLockScreen(dispdev);
#endif
#ifdef PIN_EINK_EN
#if HAS_PWM_BACKLIGHT
graphics::backlightOff();
#elif defined(PIN_EINK_EN)
digitalWrite(PIN_EINK_EN, LOW);
#elif defined(PCA_PIN_EINK_EN)
io.digitalWrite(PCA_PIN_EINK_EN, LOW);
@@ -1848,8 +1853,12 @@ void Screen::handleStartFirmwareUpdateScreen()
void Screen::increaseBrightness()
{
#if HAS_PWM_BACKLIGHT
graphics::backlightStepUp();
brightness = graphics::backlightGet();
#else
brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62);
#endif
#if defined(ST7789_CS)
// run the setDisplayBrightness function. This works on t-decks
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
@@ -1860,8 +1869,12 @@ void Screen::increaseBrightness()
void Screen::decreaseBrightness()
{
#if HAS_PWM_BACKLIGHT
graphics::backlightStepDown();
brightness = graphics::backlightGet();
#else
brightness = (brightness < 70) ? brightness : (brightness - 62);
#endif
#if defined(ST7789_CS)
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
#endif
@@ -2057,10 +2070,18 @@ int Screen::handleInputEvent(const InputEvent *event)
}
#ifdef USE_EINK // the screen is the last input handler, so if an event makes it here, we can assume it will prompt a screen draw.
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update
handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?)
setFastFramerate(); // Draw ASAP
#if HAS_PWM_BACKLIGHT
// A PWM backlight change leaves the frame identical, and the refresh is blocking.
const bool backlightOnly =
event->kbchar == INPUT_BROKER_MSG_BRIGHTNESS_UP || event->kbchar == INPUT_BROKER_MSG_BRIGHTNESS_DOWN;
if (!backlightOnly)
#endif
{
EINK_ADD_FRAMEFLAG(dispdev, DEMAND_FAST); // Use fast-refresh for next frame, no skip please
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update
handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?)
setFastFramerate(); // Draw ASAP
}
#endif
if (NotificationRenderer::isOverlayBannerShowing()) {
NotificationRenderer::inEvent = *event;
+6 -2
View File
@@ -10,6 +10,7 @@
#include "MessageStore.h"
#include "NodeDB.h"
#include "buzz.h"
#include "graphics/Backlight.h"
#include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h"
#include "graphics/TFTColorRegions.h"
@@ -1108,7 +1109,7 @@ void menuHandler::homeBaseMenu()
}
optionsEnumArray[options++] = Mute;
}
#if defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN)
#if HAS_PWM_BACKLIGHT || defined(PIN_EINK_EN) || defined(PCA_PIN_EINK_EN)
optionsArray[options] = "Toggle Backlight";
optionsEnumArray[options++] = Backlight;
#else
@@ -1138,7 +1139,10 @@ void menuHandler::homeBaseMenu()
}
} else if (selected == Backlight) {
screen->setOn(false);
#if defined(PIN_EINK_EN)
#if HAS_PWM_BACKLIGHT
graphics::backlightToggle();
saveUIConfig();
#elif defined(PIN_EINK_EN)
if (uiconfig.screen_brightness == 1) {
uiconfig.screen_brightness = 0;
digitalWrite(PIN_EINK_EN, LOW);
+7 -1
View File
@@ -227,7 +227,13 @@ int32_t ButtonThread::runOnce()
case BUTTON_EVENT_DOUBLE_PRESSED: { // not wired in if screen detected
LOG_INFO("Double press!");
#if defined(ELECROW_ThinkNode_M8)
if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_ENABLED)
config.device.buzzer_mode = meshtastic_Config_DeviceConfig_BuzzerMode_DISABLED;
else if (config.position.gps_mode == meshtastic_Config_PositionConfig_GpsMode_DISABLED)
config.device.buzzer_mode = meshtastic_Config_DeviceConfig_BuzzerMode_ALL_ENABLED;
service->reloadConfig(SEGMENT_CONFIG);
#endif
// Reset combination tracking
waitingForLongPress = false;
+40 -6
View File
@@ -35,11 +35,12 @@
#endif
#if HAS_BUTTON || defined(ARCH_PORTDUINO)
#include "graphics/Backlight.h"
#include "input/ButtonThread.h"
#if defined(BUTTON_PIN_TOUCH)
ButtonThread *TouchButtonThread = nullptr;
#if defined(PIN_EINK_EN)
#if HAS_PWM_BACKLIGHT || defined(PIN_EINK_EN)
static bool touchBacklightWasOn = false;
static bool touchBacklightActive = false;
#endif
@@ -241,20 +242,28 @@ void InputBroker::Init()
};
touchConfig.singlePress = INPUT_BROKER_NONE;
touchConfig.longPress = INPUT_BROKER_BACK;
#if defined(PIN_EINK_EN)
// Touch pad drives the backlight on devices with e-ink backlight pin
#if HAS_PWM_BACKLIGHT || defined(PIN_EINK_EN)
// Touch pad drives the backlight on devices that have one
touchConfig.longPress = INPUT_BROKER_NONE;
touchConfig.suppressLeadUpSound = true;
touchConfig.onPress = []() {
touchBacklightWasOn = uiconfig.screen_brightness == 1;
touchBacklightWasOn = uiconfig.screen_brightness > 0;
if (!touchBacklightWasOn) {
#if HAS_PWM_BACKLIGHT
graphics::backlightOn();
#else
digitalWrite(PIN_EINK_EN, HIGH);
#endif
}
touchBacklightActive = true;
};
touchConfig.onRelease = []() {
if (touchBacklightActive && !touchBacklightWasOn) {
#if HAS_PWM_BACKLIGHT
graphics::backlightOff();
#else
digitalWrite(PIN_EINK_EN, LOW);
#endif
}
touchBacklightActive = false;
};
@@ -327,6 +336,30 @@ void InputBroker::Init()
#define BUTTON_ACTIVE_PULLUP true
#endif
#if defined(ELECROW_ThinkNode_M8)
// Rotary encoder drives the UI, so the function button keeps a fixed map.
LOG_DEBUG("ThinkNode_M8 button");
UserButtonThread = new ButtonThread("FunctionButton");
{
ButtonConfig userConfig;
userConfig.pinNumber = (uint8_t)_pinNum;
userConfig.activeLow = BUTTON_ACTIVE_LOW;
userConfig.activePullup = BUTTON_ACTIVE_PULLUP;
userConfig.pullupSense = pullup_sense;
userConfig.intRoutine = []() {
UserButtonThread->userButton.tick();
UserButtonThread->setIntervalFromNow(0);
runASAP = true;
BaseType_t higherWake = 0;
concurrency::mainDelay.interruptFromISR(&higherWake);
};
userConfig.singlePress = INPUT_BROKER_SEND_PING;
userConfig.longPress = INPUT_BROKER_SHUTDOWN;
userConfig.longPressTime = 5000;
userConfig.doublePress = INPUT_BROKER_GPS_TOGGLE;
UserButtonThread->initButton(userConfig);
}
#else
// Buttons. Moved here cause we need NodeDB to be initialized
// If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP
UserButtonThread = new ButtonThread("UserButton");
@@ -378,8 +411,9 @@ void InputBroker::Init()
userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE;
UserButtonThread->initButton(userConfigNoScreen);
}
#endif
#endif
#endif // ELECROW_ThinkNode_M8
#endif // BUTTON_PIN
#endif // HAS_BUTTON
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
+44 -6
View File
@@ -56,24 +56,50 @@ int32_t RotaryEncoderInterruptBase::runOnce()
if (!pressDetected && buttonPressed) {
pressDetected = true;
pressStartTime = now;
pressAndTurnFired = false;
}
if (pressDetected) {
// Press-and-turn takes precedence over the press itself.
if (pressAndTurnEnabled() && pressAndTurnDelta.load(std::memory_order_relaxed) != 0) {
// Drain in one pass: releasing the button would discard anything left over.
// Exchange, so a detent arriving from the ISR mid-drain is not lost.
int32_t pending = pressAndTurnDelta.exchange(0, std::memory_order_relaxed);
LOG_DEBUG("Rotary event Press %s (%d detents)", pending > 0 ? "CW" : "CCW", pending);
while (pending != 0) {
bool cw = pending > 0;
InputEvent turn = {};
turn.source = this->_originName;
turn.inputEvent = INPUT_BROKER_NONE;
turn.kbchar = cw ? _pressAndTurnCw : _pressAndTurnCcw;
pending -= cw ? 1 : -1;
this->notifyObservers(&turn);
}
pressAndTurnFired = true;
}
uint32_t duration = now - pressStartTime;
if (!buttonPressed) {
// released -> if short press, send short, else already sent long
if (duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) {
if (!pressAndTurnFired && duration < LONG_PRESS_DURATION && now - lastPressKeyTime >= pressDebounceMs) {
lastPressKeyTime = now;
LOG_DEBUG("Rotary event Press short");
e.inputEvent = this->_eventPressed;
} else if (pressAndTurnEnabled() && !pressAndTurnFired && duration >= LONG_PRESS_DURATION &&
this->_eventPressedLong != INPUT_BROKER_NONE) {
// Held long enough and no turn came, so the long press stands
LOG_DEBUG("Rotary event Press long");
e.inputEvent = this->_eventPressedLong;
}
pressDetected = false;
pressStartTime = 0;
lastPressLongEventTime = 0;
pressAndTurnDelta.store(0, std::memory_order_relaxed);
pressAndTurnFired = false;
this->action = ROTARY_ACTION_NONE;
} else if (duration >= LONG_PRESS_DURATION && this->_eventPressedLong != INPUT_BROKER_NONE &&
lastPressLongEventTime == 0) {
// fire single-shot long press
} else if (!pressAndTurnEnabled() && duration >= LONG_PRESS_DURATION &&
this->_eventPressedLong != INPUT_BROKER_NONE && lastPressLongEventTime == 0) {
// fire single-shot long press; press-and-turn encoders defer this to release
lastPressLongEventTime = now;
LOG_DEBUG("Rotary event Press long");
e.inputEvent = this->_eventPressedLong;
@@ -87,7 +113,7 @@ int32_t RotaryEncoderInterruptBase::runOnce()
e.inputEvent = this->_eventCcw;
}
if (e.inputEvent != INPUT_BROKER_NONE) {
if (e.inputEvent != INPUT_BROKER_NONE || e.kbchar != 0) {
this->notifyObservers(&e);
}
@@ -95,11 +121,20 @@ int32_t RotaryEncoderInterruptBase::runOnce()
this->action = ROTARY_ACTION_NONE;
} else if (now - pressStartTime < LONG_PRESS_DURATION) {
return (20); // keep checking for long/short until time expires
} else if (pressAndTurnEnabled()) {
// Keep polling while held, rather than relying on intHandler()'s reschedule from ISR.
return (20);
}
return INT32_MAX;
}
void RotaryEncoderInterruptBase::setPressAndTurnChars(unsigned char cw, unsigned char ccw)
{
this->_pressAndTurnCw = cw;
this->_pressAndTurnCcw = ccw;
}
void RotaryEncoderInterruptBase::intPressHandler()
{
this->action = ROTARY_ACTION_PRESSED;
@@ -145,7 +180,10 @@ RotaryEncoderInterruptBaseStateType RotaryEncoderInterruptBase::intHandler(bool
if (actualPinRaising && (otherPinLevel == LOW)) {
if (state == ROTARY_EVENT_CLEARED) {
newState = ROTARY_EVENT_OCCURRED;
if ((this->action != ROTARY_ACTION_PRESSED) && (this->action != action)) {
if (this->action == ROTARY_ACTION_PRESSED) {
// Turning while held; runOnce() ignores this unless press-and-turn is enabled.
pressAndTurnDelta.fetch_add((action == ROTARY_ACTION_CW) ? 1 : -1, std::memory_order_relaxed);
} else {
this->action = action;
}
}
+14 -1
View File
@@ -3,9 +3,9 @@
#include "InputBroker.h"
#include "concurrency/OSThread.h"
#include "mesh/NodeDB.h"
#include <atomic>
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<const InputEvent *>, public concurrency::OSThread
@@ -20,6 +20,10 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
void intAHandler();
void intBHandler();
// Turning while the button is held emits `cw` / `ccw` as kbchar and suppresses the press
// event. Both default to 0, leaving rotation-while-pressed ignored.
void setPressAndTurnChars(unsigned char cw, unsigned char ccw);
protected:
virtual int32_t runOnce() override;
RotaryEncoderInterruptBaseStateType intHandler(bool actualPinRaising, int otherPinLevel,
@@ -31,6 +35,8 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
volatile int rotaryLevelA = LOW;
volatile int rotaryLevelB = LOW;
volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE;
// Detents counted while the button is held: positive clockwise, negative counter-clockwise.
std::atomic<int32_t> pressAndTurnDelta{0};
private:
// pins and events
@@ -43,9 +49,16 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
const char *_originName;
// Press-and-turn characters, 0 when the gesture is not in use
unsigned char _pressAndTurnCw = 0;
unsigned char _pressAndTurnCcw = 0;
bool pressAndTurnEnabled() const { return _pressAndTurnCw != 0 || _pressAndTurnCcw != 0; }
// Long press detection variables
uint32_t pressStartTime = 0;
bool pressDetected = false;
// Suppresses the press event on release once press-and-turn has fired.
bool pressAndTurnFired = false;
uint32_t lastPressLongEventTime = 0;
unsigned long lastPressKeyTime = 0;
static const uint32_t LONG_PRESS_DURATION = 300; // ms
@@ -26,6 +26,9 @@ bool RotaryEncoderInterruptImpl1::init()
RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong,
RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB,
RotaryEncoderInterruptImpl1::handleIntPressed);
#if defined(INPUTDRIVER_PRESS_AND_TURN_CW) && defined(INPUTDRIVER_PRESS_AND_TURN_CCW)
setPressAndTurnChars(INPUTDRIVER_PRESS_AND_TURN_CW, INPUTDRIVER_PRESS_AND_TURN_CCW);
#endif
inputBroker->registerSource(this);
#ifndef HAS_PHYSICAL_KEYBOARD
osk_found = true;
+9
View File
@@ -1270,6 +1270,15 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.active = true;
#endif // NANO_G2_ULTRA
#ifdef ELECROW_ThinkNode_M8
moduleConfig.canned_message.rotary1_enabled = true;
moduleConfig.canned_message.inputbroker_pin_a = PIN_BUTTON_EC04_A;
moduleConfig.canned_message.inputbroker_pin_b = PIN_BUTTON_EC04_B;
moduleConfig.canned_message.inputbroker_pin_press = PIN_BUTTON_EC04;
moduleConfig.canned_message.inputbroker_event_cw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT;
moduleConfig.canned_message.inputbroker_event_ccw = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT;
moduleConfig.canned_message.inputbroker_event_press = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT;
#endif
#ifdef T_LORA_PAGER
moduleConfig.canned_message.updown1_enabled = true;
moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A;
+1
View File
@@ -106,6 +106,7 @@ class AccelerometerThread : public concurrency::OSThread
break;
#if __has_include(<Adafruit_LIS3DH.h>)
case ScanI2C::DeviceType::LIS3DH:
case ScanI2C::DeviceType::SC7A20:
sensor = new LIS3DHSensor(device);
break;
#endif
+5 -3
View File
@@ -7,14 +7,16 @@ LIS3DHSensor::LIS3DHSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::Mot
bool LIS3DHSensor::init()
{
if (sensor.begin(deviceAddress())) {
// The SC7A20 shares the register map but identifies as 0x11.
const uint8_t whoAmI = (deviceType() == ScanI2C::DeviceType::SC7A20) ? 0x11 : 0x33;
if (sensor.begin(deviceAddress(), whoAmI)) {
sensor.setRange(LIS3DH_RANGE_2_G);
// Adjust threshold, higher numbers are less sensitive
sensor.setClick(config.device.double_tap_as_button_press ? 2 : 1, MOTION_SENSOR_CHECK_INTERVAL_MS);
LOG_DEBUG("LIS3DH init ok");
LOG_DEBUG("%s init ok", deviceType() == ScanI2C::DeviceType::SC7A20 ? "SC7A20" : "LIS3DH");
return true;
}
LOG_DEBUG("LIS3DH init failed");
LOG_DEBUG("%s init failed", deviceType() == ScanI2C::DeviceType::SC7A20 ? "SC7A20" : "LIS3DH");
return false;
}
+2
View File
@@ -99,6 +99,8 @@
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M6
#elif defined(ELECROW_ThinkNode_M4)
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M4
#elif defined(ELECROW_ThinkNode_M8)
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M8
#elif defined(NANO_G2_ULTRA)
#define HW_VENDOR meshtastic_HardwareModel_NANO_G2_ULTRA
#elif defined(CANARYONE)