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
+53
View File
@@ -0,0 +1,53 @@
{
"build": {
"arduino": {
"ldscript": "nrf52840_s140_v6.ld"
},
"core": "nRF5",
"cpu": "cortex-m4",
"extra_flags": "-DARDUINO_NRF52840_ThinkNode_M8 -DNRF52840_XXAA",
"f_cpu": "64000000L",
"hwids": [
["0x239A", "0x4405"],
["0x239A", "0x0029"],
["0x239A", "0x002A"]
],
"usb_product": "elecrow_thinknode_m8",
"mcu": "nrf52840",
"variant": "ELECROW-ThinkNode-M8",
"variants_dir": "variants",
"bsp": {
"name": "adafruit"
},
"softdevice": {
"sd_flags": "-DS140",
"sd_name": "s140",
"sd_version": "6.1.1",
"sd_fwid": "0x00B6"
},
"bootloader": {
"settings_addr": "0xFF000"
}
},
"connectivity": ["bluetooth"],
"debug": {
"jlink_device": "nRF52840_xxAA",
"onboard_tools": ["jlink"],
"svd_path": "nrf52840.svd",
"openocd_target": "nrf52840-mdk-rs"
},
"frameworks": ["arduino"],
"name": "elecrow thinknode m8",
"upload": {
"maximum_ram_size": 248832,
"maximum_size": 815104,
"speed": 115200,
"protocol": "nrfutil",
"protocols": ["jlink", "nrfjprog", "nrfutil", "stlink"],
"use_1200bps_touch": true,
"require_upload_port": true,
"wait_for_upload_port": true
},
"url": "",
"vendor": "ELECROW"
}
+1 -1
View File
@@ -566,7 +566,7 @@ class AnalogBatteryLevel : public HasBatteryLevel
// technically speaking this should work for all(?) NRF52 boards // technically speaking this should work for all(?) NRF52 boards
// but needs testing across multiple devices. NRF52 USB would not even work if // but needs testing across multiple devices. NRF52 USB would not even work if
// VBUS was not properly connected and detected by the CPU // 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(); return powerHAL_isVBUSConnected();
#endif #endif
return getBattVoltage() > chargingVolt; return getBattVoltage() > chargingVolt;
+3 -3
View File
@@ -37,9 +37,9 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const
ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const
{ {
ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423, LSM6DS3, BMX160, STK8BAXX, ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, SC7A20, BMA423, LSM6DS3, BMX160, STK8BAXX,
ICM20948, QMA6100P, BMM150, BMI270, ICM42607P, ISM330DHCX}; ICM20948, BMM150, BMI270, ICM42607P, ISM330DHCX, QMA6100P};
return firstOfOrNONE(12, types); return firstOfOrNONE(13, types);
} }
ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const
+1
View File
@@ -46,6 +46,7 @@ class ScanI2C
QMA6100P, QMA6100P,
MPU6050, MPU6050,
LIS3DH, LIS3DH,
SC7A20, // LIS3DH register map, different WHO_AM_I
BMA423, BMA423,
BQ24295, BQ24295,
LSM6DS3, 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 if (registerValue == 0x3300 || registerValue == 0x3333) { // RAK4631 WisBlock has LIS3DH register at 0x3333
type = LIS3DH; type = LIS3DH;
logFoundDevice("LIS3DH", (uint8_t)addr.address); 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 { } else {
type = BMA423; type = BMA423;
logFoundDevice("BMA423", (uint8_t)addr.address); 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 "configuration.h"
#include "graphics/Backlight.h"
#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY) #if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY)
#include "EInkDisplay2.h" #include "EInkDisplay2.h"
@@ -158,13 +159,14 @@ bool EInkDisplay::connect()
#endif #endif
#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); 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 = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
adafruitDisplay->init(); 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); adafruitDisplay->setRotation(4);
#else #else
adafruitDisplay->setRotation(3); 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 "error.h"
#include "gps/GeoCoord.h" #include "gps/GeoCoord.h"
#include "gps/RTC.h" #include "gps/RTC.h"
#include "graphics/Backlight.h"
#include "graphics/ScreenFonts.h" #include "graphics/ScreenFonts.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/TFTPalette.h" #include "graphics/TFTPalette.h"
@@ -691,7 +692,9 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
dispdev->displayOn(); dispdev->displayOn();
#endif #endif
#ifdef PIN_EINK_EN #if HAS_PWM_BACKLIGHT
graphics::backlightOn();
#elif defined(PIN_EINK_EN)
if (uiconfig.screen_brightness == 1) if (uiconfig.screen_brightness == 1)
digitalWrite(PIN_EINK_EN, HIGH); digitalWrite(PIN_EINK_EN, HIGH);
#elif defined(PCA_PIN_EINK_EN) #elif defined(PCA_PIN_EINK_EN)
@@ -751,7 +754,9 @@ void Screen::handleSetOn(bool on, FrameCallback einkScreensaver)
drawLockdownLockScreen(dispdev); drawLockdownLockScreen(dispdev);
#endif #endif
#ifdef PIN_EINK_EN #if HAS_PWM_BACKLIGHT
graphics::backlightOff();
#elif defined(PIN_EINK_EN)
digitalWrite(PIN_EINK_EN, LOW); digitalWrite(PIN_EINK_EN, LOW);
#elif defined(PCA_PIN_EINK_EN) #elif defined(PCA_PIN_EINK_EN)
io.digitalWrite(PCA_PIN_EINK_EN, LOW); io.digitalWrite(PCA_PIN_EINK_EN, LOW);
@@ -1848,8 +1853,12 @@ void Screen::handleStartFirmwareUpdateScreen()
void Screen::increaseBrightness() void Screen::increaseBrightness()
{ {
#if HAS_PWM_BACKLIGHT
graphics::backlightStepUp();
brightness = graphics::backlightGet();
#else
brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62); brightness = ((brightness + 62) > 254) ? brightness : (brightness + 62);
#endif
#if defined(ST7789_CS) #if defined(ST7789_CS)
// run the setDisplayBrightness function. This works on t-decks // run the setDisplayBrightness function. This works on t-decks
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness); static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
@@ -1860,8 +1869,12 @@ void Screen::increaseBrightness()
void Screen::decreaseBrightness() void Screen::decreaseBrightness()
{ {
#if HAS_PWM_BACKLIGHT
graphics::backlightStepDown();
brightness = graphics::backlightGet();
#else
brightness = (brightness < 70) ? brightness : (brightness - 62); brightness = (brightness < 70) ? brightness : (brightness - 62);
#endif
#if defined(ST7789_CS) #if defined(ST7789_CS)
static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness); static_cast<TFTDisplay *>(dispdev)->setDisplayBrightness(brightness);
#endif #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. #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 #if HAS_PWM_BACKLIGHT
EINK_ADD_FRAMEFLAG(dispdev, BLOCKING); // Edge case: if this frame is promoted to COSMETIC, wait for update // A PWM backlight change leaves the frame identical, and the refresh is blocking.
handleSetOn(true); // Ensure power-on to receive deep-sleep screensaver (PowerFSM should handle?) const bool backlightOnly =
setFastFramerate(); // Draw ASAP 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 #endif
if (NotificationRenderer::isOverlayBannerShowing()) { if (NotificationRenderer::isOverlayBannerShowing()) {
NotificationRenderer::inEvent = *event; NotificationRenderer::inEvent = *event;
+6 -2
View File
@@ -10,6 +10,7 @@
#include "MessageStore.h" #include "MessageStore.h"
#include "NodeDB.h" #include "NodeDB.h"
#include "buzz.h" #include "buzz.h"
#include "graphics/Backlight.h"
#include "graphics/Screen.h" #include "graphics/Screen.h"
#include "graphics/SharedUIDisplay.h" #include "graphics/SharedUIDisplay.h"
#include "graphics/TFTColorRegions.h" #include "graphics/TFTColorRegions.h"
@@ -1108,7 +1109,7 @@ void menuHandler::homeBaseMenu()
} }
optionsEnumArray[options++] = Mute; 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"; optionsArray[options] = "Toggle Backlight";
optionsEnumArray[options++] = Backlight; optionsEnumArray[options++] = Backlight;
#else #else
@@ -1138,7 +1139,10 @@ void menuHandler::homeBaseMenu()
} }
} else if (selected == Backlight) { } else if (selected == Backlight) {
screen->setOn(false); 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) { if (uiconfig.screen_brightness == 1) {
uiconfig.screen_brightness = 0; uiconfig.screen_brightness = 0;
digitalWrite(PIN_EINK_EN, LOW); 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 case BUTTON_EVENT_DOUBLE_PRESSED: { // not wired in if screen detected
LOG_INFO("Double press!"); 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 // Reset combination tracking
waitingForLongPress = false; waitingForLongPress = false;
+40 -6
View File
@@ -35,11 +35,12 @@
#endif #endif
#if HAS_BUTTON || defined(ARCH_PORTDUINO) #if HAS_BUTTON || defined(ARCH_PORTDUINO)
#include "graphics/Backlight.h"
#include "input/ButtonThread.h" #include "input/ButtonThread.h"
#if defined(BUTTON_PIN_TOUCH) #if defined(BUTTON_PIN_TOUCH)
ButtonThread *TouchButtonThread = nullptr; ButtonThread *TouchButtonThread = nullptr;
#if defined(PIN_EINK_EN) #if HAS_PWM_BACKLIGHT || defined(PIN_EINK_EN)
static bool touchBacklightWasOn = false; static bool touchBacklightWasOn = false;
static bool touchBacklightActive = false; static bool touchBacklightActive = false;
#endif #endif
@@ -241,20 +242,28 @@ void InputBroker::Init()
}; };
touchConfig.singlePress = INPUT_BROKER_NONE; touchConfig.singlePress = INPUT_BROKER_NONE;
touchConfig.longPress = INPUT_BROKER_BACK; touchConfig.longPress = INPUT_BROKER_BACK;
#if defined(PIN_EINK_EN) #if HAS_PWM_BACKLIGHT || defined(PIN_EINK_EN)
// Touch pad drives the backlight on devices with e-ink backlight pin // Touch pad drives the backlight on devices that have one
touchConfig.longPress = INPUT_BROKER_NONE; touchConfig.longPress = INPUT_BROKER_NONE;
touchConfig.suppressLeadUpSound = true; touchConfig.suppressLeadUpSound = true;
touchConfig.onPress = []() { touchConfig.onPress = []() {
touchBacklightWasOn = uiconfig.screen_brightness == 1; touchBacklightWasOn = uiconfig.screen_brightness > 0;
if (!touchBacklightWasOn) { if (!touchBacklightWasOn) {
#if HAS_PWM_BACKLIGHT
graphics::backlightOn();
#else
digitalWrite(PIN_EINK_EN, HIGH); digitalWrite(PIN_EINK_EN, HIGH);
#endif
} }
touchBacklightActive = true; touchBacklightActive = true;
}; };
touchConfig.onRelease = []() { touchConfig.onRelease = []() {
if (touchBacklightActive && !touchBacklightWasOn) { if (touchBacklightActive && !touchBacklightWasOn) {
#if HAS_PWM_BACKLIGHT
graphics::backlightOff();
#else
digitalWrite(PIN_EINK_EN, LOW); digitalWrite(PIN_EINK_EN, LOW);
#endif
} }
touchBacklightActive = false; touchBacklightActive = false;
}; };
@@ -327,6 +336,30 @@ void InputBroker::Init()
#define BUTTON_ACTIVE_PULLUP true #define BUTTON_ACTIVE_PULLUP true
#endif #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 // 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 // If your variant.h has a BUTTON_PIN defined, go ahead and define BUTTON_ACTIVE_LOW and BUTTON_ACTIVE_PULLUP
UserButtonThread = new ButtonThread("UserButton"); UserButtonThread = new ButtonThread("UserButton");
@@ -378,8 +411,9 @@ void InputBroker::Init()
userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE; userConfigNoScreen.triplePress = INPUT_BROKER_GPS_TOGGLE;
UserButtonThread->initButton(userConfigNoScreen); UserButtonThread->initButton(userConfigNoScreen);
} }
#endif #endif // ELECROW_ThinkNode_M8
#endif #endif // BUTTON_PIN
#endif // HAS_BUTTON
#if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER #if (HAS_BUTTON || ARCH_PORTDUINO) && !MESHTASTIC_EXCLUDE_INPUTBROKER
if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) { if (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_COLOR) {
+44 -6
View File
@@ -56,24 +56,50 @@ int32_t RotaryEncoderInterruptBase::runOnce()
if (!pressDetected && buttonPressed) { if (!pressDetected && buttonPressed) {
pressDetected = true; pressDetected = true;
pressStartTime = now; pressStartTime = now;
pressAndTurnFired = false;
} }
if (pressDetected) { 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; uint32_t duration = now - pressStartTime;
if (!buttonPressed) { if (!buttonPressed) {
// released -> if short press, send short, else already sent long // 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; lastPressKeyTime = now;
LOG_DEBUG("Rotary event Press short"); LOG_DEBUG("Rotary event Press short");
e.inputEvent = this->_eventPressed; 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; pressDetected = false;
pressStartTime = 0; pressStartTime = 0;
lastPressLongEventTime = 0; lastPressLongEventTime = 0;
pressAndTurnDelta.store(0, std::memory_order_relaxed);
pressAndTurnFired = false;
this->action = ROTARY_ACTION_NONE; this->action = ROTARY_ACTION_NONE;
} else if (duration >= LONG_PRESS_DURATION && this->_eventPressedLong != INPUT_BROKER_NONE && } else if (!pressAndTurnEnabled() && duration >= LONG_PRESS_DURATION &&
lastPressLongEventTime == 0) { this->_eventPressedLong != INPUT_BROKER_NONE && lastPressLongEventTime == 0) {
// fire single-shot long press // fire single-shot long press; press-and-turn encoders defer this to release
lastPressLongEventTime = now; lastPressLongEventTime = now;
LOG_DEBUG("Rotary event Press long"); LOG_DEBUG("Rotary event Press long");
e.inputEvent = this->_eventPressedLong; e.inputEvent = this->_eventPressedLong;
@@ -87,7 +113,7 @@ int32_t RotaryEncoderInterruptBase::runOnce()
e.inputEvent = this->_eventCcw; e.inputEvent = this->_eventCcw;
} }
if (e.inputEvent != INPUT_BROKER_NONE) { if (e.inputEvent != INPUT_BROKER_NONE || e.kbchar != 0) {
this->notifyObservers(&e); this->notifyObservers(&e);
} }
@@ -95,11 +121,20 @@ int32_t RotaryEncoderInterruptBase::runOnce()
this->action = ROTARY_ACTION_NONE; this->action = ROTARY_ACTION_NONE;
} else if (now - pressStartTime < LONG_PRESS_DURATION) { } else if (now - pressStartTime < LONG_PRESS_DURATION) {
return (20); // keep checking for long/short until time expires 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; return INT32_MAX;
} }
void RotaryEncoderInterruptBase::setPressAndTurnChars(unsigned char cw, unsigned char ccw)
{
this->_pressAndTurnCw = cw;
this->_pressAndTurnCcw = ccw;
}
void RotaryEncoderInterruptBase::intPressHandler() void RotaryEncoderInterruptBase::intPressHandler()
{ {
this->action = ROTARY_ACTION_PRESSED; this->action = ROTARY_ACTION_PRESSED;
@@ -145,7 +180,10 @@ RotaryEncoderInterruptBaseStateType RotaryEncoderInterruptBase::intHandler(bool
if (actualPinRaising && (otherPinLevel == LOW)) { if (actualPinRaising && (otherPinLevel == LOW)) {
if (state == ROTARY_EVENT_CLEARED) { if (state == ROTARY_EVENT_CLEARED) {
newState = ROTARY_EVENT_OCCURRED; 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; this->action = action;
} }
} }
+14 -1
View File
@@ -3,9 +3,9 @@
#include "InputBroker.h" #include "InputBroker.h"
#include "concurrency/OSThread.h" #include "concurrency/OSThread.h"
#include "mesh/NodeDB.h" #include "mesh/NodeDB.h"
#include <atomic>
enum RotaryEncoderInterruptBaseStateType { ROTARY_EVENT_OCCURRED, ROTARY_EVENT_CLEARED }; enum RotaryEncoderInterruptBaseStateType { ROTARY_EVENT_OCCURRED, ROTARY_EVENT_CLEARED };
enum RotaryEncoderInterruptBaseActionType { ROTARY_ACTION_NONE, ROTARY_ACTION_PRESSED, ROTARY_ACTION_CW, ROTARY_ACTION_CCW }; enum RotaryEncoderInterruptBaseActionType { ROTARY_ACTION_NONE, ROTARY_ACTION_PRESSED, ROTARY_ACTION_CW, ROTARY_ACTION_CCW };
class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public concurrency::OSThread class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public concurrency::OSThread
@@ -20,6 +20,10 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
void intAHandler(); void intAHandler();
void intBHandler(); 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: protected:
virtual int32_t runOnce() override; virtual int32_t runOnce() override;
RotaryEncoderInterruptBaseStateType intHandler(bool actualPinRaising, int otherPinLevel, RotaryEncoderInterruptBaseStateType intHandler(bool actualPinRaising, int otherPinLevel,
@@ -31,6 +35,8 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
volatile int rotaryLevelA = LOW; volatile int rotaryLevelA = LOW;
volatile int rotaryLevelB = LOW; volatile int rotaryLevelB = LOW;
volatile RotaryEncoderInterruptBaseActionType action = ROTARY_ACTION_NONE; 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: private:
// pins and events // pins and events
@@ -43,9 +49,16 @@ class RotaryEncoderInterruptBase : public Observable<const InputEvent *>, public
input_broker_event _eventPressedLong = INPUT_BROKER_NONE; input_broker_event _eventPressedLong = INPUT_BROKER_NONE;
const char *_originName; 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 // Long press detection variables
uint32_t pressStartTime = 0; uint32_t pressStartTime = 0;
bool pressDetected = false; bool pressDetected = false;
// Suppresses the press event on release once press-and-turn has fired.
bool pressAndTurnFired = false;
uint32_t lastPressLongEventTime = 0; uint32_t lastPressLongEventTime = 0;
unsigned long lastPressKeyTime = 0; unsigned long lastPressKeyTime = 0;
static const uint32_t LONG_PRESS_DURATION = 300; // ms 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, RotaryEncoderInterruptBase::init(pinA, pinB, pinPress, eventCw, eventCcw, eventPressed, eventPressedLong,
RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB, RotaryEncoderInterruptImpl1::handleIntA, RotaryEncoderInterruptImpl1::handleIntB,
RotaryEncoderInterruptImpl1::handleIntPressed); 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); inputBroker->registerSource(this);
#ifndef HAS_PHYSICAL_KEYBOARD #ifndef HAS_PHYSICAL_KEYBOARD
osk_found = true; osk_found = true;
+9
View File
@@ -1270,6 +1270,15 @@ void NodeDB::installDefaultModuleConfig()
moduleConfig.external_notification.active = true; moduleConfig.external_notification.active = true;
#endif // NANO_G2_ULTRA #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 #ifdef T_LORA_PAGER
moduleConfig.canned_message.updown1_enabled = true; moduleConfig.canned_message.updown1_enabled = true;
moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A; moduleConfig.canned_message.inputbroker_pin_a = ROTARY_A;
+1
View File
@@ -106,6 +106,7 @@ class AccelerometerThread : public concurrency::OSThread
break; break;
#if __has_include(<Adafruit_LIS3DH.h>) #if __has_include(<Adafruit_LIS3DH.h>)
case ScanI2C::DeviceType::LIS3DH: case ScanI2C::DeviceType::LIS3DH:
case ScanI2C::DeviceType::SC7A20:
sensor = new LIS3DHSensor(device); sensor = new LIS3DHSensor(device);
break; break;
#endif #endif
+5 -3
View File
@@ -7,14 +7,16 @@ LIS3DHSensor::LIS3DHSensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::Mot
bool LIS3DHSensor::init() 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); sensor.setRange(LIS3DH_RANGE_2_G);
// Adjust threshold, higher numbers are less sensitive // Adjust threshold, higher numbers are less sensitive
sensor.setClick(config.device.double_tap_as_button_press ? 2 : 1, MOTION_SENSOR_CHECK_INTERVAL_MS); 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; return true;
} }
LOG_DEBUG("LIS3DH init failed"); LOG_DEBUG("%s init failed", deviceType() == ScanI2C::DeviceType::SC7A20 ? "SC7A20" : "LIS3DH");
return false; return false;
} }
+2
View File
@@ -99,6 +99,8 @@
#define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M6 #define HW_VENDOR meshtastic_HardwareModel_THINKNODE_M6
#elif defined(ELECROW_ThinkNode_M4) #elif defined(ELECROW_ThinkNode_M4)
#define HW_VENDOR meshtastic_HardwareModel_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) #elif defined(NANO_G2_ULTRA)
#define HW_VENDOR meshtastic_HardwareModel_NANO_G2_ULTRA #define HW_VENDOR meshtastic_HardwareModel_NANO_G2_ULTRA
#elif defined(CANARYONE) #elif defined(CANARYONE)
@@ -0,0 +1,41 @@
; First prototype eink/nrf52840/sx1262 device
[env:thinknode_m8]
custom_meshtastic_support_level = 1
custom_meshtastic_images = thinknode_m8.svg
custom_meshtastic_tags = Elecrow
custom_meshtastic_hw_model = 130
custom_meshtastic_hw_model_slug = THINKNODE_M8
custom_meshtastic_architecture = nrf52840
custom_meshtastic_display_name = Elecrow ThinkNode M8
custom_meshtastic_actively_supported = true
extends = nrf52840_base
board = ThinkNode-M8
board_check = true
debug_tool = jlink
# add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling.
build_flags =
${nrf52840_base.build_flags}
-I variants/nrf52840/ELECROW-ThinkNode-M8
-D ELECROW_ThinkNode_M8
-D EINK_DISPLAY_MODEL=GxEPD2_154_D67
-D EINK_WIDTH=200
-D EINK_HEIGHT=200
-D USE_EINK_DYNAMICDISPLAY ; Enable Dynamic EInk
-D EINK_LIMIT_FASTREFRESH=20 ; How many consecutive fast-refreshes are permitted //20
-D EINK_LIMIT_RATE_BACKGROUND_SEC=10 ; Minimum interval between BACKGROUND updates //30
-D EINK_LIMIT_RATE_RESPONSIVE_SEC=1 ; Minimum interval between RESPONSIVE updates
-D EINK_BACKGROUND_USES_FAST ; (Optional) Use FAST refresh for both BACKGROUND and RESPONSIVE, until a limit is reached.
build_src_filter =
${nrf52_base.build_src_filter}
+<../variants/nrf52840/ELECROW-ThinkNode-M8>
lib_deps =
${nrf52840_base.lib_deps}
# renovate: datasource=git-refs depName=meshtastic-GxEPD2 packageName=https://github.com/meshtastic/GxEPD2 gitBranch=master
https://github.com/meshtastic/GxEPD2/archive/c7eb4c3c167cf396ef4f541cc5d4c6aa42f3c46b.zip
# renovate: datasource=custom.pio depName=nRF52_PWM packageName=khoih-prog/library/nRF52_PWM
khoih-prog/nRF52_PWM@1.0.1
# renovate: datasource=custom.pio depName=SensorLib packageName=lewisxhe/library/SensorLib
lewisxhe/SensorLib@0.3.4
@@ -0,0 +1,75 @@
/*
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
Copyright (c) 2016 Sandeep Mistry All right reserved.
Copyright (c) 2018, Adafruit Industries (adafruit.com)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "variant.h"
#include "main.h"
#include "nrf.h"
#include "wiring_constants.h"
#include "wiring_digital.h"
#include <Wire.h>
const uint32_t g_ADigitalPinMap[] = {
// P0 - pins 0 and 1 are hardwired for xtal and should never be enabled
0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31,
// P1
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
void initVariant()
{
pinMode(I2C_EN, OUTPUT);
digitalWrite(I2C_EN, HIGH);
pinMode(VCC_ELNK_EN, OUTPUT);
digitalWrite(VCC_ELNK_EN, HIGH);
pinMode(PIN_GPS_EN, OUTPUT);
digitalWrite(PIN_GPS_EN, HIGH);
pinMode(ADC_EN, OUTPUT);
digitalWrite(ADC_EN, HIGH);
Wire.setPins(PIN_WIRE_SDA, PIN_WIRE_SCL);
}
void variant_shutdown()
{
auto dispdev = screen->getDisplayDevice();
dispdev->resetDisplay();
screen->forceDisplay();
delay(500);
digitalWrite(I2C_EN, LOW);
digitalWrite(VCC_ELNK_EN, LOW);
digitalWrite(PIN_GPS_EN, LOW);
digitalWrite(ADC_EN, LOW);
for (int pin = 0; pin < 48; pin++) {
if (pin == I2C_EN || pin == VCC_ELNK_EN || pin == PIN_GPS_EN || pin == ADC_EN || pin == PIN_BUTTON1 ||
pin == SX1262_SPI_NSS_PIN || pin == SX1262_SPI_SCK_PIN || pin == SX1262_SPI_MOSI_PIN || pin == SX1262_SPI_MISO_PIN ||
pin == SX1262_IRQ_PIN || pin == SX1262_NRESET_PIN || pin == SX126X_BUSY) {
continue;
}
pinMode(pin, OUTPUT);
digitalWrite(pin, LOW);
if (pin >= 32) {
NRF_P1->DIRCLR = (1 << (pin - 32));
} else {
NRF_GPIO->DIRCLR = (1 << pin);
}
}
nrf_gpio_cfg_input(PIN_BUTTON1, NRF_GPIO_PIN_PULLUP); // Configure the pin to be woken up as an input
nrf_gpio_pin_sense_t sense1 = NRF_GPIO_PIN_SENSE_LOW;
nrf_gpio_cfg_sense_set(PIN_BUTTON1, sense1);
}
@@ -0,0 +1,182 @@
/*
Copyright (c) 2014-2015 Arduino LLC. All right reserved.
Copyright (c) 2016 Sandeep Mistry All right reserved.
Copyright (c) 2018, Adafruit Industries (adafruit.com)
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef _VARIANT_ELECROW_EINK_V1_0_
#define _VARIANT_ELECROW_EINK_V1_0_
/** Master clock frequency */
#define VARIANT_MCK (64000000ul)
#define USE_LFXO // Board uses 32khz crystal for LF
/*----------------------------------------------------------------------------
* Headers
*----------------------------------------------------------------------------*/
#include "WVariant.h"
#ifdef __cplusplus
extern "C" {
#endif // __cplusplus
// Number of pins defined in PinDescription array
#define PINS_COUNT (48)
#define NUM_DIGITAL_PINS (48)
#define NUM_ANALOG_INPUTS (1)
#define NUM_ANALOG_OUTPUTS (0)
#define PIN_NFC1 (9)
#define PIN_NFC2 (10)
#define CANNED_MESSAGE_MODULE_ENABLE 1
/*Power*/
#define I2C_EN (0 + 13)
#define VCC_ELNK_EN (32 + 10)
#define PIN_GPS_EN (0 + 16) // active high
#define ADC_EN (32 + 8)
/*Buttons*/
#define HAS_BUTTON 1
#define PIN_BUTTON_E (0 + 12)
#define PIN_BUTTON_EC04_A (0 + 8)
#define PIN_BUTTON_EC04_B (32 + 9)
#define PIN_BUTTON_EC04 (0 + 6)
#define PIN_BUTTON1 PIN_BUTTON_E
// Press and turn the encoder to dim the frontlight
#define INPUTDRIVER_PRESS_AND_TURN_CW INPUT_BROKER_MSG_BRIGHTNESS_UP
#define INPUTDRIVER_PRESS_AND_TURN_CCW INPUT_BROKER_MSG_BRIGHTNESS_DOWN
/*LED*/
#define PIN_LED1 -1
#define LED_STATE_ON HIGH // State when LED is lit
#define LED_BLUE PIN_LED1
/*BUZZER*/
#define PIN_BUZZER (32 + 1)
/*USB_CHECK*/
// Reference only: sits on a divider from VBUS, too near the input threshold to read
// digitally. USB presence comes from USBREGSTATUS instead.
#define USB_VBUS (32 + 3)
/*CHARGE_CHECK*/
// Reference only: link resistors and pull-ups unpopulated on V0.3, so these never reach the MCU.
#define CHRG (32 + 5)
#define DONE (32 + 6)
/*Wire Interfaces*/
// Found by the I2C scan: PCF8563 RTC at 0x51, SC7A20 accelerometer at 0x19.
#define WIRE_INTERFACES_COUNT 1
#define PIN_WIRE_SDA (0 + 26)
#define PIN_WIRE_SCL (0 + 27)
/*GPS*/
// ATGM336H-5NR32. Probes as GNSS_MODEL_MTK; both branches speak CASIC $PCAS.
#define HAS_GPS 1
#define GPS_BAUDRATE 9600
#define PIN_GPS_RESET (0 + 17)
#define PIN_GPS_STANDBY (0 + 15) // An output to wake GPS, low means allow sleep, high means force wake
#define PIN_GPS_PPS (0 + 14) // 1PPS output, configured as an input by GPS.cpp
#define PIN_SERIAL1_RX (32 + 2)
#define PIN_SERIAL1_TX (32 + 4)
#define GPS_TX_PIN PIN_SERIAL1_TX
#define GPS_RX_PIN PIN_SERIAL1_RX
#define GPS_THREAD_INTERVAL 50
/*FLASH*/
#define PIN_QSPI_CS (32 + 15)
#define PIN_QSPI_SCK (32 + 14)
#define PIN_QSPI_IO0 (32 + 12) // MOSI if using two bit interface
#define PIN_QSPI_IO1 (32 + 13) // MISO if using two bit interface
#define PIN_QSPI_IO2 (0 + 7) // WP if using two bit interface (i.e. not used)
#define PIN_QSPI_IO3 (0 + 5) // HOLD if using two bit interface (i.e. not used)
#define EXTERNAL_FLASH_DEVICES MX25R1635F
#define EXTERNAL_FLASH_USE_QSPI
/*SPI*/
#define SPI_INTERFACES_COUNT 2
#define PIN_SPI_NSS (0 + 21)
#define PIN_SPI_SCK (0 + 19)
#define PIN_SPI_MOSI (0 + 20)
#define PIN_SPI_MISO (0 + 22)
#define PIN_SPI1_NSS (0 + 30)
#define PIN_SPI1_SCK (0 + 31)
#define PIN_SPI1_MOSI (0 + 29)
#define PIN_SPI1_MISO -1
/*EINK*/
#define MESHTASTIC_USE_EINK_UI 1
#define USE_EINK 1
#define PIN_EINK_CS PIN_SPI1_NSS
#define PIN_EINK_SCLK PIN_SPI1_SCK
#define PIN_EINK_MOSI PIN_SPI1_MOSI
#define PIN_EINK_BUSY (0 + 3)
#define PIN_EINK_DC (0 + 28)
#define PIN_EINK_RES (0 + 2)
// Panel supply is VCC_ELNK_EN; P1.11 is the dimmable frontlight. No PIN_EINK_EN on this board.
#define PIN_PWM_BACKLIGHT (32 + 11)
#define PWM_BACKLIGHT_DEFAULT 128
#define PWM_BACKLIGHT_MIN 8
#define PWM_BACKLIGHT_MAX 248
#define PWM_BACKLIGHT_STEP 20
/*Lora radio*/
#define USE_SX1262
// PE4259 /CTRL pad, held high for single-pin mode; the SX1262 selects the path from DIO2.
#define SX126X_ANT_SW (0 + 23)
#define SX126X_RESET (0 + 24) // RST
#define SX126X_DIO1 (0 + 25) // IRQ
#define SX126X_DIO2 (32 + 0) // BUSY
#define SX126X_SCK PIN_SPI_SCK
#define SX126X_MISO PIN_SPI_MISO
#define SX126X_MOSI PIN_SPI_MOSI
#define SX126X_CS PIN_SPI_NSS
#define SX1262_IRQ_PIN SX126X_DIO1
#define SX1262_NRESET_PIN SX126X_RESET
#define SX126X_BUSY SX126X_DIO2
#define SX1262_SPI_NSS_PIN SX126X_CS
#define SX1262_SPI_SCK_PIN SX126X_SCK
#define SX1262_SPI_MOSI_PIN SX126X_MOSI
#define SX1262_SPI_MISO_PIN SX126X_MISO
#define SX126X_DIO2_AS_RF_SWITCH
#define SX126X_DIO3_TCXO_VOLTAGE 3.3
/*Serial*/
#define SERIAL_PRINT_PORT 0 // USB CDC is the StreamAPI transport; no secondary UART
/*RTC*/
#define PCF8563_RTC 0x51
/*Battert*/
#define BATTERY_PIN (0 + 4)
#define ADC_V (0 + 4)
#define BATTERY_SENSE_RESOLUTION_BITS 12
#define BATTERY_SENSE_RESOLUTION 4096.0
#define BATTERY_SENSE_SAMPLES 100
#undef AREF_VOLTAGE
#define AREF_VOLTAGE 2.4
#define VBAT_AR_INTERNAL AR_INTERNAL_2_4
#define ADC_MULTIPLIER (1.75)
#ifdef __cplusplus
}
#endif
#endif