From c308d0aca4b8de4034cb19b591d0d2c3f13d950b Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Mon, 17 Aug 2026 17:28:33 +0000 Subject: [PATCH] feat: Support Elecrow ThinkNode M9 (#10908) * thinknode-m9 variant * move lora to SPI1 device * enable SDcard * use HSPI * BaseUI tft -> HSPI * buzzer, webdav lib * fix build issues * M9 default to MUI, no BT, short ringtone * add keyboard long-press config * update variant * add ThingNode-M9 GPS string * GPS 115200 baud * Basic BaseUI support * Fixup power detection * Compass and KB fixes for M9 * add timed Lock::lock() * add SD card * point device-ui to thinknode m9 draft branch * trunk fmt * fix FusionCompass * Fix t-deck-tft linker arg list overflow in CI * SDcard/lora fix: SPI1 must not be declared twice in arduino 3.x -> reuse SPI1 defined in FSCommon.cpp * update battery parameters * reinit SD card when updating; fix PSRAM size * update lib versions * fix wakeup on key press (KB_INT) * fix default nag_timeout for TFT/MUI devices with buzzer * increase PSRAM and SD freq * trunk fmt * update lovyanGFX 1.2.26 * update device-ui commit reference * fix screen definition * remove DONE; maybe a keyword or other used identifier * fixed CI error nag_timeout * fix prepareSleep initialization * trunk fmt * reduce SD SPI frequency * update device-ui * fix SDcard issue * stage * fix device-ui commit reference * fix device-ui commit * update device-ui commit (fixed keyboard lag) * fix QMI8658 * trunk fmt * update .ini meta information, align SD freq * fix device-ui reference to target (ready to merge) * device-ui for all other targets * make the rabbit happy * trunk fmt * fixed lock screen * fix compile error * SPI lock timeout * apply device-ui fix * revert bad RadioLib commit hash in platformio.ini Co-authored-by: mverch67 <71137295+mverch67@users.noreply.github.com> * fix wrong commit hash change * fix fix commit fix * I love changing random numbers in random files --------- Co-authored-by: Jonathan Bennett Co-authored-by: Ben Meadors Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- platformio.ini | 2 +- src/PowerFSM.cpp | 24 +-- src/concurrency/Lock.cpp | 10 ++ src/concurrency/Lock.h | 5 + src/configuration.h | 2 + src/detect/ScanI2C.cpp | 14 +- src/detect/ScanI2C.h | 2 + src/detect/ScanI2CTwoWire.cpp | 15 ++ src/gps/GPS.cpp | 8 +- src/graphics/draw/UIRenderer.cpp | 23 +++ src/graphics/tftSetup.cpp | 29 +++- src/input/STC8HKeyboard.cpp | 155 ++++++++++++++++++ src/input/STC8HKeyboard.h | 74 +++++++++ src/input/cardKbI2cImpl.cpp | 4 + src/input/kbI2cBase.cpp | 114 +++++++++++++ src/main.cpp | 4 + src/mesh/NodeDB.cpp | 14 +- src/motion/AccelerometerThread.h | 6 + src/motion/MagnetometerThread.h | 6 + src/motion/MotionSensor.cpp | 32 ++++ src/motion/MotionSensor.h | 7 +- src/motion/QMC6309Sensor.cpp | 152 +++++++++++++++++ src/motion/QMC6309Sensor.h | 40 +++++ src/motion/QMI8658Sensor.cpp | 88 ++++++++++ src/motion/QMI8658Sensor.h | 25 +++ src/sleep.cpp | 6 +- .../ELECROW-ThinkNode-M9/pins_arduino.h | 20 +++ .../ELECROW-ThinkNode-M9/platformio.ini | 99 +++++++++++ .../esp32s3/ELECROW-ThinkNode-M9/rfswitch.h | 11 ++ .../esp32s3/ELECROW-ThinkNode-M9/variant.cpp | 34 ++++ .../esp32s3/ELECROW-ThinkNode-M9/variant.h | 107 ++++++++++++ variants/esp32s3/t-deck/platformio.ini | 4 + 32 files changed, 1098 insertions(+), 38 deletions(-) create mode 100644 src/input/STC8HKeyboard.cpp create mode 100644 src/input/STC8HKeyboard.h create mode 100644 src/motion/QMC6309Sensor.cpp create mode 100644 src/motion/QMC6309Sensor.h create mode 100644 src/motion/QMI8658Sensor.cpp create mode 100644 src/motion/QMI8658Sensor.h create mode 100644 variants/esp32s3/ELECROW-ThinkNode-M9/pins_arduino.h create mode 100644 variants/esp32s3/ELECROW-ThinkNode-M9/platformio.ini create mode 100644 variants/esp32s3/ELECROW-ThinkNode-M9/rfswitch.h create mode 100644 variants/esp32s3/ELECROW-ThinkNode-M9/variant.cpp create mode 100644 variants/esp32s3/ELECROW-ThinkNode-M9/variant.h diff --git a/platformio.ini b/platformio.ini index 6e0d20d463..a1dd0d310c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -137,7 +137,7 @@ lib_deps = [device-ui_base] lib_deps = # renovate: datasource=git-refs depName=meshtastic/device-ui packageName=https://github.com/meshtastic/device-ui gitBranch=master - https://github.com/meshtastic/device-ui/archive/e1de01e0b3c4a6b149c00e95d59cfb0cca7ad49e.zip + https://github.com/meshtastic/device-ui/archive/adfbd3811a53b6aed0649c8d8f078118c042a407.zip custom_sdkconfig = # CONFIG_MBEDTLS_INTERNAL_MEM_ALLOC is not set CONFIG_MBEDTLS_EXTERNAL_MEM_ALLOC=y diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index 268cb8211d..2ef5b2ea1c 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -173,23 +173,25 @@ static void lsIdle() powerFSM.trigger(EVENT_SERIAL_CONNECTED); break; - default: - // We woke for some other reason (button press, device IRQ interrupt) - -#ifdef BUTTON_PIN - bool pressed = !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); -#else + case ESP_SLEEP_WAKEUP_GPIO: { bool pressed = false; +#if defined(BUTTON_PIN) + pressed = !digitalRead(config.device.button_gpio ? config.device.button_gpio : BUTTON_PIN); +#elif defined(KB_INT) + // keyboard press (probably) triggered GPIO interrupt + pressed = true; #endif - if (pressed) { // If we woke because of press, instead generate a PRESS event. + if (pressed) { powerFSM.trigger(EVENT_PRESS); - } else { - // Otherwise let the NB state handle the IRQ (and that state will handle stuff like IRQs etc) - // we lie and say "wake timer" because the interrupt will be handled by the regular IRQ code - powerFSM.trigger(EVENT_WAKE_TIMER); } break; } + default: + // Otherwise let the NB state handle the IRQ (and that state will handle stuff like IRQs etc) + // we lie and say "wake timer" because the interrupt will be handled by the regular IRQ code + powerFSM.trigger(EVENT_WAKE_TIMER); + break; + } } else { // Someone says we can't sleep now, so just save some power by sleeping the CPU for 100ms or so delay(100); diff --git a/src/concurrency/Lock.cpp b/src/concurrency/Lock.cpp index 4596e0edfd..9068cee433 100644 --- a/src/concurrency/Lock.cpp +++ b/src/concurrency/Lock.cpp @@ -26,6 +26,11 @@ void Lock::lock() } } +bool Lock::lock(uint32_t timeout) +{ + return xSemaphoreTake(handle, pdMS_TO_TICKS(timeout)) == pdTRUE; +} + void Lock::unlock() { if (xSemaphoreGive(handle) == false) { @@ -39,6 +44,11 @@ Lock::~Lock() {} void Lock::lock() {} +bool Lock::lock(uint32_t) +{ + return true; +} + void Lock::unlock() {} #endif diff --git a/src/concurrency/Lock.h b/src/concurrency/Lock.h index a512481178..342747ae7f 100644 --- a/src/concurrency/Lock.h +++ b/src/concurrency/Lock.h @@ -22,6 +22,11 @@ class Lock // Must not be called from an ISR. void lock(); + /// Locks the lock with timeout. + // + // Must not be called from an ISR. + bool lock(uint32_t timeout); + // Unlocks the lock. // // Must not be called from an ISR. diff --git a/src/configuration.h b/src/configuration.h index 1a6550366e..03cb12bf36 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -253,6 +253,7 @@ along with this program. If not, see . #define BBQ10_KB_ADDR 0x1F #define MPR121_KB_ADDR 0x5A #define TCA8418_KB_ADDR 0x34 +#define TSTC8_KB_ADDR 0x6C // STC8H companion-MCU keypad on the ThinkNode-M9 // ----------------------------------------------------------------------------- // SENSOR @@ -270,6 +271,7 @@ along with this program. If not, see . #define QMC5883L_ADDR 0x0D #define HMC5883L_ADDR 0x1E #define MMC5983MA_ADDR 0x30 +#define QMC6309_ADDR 0x7C #define SHTC3_ADDR 0x70 #define LPS22HB_ADDR 0x5C #define LPS22HB_ADDR_ALT 0x5D diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index eb07101010..eff44c114f 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -31,21 +31,21 @@ ScanI2C::FoundDevice ScanI2C::firstRTC() const ScanI2C::FoundDevice ScanI2C::firstKeyboard() const { - ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, BBQ10KB, RAK14004, MPR121KB, TCA8418KB}; - return firstOfOrNONE(6, types); + ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, BBQ10KB, RAK14004, MPR121KB, TCA8418KB, STC8HKB}; + return firstOfOrNONE(7, types); } ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const { - ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, SC7A20, BMA423, LSM6DS3, BMX160, STK8BAXX, - ICM20948, BMM150, BMI270, ICM42607P, ISM330DHCX, QMA6100P}; - return firstOfOrNONE(13, types); + ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, SC7A20, BMA423, LSM6DS3, BMX160, STK8BAXX, + ICM20948, BMM150, BMI270, ICM42607P, ISM330DHCX, QMA6100P, QMI8658}; + return firstOfOrNONE(14, types); } ScanI2C::FoundDevice ScanI2C::firstMagnetometer() const { - ScanI2C::DeviceType types[] = {MMC5983MA, IIS2MDCTR}; - return firstOfOrNONE(2, types); + ScanI2C::DeviceType types[] = {MMC5983MA, IIS2MDCTR, QMC6309}; + return firstOfOrNONE(3, types); } ScanI2C::FoundDevice ScanI2C::firstAQI() const diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 6a97370dcb..c36434ae78 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -42,6 +42,7 @@ class ScanI2C QMC5883L, HMC5883L, MMC5983MA, + QMC6309, PMSA003I, QMA6100P, MPU6050, @@ -105,6 +106,7 @@ class ScanI2C IIS2MDCTR, ISM330DHCX, SPA06, + STC8HKB, // STC8H companion-MCU keypad (ThinkNode-M9) DS248X, HM330X } DeviceType; diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index f0f4671f59..9085763d5c 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -444,6 +444,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) type = BBQ10KB; logFoundDevice("BB Q10", (uint8_t)addr.address); break; + SCAN_SIMPLE_CASE(TSTC8_KB_ADDR, STC8HKB, "STC8H KB", (uint8_t)addr.address); SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "ST7567", (uint8_t)addr.address); #ifdef HAS_NCP5623 SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623", (uint8_t)addr.address); @@ -1093,6 +1094,20 @@ void ScanI2CTwoWire::scanPort(I2CPort port, uint8_t *address, uint8_t asize) foundDevices[addr] = type; } } + + // The QMC6309 magnetometer sits at 0x7C, above the general scan ceiling (the loop above stops at 0x77 to + // avoid the reserved 0x78-0x7F block). Probe it explicitly. Gated on the SensorLib driver being present so + // only boards that can actually drive the chip poke this reserved address. +#if __has_include() + addr.address = QMC6309_ADDR; + i2cBus->beginTransmission(addr.address); + if (i2cBus->endTransmission() == 0 && + getRegisterValue(ScanI2CTwoWire::RegisterLocation(addr, 0x00), 1) == 0x90 /* QMC6309 chip id */) { + deviceAddresses[QMC6309] = addr; + foundDevices[addr] = QMC6309; + logFoundDevice("QMC6309", (uint8_t)addr.address); + } +#endif } void ScanI2CTwoWire::scanPort(I2CPort port) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 69000f2fef..73d1d03551 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -1686,7 +1686,7 @@ GnssModel_t GPS::probe(int serialSpeed) {"AG3335", "$PAIR021,AG3335", GNSS_MODEL_AG3335}, {"AG3352", "$PAIR021,AG3352", GNSS_MODEL_AG3352}, {"RYS3520", "$PAIR021,REYAX_RYS3520_V2", GNSS_MODEL_AG3352}, - {"UC6580", "UC6580", GNSS_MODEL_UC6580}, + {"UC6580", "UC6580", GNSS_MODEL_UC6580} // as L76K is sort of a last ditch effort, we won't attempt to detect it by startup messages for now. /*{"L76K", "SW=URANUS", GNSS_MODEL_MTK}*/}; GnssModel_t detectedDriver = getProbeResponse(500, passive_detect, serialSpeed); @@ -1713,8 +1713,10 @@ GnssModel_t GPS::probe(int serialSpeed) case 1: { // Unicore UFirebirdII Series: UC6580, UM620, UM621, UM670A, UM680A, or UM681A,or CM121 - std::vector unicore = { - {"UC6580", "UC6580", GNSS_MODEL_UC6580}, {"UM600", "UM600", GNSS_MODEL_UC6580}, {"CM121", "CM121", GNSS_MODEL_CM121}}; + std::vector unicore = {{"UC6580", "UC6580", GNSS_MODEL_UC6580}, + {"UM600", "UM600", GNSS_MODEL_UC6580}, + {"CM121", "CM121", GNSS_MODEL_CM121}, + {"CC1167Q", "CC1167Q", GNSS_MODEL_CM121}}; PROBE_FAMILY("Unicore Family", "$PDTINFO", unicore, 500); currentDelay = 20; currentStep = 2; diff --git a/src/graphics/draw/UIRenderer.cpp b/src/graphics/draw/UIRenderer.cpp index a81942abab..fad540cad7 100644 --- a/src/graphics/draw/UIRenderer.cpp +++ b/src/graphics/draw/UIRenderer.cpp @@ -23,6 +23,9 @@ #include "graphics/images.h" #include "main.h" #include "target_specific.h" +#ifdef COMPASS_SENSOR_DEBUG +#include "motion/MotionSensor.h" +#endif #include #include #include @@ -1776,6 +1779,26 @@ void UIRenderer::drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayU const int *textPos = getTextPositions(display); const bool compactPanel = graphics::isCompactPanel(display); +#ifdef COMPASS_SENSOR_DEBUG + // Optional raw IMU accel + magnetometer x/y/z readout for on-device axis/sign tuning. + { + char dbg[40]; + float sx = 0, sy = 0, sz = 0; + uint32_t age = 0; + if (MotionSensor::getLatestCompassAccelSample(sx, sy, sz, age)) + snprintf(dbg, sizeof(dbg), "A %.2f %.2f %.2f", sx, sy, sz); + else + snprintf(dbg, sizeof(dbg), "A ---"); + display->drawString(x, textPos[line++], dbg); + + if (MotionSensor::getLatestCompassMagSample(sx, sy, sz, age)) + snprintf(dbg, sizeof(dbg), "M %.2f %.2f %.2f", sx, sy, sz); + else + snprintf(dbg, sizeof(dbg), "M ---"); + display->drawString(x, textPos[line++], dbg); + } +#endif + // === First Row: My Location === #if HAS_GPS bool origBold = config.display.heading_bold; diff --git a/src/graphics/tftSetup.cpp b/src/graphics/tftSetup.cpp index cfb23443e4..460b2f86e8 100644 --- a/src/graphics/tftSetup.cpp +++ b/src/graphics/tftSetup.cpp @@ -235,10 +235,8 @@ DeviceScreen *deviceScreen = nullptr; #ifdef ARCH_ESP32 // Get notified when the system is entering light sleep -CallbackObserver tftSleepObserver = - CallbackObserver(deviceScreen, &DeviceScreen::prepareSleep); -CallbackObserver endSleepObserver = - CallbackObserver(deviceScreen, &DeviceScreen::wakeUp); +static CallbackObserver *tftSleepObserver = nullptr; +static CallbackObserver *endSleepObserver = nullptr; #endif /** @@ -277,6 +275,19 @@ class ReentrantSpiLock : public ISpiLock depth = 1; } + bool lock(uint32_t timeout) override + { + ThreadId self = currentThread(); + if (depth && owner == self) { + depth++; + return true; + } + bool result = spiLock->lock(timeout); + owner = self; + depth = 1; + return result; + } + void unlock(void) override { if (--depth == 0) { @@ -413,8 +424,14 @@ void tftSetup(void) if (deviceScreen) { #ifdef ARCH_ESP32 - tftSleepObserver.observe(¬ifyLightSleep); - endSleepObserver.observe(¬ifyLightSleepEnd); + if (!tftSleepObserver) { + tftSleepObserver = new CallbackObserver(deviceScreen, &DeviceScreen::prepareSleep); + } + if (!endSleepObserver) { + endSleepObserver = new CallbackObserver(deviceScreen, &DeviceScreen::wakeUp); + } + tftSleepObserver->observe(¬ifyLightSleep); + endSleepObserver->observe(¬ifyLightSleepEnd); xTaskCreatePinnedToCore(tft_task_handler, "tft", TFT_TASK_STACK_SIZE, NULL, 1, NULL, 0); #elif defined(ARCH_PORTDUINO) std::thread *tft_task = new std::thread([] { tft_task_handler(); }); diff --git a/src/input/STC8HKeyboard.cpp b/src/input/STC8HKeyboard.cpp new file mode 100644 index 0000000000..134153593a --- /dev/null +++ b/src/input/STC8HKeyboard.cpp @@ -0,0 +1,155 @@ +#include "STC8HKeyboard.h" + +#if defined(ELECROW_ThinkNode_M9) +#include "cardKbI2cImpl.h" + +#include "configuration.h" + +// --------------------------------------------------------------------------- +// STC8H companion-MCU keypad driver (ThinkNode-M9). +// +// The original STC8HKeyboard.cpp was lost from the reference source tree, so +// this was recovered from the linked reference firmware.elf (the .o was an LTO +// object with no machine code; the final ELF had the real inlined bodies). +// +// How the hardware works: +// - The STC8H raises KB_INT (rising edge, idle-low) when a key is pressed. The ISR +// latches key_event; is_key_event() just returns that flag. +// - The pressed key code is read over I2C from register 0x05. +// - is_key_state() polls KB_INT directly to keep the backlight lit while a +// key is held. +// - Battery voltage lives in registers 0x01..0x04, little-endian. +// - Sleep is requested by writing 0x01 to the STATE register (0x06). +// - The keypad backlight (KB_LED) and torch (PIN_LED) are plain host GPIOs, +// not I2C commands. +// --------------------------------------------------------------------------- + +STC8HKeyboard Stc8HKeyBoard; + +// ISR latched on each KB_INT rising edge (a key was pressed). +static void has_key_event() +{ + Stc8HKeyBoard.key_event = true; + if (cardKbI2cImpl) { + cardKbI2cImpl->setIntervalFromNow(0); + // runASAP = true; + BaseType_t higherWake = 0; + concurrency::mainDelay.interruptFromISR(&higherWake); + } +} + +void STC8HKeyboard::writeRegister(uint8_t reg, uint8_t val) +{ + _pWire->beginTransmission(_I2C_addr); + _pWire->write(reg); + _pWire->write(val); + _pWire->endTransmission(); +} + +uint8_t STC8HKeyboard::readRegister(uint8_t reg) +{ + _pWire->beginTransmission(_I2C_addr); + _pWire->write(reg); + if (_pWire->endTransmission(false) != 0) + return 0xFF; + if (_pWire->requestFrom(_I2C_addr, (uint8_t)1) != 1) + return 0xFF; + return _pWire->read(); +} + +void STC8HKeyboard::begin(uint8_t addr, TwoWire *wire) +{ + LOG_DEBUG("STC8HKeyboard::begin() addr=0x%02x", addr); + _I2C_addr = addr; + _pWire = wire; + pinMode(KB_INT, INPUT); +#ifdef KB_LED + pinMode(KB_LED, OUTPUT); +#endif +#ifdef PIN_LED + pinMode(PIN_LED, OUTPUT); +#endif + attachInterrupt(KB_INT, has_key_event, RISING); + _pWire->begin(); + Keyboard_state = true; +#ifdef ARCH_ESP32 + // Detach/reattach the key interrupt around ESP32 light sleep + lsObserver.observe(¬ifyLightSleep); + lsEndObserver.observe(¬ifyLightSleepEnd); +#endif +} + +bool STC8HKeyboard::is_Keyboard_begin() +{ + return Keyboard_state; +} + +// A key is currently active (KB_INT held); used to wake the keypad backlight. +bool STC8HKeyboard::is_key_state() +{ + return digitalRead(KB_INT); +} + +// A key-press interrupt has been latched since the flag was last cleared. +bool STC8HKeyboard::is_key_event() +{ + return key_event; +} + +uint8_t STC8HKeyboard::bsp_get_key_value() +{ + return readRegister(0x01); +} + +// Battery millivolts: registers 0x01..0x04 read little-endian, low 16 bits. +uint16_t STC8HKeyboard::bsp_get_battery_voltage() +{ + if (!Keyboard_state) + return 0; + uint32_t voltage = 0; + for (uint8_t i = 0; i < 4; i++) + voltage |= (uint32_t)readRegister(STC8_REG_ADDR_BATTERY + i) << (i * 8); + return voltage > 0xFFFF ? 0xFFFF : (uint16_t)voltage; +} + +void STC8HKeyboard::set_keyboard_blight(bool state) +{ +#ifdef KB_LED + digitalWrite(KB_LED, state); +#else + (void)state; // KB_LED pin not defined for this board +#endif +} + +void STC8HKeyboard::switch_flashlight() +{ +#ifdef PIN_LED + digitalWrite(PIN_LED, !digitalRead(PIN_LED)); +#endif + // else: torch pin unresolved on this board (old board used PIN_LED 13, + // which the current variant assigns to BATTERY_PIN) -- see variant.h. +} + +void STC8HKeyboard::set_sleep_status(void) +{ + writeRegister(STC8_REG_ADDR_STATE, 0x01); + _pWire->end(); +} + +#ifdef ARCH_ESP32 +// Detach the key interrupt before ESP32 light sleep, so it can't fire while asleep. +int STC8HKeyboard::beforeLightSleep(void *unused) +{ + detachInterrupt(KB_INT); + return 0; // Indicates success +} + +// Reattach the key interrupt after waking from light sleep. +int STC8HKeyboard::afterLightSleep(esp_sleep_wakeup_cause_t cause) +{ + attachInterrupt(KB_INT, has_key_event, RISING); + return 0; // Indicates success +} +#endif + +#endif // ELECROW_ThinkNode_M9 diff --git a/src/input/STC8HKeyboard.h b/src/input/STC8HKeyboard.h new file mode 100644 index 0000000000..7de3b3d281 --- /dev/null +++ b/src/input/STC8HKeyboard.h @@ -0,0 +1,74 @@ +#pragma once +#ifndef _STC8H_KEYBOARD_H_ +#define _STC8H_KEYBOARD_H_ + +#include "configuration.h" +#include "kbI2cBase.h" +#include +#if defined(ELECROW_ThinkNode_M9) + +#ifdef ARCH_ESP32 +#include "sleep.h" // notifyLightSleep / notifyLightSleepEnd + esp_sleep_wakeup_cause_t +#endif + +// Registers exposed by the STC8H companion MCU over I2C. +#define STC8_REG_ADDR_BATTERY 0x01 +#define STC8_REG_ADDR_MATRIX_KEY 0x05 +#define STC8_REG_ADDR_STATE 0x06 + +class STC8HKeyboard +{ + public: + STC8HKeyboard(){}; + + void begin(uint8_t addr, TwoWire *wire); + + void set_sleep_status(void); + + uint16_t bsp_get_battery_voltage(); + + bool is_key_event(); + + bool is_Keyboard_begin(); + + bool is_key_state(); + + uint8_t bsp_get_key_value(); + + void set_keyboard_blight(bool state); + + void switch_flashlight(); + + uint8_t readRegister(uint8_t reg); + + bool key_event; + +#ifdef ARCH_ESP32 + // Detach/reattach the KB_INT interrupt around ESP32 light sleep, so the + // companion MCU's key interrupt can't fire spuriously while asleep. + int beforeLightSleep(void *unused); + int afterLightSleep(esp_sleep_wakeup_cause_t cause); +#endif + + private: + void writeRegister(uint8_t reg, uint8_t val); + + uint8_t _I2C_addr = TSTC8_KB_ADDR; + + TwoWire *_pWire = &Wire; + + bool Keyboard_state = false; + +#ifdef ARCH_ESP32 + // Get notified when light sleep begins and ends (mirrors TwoButton / Power) + CallbackObserver lsObserver = + CallbackObserver(this, &STC8HKeyboard::beforeLightSleep); + CallbackObserver lsEndObserver = + CallbackObserver(this, &STC8HKeyboard::afterLightSleep); +#endif +}; + +extern STC8HKeyboard Stc8HKeyBoard; + +#endif +#endif diff --git a/src/input/cardKbI2cImpl.cpp b/src/input/cardKbI2cImpl.cpp index cb03eb4ff6..dd86607c05 100644 --- a/src/input/cardKbI2cImpl.cpp +++ b/src/input/cardKbI2cImpl.cpp @@ -51,6 +51,10 @@ void CardKbI2cImpl::init() // assign an arbitrary value to distinguish from other models kb_model = 0x84; break; + case ScanI2C::DeviceType::STC8HKB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x12; + break; default: // use this as default since it's also just zero LOG_WARN("kb_info.type is unknown(0x%02x), setting kb_model=0x00", kb_info.type); diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index b786774605..d4851c7f65 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -15,6 +15,12 @@ #include "TCA8418Keyboard.h" #endif +#if defined(ELECROW_ThinkNode_M9) +#include "STC8HKeyboard.h" +#include "graphics/Screen.h" // for the global `screen` + FrameFocus +#include "graphics/draw/NotificationRenderer.h" // for resetBanner() +#endif + extern ScanI2C::DeviceAddress cardkb_found; extern uint8_t kb_model; @@ -64,6 +70,11 @@ int32_t KbI2cBase::runOnce() // resolved via the scanner: WIRE1 may be a bridged bus rather // than the local Wire1 (e.g. SenseCAP Indicator) i2cBus = ScanI2CTwoWire::fetchI2CBus(cardkb_found); +#if defined(ELECROW_ThinkNode_M9) + if (cardkb_found.address == TSTC8_KB_ADDR) { + Stc8HKeyBoard.begin(TSTC8_KB_ADDR, &Wire1); + } +#endif if (cardkb_found.address == BBQ10_KB_ADDR) { Q10keyboard.begin(BBQ10_KB_ADDR, i2cBus); Q10keyboard.setBacklight(0); @@ -79,6 +90,11 @@ int32_t KbI2cBase::runOnce() case ScanI2C::WIRE: LOG_DEBUG("Use I2C Bus 0 (the first one)"); i2cBus = &Wire; +#if defined(ELECROW_ThinkNode_M9) + if (cardkb_found.address == TSTC8_KB_ADDR) { + Stc8HKeyBoard.begin(TSTC8_KB_ADDR, &Wire); + } +#endif if (cardkb_found.address == BBQ10_KB_ADDR) { Q10keyboard.begin(BBQ10_KB_ADDR, &Wire); Q10keyboard.setBacklight(0); @@ -546,6 +562,104 @@ int32_t KbI2cBase::runOnce() } break; } +#if defined(ELECROW_ThinkNode_M9) + case 0x12: { // STC8H companion-MCU keypad (ThinkNode-M9) + Stc8HKeyBoard.key_event = false; + InputEvent e = {}; + e.inputEvent = INPUT_BROKER_NONE; + e.source = this->_originName; + uint8_t c = Stc8HKeyBoard.bsp_get_key_value(); // unsigned so the 0x8x/0xbx codes match + switch (c) { + case 0x81: // Mute + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = INPUT_BROKER_MSG_MUTE_TOGGLE; + break; + case 0x82: // Home + e.inputEvent = INPUT_BROKER_ANYKEY; + graphics::NotificationRenderer::resetBanner(); + // TODO(M9): also reset CannedMessage/PresetMessage state once those modules are ported + if (screen) + screen->setFrames(graphics::Screen::FOCUS_FAULT); + break; + case 0x83: // Time + e.inputEvent = INPUT_BROKER_ANYKEY; + graphics::NotificationRenderer::resetBanner(); + // TODO(M9): also reset CannedMessage/PresetMessage state once those modules are ported + if (screen) + screen->setFrames(graphics::Screen::FOCUS_CLOCK); + break; + case 0x84: + e.inputEvent = INPUT_BROKER_GPS_TOGGLE; + Stc8HKeyBoard.switch_flashlight(); + break; + case 0x85: // FM + e.inputEvent = INPUT_BROKER_SEND_PING; + e.kbchar = 0; + break; + case 0x86: // FM (long press) + e.inputEvent = INPUT_BROKER_CANCEL; + e.kbchar = 0; + break; + case 0x87: // Preset + graphics::NotificationRenderer::resetBanner(); + // TODO(M9): also reset CannedMessage state once that module is ported + e.inputEvent = INPUT_BROKER_SELECT_LONG; + e.kbchar = 0; + break; + case 0xb5: // Up + e.inputEvent = INPUT_BROKER_UP; + e.kbchar = 0; + break; + case 0xb4: // Left + e.inputEvent = INPUT_BROKER_LEFT; + e.kbchar = 0; + break; + case 0xb6: // Down + e.inputEvent = INPUT_BROKER_DOWN; + e.kbchar = 0; + break; + case 0xb7: // Right + e.inputEvent = INPUT_BROKER_RIGHT; + e.kbchar = 0; + break; + case 0x20: // Space + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = 0x20; + break; + case 0x0d: // Enter + e.inputEvent = INPUT_BROKER_SELECT; + e.kbchar = 0; + break; + case 0x08: // Del + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0; + break; + case 0x89: // Del (long press) + e.inputEvent = INPUT_BROKER_BACK; + e.kbchar = 0; + break; + case 0x88: // Invalid key value + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = 0; + break; + default: // all other keys (printable ASCII) + if ((c >= 0x20) && (c <= 0x7F)) { + e.inputEvent = INPUT_BROKER_ANYKEY; + e.kbchar = c; + } else { + e.inputEvent = INPUT_BROKER_NONE; + e.kbchar = 0; + } + break; + } + if (e.inputEvent != INPUT_BROKER_NONE) { + // LOG_DEBUG("STC8H companion-MCU keypad key event: 0x%02x", c); + this->notifyObservers(&e); + } + + break; + } +#endif default: LOG_WARN("Unknown kb_model 0x%02x", kb_model); } diff --git a/src/main.cpp b/src/main.cpp index 6b192aa049..df87cfcdeb 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -761,6 +761,10 @@ void setup() // assign an arbitrary value to distinguish from other models kb_model = 0x84; break; + case ScanI2C::DeviceType::STC8HKB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x12; + break; default: // use this as default since it's also just zero LOG_WARN("kb_info.type unknown(0x%02x), set kb_model=0x00", kb_info.type); diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 715daff1b9..41d570f3e1 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -1032,7 +1032,8 @@ void NodeDB::installDefaultConfig(bool preserveKey = false) strncpy(config.network.ntp_server, "meshtastic.pool.ntp.org", 32); #if (defined(T_DECK) || defined(T_WATCH_S3) || defined(UNPHONE) || defined(PICOMPUTER_S3) || defined(SENSECAP_INDICATOR) || \ - defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT) || defined(RAK_WISMESH_TAP_V2)) && \ + defined(ELECROW_PANEL) || defined(HELTEC_V4_TFT) || defined(HELTEC_V4_R8_TFT) || defined(RAK_WISMESH_TAP_V2) || \ + defined(ELECROW_ThinkNode_M9)) && \ HAS_TFT // switch BT off by default; use TFT programming mode or hotkey to enable config.bluetooth.enabled = false; @@ -1264,7 +1265,10 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.output_ms = 1000; #endif -#if defined(PIN_VIBRATION) +#if HAS_TFT + if (moduleConfig.external_notification.nag_timeout == default_ringtone_nag_secs) + moduleConfig.external_notification.nag_timeout = 0; +#elif defined(PIN_VIBRATION) moduleConfig.external_notification.nag_timeout = 2; #elif defined(PIN_BUZZER) || defined(LED_NOTIFICATION) || defined(NEOPIXEL_STATUS_NOTIFICATION_PIN) || \ defined(HAS_I2S_SPEAKER_NRF52) @@ -1276,12 +1280,6 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.enabled = true; moduleConfig.external_notification.use_i2s_as_buzzer = true; moduleConfig.external_notification.alert_message_buzzer = true; -#if HAS_TFT - if (moduleConfig.external_notification.nag_timeout == default_ringtone_nag_secs) - moduleConfig.external_notification.nag_timeout = 0; -#else - moduleConfig.external_notification.nag_timeout = default_ringtone_nag_secs; -#endif // HAS_TFT #endif // HAS_I2S #ifdef NANO_G2_ULTRA diff --git a/src/motion/AccelerometerThread.h b/src/motion/AccelerometerThread.h index 0bcb504fbb..63009aecc7 100755 --- a/src/motion/AccelerometerThread.h +++ b/src/motion/AccelerometerThread.h @@ -21,6 +21,7 @@ #include "LSM6DS3Sensor.h" #include "MPU6050Sensor.h" #include "MotionSensor.h" +#include "QMI8658Sensor.h" #include #ifdef HAS_QMA6100P @@ -146,6 +147,11 @@ class AccelerometerThread : public concurrency::OSThread case ScanI2C::DeviceType::QMA6100P: sensor.reset(new QMA6100PSensor(device)); break; +#endif +#if __has_include() + case ScanI2C::DeviceType::QMI8658: + sensor.reset(new QMI8658Sensor(device)); + break; #endif default: disable(); diff --git a/src/motion/MagnetometerThread.h b/src/motion/MagnetometerThread.h index 8185f296aa..4a3d7d69a4 100644 --- a/src/motion/MagnetometerThread.h +++ b/src/motion/MagnetometerThread.h @@ -9,6 +9,7 @@ #include "../concurrency/OSThread.h" #include "MMC5983MASensor.h" #include "MotionSensor.h" +#include "QMC6309Sensor.h" #include @@ -73,6 +74,11 @@ class MagnetometerThread : public concurrency::OSThread case ScanI2C::DeviceType::MMC5983MA: sensor.reset(new MMC5983MASensor(device)); break; +#endif +#if __has_include() + case ScanI2C::DeviceType::QMC6309: + sensor.reset(new QMC6309Sensor(device)); + break; #endif default: disable(); diff --git a/src/motion/MotionSensor.cpp b/src/motion/MotionSensor.cpp index 6cbe8e21db..e6331ea857 100755 --- a/src/motion/MotionSensor.cpp +++ b/src/motion/MotionSensor.cpp @@ -42,6 +42,9 @@ struct CompassAccelSample { concurrency::Lock latestCompassAccelLock; CompassAccelSample latestCompassAccelSample; + +concurrency::Lock latestCompassMagLock; +CompassAccelSample latestCompassMagSample; } // namespace // screen is defined in main.cpp @@ -245,6 +248,35 @@ bool MotionSensor::getLatestCompassAccelSample(float &x, float &y, float &z, uin return true; } +void MotionSensor::publishCompassMagSample(float x, float y, float z) +{ + concurrency::LockGuard guard(&latestCompassMagLock); + latestCompassMagSample.x = x; + latestCompassMagSample.y = y; + latestCompassMagSample.z = z; + latestCompassMagSample.sampledAtMs = millis(); + latestCompassMagSample.valid = true; +} + +bool MotionSensor::getLatestCompassMagSample(float &x, float &y, float &z, uint32_t &ageMs) +{ + uint32_t sampledAtMs = 0; + { + concurrency::LockGuard guard(&latestCompassMagLock); + if (!latestCompassMagSample.valid) { + return false; + } + + x = latestCompassMagSample.x; + y = latestCompassMagSample.y; + z = latestCompassMagSample.z; + sampledAtMs = latestCompassMagSample.sampledAtMs; + } + + ageMs = millis() - sampledAtMs; + return true; +} + #if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN void MotionSensor::drawFrameCalibration(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { diff --git a/src/motion/MotionSensor.h b/src/motion/MotionSensor.h index 38876fb776..ef84e1b19b 100755 --- a/src/motion/MotionSensor.h +++ b/src/motion/MotionSensor.h @@ -42,6 +42,11 @@ class MotionSensor virtual void calibrate(uint16_t forSeconds){}; + // Latest samples published by the compass-fusion drivers (accel from the IMU, mag from the magnetometer). + // Public so an optional on-screen sensor debug readout can read them. Return false if nothing published yet. + static bool getLatestCompassAccelSample(float &x, float &y, float &z, uint32_t &ageMs); + static bool getLatestCompassMagSample(float &x, float &y, float &z, uint32_t &ageMs); + // True if this sensor produces the compass heading (screen->setHeading()) in runOnce(). // Combined accel+magnetometer parts (e.g. BMX160, ICM20948) and standalone magnetometers // handled by the accelerometer thread (e.g. BMM150) override this. Used to avoid halting @@ -74,7 +79,7 @@ class MotionSensor float &lowestY, float &highestZ, float &lowestZ); static float applyCompassOrientation(float heading); static void publishCompassAccelSample(float x, float y, float z); - static bool getLatestCompassAccelSample(float &x, float &y, float &z, uint32_t &ageMs); + static void publishCompassMagSample(float x, float y, float z); ScanI2C::FoundDevice device; diff --git a/src/motion/QMC6309Sensor.cpp b/src/motion/QMC6309Sensor.cpp new file mode 100644 index 0000000000..bdf82878b2 --- /dev/null +++ b/src/motion/QMC6309Sensor.cpp @@ -0,0 +1,152 @@ +#include "QMC6309Sensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() + +#include "Fusion/Fusion.h" +#include "detect/ScanI2CTwoWire.h" +#include + +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) +extern std::unique_ptr screen; +#endif + +static constexpr int32_t QMC6309_UPDATE_INTERVAL_MS = 20; +// Heading offset/flip below is a starting point copied from the MMC5983MA path; it is orientation-specific +// and must be verified/tuned against known North on real M9 hardware (see plan). +static constexpr float QMC6309_HEADING_OFFSET_DEG = 180.0f; +static constexpr uint32_t QMC6309_ACCEL_STALE_MS = 300; +static constexpr float QMC6309_MIN_AXIS_RADIUS = 1e-4f; + +QMC6309Sensor::QMC6309Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} + +bool QMC6309Sensor::init() +{ + LOG_DEBUG("QMC6309 begin on addr 0x%02X (port=%d)", device.address.address, device.address.port); + TwoWire *wire = ScanI2CTwoWire::fetchI2CBus(device.address); + + if (!sensor.begin(*wire, deviceAddress())) { + LOG_DEBUG("QMC6309 init error"); + return false; + } + + sensor.reset(); + + // 8 Gauss full-scale easily covers Earth's ~0.5 G field; OSR_8 for low noise. Tunable. + if (!sensor.configMagnetometer(OperationMode::CONTINUOUS_MEASUREMENT, MagFullScaleRange::FS_8G, 100.0f, + MagOverSampleRatio::OSR_8)) { + LOG_DEBUG("QMC6309 config failed"); + return false; + } + + loadMagnetometerCalibration(compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + LOG_DEBUG("QMC6309 init ok"); + LOG_DEBUG("QMC6309 calibration extrema: X=(%.3f, %.3f), Y=(%.3f, %.3f), Z=(%.3f, %.3f)", lowestX, highestX, lowestY, highestY, + lowestZ, highestZ); + return true; +} + +bool QMC6309Sensor::readMagnetometer(float &xGauss, float &yGauss, float &zGauss) +{ + MagnetometerData data; + if (!sensor.readData(data)) { + return false; + } + + // magnetic_field is already scaled to Gauss by the driver. + xGauss = data.magnetic_field.x; + yGauss = data.magnetic_field.y; + zGauss = data.magnetic_field.z; + return true; +} + +int32_t QMC6309Sensor::runOnce() +{ + float magX = 0, magY = 0, magZ = 0; + if (!readMagnetometer(magX, magY, magZ)) { + return QMC6309_UPDATE_INTERVAL_MS; + } + +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) + if (doCalibration) { + beginCalibrationDisplay(showingScreen); + updateCalibrationExtrema(magX, magY, magZ, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + finishCalibrationIfExpired(showingScreen, compassCalibrationFileName, highestX, lowestX, highestY, lowestY, highestZ, + lowestZ); + } +#endif + + // Hard-iron bias removal. + magX -= (highestX + lowestX) * 0.5f; + magY -= (highestY + lowestY) * 0.5f; + magZ -= (highestZ + lowestZ) * 0.5f; + // LOG_WARN("QMC6309 extrema=(%.3f, %.3f, %.3f) to (%.3f, %.3f, %.3f)", + // lowestX, lowestY, lowestZ, highestX, highestY, highestZ); + + // Soft-iron diagonal scaling from calibration extrema. + const float radiusX = (highestX - lowestX) * 0.5f; + const float radiusY = (highestY - lowestY) * 0.5f; + const float radiusZ = (highestZ - lowestZ) * 0.5f; + const float avgRadius = (radiusX + radiusY + radiusZ) / 3.0f; + // magX *= (radiusX > QMC6309_MIN_AXIS_RADIUS) ? (avgRadius / radiusX) : 1.0f; + // magY *= (radiusY > QMC6309_MIN_AXIS_RADIUS) ? (avgRadius / radiusY) : 1.0f; + // magZ *= (radiusZ > QMC6309_MIN_AXIS_RADIUS) ? (avgRadius / radiusZ) : 1.0f; + + // Publish the calibrated magnetometer values (hard/soft-iron applied) for the optional on-screen debug readout. + publishCompassMagSample(magX, magY, magZ); + +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) && HAS_SCREEN + float heading; + float accelX = 0.0f; + float accelY = 0.0f; + float accelZ = 0.0f; + uint32_t accelAgeMs = 0; + + // Fuse with the latest accelerometer sample (published by the QMI8658 driver) for tilt compensation. + if (getLatestCompassAccelSample(accelX, accelY, accelZ, accelAgeMs) && accelAgeMs <= QMC6309_ACCEL_STALE_MS) { + FusionVector ga = {.axis = {accelX, accelY, accelZ}}; + FusionVector ma = {.axis = {magX, magY, magZ}}; + // if (config.display.compass_orientation > meshtastic_Config_DisplayConfig_CompassOrientation_DEGREES_270) { + // ma = FusionAxesSwap(ma, FusionAxesAlignmentNXNYPZ); + // ga = FusionAxesSwap(ga, FusionAxesAlignmentNXNYPZ); + //} + // LOG_WARN("QMC6309 accel age %ums, ga=(%.3f, %.3f, %.3f), ma=(%.3f, %.3f, %.3f)", accelAgeMs, ga.axis.x, ga.axis.y, + // ga.axis.z, ma.axis.x, ma.axis.y, ma.axis.z); + heading = FusionCompass(ga, ma, FusionConventionNed); + if (ga.axis.z > 0.0f) + heading = 360.0f - heading; + + } else { + heading = atan2f(-magY, magX) * RAD_TO_DEG; + } + + if (heading >= 360.0f) + heading -= 360.0f; + else if (heading < 0.0f) + heading += 360.0f; + + heading = applyCompassOrientation(heading); + if (screen) + screen->setHeading(heading); +#endif + + return QMC6309_UPDATE_INTERVAL_MS; +} + +void QMC6309Sensor::calibrate(uint16_t forSeconds) +{ +#if !defined(MESHTASTIC_EXCLUDE_SCREEN) + float xGauss = 0.0f; + float yGauss = 0.0f; + float zGauss = 0.0f; + + LOG_DEBUG("QMC6309 calibration started for %is", forSeconds); + if (readMagnetometer(xGauss, yGauss, zGauss)) { + seedCalibrationExtrema(xGauss, yGauss, zGauss, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + } else { + seedCalibrationExtrema(0.0f, 0.0f, 0.0f, highestX, lowestX, highestY, lowestY, highestZ, lowestZ); + } + startCalibrationWindow(forSeconds); +#endif +} + +#endif diff --git a/src/motion/QMC6309Sensor.h b/src/motion/QMC6309Sensor.h new file mode 100644 index 0000000000..c457f35269 --- /dev/null +++ b/src/motion/QMC6309Sensor.h @@ -0,0 +1,40 @@ +#pragma once +#ifndef _QMC6309_SENSOR_H_ +#define _QMC6309_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() + +// SensorQMC6309.hpp (SensorLib 0.4.1) references the isBitSet() macro in an inline method but never includes +// SensorLib.h where it is defined. Define it here (guarded) so the header compiles regardless of include order +// (pulling in SensorLib.h is unreliable - its #pragma once can already be tripped by an in-progress include). +#ifndef isBitSet +#define isBitSet(value, bit) (((value) & (1UL << (bit))) == (1UL << (bit))) +#endif +#include + +class QMC6309Sensor : public MotionSensor +{ + private: + SensorQMC6309 sensor; + bool showingScreen = false; + static constexpr const char *compassCalibrationFileName = "/prefs/compass_qmc6309.dat"; +#ifdef ELECROW_ThinkNode_M9 + float highestX = -5.548, lowestX = -6.530, highestY = -6.638, lowestY = -7.637, highestZ = -6.676, lowestZ = -7.633; +#else + float highestX = 0, lowestX = 0, highestY = 0, lowestY = 0, highestZ = 0, lowestZ = 0; +#endif + + bool readMagnetometer(float &xGauss, float &yGauss, float &zGauss); + + public: + explicit QMC6309Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; + virtual void calibrate(uint16_t forSeconds) override; +}; + +#endif + +#endif diff --git a/src/motion/QMI8658Sensor.cpp b/src/motion/QMI8658Sensor.cpp new file mode 100644 index 0000000000..e55ffc26f7 --- /dev/null +++ b/src/motion/QMI8658Sensor.cpp @@ -0,0 +1,88 @@ +#include "QMI8658Sensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() + +#include "NodeDB.h" +#include "detect/ScanI2CTwoWire.h" +#include + +// Accelerometer configuration. 2G full-scale gives the best gravity resolution for the tilt +// compensation that the (future) separate compass module will apply to these samples. +static constexpr SensorQMI8658::AccelRange QMI8658_ACCEL_RANGE = SensorQMI8658::ACC_RANGE_2G; +static constexpr SensorQMI8658::AccelODR QMI8658_ACCEL_ODR = SensorQMI8658::ACC_ODR_125Hz; + +// Any-motion slope threshold (in mg) used to wake the screen. Tunable: raise to reduce false wakes, +// lower to make it more sensitive. 200mg (~0.2g) requires a deliberate movement. +static constexpr float QMI8658_ANY_MOTION_THRESHOLD_MG = 200.0f; +static constexpr uint8_t QMI8658_ANY_MOTION_WINDOW = 1; + +// Optional board-defined rotation (degrees) applied to the accel X/Y before publishing to the compass +// fusion path, mirroring the ICM42607P driver. Defaults to no rotation. +static constexpr float QMI8658_ACCEL_TO_COMPASS_ROTATION_DEG_VALUE = +#ifdef QMI8658_ACCEL_TO_COMPASS_ROTATION_DEG + QMI8658_ACCEL_TO_COMPASS_ROTATION_DEG; +#else + 0.0f; +#endif + +QMI8658Sensor::QMI8658Sensor(ScanI2C::FoundDevice foundDevice) : MotionSensor::MotionSensor(foundDevice) {} + +bool QMI8658Sensor::init() +{ + LOG_DEBUG("QMI8658 begin on addr 0x%02X (port=%d)", deviceAddress(), devicePort()); + TwoWire *wire = ScanI2CTwoWire::fetchI2CBus(device.address); + + if (!sensor.begin(*wire, deviceAddress())) { + LOG_DEBUG("QMI8658 init failed"); + return false; + } + + sensor.configAccelerometer(QMI8658_ACCEL_RANGE, QMI8658_ACCEL_ODR, SensorQMI8658::LPF_MODE_0); + sensor.enableAccelerometer(); + + // Configure the on-chip any-motion engine so we can wake the screen without a dedicated interrupt pin. + // configMotion() runs alongside normal accel data output (unlike Wake-on-Motion, which halts data), so + // we keep publishing samples for compass fusion while still detecting motion. + wakeOnMotion = config.display.wake_on_tap_or_motion; + if (wakeOnMotion) { + const uint8_t modeCtrl = SensorQMI8658::ANY_MOTION_EN_X | SensorQMI8658::ANY_MOTION_EN_Y | SensorQMI8658::ANY_MOTION_EN_Z; + // No-motion detection is left disabled (unreliable per the SensorLib example); its thresholds/windows + // are still required arguments but are ignored when the mode bits above are clear. + sensor.configMotion(modeCtrl, QMI8658_ANY_MOTION_THRESHOLD_MG, QMI8658_ANY_MOTION_THRESHOLD_MG, + QMI8658_ANY_MOTION_THRESHOLD_MG, QMI8658_ANY_MOTION_WINDOW, /*NoMotion X/Y/Z*/ 0.1f, 0.1f, 0.1f, + /*NoMotionWindow*/ 1, /*SigMotionWaitWindow*/ 1, /*SigMotionConfirmWindow*/ 1); + sensor.enableMotionDetect(); + } + + LOG_DEBUG("QMI8658 init ok"); + return true; +} + +int32_t QMI8658Sensor::runOnce() +{ + float ax, ay, az; + if (sensor.getAccelerometer(ax, ay, az)) { + if (QMI8658_ACCEL_TO_COMPASS_ROTATION_DEG_VALUE != 0.0f) { + static const float rotRad = QMI8658_ACCEL_TO_COMPASS_ROTATION_DEG_VALUE * DEG_TO_RAD; + static const float cosTheta = cosf(rotRad); + static const float sinTheta = sinf(rotRad); + const float rotatedX = (ax * cosTheta) - (ay * sinTheta); + const float rotatedY = (ax * sinTheta) + (ay * cosTheta); + ax = rotatedX; + ay = rotatedY; + } + + // Match the accel sign convention used by the other FusionCompass sensor paths (e.g. ICM42607P). + // The final handedness must be verified against the QMI8658 datasheet and real calibration once the + // separate compass module is wired up; do not hand-tune the signs before then. + publishCompassAccelSample(ax, ay, az); + } + + if (wakeOnMotion && (sensor.getStatusRegister() & SensorQMI8658::EVENT_ANY_MOTION)) { + wakeScreen(); + } + + return MOTION_SENSOR_CHECK_INTERVAL_MS; +} + +#endif diff --git a/src/motion/QMI8658Sensor.h b/src/motion/QMI8658Sensor.h new file mode 100644 index 0000000000..6aa310132f --- /dev/null +++ b/src/motion/QMI8658Sensor.h @@ -0,0 +1,25 @@ +#pragma once +#ifndef _QMI8658_SENSOR_H_ +#define _QMI8658_SENSOR_H_ + +#include "MotionSensor.h" + +#if !defined(ARCH_STM32WL) && !MESHTASTIC_EXCLUDE_I2C && __has_include() + +#include + +class QMI8658Sensor : public MotionSensor +{ + private: + SensorQMI8658 sensor; + bool wakeOnMotion = false; + + public: + explicit QMI8658Sensor(ScanI2C::FoundDevice foundDevice); + virtual bool init() override; + virtual int32_t runOnce() override; +}; + +#endif + +#endif diff --git a/src/sleep.cpp b/src/sleep.cpp index c5d469b42a..0a6a379788 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -453,8 +453,12 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t sleepMsec) // FIXME, use a more r gpio_wakeup_enable((gpio_num_t)ROTARY_PRESS, GPIO_INTR_LOW_LEVEL); #endif #ifdef KB_INT +#if KB_INT_WAKE_ON_HIGH + gpio_wakeup_enable((gpio_num_t)KB_INT, GPIO_INTR_HIGH_LEVEL); +#else gpio_wakeup_enable((gpio_num_t)KB_INT, GPIO_INTR_LOW_LEVEL); -#endif +#endif // KB_INT_WAKE_ON_HIGH +#endif // KB_INT #ifdef BOARD_PCA9535_INT // Side-key interrupt line from PCA9535 expander (active low). gpio_wakeup_enable((gpio_num_t)BOARD_PCA9535_INT, GPIO_INTR_LOW_LEVEL); diff --git a/variants/esp32s3/ELECROW-ThinkNode-M9/pins_arduino.h b/variants/esp32s3/ELECROW-ThinkNode-M9/pins_arduino.h new file mode 100644 index 0000000000..0099711948 --- /dev/null +++ b/variants/esp32s3/ELECROW-ThinkNode-M9/pins_arduino.h @@ -0,0 +1,20 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 20; +static const uint8_t SCL = 21; + +static const uint8_t SS = 39; +static const uint8_t MOSI = 47; +static const uint8_t MISO = 38; +static const uint8_t SCK = 40; + +#endif \ No newline at end of file diff --git a/variants/esp32s3/ELECROW-ThinkNode-M9/platformio.ini b/variants/esp32s3/ELECROW-ThinkNode-M9/platformio.ini new file mode 100644 index 0000000000..fbd595dee9 --- /dev/null +++ b/variants/esp32s3/ELECROW-ThinkNode-M9/platformio.ini @@ -0,0 +1,99 @@ +[thinknode_m9_base] +custom_meshtastic_hw_model = 131 +custom_meshtastic_hw_model_slug = ELECROW_ThinkNode_M9 +custom_meshtastic_architecture = esp32-s3 +custom_meshtastic_actively_supported = true +custom_meshtastic_support_level = 1 +custom_meshtastic_display_name = ThinkNode M9 +custom_meshtastic_images = thinknode_m9.svg +custom_meshtastic_tags = Elecrow +custom_meshtastic_requires_dfu = false +custom_meshtastic_partition_scheme = 16MB +custom_meshtastic_has_mui = true + +extends = esp32s3_base +board = crowpanel +board_level = release +board_build.partitions = default_16MB.csv +upload_protocol = esptool +build_src_filter = + ${esp32s3_base.build_src_filter} + +<../variants/esp32s3/ELECROW-ThinkNode-M9> +build_flags = + ${esp32s3_base.build_flags} + -I variants/esp32s3/ELECROW-ThinkNode-M9 + -D ELECROW_ThinkNode_M9 + -D BOARD_HAS_PSRAM + -D HAS_SDCARD=1 + -D SDCARD_CS=48 + -D SDCARD_USE_SPI1 + -D SDCARD_SHARE_SPI + -D SDCARD_INIT_SPI + -D SD_SPI_FREQUENCY=75000000U + -D HAS_SCREEN=1 +; -D COMPASS_SENSOR_DEBUG=1 +lib_deps = ${esp32s3_base.lib_deps} + # renovate: datasource=custom depName=LovyanGFX packageName=lovyan03/library/LovyanGFX + lovyan03/LovyanGFX@1.2.26 + # renovate: datasource=custom depName=SensorLib packageName=lewisxhe/library/SensorLib + lewisxhe/SensorLib@0.4.1 + +[env:thinknode_m9] +extends = thinknode_m9_base +build_flags = + ${thinknode_m9_base.build_flags} + +[env:thinknode_m9-tft] +extends = thinknode_m9_base +build_flags = + ${thinknode_m9_base.build_flags} + -D RADIOLIB_SPI_PARANOID=0 + -D CONFIG_DISABLE_HAL_LOCKS=1 + -D HAS_TFT=1 + -D USE_PACKET_API + -D USE_PIN_BUZZER=PIN_BUZZER + -D LV_LVGL_H_INCLUDE_SIMPLE + -D LV_CONF_INCLUDE_SIMPLE + -D LV_COMP_CONF_INCLUDE_SIMPLE + -D LV_USE_SYSMON=0 + -D LV_USE_PROFILER=0 + -D LV_USE_PERF_MONITOR=0 + -D LV_USE_MEM_MONITOR=0 + -D LV_USE_LOG=0 + -D LV_BUILD_TEST=0 + -D USE_LOG_DEBUG + -D LOG_DEBUG_INC=\"DebugConfiguration.h\" + -D RAM_SIZE=5120 + -D LGFX_BUFSIZE=153600 + -D LGFX_DRIVER_TEMPLATE + -D DISPLAY_SIZE=320x240 + -D LGFX_DRIVER=LGFX_GENERIC + -D GFX_DRIVER_INC=\"graphics/LGFX/LGFX_GENERIC.h\" + -D VIEW_320x240 + -D LGFX_PANEL=ST7789 + -D LGFX_ROTATION=3 + -D LGFX_CFG_HOST=SPI3_HOST + -D LGFX_PIN_SCK=40 + -D LGFX_PIN_MOSI=47 + -D LGFX_PIN_DC=15 + -D LGFX_PIN_CS=16 + -D LGFX_PIN_BL=17 + -D LGFX_PIN_RST=14 + -D LGFX_SCREEN_WIDTH=240 + -D LGFX_SCREEN_HEIGHT=320 + -D LGFX_INVERT_LIGHT=true + -D SPI_FREQUENCY=75000000 +; -D MAP_FULL_REDRAW + -D MUI_WIFI_PS_MIN_MODEM + -D DEFAULT_FTP_SERVER_NETWORK_TYPE_ESP32=NETWORK_ESP32 + -D DEFAULT_STORAGE_TYPE_ESP32=STORAGE_SD + -D CHARGING_VOLTAGE=4.25 + +lib_deps = + ${thinknode_m9_base.lib_deps} + https://github.com/meshtastic/device-ui/archive/e8a5ff337d1ead20b290307fb2159ad27fe47f86.zip ; PR314 input-policy + https://github.com/mverch67/MultiFTPServer/archive/0e854335b9916ed9f2d3bcfe68975ce746992ccd.zip + +custom_sdkconfig = + ${esp32s3_base.custom_sdkconfig} + ${device-ui_base.custom_sdkconfig} \ No newline at end of file diff --git a/variants/esp32s3/ELECROW-ThinkNode-M9/rfswitch.h b/variants/esp32s3/ELECROW-ThinkNode-M9/rfswitch.h new file mode 100644 index 0000000000..e5fe182c4f --- /dev/null +++ b/variants/esp32s3/ELECROW-ThinkNode-M9/rfswitch.h @@ -0,0 +1,11 @@ +#include "RadioLib.h" + +static const uint32_t rfswitch_dio_pins[] = {RADIOLIB_LR11X0_DIO5, RADIOLIB_LR11X0_DIO6, RADIOLIB_NC, RADIOLIB_NC, RADIOLIB_NC}; + +static const Module::RfSwitchMode_t rfswitch_table[] = { + // mode DIO5 DIO6 + {LR11x0::MODE_STBY, {LOW, LOW}}, {LR11x0::MODE_RX, {HIGH, LOW}}, + {LR11x0::MODE_TX, {HIGH, HIGH}}, {LR11x0::MODE_TX_HP, {LOW, HIGH}}, + {LR11x0::MODE_TX_HF, {LOW, LOW}}, {LR11x0::MODE_GNSS, {LOW, LOW}}, + {LR11x0::MODE_WIFI, {LOW, LOW}}, END_OF_MODE_TABLE, +}; diff --git a/variants/esp32s3/ELECROW-ThinkNode-M9/variant.cpp b/variants/esp32s3/ELECROW-ThinkNode-M9/variant.cpp new file mode 100644 index 0000000000..279dedeb50 --- /dev/null +++ b/variants/esp32s3/ELECROW-ThinkNode-M9/variant.cpp @@ -0,0 +1,34 @@ +#include "variant.h" +#include "Arduino.h" +#include "SPILock.h" +#include "Wire.h" + +void earlyInitVariant() +{ + pinMode(LORA_CS, OUTPUT); + digitalWrite(LORA_CS, HIGH); + pinMode(SDCARD_CS, OUTPUT); + digitalWrite(SDCARD_CS, HIGH); + pinMode(TFT_CS, OUTPUT); + digitalWrite(TFT_CS, HIGH); + delay(100); +} + +void lateInitVariant() +{ + // configure keyboard long-press time + const uint16_t ms = 700; + concurrency::LockGuard g(spiLock); + Wire.beginTransmission(0x6C); + Wire.write(0x03); + Wire.write((ms >> 8) & 0xFF); + Wire.write(ms & 0xFF); + Wire.endTransmission(); +} + +void variant_shutdown() +{ + uint64_t gpioMask = (1ULL << KB_INT); + gpio_pulldown_en((gpio_num_t)KB_INT); + esp_sleep_enable_ext1_wakeup(gpioMask, ESP_EXT1_WAKEUP_ANY_HIGH); +} \ No newline at end of file diff --git a/variants/esp32s3/ELECROW-ThinkNode-M9/variant.h b/variants/esp32s3/ELECROW-ThinkNode-M9/variant.h new file mode 100644 index 0000000000..543a690899 --- /dev/null +++ b/variants/esp32s3/ELECROW-ThinkNode-M9/variant.h @@ -0,0 +1,107 @@ +#define CANNED_MESSAGE_MODULE_ENABLE 1 +#define PRESET_MESSAGE_MODULE_ENABLE 1 + +/*Power*/ +#define VEXT_ENABLE 18 +#define VEXT_ON_VALUE LOW +#define PIN_GPS_EN 11 +#define GPS_EN_ACTIVE LOW + +#define USE_POWERSAVE +#define SLEEP_TIME 120 + +/*Wire Interface*/ +#define WIRE_INTERFACES_COUNT 2 +// I2C keyboard +#define I2C_SCL 21 +#define I2C_SDA 20 +#define KB_INT 12 // STC8H key-press interrupt (idle low, rising edge on press) +#define KB_INT_WAKE_ON_HIGH 1 // KB_INT rests low; wake light sleep on its HIGH (active) level +#define KB_LED 46 // STC8H keypad backlight LED +// I2C peripheral +#define I2C_SCL1 6 +#define I2C_SDA1 7 + +/*BUZZER*/ +#define PIN_BUZZER 9 + +/*CHARGE_CHECK*/ +#define EXT_PWR_DETECT 1 +// #define EXT_CHRG_DETECT 1 +#define EXT_PWR_DETECT_VALUE LOW + +/*GPS*/ +#define HAS_GPS 1 +#define GPS_BAUDRATE 115200 +#define PIN_GPS_RESET 5 +#define PIN_GPS_PPS 4 +#define GPS_TX_PIN 3 +#define GPS_RX_PIN 2 +#define GPS_THREAD_INTERVAL 50 + +/*SPI*/ +#define SPI_MOSI 47 +#define SPI_SCK 40 +#define SPI_MISO 38 + +/*Screen*/ +#define ST7789_CS 16 +#define ST7789_RS 15 +#define ST7789_TE 19 +#define ST7789_SDA SPI_MOSI // MOSI +#define ST7789_SCK SPI_SCK +#define ST7789_RESET 14 +#define ST7789_MISO SPI_MISO +#define ST7789_BUSY -1 +#define ST7789_BL 17 +#define ST7789_SPI_HOST SPI3_HOST +#define SPI_READ_FREQUENCY 16000000 + +#define USE_TFTDISPLAY 1 +#define HAS_SPI_TFT 1 +#define TFT_CS ST7789_CS +#define TFT_BL ST7789_BL +#define TFT_HEIGHT 320 +#define TFT_WIDTH 240 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define TFT_OFFSET_ROTATION 0 +#define TFT_PWM_FREQ 44000 +#define TFT_PWM_CHANNEL 7 +#define TFT_INVERT_LIGHT true +#define TFT_BACKLIGHT_ON LOW +#define SCREEN_ROTATE +#define SCREEN_TRANSITION_FRAMERATE 10 +#define BRIGHTNESS_DEFAULT 128 + +/*Lora radio*/ +#define HW_SPI1_DEVICE +#define LORA_SCK SPI_SCK +#define LORA_MISO SPI_MISO +#define LORA_MOSI SPI_MOSI +#define LORA_CS 39 +#define LORA_RESET 45 +#define LORA_DIO0 41 +#define LORA_DIO1 42 + +#define USE_LR1110 +#define LR1110_IRQ_PIN LORA_DIO1 +#define LR1110_NRESET_PIN LORA_RESET +#define LR1110_BUSY_PIN LORA_DIO0 +#define LR1110_SPI_NSS_PIN LORA_CS +#define LR1110_SPI_SCK_PIN LORA_SCK +#define LR1110_SPI_MOSI_PIN LORA_MOSI +#define LR1110_SPI_MISO_PIN LORA_MISO +#define LR11X0_DIO3_TCXO_VOLTAGE 3.3 +#define LR11X0_DIO_AS_RF_SWITCH + +/*RTC*/ +#define PCF8563_RTC 0x51 + +/*BATTERY*/ +#define BATTERY_PIN 13 +#define BATTERY_IMMUTABLE +#define ADC_MULTIPLIER 2.0f +#define BAT_MEASURE_ADC_UNIT ADC_UNIT_2 +#define ADC_CHANNEL ADC_CHANNEL_2 +#define OCV_ARRAY 4200, 4080, 3980, 3920, 3870, 3820, 3790, 3750, 3700, 3600, 3100 diff --git a/variants/esp32s3/t-deck/platformio.ini b/variants/esp32s3/t-deck/platformio.ini index 047371db9e..6448777935 100644 --- a/variants/esp32s3/t-deck/platformio.ini +++ b/variants/esp32s3/t-deck/platformio.ini @@ -39,6 +39,10 @@ lib_deps = ${esp32s3_base.lib_deps} extends = env:t-deck board_level = pr +extra_scripts = + ${env:t-deck.extra_scripts} + extra_scripts/ld_response_file.py + build_flags = ${env:t-deck.build_flags} -D CONFIG_DISABLE_HAL_LOCKS=1 ; "feels" to be a bit more stable without locks