From 597f6767b5e0c37758444071477d70c74657eecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Thu, 30 Jul 2026 17:59:35 +0200 Subject: [PATCH] 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 * 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 --- boards/ThinkNode-M8.json | 53 +++++ src/Power.cpp | 2 +- src/detect/ScanI2C.cpp | 6 +- src/detect/ScanI2C.h | 1 + src/detect/ScanI2CTwoWire.cpp | 4 + src/graphics/Backlight.cpp | 71 +++++++ src/graphics/Backlight.h | 46 +++++ src/graphics/EInkDisplay2.cpp | 6 +- src/graphics/Screen.cpp | 37 +++- src/graphics/draw/MenuHandler.cpp | 8 +- src/input/ButtonThread.cpp | 8 +- src/input/InputBroker.cpp | 46 ++++- src/input/RotaryEncoderInterruptBase.cpp | 50 ++++- src/input/RotaryEncoderInterruptBase.h | 15 +- src/input/RotaryEncoderInterruptImpl1.cpp | 3 + src/mesh/NodeDB.cpp | 9 + src/motion/AccelerometerThread.h | 1 + src/motion/LIS3DHSensor.cpp | 8 +- src/platform/nrf52/architecture.h | 2 + .../ELECROW-ThinkNode-M8/platformio.ini | 41 ++++ .../nrf52840/ELECROW-ThinkNode-M8/variant.cpp | 75 ++++++++ .../nrf52840/ELECROW-ThinkNode-M8/variant.h | 182 ++++++++++++++++++ 22 files changed, 641 insertions(+), 33 deletions(-) create mode 100644 boards/ThinkNode-M8.json create mode 100644 src/graphics/Backlight.cpp create mode 100644 src/graphics/Backlight.h create mode 100644 variants/nrf52840/ELECROW-ThinkNode-M8/platformio.ini create mode 100644 variants/nrf52840/ELECROW-ThinkNode-M8/variant.cpp create mode 100644 variants/nrf52840/ELECROW-ThinkNode-M8/variant.h diff --git a/boards/ThinkNode-M8.json b/boards/ThinkNode-M8.json new file mode 100644 index 000000000..acd7f89c0 --- /dev/null +++ b/boards/ThinkNode-M8.json @@ -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" +} diff --git a/src/Power.cpp b/src/Power.cpp index 350208dcd..a886082c6 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -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; diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index a147194a7..a919bd105 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -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 diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 3dcbe3c6a..87965245a 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -46,6 +46,7 @@ class ScanI2C QMA6100P, MPU6050, LIS3DH, + SC7A20, // LIS3DH register map, different WHO_AM_I BMA423, BQ24295, LSM6DS3, diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index c543aac85..be326f4db 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -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); diff --git a/src/graphics/Backlight.cpp b/src/graphics/Backlight.cpp new file mode 100644 index 000000000..111af66d6 --- /dev/null +++ b/src/graphics/Backlight.cpp @@ -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 diff --git a/src/graphics/Backlight.h b/src/graphics/Backlight.h new file mode 100644 index 000000000..449fd5736 --- /dev/null +++ b/src/graphics/Backlight.h @@ -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 diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 7c8b6ab8e..a44e8ef4b 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -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(*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); diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 7892cbdda..9c1688db4 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -61,6 +61,7 @@ along with this program. If not, see . #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(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(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; diff --git a/src/graphics/draw/MenuHandler.cpp b/src/graphics/draw/MenuHandler.cpp index 6625d4303..d4ff187ec 100644 --- a/src/graphics/draw/MenuHandler.cpp +++ b/src/graphics/draw/MenuHandler.cpp @@ -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); diff --git a/src/input/ButtonThread.cpp b/src/input/ButtonThread.cpp index 481b245da..bf0883691 100644 --- a/src/input/ButtonThread.cpp +++ b/src/input/ButtonThread.cpp @@ -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; diff --git a/src/input/InputBroker.cpp b/src/input/InputBroker.cpp index d6c8fe832..4f09e3224 100644 --- a/src/input/InputBroker.cpp +++ b/src/input/InputBroker.cpp @@ -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) { diff --git a/src/input/RotaryEncoderInterruptBase.cpp b/src/input/RotaryEncoderInterruptBase.cpp index 80ac08175..c177403bf 100644 --- a/src/input/RotaryEncoderInterruptBase.cpp +++ b/src/input/RotaryEncoderInterruptBase.cpp @@ -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; } } diff --git a/src/input/RotaryEncoderInterruptBase.h b/src/input/RotaryEncoderInterruptBase.h index 4f9757609..c6bd79e5c 100644 --- a/src/input/RotaryEncoderInterruptBase.h +++ b/src/input/RotaryEncoderInterruptBase.h @@ -3,9 +3,9 @@ #include "InputBroker.h" #include "concurrency/OSThread.h" #include "mesh/NodeDB.h" +#include 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, public concurrency::OSThread @@ -20,6 +20,10 @@ class RotaryEncoderInterruptBase : public Observable, 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, 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 pressAndTurnDelta{0}; private: // pins and events @@ -43,9 +49,16 @@ class RotaryEncoderInterruptBase : public Observable, 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 diff --git a/src/input/RotaryEncoderInterruptImpl1.cpp b/src/input/RotaryEncoderInterruptImpl1.cpp index 1da2ea008..4c1685d5a 100644 --- a/src/input/RotaryEncoderInterruptImpl1.cpp +++ b/src/input/RotaryEncoderInterruptImpl1.cpp @@ -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; diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 3ba46ccf2..f1a32b4b9 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -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; diff --git a/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h index 6847b9aa4..571767715 100755 --- a/src/motion/AccelerometerThread.h +++ b/src/motion/AccelerometerThread.h @@ -106,6 +106,7 @@ class AccelerometerThread : public concurrency::OSThread break; #if __has_include() case ScanI2C::DeviceType::LIS3DH: + case ScanI2C::DeviceType::SC7A20: sensor = new LIS3DHSensor(device); break; #endif diff --git a/src/motion/LIS3DHSensor.cpp b/src/motion/LIS3DHSensor.cpp index 903cc92f7..a29ff2b85 100755 --- a/src/motion/LIS3DHSensor.cpp +++ b/src/motion/LIS3DHSensor.cpp @@ -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; } diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index 600fb8192..0193eeb5c 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -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) diff --git a/variants/nrf52840/ELECROW-ThinkNode-M8/platformio.ini b/variants/nrf52840/ELECROW-ThinkNode-M8/platformio.ini new file mode 100644 index 000000000..6b845b2ef --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M8/platformio.ini @@ -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 + + diff --git a/variants/nrf52840/ELECROW-ThinkNode-M8/variant.cpp b/variants/nrf52840/ELECROW-ThinkNode-M8/variant.cpp new file mode 100644 index 000000000..99d6b8a91 --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M8/variant.cpp @@ -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 + +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); +} diff --git a/variants/nrf52840/ELECROW-ThinkNode-M8/variant.h b/variants/nrf52840/ELECROW-ThinkNode-M8/variant.h new file mode 100644 index 000000000..bb9cd92dd --- /dev/null +++ b/variants/nrf52840/ELECROW-ThinkNode-M8/variant.h @@ -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 \ No newline at end of file