From 4ef61f0f1599cde71d7f85c3d29274fa21522810 Mon Sep 17 00:00:00 2001 From: Dmitry Galenko Date: Sun, 2 Jul 2023 02:20:40 +0200 Subject: [PATCH 01/57] GPS: Performance improvment for U-Blox hardware (#2574) * Add proper configuration procedure for U-Blox modules * More human friendly getACK * Fix checksum calculation and payload * GPS: move unsigned int check * Introduce UBX protocol payload checksuming * Fix missed checksums calculation for UBX-CFG-CFG --- src/gps/GPS.cpp | 167 ++++++++++++++++++++++++++++++++++++++++++++++-- src/gps/GPS.h | 5 +- 2 files changed, 164 insertions(+), 8 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 1b7c8511f..13c46d62e 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -21,11 +21,26 @@ GPS *gps; /// only init that port once. static bool didSerialInit; -bool GPS::getACK(uint8_t c, uint8_t i) +void GPS::UBXChecksum(byte *message, size_t length) +{ + uint8_t CK_A = 0, CK_B = 0; + + // Calculate the checksum, starting from the CLASS field (which is message[2]) + for (size_t i = 2; i < length - 2; i++) { + CK_A = (CK_A + message[i]) & 0xFF; + CK_B = (CK_B + CK_A) & 0xFF; + } + + // Place the calculated checksum values in the message + message[length - 2] = CK_A; + message[length - 1] = CK_B; +} + +bool GPS::getACK(uint8_t class_id, uint8_t msg_id) { uint8_t b; uint8_t ack = 0; - const uint8_t ackP[2] = {c, i}; + const uint8_t ackP[2] = {class_id, msg_id}; uint8_t buf[10] = {0xB5, 0x62, 0x05, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00}; unsigned long startTime = millis(); @@ -42,17 +57,23 @@ bool GPS::getACK(uint8_t c, uint8_t i) while (1) { if (ack > 9) { - return true; + // LOG_INFO("Got ACK for class %02X message %02X\n", class_id, msg_id); + return true; // ACK received } - if (millis() - startTime > 1000) { - return false; + if (millis() - startTime > 1500) { + LOG_WARN("No response for class %02X message %02X\n", class_id, msg_id); + return false; // No response received within 1.5 second } if (_serial_gps->available()) { b = _serial_gps->read(); if (b == buf[ack]) { ack++; } else { - ack = 0; + ack = 0; // Reset the acknowledgement counter + if (buf[3] == 0x00) { // UBX-ACK-NAK message + LOG_WARN("Got NAK for class %02X message %02X\n", class_id, msg_id); + return false; // NAK received + } } } } @@ -192,6 +213,114 @@ bool GPS::setupGPS() delay(250); } else if (gnssModel == GNSS_MODEL_UBLOX) { + uint8_t CK_A = 0, CK_B = 0; // checksum bytes + + // Configure GNSS system to GPS+SBAS+GLONASS (Module may restart after this command) + // We need set it because by default it is GPS only, and we want to use GLONASS too + // Also we need SBAS for better accuracy and extra features + // ToDo: Dynamic configure GNSS systems depending of LoRa region + byte _message_GNSS[36] = { + 0xb5, 0x62, // Sync message for UBX protocol + 0x06, 0x3e, // Message class and ID (UBX-CFG-GNSS) + 0x1c, 0x00, // Length of payload (28 bytes) + 0x00, // msgVer (0 for this version) + 0x00, // numTrkChHw (max number of hardware channels, read only, so it's always 0) + 0xff, // numTrkChUse (max number of channels to use, 0xff = max available) + 0x03, // numConfigBlocks (number of GNSS systems), most modules support maximum 3 GNSS systems + // GNSS config format: gnssId, resTrkCh, maxTrkCh, reserved1, flags + 0x00, 0x08, 0x10, 0x00, 0x01, 0x00, 0x01, 0x01, // GPS + 0x01, 0x01, 0x03, 0x00, 0x01, 0x00, 0x01, 0x01, // SBAS + 0x06, 0x08, 0x0e, 0x00, 0x01, 0x00, 0x01, 0x01, // GLONASS + 0x00, 0x00 // Checksum (to be calculated below) + }; + + // Calculate the checksum and update the message. + UBXChecksum(_message_GNSS, sizeof(_message_GNSS)); + + // Send the message to the module + _serial_gps->write(_message_GNSS, sizeof(_message_GNSS)); + + if (!getACK(0x06, 0x3e)) { + LOG_WARN("Unable to reconfigure GNSS, keep factory defaults\n"); + } else { + LOG_INFO("GNSS set to GPS+SBAS+GLONASS, waiting before sending next command (0.75s)\n"); + delay(750); + } + + // Enable interference resistance, because we are using LoRa, WiFi and Bluetoot on same board, + // and we need to reduce interference from them + byte _message_JAM[16] = { + 0xB5, 0x62, // UBX protocol sync characters + 0x06, 0x39, // Message class and ID (UBX-CFG-ITFM) + 0x08, 0x00, // Length of payload (8 bytes) + // bbThreshold (Broadband jamming detection threshold) is set to 0x3F (63 in decimal) + // cwThreshold (CW jamming detection threshold) is set to 0x10 (16 in decimal) + // algorithmBits (Reserved algorithm settings) is set to 0x16B156 as recommended + // enable (Enable interference detection) is set to 1 (enabled) + 0x3F, 0x10, 0xB1, 0x56, // config: Interference config word + // generalBits (General settings) is set to 0x31E as recommended + // antSetting (Antenna setting, 0=unknown, 1=passive, 2=active) is set to 0 (unknown) + // ToDo: Set to 1 (passive) or 2 (active) if known, for example from UBX-MON-HW, or from board info + // enable2 (Set to 1 to scan auxiliary bands, u-blox 8 / u-blox M8 only, otherwise ignored) is set to 1 (enabled) + 0x1E, 0x03, 0x00, 0x01, // config2: Extra settings for jamming/interference monitor + 0x00, 0x00 // Checksum (calculated below) + }; + + // Calculate the checksum and update the message. + UBXChecksum(_message_JAM, sizeof(_message_JAM)); + + // Send the message to the module + _serial_gps->write(_message_JAM, sizeof(_message_JAM)); + + if (!getACK(0x06, 0x39)) { + LOG_WARN("Unable to enable interference resistance.\n"); + } + + // Configure navigation engine expert settings: + byte _message_NAVX5[48] = { + 0xb5, 0x62, // UBX protocol sync characters + 0x06, 0x23, // Message class and ID (UBX-CFG-NAVX5) + 0x28, 0x00, // Length of payload (40 bytes) + 0x00, 0x00, // msgVer (0 for this version) + // minMax flag = 1: apply min/max SVs settings + // minCno flag = 1: apply minimum C/N0 setting + // initial3dfix flag = 0: apply initial 3D fix settings + // aop flag = 1: apply aopCfg (useAOP flag) settings (AssistNow Autonomous) + 0x1B, 0x00, // mask1 (First parameters bitmask) + // adr flag = 0: apply ADR sensor fusion on/off setting (useAdr flag) + // If firmware is not ADR/UDR, enabling this flag will fail configuration + // ToDo: check this with UBX-MON-VER + 0x00, 0x00, 0x00, 0x00, // mask2 (Second parameters bitmask) + 0x00, 0x00, // Reserved + 0x03, // minSVs (Minimum number of satellites for navigation) = 3 + 0x10, // maxSVs (Maximum number of satellites for navigation) = 16 + 0x06, // minCNO (Minimum satellite signal level for navigation) = 6 dBHz + 0x00, // Reserved + 0x00, // iniFix3D (Initial fix must be 3D) = 0 (disabled) + 0x00, 0x00, // Reserved + 0x00, // ackAiding (Issue acknowledgements for assistance message input) = 0 (disabled) + 0x00, 0x00, // Reserved + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Reserved + 0x00, // Reserved + 0x01, // aopCfg (AssistNow Autonomous configuration) = 1 (enabled) + 0x00, 0x00, // Reserved + 0x00, 0x00, // Reserved + 0x00, 0x00, 0x00, 0x00, // Reserved + 0x00, 0x00, 0x00, // Reserved + 0x01, // useAdr (Enable/disable ADR sensor fusion) = 1 (enabled) + 0x00, 0x00 // Checksum (calculated below) + }; + + // Calculate the checksum and update the message. + UBXChecksum(_message_NAVX5, sizeof(_message_NAVX5)); + + // Send the message to the module + _serial_gps->write(_message_NAVX5, sizeof(_message_NAVX5)); + + if (!getACK(0x06, 0x23)) { + LOG_WARN("Unable to configure extra settings.\n"); + } + /* tips: NMEA Only should not be set here, otherwise initializing Ublox gnss module again after setting will not output command messages in UART1, resulting in unrecognized module information @@ -260,6 +389,30 @@ bool GPS::setupGPS() if (!getACK(0x06, 0x01)) { LOG_WARN("Unable to enable NMEA GGA.\n"); } + + // We need save configuration to flash to make our config changes persistent + byte _message_SAVE[21] = { + 0xB5, 0x62, // UBX protocol header + 0x06, 0x09, // UBX class ID (Configuration Input Messages), message ID (UBX-CFG-CFG) + 0x0D, 0x00, // Length of payload (13 bytes) + 0x00, 0x00, 0x00, 0x00, // clearMask: no sections cleared + 0xFF, 0xFF, 0x00, 0x00, // saveMask: save all sections + 0x00, 0x00, 0x00, 0x00, // loadMask: no sections loaded + 0x0F, // deviceMask: BBR, Flash, EEPROM, and SPI Flash + 0x00, 0x00 // Checksum (calculated below) + }; + + // Calculate the checksum and update the message. + UBXChecksum(_message_SAVE, sizeof(_message_SAVE)); + + // Send the message to the module + _serial_gps->write(_message_SAVE, sizeof(_message_SAVE)); + + if (!getACK(0x06, 0x09)) { + LOG_WARN("Unable to save GNSS module configuration.\n"); + } else { + LOG_INFO("GNSS module configuration saved!\n"); + } } } @@ -688,4 +841,4 @@ GPS *createGps() } return nullptr; #endif -} +} \ No newline at end of file diff --git a/src/gps/GPS.h b/src/gps/GPS.h index a5f5f2ff4..136632741 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -139,6 +139,9 @@ class GPS : private concurrency::OSThread /// always returns 0 to indicate okay to sleep int prepareDeepSleep(void *unused); + // Calculate checksum + void UBXChecksum(byte *message, size_t length); + /** * Switch the GPS into a mode where we are actively looking for a lock, or alternatively switch GPS into a low power mode * @@ -179,4 +182,4 @@ class GPS : private concurrency::OSThread // Returns the new instance or null if the GPS is not present. GPS *createGps(); -extern GPS *gps; +extern GPS *gps; \ No newline at end of file From b9ad274104928381ebeed77694b77f0f839cf70e Mon Sep 17 00:00:00 2001 From: GUVWAF <78759985+GUVWAF@users.noreply.github.com> Date: Sun, 2 Jul 2023 23:30:28 +0200 Subject: [PATCH 02/57] Update retransmission timer based on client offset (#2583) --- src/mesh/RadioInterface.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 00af93e15..8bc66f3b5 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -196,8 +196,9 @@ uint32_t RadioInterface::getRetransmissionMsec(const meshtastic_MeshPacket *p) // LOG_DEBUG("Waiting for flooding message with airtime %d and slotTime is %d\n", packetAirtime, slotTimeMsec); float channelUtil = airTime->channelUtilizationPercent(); uint8_t CWsize = map(channelUtil, 0, 100, CWmin, CWmax); - // Assuming we pick max. of CWsize and there will be a receiver with SNR at half the range - return 2 * packetAirtime + (pow(2, CWsize) + pow(2, int((CWmax + CWmin) / 2))) * slotTimeMsec + PROCESSING_TIME_MSEC; + // Assuming we pick max. of CWsize and there will be a client with SNR at half the range + return 2 * packetAirtime + (pow(2, CWsize) + 2 * CWmax + pow(2, int((CWmax + CWmin) / 2))) * slotTimeMsec + + PROCESSING_TIME_MSEC; } /** The delay to use when we want to send something */ From 9c141919f687148262b3fc859b9ac98f9a894070 Mon Sep 17 00:00:00 2001 From: Dmitry Galenko Date: Mon, 3 Jul 2023 16:34:32 +0200 Subject: [PATCH 03/57] Initial support for MonteOps's fixed hardware platform (#2582) * Initial support for MonteOps's fixed hardware platform * Update platformio env config + cleanup * Fix platformio build * Fix platformio build * Fix wrong definition logic for NCP5623 * Fix another wrong definition logic for NCP5623, it's not board feature * Fix wrong definition logic for NCP5623 in External Notification code, it's not board feature * We need for CI magic here * Another fix related to NCP5623 * Fix cosmetic issue with redifined variable * Fix typo * Cleanup and update defs for HW1 * Fix OEM RAK4631 * Fix AQ sensor reading * Fix AQ sensor reading (better variant) * Fix build for other nRF52 devices * Replace HAS_EINK_RAK to RAK_4631 --- .github/workflows/main_matrix.yml | 1 + src/detect/ScanI2C.h | 4 +- src/detect/ScanI2CTwoWire.cpp | 7 +- src/detect/einkScan.h | 4 +- src/graphics/RAKled.h | 2 +- src/main.cpp | 20 +- src/modules/ExternalNotificationModule.cpp | 10 +- src/platform/nrf52/main-nrf52.cpp | 3 + variants/monteops_hw1/platformio.ini | 14 ++ variants/monteops_hw1/variant.cpp | 41 ++++ variants/monteops_hw1/variant.h | 240 +++++++++++++++++++++ variants/rak4631/variant.h | 5 + variants/rak4631_epaper/variant.h | 7 +- variants/rak4631_epaper_onrxtx/variant.h | 5 +- 14 files changed, 340 insertions(+), 23 deletions(-) create mode 100644 variants/monteops_hw1/platformio.ini create mode 100644 variants/monteops_hw1/variant.cpp create mode 100644 variants/monteops_hw1/variant.h diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 1cac7479b..651a9a3ee 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -97,6 +97,7 @@ jobs: include: - board: rak4631 - board: rak4631_eink + - board: monteops_hw1 - board: t-echo - board: pca10059_diy_eink - board: feather_diy diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index a56ce86fe..4b6361cfd 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -33,7 +33,9 @@ class ScanI2C PMSA0031, MPU6050, LIS3DH, +#ifdef HAS_NCP5623 NCP5623, +#endif } DeviceType; // typedef uint8_t DeviceAddress; @@ -95,4 +97,4 @@ class ScanI2C private: bool shouldSuppressScreen = false; -}; +}; \ No newline at end of file diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 7afb03ee2..7b5bb0a12 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -212,9 +212,10 @@ void ScanI2CTwoWire::scanPort(I2CPort port) } break; - SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "st7567 display found\n") + SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "st7567 display found\n"); +#ifdef HAS_NCP5623 SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623 RGB LED found\n"); - +#endif #ifdef HAS_PMU SCAN_SIMPLE_CASE(XPOWERS_AXP192_AXP2101_ADDRESS, PMU_AXP192_AXP2101, "axp192/axp2101 PMU found\n") #endif @@ -305,4 +306,4 @@ TwoWire *ScanI2CTwoWire::fetchI2CBus(ScanI2C::DeviceAddress address) const size_t ScanI2CTwoWire::countDevices() const { return foundDevices.size(); -} +} \ No newline at end of file diff --git a/src/detect/einkScan.h b/src/detect/einkScan.h index 8d82f4f81..6915709de 100644 --- a/src/detect/einkScan.h +++ b/src/detect/einkScan.h @@ -1,6 +1,6 @@ #include "../configuration.h" -#ifdef RAK4630 +#ifdef RAK_4631 #include "../main.h" #include @@ -64,4 +64,4 @@ void scanEInkDevice(void) LOG_DEBUG("EInk display not found\n"); SPI1.end(); } -#endif +#endif \ No newline at end of file diff --git a/src/graphics/RAKled.h b/src/graphics/RAKled.h index 06e2a717f..2e36b874a 100644 --- a/src/graphics/RAKled.h +++ b/src/graphics/RAKled.h @@ -1,6 +1,6 @@ #include "main.h" -#ifdef RAK4630 +#ifdef HAS_NCP5623 #include extern NCP5623 rgb; diff --git a/src/main.cpp b/src/main.cpp index c867930d0..2ff04475b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -261,10 +261,11 @@ void setup() #endif #ifdef RAK4630 +#ifdef PIN_3V3_EN // We need to enable 3.3V periphery in order to scan it pinMode(PIN_3V3_EN, OUTPUT); digitalWrite(PIN_3V3_EN, HIGH); - +#endif #ifndef USE_EINK // RAK-12039 set pin for Air quality sensor pinMode(AQ_SET_PIN, OUTPUT); @@ -352,17 +353,18 @@ void setup() pmu_found = i2cScanner->exists(ScanI2C::DeviceType::PMU_AXP192_AXP2101); - /* - * There are a bunch of sensors that have no further logic than to be found and stuffed into the - * nodeTelemetrySensorsMap singleton. This wraps that logic in a temporary scope to declare the temporary field - * "found". - */ +/* + * There are a bunch of sensors that have no further logic than to be found and stuffed into the + * nodeTelemetrySensorsMap singleton. This wraps that logic in a temporary scope to declare the temporary field + * "found". + */ - // Only one supported RGB LED currently +// Only one supported RGB LED currently +#ifdef HAS_NCP5623 rgb_found = i2cScanner->find(ScanI2C::DeviceType::NCP5623); -// Start the RGB LED at 50% -#ifdef RAK4630 + // Start the RGB LED at 50% + if (rgb_found.type == ScanI2C::NCP5623) { rgb.begin(); rgb.setCurrent(10); diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 82701cdc0..79bbb4028 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -10,7 +10,7 @@ #include "main.h" -#ifdef RAK4630 +#ifdef HAS_NCP5623 #include NCP5623 rgb; @@ -84,7 +84,7 @@ int32_t ExternalNotificationModule::runOnce() millis()) { getExternal(2) ? setExternalOff(2) : setExternalOn(2); } -#ifdef RAK4630 +#ifdef HAS_NCP5623 if (rgb_found.type == ScanI2C::NCP5623) { green = (green + 50) % 255; red = abs(red - green) % 255; @@ -127,7 +127,7 @@ void ExternalNotificationModule::setExternalOn(uint8_t index) digitalWrite(output, (moduleConfig.external_notification.active ? true : false)); break; } -#ifdef RAK4630 +#ifdef HAS_NCP5623 if (rgb_found.type == ScanI2C::NCP5623) { rgb.setColor(red, green, blue); } @@ -153,7 +153,7 @@ void ExternalNotificationModule::setExternalOff(uint8_t index) break; } -#ifdef RAK4630 +#ifdef HAS_NCP5623 if (rgb_found.type == ScanI2C::NCP5623) { red = 0; green = 0; @@ -235,7 +235,7 @@ ExternalNotificationModule::ExternalNotificationModule() LOG_INFO("Using Pin %i in PWM mode\n", config.device.buzzer_gpio); } } -#ifdef RAK4630 +#ifdef HAS_NCP5623 if (rgb_found.type == ScanI2C::NCP5623) { rgb.begin(); rgb.setCurrent(10); diff --git a/src/platform/nrf52/main-nrf52.cpp b/src/platform/nrf52/main-nrf52.cpp index c630aa13b..fd6fe2cc2 100644 --- a/src/platform/nrf52/main-nrf52.cpp +++ b/src/platform/nrf52/main-nrf52.cpp @@ -170,8 +170,11 @@ void cpuDeepSleep(uint32_t msecToWake) Serial1.end(); #endif setBluetoothEnable(false); + #ifdef RAK4630 +#ifdef PIN_3V3_EN digitalWrite(PIN_3V3_EN, LOW); +#endif #ifndef USE_EINK // RAK-12039 set pin for Air quality sensor digitalWrite(AQ_SET_PIN, LOW); diff --git a/variants/monteops_hw1/platformio.ini b/variants/monteops_hw1/platformio.ini new file mode 100644 index 000000000..f9d260e74 --- /dev/null +++ b/variants/monteops_hw1/platformio.ini @@ -0,0 +1,14 @@ +; MonteOps M.Node/M.Backbone/M.Eagle hardware based on hardware variant #1 (RAK4630 based) +[env:monteops_hw1] +extends = nrf52840_base +board = wiscore_rak4631 +build_flags = ${nrf52840_base.build_flags} -Ivariants/monteops_hw1 -D MONTEOPS_HW1 + -L "${platformio.libdeps_dir}/${this.__env__}/BSEC2 Software Library/src/cortex-m4/fpv4-sp-d16-hard" +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/monteops_hw1> + + + +lib_deps = + ${nrf52840_base.lib_deps} + ${networking_base.lib_deps} + https://github.com/RAKWireless/RAK13800-W5100S.git#1.0.2 +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink \ No newline at end of file diff --git a/variants/monteops_hw1/variant.cpp b/variants/monteops_hw1/variant.cpp new file mode 100644 index 000000000..75cca1dc3 --- /dev/null +++ b/variants/monteops_hw1/variant.cpp @@ -0,0 +1,41 @@ +/* + 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 "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 + 0, 1, 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() +{ + // LED1 & LED2 + pinMode(PIN_LED1, OUTPUT); + ledOff(PIN_LED1); + + pinMode(PIN_LED2, OUTPUT); + ledOff(PIN_LED2); +} diff --git a/variants/monteops_hw1/variant.h b/variants/monteops_hw1/variant.h new file mode 100644 index 000000000..866ddf471 --- /dev/null +++ b/variants/monteops_hw1/variant.h @@ -0,0 +1,240 @@ +/* + 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_MOPS_HW1_ +#define _VARIANT_MOPS_HW1_ + +#define RAK4630 + +// MonteOps hardware design variant +#ifndef MONTEOPS_HW1 +#define MONTEOPS_HW1 +#endif + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +// define USE_LFRC // Board uses RC 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 (6) +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs +#define PIN_LED1 (35) +#define PIN_LED2 (36) // Connected to WWAN host LED (if present) + +#define LED_BUILTIN PIN_LED1 +#define LED_CONN PIN_LED2 + +#define LED_GREEN PIN_LED1 +#define LED_BLUE PIN_LED2 + +#define LED_STATE_ON 1 // State when LED is litted + +/* + * Buttons + */ + +//#define PIN_BUTTON1 9 // Pin for button on E-ink button module or IO expansion +#define BUTTON_NEED_PULLUP +#define PIN_BUTTON2 12 +#define PIN_BUTTON3 24 +#define PIN_BUTTON4 25 + +/* + * Analog pins + */ +#define PIN_A0 (5) +#define PIN_A1 (31) +#define PIN_A2 (28) +#define PIN_A3 (29) +#define PIN_A4 (30) +#define PIN_A5 (31) +#define PIN_A6 (0xff) +#define PIN_A7 (0xff) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +static const uint8_t A6 = PIN_A6; +static const uint8_t A7 = PIN_A7; +#define ADC_RESOLUTION 14 + +// Other pins +#define PIN_AREF (2) +#define PIN_NFC1 (9) +#define PIN_NFC2 (10) + +static const uint8_t AREF = PIN_AREF; + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (15) +#define PIN_SERIAL1_TX (16) + +// Connected to Jlink CDC +#define PIN_SERIAL2_RX (8) +#define PIN_SERIAL2_TX (6) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 2 + +#define PIN_SPI_MISO (45) +#define PIN_SPI_MOSI (44) +#define PIN_SPI_SCK (43) + +#define PIN_SPI1_MISO (29) // (0 + 29) +#define PIN_SPI1_MOSI (30) // (0 + 30) +#define PIN_SPI1_SCK (3) // (0 + 3) + +static const uint8_t SS = 42; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (13) +#define PIN_WIRE_SCL (14) + +// QSPI Pins +#define PIN_QSPI_SCK 3 +#define PIN_QSPI_CS 26 +#define PIN_QSPI_IO0 30 +#define PIN_QSPI_IO1 29 +#define PIN_QSPI_IO2 28 +#define PIN_QSPI_IO3 2 + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES IS25LP080D +#define EXTERNAL_FLASH_USE_QSPI + +/* @note RAK5005-O GPIO mapping to RAK4631 GPIO ports + RAK5005-O <-> nRF52840 + IO1 <-> P0.17 (Arduino GPIO number 17) + IO2 <-> P1.02 (Arduino GPIO number 34) + IO3 <-> P0.21 (Arduino GPIO number 21) + IO4 <-> P0.04 (Arduino GPIO number 4) + IO5 <-> P0.09 (Arduino GPIO number 9) + IO6 <-> P0.10 (Arduino GPIO number 10) + IO7 <-> P0.28 (Arduino GPIO number 28) + SW1 <-> P0.01 (Arduino GPIO number 1) + A0 <-> P0.04/AIN2 (Arduino Analog A2) + A1 <-> P0.31/AIN7 (Arduino Analog A7) + SPI_CS <-> P0.26 (Arduino GPIO number 26) + */ + +// RAK4630 LoRa module + +/* Setup of the SX1262 LoRa module ( https://docs.rakwireless.com/Product-Categories/WisBlock/RAK4631/Datasheet/ ) + +P1.10 NSS SPI NSS (Arduino GPIO number 42) +P1.11 SCK SPI CLK (Arduino GPIO number 43) +P1.12 MOSI SPI MOSI (Arduino GPIO number 44) +P1.13 MISO SPI MISO (Arduino GPIO number 45) +P1.14 BUSY BUSY signal (Arduino GPIO number 46) +P1.15 DIO1 DIO1 event interrupt (Arduino GPIO number 47) +P1.06 NRESET NRESET manual reset of the SX1262 (Arduino GPIO number 38) + +Important for successful SX1262 initialization: + +* Setup DIO2 to control the antenna switch +* Setup DIO3 to control the TCXO power supply +* Setup the SX1262 to use it's DCDC regulator and not the LDO +* RAK4630 schematics show GPIO P1.07 connected to the antenna switch, but it should not be initialized, as DIO2 will do the +control of the antenna switch + +SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG + +*/ + +#define USE_SX1262 +#define SX126X_CS (42) +#define SX126X_DIO1 (47) +#define SX126X_BUSY (46) +#define SX126X_RESET (38) +// #define SX126X_TXEN (39) +// #define SX126X_RXEN (37) +#define SX126X_POWER_EN (37) +#define SX126X_E22 // DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 + +#define PIN_GPS_RESET (34) // Must be P1.02 +// #define PIN_GPS_EN +// #define PIN_GPS_PPS (17) // Pulse per second input from the GPS + +#define GPS_RX_PIN PIN_SERIAL1_RX +#define GPS_TX_PIN PIN_SERIAL1_TX + +// Battery +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_A0 +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +// Definition of milliVolt per LSB => 3.0V ADC range and 12-bit ADC resolution = 3000mV/4096 +#define VBAT_MV_PER_LSB (0.73242188F) +// Voltage divider value => 1.5M + 1M voltage divider on VBAT = (1.5M / (1M + 1.5M)) +#define VBAT_DIVIDER (0.4F) +// Compensation factor for the VBAT divider +#define VBAT_DIVIDER_COMP (1.73) +// Fixed calculation of milliVolt from compensation value +#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER VBAT_DIVIDER_COMP // REAL_VBAT_MV_PER_LSB +#define VBAT_RAW_TO_SCALED(x) (REAL_VBAT_MV_PER_LSB * x) + +//#define HAS_RTC 1 + +#define HAS_ETHERNET 1 + +#define PIN_ETHERNET_RESET 21 +#define PIN_ETHERNET_SS 26 // P0.26 QSPI_CS +#define ETH_SPI_PORT SPI1 +#define AQ_SET_PIN 10 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/rak4631/variant.h b/variants/rak4631/variant.h index fe9f062c8..258e4eb3c 100644 --- a/variants/rak4631/variant.h +++ b/variants/rak4631/variant.h @@ -139,6 +139,9 @@ static const uint8_t SCK = PIN_SPI_SCK; // #define USE_EINK +// RAKRGB +#define HAS_NCP5623 + /* * Wire Interfaces */ @@ -255,6 +258,8 @@ SO GPIO 39/TXEN MAY NOT BE DEFINED FOR SUCCESSFUL OPERATION OF THE SX1262 - TG #define HAS_ETHERNET 1 +#define RAK_4631 1 + #define PIN_ETHERNET_RESET 21 #define PIN_ETHERNET_SS PIN_EINK_CS #define ETH_SPI_PORT SPI1 diff --git a/variants/rak4631_epaper/variant.h b/variants/rak4631_epaper/variant.h index a43229088..ad3e4b87f 100644 --- a/variants/rak4631_epaper/variant.h +++ b/variants/rak4631_epaper/variant.h @@ -139,6 +139,9 @@ static const uint8_t SCK = PIN_SPI_SCK; #define USE_EINK +// RAKRGB +#define HAS_NCP5623 + /* * Wire Interfaces */ @@ -226,6 +229,8 @@ static const uint8_t SCK = PIN_SPI_SCK; #define HAS_RTC 1 +#define RAK_4631 1 + #ifdef __cplusplus } #endif @@ -234,4 +239,4 @@ static const uint8_t SCK = PIN_SPI_SCK; * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif +#endif \ No newline at end of file diff --git a/variants/rak4631_epaper_onrxtx/variant.h b/variants/rak4631_epaper_onrxtx/variant.h index 44db0b7f0..e4d7c7d45 100644 --- a/variants/rak4631_epaper_onrxtx/variant.h +++ b/variants/rak4631_epaper_onrxtx/variant.h @@ -120,6 +120,9 @@ static const uint8_t SCK = PIN_SPI_SCK; // FIXME - I think this is actually just the board power enable - it enables power to the CPU also // #define PIN_EINK_PWR_ON (-1) +// RAKRGB +#define HAS_NCP5623 + /* * Wire Interfaces */ @@ -207,4 +210,4 @@ static const uint8_t SCK = PIN_SPI_SCK; * Arduino objects - C++ only *----------------------------------------------------------------------------*/ -#endif +#endif \ No newline at end of file From 5c34e36bec240015d7a13373ec8c5e60aa549462 Mon Sep 17 00:00:00 2001 From: prokrypt Date: Thu, 6 Jul 2023 04:43:21 -0700 Subject: [PATCH 04/57] Temporary band-aid to address mesh [un]reliability after queue "fix" (#2588) --- src/mesh/FloodingRouter.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/mesh/FloodingRouter.cpp b/src/mesh/FloodingRouter.cpp index 171199277..83bd0e325 100644 --- a/src/mesh/FloodingRouter.cpp +++ b/src/mesh/FloodingRouter.cpp @@ -21,7 +21,12 @@ bool FloodingRouter::shouldFilterReceived(const meshtastic_MeshPacket *p) { if (wasSeenRecently(p)) { // Note: this will also add a recent packet record printPacket("Ignoring incoming msg, because we've already seen it", p); - Router::cancelSending(p->from, p->id); // cancel rebroadcast of this message *if* there was already one + if (config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER && + config.device.role != meshtastic_Config_DeviceConfig_Role_ROUTER_CLIENT && + config.device.role != meshtastic_Config_DeviceConfig_Role_REPEATER) { + // cancel rebroadcast of this message *if* there was already one, unless we're a router/repeater! + Router::cancelSending(p->from, p->id); + } return true; } From 97606cd3823117e9b1429a71b06609e92de67296 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 7 Jul 2023 18:58:49 -0500 Subject: [PATCH 05/57] New platform updates (#2593) --- arch/esp32/esp32.ini | 2 +- arch/nrf52/nrf52.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/arch/esp32/esp32.ini b/arch/esp32/esp32.ini index 62a943ece..74d693871 100644 --- a/arch/esp32/esp32.ini +++ b/arch/esp32/esp32.ini @@ -1,7 +1,7 @@ ; Common settings for ESP targes, mixin with extends = esp32_base [esp32_base] extends = arduino_base -platform = platformio/espressif32@^6.2.0 +platform = platformio/espressif32@^6.3.2 build_src_filter = ${arduino_base.build_src_filter} - - - - diff --git a/arch/nrf52/nrf52.ini b/arch/nrf52/nrf52.ini index f44054f24..ec290a30e 100644 --- a/arch/nrf52/nrf52.ini +++ b/arch/nrf52/nrf52.ini @@ -1,6 +1,6 @@ [nrf52_base] ; Instead of the standard nordicnrf52 platform, we use our fork which has our added variant files -platform = platformio/nordicnrf52@^9.6.0 +platform = platformio/nordicnrf52@^10.0.0 extends = arduino_base build_type = debug ; I'm debugging with ICE a lot now From d8ad2b3f48776462e9afe6fc944d6ae3c6fb9223 Mon Sep 17 00:00:00 2001 From: GUVWAF <78759985+GUVWAF@users.noreply.github.com> Date: Sat, 8 Jul 2023 18:32:36 +0200 Subject: [PATCH 06/57] RPi Pico screen, CannedMessageModule (CardKB) and reboot support (#2595) * Make input_source case insensitive * Implement reboot for RP2040 * Remove EXT_NOTFIFY_OUT as it conflicts with I2C and module is not supported * RP2040 has screen, button and wire * Add default I2C pins also for Pico W --- src/modules/CannedMessageModule.cpp | 4 ++-- src/platform/rp2040/architecture.h | 9 +++++++++ src/shutdown.h | 4 +++- variants/rpipico/variant.h | 6 +++--- variants/rpipicow/variant.h | 6 +++--- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index bbd39f696..1119e0c27 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -123,8 +123,8 @@ int CannedMessageModule::splitConfiguredMessages() int CannedMessageModule::handleInputEvent(const InputEvent *event) { if ((strlen(moduleConfig.canned_message.allow_input_source) > 0) && - (strcmp(moduleConfig.canned_message.allow_input_source, event->source) != 0) && - (strcmp(moduleConfig.canned_message.allow_input_source, "_any") != 0)) { + (strcasecmp(moduleConfig.canned_message.allow_input_source, event->source) != 0) && + (strcasecmp(moduleConfig.canned_message.allow_input_source, "_any") != 0)) { // Event source is not accepted. // Event only accepted if source matches the configured one, or // the configured one is "_any" (or if there is no configured diff --git a/src/platform/rp2040/architecture.h b/src/platform/rp2040/architecture.h index 772e4b1af..762a2dc83 100644 --- a/src/platform/rp2040/architecture.h +++ b/src/platform/rp2040/architecture.h @@ -2,9 +2,18 @@ #define ARCH_RP2040 +#ifndef HAS_BUTTON +#define HAS_BUTTON 1 +#endif #ifndef HAS_TELEMETRY #define HAS_TELEMETRY 1 #endif +#ifndef HAS_SCREEN +#define HAS_SCREEN 1 +#endif +#ifndef HAS_WIRE +#define HAS_WIRE 1 +#endif #ifndef HAS_SENSOR #define HAS_SENSOR 1 #endif diff --git a/src/shutdown.h b/src/shutdown.h index ee63422dd..d9077151c 100644 --- a/src/shutdown.h +++ b/src/shutdown.h @@ -12,6 +12,8 @@ void powerCommandsCheck() ESP.restart(); #elif defined(ARCH_NRF52) NVIC_SystemReset(); +#elif defined(ARCH_RP2040) + rp2040.reboot(); #else rebootAtMsec = -1; LOG_WARN("FIXME implement reboot for this platform. Skipping for now.\n"); @@ -33,4 +35,4 @@ void powerCommandsCheck() LOG_WARN("FIXME implement shutdown for this platform"); #endif } -} +} \ No newline at end of file diff --git a/variants/rpipico/variant.h b/variants/rpipico/variant.h index 7e2660aa6..fb4b9bd75 100644 --- a/variants/rpipico/variant.h +++ b/variants/rpipico/variant.h @@ -15,11 +15,11 @@ #define USE_SH1106 1 #undef GPS_SERIAL_NUM -// #define I2C_SDA 6 -// #define I2C_SCL 7 +// default I2C pins: +// SDA = 4 +// SCL = 5 #define BUTTON_PIN 17 -#define EXT_NOTIFY_OUT 4 #define LED_PIN PIN_LED diff --git a/variants/rpipicow/variant.h b/variants/rpipicow/variant.h index 4741fd130..59f8d2ec2 100644 --- a/variants/rpipicow/variant.h +++ b/variants/rpipicow/variant.h @@ -15,11 +15,11 @@ #define USE_SH1106 1 #undef GPS_SERIAL_NUM -// #define I2C_SDA 6 -// #define I2C_SCL 7 +// default I2C pins: +// SDA = 4 +// SCL = 5 #define BUTTON_PIN 17 -#define EXT_NOTIFY_OUT 4 #define BATTERY_PIN 26 // ratio of voltage divider = 3.0 (R17=200k, R18=100k) From da389eb787dd226b560eda38381e408095c313f1 Mon Sep 17 00:00:00 2001 From: Max-Plastix <66535617+Max-Plastix@users.noreply.github.com> Date: Sat, 8 Jul 2023 16:30:52 -0700 Subject: [PATCH 07/57] Correct unused variable warning and typo around GNSS_MODEL_UNKNOWN (#2596) * Small warning and typo cleanup. * Update GPS.cpp (missed one instance of GNSS_MODEL_UNKONW) --- src/gps/GPS.cpp | 7 ++----- src/gps/GPS.h | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 13c46d62e..566d90a5c 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -212,9 +212,6 @@ bool GPS::setupGPS() _serial_gps->write("$PCAS11,3*1E\r\n"); delay(250); } else if (gnssModel == GNSS_MODEL_UBLOX) { - - uint8_t CK_A = 0, CK_B = 0; // checksum bytes - // Configure GNSS system to GPS+SBAS+GLONASS (Module may restart after this command) // We need set it because by default it is GPS only, and we want to use GLONASS too // Also we need SBAS for better accuracy and extra features @@ -752,7 +749,7 @@ GnssModel_t GPS::probe() // Check that the returned response class and message ID are correct if (!getAck(buffer, 256, 0x06, 0x08)) { LOG_WARN("Failed to find UBlox & MTK GNSS Module\n"); - return GNSS_MODEL_UNKONW; + return GNSS_MODEL_UNKNOWN; } // Get Ublox gnss module hardware and software info @@ -841,4 +838,4 @@ GPS *createGps() } return nullptr; #endif -} \ No newline at end of file +} diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 136632741..32f309789 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -14,7 +14,7 @@ struct uBloxGnssModelInfo { typedef enum { GNSS_MODEL_MTK, GNSS_MODEL_UBLOX, - GNSS_MODEL_UNKONW, + GNSS_MODEL_UNKNOWN, } GnssModel_t; // Generate a string representation of DOP @@ -175,7 +175,7 @@ class GPS : private concurrency::OSThread uint8_t fixeddelayCtr = 0; protected: - GnssModel_t gnssModel = GNSS_MODEL_UNKONW; + GnssModel_t gnssModel = GNSS_MODEL_UNKNOWN; }; // Creates an instance of the GPS class. From 6e96216ba34e8eeebaad35e2fc37c1e230af4130 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 8 Jul 2023 20:37:04 -0500 Subject: [PATCH 08/57] MQTT client proxying (#2587) * WIP on MQTT proxy message queue * Fix copy paste goof * Progress on uplink * Has packets * Avoid trying to connect if we're proxying * Pointer correctly * Remove wifi guards * Client proxy subscribe * Fixed method that got bababababorked somehow... personally I blame CoPilot * Short circuit logic * Remove canned settings * Missed some stuff in the move * Guard pubsub client for non-networked variants * Has networking guard * else * Return statement for fall-thru * More gaurd removals * Removed source filters. No wonder I was confused * Bounding * Scope guard around else and fix return * Portduino * Defs instead * Move macro up to actually fix portduino * Size_t * Unsigned int * Thread interval * Protos * Protobufs ref --- arch/nrf52/nrf52.ini | 2 +- arch/rp2040/rp2040.ini | 2 +- arch/stm32/stm32wl5e.ini | 2 +- protobufs | 2 +- src/main.cpp | 5 +- src/mesh/MeshService.cpp | 21 +- src/mesh/MeshService.h | 13 ++ src/mesh/PhoneAPI.cpp | 27 ++- src/mesh/PhoneAPI.h | 5 + src/mesh/Router.cpp | 6 - src/mesh/eth/ethClient.cpp | 4 +- src/mesh/generated/meshtastic/deviceonly.pb.h | 2 +- src/mesh/generated/meshtastic/localonly.pb.h | 2 +- src/mesh/generated/meshtastic/mesh.pb.c | 3 + src/mesh/generated/meshtastic/mesh.pb.h | 52 ++++- .../generated/meshtastic/module_config.pb.c | 3 + .../generated/meshtastic/module_config.pb.h | 55 ++++- src/mesh/generated/meshtastic/portnums.pb.h | 2 + src/modules/AdminModule.cpp | 6 +- src/mqtt/MQTT.cpp | 199 +++++++++++++----- src/mqtt/MQTT.h | 29 ++- 21 files changed, 339 insertions(+), 103 deletions(-) diff --git a/arch/nrf52/nrf52.ini b/arch/nrf52/nrf52.ini index ec290a30e..858dcdc9c 100644 --- a/arch/nrf52/nrf52.ini +++ b/arch/nrf52/nrf52.ini @@ -9,7 +9,7 @@ build_flags = -Isrc/platform/nrf52 build_src_filter = - ${arduino_base.build_src_filter} - - - - - - - - - + ${arduino_base.build_src_filter} - - - - - - - - lib_deps= ${arduino_base.lib_deps} diff --git a/arch/rp2040/rp2040.ini b/arch/rp2040/rp2040.ini index 52fba9cba..b6ac4f171 100644 --- a/arch/rp2040/rp2040.ini +++ b/arch/rp2040/rp2040.ini @@ -12,7 +12,7 @@ build_flags = -D__PLAT_RP2040__ # -D _POSIX_THREADS build_src_filter = - ${arduino_base.build_src_filter} - - - - - - - - - + ${arduino_base.build_src_filter} - - - - - - - - lib_ignore = BluetoothOTA diff --git a/arch/stm32/stm32wl5e.ini b/arch/stm32/stm32wl5e.ini index 819ecc31c..524edd6b9 100644 --- a/arch/stm32/stm32wl5e.ini +++ b/arch/stm32/stm32wl5e.ini @@ -13,7 +13,7 @@ build_flags = -DVECT_TAB_OFFSET=0x08000000 build_src_filter = - ${arduino_base.build_src_filter} - - - - - - - - - - - - - + ${arduino_base.build_src_filter} - - - - - - - - - - - - board_upload.offset_address = 0x08000000 upload_protocol = stlink diff --git a/protobufs b/protobufs index e4396fd49..f2d1ebbd3 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit e4396fd499769f24c265985ae0ee7be05c18f65a +Subproject commit f2d1ebbd3485f6e4814608da0cfc7a82d97305f1 diff --git a/src/main.cpp b/src/main.cpp index 2ff04475b..c7cbb7680 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -47,13 +47,12 @@ NRF52Bluetooth *nrf52Bluetooth; #if HAS_WIFI #include "mesh/api/WiFiServerAPI.h" -#include "mqtt/MQTT.h" #endif #if HAS_ETHERNET #include "mesh/api/ethServerAPI.h" -#include "mqtt/MQTT.h" #endif +#include "mqtt/MQTT.h" #include "LLCC68Interface.h" #include "RF95Interface.h" @@ -656,9 +655,7 @@ void setup() } } -#if HAS_WIFI || HAS_ETHERNET mqttInit(); -#endif #ifndef ARCH_PORTDUINO // Initialize Wifi diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 2ad46a6b7..64741619f 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -52,13 +52,18 @@ FIXME in the initial proof of concept we just skip the entire want/deny flow and MeshService service; +static MemoryDynamic staticMqttClientProxyMessagePool; + static MemoryDynamic staticQueueStatusPool; +Allocator &mqttClientProxyMessagePool = staticMqttClientProxyMessagePool; + Allocator &queueStatusPool = staticQueueStatusPool; #include "Router.h" -MeshService::MeshService() : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_TOPHONE) +MeshService::MeshService() + : toPhoneQueue(MAX_RX_TOPHONE), toPhoneQueueStatusQueue(MAX_RX_TOPHONE), toPhoneMqttProxyQueue(MAX_RX_TOPHONE) { lastQueueStatus = {0, 0, 16, 0}; } @@ -269,6 +274,20 @@ void MeshService::sendToPhone(meshtastic_MeshPacket *p) fromNum++; } +void MeshService::sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m) +{ + LOG_DEBUG("Sending mqtt message on topic '%s' to client for proxying to server\n", m->topic); + if (toPhoneMqttProxyQueue.numFree() == 0) { + LOG_WARN("MqttClientProxyMessagePool queue is full, discarding oldest\n"); + meshtastic_MqttClientProxyMessage *d = toPhoneMqttProxyQueue.dequeuePtr(0); + if (d) + releaseMqttClientProxyMessageToPool(d); + } + + assert(toPhoneMqttProxyQueue.enqueue(m, 0)); + fromNum++; +} + meshtastic_NodeInfoLite *MeshService::refreshLocalMeshNode() { meshtastic_NodeInfoLite *node = nodeDB.getMeshNode(nodeDB.getNodeNum()); diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h index d14db7139..3cc197a5a 100644 --- a/src/mesh/MeshService.h +++ b/src/mesh/MeshService.h @@ -15,6 +15,7 @@ #endif extern Allocator &queueStatusPool; +extern Allocator &mqttClientProxyMessagePool; /** * Top level app for this service. keeps the mesh, the radio config and the queue of received packets. @@ -34,6 +35,9 @@ class MeshService // keep list of QueueStatus packets to be send to the phone PointerQueue toPhoneQueueStatusQueue; + // keep list of MqttClientProxyMessages to be send to the client for delivery + PointerQueue toPhoneMqttProxyQueue; + // This holds the last QueueStatus send meshtastic_QueueStatus lastQueueStatus; @@ -67,9 +71,15 @@ class MeshService /// Return the next QueueStatus packet destined to the phone. meshtastic_QueueStatus *getQueueStatusForPhone() { return toPhoneQueueStatusQueue.dequeuePtr(0); } + /// Return the next MqttClientProxyMessage packet destined to the phone. + meshtastic_MqttClientProxyMessage *getMqttClientProxyMessageForPhone() { return toPhoneMqttProxyQueue.dequeuePtr(0); } + // Release QueueStatus packet to pool void releaseQueueStatusToPool(meshtastic_QueueStatus *p) { queueStatusPool.release(p); } + // Release MqttClientProxyMessage packet to pool + void releaseMqttClientProxyMessageToPool(meshtastic_MqttClientProxyMessage *p) { mqttClientProxyMessagePool.release(p); } + /** * Given a ToRadio buffer parse it and properly handle it (setup radio, owner or send packet into the mesh) * Called by PhoneAPI.handleToRadio. Note: p is a scratch buffer, this function is allowed to write to it but it can not keep @@ -103,6 +113,9 @@ class MeshService /// Send a packet to the phone void sendToPhone(meshtastic_MeshPacket *p); + /// Send an MQTT message to the phone for client proxying + void sendMqttMessageToClientProxy(meshtastic_MqttClientProxyMessage *m); + bool isToPhoneQueueEmpty(); private: diff --git a/src/mesh/PhoneAPI.cpp b/src/mesh/PhoneAPI.cpp index ebc886301..6c6c70165 100644 --- a/src/mesh/PhoneAPI.cpp +++ b/src/mesh/PhoneAPI.cpp @@ -18,6 +18,8 @@ #error ToRadio is too big #endif +#include "mqtt/MQTT.h" + PhoneAPI::PhoneAPI() { lastContactMsec = millis(); @@ -54,6 +56,7 @@ void PhoneAPI::close() unobserve(&xModem.packetReady); releasePhonePacket(); // Don't leak phone packets on shutdown releaseQueueStatusPhonePacket(); + releaseMqttClientProxyPhonePacket(); onConnectionChanged(false); } @@ -98,6 +101,12 @@ bool PhoneAPI::handleToRadio(const uint8_t *buf, size_t bufLength) LOG_INFO("Got xmodem packet\n"); xModem.handlePacket(toRadioScratch.xmodemPacket); break; + case meshtastic_ToRadio_mqttClientProxyMessage_tag: + LOG_INFO("Got MqttClientProxy message\n"); + if (mqtt && moduleConfig.mqtt.proxy_to_client_enabled) { + mqtt->onClientProxyReceive(toRadioScratch.mqttClientProxyMessage); + } + break; default: // Ignore nop messages // LOG_DEBUG("Error: unexpected ToRadio variant\n"); @@ -295,12 +304,16 @@ size_t PhoneAPI::getFromRadio(uint8_t *buf) break; case STATE_SEND_PACKETS: - // Do we have a message from the mesh? + // Do we have a message from the mesh or packet from the local device? LOG_INFO("getFromRadio=STATE_SEND_PACKETS\n"); if (queueStatusPacketForPhone) { fromRadioScratch.which_payload_variant = meshtastic_FromRadio_queueStatus_tag; fromRadioScratch.queueStatus = *queueStatusPacketForPhone; releaseQueueStatusPhonePacket(); + } else if (mqttClientProxyMessageForPhone) { + fromRadioScratch.which_payload_variant = meshtastic_FromRadio_mqttClientProxyMessage_tag; + fromRadioScratch.mqttClientProxyMessage = *mqttClientProxyMessageForPhone; + releaseMqttClientProxyPhonePacket(); } else if (xmodemPacketForPhone.control != meshtastic_XModem_Control_NUL) { fromRadioScratch.which_payload_variant = meshtastic_FromRadio_xmodemPacket_tag; fromRadioScratch.xmodemPacket = xmodemPacketForPhone; @@ -353,6 +366,14 @@ void PhoneAPI::releaseQueueStatusPhonePacket() } } +void PhoneAPI::releaseMqttClientProxyPhonePacket() +{ + if (mqttClientProxyMessageForPhone) { + service.releaseMqttClientProxyMessageToPool(mqttClientProxyMessageForPhone); + mqttClientProxyMessageForPhone = NULL; + } +} + /** * Return true if we have data available to send to the phone */ @@ -381,7 +402,9 @@ bool PhoneAPI::available() case STATE_SEND_PACKETS: { if (!queueStatusPacketForPhone) queueStatusPacketForPhone = service.getQueueStatusForPhone(); - bool hasPacket = !!queueStatusPacketForPhone; + if (!mqttClientProxyMessageForPhone) + mqttClientProxyMessageForPhone = service.getMqttClientProxyMessageForPhone(); + bool hasPacket = !!queueStatusPacketForPhone || !!mqttClientProxyMessageForPhone; if (hasPacket) return true; diff --git a/src/mesh/PhoneAPI.h b/src/mesh/PhoneAPI.h index 8097ad34b..65a06bc6b 100644 --- a/src/mesh/PhoneAPI.h +++ b/src/mesh/PhoneAPI.h @@ -50,6 +50,9 @@ class PhoneAPI // Keep QueueStatus packet just as packetForPhone meshtastic_QueueStatus *queueStatusPacketForPhone = NULL; + // Keep MqttClientProxyMessage packet just as packetForPhone + meshtastic_MqttClientProxyMessage *mqttClientProxyMessageForPhone = NULL; + /// We temporarily keep the nodeInfo here between the call to available and getFromRadio meshtastic_NodeInfo nodeInfoForPhone = meshtastic_NodeInfo_init_default; @@ -126,6 +129,8 @@ class PhoneAPI void releaseQueueStatusPhonePacket(); + void releaseMqttClientProxyPhonePacket(); + /// begin a new connection void handleStartConfig(); diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 6f020d739..294b2531f 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -12,9 +12,7 @@ extern "C" { #include "mesh/compression/unishox2.h" } -#if HAS_WIFI || HAS_ETHERNET #include "mqtt/MQTT.h" -#endif /** * Router todo @@ -248,7 +246,6 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) bool shouldActuallyEncrypt = true; -#if HAS_WIFI || HAS_ETHERNET if (moduleConfig.mqtt.enabled) { // check if we should send decrypted packets to mqtt @@ -272,7 +269,6 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) if (mqtt && !shouldActuallyEncrypt) mqtt->onSend(*p, chIndex); } -#endif auto encodeResult = perhapsEncode(p); if (encodeResult != meshtastic_Routing_Error_NONE) { @@ -280,14 +276,12 @@ ErrorCode Router::send(meshtastic_MeshPacket *p) return encodeResult; // FIXME - this isn't a valid ErrorCode } -#if HAS_WIFI || HAS_ETHERNET if (moduleConfig.mqtt.enabled) { // the packet is now encrypted. // check if we should send encrypted packets to mqtt if (mqtt && shouldActuallyEncrypt) mqtt->onSend(*p, chIndex); } -#endif } assert(iface); // This should have been detected already in sendLocal (or we just received a packet from outside) diff --git a/src/mesh/eth/ethClient.cpp b/src/mesh/eth/ethClient.cpp index c60e35394..f10c96866 100644 --- a/src/mesh/eth/ethClient.cpp +++ b/src/mesh/eth/ethClient.cpp @@ -68,7 +68,7 @@ static int32_t reconnectETH() } // FIXME this is kinda yucky, instead we should just have an observable for 'wifireconnected' - if (mqtt && !mqtt->connected()) { + if (mqtt && !moduleConfig.mqtt.proxy_to_client_enabled && !mqtt->isConnectedDirectly()) { mqtt->reconnect(); } } @@ -87,7 +87,6 @@ static int32_t reconnectETH() perhapsSetRTC(RTCQualityNTP, &tv); ntp_renew = millis() + 43200 * 1000; // success, refresh every 12 hours - } else { LOG_ERROR("NTP Update failed\n"); ntp_renew = millis() + 300 * 1000; // failure, retry every 5 minutes @@ -170,7 +169,6 @@ bool initEthernet() ethEvent = new Periodic("ethConnect", reconnectETH); return true; - } else { LOG_INFO("Not using Ethernet\n"); return false; diff --git a/src/mesh/generated/meshtastic/deviceonly.pb.h b/src/mesh/generated/meshtastic/deviceonly.pb.h index d0c3b7bd8..a093c9fe2 100644 --- a/src/mesh/generated/meshtastic/deviceonly.pb.h +++ b/src/mesh/generated/meshtastic/deviceonly.pb.h @@ -323,7 +323,7 @@ extern const pb_msgdesc_t meshtastic_NodeRemoteHardwarePin_msg; #define meshtastic_DeviceState_size 35056 #define meshtastic_NodeInfoLite_size 151 #define meshtastic_NodeRemoteHardwarePin_size 29 -#define meshtastic_OEMStore_size 3152 +#define meshtastic_OEMStore_size 3154 #define meshtastic_PositionLite_size 28 #ifdef __cplusplus diff --git a/src/mesh/generated/meshtastic/localonly.pb.h b/src/mesh/generated/meshtastic/localonly.pb.h index d70acc7fc..b7199a001 100644 --- a/src/mesh/generated/meshtastic/localonly.pb.h +++ b/src/mesh/generated/meshtastic/localonly.pb.h @@ -163,7 +163,7 @@ extern const pb_msgdesc_t meshtastic_LocalModuleConfig_msg; /* Maximum encoded size of messages (where known) */ #define meshtastic_LocalConfig_size 461 -#define meshtastic_LocalModuleConfig_size 545 +#define meshtastic_LocalModuleConfig_size 547 #ifdef __cplusplus } /* extern "C" */ diff --git a/src/mesh/generated/meshtastic/mesh.pb.c b/src/mesh/generated/meshtastic/mesh.pb.c index ce7d48b14..790f8be2d 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.c +++ b/src/mesh/generated/meshtastic/mesh.pb.c @@ -24,6 +24,9 @@ PB_BIND(meshtastic_Data, meshtastic_Data, 2) PB_BIND(meshtastic_Waypoint, meshtastic_Waypoint, AUTO) +PB_BIND(meshtastic_MqttClientProxyMessage, meshtastic_MqttClientProxyMessage, 2) + + PB_BIND(meshtastic_MeshPacket, meshtastic_MeshPacket, 2) diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 0ed2f8e68..3d4c41cb2 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -462,6 +462,22 @@ typedef struct _meshtastic_Waypoint { uint32_t icon; } meshtastic_Waypoint; +typedef PB_BYTES_ARRAY_T(435) meshtastic_MqttClientProxyMessage_data_t; +/* This message will be proxied over the PhoneAPI for the client to deliver to the MQTT server */ +typedef struct _meshtastic_MqttClientProxyMessage { + /* The MQTT topic this message will be sent /received on */ + char topic[60]; + pb_size_t which_payload_variant; + union { + /* Bytes */ + meshtastic_MqttClientProxyMessage_data_t data; + /* Text */ + char text[435]; + } payload_variant; + /* Whether the message should be retained (or not) */ + bool retained; +} meshtastic_MqttClientProxyMessage; + typedef PB_BYTES_ARRAY_T(256) meshtastic_MeshPacket_encrypted_t; /* A packet envelope sent/received over the mesh only payload_variant is sent in the payload portion of the LORA packet. @@ -683,6 +699,8 @@ typedef struct _meshtastic_ToRadio { (Sending this message is optional for clients) */ bool disconnect; meshtastic_XModem xmodemPacket; + /* MQTT Client Proxy Message */ + meshtastic_MqttClientProxyMessage mqttClientProxyMessage; }; } meshtastic_ToRadio; @@ -780,6 +798,8 @@ typedef struct _meshtastic_FromRadio { meshtastic_XModem xmodemPacket; /* Device metadata message */ meshtastic_DeviceMetadata metadata; + /* MQTT Client Proxy Message */ + meshtastic_MqttClientProxyMessage mqttClientProxyMessage; }; } meshtastic_FromRadio; @@ -836,6 +856,7 @@ extern "C" { #define meshtastic_Data_portnum_ENUMTYPE meshtastic_PortNum + #define meshtastic_MeshPacket_priority_ENUMTYPE meshtastic_MeshPacket_Priority #define meshtastic_MeshPacket_delayed_ENUMTYPE meshtastic_MeshPacket_Delayed @@ -862,6 +883,7 @@ extern "C" { #define meshtastic_Routing_init_default {0, {meshtastic_RouteDiscovery_init_default}} #define meshtastic_Data_init_default {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0} #define meshtastic_Waypoint_init_default {0, 0, 0, 0, 0, "", "", 0} +#define meshtastic_MqttClientProxyMessage_init_default {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_default {0, 0, 0, 0, {meshtastic_Data_init_default}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN} #define meshtastic_NodeInfo_init_default {0, false, meshtastic_User_init_default, false, meshtastic_Position_init_default, 0, 0, false, meshtastic_DeviceMetrics_init_default, 0} #define meshtastic_MyNodeInfo_init_default {0, 0, 0, "", _meshtastic_CriticalErrorCode_MIN, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, 0, 0} @@ -879,6 +901,7 @@ extern "C" { #define meshtastic_Routing_init_zero {0, {meshtastic_RouteDiscovery_init_zero}} #define meshtastic_Data_init_zero {_meshtastic_PortNum_MIN, {0, {0}}, 0, 0, 0, 0, 0, 0} #define meshtastic_Waypoint_init_zero {0, 0, 0, 0, 0, "", "", 0} +#define meshtastic_MqttClientProxyMessage_init_zero {"", 0, {{0, {0}}}, 0} #define meshtastic_MeshPacket_init_zero {0, 0, 0, 0, {meshtastic_Data_init_zero}, 0, 0, 0, 0, 0, _meshtastic_MeshPacket_Priority_MIN, 0, _meshtastic_MeshPacket_Delayed_MIN} #define meshtastic_NodeInfo_init_zero {0, false, meshtastic_User_init_zero, false, meshtastic_Position_init_zero, 0, 0, false, meshtastic_DeviceMetrics_init_zero, 0} #define meshtastic_MyNodeInfo_init_zero {0, 0, 0, "", _meshtastic_CriticalErrorCode_MIN, 0, 0, 0, 0, 0, 0, 0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, {0, 0, 0, 0, 0, 0, 0, 0}, 0, 0, 0} @@ -940,6 +963,10 @@ extern "C" { #define meshtastic_Waypoint_name_tag 6 #define meshtastic_Waypoint_description_tag 7 #define meshtastic_Waypoint_icon_tag 8 +#define meshtastic_MqttClientProxyMessage_topic_tag 1 +#define meshtastic_MqttClientProxyMessage_data_tag 2 +#define meshtastic_MqttClientProxyMessage_text_tag 3 +#define meshtastic_MqttClientProxyMessage_retained_tag 4 #define meshtastic_MeshPacket_from_tag 1 #define meshtastic_MeshPacket_to_tag 2 #define meshtastic_MeshPacket_channel_tag 3 @@ -988,6 +1015,7 @@ extern "C" { #define meshtastic_ToRadio_want_config_id_tag 3 #define meshtastic_ToRadio_disconnect_tag 4 #define meshtastic_ToRadio_xmodemPacket_tag 5 +#define meshtastic_ToRadio_mqttClientProxyMessage_tag 6 #define meshtastic_Compressed_portnum_tag 1 #define meshtastic_Compressed_data_tag 2 #define meshtastic_Neighbor_node_id_tag 1 @@ -1018,6 +1046,7 @@ extern "C" { #define meshtastic_FromRadio_queueStatus_tag 11 #define meshtastic_FromRadio_xmodemPacket_tag 12 #define meshtastic_FromRadio_metadata_tag 13 +#define meshtastic_FromRadio_mqttClientProxyMessage_tag 14 /* Struct field encoding specification for nanopb */ #define meshtastic_Position_FIELDLIST(X, a) \ @@ -1094,6 +1123,14 @@ X(a, STATIC, SINGULAR, FIXED32, icon, 8) #define meshtastic_Waypoint_CALLBACK NULL #define meshtastic_Waypoint_DEFAULT NULL +#define meshtastic_MqttClientProxyMessage_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, STRING, topic, 1) \ +X(a, STATIC, ONEOF, BYTES, (payload_variant,data,payload_variant.data), 2) \ +X(a, STATIC, ONEOF, STRING, (payload_variant,text,payload_variant.text), 3) \ +X(a, STATIC, SINGULAR, BOOL, retained, 4) +#define meshtastic_MqttClientProxyMessage_CALLBACK NULL +#define meshtastic_MqttClientProxyMessage_DEFAULT NULL + #define meshtastic_MeshPacket_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, FIXED32, from, 1) \ X(a, STATIC, SINGULAR, FIXED32, to, 2) \ @@ -1175,7 +1212,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,moduleConfig,moduleConfig), X(a, STATIC, ONEOF, MESSAGE, (payload_variant,channel,channel), 10) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,queueStatus,queueStatus), 11) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,xmodemPacket,xmodemPacket), 12) \ -X(a, STATIC, ONEOF, MESSAGE, (payload_variant,metadata,metadata), 13) +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,metadata,metadata), 13) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,mqttClientProxyMessage,mqttClientProxyMessage), 14) #define meshtastic_FromRadio_CALLBACK NULL #define meshtastic_FromRadio_DEFAULT NULL #define meshtastic_FromRadio_payload_variant_packet_MSGTYPE meshtastic_MeshPacket @@ -1188,16 +1226,19 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,metadata,metadata), 13) #define meshtastic_FromRadio_payload_variant_queueStatus_MSGTYPE meshtastic_QueueStatus #define meshtastic_FromRadio_payload_variant_xmodemPacket_MSGTYPE meshtastic_XModem #define meshtastic_FromRadio_payload_variant_metadata_MSGTYPE meshtastic_DeviceMetadata +#define meshtastic_FromRadio_payload_variant_mqttClientProxyMessage_MSGTYPE meshtastic_MqttClientProxyMessage #define meshtastic_ToRadio_FIELDLIST(X, a) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,packet,packet), 1) \ X(a, STATIC, ONEOF, UINT32, (payload_variant,want_config_id,want_config_id), 3) \ X(a, STATIC, ONEOF, BOOL, (payload_variant,disconnect,disconnect), 4) \ -X(a, STATIC, ONEOF, MESSAGE, (payload_variant,xmodemPacket,xmodemPacket), 5) +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,xmodemPacket,xmodemPacket), 5) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,mqttClientProxyMessage,mqttClientProxyMessage), 6) #define meshtastic_ToRadio_CALLBACK NULL #define meshtastic_ToRadio_DEFAULT NULL #define meshtastic_ToRadio_payload_variant_packet_MSGTYPE meshtastic_MeshPacket #define meshtastic_ToRadio_payload_variant_xmodemPacket_MSGTYPE meshtastic_XModem +#define meshtastic_ToRadio_payload_variant_mqttClientProxyMessage_MSGTYPE meshtastic_MqttClientProxyMessage #define meshtastic_Compressed_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UENUM, portnum, 1) \ @@ -1239,6 +1280,7 @@ extern const pb_msgdesc_t meshtastic_RouteDiscovery_msg; extern const pb_msgdesc_t meshtastic_Routing_msg; extern const pb_msgdesc_t meshtastic_Data_msg; extern const pb_msgdesc_t meshtastic_Waypoint_msg; +extern const pb_msgdesc_t meshtastic_MqttClientProxyMessage_msg; extern const pb_msgdesc_t meshtastic_MeshPacket_msg; extern const pb_msgdesc_t meshtastic_NodeInfo_msg; extern const pb_msgdesc_t meshtastic_MyNodeInfo_msg; @@ -1258,6 +1300,7 @@ extern const pb_msgdesc_t meshtastic_DeviceMetadata_msg; #define meshtastic_Routing_fields &meshtastic_Routing_msg #define meshtastic_Data_fields &meshtastic_Data_msg #define meshtastic_Waypoint_fields &meshtastic_Waypoint_msg +#define meshtastic_MqttClientProxyMessage_fields &meshtastic_MqttClientProxyMessage_msg #define meshtastic_MeshPacket_fields &meshtastic_MeshPacket_msg #define meshtastic_NodeInfo_fields &meshtastic_NodeInfo_msg #define meshtastic_MyNodeInfo_fields &meshtastic_MyNodeInfo_msg @@ -1274,9 +1317,10 @@ extern const pb_msgdesc_t meshtastic_DeviceMetadata_msg; #define meshtastic_Compressed_size 243 #define meshtastic_Data_size 270 #define meshtastic_DeviceMetadata_size 46 -#define meshtastic_FromRadio_size 330 +#define meshtastic_FromRadio_size 510 #define meshtastic_LogRecord_size 81 #define meshtastic_MeshPacket_size 321 +#define meshtastic_MqttClientProxyMessage_size 501 #define meshtastic_MyNodeInfo_size 179 #define meshtastic_NeighborInfo_size 142 #define meshtastic_Neighbor_size 11 @@ -1285,7 +1329,7 @@ extern const pb_msgdesc_t meshtastic_DeviceMetadata_msg; #define meshtastic_QueueStatus_size 23 #define meshtastic_RouteDiscovery_size 40 #define meshtastic_Routing_size 42 -#define meshtastic_ToRadio_size 324 +#define meshtastic_ToRadio_size 504 #define meshtastic_User_size 77 #define meshtastic_Waypoint_size 165 diff --git a/src/mesh/generated/meshtastic/module_config.pb.c b/src/mesh/generated/meshtastic/module_config.pb.c index 9352feb17..86614d18c 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.c +++ b/src/mesh/generated/meshtastic/module_config.pb.c @@ -39,6 +39,9 @@ PB_BIND(meshtastic_ModuleConfig_TelemetryConfig, meshtastic_ModuleConfig_Telemet PB_BIND(meshtastic_ModuleConfig_CannedMessageConfig, meshtastic_ModuleConfig_CannedMessageConfig, AUTO) +PB_BIND(meshtastic_ModuleConfig_AmbientLightingConfig, meshtastic_ModuleConfig_AmbientLightingConfig, AUTO) + + PB_BIND(meshtastic_RemoteHardwarePin, meshtastic_RemoteHardwarePin, AUTO) diff --git a/src/mesh/generated/meshtastic/module_config.pb.h b/src/mesh/generated/meshtastic/module_config.pb.h index 6273a89ae..43b330b04 100644 --- a/src/mesh/generated/meshtastic/module_config.pb.h +++ b/src/mesh/generated/meshtastic/module_config.pb.h @@ -112,6 +112,8 @@ typedef struct _meshtastic_ModuleConfig_MQTTConfig { /* The root topic to use for MQTT messages. Default is "msh". This is useful if you want to use a single MQTT server for multiple meshtastic networks and separate them via ACLs */ char root[16]; + /* If true, we can use the connected phone / client to proxy messages to MQTT instead of a direct connection */ + bool proxy_to_client_enabled; } meshtastic_ModuleConfig_MQTTConfig; /* NeighborInfoModule Config */ @@ -279,6 +281,20 @@ typedef struct _meshtastic_ModuleConfig_CannedMessageConfig { bool send_bell; } meshtastic_ModuleConfig_CannedMessageConfig; +/* Ambient Lighting Module - Settings for control of onboard LEDs to allow users to adjust the brightness levels and respective color levels. +Initially created for the RAK14001 RGB LED module. */ +typedef struct _meshtastic_ModuleConfig_AmbientLightingConfig { + /* Sets LED to on or off. */ + bool led_state; + /* Sets the overall current for the LED, firmware side range for the RAK14001 is 1-31, but users should be given a range of 0-100% */ + uint8_t current; + uint8_t red; /* Red level */ + /* Sets the green level of the LED, firmware side values are 0-255, but users should be given a range of 0-100% */ + uint8_t green; /* Green level */ + /* Sets the blue level of the LED, firmware side values are 0-255, but users should be given a range of 0-100% */ + uint8_t blue; /* Blue level */ +} meshtastic_ModuleConfig_AmbientLightingConfig; + /* A GPIO pin definition for remote hardware module */ typedef struct _meshtastic_RemoteHardwarePin { /* GPIO Pin number (must match Arduino) */ @@ -324,6 +340,8 @@ typedef struct _meshtastic_ModuleConfig { meshtastic_ModuleConfig_RemoteHardwareConfig remote_hardware; /* TODO: REPLACE */ meshtastic_ModuleConfig_NeighborInfoConfig neighbor_info; + /* TODO: REPLACE */ + meshtastic_ModuleConfig_AmbientLightingConfig ambient_lighting; } payload_variant; } meshtastic_ModuleConfig; @@ -370,12 +388,13 @@ extern "C" { #define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_event_ccw_ENUMTYPE meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar #define meshtastic_ModuleConfig_CannedMessageConfig_inputbroker_event_press_ENUMTYPE meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar + #define meshtastic_RemoteHardwarePin_type_ENUMTYPE meshtastic_RemoteHardwarePinType /* Initializer values for message structs */ #define meshtastic_ModuleConfig_init_default {0, {meshtastic_ModuleConfig_MQTTConfig_init_default}} -#define meshtastic_ModuleConfig_MQTTConfig_init_default {0, "", "", "", 0, 0, 0, ""} +#define meshtastic_ModuleConfig_MQTTConfig_init_default {0, "", "", "", 0, 0, 0, "", 0} #define meshtastic_ModuleConfig_RemoteHardwareConfig_init_default {0, 0, 0, {meshtastic_RemoteHardwarePin_init_default, meshtastic_RemoteHardwarePin_init_default, meshtastic_RemoteHardwarePin_init_default, meshtastic_RemoteHardwarePin_init_default}} #define meshtastic_ModuleConfig_NeighborInfoConfig_init_default {0, 0} #define meshtastic_ModuleConfig_AudioConfig_init_default {0, 0, _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_MIN, 0, 0, 0, 0} @@ -385,9 +404,10 @@ extern "C" { #define meshtastic_ModuleConfig_RangeTestConfig_init_default {0, 0, 0} #define meshtastic_ModuleConfig_TelemetryConfig_init_default {0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_CannedMessageConfig_init_default {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} +#define meshtastic_ModuleConfig_AmbientLightingConfig_init_default {0, 0, 0, 0, 0} #define meshtastic_RemoteHardwarePin_init_default {0, "", _meshtastic_RemoteHardwarePinType_MIN} #define meshtastic_ModuleConfig_init_zero {0, {meshtastic_ModuleConfig_MQTTConfig_init_zero}} -#define meshtastic_ModuleConfig_MQTTConfig_init_zero {0, "", "", "", 0, 0, 0, ""} +#define meshtastic_ModuleConfig_MQTTConfig_init_zero {0, "", "", "", 0, 0, 0, "", 0} #define meshtastic_ModuleConfig_RemoteHardwareConfig_init_zero {0, 0, 0, {meshtastic_RemoteHardwarePin_init_zero, meshtastic_RemoteHardwarePin_init_zero, meshtastic_RemoteHardwarePin_init_zero, meshtastic_RemoteHardwarePin_init_zero}} #define meshtastic_ModuleConfig_NeighborInfoConfig_init_zero {0, 0} #define meshtastic_ModuleConfig_AudioConfig_init_zero {0, 0, _meshtastic_ModuleConfig_AudioConfig_Audio_Baud_MIN, 0, 0, 0, 0} @@ -397,6 +417,7 @@ extern "C" { #define meshtastic_ModuleConfig_RangeTestConfig_init_zero {0, 0, 0} #define meshtastic_ModuleConfig_TelemetryConfig_init_zero {0, 0, 0, 0, 0, 0, 0} #define meshtastic_ModuleConfig_CannedMessageConfig_init_zero {0, 0, 0, 0, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, _meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_MIN, 0, 0, "", 0} +#define meshtastic_ModuleConfig_AmbientLightingConfig_init_zero {0, 0, 0, 0, 0} #define meshtastic_RemoteHardwarePin_init_zero {0, "", _meshtastic_RemoteHardwarePinType_MIN} /* Field tags (for use in manual encoding/decoding) */ @@ -408,6 +429,7 @@ extern "C" { #define meshtastic_ModuleConfig_MQTTConfig_json_enabled_tag 6 #define meshtastic_ModuleConfig_MQTTConfig_tls_enabled_tag 7 #define meshtastic_ModuleConfig_MQTTConfig_root_tag 8 +#define meshtastic_ModuleConfig_MQTTConfig_proxy_to_client_enabled_tag 9 #define meshtastic_ModuleConfig_NeighborInfoConfig_enabled_tag 1 #define meshtastic_ModuleConfig_NeighborInfoConfig_update_interval_tag 2 #define meshtastic_ModuleConfig_AudioConfig_codec2_enabled_tag 1 @@ -465,6 +487,11 @@ extern "C" { #define meshtastic_ModuleConfig_CannedMessageConfig_enabled_tag 9 #define meshtastic_ModuleConfig_CannedMessageConfig_allow_input_source_tag 10 #define meshtastic_ModuleConfig_CannedMessageConfig_send_bell_tag 11 +#define meshtastic_ModuleConfig_AmbientLightingConfig_led_state_tag 1 +#define meshtastic_ModuleConfig_AmbientLightingConfig_current_tag 2 +#define meshtastic_ModuleConfig_AmbientLightingConfig_red_tag 3 +#define meshtastic_ModuleConfig_AmbientLightingConfig_green_tag 4 +#define meshtastic_ModuleConfig_AmbientLightingConfig_blue_tag 5 #define meshtastic_RemoteHardwarePin_gpio_pin_tag 1 #define meshtastic_RemoteHardwarePin_name_tag 2 #define meshtastic_RemoteHardwarePin_type_tag 3 @@ -481,6 +508,7 @@ extern "C" { #define meshtastic_ModuleConfig_audio_tag 8 #define meshtastic_ModuleConfig_remote_hardware_tag 9 #define meshtastic_ModuleConfig_neighbor_info_tag 10 +#define meshtastic_ModuleConfig_ambient_lighting_tag 11 /* Struct field encoding specification for nanopb */ #define meshtastic_ModuleConfig_FIELDLIST(X, a) \ @@ -493,7 +521,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,telemetry,payload_variant.te X(a, STATIC, ONEOF, MESSAGE, (payload_variant,canned_message,payload_variant.canned_message), 7) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,audio,payload_variant.audio), 8) \ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,remote_hardware,payload_variant.remote_hardware), 9) \ -X(a, STATIC, ONEOF, MESSAGE, (payload_variant,neighbor_info,payload_variant.neighbor_info), 10) +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,neighbor_info,payload_variant.neighbor_info), 10) \ +X(a, STATIC, ONEOF, MESSAGE, (payload_variant,ambient_lighting,payload_variant.ambient_lighting), 11) #define meshtastic_ModuleConfig_CALLBACK NULL #define meshtastic_ModuleConfig_DEFAULT NULL #define meshtastic_ModuleConfig_payload_variant_mqtt_MSGTYPE meshtastic_ModuleConfig_MQTTConfig @@ -506,6 +535,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload_variant,neighbor_info,payload_varian #define meshtastic_ModuleConfig_payload_variant_audio_MSGTYPE meshtastic_ModuleConfig_AudioConfig #define meshtastic_ModuleConfig_payload_variant_remote_hardware_MSGTYPE meshtastic_ModuleConfig_RemoteHardwareConfig #define meshtastic_ModuleConfig_payload_variant_neighbor_info_MSGTYPE meshtastic_ModuleConfig_NeighborInfoConfig +#define meshtastic_ModuleConfig_payload_variant_ambient_lighting_MSGTYPE meshtastic_ModuleConfig_AmbientLightingConfig #define meshtastic_ModuleConfig_MQTTConfig_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, BOOL, enabled, 1) \ @@ -515,7 +545,8 @@ X(a, STATIC, SINGULAR, STRING, password, 4) \ X(a, STATIC, SINGULAR, BOOL, encryption_enabled, 5) \ X(a, STATIC, SINGULAR, BOOL, json_enabled, 6) \ X(a, STATIC, SINGULAR, BOOL, tls_enabled, 7) \ -X(a, STATIC, SINGULAR, STRING, root, 8) +X(a, STATIC, SINGULAR, STRING, root, 8) \ +X(a, STATIC, SINGULAR, BOOL, proxy_to_client_enabled, 9) #define meshtastic_ModuleConfig_MQTTConfig_CALLBACK NULL #define meshtastic_ModuleConfig_MQTTConfig_DEFAULT NULL @@ -616,6 +647,15 @@ X(a, STATIC, SINGULAR, BOOL, send_bell, 11) #define meshtastic_ModuleConfig_CannedMessageConfig_CALLBACK NULL #define meshtastic_ModuleConfig_CannedMessageConfig_DEFAULT NULL +#define meshtastic_ModuleConfig_AmbientLightingConfig_FIELDLIST(X, a) \ +X(a, STATIC, SINGULAR, BOOL, led_state, 1) \ +X(a, STATIC, SINGULAR, UINT32, current, 2) \ +X(a, STATIC, SINGULAR, UINT32, red, 3) \ +X(a, STATIC, SINGULAR, UINT32, green, 4) \ +X(a, STATIC, SINGULAR, UINT32, blue, 5) +#define meshtastic_ModuleConfig_AmbientLightingConfig_CALLBACK NULL +#define meshtastic_ModuleConfig_AmbientLightingConfig_DEFAULT NULL + #define meshtastic_RemoteHardwarePin_FIELDLIST(X, a) \ X(a, STATIC, SINGULAR, UINT32, gpio_pin, 1) \ X(a, STATIC, SINGULAR, STRING, name, 2) \ @@ -634,6 +674,7 @@ extern const pb_msgdesc_t meshtastic_ModuleConfig_StoreForwardConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_RangeTestConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_TelemetryConfig_msg; extern const pb_msgdesc_t meshtastic_ModuleConfig_CannedMessageConfig_msg; +extern const pb_msgdesc_t meshtastic_ModuleConfig_AmbientLightingConfig_msg; extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; /* Defines for backwards compatibility with code written before nanopb-0.4.0 */ @@ -648,20 +689,22 @@ extern const pb_msgdesc_t meshtastic_RemoteHardwarePin_msg; #define meshtastic_ModuleConfig_RangeTestConfig_fields &meshtastic_ModuleConfig_RangeTestConfig_msg #define meshtastic_ModuleConfig_TelemetryConfig_fields &meshtastic_ModuleConfig_TelemetryConfig_msg #define meshtastic_ModuleConfig_CannedMessageConfig_fields &meshtastic_ModuleConfig_CannedMessageConfig_msg +#define meshtastic_ModuleConfig_AmbientLightingConfig_fields &meshtastic_ModuleConfig_AmbientLightingConfig_msg #define meshtastic_RemoteHardwarePin_fields &meshtastic_RemoteHardwarePin_msg /* Maximum encoded size of messages (where known) */ +#define meshtastic_ModuleConfig_AmbientLightingConfig_size 14 #define meshtastic_ModuleConfig_AudioConfig_size 19 #define meshtastic_ModuleConfig_CannedMessageConfig_size 49 #define meshtastic_ModuleConfig_ExternalNotificationConfig_size 40 -#define meshtastic_ModuleConfig_MQTTConfig_size 220 +#define meshtastic_ModuleConfig_MQTTConfig_size 222 #define meshtastic_ModuleConfig_NeighborInfoConfig_size 8 #define meshtastic_ModuleConfig_RangeTestConfig_size 10 #define meshtastic_ModuleConfig_RemoteHardwareConfig_size 96 #define meshtastic_ModuleConfig_SerialConfig_size 28 #define meshtastic_ModuleConfig_StoreForwardConfig_size 22 #define meshtastic_ModuleConfig_TelemetryConfig_size 26 -#define meshtastic_ModuleConfig_size 223 +#define meshtastic_ModuleConfig_size 225 #define meshtastic_RemoteHardwarePin_size 21 #ifdef __cplusplus diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h index 089d7b59f..e4aaeeb96 100644 --- a/src/mesh/generated/meshtastic/portnums.pb.h +++ b/src/mesh/generated/meshtastic/portnums.pb.h @@ -54,6 +54,8 @@ typedef enum _meshtastic_PortNum { /* Audio Payloads. Encapsulated codec2 packets. On 2.4 GHZ Bandwidths only for now */ meshtastic_PortNum_AUDIO_APP = 9, + /* Payloads for clients with a network connection proxying MQTT pub/sub to the device */ + meshtastic_PortNum_MQTT_CLIENT_PROXY_APP = 10, /* Provides a 'ping' service that replies to any packet it receives. Also serves as a small example module. */ meshtastic_PortNum_REPLY_APP = 32, diff --git a/src/modules/AdminModule.cpp b/src/modules/AdminModule.cpp index ae254e7f7..be76f62a5 100644 --- a/src/modules/AdminModule.cpp +++ b/src/modules/AdminModule.cpp @@ -16,9 +16,7 @@ #include "unistd.h" #endif -#if HAS_WIFI || HAS_ETHERNET #include "mqtt/MQTT.h" -#endif #define DEFAULT_REBOOT_SECONDS 7 @@ -567,7 +565,7 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r if (conn.wifi.status.is_connected) { conn.wifi.rssi = WiFi.RSSI(); conn.wifi.status.ip_address = WiFi.localIP(); - conn.wifi.status.is_mqtt_connected = mqtt && mqtt->connected(); + conn.wifi.status.is_mqtt_connected = mqtt && mqtt->isConnectedDirectly(); conn.wifi.status.is_syslog_connected = false; // FIXME wire this up } #endif @@ -578,7 +576,7 @@ void AdminModule::handleGetDeviceConnectionStatus(const meshtastic_MeshPacket &r if (Ethernet.linkStatus() == LinkON) { conn.ethernet.status.is_connected = true; conn.ethernet.status.ip_address = Ethernet.localIP(); - conn.ethernet.status.is_mqtt_connected = mqtt && mqtt->connected(); + conn.ethernet.status.is_mqtt_connected = mqtt && mqtt->isConnectedDirectly(); conn.ethernet.status.is_syslog_connected = false; // FIXME wire this up } else { conn.ethernet.status.is_connected = false; diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 532a2d125..c10f9182d 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -25,12 +25,16 @@ Allocator &mqttPool = staticMqttPool; void MQTT::mqttCallback(char *topic, byte *payload, unsigned int length) { - mqtt->onPublish(topic, payload, length); + mqtt->onReceive(topic, payload, length); } -void MQTT::onPublish(char *topic, byte *payload, unsigned int length) +void MQTT::onClientProxyReceive(meshtastic_MqttClientProxyMessage msg) +{ + onReceive(msg.topic, msg.payload_variant.data.bytes, msg.payload_variant.data.size); +} + +void MQTT::onReceive(char *topic, byte *payload, size_t length) { - // parsing ServiceEnvelope meshtastic_ServiceEnvelope e = meshtastic_ServiceEnvelope_init_default; if (moduleConfig.mqtt.json_enabled && (strncmp(topic, jsonTopic.c_str(), jsonTopic.length()) == 0)) { @@ -153,10 +157,13 @@ void mqttInit() new MQTT(); } +#ifdef HAS_NETWORKING MQTT::MQTT() : concurrency::OSThread("mqtt"), pubSub(mqttClient), mqttQueue(MAX_MQTT_QUEUE) +#else +MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) +#endif { if (moduleConfig.mqtt.enabled) { - assert(!mqtt); mqtt = this; @@ -170,22 +177,77 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), pubSub(mqttClient), mqttQueue(MAX_ jsonTopic = "msh" + jsonTopic; } - pubSub.setCallback(mqttCallback); - +#ifdef HAS_NETWORKING + if (!moduleConfig.mqtt.proxy_to_client_enabled) + pubSub.setCallback(mqttCallback); +#endif // preflightSleepObserver.observe(&preflightSleep); } else { disable(); } } -bool MQTT::connected() +bool MQTT::isConnectedDirectly() { +#ifdef HAS_NETWORKING return pubSub.connected(); +#else + return false; +#endif +} + +bool MQTT::publish(const char *topic, const char *payload, bool retained) +{ + if (moduleConfig.mqtt.proxy_to_client_enabled) { + meshtastic_MqttClientProxyMessage *msg = mqttClientProxyMessagePool.allocZeroed(); + msg->which_payload_variant = meshtastic_MqttClientProxyMessage_text_tag; + strcpy(msg->topic, topic); + strcpy(msg->payload_variant.text, payload); + msg->retained = retained; + service.sendMqttMessageToClientProxy(msg); + return true; + } +#ifdef HAS_NETWORKING + else if (isConnectedDirectly()) { + return pubSub.publish(topic, payload, retained); + } +#endif + return false; +} + +bool MQTT::publish(const char *topic, const uint8_t *payload, size_t length, bool retained) +{ + if (moduleConfig.mqtt.proxy_to_client_enabled) { + meshtastic_MqttClientProxyMessage *msg = mqttClientProxyMessagePool.allocZeroed(); + msg->which_payload_variant = meshtastic_MqttClientProxyMessage_data_tag; + strcpy(msg->topic, topic); + msg->payload_variant.data.size = length; + memcpy(msg->payload_variant.data.bytes, payload, length); + msg->retained = retained; + service.sendMqttMessageToClientProxy(msg); + return true; + } +#ifdef HAS_NETWORKING + else if (isConnectedDirectly()) { + return pubSub.publish(topic, payload, length, retained); + } +#endif + return false; } void MQTT::reconnect() { if (wantsLink()) { + if (moduleConfig.mqtt.proxy_to_client_enabled) { + LOG_INFO("MQTT connecting via client proxy instead...\n"); + enabled = true; + runASAP = true; + reconnectCount = 0; + + publishStatus(); + return; // Don't try to connect directly to the server + } +#ifdef HAS_NETWORKING // Defaults int serverPort = 1883; const char *serverAddr = default_mqtt_address; @@ -197,7 +259,6 @@ void MQTT::reconnect() mqttUsername = moduleConfig.mqtt.username; mqttPassword = moduleConfig.mqtt.password; } - #if HAS_WIFI && !defined(ARCH_PORTDUINO) if (moduleConfig.mqtt.tls_enabled) { // change default for encrypted to 8883 @@ -214,7 +275,7 @@ void MQTT::reconnect() LOG_INFO("Using non-TLS-encrypted session\n"); pubSub.setClient(mqttClient); } -#else +#elif HAS_NETWORKING pubSub.setClient(mqttClient); #endif @@ -229,8 +290,9 @@ void MQTT::reconnect() pubSub.setServer(serverAddr, serverPort); pubSub.setBufferSize(512); - LOG_INFO("Connecting to MQTT server %s, port: %d, username: %s, password: %s\n", serverAddr, serverPort, mqttUsername, - mqttPassword); + LOG_INFO("Attempting to connnect directly to MQTT server %s, port: %d, username: %s, password: %s\n", serverAddr, + serverPort, mqttUsername, mqttPassword); + auto myStatus = (statusTopic + owner.id); bool connected = pubSub.connect(owner.id, mqttUsername, mqttPassword, myStatus.c_str(), 1, true, "offline"); if (connected) { @@ -239,15 +301,12 @@ void MQTT::reconnect() runASAP = true; reconnectCount = 0; - /// FIXME, include more information in the status text - bool ok = pubSub.publish(myStatus.c_str(), "online", true); - LOG_INFO("published %d\n", ok); - + publishStatus(); sendSubscriptions(); } else { #if HAS_WIFI && !defined(ARCH_PORTDUINO) reconnectCount++; - LOG_ERROR("Failed to contact MQTT server (%d/%d)...\n", reconnectCount, reconnectMax); + LOG_ERROR("Failed to contact MQTT server directly (%d/%d)...\n", reconnectCount, reconnectMax); if (reconnectCount >= reconnectMax) { needReconnect = true; wifiReconnect->setIntervalFromNow(0); @@ -255,11 +314,13 @@ void MQTT::reconnect() } #endif } +#endif } } void MQTT::sendSubscriptions() { +#ifdef HAS_NETWORKING size_t numChan = channels.getNumChannels(); for (size_t i = 0; i < numChan; i++) { auto &ch = channels.getByIndex(i); @@ -274,6 +335,7 @@ void MQTT::sendSubscriptions() } } } +#endif } bool MQTT::wantsLink() const @@ -291,60 +353,44 @@ bool MQTT::wantsLink() const } } } + if (hasChannel && moduleConfig.mqtt.proxy_to_client_enabled) + return true; #if HAS_WIFI return hasChannel && WiFi.isConnected(); #endif #if HAS_ETHERNET - return hasChannel && (Ethernet.linkStatus() == LinkON); + return hasChannel && Ethernet.linkStatus() == LinkON; #endif return false; } int32_t MQTT::runOnce() { - if (!moduleConfig.mqtt.enabled) { + if (!moduleConfig.mqtt.enabled) return disable(); - } + bool wantConnection = wantsLink(); // If connected poll rapidly, otherwise only occasionally check for a wifi connection change and ability to contact server - if (!pubSub.loop()) { - if (wantConnection) { + if (moduleConfig.mqtt.proxy_to_client_enabled) { + publishQueuedMessages(); + return 200; + } +#ifdef HAS_NETWORKING + else if (!pubSub.loop()) { + if (!wantConnection) + return 5000; // If we don't want connection now, check again in 5 secs + else { reconnect(); - // If we succeeded, empty the queue one by one and start reading rapidly, else try again in 30 seconds (TCP // connections are EXPENSIVE so try rarely) - if (pubSub.connected()) { - if (!mqttQueue.isEmpty()) { - // FIXME - this size calculation is super sloppy, but it will go away once we dynamically alloc meshpackets - meshtastic_ServiceEnvelope *env = mqttQueue.dequeuePtr(0); - static uint8_t bytes[meshtastic_MeshPacket_size + 64]; - size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, env); - - std::string topic = cryptTopic + env->channel_id + "/" + owner.id; - LOG_INFO("publish %s, %u bytes from queue\n", topic.c_str(), numBytes); - - pubSub.publish(topic.c_str(), bytes, numBytes, false); - - if (moduleConfig.mqtt.json_enabled) { - // handle json topic - auto jsonString = this->downstreamPacketToJson(env->packet); - if (jsonString.length() != 0) { - std::string topicJson = jsonTopic + env->channel_id + "/" + owner.id; - LOG_INFO("JSON publish message to %s, %u bytes: %s\n", topicJson.c_str(), jsonString.length(), - jsonString.c_str()); - pubSub.publish(topicJson.c_str(), jsonString.c_str(), false); - } - } - mqttPool.release(env); - } + if (isConnectedDirectly()) { + publishQueuedMessages(); return 200; - } else { + } else return 30000; - } - } else - return 5000; // If we don't want connection now, check again in 5 secs + } } else { // we are connected to server, check often for new requests on the TCP port if (!wantConnection) { @@ -355,6 +401,44 @@ int32_t MQTT::runOnce() powerFSM.trigger(EVENT_CONTACT_FROM_PHONE); // Suppress entering light sleep (because that would turn off bluetooth) return 20; } +#endif + return 30000; +} + +/// FIXME, include more information in the status text +void MQTT::publishStatus() +{ + auto myStatus = (statusTopic + owner.id); + bool ok = publish(myStatus.c_str(), "online", true); + LOG_INFO("published online=%d\n", ok); +} + +void MQTT::publishQueuedMessages() +{ + if (!mqttQueue.isEmpty()) { + LOG_DEBUG("Publishing enqueued MQTT message\n"); + // FIXME - this size calculation is super sloppy, but it will go away once we dynamically alloc meshpackets + meshtastic_ServiceEnvelope *env = mqttQueue.dequeuePtr(0); + static uint8_t bytes[meshtastic_MeshPacket_size + 64]; + size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, env); + + std::string topic = cryptTopic + env->channel_id + "/" + owner.id; + LOG_INFO("publish %s, %u bytes from queue\n", topic.c_str(), numBytes); + + publish(topic.c_str(), bytes, numBytes, false); + + if (moduleConfig.mqtt.json_enabled) { + // handle json topic + auto jsonString = this->meshPacketToJson(env->packet); + if (jsonString.length() != 0) { + std::string topicJson = jsonTopic + env->channel_id + "/" + owner.id; + LOG_INFO("JSON publish message to %s, %u bytes: %s\n", topicJson.c_str(), jsonString.length(), + jsonString.c_str()); + publish(topicJson.c_str(), jsonString.c_str(), false); + } + } + mqttPool.release(env); + } } void MQTT::onSend(const meshtastic_MeshPacket &mp, ChannelIndex chIndex) @@ -368,27 +452,26 @@ void MQTT::onSend(const meshtastic_MeshPacket &mp, ChannelIndex chIndex) env->channel_id = (char *)channelId; env->gateway_id = owner.id; env->packet = (meshtastic_MeshPacket *)∓ + LOG_DEBUG("MQTT onSend - Publishing portnum %i message\n", env->packet->decoded.portnum); - // don't bother sending if not connected... - if (pubSub.connected()) { - + if (moduleConfig.mqtt.proxy_to_client_enabled || this->isConnectedDirectly()) { // FIXME - this size calculation is super sloppy, but it will go away once we dynamically alloc meshpackets static uint8_t bytes[meshtastic_MeshPacket_size + 64]; size_t numBytes = pb_encode_to_bytes(bytes, sizeof(bytes), &meshtastic_ServiceEnvelope_msg, env); std::string topic = cryptTopic + channelId + "/" + owner.id; - LOG_DEBUG("publish %s, %u bytes\n", topic.c_str(), numBytes); + LOG_DEBUG("MQTT Publish %s, %u bytes\n", topic.c_str(), numBytes); - pubSub.publish(topic.c_str(), bytes, numBytes, false); + publish(topic.c_str(), bytes, numBytes, false); if (moduleConfig.mqtt.json_enabled) { // handle json topic - auto jsonString = this->downstreamPacketToJson((meshtastic_MeshPacket *)&mp); + auto jsonString = this->meshPacketToJson((meshtastic_MeshPacket *)&mp); if (jsonString.length() != 0) { std::string topicJson = jsonTopic + channelId + "/" + owner.id; LOG_INFO("JSON publish message to %s, %u bytes: %s\n", topicJson.c_str(), jsonString.length(), jsonString.c_str()); - pubSub.publish(topicJson.c_str(), jsonString.c_str(), false); + publish(topicJson.c_str(), jsonString.c_str(), false); } } } else { @@ -408,7 +491,7 @@ void MQTT::onSend(const meshtastic_MeshPacket &mp, ChannelIndex chIndex) } // converts a downstream packet into a json message -std::string MQTT::downstreamPacketToJson(meshtastic_MeshPacket *mp) +std::string MQTT::meshPacketToJson(meshtastic_MeshPacket *mp) { // the created jsonObj is immutable after creation, so // we need to do the heavy lifting before assembling it. diff --git a/src/mqtt/MQTT.h b/src/mqtt/MQTT.h index 1e626c3e0..fc436c22e 100644 --- a/src/mqtt/MQTT.h +++ b/src/mqtt/MQTT.h @@ -5,15 +5,20 @@ #include "concurrency/OSThread.h" #include "mesh/Channels.h" #include "mesh/generated/meshtastic/mqtt.pb.h" -#include #if HAS_WIFI #include +#define HAS_NETWORKING 1 #if !defined(ARCH_PORTDUINO) #include #endif #endif #if HAS_ETHERNET #include +#define HAS_NETWORKING 1 +#endif + +#ifdef HAS_NETWORKING +#include #endif #define MAX_MQTT_QUEUE 16 @@ -35,12 +40,9 @@ class MQTT : private concurrency::OSThread #if HAS_ETHERNET EthernetClient mqttClient; #endif -#if !defined(DEBUG_HEAP_MQTT) - PubSubClient pubSub; public: -#else - public: +#ifdef HAS_NETWORKING PubSubClient pubSub; #endif MQTT(); @@ -59,7 +61,13 @@ class MQTT : private concurrency::OSThread */ void reconnect(); - bool connected(); + bool isConnectedDirectly(); + + bool publish(const char *topic, const char *payload, bool retained); + + bool publish(const char *topic, const uint8_t *payload, size_t length, const bool retained); + + void onClientProxyReceive(meshtastic_MqttClientProxyMessage msg); protected: PointerQueue mqttQueue; @@ -80,14 +88,17 @@ class MQTT : private concurrency::OSThread */ void sendSubscriptions(); - /// Just C glue to call onPublish + /// Callback for direct mqtt subscription messages static void mqttCallback(char *topic, byte *payload, unsigned int length); /// Called when a new publish arrives from the MQTT server - void onPublish(char *topic, byte *payload, unsigned int length); + void onReceive(char *topic, byte *payload, size_t length); /// Called when a new publish arrives from the MQTT server - std::string downstreamPacketToJson(meshtastic_MeshPacket *mp); + std::string meshPacketToJson(meshtastic_MeshPacket *mp); + + void publishStatus(); + void publishQueuedMessages(); /// Return 0 if sleep is okay, veto sleep if we are connected to pubsub server // int preflightSleepCb(void *unused = NULL) { return pubSub.connected() ? 1 : 0; } From 65aafe7ea104ff5ac8f6eb21e3a7ae7d09368556 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 8 Jul 2023 20:46:34 -0500 Subject: [PATCH 09/57] Update protos --- protobufs | 2 +- src/mesh/generated/meshtastic/mesh.pb.h | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/protobufs b/protobufs index f2d1ebbd3..e0b136f5f 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit f2d1ebbd3485f6e4814608da0cfc7a82d97305f1 +Subproject commit e0b136f5f8e26094d02c28d1fdcacd61e087298c diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 3d4c41cb2..3814875d3 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -97,6 +97,14 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_BETAFPV_900_NANO_TX = 46, /* Raspberry Pi Pico (W) with Waveshare SX1262 LoRa Node Module */ meshtastic_HardwareModel_RPI_PICO = 47, + /* Heltec Wireless Tracker with ESP32-S3 CPU, built-in GPS, and TFT */ + meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER = 48, + /* Heltec Wireless Paper with ESP32-S3 CPU and E-Ink display */ + meshtastic_HardwareModel_HELTEC_WIRELESS_PAPER = 49, + /* LilyGo T-Deck with ESP32-S3 CPU, Keyboard, and IPS display */ + meshtastic_HardwareModel_T_DECK = 50, + /* LilyGo T-Watch S3 with ESP32-S3 CPU and IPS display */ + meshtastic_HardwareModel_T_WATCH_S3 = 51, /* ------------------------------------------------------------------------------------------------------------------------------------------ Reserved ID For developing private Ports. These will show up in live traffic sparsely, so we can use a high number. Keep it within 8 bits. ------------------------------------------------------------------------------------------------------------------------------------------ */ From de53280ffc1963609379379be4c4682e9c4f2546 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 8 Jul 2023 21:01:00 -0500 Subject: [PATCH 10/57] PIN_GPS_EN power toggling (#2592) * PIN_GPS_EN * Remove extra digitalWrite * GPS_POWER_TOGGLE macro enabled. Added WSLv3 too * Update variant.h * Update variant.h * Fixed macro guard --- src/gps/GPS.cpp | 9 ++++----- src/sleep.cpp | 6 +++++- variants/heltec_v3/platformio.ini | 1 + variants/heltec_v3/variant.h | 4 +++- variants/heltec_wsl_v3/platformio.ini | 1 + variants/heltec_wsl_v3/variant.h | 4 +++- 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 566d90a5c..65fb9595c 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -419,13 +419,12 @@ bool GPS::setupGPS() bool GPS::setup() { // Master power for the GPS -#ifdef PIN_GPS_EN - digitalWrite(PIN_GPS_EN, 1); - pinMode(PIN_GPS_EN, OUTPUT); -#endif -#ifdef HAS_PMU +#if defined(HAS_PMU) || defined(PIN_GPS_EN) if (config.position.gps_enabled) { +#ifdef PIN_GPS_EN + pinMode(PIN_GPS_EN, OUTPUT); +#endif setGPSPower(true); } #endif diff --git a/src/sleep.cpp b/src/sleep.cpp index 5331eaf75..483c491b4 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -99,6 +99,10 @@ void setGPSPower(bool on) { LOG_INFO("Setting GPS power=%d\n", on); +#ifdef PIN_GPS_EN + digitalWrite(PIN_GPS_EN, on ? 1 : 0); +#endif + #ifdef HAS_PMU if (pmu_found && PMU) { uint8_t model = PMU->getChipModel(); @@ -185,7 +189,7 @@ static void waitEnterSleep() void doGPSpowersave(bool on) { -#ifdef HAS_PMU +#if defined(HAS_PMU) || defined(PIN_GPS_EN) if (on) { LOG_INFO("Turning GPS back on\n"); gps->forceWake(1); diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 3e0ace39c..58ee0b5ba 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -4,3 +4,4 @@ board = heltec_wifi_lora_32_V3 # Temporary until espressif creates a release with this new target build_flags = ${esp32s3_base.build_flags} -D HELTEC_V3 -I variants/heltec_v3 + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. \ No newline at end of file diff --git a/variants/heltec_v3/variant.h b/variants/heltec_v3/variant.h index d9fc0b4c2..aedfbe677 100644 --- a/variants/heltec_v3/variant.h +++ b/variants/heltec_v3/variant.h @@ -7,6 +7,8 @@ #define VEXT_ENABLE Vext // active low, powers the oled display and the lora antenna boost #define BUTTON_PIN 0 +#define PIN_GPS_EN 46 // GPS power enable pin + #define BATTERY_PIN 1 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO1_CHANNEL #define ADC_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider @@ -29,4 +31,4 @@ #define SX126X_DIO1 LORA_DIO1 #define SX126X_BUSY LORA_DIO2 #define SX126X_RESET LORA_RESET -#define SX126X_E22 \ No newline at end of file +#define SX126X_E22 diff --git a/variants/heltec_wsl_v3/platformio.ini b/variants/heltec_wsl_v3/platformio.ini index 5f89a7466..c95659156 100644 --- a/variants/heltec_wsl_v3/platformio.ini +++ b/variants/heltec_wsl_v3/platformio.ini @@ -4,3 +4,4 @@ board = heltec_wifi_lora_32_V3 # Temporary until espressif creates a release with this new target build_flags = ${esp32s3_base.build_flags} -D HELTEC_WSL_V3 -I variants/heltec_wsl_v3 + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. \ No newline at end of file diff --git a/variants/heltec_wsl_v3/variant.h b/variants/heltec_wsl_v3/variant.h index 0ecc5bea7..ec5796313 100644 --- a/variants/heltec_wsl_v3/variant.h +++ b/variants/heltec_wsl_v3/variant.h @@ -8,6 +8,8 @@ #define VEXT_ENABLE Vext // active low, powers the oled display and the lora antenna boost #define BUTTON_PIN 0 +#define PIN_GPS_EN 46 // GPS power enable pin + #define BATTERY_PIN 1 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage #define ADC_CHANNEL ADC1_GPIO1_CHANNEL #define ADC_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider @@ -30,4 +32,4 @@ #define SX126X_DIO1 LORA_DIO1 #define SX126X_BUSY LORA_DIO2 #define SX126X_RESET LORA_RESET -#define SX126X_E22 \ No newline at end of file +#define SX126X_E22 From 0cca7751cdc9639d96d91306d44686a3b26cf323 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 9 Jul 2023 06:15:48 -0500 Subject: [PATCH 11/57] [create-pull-request] automated change (#2600) Co-authored-by: thebentern --- version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.properties b/version.properties index 5cc440cdd..e279aa749 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 1 -build = 18 +build = 19 From d3e7e45ded441ad4842a7e2f8b501f2b927be063 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 9 Jul 2023 06:17:17 -0500 Subject: [PATCH 12/57] Append alpha to release name --- .github/workflows/main_matrix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 651a9a3ee..2dc5264a2 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -307,7 +307,7 @@ jobs: with: draft: true prerelease: true - release_name: Meshtastic Firmware ${{ steps.version.outputs.version }} + release_name: Meshtastic Firmware ${{ steps.version.outputs.version }} Alpha tag_name: v${{ steps.version.outputs.version }} body: | Autogenerated by github action, developer should edit as required before publishing... From 42d79d012e03bdc0846c664b66eeb359cce40213 Mon Sep 17 00:00:00 2001 From: Ben Lipsey <117498748+pdxlocations@users.noreply.github.com> Date: Sun, 9 Jul 2023 18:16:36 -0700 Subject: [PATCH 13/57] center text based on screen width (#2603) --- src/graphics/Screen.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index f2c5da4e2..cf7ed1ad0 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -320,18 +320,18 @@ static void drawFrameBluetooth(OLEDDisplay *display, OLEDDisplayUiState *state, static void drawFrameShutdown(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + uint16_t x_offset = display->width() / 2; display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(64 + x, 26 + y, "Shutting down..."); + display->drawString(x_offset + x, 26 + y, "Shutting down..."); } static void drawFrameReboot(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { + uint16_t x_offset = display->width() / 2; display->setTextAlignment(TEXT_ALIGN_CENTER); - display->setFont(FONT_MEDIUM); - display->drawString(64 + x, 26 + y, "Rebooting..."); + display->drawString(x_offset + x, 26 + y, "Rebooting..."); } static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) From aa0b56e947a0655a5dc4fdebe74fd556690ea00a Mon Sep 17 00:00:00 2001 From: Dmitry Galenko Date: Thu, 13 Jul 2023 02:35:41 +0200 Subject: [PATCH 14/57] GPS: Implement Power Management, Refactor Code and Fix GSA Message Configuration for U-Blox hardware (#2606) --- src/gps/GPS.cpp | 191 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 164 insertions(+), 27 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 65fb9595c..c6298f575 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -60,9 +60,9 @@ bool GPS::getACK(uint8_t class_id, uint8_t msg_id) // LOG_INFO("Got ACK for class %02X message %02X\n", class_id, msg_id); return true; // ACK received } - if (millis() - startTime > 1500) { + if (millis() - startTime > 3000) { LOG_WARN("No response for class %02X message %02X\n", class_id, msg_id); - return false; // No response received within 1.5 second + return false; // No response received within 3 seconds } if (_serial_gps->available()) { b = _serial_gps->read(); @@ -212,6 +212,7 @@ bool GPS::setupGPS() _serial_gps->write("$PCAS11,3*1E\r\n"); delay(250); } else if (gnssModel == GNSS_MODEL_UBLOX) { + // Configure GNSS system to GPS+SBAS+GLONASS (Module may restart after this command) // We need set it because by default it is GPS only, and we want to use GLONASS too // Also we need SBAS for better accuracy and extra features @@ -238,10 +239,19 @@ bool GPS::setupGPS() _serial_gps->write(_message_GNSS, sizeof(_message_GNSS)); if (!getACK(0x06, 0x3e)) { - LOG_WARN("Unable to reconfigure GNSS, keep factory defaults\n"); + // It's not critical if the module doesn't acknowledge this configuration. + // The module should operate adequately with its factory or previously saved settings. + // It appears that there is a firmware bug in some GPS modules: When an attempt is made + // to overwrite a saved state with identical values, no ACK/NAK is received, contrary to + // what is specified in the Ublox documentation. + // There is also a possibility that the module may be GPS-only. + LOG_INFO("Unable to reconfigure GNSS - defaults maintained. Is this module GPS-only?\n"); + return true; } else { - LOG_INFO("GNSS set to GPS+SBAS+GLONASS, waiting before sending next command (0.75s)\n"); + LOG_INFO("GNSS configured for GPS+SBAS+GLONASS. Pause for 0.75s before sending next command.\n"); + // Documentation say, we need wait atleast 0.5s after reconfiguration of GNSS module, before sending next commands delay(750); + return true; } // Enable interference resistance, because we are using LoRa, WiFi and Bluetoot on same board, @@ -258,7 +268,8 @@ bool GPS::setupGPS() // generalBits (General settings) is set to 0x31E as recommended // antSetting (Antenna setting, 0=unknown, 1=passive, 2=active) is set to 0 (unknown) // ToDo: Set to 1 (passive) or 2 (active) if known, for example from UBX-MON-HW, or from board info - // enable2 (Set to 1 to scan auxiliary bands, u-blox 8 / u-blox M8 only, otherwise ignored) is set to 1 (enabled) + // enable2 (Set to 1 to scan auxiliary bands, u-blox 8 / u-blox M8 only, otherwise ignored) is set to 1 + // (enabled) 0x1E, 0x03, 0x00, 0x01, // config2: Extra settings for jamming/interference monitor 0x00, 0x00 // Checksum (calculated below) }; @@ -271,6 +282,7 @@ bool GPS::setupGPS() if (!getACK(0x06, 0x39)) { LOG_WARN("Unable to enable interference resistance.\n"); + return true; } // Configure navigation engine expert settings: @@ -316,6 +328,7 @@ bool GPS::setupGPS() if (!getACK(0x06, 0x23)) { LOG_WARN("Unable to configure extra settings.\n"); + return true; } /* @@ -334,57 +347,179 @@ bool GPS::setupGPS() // ublox-M10S can be compatible with UBLOX traditional protocol, so the following sentence settings are also valid - // disable GGL - byte _message_GGL[] = {0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x01, - 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x05, 0x3A}; + // Set GPS update rate to 1Hz + // Lowering the update rate helps to save power. + // Additionally, for some new modules like the M9/M10, an update rate lower than 5Hz + // is recommended to avoid a known issue with satellites disappearing. + byte _message_1Hz[] = { + 0xB5, 0x62, // UBX protocol sync characters + 0x06, 0x08, // Message class and ID (UBX-CFG-RATE) + 0x06, 0x00, // Length of payload (6 bytes) + 0xE8, 0x03, // Measurement Rate (1000ms for 1Hz) + 0x01, 0x00, // Navigation rate, always 1 in GPS mode + 0x01, 0x00, // Time reference + 0x00, 0x00 // Placeholder for checksum, will be calculated next + }; + + // Calculate the checksum and update the message. + UBXChecksum(_message_1Hz, sizeof(_message_1Hz)); + + // Send the message to the module + _serial_gps->write(_message_1Hz, sizeof(_message_1Hz)); + + if (!getACK(0x06, 0x08)) { + LOG_WARN("Unable to set GPS update rate.\n"); + return true; + } + + // Disable GGL. GGL - Geographic position (latitude and longitude), which provides the current geographical + // coordinates. + byte _message_GGL[] = { + 0xB5, 0x62, // UBX sync characters + 0x06, 0x01, // Message class and ID (UBX-CFG-MSG) + 0x08, 0x00, // Length of payload (8 bytes) + 0xF0, 0x01, // NMEA ID for GLL + 0x01, // I/O Target 0=I/O, 1=UART1, 2=UART2, 3=USB, 4=SPI + 0x00, // Disable + 0x01, 0x01, 0x01, 0x01, // Reserved + 0x00, 0x00 // CK_A and CK_B (Checksum) + }; + + // Calculate the checksum and update the message. + UBXChecksum(_message_GGL, sizeof(_message_GGL)); + + // Send the message to the module _serial_gps->write(_message_GGL, sizeof(_message_GGL)); + if (!getACK(0x06, 0x01)) { LOG_WARN("Unable to disable NMEA GGL.\n"); return true; } - // disable GSA - byte _message_GSA[] = {0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x02, - 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x06, 0x41}; + // Enable GSA. GSA - GPS DOP and active satellites, used for detailing the satellites used in the positioning and + // the DOP (Dilution of Precision) + byte _message_GSA[] = { + 0xB5, 0x62, // UBX sync characters + 0x06, 0x01, // Message class and ID (UBX-CFG-MSG) + 0x08, 0x00, // Length of payload (8 bytes) + 0xF0, 0x02, // NMEA ID for GSA + 0x01, // I/O Target 0=I/O, 1=UART1, 2=UART2, 3=USB, 4=SPI + 0x01, // Enable + 0x01, 0x01, 0x01, 0x01, // Reserved + 0x00, 0x00 // CK_A and CK_B (Checksum) + }; + UBXChecksum(_message_GSA, sizeof(_message_GSA)); _serial_gps->write(_message_GSA, sizeof(_message_GSA)); if (!getACK(0x06, 0x01)) { - LOG_WARN("Unable to disable NMEA GSA.\n"); + LOG_WARN("Unable to Enable NMEA GSA.\n"); return true; } - // disable GSV - byte _message_GSV[] = {0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x03, - 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x07, 0x48}; + // Disable GSV. GSV - Satellites in view, details the number and location of satellites in view. + byte _message_GSV[] = { + 0xB5, 0x62, // UBX sync characters + 0x06, 0x01, // Message class and ID (UBX-CFG-MSG) + 0x08, 0x00, // Length of payload (8 bytes) + 0xF0, 0x03, // NMEA ID for GSV + 0x01, // I/O Target 0=I/O, 1=UART1, 2=UART2, 3=USB, 4=SPI + 0x00, // Disable + 0x01, 0x01, 0x01, 0x01, // Reserved + 0x00, 0x00 // CK_A and CK_B (Checksum) + }; + UBXChecksum(_message_GSV, sizeof(_message_GSV)); _serial_gps->write(_message_GSV, sizeof(_message_GSV)); if (!getACK(0x06, 0x01)) { LOG_WARN("Unable to disable NMEA GSV.\n"); return true; } - // disable VTG - byte _message_VTG[] = {0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x05, - 0x01, 0x00, 0x01, 0x01, 0x01, 0x01, 0x09, 0x56}; + // Disable VTG. VTG - Track made good and ground speed, which provides course and speed information relative to + // the ground. + byte _message_VTG[] = { + 0xB5, 0x62, // UBX sync characters + 0x06, 0x01, // Message class and ID (UBX-CFG-MSG) + 0x08, 0x00, // Length of payload (8 bytes) + 0xF0, 0x05, // NMEA ID for VTG + 0x01, // I/O Target 0=I/O, 1=UART1, 2=UART2, 3=USB, 4=SPI + 0x00, // Disable + 0x01, 0x01, 0x01, 0x01, // Reserved + 0x00, 0x00 // CK_A and CK_B (Checksum) + }; + UBXChecksum(_message_VTG, sizeof(_message_VTG)); _serial_gps->write(_message_VTG, sizeof(_message_VTG)); if (!getACK(0x06, 0x01)) { LOG_WARN("Unable to disable NMEA VTG.\n"); return true; } - // enable RMC - byte _message_RMC[] = {0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x04, - 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x09, 0x54}; + // Enable RMC. RMC - Recommended Minimum data, the essential gps pvt (position, velocity, time) data. + byte _message_RMC[] = { + 0xB5, 0x62, // UBX sync characters + 0x06, 0x01, // Message class and ID (UBX-CFG-MSG) + 0x08, 0x00, // Length of payload (8 bytes) + 0xF0, 0x04, // NMEA ID for RMC + 0x01, // I/O Target 0=I/O, 1=UART1, 2=UART2, 3=USB, 4=SPI + 0x01, // Enable + 0x01, 0x01, 0x01, 0x01, // Reserved + 0x00, 0x00 // CK_A and CK_B (Checksum) + }; + UBXChecksum(_message_RMC, sizeof(_message_RMC)); _serial_gps->write(_message_RMC, sizeof(_message_RMC)); if (!getACK(0x06, 0x01)) { LOG_WARN("Unable to enable NMEA RMC.\n"); return true; } - // enable GGA - byte _message_GGA[] = {0xB5, 0x62, 0x06, 0x01, 0x08, 0x00, 0xF0, 0x00, - 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x05, 0x38}; + // Enable GGA. GGA - Global Positioning System Fix Data, which provides 3D location and accuracy data. + byte _message_GGA[] = { + 0xB5, 0x62, // UBX sync characters + 0x06, 0x01, // Message class and ID (UBX-CFG-MSG) + 0x08, 0x00, // Length of payload (8 bytes) + 0xF0, 0x00, // NMEA ID for GGA + 0x01, // I/O Target 0=I/O, 1=UART1, 2=UART2, 3=USB, 4=SPI + 0x01, // Enable + 0x01, 0x01, 0x01, 0x01, // Reserved + 0x00, 0x00 // CK_A and CK_B (Checksum) + }; + UBXChecksum(_message_GGA, sizeof(_message_GGA)); _serial_gps->write(_message_GGA, sizeof(_message_GGA)); if (!getACK(0x06, 0x01)) { LOG_WARN("Unable to enable NMEA GGA.\n"); + return true; + } + + // The Power Management configuration allows the GPS module to operate in different power modes for optimized power + // consumption. + // The modes supported are: + // 0x00 = Full power: The module operates at full power with no power saving. + // 0x01 = Balanced: The module dynamically adjusts the tracking behavior to balance power consumption. + // 0x02 = Interval: The module operates in a periodic mode, cycling between tracking and power saving states. + // 0x03 = Aggressive with 1 Hz: The module operates in a power saving mode with a 1 Hz update rate. + // 0x04 = Aggressive with 2 Hz: The module operates in a power saving mode with a 2 Hz update rate. + // 0x05 = Aggressive with 4 Hz: The module operates in a power saving mode with a 4 Hz update rate. + // The 'period' field specifies the position update and search period. It is only valid when the powerSetupValue is + // set to Interval; otherwise, it must be set to '0'. The 'onTime' field specifies the duration of the ON phase and + // must be smaller than the period. It is only valid when the powerSetupValue is set to Interval; otherwise, it must + // be set to '0'. + byte UBX_CFG_PMS[14] = { + 0xB5, 0x62, // UBX sync characters + 0x06, 0x86, // Message class and ID (UBX-CFG-PMS) + 0x06, 0x00, // Length of payload (6 bytes) + 0x00, // Version (0) + 0x03, // Power setup value + 0x00, 0x00, // period: not applicable, set to 0 + 0x00, 0x00, // onTime: not applicable, set to 0 + 0x00, 0x00 // Placeholder for checksum, will be calculated next + }; + + // Calculate the checksum and update the message + UBXChecksum(UBX_CFG_PMS, sizeof(UBX_CFG_PMS)); + + // Send the message to the module + _serial_gps->write(UBX_CFG_PMS, sizeof(UBX_CFG_PMS)); + if (!getACK(0x06, 0x86)) { + LOG_WARN("Unable to enable powersaving for GPS.\n"); + return true; } // We need save configuration to flash to make our config changes persistent @@ -407,8 +542,10 @@ bool GPS::setupGPS() if (!getACK(0x06, 0x09)) { LOG_WARN("Unable to save GNSS module configuration.\n"); + return true; } else { LOG_INFO("GNSS module configuration saved!\n"); + return true; } } } @@ -824,8 +961,8 @@ GPS *createGps() LOG_DEBUG("Using MSL altitude model\n"); #endif if (GPS::_serial_gps) { - // Some boards might have only the TX line from the GPS connected, in that case, we can't configure it at all. Just - // assume NMEA at 9600 baud. + // Some boards might have only the TX line from the GPS connected, in that case, we can't configure it at all. + // Just assume NMEA at 9600 baud. GPS *new_gps = new NMEAGPS(); new_gps->setup(); return new_gps; @@ -837,4 +974,4 @@ GPS *createGps() } return nullptr; #endif -} +} \ No newline at end of file From 4ace59fc185174dfee59116c78656434e126dab4 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 14 Jul 2023 16:12:30 -0500 Subject: [PATCH 15/57] Partial Heltec Wireless Paper and Wireless Tracker support (#2594) * WIP * Comment * WIP * TFT_CTRL * Update platformio.ini update to current latest version available * Update EInkDisplay2.cpp Is the e-ink Display a DEPG0213BN ? * Logging * trunk fmt --------- Co-authored-by: Mark Trevor Birss --- src/gps/GPS.cpp | 6 ++- src/graphics/EInkDisplay2.cpp | 27 ++++++++-- src/graphics/TFTDisplay.cpp | 4 ++ src/main.cpp | 10 ++++ src/platform/esp32/architecture.h | 4 ++ variants/heltec_wireless_paper/platformio.ini | 8 +++ variants/heltec_wireless_paper/variant.h | 39 ++++++++++++++ .../heltec_wireless_tracker/platformio.ini | 23 ++++++++ variants/heltec_wireless_tracker/variant.h | 53 +++++++++++++++++++ 9 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 variants/heltec_wireless_paper/platformio.ini create mode 100644 variants/heltec_wireless_paper/variant.h create mode 100644 variants/heltec_wireless_tracker/platformio.ini create mode 100644 variants/heltec_wireless_tracker/variant.h diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index c6298f575..f4041c25f 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -181,10 +181,14 @@ bool GPS::setupGPS() config.position.tx_gpio = GPS_TX_PIN; #endif +//#define BAUD_RATE 115200 // ESP32 has a special set of parameters vs other arduino ports #if defined(ARCH_ESP32) - if (config.position.rx_gpio) + if (config.position.rx_gpio) { + LOG_DEBUG("Using GPIO%d for GPS RX\n", config.position.rx_gpio); + LOG_DEBUG("Using GPIO%d for GPS TX\n", config.position.tx_gpio); _serial_gps->begin(GPS_BAUDRATE, SERIAL_8N1, config.position.rx_gpio, config.position.tx_gpio); + } #else _serial_gps->begin(GPS_BAUDRATE); #endif diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 048e8dd6e..066f63be8 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -7,6 +7,10 @@ #include "main.h" #include +// #ifdef HELTEC_WIRELESS_PAPER +// SPIClass *hspi = NULL; +// #endif + #define COLORED GxEPD_BLACK #define UNCOLORED GxEPD_WHITE @@ -19,13 +23,13 @@ #define TECHO_DISPLAY_MODEL GxEPD2_213_BN // 4.2 inch 300x400 - GxEPD2_420_M01 -//#define TECHO_DISPLAY_MODEL GxEPD2_420_M01 +// #define TECHO_DISPLAY_MODEL GxEPD2_420_M01 // 2.9 inch 296x128 - GxEPD2_290_T5D -//#define TECHO_DISPLAY_MODEL GxEPD2_290_T5D +// #define TECHO_DISPLAY_MODEL GxEPD2_290_T5D // 1.54 inch 200x200 - GxEPD2_154_M09 -//#define TECHO_DISPLAY_MODEL GxEPD2_154_M09 +// #define TECHO_DISPLAY_MODEL GxEPD2_154_M09 #elif defined(MAKERPYTHON) // 2.9 inch 296x128 - GxEPD2_290_T5D @@ -41,6 +45,9 @@ // 1.54 inch 200x200 - GxEPD2_154_M09 #define TECHO_DISPLAY_MODEL GxEPD2_154_M09 +#elif defined(HELTEC_WIRELESS_PAPER) +//#define TECHO_DISPLAY_MODEL GxEPD2_213_T5D +#define TECHO_DISPLAY_MODEL GxEPD2_213_BN #endif GxEPD2_BW *adafruitDisplay; @@ -62,6 +69,10 @@ EInkDisplay::EInkDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY // GxEPD2_154_M09 // setGeometry(GEOMETRY_RAWMODE, 200, 200); + +#elif defined(HELTEC_WIRELESS_PAPER) + // setGeometry(GEOMETRY_RAWMODE, 212, 104); + setGeometry(GEOMETRY_RAWMODE, 250, 122); #elif defined(MAKERPYTHON) // GxEPD2_290_T5D setGeometry(GEOMETRY_RAWMODE, 296, 128); @@ -218,6 +229,16 @@ bool EInkDisplay::connect() (void)adafruitDisplay; } } +#elif defined(HELTEC_WIRELESS_PAPER) + { + auto lowLevel = new TECHO_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); + adafruitDisplay = new GxEPD2_BW(*lowLevel); + // hspi = new SPIClass(HSPI); + // hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS + adafruitDisplay->init(115200, true, 10, false, SPI, SPISettings(6000000, MSBFIRST, SPI_MODE0)); + adafruitDisplay->setRotation(3); + adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight); + } #elif defined(PCA10059) { auto lowLevel = new TECHO_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY); diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 8c07a4204..d667490fc 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -65,6 +65,10 @@ bool TFTDisplay::connect() digitalWrite(TFT_BL, HIGH); pinMode(TFT_BL, OUTPUT); #endif +#ifdef TFT_POWER_EN + digitalWrite(TFT_POWER_EN, HIGH); + pinMode(TFT_POWER_EN, OUTPUT); +#endif #ifdef ST7735_BACKLIGHT_EN digitalWrite(ST7735_BACKLIGHT_EN, HIGH); diff --git a/src/main.cpp b/src/main.cpp index c7cbb7680..5eff8262b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -214,6 +214,16 @@ void setup() digitalWrite(VEXT_ENABLE, 0); // turn on the display power #endif +#ifdef VGNSS_CTRL + pinMode(VGNSS_CTRL, OUTPUT); + digitalWrite(VGNSS_CTRL, LOW); +#endif + +#if defined(VTFT_CTRL) + pinMode(VTFT_CTRL, OUTPUT); + digitalWrite(VTFT_CTRL, LOW); +#endif + #ifdef RESET_OLED pinMode(RESET_OLED, OUTPUT); digitalWrite(RESET_OLED, 1); diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 7a1e9ba49..23346d493 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -97,6 +97,10 @@ #define HW_VENDOR meshtastic_HardwareModel_HELTEC_V3 #elif defined(HELTEC_WSL_V3) #define HW_VENDOR meshtastic_HardwareModel_HELTEC_WSL_V3 +#elif defined(HELTEC_WIRELESS_TRACKER) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_TRACKER +#elif defined(HELTEC_WIRELESS_PAPER) +#define HW_VENDOR meshtastic_HardwareModel_HELTEC_WIRELESS_PAPER #elif defined(TLORA_T3S3_V1) #define HW_VENDOR meshtastic_HardwareModel_TLORA_T3_S3 #elif defined(BETAFPV_2400_TX) diff --git a/variants/heltec_wireless_paper/platformio.ini b/variants/heltec_wireless_paper/platformio.ini new file mode 100644 index 000000000..0bc7f14d1 --- /dev/null +++ b/variants/heltec_wireless_paper/platformio.ini @@ -0,0 +1,8 @@ +[env:heltec-wireless-paper] +extends = esp32s3_base +board = heltec_wifi_lora_32_V3 +build_flags = + ${esp32s3_base.build_flags} -D HELTEC_WIRELESS_PAPER -I variants/heltec_wireless_paper +lib_deps = + ${esp32s3_base.lib_deps} + zinggjm/GxEPD2@^1.5.2 diff --git a/variants/heltec_wireless_paper/variant.h b/variants/heltec_wireless_paper/variant.h new file mode 100644 index 000000000..84c16c884 --- /dev/null +++ b/variants/heltec_wireless_paper/variant.h @@ -0,0 +1,39 @@ +#define LED_PIN 18 + +#define USE_EINK +/* + * eink display pins + */ +#define PIN_EINK_CS 4 +#define PIN_EINK_BUSY 7 +#define PIN_EINK_DC 5 +#define PIN_EINK_RES 6 +#define PIN_EINK_SCLK 3 +#define PIN_EINK_MOSI 2 + +#define VEXT_ENABLE Vext // active low, powers the oled display and the lora antenna boost +#define BUTTON_PIN 0 + +#define BATTERY_PIN 1 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO1_CHANNEL +#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider +#define ADC_MULTIPLIER 4.9 + +#define USE_SX1262 + +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define RF95_SCK 9 +#define RF95_MISO 11 +#define RF95_MOSI 10 +#define RF95_NSS 8 + +#define SX126X_CS RF95_NSS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_E22 \ No newline at end of file diff --git a/variants/heltec_wireless_tracker/platformio.ini b/variants/heltec_wireless_tracker/platformio.ini new file mode 100644 index 000000000..ec28fe6ca --- /dev/null +++ b/variants/heltec_wireless_tracker/platformio.ini @@ -0,0 +1,23 @@ +[env:heltec-wireless-tracker] +extends = esp32s3_base +board = heltec_wifi_lora_32_V3 +build_flags = + ${esp32s3_base.build_flags} -D HELTEC_WIRELESS_TRACKER -I variants/heltec_wireless_tracker + -DUSER_SETUP_LOADED + -DTFT_WIDTH=80 + -DTFT_HEIGHT=160 + -DST7735_GREENTAB160x80 + -DST7735_DRIVER + ;-TFT_RGB_ORDER=TFT_BGR + -DTFT_CS=38 + -DTFT_DC=40 + -DTFT_RST=39 + -DTFT_WR=42 + -DTFT_SCLK=41 + ;-DSPI_FREQUENCY=40000000 + ;-DSPI_FREQUENCY=27000000 + ;-DSPI_READ_FREQUENCY=16000000 + ;-DDISABLE_ALL_LIBRARY_WARNINGS +lib_deps = + ${esp32s3_base.lib_deps} + bodmer/TFT_eSPI@^2.4.76 \ No newline at end of file diff --git a/variants/heltec_wireless_tracker/variant.h b/variants/heltec_wireless_tracker/variant.h new file mode 100644 index 000000000..f1bb071ef --- /dev/null +++ b/variants/heltec_wireless_tracker/variant.h @@ -0,0 +1,53 @@ +#define LED_PIN 18 + +#define TFT_POWER_EN 46 + +#define ST7735_RESET 39 // Output +#define ST7735_CS 38 +#define ST7735_BACKLIGHT_EN 45 +#define ST7735_RS 40 +#define ST7735_SDA 42 +#define ST7735_SCK 41 + +// #define RESET_OLED 21 +// #define I2C_SDA 17 // I2C pins for this board +// #define I2C_SCL 18 + +#define SCREEN_TRANSITION_FRAMERATE 1 // fps + +#define VEXT_ENABLE Vext // active low, powers the oled display and the lora antenna boost +#define BUTTON_PIN 0 + +#define BATTERY_PIN 1 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO1_CHANNEL +#define ADC_ATTENUATION ADC_ATTEN_DB_2_5 // lower dB for high resistance voltage divider +#define ADC_MULTIPLIER 4.9 + +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 33 +#define GPS_TX_PIN 34 +#define PIN_GPS_RESET 35 +#define PIN_GPS_PPS 36 +#define VGNSS_CTRL 37 // Heltec Tracker needs this pulled low for GPS + +#define VTFT_CTRL 46 // Heltec Tracker needs this pulled low for TFT + +#define USE_SX1262 + +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 12 +#define LORA_DIO1 14 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define RF95_SCK 9 +#define RF95_MISO 11 +#define RF95_MOSI 10 +#define RF95_NSS 8 + +#define SX126X_CS RF95_NSS +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_E22 \ No newline at end of file From 003047baaf792525c13c4ed12ccbb914df6f5628 Mon Sep 17 00:00:00 2001 From: luzpaz Date: Fri, 14 Jul 2023 17:25:20 -0400 Subject: [PATCH 16/57] Fix various typos (#2607) * Fix various typos Found via `codespell -q 3 -L acount,clen,dout` * Trunk reformatting --------- Co-authored-by: code8buster Co-authored-by: Ben Meadors --- src/AccelerometerThread.h | 2 +- src/ButtonThread.h | 2 +- src/OSTimer.cpp | 3 ++- src/Power.cpp | 4 ++-- src/PowerFSM.cpp | 2 +- src/airtime.h | 4 ++-- src/concurrency/InterruptableDelay.h | 2 +- src/gps/GPS.cpp | 10 ++++---- src/gps/GPS.h | 2 +- src/gps/GeoCoord.cpp | 12 +++++----- src/gps/GeoCoord.h | 2 +- src/gps/RTC.cpp | 2 +- src/graphics/EInkDisplay2.cpp | 2 +- src/graphics/Screen.cpp | 6 ++--- src/graphics/TFTDisplay.cpp | 2 +- src/main.cpp | 4 ++-- src/main.h | 2 +- src/mesh/Channels.h | 2 +- src/mesh/FloodingRouter.h | 2 +- src/mesh/MeshModule.cpp | 6 ++--- src/mesh/MeshModule.h | 2 +- src/mesh/MeshService.cpp | 2 +- src/mesh/MeshService.h | 2 +- src/mesh/MeshTypes.h | 2 +- src/mesh/NodeDB.cpp | 4 ++-- src/mesh/NodeDB.h | 2 +- src/mesh/RF95Interface.cpp | 4 ++-- src/mesh/RadioInterface.cpp | 2 +- src/mesh/RadioInterface.h | 8 +++---- src/mesh/RadioLibInterface.cpp | 2 +- src/mesh/Router.cpp | 2 +- src/mesh/Router.h | 2 +- src/mesh/SX126xInterface.cpp | 2 +- src/mesh/SX128xInterface.cpp | 2 +- src/mesh/SinglePortModule.h | 2 +- src/mesh/compression/unishox2.c | 8 +++---- src/mesh/compression/unishox2.h | 26 ++++++++++----------- src/mesh/generated/meshtastic/channel.pb.h | 2 +- src/mesh/generated/meshtastic/config.pb.h | 6 ++--- src/mesh/generated/meshtastic/mesh.pb.h | 8 +++---- src/mesh/generated/meshtastic/portnums.pb.h | 2 +- src/mesh/http/ContentHandler.cpp | 6 ++--- src/mesh/http/WebServer.cpp | 2 +- src/mesh/mesh-pb-constants.cpp | 2 +- src/modules/CannedMessageModule.cpp | 2 +- src/modules/RangeTestModule.cpp | 10 ++++---- src/modules/SerialModule.cpp | 4 ++-- src/modules/esp32/StoreForwardModule.cpp | 4 ++-- src/mqtt/JSONValue.cpp | 2 +- src/mqtt/MQTT.cpp | 2 +- src/mqtt/MQTT.h | 2 +- src/platform/esp32/CallbackCharacteristic.h | 2 +- src/platform/esp32/main-esp32.cpp | 2 +- src/platform/nrf52/JLINK_MONITOR_ISR_SES.S | 2 +- src/platform/nrf52/NRF52Bluetooth.cpp | 2 +- src/platform/portduino/SimRadio.cpp | 2 +- src/platform/stm32wl/InternalFileSystem.cpp | 6 ++--- src/sleep.cpp | 2 +- 58 files changed, 110 insertions(+), 109 deletions(-) diff --git a/src/AccelerometerThread.h b/src/AccelerometerThread.h index 875ca2e22..307ca233e 100644 --- a/src/AccelerometerThread.h +++ b/src/AccelerometerThread.h @@ -43,7 +43,7 @@ class AccelerometerThread : public concurrency::OSThread } else if (accleremoter_type == ScanI2C::DeviceType::LIS3DH && lis.begin(accelerometer_found.address)) { LOG_DEBUG("LIS3DH initializing\n"); lis.setRange(LIS3DH_RANGE_2_G); - // Adjust threshhold, higher numbers are less sensitive + // Adjust threshold, higher numbers are less sensitive lis.setClick(config.device.double_tap_as_button_press ? 2 : 1, ACCELEROMETER_CLICK_THRESHOLD); } } diff --git a/src/ButtonThread.h b/src/ButtonThread.h index 135d727c2..f03d2861a 100644 --- a/src/ButtonThread.h +++ b/src/ButtonThread.h @@ -98,7 +98,7 @@ class ButtonThread : public concurrency::OSThread userButtonTouch.tick(); canSleep &= userButtonTouch.isIdle(); #endif - // if (!canSleep) LOG_DEBUG("Supressing sleep!\n"); + // if (!canSleep) LOG_DEBUG("Suppressing sleep!\n"); // else LOG_DEBUG("sleep ok\n"); return 5; diff --git a/src/OSTimer.cpp b/src/OSTimer.cpp index 0f7177a87..21744615f 100644 --- a/src/OSTimer.cpp +++ b/src/OSTimer.cpp @@ -5,7 +5,8 @@ * Schedule a callback to run. The callback must _not_ block, though it is called from regular thread level (not ISR) * * NOTE! xTimerPend... seems to ignore the time passed in on ESP32 and on NRF52 - * The reason this didn't work is bcause xTimerPednFunctCall really isn't a timer function at all - it just means run the callback + * The reason this didn't work is because xTimerPednFunctCall really isn't a timer function at all - it just means run the +callback * from the timer thread the next time you have spare cycles. * * @return true if successful, false if the timer fifo is too full. diff --git a/src/Power.cpp b/src/Power.cpp index 37d80a31f..ac1789cb0 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -50,7 +50,7 @@ XPowersLibInterface *PMU = NULL; #else // Copy of the base class defined in axp20x.h. -// I'd rather not inlude axp20x.h as it brings Wire dependency. +// I'd rather not include axp20x.h as it brings Wire dependency. class HasBatteryLevel { public: @@ -712,7 +712,7 @@ bool Power::axpChipInit() PMU->setPowerChannelVoltage(XPOWERS_ALDO1, 3300); PMU->enablePowerOutput(XPOWERS_ALDO1); - // sdcard power channle + // sdcard power channel PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); PMU->enablePowerOutput(XPOWERS_BLDO1); diff --git a/src/PowerFSM.cpp b/src/PowerFSM.cpp index e2cf94258..2d42ef655 100644 --- a/src/PowerFSM.cpp +++ b/src/PowerFSM.cpp @@ -352,5 +352,5 @@ void PowerFSM_setup() "mesh timeout"); #endif - powerFSM.run_machine(); // run one interation of the state machine, so we run our on enter tasks for the initial DARK state + powerFSM.run_machine(); // run one iteration of the state machine, so we run our on enter tasks for the initial DARK state } \ No newline at end of file diff --git a/src/airtime.h b/src/airtime.h index cb5f8bf6d..3ed7b6d7c 100644 --- a/src/airtime.h +++ b/src/airtime.h @@ -17,9 +17,9 @@ Example analytics: - TX_LOG + RX_LOG = Total air time for a perticular meshtastic channel. + TX_LOG + RX_LOG = Total air time for a particular meshtastic channel. - TX_LOG + RX_LOG = Total air time for a perticular meshtastic channel, including + TX_LOG + RX_LOG = Total air time for a particular meshtastic channel, including other lora radios. RX_ALL_LOG - RX_LOG = Other lora radios on our frequency channel. diff --git a/src/concurrency/InterruptableDelay.h b/src/concurrency/InterruptableDelay.h index 2b499073a..41bc40a21 100644 --- a/src/concurrency/InterruptableDelay.h +++ b/src/concurrency/InterruptableDelay.h @@ -18,7 +18,7 @@ namespace concurrency * * Useful for they top level loop() delay call to keep the CPU powered down until our next scheduled event or some external event. * - * This is implmented for FreeRTOS but should be easy to port to other operating systems. + * This is implemented for FreeRTOS but should be easy to port to other operating systems. */ class InterruptableDelay { diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index f4041c25f..59e09f5e5 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -129,12 +129,12 @@ int GPS::getAck(uint8_t *buffer, uint16_t size, uint8_t requestedClass, uint8_t } break; case 4: - // Payload lenght lsb + // Payload length lsb needRead = c; ubxFrameCounter++; break; case 5: - // Payload lenght msb + // Payload length msb needRead |= (c << 8); ubxFrameCounter++; break; @@ -147,7 +147,7 @@ int GPS::getAck(uint8_t *buffer, uint16_t size, uint8_t requestedClass, uint8_t if (_serial_gps->readBytes(buffer, needRead) != needRead) { ubxFrameCounter = 0; } else { - // return payload lenght + // return payload length return needRead; } break; @@ -258,7 +258,7 @@ bool GPS::setupGPS() return true; } - // Enable interference resistance, because we are using LoRa, WiFi and Bluetoot on same board, + // Enable interference resistance, because we are using LoRa, WiFi and Bluetooth on same board, // and we need to reduce interference from them byte _message_JAM[16] = { 0xB5, 0x62, // UBX protocol sync characters @@ -674,7 +674,7 @@ void GPS::setAwake(bool on) } } -/** Get how long we should stay looking for each aquisition in msecs +/** Get how long we should stay looking for each acquisition in msecs */ uint32_t GPS::getWakeTime() const { diff --git a/src/gps/GPS.h b/src/gps/GPS.h index 32f309789..c06c2fa91 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -149,7 +149,7 @@ class GPS : private concurrency::OSThread */ void setAwake(bool on); - /** Get how long we should stay looking for each aquisition + /** Get how long we should stay looking for each acquisition */ uint32_t getWakeTime() const; diff --git a/src/gps/GeoCoord.cpp b/src/gps/GeoCoord.cpp index 9d5e6315e..19a753c02 100644 --- a/src/gps/GeoCoord.cpp +++ b/src/gps/GeoCoord.cpp @@ -12,7 +12,7 @@ GeoCoord::GeoCoord(int32_t lat, int32_t lon, int32_t alt) : _latitude(lat), _lon GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt) { - // Change decimial reprsentation to int32_t. I.e., 12.345 becomes 123450000 + // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000 _latitude = int32_t(lat * 1e+7); _longitude = int32_t(lon * 1e+7); GeoCoord::setCoords(); @@ -20,7 +20,7 @@ GeoCoord::GeoCoord(float lat, float lon, int32_t alt) : _altitude(alt) GeoCoord::GeoCoord(double lat, double lon, int32_t alt) : _altitude(alt) { - // Change decimial reprsentation to int32_t. I.e., 12.345 becomes 123450000 + // Change decimial representation to int32_t. I.e., 12.345 becomes 123450000 _latitude = int32_t(lat * 1e+7); _longitude = int32_t(lon * 1e+7); GeoCoord::setCoords(); @@ -41,7 +41,7 @@ void GeoCoord::setCoords() void GeoCoord::updateCoords(int32_t lat, int32_t lon, int32_t alt) { - // If marked dirty or new coordiantes + // If marked dirty or new coordinates if (_dirty || _latitude != lat || _longitude != lon || _altitude != alt) { _dirty = true; _latitude = lat; @@ -55,7 +55,7 @@ void GeoCoord::updateCoords(const double lat, const double lon, const int32_t al { int32_t iLat = lat * 1e+7; int32_t iLon = lon * 1e+7; - // If marked dirty or new coordiantes + // If marked dirty or new coordinates if (_dirty || _latitude != iLat || _longitude != iLon || _altitude != alt) { _dirty = true; _latitude = iLat; @@ -69,7 +69,7 @@ void GeoCoord::updateCoords(const float lat, const float lon, const int32_t alt) { int32_t iLat = lat * 1e+7; int32_t iLon = lon * 1e+7; - // If marked dirty or new coordiantes + // If marked dirty or new coordinates if (_dirty || _latitude != iLat || _longitude != iLon || _altitude != alt) { _dirty = true; _latitude = iLat; @@ -217,7 +217,7 @@ void GeoCoord::latLongToOSGR(const double lat, const double lon, OSGR &osgr) double eta2 = v / rho - 1; double mA = (1 + n + (5 / 4) * n * n + (5 / 4) * n * n * n) * (phi - phi0); double mB = (3 * n + 3 * n * n + (21 / 8) * n * n * n) * sin(phi - phi0) * cos(phi + phi0); - // loss of precision in mC & mD due to floating point rounding can cause innaccuracy of northing by a few meters + // loss of precision in mC & mD due to floating point rounding can cause inaccuracy of northing by a few meters double mC = (15 / 8 * n * n + 15 / 8 * n * n * n) * sin(2 * (phi - phi0)) * cos(2 * (phi + phi0)); double mD = (35 / 24) * n * n * n * sin(3 * (phi - phi0)) * cos(3 * (phi + phi0)); double m = b * f0 * (mA - mB + mC - mD); diff --git a/src/gps/GeoCoord.h b/src/gps/GeoCoord.h index 28e9e14e9..06b11c3de 100644 --- a/src/gps/GeoCoord.h +++ b/src/gps/GeoCoord.h @@ -65,7 +65,7 @@ struct MGRS { uint32_t northing; }; -// A struct to hold the data for a OSGR coordiante +// A struct to hold the data for a OSGR coordinate struct OSGR { char e100k; char n100k; diff --git a/src/gps/RTC.cpp b/src/gps/RTC.cpp index 118c2128c..b80fd04aa 100644 --- a/src/gps/RTC.cpp +++ b/src/gps/RTC.cpp @@ -19,7 +19,7 @@ static uint64_t zeroOffsetSecs; // GPS based time in secs since 1970 - only upda void readFromRTC() { - struct timeval tv; /* btw settimeofday() is helpfull here too*/ + struct timeval tv; /* btw settimeofday() is helpful here too*/ #ifdef RV3028_RTC if (rtc_found.address == RV3028_RTC) { uint32_t now = millis(); diff --git a/src/graphics/EInkDisplay2.cpp b/src/graphics/EInkDisplay2.cpp index 066f63be8..61d0eea5a 100644 --- a/src/graphics/EInkDisplay2.cpp +++ b/src/graphics/EInkDisplay2.cpp @@ -120,7 +120,7 @@ bool EInkDisplay::forceDisplay(uint32_t msecLimit) for (uint32_t y = 0; y < displayHeight; y++) { for (uint32_t x = 0; x < displayWidth; x++) { - // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficent + // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient auto b = buffer[x + (y / 8) * displayWidth]; auto isset = b & (1 << (y & 7)); adafruitDisplay->drawPixel(x, y, isset ? COLORED : UNCOLORED); diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index cf7ed1ad0..3105ee218 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -360,7 +360,7 @@ static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *sta display->drawString(0 + x, FONT_HEIGHT_MEDIUM + y, "For help, please visit \nmeshtastic.org"); } -// Ignore messages orginating from phone (from the current node 0x0) unless range test or store and forward module are enabled +// Ignore messages originating from phone (from the current node 0x0) unless range test or store and forward module are enabled static bool shouldDrawMessage(const meshtastic_MeshPacket *packet) { return packet->from != 0 && !moduleConfig.range_test.enabled && !moduleConfig.store_forward.enabled; @@ -442,7 +442,7 @@ static void drawWaypointFrame(OLEDDisplay *display, OLEDDisplayUiState *state, i } } -/// Draw a series of fields in a column, wrapping to multiple colums if needed +/// Draw a series of fields in a column, wrapping to multiple columns if needed static void drawColumns(OLEDDisplay *display, int16_t x, int16_t y, const char **fields) { // The coordinates define the left starting point of the text @@ -1789,7 +1789,7 @@ void DebugInfo::drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *stat heartbeat = !heartbeat; #endif } -// adjust Brightness cycle trough 1 to 254 as long as attachDuringLongPress is true +// adjust Brightness cycle through 1 to 254 as long as attachDuringLongPress is true void Screen::adjustBrightness() { if (!useDisplay) diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index d667490fc..a7d33174a 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -26,7 +26,7 @@ void TFTDisplay::display(void) for (y = 0; y < displayHeight; y++) { for (x = 0; x < displayWidth; x++) { - // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficent + // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient auto isset = buffer[x + (y / 8) * displayWidth] & (1 << (y & 7)); auto dblbuf_isset = buffer_back[x + (y / 8) * displayWidth] & (1 << (y & 7)); if (isset != dblbuf_isset) { diff --git a/src/main.cpp b/src/main.cpp index 5eff8262b..e503aadcf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -169,7 +169,7 @@ SPISettings spiSettings(4000000, MSBFIRST, SPI_MODE0); RadioInterface *rIf = NULL; /** - * Some platforms (nrf52) might provide an alterate version that supresses calling delay from sleep. + * Some platforms (nrf52) might provide an alterate version that suppresses calling delay from sleep. */ __attribute__((weak, noinline)) bool loopCanSleep() { @@ -710,7 +710,7 @@ uint32_t rebootAtMsec; // If not zero we will reboot at this time (used to reb uint32_t shutdownAtMsec; // If not zero we will shutdown at this time (used to shutdown from python or mobile client) // If a thread does something that might need for it to be rescheduled ASAP it can set this flag -// This will supress the current delay and instead try to run ASAP. +// This will suppress the current delay and instead try to run ASAP. bool runASAP; extern meshtastic_DeviceMetadata getDeviceMetadata() diff --git a/src/main.h b/src/main.h index 93baec590..c13c92bd4 100644 --- a/src/main.h +++ b/src/main.h @@ -59,7 +59,7 @@ extern uint32_t shutdownAtMsec; extern uint32_t serialSinceMsec; // If a thread does something that might need for it to be rescheduled ASAP it can set this flag -// This will supress the current delay and instead try to run ASAP. +// This will suppress the current delay and instead try to run ASAP. extern bool runASAP; void nrf52Setup(), esp32Setup(), nrf52Loop(), esp32Loop(), clearBonds(); diff --git a/src/mesh/Channels.h b/src/mesh/Channels.h index f3d696f90..b4bdcbd5c 100644 --- a/src/mesh/Channels.h +++ b/src/mesh/Channels.h @@ -20,7 +20,7 @@ class Channels /// The index of the primary channel ChannelIndex primaryIndex = 0; - /** The channel index that was requested for sending/receving. Note: if this channel is a secondary + /** The channel index that was requested for sending/receiving. Note: if this channel is a secondary channel and does not have a PSK, we will use the PSK from the primary channel. If this channel is disabled no sending or receiving will be allowed */ ChannelIndex activeChannelIndex = 0; diff --git a/src/mesh/FloodingRouter.h b/src/mesh/FloodingRouter.h index 73dbd1f3f..563b850a5 100644 --- a/src/mesh/FloodingRouter.h +++ b/src/mesh/FloodingRouter.h @@ -48,7 +48,7 @@ class FloodingRouter : public Router, protected PacketHistory /** * Should this incoming filter be dropped? * - * Called immedately on receiption, before any further processing. + * Called immediately on reception, before any further processing. * @return true to abandon the packet */ virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) override; diff --git a/src/mesh/MeshModule.cpp b/src/mesh/MeshModule.cpp index 55948b33f..a1e719721 100644 --- a/src/mesh/MeshModule.cpp +++ b/src/mesh/MeshModule.cpp @@ -18,7 +18,7 @@ meshtastic_MeshPacket *MeshModule::currentReply; MeshModule::MeshModule(const char *_name) : name(_name) { - // Can't trust static initalizer order, so we check each time + // Can't trust static initializer order, so we check each time if (!modules) modules = new std::vector(); @@ -39,7 +39,7 @@ meshtastic_MeshPacket *MeshModule::allocAckNak(meshtastic_Routing_Error err, Nod c.error_reason = err; c.which_variant = meshtastic_Routing_error_reason_tag; - // Now that we have moded sendAckNak up one level into the class heirarchy we can no longer assume we are a RoutingPlugin + // Now that we have moded sendAckNak up one level into the class hierarchy we can no longer assume we are a RoutingPlugin // So we manually call pb_encode_to_bytes and specify routing port number // auto p = allocDataProtobuf(c); meshtastic_MeshPacket *p = router->allocForSending(); @@ -169,7 +169,7 @@ void MeshModule::callPlugins(const meshtastic_MeshPacket &mp, RxSource src) // Note: if the message started with the local node or a module asked to ignore the request, we don't want to send a // no response reply - // No one wanted to reply to this requst, tell the requster that happened + // No one wanted to reply to this request, tell the requster that happened LOG_DEBUG("No one responded, send a nak\n"); // SECURITY NOTE! I considered sending back a different error code if we didn't find the psk (i.e. !isDecoded) diff --git a/src/mesh/MeshModule.h b/src/mesh/MeshModule.h index 2eee04f5d..323cc8595 100644 --- a/src/mesh/MeshModule.h +++ b/src/mesh/MeshModule.h @@ -47,7 +47,7 @@ typedef struct _UIFrameEvent { * A key concept for this is that your module should use a particular "portnum" for each message type you want to receive * and handle. * - * Interally we use modules to implement the core meshtastic text messaging and gps position sharing features. You + * Internally we use modules to implement the core meshtastic text messaging and gps position sharing features. You * can use these classes as examples for how to write your own custom module. See here: (FIXME) */ class MeshModule diff --git a/src/mesh/MeshService.cpp b/src/mesh/MeshService.cpp index 64741619f..32565cb29 100644 --- a/src/mesh/MeshService.cpp +++ b/src/mesh/MeshService.cpp @@ -34,7 +34,7 @@ arbitrating to select a node number and keeping the current nodedb. /* Broadcast when a newly powered mesh node wants to find a node num it can use -The algoritm is as follows: +The algorithm is as follows: * when a node starts up, it broadcasts their user and the normal flow is for all other nodes to reply with their User as well (so the new node can build its node db) * If a node ever receives a User (not just the first broadcast) message where the sender node number equals our node number, that diff --git a/src/mesh/MeshService.h b/src/mesh/MeshService.h index 3cc197a5a..fa184b391 100644 --- a/src/mesh/MeshService.h +++ b/src/mesh/MeshService.h @@ -120,7 +120,7 @@ class MeshService private: /// Called when our gps position has changed - updates nodedb and sends Location message out into the mesh - /// returns 0 to allow futher processing + /// returns 0 to allow further processing int onGPSChanged(const meshtastic::GPSStatus *arg); /// Handle a packet that just arrived from the radio. This method does _ReliableRouternot_ free the provided packet. If it diff --git a/src/mesh/MeshTypes.h b/src/mesh/MeshTypes.h index ee23b9158..5b2cbd1b1 100644 --- a/src/mesh/MeshTypes.h +++ b/src/mesh/MeshTypes.h @@ -13,7 +13,7 @@ typedef uint32_t PacketId; // A packet sequence number #define ERRNO_OK 0 #define ERRNO_NO_INTERFACES 33 #define ERRNO_UNKNOWN 32 // pick something that doesn't conflict with RH_ROUTER_ERROR_UNABLE_TO_DELIVER -#define ERRNO_DISABLED 34 // the itnerface is disabled +#define ERRNO_DISABLED 34 // the interface is disabled /* * Source of a received message diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 565a08ff9..0192544fb 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -138,7 +138,7 @@ bool NodeDB::factoryReset() // third, write everything to disk saveToDisk(); #ifdef ARCH_ESP32 - // This will erase what's in NVS including ssl keys, persistant variables and ble pairing + // This will erase what's in NVS including ssl keys, persistent variables and ble pairing nvs_flash_erase(); #endif #ifdef ARCH_NRF52 @@ -911,7 +911,7 @@ void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, co error_code = code; error_address = address; - // Currently portuino is mostly used for simulation. Make sue the user notices something really bad happend + // Currently portuino is mostly used for simulation. Make sure the user notices something really bad happened #ifdef ARCH_PORTDUINO LOG_ERROR("A critical failure occurred, portduino is exiting..."); exit(2); diff --git a/src/mesh/NodeDB.h b/src/mesh/NodeDB.h index 7c8972861..9b0249dcd 100644 --- a/src/mesh/NodeDB.h +++ b/src/mesh/NodeDB.h @@ -56,7 +56,7 @@ class NodeDB meshtastic_NodeInfoLite *updateGUIforNode = NULL; // if currently showing this node, we think you should update the GUI Observable newStatus; - /// don't do mesh based algoritm for node id assignment (initially) + /// don't do mesh based algorithm for node id assignment (initially) /// instead just store in flash - possibly even in the initial alpha release do this hack NodeDB(); diff --git a/src/mesh/RF95Interface.cpp b/src/mesh/RF95Interface.cpp index cf9cd9477..3102aa029 100644 --- a/src/mesh/RF95Interface.cpp +++ b/src/mesh/RF95Interface.cpp @@ -192,7 +192,7 @@ bool RF95Interface::isChannelActive() return false; } -/** Could we send right now (i.e. either not actively receving or transmitting)? */ +/** Could we send right now (i.e. either not actively receiving or transmitting)? */ bool RF95Interface::isActivelyReceiving() { return lora->isReceiving(); @@ -201,7 +201,7 @@ bool RF95Interface::isActivelyReceiving() bool RF95Interface::sleep() { // put chipset into sleep mode - setStandby(); // First cancel any active receving/sending + setStandby(); // First cancel any active receiving/sending lora->sleep(); return true; diff --git a/src/mesh/RadioInterface.cpp b/src/mesh/RadioInterface.cpp index 8bc66f3b5..61b6b85d0 100644 --- a/src/mesh/RadioInterface.cpp +++ b/src/mesh/RadioInterface.cpp @@ -308,7 +308,7 @@ bool RadioInterface::init() preflightSleepObserver.observe(&preflightSleep); notifyDeepSleepObserver.observe(¬ifyDeepSleep); - // we now expect interfaces to operate in promiscous mode + // we now expect interfaces to operate in promiscuous mode // radioIf.setThisAddress(nodeDB.getNodeNum()); // Note: we must do this here, because the nodenum isn't inited at constructor // time. diff --git a/src/mesh/RadioInterface.h b/src/mesh/RadioInterface.h index b5fb5fcd3..9c5d66293 100644 --- a/src/mesh/RadioInterface.h +++ b/src/mesh/RadioInterface.h @@ -15,7 +15,7 @@ /** * This structure has to exactly match the wire layout when sent over the radio link. Used to keep compatibility - * wtih the old radiohead implementation. + * with the old radiohead implementation. */ typedef struct { NodeNum to, from; // can be 1 byte or four bytes @@ -75,7 +75,7 @@ class RadioInterface uint32_t lastTxStart = 0L; /** - * A temporary buffer used for sending/receving packets, sized to hold the biggest buffer we might need + * A temporary buffer used for sending/receiving packets, sized to hold the biggest buffer we might need * */ uint8_t radiobuf[MAX_RHPACKETLEN]; @@ -198,7 +198,7 @@ class RadioInterface virtual void saveFreq(float savedFreq); /** - * Save the chanel we selected for later reuse. + * Save the channel we selected for later reuse. */ virtual void saveChannelNum(uint32_t savedChannelNum); @@ -206,7 +206,7 @@ class RadioInterface /** * Convert our modemConfig enum into wf, sf, etc... * - * These paramaters will be pull from the channelSettings global + * These parameters will be pull from the channelSettings global */ void applyModemConfig(); diff --git a/src/mesh/RadioLibInterface.cpp b/src/mesh/RadioLibInterface.cpp index a74d41090..4f0c52e67 100644 --- a/src/mesh/RadioLibInterface.cpp +++ b/src/mesh/RadioLibInterface.cpp @@ -68,7 +68,7 @@ void INTERRUPT_ATTR RadioLibInterface::isrTxLevel0() */ RadioLibInterface *RadioLibInterface::instance; -/** Could we send right now (i.e. either not actively receving or transmitting)? */ +/** Could we send right now (i.e. either not actively receiving or transmitting)? */ bool RadioLibInterface::canSendImmediately() { // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). diff --git a/src/mesh/Router.cpp b/src/mesh/Router.cpp index 294b2531f..e605cfc94 100644 --- a/src/mesh/Router.cpp +++ b/src/mesh/Router.cpp @@ -399,7 +399,7 @@ meshtastic_Routing_Error perhapsEncode(meshtastic_MeshPacket *p) if (compressed_len >= p->decoded.payload.size) { LOG_DEBUG("Not using compressing message.\n"); - // Set the uncompressed payload varient anyway. Shouldn't hurt? + // Set the uncompressed payload variant anyway. Shouldn't hurt? // p->decoded.which_payloadVariant = Data_payload_tag; // Otherwise we use the compressor diff --git a/src/mesh/Router.h b/src/mesh/Router.h index f43f92158..db810e42e 100644 --- a/src/mesh/Router.h +++ b/src/mesh/Router.h @@ -90,7 +90,7 @@ class Router : protected concurrency::OSThread * * FIXME, move this into the new RoutingModule and do the filtering there using the regular module logic * - * Called immedately on receiption, before any further processing. + * Called immediately on reception, before any further processing. * @return true to abandon the packet */ virtual bool shouldFilterReceived(const meshtastic_MeshPacket *p) { return false; } diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 495840d50..144b8847d 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -249,7 +249,7 @@ template bool SX126xInterface::isChannelActive() return false; } -/** Could we send right now (i.e. either not actively receving or transmitting)? */ +/** Could we send right now (i.e. either not actively receiving or transmitting)? */ template bool SX126xInterface::isActivelyReceiving() { // The IRQ status will be cleared when we start our read operation. Check if we've started a header, but haven't yet diff --git a/src/mesh/SX128xInterface.cpp b/src/mesh/SX128xInterface.cpp index 9c3ec3e91..f056f7369 100644 --- a/src/mesh/SX128xInterface.cpp +++ b/src/mesh/SX128xInterface.cpp @@ -242,7 +242,7 @@ template bool SX128xInterface::isChannelActive() return false; } -/** Could we send right now (i.e. either not actively receving or transmitting)? */ +/** Could we send right now (i.e. either not actively receiving or transmitting)? */ template bool SX128xInterface::isActivelyReceiving() { uint16_t irq = lora.getIrqStatus(); diff --git a/src/mesh/SinglePortModule.h b/src/mesh/SinglePortModule.h index 6fa69d964..a5aaa2582 100644 --- a/src/mesh/SinglePortModule.h +++ b/src/mesh/SinglePortModule.h @@ -3,7 +3,7 @@ #include "Router.h" /** - * Most modules are only interested in sending/receving one particular portnum. This baseclass simplifies that common + * Most modules are only interested in sending/receiving one particular portnum. This baseclass simplifies that common * case. */ class SinglePortModule : public MeshModule diff --git a/src/mesh/compression/unishox2.c b/src/mesh/compression/unishox2.c index 1632f970a..99c62f659 100644 --- a/src/mesh/compression/unishox2.c +++ b/src/mesh/compression/unishox2.c @@ -57,7 +57,7 @@ uint8_t usx_code_94[94]; uint8_t usx_vcodes[] = {0x00, 0x40, 0x60, 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xD8, 0xE0, 0xE4, 0xE8, 0xEC, 0xEE, 0xF0, 0xF2, 0xF4, 0xF6, 0xF7, 0xF8, 0xF9, 0xFA, 0xFB, 0xFC, 0xFD, 0xFE, 0xFF}; -/// Length of each veritical code +/// Length of each vertical code uint8_t usx_vcode_lens[] = {2, 3, 3, 4, 4, 4, 4, 4, 5, 5, 6, 6, 6, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8}; /// Vertical Codes and Set number for frequent sequences in sets USX_SYM and USX_NUM. First 3 bits indicate set (USX_SYM/USX_NUM) @@ -188,7 +188,7 @@ int append_switch_code(char *out, int olen, int ol, uint8_t state) return ol; } -/// Appends given horizontal and veritical code bits to out +/// Appends given horizontal and vertical code bits to out int append_code(char *out, int olen, int ol, uint8_t code, uint8_t *state, const uint8_t usx_hcodes[], const uint8_t usx_hcode_lens[]) { @@ -888,7 +888,7 @@ int read8bitCode(const char *in, int len, int bit_no) return code; } -/// The list of veritical codes is split into 5 sections. Used by readVCodeIdx() +/// The list of vertical codes is split into 5 sections. Used by readVCodeIdx() #define SECTION_COUNT 5 /// Used by readVCodeIdx() for finding the section under which the code read using read8bitCode() falls uint8_t usx_vsections[] = {0x7F, 0xBF, 0xDF, 0xEF, 0xFF}; @@ -915,7 +915,7 @@ uint8_t usx_vcode_lookup[36] = {(1 << 5) + 0, (1 << 5) + 0, (2 << 5) + 1, (2 /// compared to using a 256 uint8_t buffer to decode the next 8 bits read by read8bitCode() \n /// by splitting the list of vertical codes. \n /// Decoder is designed for using less memory, not speed. \n -/// Returns the veritical code index or 99 if match could not be found. \n +/// Returns the vertical code index or 99 if match could not be found. \n /// Also updates bit_no_p with how many ever bits used by the vertical code. int readVCodeIdx(const char *in, int len, int *bit_no_p) { diff --git a/src/mesh/compression/unishox2.h b/src/mesh/compression/unishox2.h index e3674fb7c..a5117a5de 100644 --- a/src/mesh/compression/unishox2.h +++ b/src/mesh/compression/unishox2.h @@ -198,45 +198,45 @@ 2, 2, 2, 2, 0 \ } -/// Default frequently occuring sequences. When composition of text is know beforehand, the other sequences in this section can be -/// used to achieve more compression. +/// Default frequently occurring sequences. When composition of text is know beforehand, the other sequences in this section can +/// be used to achieve more compression. #define USX_FREQ_SEQ_DFLT \ (const char *[]) \ { \ "\": \"", "\": ", "" \ } -/// Frequently occuring sequences in XML content +/// Frequently occurring sequences in XML content #define USX_FREQ_SEQ_XML \ (const char *[]) \ { \ "", ". + In the case of Signal that would mean +16504442323, for the default macaddr derived id it would be !<8 hexadecimal bytes>. Note: app developers are encouraged to also use the following standard node IDs "^all" (for broadcast), "^local" (for the locally connected node) */ char id[16]; @@ -418,7 +418,7 @@ typedef struct _meshtastic_Routing { typedef PB_BYTES_ARRAY_T(237) meshtastic_Data_payload_t; /* (Formerly called SubPacket) - The payload portion fo a packet, this is the actual bytes that are sent + The payload portion for a packet, this is the actual bytes that are sent inside a radio packet (because from/to are broken out by the comms library) */ typedef struct _meshtastic_Data { /* Formerly named typ and of type Type */ @@ -552,7 +552,7 @@ typedef struct _meshtastic_MeshPacket { /* The priority of this message for sending. See MeshPacket.Priority description for more details. */ meshtastic_MeshPacket_Priority priority; - /* rssi of received packet. Only sent to phone for dispay purposes. */ + /* rssi of received packet. Only sent to phone for display purposes. */ int32_t rx_rssi; /* Describe if this message is delayed */ meshtastic_MeshPacket_Delayed delayed; diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h index e4aaeeb96..e089f9182 100644 --- a/src/mesh/generated/meshtastic/portnums.pb.h +++ b/src/mesh/generated/meshtastic/portnums.pb.h @@ -17,7 +17,7 @@ PortNums should be assigned in the following range: 0-63 Core Meshtastic use, do not use for third party apps 64-127 Registered 3rd party apps, send in a pull request that adds a new entry to portnums.proto to register your application - 256-511 Use one of these portnums for your private applications that you don't want to register publically + 256-511 Use one of these portnums for your private applications that you don't want to register publicly All other values are reserved. Note: This was formerly a Type enum named 'typ' with the same id # We have change to this 'portnum' based scheme for specifying app handlers for particular payloads. diff --git a/src/mesh/http/ContentHandler.cpp b/src/mesh/http/ContentHandler.cpp index 9ddb5ca3a..acee9be0d 100644 --- a/src/mesh/http/ContentHandler.cpp +++ b/src/mesh/http/ContentHandler.cpp @@ -165,7 +165,7 @@ void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res) if (params->getQueryParameter("all", valueAll)) { - // If all is ture, return all the buffers we have available + // If all is true, return all the buffers we have available // to us at this point in time. if (valueAll == "true") { while (len) { @@ -179,7 +179,7 @@ void handleAPIv1FromRadio(HTTPRequest *req, HTTPResponse *res) res->write(txBuf, len); } - // the param "all" was not spcified. Return just one protobuf + // the param "all" was not specified. Return just one protobuf } else { len = webAPI.getFromRadio(txBuf); res->write(txBuf, len); @@ -460,7 +460,7 @@ void handleFormUpload(HTTPRequest *req, HTTPResponse *res) HTTPBodyParser *parser; std::string contentType = req->getHeader("Content-Type"); - // The content type may have additional properties after a semicolon, for exampel: + // The content type may have additional properties after a semicolon, for example: // Content-Type: text/html;charset=utf-8 // Content-Type: multipart/form-data;boundary=------s0m3w31rdch4r4c73rs // As we're interested only in the actual mime _type_, we strip everything after the diff --git a/src/mesh/http/WebServer.cpp b/src/mesh/http/WebServer.cpp index 289f1429b..2b045c0be 100644 --- a/src/mesh/http/WebServer.cpp +++ b/src/mesh/http/WebServer.cpp @@ -15,7 +15,7 @@ #include "esp_task_wdt.h" #endif -// Persistant Data Storage +// Persistent Data Storage #include Preferences prefs; diff --git a/src/mesh/mesh-pb-constants.cpp b/src/mesh/mesh-pb-constants.cpp index f9fa02251..994fab61f 100644 --- a/src/mesh/mesh-pb-constants.cpp +++ b/src/mesh/mesh-pb-constants.cpp @@ -14,7 +14,7 @@ size_t pb_encode_to_bytes(uint8_t *destbuf, size_t destbufsize, const pb_msgdesc if (!pb_encode(&stream, fields, src_struct)) { LOG_ERROR("Panic: can't encode protobuf reason='%s'\n", PB_GET_ERROR(&stream)); assert( - 0); // If this asser fails it probably means you made a field too large for the max limits specified in mesh.options + 0); // If this assert fails it probably means you made a field too large for the max limits specified in mesh.options } else { return stream.bytes_written; } diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 1119e0c27..5e8cdcbf5 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -4,7 +4,7 @@ #include "FSCommon.h" #include "MeshService.h" #include "NodeDB.h" -#include "PowerFSM.h" // neede for button bypass +#include "PowerFSM.h" // needed for button bypass #include "detect/ScanI2C.h" #include "mesh/generated/meshtastic/cannedmessages.pb.h" diff --git a/src/modules/RangeTestModule.cpp b/src/modules/RangeTestModule.cpp index 783fbd758..28835406d 100644 --- a/src/modules/RangeTestModule.cpp +++ b/src/modules/RangeTestModule.cpp @@ -54,7 +54,7 @@ int32_t RangeTestModule::runOnce() if (moduleConfig.range_test.sender) { LOG_INFO("Initializing Range Test Module -- Sender\n"); started = millis(); // make a note of when we started - return (5000); // Sending first message 5 seconds after initilization. + return (5000); // Sending first message 5 seconds after initialization. } else { LOG_INFO("Initializing Range Test Module -- Receiver\n"); return disable(); @@ -147,8 +147,8 @@ ProcessMessage RangeTestModuleRadio::handleReceived(const meshtastic_MeshPacket LOG_DEBUG("mp.from %d\n", mp.from); LOG_DEBUG("mp.rx_snr %f\n", mp.rx_snr); LOG_DEBUG("mp.hop_limit %d\n", mp.hop_limit); - // LOG_DEBUG("mp.decoded.position.latitude_i %d\n", mp.decoded.position.latitude_i); // Depricated - // LOG_DEBUG("mp.decoded.position.longitude_i %d\n", mp.decoded.position.longitude_i); // Depricated + // LOG_DEBUG("mp.decoded.position.latitude_i %d\n", mp.decoded.position.latitude_i); // Deprecated + // LOG_DEBUG("mp.decoded.position.longitude_i %d\n", mp.decoded.position.longitude_i); // Deprecated LOG_DEBUG("---- Node Information of Received Packet (mp.from):\n"); LOG_DEBUG("n->user.long_name %s\n", n->user.long_name); LOG_DEBUG("n->user.short_name %s\n", n->user.short_name); @@ -186,8 +186,8 @@ bool RangeTestModuleRadio::appendFile(const meshtastic_MeshPacket &mp) LOG_DEBUG("mp.from %d\n", mp.from); LOG_DEBUG("mp.rx_snr %f\n", mp.rx_snr); LOG_DEBUG("mp.hop_limit %d\n", mp.hop_limit); - // LOG_DEBUG("mp.decoded.position.latitude_i %d\n", mp.decoded.position.latitude_i); // Depricated - // LOG_DEBUG("mp.decoded.position.longitude_i %d\n", mp.decoded.position.longitude_i); // Depricated + // LOG_DEBUG("mp.decoded.position.latitude_i %d\n", mp.decoded.position.latitude_i); // Deprecated + // LOG_DEBUG("mp.decoded.position.longitude_i %d\n", mp.decoded.position.longitude_i); // Deprecated LOG_DEBUG("---- Node Information of Received Packet (mp.from):\n"); LOG_DEBUG("n->user.long_name %s\n", n->user.long_name); LOG_DEBUG("n->user.short_name %s\n", n->user.short_name); diff --git a/src/modules/SerialModule.cpp b/src/modules/SerialModule.cpp index af7ba1381..a3cac53b1 100644 --- a/src/modules/SerialModule.cpp +++ b/src/modules/SerialModule.cpp @@ -33,11 +33,11 @@ to your device. TODO (in this order): - * Define a verbose RX mode to report on mesh and packet infomration. + * Define a verbose RX mode to report on mesh and packet information. - This won't happen any time soon. KNOWN PROBLEMS - * Until the module is initilized by the startup sequence, the TX pin is in a floating + * Until the module is initialized by the startup sequence, the TX pin is in a floating state. Device connected to that pin may see this as "noise". * Will not work on Linux device targets. diff --git a/src/modules/esp32/StoreForwardModule.cpp b/src/modules/esp32/StoreForwardModule.cpp index 767e1fb2b..fccbd7199 100644 --- a/src/modules/esp32/StoreForwardModule.cpp +++ b/src/modules/esp32/StoreForwardModule.cpp @@ -62,7 +62,7 @@ void StoreForwardModule::populatePSRAM() https://learn.upesy.com/en/programmation/psram.html#psram-tab */ - LOG_DEBUG("*** Before PSRAM initilization: heap %d/%d PSRAM %d/%d\n", memGet.getFreeHeap(), memGet.getHeapSize(), + LOG_DEBUG("*** Before PSRAM initialization: heap %d/%d PSRAM %d/%d\n", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreePsram(), memGet.getPsramSize()); this->packetHistoryTXQueue = @@ -77,7 +77,7 @@ void StoreForwardModule::populatePSRAM() this->packetHistory = static_cast(ps_calloc(numberOfPackets, sizeof(PacketHistoryStruct))); - LOG_DEBUG("*** After PSRAM initilization: heap %d/%d PSRAM %d/%d\n", memGet.getFreeHeap(), memGet.getHeapSize(), + LOG_DEBUG("*** After PSRAM initialization: heap %d/%d PSRAM %d/%d\n", memGet.getFreeHeap(), memGet.getHeapSize(), memGet.getFreePsram(), memGet.getPsramSize()); LOG_DEBUG("*** numberOfPackets for packetHistory - %u\n", numberOfPackets); } diff --git a/src/mqtt/JSONValue.cpp b/src/mqtt/JSONValue.cpp index 10d79a4df..1990a13b6 100644 --- a/src/mqtt/JSONValue.cpp +++ b/src/mqtt/JSONValue.cpp @@ -282,7 +282,7 @@ JSONValue *JSONValue::Parse(const char **data) return NULL; } - // Ran out of possibilites, it's bad! + // Ran out of possibilities, it's bad! else { return NULL; } diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index c10f9182d..50198efca 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -290,7 +290,7 @@ void MQTT::reconnect() pubSub.setServer(serverAddr, serverPort); pubSub.setBufferSize(512); - LOG_INFO("Attempting to connnect directly to MQTT server %s, port: %d, username: %s, password: %s\n", serverAddr, + LOG_INFO("Attempting to connect directly to MQTT server %s, port: %d, username: %s, password: %s\n", serverAddr, serverPort, mqttUsername, mqttPassword); auto myStatus = (statusTopic + owner.id); diff --git a/src/mqtt/MQTT.h b/src/mqtt/MQTT.h index fc436c22e..565f46ecf 100644 --- a/src/mqtt/MQTT.h +++ b/src/mqtt/MQTT.h @@ -53,7 +53,7 @@ class MQTT : private concurrency::OSThread * @param chIndex the index of the channel for this message * * Note: for messages we are forwarding on the mesh that we can't find the channel for (because we don't have the keys), we - * can not forward those messages to the cloud - becuase no way to find a global channel ID. + * can not forward those messages to the cloud - because no way to find a global channel ID. */ void onSend(const meshtastic_MeshPacket &mp, ChannelIndex chIndex); diff --git a/src/platform/esp32/CallbackCharacteristic.h b/src/platform/esp32/CallbackCharacteristic.h index 9c4f59a05..cd3bc6f51 100644 --- a/src/platform/esp32/CallbackCharacteristic.h +++ b/src/platform/esp32/CallbackCharacteristic.h @@ -3,7 +3,7 @@ #include "PowerFSM.h" // FIXME - someday I want to make this OTA thing a separate lb at at that point it can't touch this /** - * A characterstic with a set of overridable callbacks + * A characteristic with a set of overridable callbacks */ class CallbackCharacteristic : public BLECharacteristic, public BLECharacteristicCallbacks { diff --git a/src/platform/esp32/main-esp32.cpp b/src/platform/esp32/main-esp32.cpp index 4cb7f4443..8abe6d56d 100644 --- a/src/platform/esp32/main-esp32.cpp +++ b/src/platform/esp32/main-esp32.cpp @@ -99,7 +99,7 @@ void esp32Setup() LOG_DEBUG("Setup Preferences in Flash Storage\n"); - // Create object to store our persistant data + // Create object to store our persistent data Preferences preferences; preferences.begin("meshtastic", false); diff --git a/src/platform/nrf52/JLINK_MONITOR_ISR_SES.S b/src/platform/nrf52/JLINK_MONITOR_ISR_SES.S index b513ea07d..cda4b1a50 100644 --- a/src/platform/nrf52/JLINK_MONITOR_ISR_SES.S +++ b/src/platform/nrf52/JLINK_MONITOR_ISR_SES.S @@ -136,7 +136,7 @@ Purpose : Implementation of debug monitor for J-Link monitor mode * This handler is also responsible for handling commands that are sent by the debugger. * * Notes -* This is actually the ISR for the debug inerrupt (exception no. 12) +* This is actually the ISR for the debug interrupt (exception no. 12) */ .thumb_func diff --git a/src/platform/nrf52/NRF52Bluetooth.cpp b/src/platform/nrf52/NRF52Bluetooth.cpp index 044b57ae6..c29739542 100644 --- a/src/platform/nrf52/NRF52Bluetooth.cpp +++ b/src/platform/nrf52/NRF52Bluetooth.cpp @@ -17,7 +17,7 @@ static BLEBas blebas; // BAS (Battery Service) helper class instance static BLEDfu bledfu; // DFU software update helper service // This scratch buffer is used for various bluetooth reads/writes - but it is safe because only one bt operation can be in -// proccess at once +// process at once // static uint8_t trBytes[_max(_max(_max(_max(ToRadio_size, RadioConfig_size), User_size), MyNodeInfo_size), FromRadio_size)]; static uint8_t fromRadioBytes[meshtastic_FromRadio_size]; static uint8_t toRadioBytes[meshtastic_ToRadio_size]; diff --git a/src/platform/portduino/SimRadio.cpp b/src/platform/portduino/SimRadio.cpp index f71113ab4..e3d56554a 100644 --- a/src/platform/portduino/SimRadio.cpp +++ b/src/platform/portduino/SimRadio.cpp @@ -92,7 +92,7 @@ void SimRadio::completeSending() } } -/** Could we send right now (i.e. either not actively receving or transmitting)? */ +/** Could we send right now (i.e. either not actively receiving or transmitting)? */ bool SimRadio::canSendImmediately() { // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). diff --git a/src/platform/stm32wl/InternalFileSystem.cpp b/src/platform/stm32wl/InternalFileSystem.cpp index 950ceb0cd..d42a646a5 100644 --- a/src/platform/stm32wl/InternalFileSystem.cpp +++ b/src/platform/stm32wl/InternalFileSystem.cpp @@ -49,7 +49,7 @@ static int _internal_flash_read(const struct lfs_config *c, lfs_block_t block, l } // Program a region in a block. The block must have previously -// been erased. Negative error codes are propogated to the user. +// been erased. Negative error codes are propagated to the user. // May return LFS_ERR_CORRUPT if the block should be considered bad. static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { @@ -67,7 +67,7 @@ static int _internal_flash_prog(const struct lfs_config *c, lfs_block_t block, l // Erase a block. A block must be erased before being programmed. // The state of an erased block is undefined. Negative error codes -// are propogated to the user. +// are propagated to the user. // May return LFS_ERR_CORRUPT if the block should be considered bad. static int _internal_flash_erase(const struct lfs_config *c, lfs_block_t block) { @@ -84,7 +84,7 @@ static int _internal_flash_erase(const struct lfs_config *c, lfs_block_t block) } // Sync the state of the underlying block device. Negative error codes -// are propogated to the user. +// are propagated to the user. static int _internal_flash_sync(const struct lfs_config *c) { // we don't use a ram cache, this is a noop diff --git a/src/sleep.cpp b/src/sleep.cpp index 483c491b4..0b8fbb782 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -250,7 +250,7 @@ void doDeepSleep(uint32_t msecToWake) // // No need to turn this off if the power draw in sleep mode really is just 0.2uA and turning it off would // leave floating input for the IRQ line - // If we want to leave the radio receving in would be 11.5mA current draw, but most of the time it is just waiting + // If we want to leave the radio receiving in would be 11.5mA current draw, but most of the time it is just waiting // in its sequencer (true?) so the average power draw should be much lower even if we were listinging for packets // all the time. From c75965480f9da9f067cf0d4722c06bf31ca7f959 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Sat, 15 Jul 2023 15:53:26 +0200 Subject: [PATCH 17/57] Heltec-Tracker: TFT LCD support (#2612) * Heltec-Tracker: TFT LCD support * trunk fmt * backwards compatibility with ST7735 devices * trunk fmt --- platformio.ini | 3 +- src/graphics/TFTDisplay.cpp | 148 ++++++++++++++++-- src/graphics/TFTDisplay.h | 1 - .../heltec_wireless_tracker/platformio.ini | 21 +-- variants/heltec_wireless_tracker/variant.h | 31 ++-- 5 files changed, 156 insertions(+), 48 deletions(-) diff --git a/platformio.ini b/platformio.ini index b580d7160..e19175af7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,6 +9,7 @@ ;default_envs = heltec-v1 ;default_envs = heltec-v2_0 ;default_envs = heltec-v2_1 +;default_envs = heltec-wireless-tracker ;default_envs = tlora-v1 ;default_envs = tlora_v1_3 ;default_envs = tlora-v2 @@ -119,4 +120,4 @@ lib_deps = adafruit/Adafruit SHT31 Library@^2.2.0 adafruit/Adafruit PM25 AQI Sensor@^1.0.6 adafruit/Adafruit MPU6050@^2.2.4 - adafruit/Adafruit LIS3DH@^1.2.4 \ No newline at end of file + adafruit/Adafruit LIS3DH@^1.2.4 diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index a7d33174a..b54ac75cf 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -1,12 +1,112 @@ #include "configuration.h" +#ifndef TFT_BACKLIGHT_ON +#define TFT_BACKLIGHT_ON HIGH +#endif + +// convert 24-bit color to 16-bit (56K) +#define COLOR565(r, g, b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3)) +#define TFT_MESH COLOR565(0x67, 0xEA, 0x94) + +#if defined(ST7735S) +#include // Graphics and font library for ST7735 driver chip + +#if defined(ST7735_BACKLIGHT_EN) && !defined(TFT_BL) +#define TFT_BL ST7735_BACKLIGHT_EN +#endif + +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_ST7735S _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; + + public: + LGFX(void) + { + { + auto cfg = _bus_instance.config(); + + // configure SPI + cfg.spi_host = ST7735_SPI_HOST; // ESP32-S2,S3,C3 : SPI2_HOST or SPI3_HOST / ESP32 : VSPI_HOST or HSPI_HOST + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing + // 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + // SPI_DMA_CH_AUTO=auto setting) + cfg.pin_sclk = ST7735_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ST7735_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ST7735_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ST7735_RS; // Set SPI DC pin number (-1 = disable) + + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } + + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + + cfg.pin_cs = ST7735_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = ST7735_RESET; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = ST7735_BUSY; // Pin number where BUSY is connected (-1 = disable) + + // The following setting values ​​are general initial values ​​for each panel, so please comment out any + // unknown items and try them. + + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = 0; // Rotation direction value offset 0~7 (4~7 is upside down) + cfg.dummy_read_pixel = 8; // Number of bits for dummy read before pixel readout + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = + false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + + // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the + // ST7735 or ILI9163. + cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + _panel_instance.config(cfg); + } + + // Set the backlight control + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + + cfg.pin_bl = ST7735_BL; // Pin number to which the backlight is connected + cfg.invert = true; // true to invert the brightness of the backlight + // cfg.freq = 44100; // PWM frequency of backlight + // cfg.pwm_channel = 1; // PWM channel number to use + + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } + + setPanel(&_panel_instance); + } +}; + +static LGFX tft; + +#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) +#include // Graphics and font library for ILI9341 driver chip + +static TFT_eSPI tft = TFT_eSPI(); // Invoke library, pins defined in User_Setup.h + +#endif + #if defined(ST7735_CS) || defined(ILI9341_DRIVER) #include "SPILock.h" #include "TFTDisplay.h" #include -#include // Graphics and font library for ST7735 driver chip - -static TFT_eSPI tft = TFT_eSPI(); // Invoke library, pins defined in User_Setup.h TFTDisplay::TFTDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus) { @@ -26,11 +126,11 @@ void TFTDisplay::display(void) for (y = 0; y < displayHeight; y++) { for (x = 0; x < displayWidth; x++) { - // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficient + // get src pixel in the page based ordering the OLED lib uses FIXME, super inefficent auto isset = buffer[x + (y / 8) * displayWidth] & (1 << (y & 7)); auto dblbuf_isset = buffer_back[x + (y / 8) * displayWidth] & (1 << (y & 7)); if (isset != dblbuf_isset) { - tft.drawPixel(x, y, isset ? TFT_WHITE : TFT_BLACK); + tft.drawPixel(x, y, isset ? TFT_MESH : TFT_BLACK); } } } @@ -46,8 +146,31 @@ void TFTDisplay::display(void) // Send a command to the display (low level function) void TFTDisplay::sendCommand(uint8_t com) { - (void)com; - // Drop all commands to device (we just update the buffer) + // handle display on/off directly + switch (com) { + case DISPLAYON: { +#ifdef TFT_BL + digitalWrite(TFT_BL, TFT_BACKLIGHT_ON); +#endif +#ifdef VTFT_CTRL + digitalWrite(VTFT_CTRL, LOW); +#endif + break; + } + case DISPLAYOFF: { +#ifdef TFT_BL + digitalWrite(TFT_BL, !TFT_BACKLIGHT_ON); +#endif +#ifdef VTFT_CTRL + digitalWrite(VTFT_CTRL, HIGH); +#endif + break; + } + default: + break; + } + + // Drop all other commands to device (we just update the buffer) } void TFTDisplay::setDetected(uint8_t detected) @@ -62,18 +185,10 @@ bool TFTDisplay::connect() LOG_INFO("Doing TFT init\n"); #ifdef TFT_BL - digitalWrite(TFT_BL, HIGH); + digitalWrite(TFT_BL, TFT_BACKLIGHT_ON); pinMode(TFT_BL, OUTPUT); #endif -#ifdef TFT_POWER_EN - digitalWrite(TFT_POWER_EN, HIGH); - pinMode(TFT_POWER_EN, OUTPUT); -#endif -#ifdef ST7735_BACKLIGHT_EN - digitalWrite(ST7735_BACKLIGHT_EN, HIGH); - pinMode(ST7735_BACKLIGHT_EN, OUTPUT); -#endif tft.init(); #ifdef M5STACK tft.setRotation(1); // M5Stack has the TFT in landscape @@ -81,7 +196,6 @@ bool TFTDisplay::connect() tft.setRotation(3); // Orient horizontal and wide underneath the silkscreen name label #endif tft.fillScreen(TFT_BLACK); - // tft.drawRect(0, 0, 40, 10, TFT_PURPLE); // wide rectangle in upper left return true; } diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h index 013f4961e..46cfe85e7 100644 --- a/src/graphics/TFTDisplay.h +++ b/src/graphics/TFTDisplay.h @@ -7,7 +7,6 @@ * * Remaining TODO: * optimize display() to only draw changed pixels (see other OLED subclasses for examples) - * implement displayOn/displayOff to turn off the TFT device (and backlight) * Use the fast NRF52 SPI API rather than the slow standard arduino version * * turn radio back on - currently with both on spi bus is fucked? or are we leaving chip select asserted? diff --git a/variants/heltec_wireless_tracker/platformio.ini b/variants/heltec_wireless_tracker/platformio.ini index ec28fe6ca..43f80687d 100644 --- a/variants/heltec_wireless_tracker/platformio.ini +++ b/variants/heltec_wireless_tracker/platformio.ini @@ -1,23 +1,12 @@ [env:heltec-wireless-tracker] extends = esp32s3_base board = heltec_wifi_lora_32_V3 +upload_protocol = esp-builtin + build_flags = ${esp32s3_base.build_flags} -D HELTEC_WIRELESS_TRACKER -I variants/heltec_wireless_tracker - -DUSER_SETUP_LOADED - -DTFT_WIDTH=80 - -DTFT_HEIGHT=160 - -DST7735_GREENTAB160x80 - -DST7735_DRIVER - ;-TFT_RGB_ORDER=TFT_BGR - -DTFT_CS=38 - -DTFT_DC=40 - -DTFT_RST=39 - -DTFT_WR=42 - -DTFT_SCLK=41 - ;-DSPI_FREQUENCY=40000000 - ;-DSPI_FREQUENCY=27000000 - ;-DSPI_READ_FREQUENCY=16000000 - ;-DDISABLE_ALL_LIBRARY_WARNINGS + -DARDUINO_USB_CDC_ON_BOOT=1 + lib_deps = ${esp32s3_base.lib_deps} - bodmer/TFT_eSPI@^2.4.76 \ No newline at end of file + lovyan03/LovyanGFX@^1.1.7 \ No newline at end of file diff --git a/variants/heltec_wireless_tracker/variant.h b/variants/heltec_wireless_tracker/variant.h index f1bb071ef..7930a18a8 100644 --- a/variants/heltec_wireless_tracker/variant.h +++ b/variants/heltec_wireless_tracker/variant.h @@ -1,18 +1,25 @@ #define LED_PIN 18 -#define TFT_POWER_EN 46 - -#define ST7735_RESET 39 // Output +// ST7735S TFT LCD +#define ST7735S 1 // there are different (sub-)versions of ST7735 #define ST7735_CS 38 -#define ST7735_BACKLIGHT_EN 45 -#define ST7735_RS 40 -#define ST7735_SDA 42 +#define ST7735_RS 40 // DC +#define ST7735_SDA 42 // MOSI #define ST7735_SCK 41 - -// #define RESET_OLED 21 -// #define I2C_SDA 17 // I2C pins for this board -// #define I2C_SCL 18 - +#define ST7735_RESET 39 +#define ST7735_MISO -1 +#define ST7735_BUSY -1 +#define ST7735_BL 45 +#define ST7735_SPI_HOST SPI3_HOST +#define ST7735_BACKLIGHT_EN 45 +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define SCREEN_ROTATE +#define TFT_HEIGHT 160 +#define TFT_WIDTH 80 +#define TFT_OFFSET_X 26 +#define TFT_OFFSET_Y 0 +#define VTFT_CTRL 46 // Heltec Tracker needs this pulled low for TFT #define SCREEN_TRANSITION_FRAMERATE 1 // fps #define VEXT_ENABLE Vext // active low, powers the oled display and the lora antenna boost @@ -31,8 +38,6 @@ #define PIN_GPS_PPS 36 #define VGNSS_CTRL 37 // Heltec Tracker needs this pulled low for GPS -#define VTFT_CTRL 46 // Heltec Tracker needs this pulled low for TFT - #define USE_SX1262 #define LORA_DIO0 -1 // a No connect on the SX1262 module From 6d97d5dfa28a00380494a812f9c5858983ef37cf Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 16 Jul 2023 15:18:42 -0500 Subject: [PATCH 18/57] Bump PR artifacts github action --- .github/workflows/main_matrix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 2dc5264a2..f30eed554 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -249,7 +249,7 @@ jobs: - name: Create request artifacts if: ${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request' }} - uses: gavv/pull-request-artifacts@v1.0.0 + uses: gavv/pull-request-artifacts@v1.1.0 with: commit: ${{ (github.event.pull_request_target || github.event.pull_request).head.sha }} repo-token: ${{ secrets.GITHUB_TOKEN }} From e4e26a819b83e46b5ef623be87e190c0aea20805 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 16 Jul 2023 15:23:31 -0500 Subject: [PATCH 19/57] Check if hasSensor an run if not initialized (#2613) --- src/modules/Telemetry/EnvironmentTelemetry.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/modules/Telemetry/EnvironmentTelemetry.cpp b/src/modules/Telemetry/EnvironmentTelemetry.cpp index 5cdc4bf4d..8a4747114 100644 --- a/src/modules/Telemetry/EnvironmentTelemetry.cpp +++ b/src/modules/Telemetry/EnvironmentTelemetry.cpp @@ -86,6 +86,10 @@ int32_t EnvironmentTelemetryModule::runOnce() result = lps22hbSensor.runOnce(); if (sht31Sensor.hasSensor()) result = sht31Sensor.runOnce(); + if (ina219Sensor.hasSensor() && !ina219Sensor.isInitialized()) + result = ina219Sensor.runOnce(); + if (ina260Sensor.hasSensor() && !ina260Sensor.isInitialized()) + result = ina260Sensor.runOnce(); } return result; } else { From ab32503601d4fa9ee1b9d87aa63ec958a5d65730 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Sun, 16 Jul 2023 23:57:14 +0200 Subject: [PATCH 20/57] Heltec-Tracker: GPS support (#2615) * Heltec-Tracker: GPS support * trunk fmt --------- Co-authored-by: Ben Meadors --- .github/workflows/main_matrix.yml | 1 + src/GPSStatus.h | 2 +- src/gps/GPS.cpp | 16 ++++++++++++++-- src/gps/GPS.h | 4 +++- variants/heltec_wireless_tracker/variant.h | 3 ++- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index f30eed554..6500eb03d 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -83,6 +83,7 @@ jobs: include: - board: heltec-v3 - board: heltec-wsl-v3 + - board: heltec-wireless-tracker - board: tbeam-s3-core - board: tlora-t3s3-v1 uses: ./.github/workflows/build_esp32_s3.yml diff --git a/src/GPSStatus.h b/src/GPSStatus.h index ed3b3fc14..bcfb5f2eb 100644 --- a/src/GPSStatus.h +++ b/src/GPSStatus.h @@ -106,7 +106,7 @@ class GPSStatus : public Status bool matches(const GPSStatus *newStatus) const { #ifdef GPS_EXTRAVERBOSE - LOG_DEBUG("GPSStatus.match() new pos@%x to old pos@%x\n", newStatus->p.pos_timestamp, p.pos_timestamp); + LOG_DEBUG("GPSStatus.match() new pos@%x to old pos@%x\n", newStatus->p.timestamp, p.timestamp); #endif return (newStatus->hasLock != hasLock || newStatus->isConnected != isConnected || newStatus->isPowerSaving != isPowerSaving || newStatus->p.latitude_i != p.latitude_i || diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index 59e09f5e5..ed52ca70a 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -4,6 +4,10 @@ #include "configuration.h" #include "sleep.h" +#ifndef GPS_RESET_MODE +#define GPS_RESET_MODE HIGH +#endif + // If we have a serial GPS port it will not be null #ifdef GPS_SERIAL_NUM HardwareSerial _serial_gps_real(GPS_SERIAL_NUM); @@ -215,6 +219,11 @@ bool GPS::setupGPS() // Switch to Vehicle Mode, since SoftRF enables Aviation < 2g _serial_gps->write("$PCAS11,3*1E\r\n"); delay(250); + } else if (gnssModel == GNSS_MODEL_UC6850) { + + // use GPS + GLONASS + _serial_gps->write("$CFGSYS,h15\r\n"); + delay(250); } else if (gnssModel == GNSS_MODEL_UBLOX) { // Configure GNSS system to GPS+SBAS+GLONASS (Module may restart after this command) @@ -571,10 +580,10 @@ bool GPS::setup() #endif #ifdef PIN_GPS_RESET - digitalWrite(PIN_GPS_RESET, 1); // assert for 10ms + digitalWrite(PIN_GPS_RESET, GPS_RESET_MODE); // assert for 10ms pinMode(PIN_GPS_RESET, OUTPUT); delay(10); - digitalWrite(PIN_GPS_RESET, 0); + digitalWrite(PIN_GPS_RESET, !GPS_RESET_MODE); #endif setAwake(true); // Wake GPS power before doing any init bool ok = setupGPS(); @@ -850,6 +859,9 @@ GnssModel_t GPS::probe() return GNSS_MODEL_UBLOX; #elif defined(GPS_L76K) return GNSS_MODEL_MTK; +#elif defined(GPS_UC6580) + _serial_gps->updateBaudRate(115200); + return GNSS_MODEL_UC6850; #else // we use autodetect, only T-BEAM S3 for now... uint8_t buffer[256]; diff --git a/src/gps/GPS.h b/src/gps/GPS.h index c06c2fa91..847a60016 100644 --- a/src/gps/GPS.h +++ b/src/gps/GPS.h @@ -14,6 +14,7 @@ struct uBloxGnssModelInfo { typedef enum { GNSS_MODEL_MTK, GNSS_MODEL_UBLOX, + GNSS_MODEL_UC6850, GNSS_MODEL_UNKNOWN, } GnssModel_t; @@ -149,7 +150,7 @@ class GPS : private concurrency::OSThread */ void setAwake(bool on); - /** Get how long we should stay looking for each acquisition + /** Get how long we should stay looking for each aquisition */ uint32_t getWakeTime() const; @@ -167,6 +168,7 @@ class GPS : private concurrency::OSThread virtual int32_t runOnce() override; // Get GNSS model + String getNMEA(); GnssModel_t probe(); int getAck(uint8_t *buffer, uint16_t size, uint8_t requestedClass, uint8_t requestedID); diff --git a/variants/heltec_wireless_tracker/variant.h b/variants/heltec_wireless_tracker/variant.h index 7930a18a8..318454522 100644 --- a/variants/heltec_wireless_tracker/variant.h +++ b/variants/heltec_wireless_tracker/variant.h @@ -37,9 +37,10 @@ #define PIN_GPS_RESET 35 #define PIN_GPS_PPS 36 #define VGNSS_CTRL 37 // Heltec Tracker needs this pulled low for GPS +#define GPS_RESET_MODE LOW +#define GPS_UC6580 #define USE_SX1262 - #define LORA_DIO0 -1 // a No connect on the SX1262 module #define LORA_RESET 12 #define LORA_DIO1 14 // SX1262 IRQ From ad5de5a724635a0e623340dbe47d3da28cb1953a Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Mon, 17 Jul 2023 13:23:27 +0200 Subject: [PATCH 21/57] increase BT NIMBLE task stack size by 1k (#2618) --- arch/esp32/esp32.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/arch/esp32/esp32.ini b/arch/esp32/esp32.ini index 74d693871..f57ad549d 100644 --- a/arch/esp32/esp32.ini +++ b/arch/esp32/esp32.ini @@ -28,6 +28,7 @@ build_flags = -DCONFIG_BT_NIMBLE_ENABLED -DCONFIG_NIMBLE_CPP_LOG_LEVEL=2 -DCONFIG_BT_NIMBLE_MAX_CCCDS=20 + -DCONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=5120 -DESP_OPENSSL_SUPPRESS_LEGACY_WARNING ;-DDEBUG_HEAP From 491fe5284154dc8cb96a0a3d46f55cbb14218134 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Mon, 17 Jul 2023 16:20:05 +0200 Subject: [PATCH 22/57] add hwid for auto-detection (#2619) --- boards/t-echo.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/boards/t-echo.json b/boards/t-echo.json index 9cb48b41a..957ba01e3 100644 --- a/boards/t-echo.json +++ b/boards/t-echo.json @@ -7,7 +7,10 @@ "cpu": "cortex-m4", "extra_flags": "-DARDUINO_NRF52840_TTGO_EINK -DNRF52840_XXAA", "f_cpu": "64000000L", - "hwids": [["0x239A", "0x4405"]], + "hwids": [ + ["0x239A", "0x4405"], + ["0x239A", "0x002A"] + ], "usb_product": "TTGO_eink", "mcu": "nrf52840", "variant": "t-echo", From 4306c32349e7448bfe02c6b04278fffe641f2f47 Mon Sep 17 00:00:00 2001 From: Mark Trevor Birss Date: Mon, 17 Jul 2023 16:20:42 +0200 Subject: [PATCH 23/57] Update variant.h (#2620) Update M5Stack CoreInk enable GPS/BDS Co-authored-by: Ben Meadors --- variants/m5stack_coreink/variant.h | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/variants/m5stack_coreink/variant.h b/variants/m5stack_coreink/variant.h index 37131caca..3abf16be7 100644 --- a/variants/m5stack_coreink/variant.h +++ b/variants/m5stack_coreink/variant.h @@ -1,7 +1,19 @@ +// Primary I2C Bus includes PCF8563 RTC Module #define I2C_SDA 21 #define I2C_SCL 22 -// LED? +// 7-07-2023 Or enable Secondary I2C Bus +//#define I2C_SDA1 32 +//#define I2C_SCL1 33 + +#define HAS_GPS 1 +#undef GPS_RX_PIN +#undef GPS_TX_PIN +// Use Secondary I2C Bus as GPS Serial +#define GPS_RX_PIN 33 +#define GPS_TX_PIN 32 + +// Green LED #define LED_INVERTED 0 #define LED_PIN 10 @@ -37,10 +49,6 @@ #define LORA_DIO1 RADIOLIB_NC #define LORA_DIO2 RADIOLIB_NC -// This board has no GPS for now -#undef GPS_RX_PIN -#undef GPS_TX_PIN - #define USE_EINK // https://docs.m5stack.com/en/core/coreink // https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/docs/schematic/Core/coreink/coreink_sch.pdf From 541291cc7056edde170eff2b7d3f96598db18819 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Mon, 17 Jul 2023 20:06:34 +0200 Subject: [PATCH 24/57] resolve heltec-wireless-tracker serial issue (#2621) * add hwid for auto-detection * fix: heltec-wireless-tracker USB serial --- boards/heltec_wireless_tracker.json | 38 +++++++++ .../heltec_wireless_tracker/pins_arduino.h | 80 +++++++++++++++++++ .../heltec_wireless_tracker/platformio.ini | 5 +- 3 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 boards/heltec_wireless_tracker.json create mode 100644 variants/heltec_wireless_tracker/pins_arduino.h diff --git a/boards/heltec_wireless_tracker.json b/boards/heltec_wireless_tracker.json new file mode 100644 index 000000000..04c6e5553 --- /dev/null +++ b/boards/heltec_wireless_tracker.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld", + "partitions": "default_8MB.csv" + }, + "core": "esp32", + "extra_flags": [ + "-DHELTEC_WIRELESS_TRACKER", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "heltec_wireless_tracker" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Heltec Wireless Tracker", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://heltec.org/project/wireless-tracker/", + "vendor": "Heltec" +} diff --git a/variants/heltec_wireless_tracker/pins_arduino.h b/variants/heltec_wireless_tracker/pins_arduino.h new file mode 100644 index 000000000..e4d2631e7 --- /dev/null +++ b/variants/heltec_wireless_tracker/pins_arduino.h @@ -0,0 +1,80 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include "soc/soc_caps.h" +#include + +#define WIFI_LoRa_32_V3 true +#define DISPLAY_HEIGHT 64 +#define DISPLAY_WIDTH 128 + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +#define EXTERNAL_NUM_INTERRUPTS 46 +#define NUM_DIGITAL_PINS 48 +#define NUM_ANALOG_INPUTS 20 + +static const uint8_t LED_BUILTIN = 18; +#define BUILTIN_LED LED_BUILTIN // backward compatibility +#define LED_BUILTIN LED_BUILTIN + +#define analogInputToDigitalPin(p) (((p) < 20) ? (analogChannelToDigitalPin(p)) : -1) +#define digitalPinToInterrupt(p) (((p) < 48) ? (p) : -1) +#define digitalPinHasPWM(p) (p < 46) + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 41; +static const uint8_t SCL = 42; + +static const uint8_t SS = 8; +static const uint8_t MOSI = 10; +static const uint8_t MISO = 11; +static const uint8_t SCK = 9; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t Vext = 36; +static const uint8_t LED = 18; + +static const uint8_t RST_LoRa = 12; +static const uint8_t BUSY_LoRa = 13; +static const uint8_t DIO0 = 14; + +#endif /* Pins_Arduino_h */ diff --git a/variants/heltec_wireless_tracker/platformio.ini b/variants/heltec_wireless_tracker/platformio.ini index 43f80687d..92e76a981 100644 --- a/variants/heltec_wireless_tracker/platformio.ini +++ b/variants/heltec_wireless_tracker/platformio.ini @@ -1,11 +1,10 @@ [env:heltec-wireless-tracker] extends = esp32s3_base -board = heltec_wifi_lora_32_V3 +board = heltec_wireless_tracker upload_protocol = esp-builtin build_flags = - ${esp32s3_base.build_flags} -D HELTEC_WIRELESS_TRACKER -I variants/heltec_wireless_tracker - -DARDUINO_USB_CDC_ON_BOOT=1 + ${esp32s3_base.build_flags} -I variants/heltec_wireless_tracker lib_deps = ${esp32s3_base.lib_deps} From 5995c7060ddec524ec8dc1218763d0da1d2a8b2e Mon Sep 17 00:00:00 2001 From: tropho23 <71199294+tropho23@users.noreply.github.com> Date: Mon, 17 Jul 2023 19:55:40 -0400 Subject: [PATCH 25/57] Added triple-press GPS toggle button changes for select ESP32 devices (#2617) * Added triple-press GPS toggle button changes * Revert edits to extensions.json * comma'd --------- Co-authored-by: Ben Meadors Co-authored-by: code8buster --- variants/heltec_v2.1/platformio.ini | 3 ++- variants/heltec_v2.1/variant.h | 2 ++ variants/tlora_t3s3_v1/platformio.ini | 3 ++- variants/tlora_t3s3_v1/variant.h | 2 ++ variants/tlora_v2_1_16/platformio.ini | 3 ++- variants/tlora_v2_1_16/variant.h | 2 ++ 6 files changed, 12 insertions(+), 3 deletions(-) diff --git a/variants/heltec_v2.1/platformio.ini b/variants/heltec_v2.1/platformio.ini index 29f544f7a..7d4daecc9 100644 --- a/variants/heltec_v2.1/platformio.ini +++ b/variants/heltec_v2.1/platformio.ini @@ -4,4 +4,5 @@ extends = esp32_base board = heltec_wifi_lora_32_V2 board_level = extra build_flags = - ${esp32_base.build_flags} -D HELTEC_V2_1 -I variants/heltec_v2.1 \ No newline at end of file + ${esp32_base.build_flags} -D HELTEC_V2_1 -I variants/heltec_v2.1 + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. \ No newline at end of file diff --git a/variants/heltec_v2.1/variant.h b/variants/heltec_v2.1/variant.h index e7cfd5b34..ed123efec 100644 --- a/variants/heltec_v2.1/variant.h +++ b/variants/heltec_v2.1/variant.h @@ -8,6 +8,8 @@ #define GPS_RX_PIN 36 #define GPS_TX_PIN 33 +#define PIN_GPS_EN 37 // GPS power enable pin + #ifndef USE_JTAG // gpio15 is TDO for JTAG, so no I2C on this board while doing jtag #define I2C_SDA 4 // I2C pins for this board #define I2C_SCL 15 diff --git a/variants/tlora_t3s3_v1/platformio.ini b/variants/tlora_t3s3_v1/platformio.ini index bef57c3b4..fd3d393d9 100644 --- a/variants/tlora_t3s3_v1/platformio.ini +++ b/variants/tlora_t3s3_v1/platformio.ini @@ -4,4 +4,5 @@ board = tlora-t3s3-v1 upload_protocol = esp-builtin build_flags = - ${esp32_base.build_flags} -D TLORA_T3S3_V1 -I variants/tlora_t3s3_v1 \ No newline at end of file + ${esp32_base.build_flags} -D TLORA_T3S3_V1 -I variants/tlora_t3s3_v1 + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. \ No newline at end of file diff --git a/variants/tlora_t3s3_v1/variant.h b/variants/tlora_t3s3_v1/variant.h index 313b08e6d..7f914c055 100644 --- a/variants/tlora_t3s3_v1/variant.h +++ b/variants/tlora_t3s3_v1/variant.h @@ -1,6 +1,8 @@ #undef GPS_RX_PIN #undef GPS_TX_PIN +#define PIN_GPS_EN 42 // GPS power enable pin + #define HAS_SDCARD #define SDCARD_USE_SPI1 diff --git a/variants/tlora_v2_1_16/platformio.ini b/variants/tlora_v2_1_16/platformio.ini index da2a32ce4..167f6c37c 100644 --- a/variants/tlora_v2_1_16/platformio.ini +++ b/variants/tlora_v2_1_16/platformio.ini @@ -2,4 +2,5 @@ extends = esp32_base board = ttgo-lora32-v21 build_flags = - ${esp32_base.build_flags} -D TLORA_V2_1_16 -I variants/tlora_v2_1_16 \ No newline at end of file + ${esp32_base.build_flags} -D TLORA_V2_1_16 -I variants/tlora_v2_1_16 + -DGPS_POWER_TOGGLE ; comment this line to disable triple press function on the user button to turn off gps entirely. \ No newline at end of file diff --git a/variants/tlora_v2_1_16/variant.h b/variants/tlora_v2_1_16/variant.h index adb5af898..30a176e4c 100644 --- a/variants/tlora_v2_1_16/variant.h +++ b/variants/tlora_v2_1_16/variant.h @@ -3,6 +3,8 @@ #define GPS_RX_PIN 15 // per @der_bear on the forum, 36 is incorrect for this board type and 15 is a better pick #define GPS_TX_PIN 13 +#define PIN_GPS_EN 19 // GPS power enable pin + #define BATTERY_PIN 35 #define ADC_CHANNEL ADC1_GPIO35_CHANNEL #define BATTERY_SENSE_SAMPLES 30 From 8927cffd646aa9fecde4cbd40a02dfaf7e7b7542 Mon Sep 17 00:00:00 2001 From: code8buster Date: Tue, 18 Jul 2023 01:27:14 +0000 Subject: [PATCH 26/57] GPS log modifications (#2609) * Move module info for use in functions outside of probe, refmt MON-VER message * use checksum function in probe message * Housekeeping on some comments, unsign the position ctr again --- src/gps/GPS.cpp | 75 +++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/src/gps/GPS.cpp b/src/gps/GPS.cpp index ed52ca70a..737ad71d2 100644 --- a/src/gps/GPS.cpp +++ b/src/gps/GPS.cpp @@ -25,6 +25,9 @@ GPS *gps; /// only init that port once. static bool didSerialInit; +struct uBloxGnssModelInfo info; +uint8_t uBloxProtocolVersion; + void GPS::UBXChecksum(byte *message, size_t length) { uint8_t CK_A = 0, CK_B = 0; @@ -98,7 +101,7 @@ int GPS::getAck(uint8_t *buffer, uint16_t size, uint8_t requestedClass, uint8_t uint32_t startTime = millis(); uint16_t needRead; - while (millis() - startTime < 800) { + while (millis() - startTime < 1200) { while (_serial_gps->available()) { int c = _serial_gps->read(); switch (ubxFrameCounter) { @@ -854,24 +857,17 @@ int GPS::prepareDeepSleep(void *unused) GnssModel_t GPS::probe() { - // return immediately if the model is set by the variant.h file -#ifdef GPS_UBLOX - return GNSS_MODEL_UBLOX; -#elif defined(GPS_L76K) + memset(&info, 0, sizeof(struct uBloxGnssModelInfo)); +// return immediately if the model is set by the variant.h file +//#ifdef GPS_UBLOX (unless it's a ublox, because we might want to know the module info! +// return GNSS_MODEL_UBLOX; think about removing this macro and return) +#if defined(GPS_L76K) return GNSS_MODEL_MTK; #elif defined(GPS_UC6580) _serial_gps->updateBaudRate(115200); return GNSS_MODEL_UC6850; #else - // we use autodetect, only T-BEAM S3 for now... - uint8_t buffer[256]; - /* - * The GNSS module information variable is temporarily placed inside the function body, - * if it needs to be used elsewhere, it can be moved to the outside - * */ - struct uBloxGnssModelInfo info; - - memset(&info, 0, sizeof(struct uBloxGnssModelInfo)); + uint8_t buffer[384] = {0}; // Close all NMEA sentences , Only valid for MTK platform _serial_gps->write("$PCAS03,0,0,0,0,0,0,0,0,0,0,,,0,0*02\r\n"); @@ -899,31 +895,37 @@ GnssModel_t GPS::probe() uint8_t cfg_rate[] = {0xB5, 0x62, 0x06, 0x08, 0x00, 0x00, 0x0E, 0x30}; _serial_gps->write(cfg_rate, sizeof(cfg_rate)); // Check that the returned response class and message ID are correct - if (!getAck(buffer, 256, 0x06, 0x08)) { + if (!getAck(buffer, 384, 0x06, 0x08)) { LOG_WARN("Failed to find UBlox & MTK GNSS Module\n"); return GNSS_MODEL_UNKNOWN; } - + memset(buffer, 0, sizeof(buffer)); + byte _message_MONVER[8] = { + 0xB5, 0x62, // Sync message for UBX protocol + 0x0A, 0x04, // Message class and ID (UBX-MON-VER) + 0x00, 0x00, // Length of payload (we're asking for an answer, so no payload) + 0x00, 0x00 // Checksum + }; // Get Ublox gnss module hardware and software info - uint8_t cfg_get_hw[] = {0xB5, 0x62, 0x0A, 0x04, 0x00, 0x00, 0x0E, 0x34}; - _serial_gps->write(cfg_get_hw, sizeof(cfg_get_hw)); + UBXChecksum(_message_MONVER, sizeof(_message_MONVER)); + _serial_gps->write(_message_MONVER, sizeof(_message_MONVER)); - uint16_t len = getAck(buffer, 256, 0x0A, 0x04); + uint16_t len = getAck(buffer, 384, 0x0A, 0x04); if (len) { - + // LOG_DEBUG("monver reply size = %d\n", len); uint16_t position = 0; for (int i = 0; i < 30; i++) { info.swVersion[i] = buffer[position]; position++; } for (int i = 0; i < 10; i++) { - info.hwVersion[i] = buffer[position]; + info.hwVersion[i] = buffer[position - 1]; position++; } while (len >= position + 30) { for (int i = 0; i < 30; i++) { - info.extension[info.extensionNo][i] = buffer[position]; + info.extension[info.extensionNo][i] = buffer[position - 1]; position++; } info.extensionNo++; @@ -933,6 +935,7 @@ GnssModel_t GPS::probe() LOG_DEBUG("Module Info : \n"); LOG_DEBUG("Soft version: %s\n", info.swVersion); + LOG_DEBUG("first char is %c\n", (char)info.swVersion[0]); LOG_DEBUG("Hard version: %s\n", info.hwVersion); LOG_DEBUG("Extensions:%d\n", info.extensionNo); for (int i = 0; i < info.extensionNo; i++) { @@ -943,19 +946,29 @@ GnssModel_t GPS::probe() // tips: extensionNo field is 0 on some 6M GNSS modules for (int i = 0; i < info.extensionNo; ++i) { - if (!strncmp(info.extension[i], "OD=", 3)) { - strncpy((char *)buffer, &(info.extension[i][3]), sizeof(buffer)); - LOG_DEBUG("GetModel:%s\n", (char *)buffer); + if (!strncmp(info.extension[i], "MOD=", 4)) { + strncpy((char *)buffer, &(info.extension[i][4]), sizeof(buffer)); + // LOG_DEBUG("GetModel:%s\n", (char *)buffer); + if (strlen((char *)buffer)) { + LOG_INFO("UBlox GNSS init succeeded, using UBlox %s GNSS Module\n", (char *)buffer); + } else { + LOG_INFO("UBlox GNSS init succeeded, using UBlox GNSS Module\n"); + } + } else if (!strncmp(info.extension[i], "PROTVER=", 8)) { + char *ptr = nullptr; + memset(buffer, 0, sizeof(buffer)); + strncpy((char *)buffer, &(info.extension[i][8]), sizeof(buffer)); + LOG_DEBUG("Protocol Version:%s\n", (char *)buffer); + if (strlen((char *)buffer)) { + uBloxProtocolVersion = strtoul((char *)buffer, &ptr, 10); + LOG_DEBUG("ProtVer=%d\n", uBloxProtocolVersion); + } else { + uBloxProtocolVersion = 0; + } } } } - if (strlen((char *)buffer)) { - LOG_INFO("UBlox GNSS init succeeded, using UBlox %s GNSS Module\n", buffer); - } else { - LOG_INFO("UBlox GNSS init succeeded, using UBlox GNSS Module\n"); - } - return GNSS_MODEL_UBLOX; #endif } From 468807466c47543e84656cb3e782d08d88d017b3 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Tue, 18 Jul 2023 13:10:39 +0200 Subject: [PATCH 27/57] fix BLE PIN screen for not so large screens (#2624) * add hwid for auto-detection * fix: heltec-wireless-tracker USB serial * fix BLE PIN screen for not so large displays --- src/graphics/Screen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 3105ee218..689c0315c 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -296,7 +296,7 @@ static void drawModuleFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int static void drawFrameBluetooth(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) { int x_offset = display->width() / 2; - int y_offset = display->height() == 64 ? 0 : 32; + int y_offset = display->height() <= 80 ? 0 : 32; display->setTextAlignment(TEXT_ALIGN_CENTER); display->setFont(FONT_MEDIUM); display->drawString(x_offset + x, y_offset + y, "Bluetooth"); From 69beef8310625564703bba5a0c38b93b691d0087 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Jul 2023 06:19:37 -0500 Subject: [PATCH 28/57] [create-pull-request] automated change (#2625) Co-authored-by: thebentern --- protobufs | 2 +- src/mesh/generated/meshtastic/channel.pb.h | 2 +- src/mesh/generated/meshtastic/config.pb.h | 6 +++--- src/mesh/generated/meshtastic/mesh.pb.h | 8 ++++---- src/mesh/generated/meshtastic/portnums.pb.h | 2 +- src/mesh/generated/meshtastic/telemetry.pb.h | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/protobufs b/protobufs index e0b136f5f..64c2a11d3 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit e0b136f5f8e26094d02c28d1fdcacd61e087298c +Subproject commit 64c2a11d371cae3a2e7bb2cc86b9e6e764de7175 diff --git a/src/mesh/generated/meshtastic/channel.pb.h b/src/mesh/generated/meshtastic/channel.pb.h index f5ee79308..535962ae6 100644 --- a/src/mesh/generated/meshtastic/channel.pb.h +++ b/src/mesh/generated/meshtastic/channel.pb.h @@ -81,7 +81,7 @@ typedef struct _meshtastic_ChannelSettings { a table of well known IDs. (see Well Known Channels FIXME) */ uint32_t id; - /* If true, messages on the mesh will be sent to the *public* internet by any gateway node */ + /* If true, messages on the mesh will be sent to the *public* internet by any gateway ndoe */ bool uplink_enabled; /* If true, messages seen on the internet will be forwarded to the local mesh. */ bool downlink_enabled; diff --git a/src/mesh/generated/meshtastic/config.pb.h b/src/mesh/generated/meshtastic/config.pb.h index 0101845dc..99314aef5 100644 --- a/src/mesh/generated/meshtastic/config.pb.h +++ b/src/mesh/generated/meshtastic/config.pb.h @@ -215,7 +215,7 @@ typedef enum _meshtastic_Config_BluetoothConfig_PairingMode { typedef struct _meshtastic_Config_DeviceConfig { /* Sets the role of node */ meshtastic_Config_DeviceConfig_Role role; - /* Disabling this will disable the SerialConsole by not initializing the StreamAPI */ + /* Disabling this will disable the SerialConsole by not initilizing the StreamAPI */ bool serial_enabled; /* By default we turn off logging as soon as an API client connects (to keep shared serial link quiet). Set this to true to leave the debug log outputting even when API is active. */ @@ -269,7 +269,7 @@ typedef struct _meshtastic_Config_PositionConfig { uint32_t tx_gpio; /* The minimum distance in meters traveled (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled */ uint32_t broadcast_smart_minimum_distance; - /* The minimum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled */ + /* The minumum number of seconds (since the last send) before we can send a position to the mesh if position_broadcast_smart_enabled */ uint32_t broadcast_smart_minimum_interval_secs; } meshtastic_Config_PositionConfig; @@ -363,7 +363,7 @@ typedef struct _meshtastic_Config_DisplayConfig { bool compass_north_top; /* Flip screen vertically, for cases that mount the screen upside down */ bool flip_screen; - /* Preferred display units */ + /* Perferred display units */ meshtastic_Config_DisplayConfig_DisplayUnits units; /* Override auto-detect in screen */ meshtastic_Config_DisplayConfig_OledType oled; diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 659dd6c0b..3814875d3 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -147,7 +147,7 @@ typedef enum _meshtastic_CriticalErrorCode { /* Radio transmit hardware failure. We sent data to the radio chip, but it didn't reply with an interrupt. */ meshtastic_CriticalErrorCode_TRANSMIT_FAILED = 8, - /* We detected that the main CPU voltage dropped below the minimum acceptable value */ + /* We detected that the main CPU voltage dropped below the minumum acceptable value */ meshtastic_CriticalErrorCode_BROWNOUT = 9, /* Selftest of SX1262 radio chip failed */ meshtastic_CriticalErrorCode_SX1262_FAILURE = 10, @@ -371,7 +371,7 @@ typedef struct _meshtastic_Position { 0 through 3 - for future use */ typedef struct _meshtastic_User { /* A globally unique ID string for this user. - In the case of Signal that would mean +16504442323, for the default macaddr derived id it would be !<8 hexadecimal bytes>. + In the case of Signal that would mean +16504442323, for the default macaddr derived id it would be !<8 hexidecimal bytes>. Note: app developers are encouraged to also use the following standard node IDs "^all" (for broadcast), "^local" (for the locally connected node) */ char id[16]; @@ -418,7 +418,7 @@ typedef struct _meshtastic_Routing { typedef PB_BYTES_ARRAY_T(237) meshtastic_Data_payload_t; /* (Formerly called SubPacket) - The payload portion for a packet, this is the actual bytes that are sent + The payload portion fo a packet, this is the actual bytes that are sent inside a radio packet (because from/to are broken out by the comms library) */ typedef struct _meshtastic_Data { /* Formerly named typ and of type Type */ @@ -552,7 +552,7 @@ typedef struct _meshtastic_MeshPacket { /* The priority of this message for sending. See MeshPacket.Priority description for more details. */ meshtastic_MeshPacket_Priority priority; - /* rssi of received packet. Only sent to phone for display purposes. */ + /* rssi of received packet. Only sent to phone for dispay purposes. */ int32_t rx_rssi; /* Describe if this message is delayed */ meshtastic_MeshPacket_Delayed delayed; diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h index e089f9182..e4aaeeb96 100644 --- a/src/mesh/generated/meshtastic/portnums.pb.h +++ b/src/mesh/generated/meshtastic/portnums.pb.h @@ -17,7 +17,7 @@ PortNums should be assigned in the following range: 0-63 Core Meshtastic use, do not use for third party apps 64-127 Registered 3rd party apps, send in a pull request that adds a new entry to portnums.proto to register your application - 256-511 Use one of these portnums for your private applications that you don't want to register publicly + 256-511 Use one of these portnums for your private applications that you don't want to register publically All other values are reserved. Note: This was formerly a Type enum named 'typ' with the same id # We have change to this 'portnum' based scheme for specifying app handlers for particular payloads. diff --git a/src/mesh/generated/meshtastic/telemetry.pb.h b/src/mesh/generated/meshtastic/telemetry.pb.h index 31646693e..4a9efc337 100644 --- a/src/mesh/generated/meshtastic/telemetry.pb.h +++ b/src/mesh/generated/meshtastic/telemetry.pb.h @@ -63,7 +63,7 @@ typedef struct _meshtastic_EnvironmentMetrics { float relative_humidity; /* Barometric pressure in hPA measured */ float barometric_pressure; - /* Gas resistance in mOhm measured */ + /* Gas resistance in MOhm measured */ float gas_resistance; /* Voltage measured */ float voltage; From eb7025f1b19c453b4c95bf199b00cf48e9e1455b Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 18 Jul 2023 07:09:55 -0500 Subject: [PATCH 29/57] Add Hydra specific target to define GPS EN pin and limit tx power (#2608) * Use DIO2 bridged to TXEN and remove TX/RXEN pin switching altogether * Add Hydra specific target to limit tx power and define GPS EN * Whoops --- .github/workflows/main_matrix.yml | 1 + variants/diy/hydra/variant.h | 41 +++++++++++++++++++++++++++++++ variants/diy/platformio.ini | 12 +++++++++ 3 files changed, 54 insertions(+) create mode 100644 variants/diy/hydra/variant.h diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 6500eb03d..c062348e8 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -66,6 +66,7 @@ jobs: - board: heltec-v2_1 - board: tbeam0_7 - board: meshtastic-diy-v1 + - board: hydra - board: meshtastic-dr-dev - board: nano-g1 - board: station-g1 diff --git a/variants/diy/hydra/variant.h b/variants/diy/hydra/variant.h new file mode 100644 index 000000000..93928a212 --- /dev/null +++ b/variants/diy/hydra/variant.h @@ -0,0 +1,41 @@ +// For OLED LCD +#define I2C_SDA 21 +#define I2C_SCL 22 + +// GPS +#undef GPS_RX_PIN +#undef GPS_TX_PIN +#define GPS_RX_PIN 12 +#define GPS_TX_PIN 15 +#define GPS_UBLOX +#define PIN_GPS_EN 4 + +#define BUTTON_PIN 39 // The middle button GPIO on the T-Beam +#define BATTERY_PIN 35 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +#define ADC_CHANNEL ADC1_GPIO35_CHANNEL +#define ADC_MULTIPLIER 1.85 // (R1 = 470k, R2 = 680k) +#define EXT_PWR_DETECT 4 // Pin to detect connected external power source for LILYGO® TTGO T-Energy T18 and other DIY boards +#define EXT_NOTIFY_OUT 12 // Overridden default pin to use for Ext Notify Module (#975). +#define LED_PIN 2 // add status LED (compatible with core-pcb and DIY targets) + +#define LORA_DIO0 26 // a No connect on the SX1262/SX1268 module +#define LORA_RESET 23 // RST for SX1276, and for SX1262/SX1268 +#define LORA_DIO1 33 // IRQ for SX1262/SX1268 +#define LORA_DIO2 32 // BUSY for SX1262/SX1268 +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262/SX1268, if DIO3 is high the TXCO is enabled + +#define RF95_SCK 5 +#define RF95_MISO 19 +#define RF95_MOSI 27 +#define RF95_NSS 18 + +#define USE_SX1262 + +#define SX126X_CS 18 // NSS for SX126X +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET + +// Set lora.tx_power to 13 for Hydra or other E22 900M30S target due to PA +#define SX126X_MAX_POWER 13 +#define SX126X_E22 \ No newline at end of file diff --git a/variants/diy/platformio.ini b/variants/diy/platformio.ini index 612035717..cb031f266 100644 --- a/variants/diy/platformio.ini +++ b/variants/diy/platformio.ini @@ -34,3 +34,15 @@ build_flags = -D DR_DEV -D EBYTE_E22 -I variants/diy/dr-dev + +; Hydra - Meshtastic DIY v1 hardware with some specific changes +[env:hydra] +extends = esp32_base +board = esp32doit-devkit-v1 +board_level = extra +build_flags = + ${esp32_base.build_flags} + -D DIY_V1 + -D EBYTE_E22 + -D GPS_POWER_TOGGLE + -I variants/diy/hydra From 77efbb3f5d69ad362e528c54d350250bf38541ec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Jul 2023 10:00:12 -0500 Subject: [PATCH 30/57] [create-pull-request] automated change (#2626) Co-authored-by: thebentern --- version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.properties b/version.properties index e279aa749..76538d46b 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 1 -build = 19 +build = 20 From 2486892e6da3212eeda4935f77ed824099c2b004 Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Wed, 19 Jul 2023 15:13:51 +0200 Subject: [PATCH 31/57] Basic T-Deck support (#2630) * add hwid for auto-detection * fix: heltec-wireless-tracker USB serial * T-Deck support * trunk fmt * set FRAMERATE to 1 * fix some defines * trunk fmt * corrected vendor link --- boards/t-deck.json | 40 ++++++++++ src/FSCommon.cpp | 2 + src/graphics/Screen.cpp | 10 +-- src/graphics/Screen.h | 2 +- src/graphics/TFTDisplay.cpp | 118 +++++++++++++++++++++++++++- src/graphics/images.h | 2 +- src/main.cpp | 2 +- src/mesh/NodeDB.cpp | 2 +- src/modules/CannedMessageModule.cpp | 2 +- src/platform/esp32/architecture.h | 2 + variants/t-deck/pins_arduino.h | 69 ++++++++++++++++ variants/t-deck/platformio.ini | 14 ++++ variants/t-deck/variant.h | 74 +++++++++++++++++ 13 files changed, 326 insertions(+), 13 deletions(-) create mode 100644 boards/t-deck.json create mode 100644 variants/t-deck/pins_arduino.h create mode 100644 variants/t-deck/platformio.ini create mode 100644 variants/t-deck/variant.h diff --git a/boards/t-deck.json b/boards/t-deck.json new file mode 100644 index 000000000..9d74834e9 --- /dev/null +++ b/boards/t-deck.json @@ -0,0 +1,40 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=0" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "dio", + "hwids": [["0x303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "t-deck" + }, + "connectivity": ["wifi", "bluetooth", "lora"], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": ["esp-builtin"], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino", "espidf"], + "name": "Espressif Systems LilyGO T-Deck (16 MB FLASH, 8 MB PSRAM)", + "upload": { + "flash_size": "16MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 921600 + }, + "url": "https://www.lilygo.cc/en-pl/products/t-deck", + "vendor": "LilyGO" +} diff --git a/src/FSCommon.cpp b/src/FSCommon.cpp index 150391237..a458a9fcf 100644 --- a/src/FSCommon.cpp +++ b/src/FSCommon.cpp @@ -8,6 +8,8 @@ #ifdef SDCARD_USE_SPI1 SPIClass SPI1(HSPI); #define SDHandler SPI1 +#else +#define SDHandler SPI #endif #endif // HAS_SDCARD diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 689c0315c..049382c19 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -102,7 +102,7 @@ static uint16_t displayWidth, displayHeight; #define SCREEN_WIDTH displayWidth #define SCREEN_HEIGHT displayHeight -#if defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) +#if defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) || defined(ST7789_CS) // The screen is bigger so use bigger fonts #define FONT_SMALL ArialMT_Plain_16 // Height: 19 #define FONT_MEDIUM ArialMT_Plain_24 // Height: 28 @@ -492,7 +492,7 @@ static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, NodeStatus *no { char usersString[20]; snprintf(usersString, sizeof(usersString), "%d/%d", nodeStatus->getNumOnline(), nodeStatus->getNumTotal()); -#if defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) +#if defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) || defined(ST7789_CS) display->drawFastImage(x, y + 3, 8, 8, imgUser); #else display->drawFastImage(x, y, 8, 8, imgUser); @@ -1482,7 +1482,7 @@ void DebugInfo::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 #ifdef ARCH_ESP32 if (millis() - storeForwardModule->lastHeartbeat > (storeForwardModule->heartbeatInterval * 1200)) { // no heartbeat, overlap a bit -#if defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) +#if defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) || defined(ST7789_CS) display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgQuestionL1); display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, @@ -1492,7 +1492,7 @@ void DebugInfo::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 imgQuestion); #endif } else { -#if defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) +#if defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) || defined(ST7789_CS) display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 16, 8, imgSFL1); display->drawFastImage(x + SCREEN_WIDTH - 18 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 16, 8, @@ -1504,7 +1504,7 @@ void DebugInfo::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16 } #endif } else { -#if defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) +#if defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) || defined(ST7789_CS) display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 3 + FONT_HEIGHT_SMALL, 12, 8, imgInfoL1); display->drawFastImage(x + SCREEN_WIDTH - 14 - display->getStringWidth(ourId), y + 11 + FONT_HEIGHT_SMALL, 12, 8, diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 992a73285..debc6ac0b 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -380,7 +380,7 @@ class Screen : public concurrency::OSThread SH1106Wire dispdev; #elif defined(USE_SSD1306) SSD1306Wire dispdev; -#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) +#elif defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ST7789_CS) TFTDisplay dispdev; #elif defined(USE_EINK) EInkDisplay dispdev; diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index b54ac75cf..6f58d421d 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -96,6 +96,118 @@ class LGFX : public lgfx::LGFX_Device static LGFX tft; +#elif defined(ST7789_CS) +#include // Graphics and font library for ST7735 driver chip + +#if defined(ST7789_BACKLIGHT_EN) && !defined(TFT_BL) +#define TFT_BL ST7789_BACKLIGHT_EN +#endif + +class LGFX : public lgfx::LGFX_Device +{ + lgfx::Panel_ST7789 _panel_instance; + lgfx::Bus_SPI _bus_instance; + lgfx::Light_PWM _light_instance; + lgfx::Touch_GT911 _touch_instance; + + public: + LGFX(void) + { + { + auto cfg = _bus_instance.config(); + + // SPI + cfg.spi_host = ST7789_SPI_HOST; + cfg.spi_mode = 0; + cfg.freq_write = SPI_FREQUENCY; // SPI clock for transmission (up to 80MHz, rounded to the value obtained by dividing + // 80MHz by an integer) + cfg.freq_read = SPI_READ_FREQUENCY; // SPI clock when receiving + cfg.spi_3wire = false; // Set to true if reception is done on the MOSI pin + cfg.use_lock = true; // Set to true to use transaction locking + cfg.dma_channel = SPI_DMA_CH_AUTO; // SPI_DMA_CH_AUTO; // Set DMA channel to use (0=not use DMA / 1=1ch / 2=ch / + cfg.pin_sclk = ST7789_SCK; // Set SPI SCLK pin number + cfg.pin_mosi = ST7789_SDA; // Set SPI MOSI pin number + cfg.pin_miso = ST7789_MISO; // Set SPI MISO pin number (-1 = disable) + cfg.pin_dc = ST7789_RS; // Set SPI DC pin number (-1 = disable) + + _bus_instance.config(cfg); // applies the set value to the bus. + _panel_instance.setBus(&_bus_instance); // set the bus on the panel. + } + + { // Set the display panel control. + auto cfg = _panel_instance.config(); // Gets a structure for display panel settings. + + cfg.pin_cs = ST7789_CS; // Pin number where CS is connected (-1 = disable) + cfg.pin_rst = -1; // Pin number where RST is connected (-1 = disable) + cfg.pin_busy = -1; // Pin number where BUSY is connected (-1 = disable) + + // The following setting values ​​are general initial values ​​for each panel, so please comment out any + // unknown items and try them. + + cfg.panel_width = TFT_WIDTH; // actual displayable width + cfg.panel_height = TFT_HEIGHT; // actual displayable height + cfg.offset_x = TFT_OFFSET_X; // Panel offset amount in X direction + cfg.offset_y = TFT_OFFSET_Y; // Panel offset amount in Y direction + cfg.offset_rotation = 0; // Rotation direction value offset 0~7 (4~7 is mirrored) + cfg.dummy_read_pixel = 9; // Number of bits for dummy read before pixel readout + cfg.dummy_read_bits = 1; // Number of bits for dummy read before non-pixel data read + cfg.readable = true; // Set to true if data can be read + cfg.invert = true; // Set to true if the light/darkness of the panel is reversed + cfg.rgb_order = false; // Set to true if the panel's red and blue are swapped + cfg.dlen_16bit = + false; // Set to true for panels that transmit data length in 16-bit units with 16-bit parallel or SPI + cfg.bus_shared = true; // If the bus is shared with the SD card, set to true (bus control with drawJpgFile etc.) + + // Set the following only when the display is shifted with a driver with a variable number of pixels, such as the + // ST7735 or ILI9163. + // cfg.memory_width = TFT_WIDTH; // Maximum width supported by the driver IC + // cfg.memory_height = TFT_HEIGHT; // Maximum height supported by the driver IC + _panel_instance.config(cfg); + } + + // Set the backlight control. (delete if not necessary) + { + auto cfg = _light_instance.config(); // Gets a structure for backlight settings. + + cfg.pin_bl = ST7789_BL; // Pin number to which the backlight is connected + cfg.invert = true; // true to invert the brightness of the backlight + // cfg.pwm_channel = 0; + + _light_instance.config(cfg); + _panel_instance.setLight(&_light_instance); // Set the backlight on the panel. + } + + // Configure settings for touch screen control. + { + auto cfg = _touch_instance.config(); + + cfg.pin_cs = -1; + cfg.x_min = 0; + cfg.x_max = TFT_HEIGHT - 1; + cfg.y_min = 0; + cfg.y_max = TFT_WIDTH - 1; + cfg.pin_int = SCREEN_TOUCH_INT; + cfg.bus_shared = true; + cfg.offset_rotation = 0; + // cfg.freq = 2500000; + + // I2C + cfg.i2c_port = 1; + cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; + cfg.pin_sda = I2C_SDA; + cfg.pin_scl = I2C_SCL; + cfg.freq = 400000; + + _touch_instance.config(cfg); + _panel_instance.setTouch(&_touch_instance); + } + + setPanel(&_panel_instance); // Sets the panel to use. + } +}; + +static LGFX tft; + #elif defined(ST7735_CS) || defined(ILI9341_DRIVER) #include // Graphics and font library for ILI9341 driver chip @@ -103,7 +215,7 @@ static TFT_eSPI tft = TFT_eSPI(); // Invoke library, pins defined in User_Setup. #endif -#if defined(ST7735_CS) || defined(ILI9341_DRIVER) +#if defined(ST7735_CS) || defined(ST7789_CS) || defined(ILI9341_DRIVER) #include "SPILock.h" #include "TFTDisplay.h" #include @@ -190,8 +302,8 @@ bool TFTDisplay::connect() #endif tft.init(); -#ifdef M5STACK - tft.setRotation(1); // M5Stack has the TFT in landscape +#if defined(M5STACK) || defined(T_DECK) + tft.setRotation(1); // M5Stack/T-Deck have the TFT in landscape #else tft.setRotation(3); // Orient horizontal and wide underneath the silkscreen name label #endif diff --git a/src/graphics/images.h b/src/graphics/images.h index b1818e32c..46c9118b1 100644 --- a/src/graphics/images.h +++ b/src/graphics/images.h @@ -14,7 +14,7 @@ const uint8_t imgUser[] PROGMEM = {0x3C, 0x42, 0x99, 0xA5, 0xA5, 0x99, 0x42, 0x3 const uint8_t imgPositionEmpty[] PROGMEM = {0x20, 0x30, 0x28, 0x24, 0x42, 0xFF}; const uint8_t imgPositionSolid[] PROGMEM = {0x20, 0x30, 0x38, 0x3C, 0x7E, 0xFF}; -#if defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) +#if defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) || defined(ST7789_CS) const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff}; const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f}; const uint8_t imgInfoL1[] PROGMEM = {0xff, 0x01, 0x01, 0x01, 0x1e, 0x7f, 0x1e, 0x01, 0x01, 0x01, 0x01, 0xff}; diff --git a/src/main.cpp b/src/main.cpp index e503aadcf..949dbcf1f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -528,7 +528,7 @@ void setup() // Don't call screen setup until after nodedb is setup (because we need // the current region name) -#if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) +#if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7789_CS) screen->setup(); #else if (screen_found.port != ScanI2C::I2CPort::NO_I2C) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 0192544fb..6d71a750c 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -184,7 +184,7 @@ void NodeDB::installDefaultConfig() // FIXME: Default to bluetooth capability of platform as default config.bluetooth.enabled = true; config.bluetooth.fixed_pin = defaultBLEPin; -#if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) +#if defined(ST7735_CS) || defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7789_CS) bool hasScreen = true; #else bool hasScreen = screen_found.port != ScanI2C::I2CPort::NO_I2C; diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index 5e8cdcbf5..d3a450371 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -18,7 +18,7 @@ #include "graphics/fonts/OLEDDisplayFontsUA.h" #endif -#if defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) +#if defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ST7735_CS) || defined(ST7789_CS) // The screen is bigger so use bigger fonts #define FONT_SMALL ArialMT_Plain_16 #define FONT_MEDIUM ArialMT_Plain_24 diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index 23346d493..c6842eee9 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -81,6 +81,8 @@ #define HW_VENDOR meshtastic_HardwareModel_TLORA_V2_1_1P6 #elif defined(TLORA_V2_1_18) #define HW_VENDOR meshtastic_HardwareModel_TLORA_V2_1_1P8 +#elif defined(T_DECK) +#define HW_VENDOR meshtastic_HardwareModel_T_DECK #elif defined(GENIEBLOCKS) #define HW_VENDOR meshtastic_HardwareModel_GENIEBLOCKS #elif defined(PRIVATE_HW) diff --git a/variants/t-deck/pins_arduino.h b/variants/t-deck/pins_arduino.h new file mode 100644 index 000000000..0150935ed --- /dev/null +++ b/variants/t-deck/pins_arduino.h @@ -0,0 +1,69 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define USB_VID 0x303a +#define USB_PID 0x1001 + +#define EXTERNAL_NUM_INTERRUPTS 46 +#define NUM_DIGITAL_PINS 48 +#define NUM_ANALOG_INPUTS 20 + +#define analogInputToDigitalPin(p) (((p) < NUM_ANALOG_INPUTS) ? (analogChannelToDigitalPin(p)) : -1) +#define digitalPinToInterrupt(p) (((p) < NUM_DIGITAL_PINS) ? (p) : -1) +#define digitalPinHasPWM(p) (p < EXTERNAL_NUM_INTERRUPTS) + +// static const uint8_t LED_BUILTIN = -1; + +static const uint8_t TX = 43; +static const uint8_t RX = 44; + +static const uint8_t SDA = 18; +static const uint8_t SCL = 8; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 9; +static const uint8_t MOSI = 41; +static const uint8_t MISO = 38; +static const uint8_t SCK = 40; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +static const uint8_t BAT_ADC_PIN = 4; + +#endif /* Pins_Arduino_h */ diff --git a/variants/t-deck/platformio.ini b/variants/t-deck/platformio.ini new file mode 100644 index 000000000..8344fb990 --- /dev/null +++ b/variants/t-deck/platformio.ini @@ -0,0 +1,14 @@ +; LilyGo T-Deck +[env:t-deck] +extends = esp32s3_base +board = t-deck +upload_protocol = esp-builtin +debug_tool = esp-builtin + +build_flags = ${esp32_base.build_flags} + -DT_DECK + -DBOARD_HAS_PSRAM + -Ivariants/t-deck + +lib_deps = ${esp32s3_base.lib_deps} + lovyan03/LovyanGFX@^1.1.7 \ No newline at end of file diff --git a/variants/t-deck/variant.h b/variants/t-deck/variant.h new file mode 100644 index 000000000..e434cd35d --- /dev/null +++ b/variants/t-deck/variant.h @@ -0,0 +1,74 @@ +// ST7789 TFT LCD +#define ST7789_CS 12 +#define ST7789_RS 11 // DC +#define ST7789_SDA 41 // MOSI +#define ST7789_SCK 40 +#define ST7789_RESET -1 +#define ST7789_MISO 38 +#define ST7789_BUSY -1 +#define ST7789_BL 42 +#define ST7789_SPI_HOST SPI2_HOST +#define ST7789_BACKLIGHT_EN 42 +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 320 +#define TFT_WIDTH 240 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define SCREEN_ROTATE +#define SCREEN_TRANSITION_FRAMERATE 1 // fps +#define SCREEN_TOUCH_INT 16 +#define TOUCH_SLAVE_ADDRESS 0x5D // GT911 + +#define BUTTON_PIN 0 +// #define BUTTON_NEED_PULLUP + +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +// Have SPI interface SD card slot +#define HAS_SDCARD 1 +#define SPI_MOSI (41) +#define SPI_SCK (40) +#define SPI_MISO (38) +#define SPI_CS (39) +#define SDCARD_CS SPI_CS + +#define BATTERY_PIN 4 // A battery voltage measurement pin, voltage divider connected here to measure battery voltage +// ratio of voltage divider = 2.0 (RD2=100k, RD3=100k) +#define ADC_MULTIPLIER 2.11 // 2.0 + 10% for correction of display undervoltage. +#define ADC_CHANNEL ADC1_GPIO1_CHANNEL + +// keyboard +#define I2C_SDA 18 // I2C pins for this board +#define I2C_SCL 8 +#define BOARD_POWERON 10 // must be set to HIGH +#define KB_SLAVE_ADDRESS 0x55 +#define KB_BL_PIN 46 // INT, set to INPUT +#define KB_UP 2 +#define KB_DOWN 3 +#define KB_LEFT 1 +#define KB_RIGHT 15 + +#define USE_SX1262 +#define USE_SX1268 + +#define RF95_SCK 40 +#define RF95_MISO 38 +#define RF95_MOSI 41 +#define RF95_NSS 9 + +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 17 +#define LORA_DIO1 45 // SX1262 IRQ +#define LORA_DIO2 13 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define SX126X_CS RF95_NSS // FIXME - we really should define LORA_CS instead +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_E22 // Not really an E22 but TTGO seems to be trying to clone that +// Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for the sx1262interface +// code) From 084ad1b722637323e550a6bd41e6f10fd167cd41 Mon Sep 17 00:00:00 2001 From: rcarteraz Date: Fri, 21 Jul 2023 17:32:39 -0700 Subject: [PATCH 32/57] Update main_matrix.yml (#2634) Add Heltec Wireless Paper to S3 Boards --- .github/workflows/main_matrix.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index c062348e8..237322433 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -85,6 +85,7 @@ jobs: - board: heltec-v3 - board: heltec-wsl-v3 - board: heltec-wireless-tracker + - board: heltec-wireless-paper - board: tbeam-s3-core - board: tlora-t3s3-v1 uses: ./.github/workflows/build_esp32_s3.yml From 1c74479555d2a63d409ab4ad6dad837c292a08b5 Mon Sep 17 00:00:00 2001 From: andrew-moroz <67253360+andrew-moroz@users.noreply.github.com> Date: Fri, 21 Jul 2023 21:37:00 -0400 Subject: [PATCH 33/57] xiao-ble: add initial support for the Xiao BLE + Ebyte E22-900M30S (#2633) Co-authored-by: Ben Meadors --- boards/xiao_ble_sense.json | 57 ++++ src/main.cpp | 13 + src/mesh/SX126xInterface.cpp | 19 +- src/platform/nrf52/architecture.h | 8 + variants/xiao_ble/README.md | 261 ++++++++++++++++++ variants/xiao_ble/platformio.ini | 13 + variants/xiao_ble/variant.cpp | 55 ++++ variants/xiao_ble/variant.h | 201 ++++++++++++++ .../xiao_ble/xiao-ble-internal-format.uf2 | Bin 0 -> 122880 bytes variants/xiao_ble/xiao_ble.sh | 15 + ...ootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip | Bin 0 -> 192586 bytes 11 files changed, 639 insertions(+), 3 deletions(-) create mode 100644 boards/xiao_ble_sense.json create mode 100644 variants/xiao_ble/README.md create mode 100644 variants/xiao_ble/platformio.ini create mode 100644 variants/xiao_ble/variant.cpp create mode 100644 variants/xiao_ble/variant.h create mode 100644 variants/xiao_ble/xiao-ble-internal-format.uf2 create mode 100755 variants/xiao_ble/xiao_ble.sh create mode 100644 variants/xiao_ble/xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip diff --git a/boards/xiao_ble_sense.json b/boards/xiao_ble_sense.json new file mode 100644 index 000000000..09a28c25d --- /dev/null +++ b/boards/xiao_ble_sense.json @@ -0,0 +1,57 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v7.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_MDBT50Q_RX -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x810B"], + ["0x239A", "0x010B"], + ["0x239A", "0x810C"] + ], + "usb_product": "XIAO-BOOT", + "mcu": "nrf52840", + "variant": "Seeed_XIAO_nRF52840_Sense", + "bsp": { + "name": "adafruit" + }, + "softdevice": { + "sd_flags": "-DS140", + "sd_name": "s140", + "sd_version": "7.3.0", + "sd_fwid": "0x0123" + }, + "bootloader": { + "settings_addr": "0xFF000" + } + }, + "connectivity": ["bluetooth"], + "debug": { + "jlink_device": "nRF52840_xxAA", + "svd_path": "nrf52840.svd" + }, + "frameworks": ["arduino"], + "name": "Seeed Xiao BLE Sense", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": [ + "jlink", + "nrfjprog", + "nrfutil", + "stlink", + "cmsis-dap", + "blackmagic" + ], + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://www.seeedstudio.com/Seeed-XIAO-BLE-Sense-nRF52840-p-5253.html", + "vendor": "Seeed Studio" +} diff --git a/src/main.cpp b/src/main.cpp index 949dbcf1f..e1f34f728 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -250,6 +250,19 @@ void setup() fsInit(); +#if defined(_SEEED_XIAO_NRF52840_SENSE_H_) + + pinMode(CHARGE_LED, INPUT); // sets to detect if charge LED is on or off to see if USB is plugged in + + pinMode(HICHG, OUTPUT); + digitalWrite(HICHG, LOW); // 100 mA charging current if set to LOW and 50mA (actually about 20mA) if set to HIGH + + pinMode(BAT_READ, OUTPUT); + digitalWrite(BAT_READ, LOW); // This is pin P0_14 = 14 and by pullling low to GND it provices path to read on pin 32 (P0,31) + // PIN_VBAT the voltage from divider on XIAO board + +#endif + #ifdef I2C_SDA1 Wire1.begin(I2C_SDA1, I2C_SCL1); #endif diff --git a/src/mesh/SX126xInterface.cpp b/src/mesh/SX126xInterface.cpp index 144b8847d..fa158c22b 100644 --- a/src/mesh/SX126xInterface.cpp +++ b/src/mesh/SX126xInterface.cpp @@ -70,12 +70,25 @@ template bool SX126xInterface::init() #endif #if defined(SX126X_TXEN) && (SX126X_TXEN != RADIOLIB_NC) - // lora.begin sets Dio2 as RF switch control, which is not true if we are manually controlling RX and TX + // If SX126X_TXEN is connected to the MCU, we are manually controlling RX and TX. + // But lora.begin (called above) sets Dio2 as RF switch control, which is not true here, so set it back to false. if (res == RADIOLIB_ERR_NONE) { - LOG_DEBUG("SX126X_TX/RX EN pins defined. Setting RF Switch: RXEN=%i, TXEN=%i\n", SX126X_RXEN, SX126X_TXEN); + LOG_DEBUG("SX126X_TXEN pin defined. Setting RF Switch: RXEN=%i, TXEN=%i\n", SX126X_RXEN, SX126X_TXEN); res = lora.setDio2AsRfSwitch(false); lora.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); } +#elif defined(SX126X_RXEN) && (SX126X_RXEN != RADIOLIB_NC && defined(E22_TXEN_CONNECTED_TO_DIO2)) + // Otherwise, if SX126X_RXEN is connected to the MCU, and E22_TXEN_CONNECTED_TO_DIO2 is defined, we are letting the + // E22 control RX and TX via DIO2. In this configuration, the E22's TXEN and DIO2 pins are connected to each other, + // but not to the MCU. + // However, we must still connect the E22's RXEN pin to the MCU, define SX126X_RXEN accordingly, and then call + // setRfSwitchPins, otherwise RX sensitivity (observed via RSSI) is greatly diminished. + LOG_DEBUG("SX126X_RXEN and E22_TXEN_CONNECTED_TO_DIO2 are defined; value of res: %d", res); + if (res == RADIOLIB_ERR_NONE) { + LOG_DEBUG("SX126X_TXEN is RADIOLIB_NC, but SX126X_RXEN and E22_TXEN_CONNECTED_TO_DIO2 are both defined; calling " + "lora.setRfSwitchPins."); + lora.setRfSwitchPins(SX126X_RXEN, SX126X_TXEN); + } #endif if (config.lora.sx126x_rx_boosted_gain) { @@ -299,4 +312,4 @@ template bool SX126xInterface::sleep() #endif return true; -} +} \ No newline at end of file diff --git a/src/platform/nrf52/architecture.h b/src/platform/nrf52/architecture.h index 037e884cb..904ab908d 100644 --- a/src/platform/nrf52/architecture.h +++ b/src/platform/nrf52/architecture.h @@ -66,6 +66,14 @@ #endif +#ifdef _SEEED_XIAO_NRF52840_SENSE_H_ + +// This board uses 0 to be mean LED on +#undef LED_INVERTED +#define LED_INVERTED 1 + +#endif + #ifndef TTGO_T_ECHO #define GPS_UBLOX #endif diff --git a/variants/xiao_ble/README.md b/variants/xiao_ble/README.md new file mode 100644 index 000000000..6fff9cd22 --- /dev/null +++ b/variants/xiao_ble/README.md @@ -0,0 +1,261 @@ +# + +

+ Xiao BLE/BLE Sense + Ebyte E22-900M30S +

+ +

+ A step-by-step guide for macOS and Linux +

+ +## Introduction + +This guide will walk you through everything needed to get the Xiao BLE (or BLE Sense) running Meshtastic using an Ebyte E22-900M30S LoRa module. The combination of the E22 with an nRF52840 MCU is desirable because it allows for both very low idle (Rx) power draw and high transmit power. The Xiao BLE is a small but surprisingly well-appointed nRF52840 board, with enough GPIO for most Meshtastic applications and a built-in LiPo charger. The E22, on the other hand, is a famously inscrutable and mysterious beast. It is one of the more readily available LoRa modules capable of transmitting at 30 dBm, and includes an LNA to boost its Rx sensitivity a few dB beyond that of the SX1262. However, its documentation is relatively sparse overall, and seems to merely hint at (or completely omit) several key details regarding its functionality. Thus, much of what follows is a synthesis of my observations and inferences over the course of many hours of trial and error. + +

Acknowledgement and friendly disclaimer

+ +Huge thanks to those in the community who have forged the way with the E22, without whose hard work none of this would have been possible! (thebentern, riddick, rainer_vie, beegee-tokyo, geeksville, caveman99, Der_Bear, PlumRugOfDoom, BigCorvus, and many others.) + +
+ +Please take the conclusions here as a tentative work in progress, representing my current (and fairly limited) understanding of the E22 when paired with this particular MCU. It is my hope that this guide will be helpful to others who are interested in trying a DIY Meshtastic build, and also be subject to revision by folks with more experience and better test equipment. + +### Obligatory liability disclaimer + +This guide and all associated content is for informational purposes only. The information presented is intended for consumption only by persons having appropriate technical skill and judgement, to be used entirely at their own discretion and risk. The authors of this guide in no way provide any warranty, express or implied, toward the content herein, nor its correctness, safety, or suitability to any particular purpose. By following the instructions in this guide in part or in full, you assume all responsibility for all potential risks, including but not limited to fire, property damage, bodily injury, and death. + +### Note + +These instructions assume you are running macOS or Linux, but it should be relatively easy to translate each command for Windows. (In this case, in step 2 below, each line of `xiao_ble.sh` would also need to be converted to the equivalent Windows CLI command and run individually.) + +## 1. Update Bootloader + +The first thing you will need to do is update the Xiao BLE's bootloader. The stock bootloader is functionally very similar to the Adafruit nRF52 UF2 bootloader, but apparently not quite enough so to work with Meshtastic out of the box. + +1. Connect the Xiao BLE to your computer via USB-C. + +2. Install `adafruit-nrfutil` by following the instructions here. + +3. Open a terminal window and navigate to `firmware/variants/xiao_ble` (where `firmware` is the directory into which you have cloned the Meshtastic firmware repo). + +4. Run the following command, replacing `/dev/cu.usbmodem2101` with the serial port your Xiao BLE is connected to: + + ```bash + adafruit-nrfutil --verbose dfu serial --package xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip --port /dev/cu.usbmodem2101 -b 115200 --singlebank --touch 1200 + ``` + +5. If all goes well, the Xiao BLE's red LED should start to pulse slowly, and you should see a new USB storage device called `XIAO-BOOT` appear under `Locations` in Finder. + +  + +## 2. PlatformIO Environment Preparation + +Before building Meshtastic for the Xiao BLE + E22, it is necessary to pull in SoftDevice 7.3.0 and its associated linker script (nrf52840_s140_v7.ld) from Seeed Studio's Arduino core. The `xiao_ble.sh` script does this. + +1. In your terminal window, run the following command: + + ```bash + sudo ./xiao_ble.sh + ``` + +  + +## 3. Build Meshtastic + +At this point, you should be able to build the firmware successfully. + +1. In VS Code, press `Command Shift P` to bring up the command palette. + +2. Search for and run the `Developer: Reload Window` command. + +3. Bring up the command palette again with `Command Shift P`. Search for and run the `PlatformIO: Pick Project Environment` command. + +4. In the list of environments, select `env:xiao_ble`. PlatformIO may update itself for a minute or two, and should let you know once done. + +5. Return to the command palette once again (`Command Shift P`). Search for and run the `PlatformIO: Build` command. + +6. PlatformIO will build the project. After a few minutes you should see a green `SUCCESS` message. + +  + +## 4. Wire the board + +Connecting the E22 to the Xiao BLE is straightforward, but there are a few gotchas to be mindful of. + +- On the Xiao BLE: + + - Pins D4 and D5 are currently mapped to `PIN_WIRE_SDA` and `PIN_WIRE_SCL`, respectively. If you are not using I²C and would like to free up pins D4 and D5 for use as GPIO, `PIN_WIRE_SDA` and `PIN_WIRE_SCL` can be reassigned to any two other unused pin numbers. + + - Pins D6 and D7 were originally mapped to the TX and RX pins for serial interface 1 (`PIN_SERIAL1_RX` and `PIN_SERIAL1_TX`) but are currently set to -1 in `variant.h`. If you need to expose a serial interface, you can restore these pins and move e.g. `SX126X_RXEN` to pin 4 or 5 (the opposite should work too). + +- On the E22: + + - There are two options for the E22's `TXEN` pin: + + 1. It can be connected to the MCU on the pin defined as `SX126X_TXEN` in `variant.h`. In this configuration, the MCU will control Tx/Rx switching "manually". As long as `SX126X_TXEN` and `SX126X_RXEN` are both defined in `variant.h` (and neither is set to `RADIOLIB_NC`), `SX126xInterface.cpp` will initialize the E22 correctly for this mode. + + 2. Alternately, it can be connected to the E22's `DIO2` pin only, with neither `TXEN` nor `DIO2` being connected to the MCU. In this configuration, the E22 will control Tx/Rx switching automatically. In `variant.h`, as long as `SX126X_TXEN` is defined as `RADIOLIB_NC`, and `SX126X_RXEN` is defined and connected to the E22's `RXEN` pin, and `E22_TXEN_CONNECTED_TO_DIO2` is defined, `SX126xInterface.cpp` will initialize the E22 correctly for this mode. This configuration frees up a GPIO, and presents no drawbacks that I have found. + + - Note that any combination other than the two described above will likely result in unexpected behavior. In my testing, some of these other configurations appeared to "work" at first glance, but every one I tried had at least one of the following flaws: weak Tx power, extremely poor Rx sensitivity, or the E22 overheating because TXEN was never pulled low, causing its PA to stay on indefinitely. + + - Along the same lines, it is a good idea to check the E22's temperature frequently by lightly touching the shield. If you feel the shield getting hot (i.e. approaching uncomfortable to touch) near pins 1, 2, and 3, something is probably misconfigured; disconnect both the Xiao BLE and E22 from power and double check wiring and pin mapping. + + - Whether you opt to let the E22 control Rx and Tx or handle this manually, the E22's `RXEN` pin must always be connected to the MCU on the pin defined as `SX126X_RXEN` in `variant.h`. + +

Note

+ +The default pin mapping in `variant.h` uses 'automatic Tx/Rx switching' mode. If you wire your board for manual Rx/Tx switching, make sure to update `variant.h` accordingly by commenting/uncommenting the necessary lines in the 'E22 Tx/Rx control options' section. + +  + +--- + +  + +

Example wiring for "E22 automatic Tx/Rx switching" mode:

+  + +MCU -> E22 connections +| Xiao BLE pin | variant.h definition | E22 pin | Notes | +| :------------ | :---------------------------- | :-----------------| :------------------------------------------------------------------------------------------------------------------- | +| D0 | SX126X_CS | 19 (NSS) | | +| D1 | SX126X_DIO1 | 13 (DIO1) | | +| D2 | SX126X_BUSY | 14 (BUSY) | | +| D3 | SX126X_RESET | 15 (NRST) | | +| D7 | SX126X_RXEN | 6 (RXEN) | These pins must still be connected, and `SX126X_RXEN` defined in `variant.h`, otherwise Rx sensitivity will be poor. | +| D8 | PIN_SPI_SCK | 18 (SCK) | | +| D9 | PIN_SPI_MISO | 16 (MISO) | | +| D10 | PIN_SPI_MOSI | 17 (MOSI) | | + +  +  + +E22 -> E22 connections: +| E22 pin | E22 pin | Notes | +| :------------ | :---------------------------- | :------------------------------------------------------------------------ | +| TXEN | DIO2 | These must be physically connected for automatic Tx/Rx switching to work. | + +

Note

+ +The schematic (`xiao-ble-e22-schematic.png`) in the `eagle-project` directory uses this wiring. + +  + +--- + +  + +

Example wiring for "Manual Tx/Rx switching" mode:

+ +MCU -> E22 connections +| Xiao BLE pin | variant.h definition | E22 pin | Notes | +| :------------ | :---------------------------- | :-----------------| :--------------------------- | +| D0 | SX126X_CS | 19 (NSS) | | +| D1 | SX126X_DIO1 | 13 (DIO1) | | +| D2 | SX126X_BUSY | 14 (BUSY) | | +| D3 | SX126X_RESET | 15 (NRST) | | +| D6 | SX126X_TXEN | 7 (TXEN) | | +| D7 | SX126X_RXEN | 6 (RXEN) | | +| D8 | PIN_SPI_SCK | 18 (SCK) | | +| D9 | PIN_SPI_MISO | 16 (MISO) | | +| D10 | PIN_SPI_MOSI | 17 (MOSI) | | + +E22 -> E22 connections: (none) + +  + +## 5. Flash the firmware to the Xiao BLE + +1. Double press the Xiao's `reset` button to put it in bootloader mode. +2. In a terminal window, navigate to the Meshtastic firmware repo's root directory, and from there to `.pio/build/xiao_ble`. +3. Convert the generated `.hex` file into a `.uf2` file: + + ```bash + ../../../bin/uf2conv.py firmware.hex -c -o firmware.uf2 -f 0xADA52840 + ``` + +4. Copy the new `.uf2` file to the Xiao's mass storage volume: + + ```bash + cp firmware.uf2 /Volumes/XIAO-BOOT + ``` + +5. The Xiao's red LED will flash for several seconds as the firmware is copied. +6. Once the firmware is copied, to verify it is running, run the following command: + + ```bash + meshtastic --noproto + ``` + +7. Then, press the Xiao's `reset` button again. You should see a lot of debug output logged in the terminal window. + +  + +## 6. Troubleshooting + +- If after flashing Meshtastic, the Xiao is bootlooped, look at the serial output (you can see this by running `meshtastic --noproto` with the device connected to your computer via USB). + + - If you see that the SX1262 init result was -2, this likely indicates a wiring problem; double check your wiring and pin mapping in `variant.h`. + + - If you see an error mentioning tinyFS, this may mean you need to reformat the Xiao's storage: + + 1. Double press the `reset` button to put the Xiao in bootloader mode. + + 2. In a terminal window, navigate to the Meshtastic firmware repo's root directory, and from there to `variants/xiao_ble`. + + 3. Run the following command:  `cp xiao-ble-internal-format.uf2 /Volumes/XIAO-BOOT` + + 4. The Xiao's red LED will flash briefly as the filesystem format firmware is copied. + + 5. Run the following command:  `meshtastic --noproto` + + 6. In the output of the above command, you should see a message saying "Formatting...done". + + 7. To flash Meshtastic again, repeat the steps in section 5 above. + + - If you don't see any specific error message, but the boot process is stuck or not proceeding as expected, this might also mean there is a conflict in `variant.h`. If you have made any changes to the pin mapping, ensure they do not result in a conflict. If all else fails, try reverting your changes and using the known-good configuration included here. + + - The above might also mean something is wired incorrectly. Try reverting to one of the known-good example wirings in section 4. + +- If the E22 gets hot to the touch: + - The power amplifier is likely running continually. Disconnect it and the Xiao from power immediately, and double check wiring and pin mapping. In my experimentation this occurred in cases where TXEN was inadvertenly high (usually due to a pin mapping conflict). + +  + +## 7. Notes + +- There are several anecdotal recommendations regarding the Tx power the E22's internal SX1262 should be set to in order to achieve the advertised output of 30 dBm, ranging from 4 (per this article in the RadioLib github repo) to 22 (per this conversation from the Meshtastic Discord). When paired with the Xiao BLE in the configurations described above, I observed that the output is at its maximum when Tx power is set to 22. + +- To achieve its full output, the E22 should have a bypass capacitor from its 5V supply to ground. 100 µF works well. + +- The E22 will happily run on voltages lower than 5V, but the full output power will not be realized. For example, with a fully charged LiPo at 4.2V, Tx power appears to max out around 26-27 dBm. + +  + +## 8. Testing Methodology + +During what became a fairly long trial-and-error process, I did a lot of careful testing of Tx power and Rx sensitivity. My methodology in these tests was as follows: + +- All tests were conducted between two nodes: + + 1. The Xiao BLE + E22 coupled with an Abracon ARRKP4065-S915A ceramic patch antenna + + 2. A RAK 5005/4631 coupled with a Laird MA9-5N antenna via a 4" U.FL to Type N pigtail. + + - No other nodes were powered up onsite or nearby. + +
+ +- Each node and its antenna was kept in exactly the same position and orientation throughout testing. + +- Other environmental factors (e.g. the location and resting position of my body in the room while testing) were controlled as carefully as possible. + +- Each test comprised at least five (and often ten) runs, after which the results were averaged. + +- All testing was done by sending single-character messages between nodes and observing the received RSSI reported in the message acknowledgement. Messages were sent one by one, waiting for each to be acknowledged or time out before sending the next. + +- The E22's Tx power was observed by sending messages from the RAK to the Xiao BLE + E22 and recording the received RSSI. + +- The opposite was done to observe the E22's Rx sensitivity: messages were sent from the Xiao BLE + E22 to the RAK, and the received RSSI was recorded. + +While this cannot match the level of accuracy achievable with actual test equipment in a lab setting, it was nonetheless sufficient to demonstrate the (sometimes very large) differences in Tx power and Rx sensitivity between various configurations. diff --git a/variants/xiao_ble/platformio.ini b/variants/xiao_ble/platformio.ini new file mode 100644 index 000000000..c52e2c644 --- /dev/null +++ b/variants/xiao_ble/platformio.ini @@ -0,0 +1,13 @@ +; Seeed Xiao BLE: https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010448/16652893 +[env:xiao_ble] +extends = nrf52840_base +board = xiao_ble_sense +board_level = extra +build_flags = ${nrf52840_base.build_flags} -Ivariants/xiao_ble -Dxiao_ble -D EBYTE_E22 + -L "${platformio.libdeps_dir}/${this.__env__}/BSEC2 Software Library/src/cortex-m4/fpv4-sp-d16-hard" +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/xiao_ble> +lib_deps = + ${nrf52840_base.lib_deps} +debug_tool = jlink +; If not set we will default to uploading over serial (first it forces bootloader entry by talking 1200bps to cdcacm) +;upload_protocol = jlink \ No newline at end of file diff --git a/variants/xiao_ble/variant.cpp b/variants/xiao_ble/variant.cpp new file mode 100644 index 000000000..2c6c3e539 --- /dev/null +++ b/variants/xiao_ble/variant.cpp @@ -0,0 +1,55 @@ +#include "variant.h" +#include "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // D0 .. D13 + 2, // D0 is P0.02 (A0) + 3, // D1 is P0.03 (A1) + 28, // D2 is P0.28 (A2) + 29, // D3 is P0.29 (A3) + 4, // D4 is P0.04 (A4,SDA) + 5, // D5 is P0.05 (A5,SCL) + 43, // D6 is P1.11 (TX) + 44, // D7 is P1.12 (RX) + 45, // D8 is P1.13 (SCK) + 46, // D9 is P1.14 (MISO) + 47, // D10 is P1.15 (MOSI) + + // LEDs + 26, // D11 is P0.26 (LED RED) + 6, // D12 is P0.06 (LED BLUE) + 30, // D13 is P0.30 (LED GREEN) + 14, // D14 is P0.14 (READ_BAT) + + // LSM6DS3TR + 40, // D15 is P1.08 (6D_PWR) + 27, // D16 is P0.27 (6D_I2C_SCL) + 7, // D17 is P0.07 (6D_I2C_SDA) + 11, // D18 is P0.11 (6D_INT1) + + // MIC + 42, // 17,//42, // D19 is P1.10 (MIC_PWR) + 32, // 26,//32, // D20 is P1.00 (PDM_CLK) + 16, // 25,//16, // D21 is P0.16 (PDM_DATA) + + // BQ25100 + 13, // D22 is P0.13 (HICHG) + 17, // D23 is P0.17 (~CHG) + + // + 21, // D24 is P0.21 (QSPI_SCK) + 25, // D25 is P0.25 (QSPI_CSN) + 20, // D26 is P0.20 (QSPI_SIO_0 DI) + 24, // D27 is P0.24 (QSPI_SIO_1 DO) + 22, // D28 is P0.22 (QSPI_SIO_2 WP) + 23, // D29 is P0.23 (QSPI_SIO_3 HOLD) + + // NFC + 9, // D30 is P0.09 (NFC1) + 10, // D31 is P0.10 (NFC2) + + // VBAT + 31, // D32 is P0.10 (VBAT) +}; \ No newline at end of file diff --git a/variants/xiao_ble/variant.h b/variants/xiao_ble/variant.h new file mode 100644 index 000000000..ae7d37458 --- /dev/null +++ b/variants/xiao_ble/variant.h @@ -0,0 +1,201 @@ +#ifndef _SEEED_XIAO_NRF52840_SENSE_H_ +#define _SEEED_XIAO_NRF52840_SENSE_H_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +//#define USE_LFRC // Board uses RC for LF + +/*---------------------------------------------------------------------------- + * Headers + *----------------------------------------------------------------------------*/ + +#include "WVariant.h" + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +#define PINS_COUNT (33) +#define NUM_DIGITAL_PINS (33) +#define NUM_ANALOG_INPUTS (8) // A6 is used for battery, A7 is analog reference +#define NUM_ANALOG_OUTPUTS (0) + +// LEDs + +#define LED_RED 11 +#define LED_BLUE 12 +#define LED_GREEN 13 + +#define PIN_LED1 LED_GREEN +#define PIN_LED2 LED_BLUE +#define PIN_LED3 LED_RED + +#define PIN_LED PIN_LED1 +#define LED_PWR (PINS_COUNT) + +#define LED_BUILTIN PIN_LED + +#define LED_STATE_ON 1 // State when LED is lit + +/* + * Buttons + */ +#define PIN_BUTTON1 (PINS_COUNT) + +// Digital PINs +#define D0 (0ul) +#define D1 (1ul) +#define D2 (2ul) +#define D3 (3ul) +#define D4 (4ul) +#define D5 (5ul) +#define D6 (6ul) +#define D7 (7ul) +#define D8 (8ul) +#define D9 (9ul) +#define D10 (10ul) + +/* + * Analog pins + */ +#define PIN_A0 (0) +#define PIN_A1 (1) +#define PIN_A2 (2) +#define PIN_A3 (3) +#define PIN_A4 (4) +#define PIN_A5 (5) +#define PIN_VBAT (32) +#define VBAT_ENABLE (14) + +static const uint8_t A0 = PIN_A0; +static const uint8_t A1 = PIN_A1; +static const uint8_t A2 = PIN_A2; +static const uint8_t A3 = PIN_A3; +static const uint8_t A4 = PIN_A4; +static const uint8_t A5 = PIN_A5; +#define ADC_RESOLUTION 12 + +// Other pins +#define PIN_NFC1 (30) +#define PIN_NFC2 (31) + +/* + * Serial interfaces + */ +#define PIN_SERIAL1_RX (-1) // (7) +#define PIN_SERIAL1_TX (-1) // (6) + +#define PIN_SERIAL2_RX (-1) +#define PIN_SERIAL2_TX (-1) + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 1 + +#define PIN_SPI_MISO (9) +#define PIN_SPI_MOSI (10) +#define PIN_SPI_SCK (8) + +static const uint8_t SS = D0; +static const uint8_t MOSI = PIN_SPI_MOSI; +static const uint8_t MISO = PIN_SPI_MISO; +static const uint8_t SCK = PIN_SPI_SCK; + +// supported modules list +#define USE_SX1262 + +// common pinouts for SX126X modules +#define SX126X_CS D0 +#define SX126X_DIO1 D1 +#define SX126X_BUSY D2 +#define SX126X_RESET D3 + +// ---------------------------------------------------------------- + +// E22 Tx/Rx control options: + +// 1. Let the E22 control Tx and Rx automagically via DIO2. + +// * The E22's TXEN and DIO2 pins are connected to each other, but not to the MCU. +// * The E22's RXEN pin *is* connected to the MCU. +// * E22_TXEN_CONNECTED_TO_DIO2 is defined so the logic in SX126XInterface.cpp handles this configuration correctly. + +#define SX126X_TXEN RADIOLIB_NC +#define SX126X_RXEN D7 +#define E22_TXEN_CONNECTED_TO_DIO2 + +// ------------------------------ OR ------------------------------ + +// 2. Control Tx and Rx manually. + +// * The E22's TXEN and RXEN pins are both connected to the MCU. + +// #define SX126X_TXEN D6 +// #define SX126X_RXEN D7 + +// ---------------------------------------------------------------- + +#ifdef EBYTE_E22 +// Internally the TTGO module hooks the SX126x-DIO2 in to control the TX/RX switch +// (which is the default for the sx1262interface code) +#define SX126X_E22 +#endif + +/* + * Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 // 2 + +#define PIN_WIRE_SDA (4) +#define PIN_WIRE_SCL (5) + +static const uint8_t SDA = PIN_WIRE_SDA; +static const uint8_t SCL = PIN_WIRE_SCL; + +#define PIN_LSM6DS3TR_C_POWER (15) +#define PIN_LSM6DS3TR_C_INT1 (18) + +// PDM Interfaces +// --------------- +#define PIN_PDM_PWR (19) +#define PIN_PDM_CLK (20) +#define PIN_PDM_DIN (21) + +// QSPI Pins +#define PIN_QSPI_SCK (24) +#define PIN_QSPI_CS (25) +#define PIN_QSPI_IO0 (26) +#define PIN_QSPI_IO1 (27) +#define PIN_QSPI_IO2 (28) +#define PIN_QSPI_IO3 (29) + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES P25Q16H +#define EXTERNAL_FLASH_USE_QSPI + +// Battery + +#define BAT_READ \ + 14 // P0_14 = 14 Reads battery voltage from divider on signal board. (PIN_VBAT is reading voltage divider on XIAO and is + // program pin 32 / or P0.31) +#define CHARGE_LED 23 // P0_17 = 17 D23 YELLOW CHARGE LED +#define HICHG 22 // P0_13 = 13 D22 Charge-select pin for Lipo for 100 mA instead of default 50mA charge + +// The battery sense is hooked to pin A0 (5) +#define BATTERY_PIN PIN_VBAT // PIN_A0 + +// ratio of voltage divider = 3.0 (R17=1M, R18=510k) +#define ADC_MULTIPLIER 3 // 3.0 + a bit for being optimistic + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file diff --git a/variants/xiao_ble/xiao-ble-internal-format.uf2 b/variants/xiao_ble/xiao-ble-internal-format.uf2 new file mode 100644 index 0000000000000000000000000000000000000000..59de2c68a6a49b308f77a83bb278e89e720860c8 GIT binary patch literal 122880 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 SzGBepAo%~^{dfsb{Qnd2oeWg~ literal 0 HcmV?d00001 diff --git a/variants/xiao_ble/xiao_ble.sh b/variants/xiao_ble/xiao_ble.sh new file mode 100755 index 000000000..2f3cc5390 --- /dev/null +++ b/variants/xiao_ble/xiao_ble.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# adapted from the script linked in this very helpful article: https://enzolombardi.net/low-power-bluetooth-advertising-with-xiao-ble-and-platformio-e8e7d0da80d2 + +# source: https://gist.githubusercontent.com/turing-complete-labs/b3105ee653782183c54b4fdbe18f411f/raw/d86779ba7702775d3b79781da63d85442acd9de6/xiao_ble.sh +# download the core for arduino from seeedstudio. Softdevice 7.3.0, linker and variants folder are what we need +curl https://files.seeedstudio.com/arduino/core/nRF52840/Arduino_core_nRF52840.tar.bz2 -o arduino.core.1.0.0.tar.bz2 +tar -xjf arduino.core.1.0.0.tar.bz2 +rm arduino.core.1.0.0.tar.bz2 + +# copy the needed files +cp 1.0.0/cores/nRF5/linker/nrf52840_s140_v7.ld ~/.platformio/packages/framework-arduinoadafruitnrf52/cores/nRF5/linker +cp -r 1.0.0/cores/nRF5/nordic/softdevice/s140_nrf52_7.3.0_API ~/.platformio/packages/framework-arduinoadafruitnrf52/cores/nRF5/nordic/softdevice + +rm -rf 1.0.0 +echo done! diff --git a/variants/xiao_ble/xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip b/variants/xiao_ble/xiao_nrf52840_ble_bootloader-0.7.0-22-g277a0c8_s140_7.3.0.zip new file mode 100644 index 0000000000000000000000000000000000000000..40b966bafe77aaca21d5408973ae8128e5040e01 GIT binary patch literal 192586 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Μ) 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 d;a^LvLT2F-S5zRGTtOKN_& Date: Sat, 22 Jul 2023 09:26:54 -0500 Subject: [PATCH 34/57] T-Watch S3 Support (#2632) * T-Watch WIP * Updates * Temp * Update screen spi bus and and backlight en * Peripherals progress * Fixes * Fixes * Updates * DRV scaffolding * Fixed touch-screen driver selection. WIP on DRV haptic feedback * DRV2605 pmu channel * Trunk * Fixes and defaults * Dropped an s * Move PMU and turn off screen that way * Add t-deck and t-watch-s3 to CI and cleanup * More cleanup --- .github/workflows/main_matrix.yml | 4 + boards/t-watch-s3.json | 38 +++++++++ platformio.ini | 1 + src/AccelerometerThread.h | 96 ++++++++++++++++++++-- src/ButtonThread.h | 16 ++++ src/Power.cpp | 11 ++- src/configuration.h | 3 +- src/detect/ScanI2C.cpp | 6 +- src/detect/ScanI2C.h | 1 + src/detect/ScanI2CTwoWire.cpp | 1 + src/graphics/Screen.cpp | 6 ++ src/graphics/Screen.h | 9 +- src/graphics/TFTDisplay.cpp | 23 +++++- src/graphics/TFTDisplay.h | 4 +- src/main.cpp | 14 ++++ src/main.h | 6 ++ src/mesh/NodeDB.cpp | 12 ++- src/modules/ExternalNotificationModule.cpp | 34 ++++++-- src/platform/esp32/architecture.h | 6 ++ src/sleep.cpp | 10 +-- src/sleep.h | 6 ++ variants/t-watch-s3/pins_arduino.h | 64 +++++++++++++++ variants/t-watch-s3/platformio.ini | 17 ++++ variants/t-watch-s3/variant.h | 64 +++++++++++++++ variants/tbeam-s3-core/variant.h | 4 +- 25 files changed, 423 insertions(+), 33 deletions(-) create mode 100644 boards/t-watch-s3.json create mode 100644 variants/t-watch-s3/pins_arduino.h create mode 100644 variants/t-watch-s3/platformio.ini create mode 100644 variants/t-watch-s3/variant.h diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 237322433..acc07dec9 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -33,6 +33,8 @@ jobs: - board: m5stack-coreink - board: tbeam-s3-core - board: tlora-t3s3-v1 + - board: t-watch-s3 + - board: t-deck #- board: rak11310 runs-on: ubuntu-latest @@ -88,6 +90,8 @@ jobs: - board: heltec-wireless-paper - board: tbeam-s3-core - board: tlora-t3s3-v1 + - board: t-watch-s3 + - board: t-deck uses: ./.github/workflows/build_esp32_s3.yml with: board: ${{ matrix.board }} diff --git a/boards/t-watch-s3.json b/boards/t-watch-s3.json new file mode 100644 index 000000000..d4c9f2abd --- /dev/null +++ b/boards/t-watch-s3.json @@ -0,0 +1,38 @@ +{ + "build": { + "arduino": { + "ldscript": "esp32s3_out.ld" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DT_WATCH_S3", + "-DARDUINO_USB_CDC_ON_BOOT=1", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "dio", + "hwids": [["0X303A", "0x1001"]], + "mcu": "esp32s3", + "variant": "t-watch-s3" + }, + "connectivity": ["wifi"], + "debug": { + "openocd_target": "esp32s3.cfg" + }, + "frameworks": ["arduino"], + "name": "LilyGo T-Watch 2020 V3", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 8388608, + "require_upload_port": true, + "use_1200bps_touch": true, + "wait_for_upload_port": true + }, + "url": "http://www.lilygo.cn/", + "vendor": "LilyGo" +} diff --git a/platformio.ini b/platformio.ini index e19175af7..08b00fee3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -121,3 +121,4 @@ lib_deps = adafruit/Adafruit PM25 AQI Sensor@^1.0.6 adafruit/Adafruit MPU6050@^2.2.4 adafruit/Adafruit LIS3DH@^1.2.4 + https://github.com/lewisxhe/BMA423_Library@^0.0.1 \ No newline at end of file diff --git a/src/AccelerometerThread.h b/src/AccelerometerThread.h index 307ca233e..da5695368 100644 --- a/src/AccelerometerThread.h +++ b/src/AccelerometerThread.h @@ -6,10 +6,37 @@ #include #include +#include +#include +#include + +BMA423 bmaSensor; +bool BMA_IRQ = false; #define ACCELEROMETER_CHECK_INTERVAL_MS 100 #define ACCELEROMETER_CLICK_THRESHOLD 40 +uint16_t readRegister(uint8_t address, uint8_t reg, uint8_t *data, uint16_t len) +{ + Wire.beginTransmission(address); + Wire.write(reg); + Wire.endTransmission(); + Wire.requestFrom((uint8_t)address, (uint8_t)len); + uint8_t i = 0; + while (Wire.available()) { + data[i++] = Wire.read(); + } + return 0; // Pass +} + +uint16_t writeRegister(uint8_t address, uint8_t reg, uint8_t *data, uint16_t len) +{ + Wire.beginTransmission(address); + Wire.write(reg); + Wire.write(data, len); + return (0 != Wire.endTransmission()); +} + namespace concurrency { class AccelerometerThread : public concurrency::OSThread @@ -29,10 +56,10 @@ class AccelerometerThread : public concurrency::OSThread return; } - accleremoter_type = type; + acceleremoter_type = type; LOG_DEBUG("AccelerometerThread initializing\n"); - if (accleremoter_type == ScanI2C::DeviceType::MPU6050 && mpu.begin(accelerometer_found.address)) { + if (acceleremoter_type == ScanI2C::DeviceType::MPU6050 && mpu.begin(accelerometer_found.address)) { LOG_DEBUG("MPU6050 initializing\n"); // setup motion detection mpu.setHighPassFilter(MPU6050_HIGHPASS_0_63_HZ); @@ -40,11 +67,60 @@ class AccelerometerThread : public concurrency::OSThread mpu.setMotionDetectionDuration(20); mpu.setInterruptPinLatch(true); // Keep it latched. Will turn off when reinitialized. mpu.setInterruptPinPolarity(true); - } else if (accleremoter_type == ScanI2C::DeviceType::LIS3DH && lis.begin(accelerometer_found.address)) { + } else if (acceleremoter_type == ScanI2C::DeviceType::LIS3DH && lis.begin(accelerometer_found.address)) { LOG_DEBUG("LIS3DH initializing\n"); lis.setRange(LIS3DH_RANGE_2_G); // Adjust threshold, higher numbers are less sensitive lis.setClick(config.device.double_tap_as_button_press ? 2 : 1, ACCELEROMETER_CLICK_THRESHOLD); + } else if (acceleremoter_type == ScanI2C::DeviceType::BMA423 && bmaSensor.begin(readRegister, writeRegister, delay)) { + LOG_DEBUG("BMA423 initializing\n"); + Acfg cfg; + cfg.odr = BMA4_OUTPUT_DATA_RATE_100HZ; + cfg.range = BMA4_ACCEL_RANGE_2G; + cfg.bandwidth = BMA4_ACCEL_NORMAL_AVG4; + cfg.perf_mode = BMA4_CONTINUOUS_MODE; + bmaSensor.setAccelConfig(cfg); + bmaSensor.enableAccel(); + + struct bma4_int_pin_config pin_config; + pin_config.edge_ctrl = BMA4_LEVEL_TRIGGER; + pin_config.lvl = BMA4_ACTIVE_HIGH; + pin_config.od = BMA4_PUSH_PULL; + pin_config.output_en = BMA4_OUTPUT_ENABLE; + pin_config.input_en = BMA4_INPUT_DISABLE; + // The correct trigger interrupt needs to be configured as needed + bmaSensor.setINTPinConfig(pin_config, BMA4_INTR1_MAP); + +#ifdef BMA423_INT + pinMode(BMA4XX_INT, INPUT); + attachInterrupt( + BMA4XX_INT, + [] { + // Set interrupt to set irq value to true + BMA_IRQ = true; + }, + RISING); // Select the interrupt mode according to the actual circuit +#endif + + struct bma423_axes_remap remap_data; + remap_data.x_axis = 0; + remap_data.x_axis_sign = 1; + remap_data.y_axis = 1; + remap_data.y_axis_sign = 0; + remap_data.z_axis = 2; + remap_data.z_axis_sign = 1; + // Need to raise the wrist function, need to set the correct axis + bmaSensor.setRemapAxes(&remap_data); + // sensor.enableFeature(BMA423_STEP_CNTR, true); + bmaSensor.enableFeature(BMA423_TILT, true); + bmaSensor.enableFeature(BMA423_WAKEUP, true); + // sensor.resetStepCounter(); + + // Turn on feature interrupt + bmaSensor.enableStepCountInterrupt(); + bmaSensor.enableTiltInterrupt(); + // It corresponds to isDoubleClick interrupt + bmaSensor.enableWakeupInterrupt(); } } @@ -53,9 +129,9 @@ class AccelerometerThread : public concurrency::OSThread { canSleep = true; // Assume we should not keep the board awake - if (accleremoter_type == ScanI2C::DeviceType::MPU6050 && mpu.getMotionInterruptStatus()) { + if (acceleremoter_type == ScanI2C::DeviceType::MPU6050 && mpu.getMotionInterruptStatus()) { wakeScreen(); - } else if (accleremoter_type == ScanI2C::DeviceType::LIS3DH && lis.getClick() > 0) { + } else if (acceleremoter_type == ScanI2C::DeviceType::LIS3DH && lis.getClick() > 0) { uint8_t click = lis.getClick(); if (!config.device.double_tap_as_button_press) { wakeScreen(); @@ -65,7 +141,13 @@ class AccelerometerThread : public concurrency::OSThread buttonPress(); return 500; } + } else if (acceleremoter_type == ScanI2C::DeviceType::BMA423 && bmaSensor.getINT()) { + if (bmaSensor.isTilt() || bmaSensor.isDoubleClick()) { + wakeScreen(); + return 500; + } } + return ACCELEROMETER_CHECK_INTERVAL_MS; } @@ -84,9 +166,9 @@ class AccelerometerThread : public concurrency::OSThread powerFSM.trigger(EVENT_PRESS); } - ScanI2C::DeviceType accleremoter_type; + ScanI2C::DeviceType acceleremoter_type; Adafruit_MPU6050 mpu; Adafruit_LIS3DH lis; }; -} // namespace concurrency +} // namespace concurrency \ No newline at end of file diff --git a/src/ButtonThread.h b/src/ButtonThread.h index f03d2861a..fcbb73af0 100644 --- a/src/ButtonThread.h +++ b/src/ButtonThread.h @@ -4,6 +4,7 @@ #include "concurrency/OSThread.h" #include "configuration.h" #include "graphics/Screen.h" +#include "main.h" #include "power.h" #include @@ -100,6 +101,21 @@ class ButtonThread : public concurrency::OSThread #endif // if (!canSleep) LOG_DEBUG("Suppressing sleep!\n"); // else LOG_DEBUG("sleep ok\n"); +#if defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ST7789_CS) + int x, y = 0; + screen->getTouch(&x, &y); + if (x > 0 && y > 0) { +#ifdef T_WATCH_S3 + drv.setWaveform(0, 75); + drv.setWaveform(1, 0); // end waveform + drv.go(); +#endif + LOG_DEBUG("touch %d %d\n", x, y); + powerFSM.trigger(EVENT_PRESS); + return 150; // Check for next touch every in 150ms + } + +#endif return 5; } diff --git a/src/Power.cpp b/src/Power.cpp index ac1789cb0..a66ed4ec7 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -540,10 +540,12 @@ int32_t Power::runOnce() LOG_DEBUG("Battery removed\n"); } */ +#ifndef T_WATCH_S3 // FIXME - why is this triggering on the T-Watch S3? if (PMU->isPekeyLongPressIrq()) { LOG_DEBUG("PEK long button press\n"); screen->setOn(false); } +#endif PMU->clearIrqStatus(); } @@ -681,7 +683,8 @@ bool Power::axpChipInit() // GNSS VDD 3300mV PMU->setPowerChannelVoltage(XPOWERS_ALDO3, 3300); PMU->enablePowerOutput(XPOWERS_ALDO3); - } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE) { + } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE || + HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) { // t-beam s3 core /** * gnss module power channel @@ -716,6 +719,12 @@ bool Power::axpChipInit() PMU->setPowerChannelVoltage(XPOWERS_BLDO1, 3300); PMU->enablePowerOutput(XPOWERS_BLDO1); +#ifdef T_WATCH_S3 + // DRV2605 power channel + PMU->setPowerChannelVoltage(XPOWERS_BLDO2, 3300); + PMU->enablePowerOutput(XPOWERS_BLDO2); +#endif + // PMU->setPowerChannelVoltage(XPOWERS_DCDC4, 3300); // PMU->enablePowerOutput(XPOWERS_DCDC4); diff --git a/src/configuration.h b/src/configuration.h index e1420c8db..3e289ef54 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -123,6 +123,7 @@ along with this program. If not, see . // ----------------------------------------------------------------------------- #define MPU6050_ADDR 0x68 #define LIS3DH_ADR 0x18 +#define BMA423_ADDR 0x19 // ----------------------------------------------------------------------------- // LED @@ -193,4 +194,4 @@ along with this program. If not, see . #ifndef HW_VENDOR #error HW_VENDOR must be defined -#endif +#endif \ No newline at end of file diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 4ce848612..75b23f419 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -36,8 +36,8 @@ ScanI2C::FoundDevice ScanI2C::firstKeyboard() const ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const { - ScanI2C::DeviceType types[] = {MPU6050, LIS3DH}; - return firstOfOrNONE(2, types); + ScanI2C::DeviceType types[] = {MPU6050, LIS3DH, BMA423}; + return firstOfOrNONE(3, types); } ScanI2C::FoundDevice ScanI2C::find(ScanI2C::DeviceType) const @@ -73,4 +73,4 @@ bool ScanI2C::DeviceAddress::operator<(const ScanI2C::DeviceAddress &other) cons || (port != NO_I2C && other.port != NO_I2C && (address < other.address)); } -ScanI2C::FoundDevice::FoundDevice(ScanI2C::DeviceType type, ScanI2C::DeviceAddress address) : type(type), address(address) {} +ScanI2C::FoundDevice::FoundDevice(ScanI2C::DeviceType type, ScanI2C::DeviceAddress address) : type(type), address(address) {} \ No newline at end of file diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 4b6361cfd..559cff7ec 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -33,6 +33,7 @@ class ScanI2C PMSA0031, MPU6050, LIS3DH, + BMA423, #ifdef HAS_NCP5623 NCP5623, #endif diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 7b5bb0a12..66e092951 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -274,6 +274,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port) SCAN_SIMPLE_CASE(PMSA0031_ADDR, PMSA0031, "PMSA0031 air quality sensor found\n") SCAN_SIMPLE_CASE(MPU6050_ADDR, MPU6050, "MPU6050 accelerometer found\n"); + SCAN_SIMPLE_CASE(BMA423_ADDR, BMA423, "BMA423 accelerometer found\n"); default: LOG_INFO("Device found at address 0x%x was not able to be enumerated\n", addr.address); diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index 049382c19..d1cc5ad36 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -944,6 +944,9 @@ void Screen::handleSetOn(bool on) if (on != screenOn) { if (on) { LOG_INFO("Turning on screen\n"); +#ifdef T_WATCH_S3 + PMU->enablePowerOutput(XPOWERS_ALDO2); +#endif dispdev.displayOn(); dispdev.displayOn(); enabled = true; @@ -952,6 +955,9 @@ void Screen::handleSetOn(bool on) } else { LOG_INFO("Turning off screen\n"); dispdev.displayOff(); +#ifdef T_WATCH_S3 + PMU->disablePowerOutput(XPOWERS_ALDO2); +#endif enabled = false; } screenOn = on; diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index debc6ac0b..9ebe1c75a 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -313,6 +313,13 @@ class Screen : public concurrency::OSThread void setWelcomeFrames(); + void getTouch(int *x, int *y) + { +#if defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ST7789_CS) + dispdev.getTouch(x, y); +#endif + }; + protected: /// Updates the UI. // @@ -394,4 +401,4 @@ class Screen : public concurrency::OSThread }; } // namespace graphics -#endif +#endif \ No newline at end of file diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 6f58d421d..1f67f90c9 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -108,7 +108,11 @@ class LGFX : public lgfx::LGFX_Device lgfx::Panel_ST7789 _panel_instance; lgfx::Bus_SPI _bus_instance; lgfx::Light_PWM _light_instance; +#ifdef T_WATCH_S3 + lgfx::Touch_FT5x06 _touch_instance; +#else lgfx::Touch_GT911 _touch_instance; +#endif public: LGFX(void) @@ -194,8 +198,13 @@ class LGFX : public lgfx::LGFX_Device // I2C cfg.i2c_port = 1; cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; +#ifdef SCREEN_TOUCH_USE_I2C1 + cfg.pin_sda = I2C_SDA1; + cfg.pin_scl = I2C_SCL1; +#else cfg.pin_sda = I2C_SDA; cfg.pin_scl = I2C_SCL; +#endif cfg.freq = 400000; _touch_instance.config(cfg); @@ -261,7 +270,7 @@ void TFTDisplay::sendCommand(uint8_t com) // handle display on/off directly switch (com) { case DISPLAYON: { -#ifdef TFT_BL +#if defined(TFT_BL) && defined(TFT_BACKLIGHT_ON) digitalWrite(TFT_BL, TFT_BACKLIGHT_ON); #endif #ifdef VTFT_CTRL @@ -270,7 +279,7 @@ void TFTDisplay::sendCommand(uint8_t com) break; } case DISPLAYOFF: { -#ifdef TFT_BL +#if defined(TFT_BL) && defined(TFT_BACKLIGHT_ON) digitalWrite(TFT_BL, !TFT_BACKLIGHT_ON); #endif #ifdef VTFT_CTRL @@ -304,6 +313,8 @@ bool TFTDisplay::connect() tft.init(); #if defined(M5STACK) || defined(T_DECK) tft.setRotation(1); // M5Stack/T-Deck have the TFT in landscape +#elif defined(T_WATCH_S3) + tft.setRotation(0); // T-Watch S3 has the TFT in portrait #else tft.setRotation(3); // Orient horizontal and wide underneath the silkscreen name label #endif @@ -311,4 +322,12 @@ bool TFTDisplay::connect() return true; } +// Get touch coords from the display +void TFTDisplay::getTouch(int *x, int *y) +{ +#ifndef M5STACK + tft.getTouch(x, y); #endif +} + +#endif \ No newline at end of file diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h index 46cfe85e7..03293d6f4 100644 --- a/src/graphics/TFTDisplay.h +++ b/src/graphics/TFTDisplay.h @@ -28,6 +28,8 @@ class TFTDisplay : public OLEDDisplay */ void setDetected(uint8_t detected); + void getTouch(int *x, int *y); + protected: // the header size of the buffer used, e.g. for the SPI command header virtual int getBufferOffset(void) override { return 0; } @@ -37,4 +39,4 @@ class TFTDisplay : public OLEDDisplay // Connect to the display virtual bool connect() override; -}; +}; \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index e1f34f728..101ef51d8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -110,6 +110,11 @@ ScanI2C::FoundDevice rgb_found = ScanI2C::FoundDevice(ScanI2C::DeviceType::NONE, ATECCX08A atecc; #endif +#ifdef T_WATCH_S3 +Adafruit_DRV2605 drv; +#endif +bool isVibrating = false; + bool eink_found = true; uint32_t serialSinceMsec; @@ -485,10 +490,19 @@ void setup() #if !defined(ARCH_PORTDUINO) && !defined(ARCH_STM32WL) if (acc_info.type != ScanI2C::DeviceType::NONE) { + config.display.wake_on_tap_or_motion = true; + moduleConfig.external_notification.enabled = true; accelerometerThread = new AccelerometerThread(acc_info.type); } #endif +#ifdef T_WATCH_S3 + drv.begin(); + drv.selectLibrary(1); + // I2C trigger by sending 'go' command + drv.setMode(DRV2605_MODE_INTTRIG); +#endif + // Init our SPI controller (must be before screen and lora) initSPI(); #ifdef ARCH_RP2040 diff --git a/src/main.h b/src/main.h index c13c92bd4..b513d6478 100644 --- a/src/main.h +++ b/src/main.h @@ -38,6 +38,12 @@ extern bool isUSBPowered; extern ATECCX08A atecc; #endif +#ifdef T_WATCH_S3 +#include +extern Adafruit_DRV2605 drv; +#endif +extern bool isVibrating; + extern int TCPPort; // set by Portduino // Global Screen singleton. diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 6d71a750c..37e19eed1 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -165,6 +165,7 @@ void NodeDB::installDefaultConfig() config.has_network = true; config.has_bluetooth = true; config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; + config.lora.sx126x_rx_boosted_gain = false; config.lora.tx_enabled = true; // FIXME: maybe false in the future, and setting region to enable it. (unset region forces it off) @@ -195,6 +196,11 @@ void NodeDB::installDefaultConfig() config.position.position_flags = (meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE | meshtastic_Config_PositionConfig_PositionFlags_ALTITUDE_MSL); +#ifdef T_WATCH_S3 + config.display.screen_on_secs = 30; + config.display.wake_on_tap_or_motion = true; +#endif + initConfigIntervals(); } @@ -233,6 +239,10 @@ void NodeDB::installDefaultModuleConfig() moduleConfig.external_notification.alert_message = true; moduleConfig.external_notification.output_ms = 1000; moduleConfig.external_notification.nag_timeout = 60; +#endif +#ifdef T_WATCH_S3 + // Don't worry about the other settings, we'll use the DRV2056 behavior for notifications + moduleConfig.external_notification.enabled = true; #endif moduleConfig.has_canned_message = true; @@ -916,4 +926,4 @@ void recordCriticalError(meshtastic_CriticalErrorCode code, uint32_t address, co LOG_ERROR("A critical failure occurred, portduino is exiting..."); exit(2); #endif -} +} \ No newline at end of file diff --git a/src/modules/ExternalNotificationModule.cpp b/src/modules/ExternalNotificationModule.cpp index 79bbb4028..7dd6dd1ce 100644 --- a/src/modules/ExternalNotificationModule.cpp +++ b/src/modules/ExternalNotificationModule.cpp @@ -93,6 +93,10 @@ int32_t ExternalNotificationModule::runOnce() rgb.setColor(red, green, blue); } #endif + +#ifdef T_WATCH_S3 + drv.go(); +#endif } // now let the PWM buzzer play @@ -124,7 +128,8 @@ void ExternalNotificationModule::setExternalOn(uint8_t index) digitalWrite(moduleConfig.external_notification.output_buzzer, true); break; default: - digitalWrite(output, (moduleConfig.external_notification.active ? true : false)); + if (output > 0) + digitalWrite(output, (moduleConfig.external_notification.active ? true : false)); break; } #ifdef HAS_NCP5623 @@ -132,6 +137,9 @@ void ExternalNotificationModule::setExternalOn(uint8_t index) rgb.setColor(red, green, blue); } #endif +#ifdef T_WATCH_S3 + drv.go(); +#endif } void ExternalNotificationModule::setExternalOff(uint8_t index) @@ -149,7 +157,8 @@ void ExternalNotificationModule::setExternalOff(uint8_t index) digitalWrite(moduleConfig.external_notification.output_buzzer, false); break; default: - digitalWrite(output, (moduleConfig.external_notification.active ? false : true)); + if (output > 0) + digitalWrite(output, (moduleConfig.external_notification.active ? false : true)); break; } @@ -161,6 +170,9 @@ void ExternalNotificationModule::setExternalOff(uint8_t index) rgb.setColor(red, green, blue); } #endif +#ifdef T_WATCH_S3 + drv.stop(); +#endif } bool ExternalNotificationModule::getExternal(uint8_t index) @@ -174,6 +186,9 @@ void ExternalNotificationModule::stopNow() nagCycleCutoff = 1; // small value isNagging = false; setIntervalFromNow(0); +#ifdef T_WATCH_S3 + drv.stop(); +#endif } ExternalNotificationModule::ExternalNotificationModule() @@ -185,7 +200,6 @@ ExternalNotificationModule::ExternalNotificationModule() without having to configure it from the PythonAPI or WebUI. */ - // moduleConfig.external_notification.enabled = true; // moduleConfig.external_notification.alert_message = true; // moduleConfig.external_notification.alert_message_buzzer = true; // moduleConfig.external_notification.alert_message_vibra = true; @@ -213,8 +227,10 @@ ExternalNotificationModule::ExternalNotificationModule() : EXT_NOTIFICATION_MODULE_OUTPUT; // Set the direction of a pin - LOG_INFO("Using Pin %i in digital mode\n", output); - pinMode(output, OUTPUT); + if (output > 0) { + LOG_INFO("Using Pin %i in digital mode\n", output); + pinMode(output, OUTPUT); + } setExternalOff(0); externalTurnedOn[0] = 0; if (moduleConfig.external_notification.output_vibra) { @@ -250,7 +266,12 @@ ExternalNotificationModule::ExternalNotificationModule() ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshPacket &mp) { if (moduleConfig.external_notification.enabled) { - +#if T_WATCH_S3 + drv.setWaveform(0, 75); + drv.setWaveform(1, 56); + drv.setWaveform(2, 0); + drv.go(); +#endif if (getFrom(&mp) != nodeDB.getNodeNum()) { // Check if the message contains a bell character. Don't do this loop for every pin, just once. @@ -343,7 +364,6 @@ ProcessMessage ExternalNotificationModule::handleReceived(const meshtastic_MeshP } setIntervalFromNow(0); // run once so we know if we should do something } - } else { LOG_INFO("External Notification Module Disabled\n"); } diff --git a/src/platform/esp32/architecture.h b/src/platform/esp32/architecture.h index c6842eee9..6e80b50e6 100644 --- a/src/platform/esp32/architecture.h +++ b/src/platform/esp32/architecture.h @@ -46,6 +46,10 @@ #if defined(HAS_AXP192) || defined(HAS_AXP2101) #define HAS_PMU #endif + +#ifdef PIN_BUTTON_TOUCH +#define BUTTON_PIN_TOUCH PIN_BUTTON_TOUCH +#endif // // set HW_VENDOR // @@ -83,6 +87,8 @@ #define HW_VENDOR meshtastic_HardwareModel_TLORA_V2_1_1P8 #elif defined(T_DECK) #define HW_VENDOR meshtastic_HardwareModel_T_DECK +#elif defined(T_WATCH_S3) +#define HW_VENDOR meshtastic_HardwareModel_T_WATCH_S3 #elif defined(GENIEBLOCKS) #define HW_VENDOR meshtastic_HardwareModel_GENIEBLOCKS #elif defined(PRIVATE_HW) diff --git a/src/sleep.cpp b/src/sleep.cpp index 0b8fbb782..3d6da7feb 100644 --- a/src/sleep.cpp +++ b/src/sleep.cpp @@ -23,11 +23,6 @@ esp_sleep_source_t wakeCause; // the reason we booted this time #define INCLUDE_vTaskSuspend 0 #endif -#ifdef HAS_PMU -#include "XPowersLibInterface.hpp" -extern XPowersLibInterface *PMU; -#endif - /// Called to ask any observers if they want to veto sleep. Return 1 to veto or 0 to allow sleep to happen Observable preflightSleep; @@ -259,7 +254,8 @@ void doDeepSleep(uint32_t msecToWake) if (HW_VENDOR == meshtastic_HardwareModel_TBEAM) { // t-beam v1.2 radio power channel PMU->disablePowerOutput(XPOWERS_ALDO2); // lora radio power channel - } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE) { + } else if (HW_VENDOR == meshtastic_HardwareModel_LILYGO_TBEAM_S3_CORE || + HW_VENDOR == meshtastic_HardwareModel_T_WATCH_S3) { PMU->disablePowerOutput(XPOWERS_ALDO3); // lora radio power channel } } else if (model == XPOWERS_AXP192) { @@ -388,4 +384,4 @@ void enableModemSleep() int rv = esp_pm_configure(&esp32_config); LOG_DEBUG("Sleep request result %x\n", rv); } -#endif +#endif \ No newline at end of file diff --git a/src/sleep.h b/src/sleep.h index a4b8f37b5..856d8d6b1 100644 --- a/src/sleep.h +++ b/src/sleep.h @@ -12,6 +12,12 @@ esp_sleep_wakeup_cause_t doLightSleep(uint64_t msecToWake); extern esp_sleep_source_t wakeCause; #endif + +#ifdef HAS_PMU +#include "XPowersLibInterface.hpp" +extern XPowersLibInterface *PMU; +#endif + void setGPSPower(bool on); void doGPSpowersave(bool on); // Perform power on init that we do on each wake from deep sleep diff --git a/variants/t-watch-s3/pins_arduino.h b/variants/t-watch-s3/pins_arduino.h new file mode 100644 index 000000000..d3dde6856 --- /dev/null +++ b/variants/t-watch-s3/pins_arduino.h @@ -0,0 +1,64 @@ +#ifndef Pins_Arduino_h +#define Pins_Arduino_h + +#include + +#define EXTERNAL_NUM_INTERRUPTS 46 +#define NUM_DIGITAL_PINS 48 +#define NUM_ANALOG_INPUTS 20 + +#define analogInputToDigitalPin(p) (((p) < NUM_ANALOG_INPUTS) ? (analogChannelToDigitalPin(p)) : -1) +#define digitalPinToInterrupt(p) (((p) < NUM_DIGITAL_PINS) ? (p) : -1) +#define digitalPinHasPWM(p) (p < EXTERNAL_NUM_INTERRUPTS) + +// static const uint8_t LED_BUILTIN = -1; + +// static const uint8_t TX = 43; +// static const uint8_t RX = 44; + +static const uint8_t SDA = 10; +static const uint8_t SCL = 11; + +// Default SPI will be mapped to Radio +static const uint8_t SS = 5; +static const uint8_t MOSI = 1; +static const uint8_t MISO = 4; +static const uint8_t SCK = 3; + +static const uint8_t A0 = 1; +static const uint8_t A1 = 2; +static const uint8_t A2 = 3; +static const uint8_t A3 = 4; +static const uint8_t A4 = 5; +static const uint8_t A5 = 6; +static const uint8_t A6 = 7; +static const uint8_t A7 = 8; +static const uint8_t A8 = 9; +static const uint8_t A9 = 10; +static const uint8_t A10 = 11; +static const uint8_t A11 = 12; +static const uint8_t A12 = 13; +static const uint8_t A13 = 14; +static const uint8_t A14 = 15; +static const uint8_t A15 = 16; +static const uint8_t A16 = 17; +static const uint8_t A17 = 18; +static const uint8_t A18 = 19; +static const uint8_t A19 = 20; + +static const uint8_t T1 = 1; +static const uint8_t T2 = 2; +static const uint8_t T3 = 3; +static const uint8_t T4 = 4; +static const uint8_t T5 = 5; +static const uint8_t T6 = 6; +static const uint8_t T7 = 7; +static const uint8_t T8 = 8; +static const uint8_t T9 = 9; +static const uint8_t T10 = 10; +static const uint8_t T11 = 11; +static const uint8_t T12 = 12; +static const uint8_t T13 = 13; +static const uint8_t T14 = 14; + +#endif /* Pins_Arduino_h */ \ No newline at end of file diff --git a/variants/t-watch-s3/platformio.ini b/variants/t-watch-s3/platformio.ini new file mode 100644 index 000000000..709ea89ee --- /dev/null +++ b/variants/t-watch-s3/platformio.ini @@ -0,0 +1,17 @@ +; LilyGo T-Watch S3 +[env:t-watch-s3] +extends = esp32s3_base +board = t-watch-s3 +upload_protocol = esptool +upload_speed = 115200 +#upload_port = /dev/tty.usbmodem3485188D636C1 + +build_flags = ${esp32_base.build_flags} + -DT_WATCH_S3 + -Ivariants/t-watch-s3 + -DPCF8563_RTC=0x51 + +lib_deps = ${esp32s3_base.lib_deps} + lovyan03/LovyanGFX@^1.1.7 + lewisxhe/PCF8563_Library@1.0.1 + adafruit/Adafruit DRV2605 Library@^1.2.2 \ No newline at end of file diff --git a/variants/t-watch-s3/variant.h b/variants/t-watch-s3/variant.h new file mode 100644 index 000000000..652696c3f --- /dev/null +++ b/variants/t-watch-s3/variant.h @@ -0,0 +1,64 @@ +// ST7789 TFT LCD +#define ST7789_CS 12 +#define ST7789_RS 38 // DC +#define ST7789_SDA 13 // MOSI +#define ST7789_SCK 18 +#define ST7789_RESET -1 +#define ST7789_MISO -1 +#define ST7789_BUSY -1 +#define ST7789_BL 45 +#define ST7789_SPI_HOST SPI3_HOST +#define ST7789_BACKLIGHT_EN 45 +#define SPI_FREQUENCY 40000000 +#define SPI_READ_FREQUENCY 16000000 +#define TFT_HEIGHT 240 +#define TFT_WIDTH 240 +#define TFT_OFFSET_X 0 +#define TFT_OFFSET_Y 0 +#define SCREEN_ROTATE +#define SCREEN_TRANSITION_FRAMERATE 1 // fps +#define SCREEN_TOUCH_INT 16 +#define SCREEN_TOUCH_USE_I2C1 1 +#define TOUCH_SLAVE_ADDRESS 0x38 // GT911 + +#define I2C_SDA1 39 // Used for capacitive touch +#define I2C_SCL1 40 // Used for capacitive touch + +#define TFT_BL ST7789_BACKLIGHT_EN + +#define HAS_AXP2101 + +#define HAS_RTC 1 + +#define I2C_SDA 10 // For QMC6310 sensors and screens +#define I2C_SCL 11 // For QMC6310 sensors and screens + +#define BUTTON_PIN 0 + +#define BMA4XX_INT 14 // Interrupt for BMA_423 axis sensor + +#define HAS_GPS 0 +#undef GPS_RX_PIN +#undef GPS_TX_PIN + +#define USE_SX1262 +#define USE_SX1268 + +#define RF95_SCK 3 +#define RF95_MISO 4 +#define RF95_MOSI 1 +#define RF95_NSS 5 + +#define LORA_DIO0 -1 // a No connect on the SX1262 module +#define LORA_RESET 8 +#define LORA_DIO1 9 // SX1262 IRQ +#define LORA_DIO2 7 // SX1262 BUSY +#define LORA_DIO3 // Not connected on PCB, but internally on the TTGO SX1262, if DIO3 is high the TXCO is enabled + +#define SX126X_CS RF95_NSS // FIXME - we really should define LORA_CS instead +#define SX126X_DIO1 LORA_DIO1 +#define SX126X_BUSY LORA_DIO2 +#define SX126X_RESET LORA_RESET +#define SX126X_E22 // Not really an E22 but TTGO seems to be trying to clone that + // Internally the TTGO module hooks the SX1262-DIO2 in to control the TX/RX switch (which is the default for + // the sx1262interface code) \ No newline at end of file diff --git a/variants/tbeam-s3-core/variant.h b/variants/tbeam-s3-core/variant.h index 69f639de8..532b5b85c 100644 --- a/variants/tbeam-s3-core/variant.h +++ b/variants/tbeam-s3-core/variant.h @@ -39,6 +39,8 @@ // #define PMU_IRQ 40 #define HAS_AXP2101 +#define HAS_RTC 1 + // Specify the PMU as Wire1. In the t-beam-s3 core, PCF8563 and PMU share the bus #define PMU_USE_WIRE1 #define RTC_USE_WIRE1 @@ -59,8 +61,6 @@ // PCF8563 RTC Module // #define PCF8563_RTC 0x51 //Putting definitions in variant. h does not compile correctly -#define HAS_RTC 1 - // has 32768 Hz crystal #define HAS_32768HZ From fb21bfe0f5f01f13f187180cbc27de7c423621c1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 22 Jul 2023 09:37:51 -0500 Subject: [PATCH 35/57] [create-pull-request] automated change (#2635) Co-authored-by: thebentern --- protobufs | 2 +- src/mesh/generated/meshtastic/mesh.pb.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/protobufs b/protobufs index 64c2a11d3..f17298c2b 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 64c2a11d371cae3a2e7bb2cc86b9e6e764de7175 +Subproject commit f17298c2b093ac0d2536642b508f6cf84771b172 diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 3814875d3..6007265d5 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -707,7 +707,7 @@ typedef struct _meshtastic_ToRadio { (Sending this message is optional for clients) */ bool disconnect; meshtastic_XModem xmodemPacket; - /* MQTT Client Proxy Message */ + /* MQTT Client Proxy Message (for client / phone subscribed to MQTT sending to device) */ meshtastic_MqttClientProxyMessage mqttClientProxyMessage; }; } meshtastic_ToRadio; @@ -806,7 +806,7 @@ typedef struct _meshtastic_FromRadio { meshtastic_XModem xmodemPacket; /* Device metadata message */ meshtastic_DeviceMetadata metadata; - /* MQTT Client Proxy Message */ + /* MQTT Client Proxy Message (device sending to client / phone for publishing to MQTT) */ meshtastic_MqttClientProxyMessage mqttClientProxyMessage; }; } meshtastic_FromRadio; From 470363d294657e4a1d411e42b5ddfe5fc463e225 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sat, 22 Jul 2023 18:59:33 -0500 Subject: [PATCH 36/57] Update Hydra to use new TXEN->DIO2 macro (#2636) --- variants/diy/hydra/variant.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/variants/diy/hydra/variant.h b/variants/diy/hydra/variant.h index 93928a212..65bf839fd 100644 --- a/variants/diy/hydra/variant.h +++ b/variants/diy/hydra/variant.h @@ -35,7 +35,10 @@ #define SX126X_DIO1 LORA_DIO1 #define SX126X_BUSY LORA_DIO2 #define SX126X_RESET LORA_RESET +#define SX126X_RXEN 14 +#define SX126X_TXEN RADIOLIB_NC +#define E22_TXEN_CONNECTED_TO_DIO2 1 // Set lora.tx_power to 13 for Hydra or other E22 900M30S target due to PA #define SX126X_MAX_POWER 13 -#define SX126X_E22 \ No newline at end of file +#define SX126X_E22 From 55701692fdc78f5f4776be71185a0c1cb8e8888c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 06:43:47 -0500 Subject: [PATCH 37/57] [create-pull-request] automated change (#2637) Co-authored-by: thebentern --- version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.properties b/version.properties index 76538d46b..00ddcfc79 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 1 -build = 20 +build = 21 From b9ae63cb3cc626636956152e12e0f2ed111269dc Mon Sep 17 00:00:00 2001 From: rcarteraz Date: Mon, 24 Jul 2023 04:44:19 -0700 Subject: [PATCH 38/57] Update Bug Report.yml (#2640) Add T-Deck, T-Watch, Wireless Paper, and Wireless Tacker to device list. --- .github/ISSUE_TEMPLATE/Bug Report.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/Bug Report.yml b/.github/ISSUE_TEMPLATE/Bug Report.yml index d4e00b4ed..7fe42051c 100644 --- a/.github/ISSUE_TEMPLATE/Bug Report.yml +++ b/.github/ISSUE_TEMPLATE/Bug Report.yml @@ -37,7 +37,9 @@ body: - T-Lora v1 - T-Lora v1.3 - T-Lora v2 1.6 + - T-Deck - T-Echo + - T-Watch - Rak4631 - Rak11200 - Rak11310 @@ -45,6 +47,8 @@ body: - Heltec v2 - Heltec v2.1 - Heltec V3 + - Heltec Wireless Paper + - Heltec Wireless Tracker - Raspberry Pi Pico (W) - Relay v1 - Relay v2 From b17436a023a08bb68e76bce0ba28cf5f275bd568 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 24 Jul 2023 06:54:05 -0500 Subject: [PATCH 39/57] Patch gather-artifacts --- .github/workflows/main_matrix.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index acc07dec9..2ef174eeb 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -242,7 +242,7 @@ jobs: - name: Device scripts permissions run: | chmod +x ./output/device-install.sh - chmod +x ./output/device-update.sh + chmod +x ./output/device-update.shath - name: Zip firmware run: zip -j -9 -r ./firmware-${{ steps.version.outputs.version }}.zip ./output @@ -255,6 +255,7 @@ jobs: retention-days: 30 - name: Create request artifacts + continue-on-error: true # FIXME: Why are we getting 502, but things still work? if: ${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request' }} uses: gavv/pull-request-artifacts@v1.1.0 with: From 490abdac965a362789d4d184253996565c0151f0 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 24 Jul 2023 07:22:04 -0500 Subject: [PATCH 40/57] Whoops --- .github/workflows/main_matrix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 2ef174eeb..573728ff0 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -242,7 +242,7 @@ jobs: - name: Device scripts permissions run: | chmod +x ./output/device-install.sh - chmod +x ./output/device-update.shath + chmod +x ./output/device-update.sh - name: Zip firmware run: zip -j -9 -r ./firmware-${{ steps.version.outputs.version }}.zip ./output From ac9c81f6d136438f3e8a9fe96b46929770b588ef Mon Sep 17 00:00:00 2001 From: Jonathan Bennett Date: Mon, 24 Jul 2023 09:37:56 -0500 Subject: [PATCH 41/57] Check Position Request for Primary Channel (#2638) Prevents leaking location data to secondary channels. Co-authored-by: Ben Meadors --- src/modules/PositionModule.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/modules/PositionModule.cpp b/src/modules/PositionModule.cpp index 35457a23e..10289b837 100644 --- a/src/modules/PositionModule.cpp +++ b/src/modules/PositionModule.cpp @@ -52,11 +52,22 @@ bool PositionModule::handleReceivedProtobuf(const meshtastic_MeshPacket &mp, mes nodeDB.updatePosition(getFrom(&mp), p); + // Only respond to location requests on the channel where we broadcast location. + if (channels.getByIndex(mp.channel).role == meshtastic_Channel_Role_PRIMARY) { + ignoreRequest = false; + } else { + ignoreRequest = true; + } + return false; // Let others look at this message also if they want } meshtastic_MeshPacket *PositionModule::allocReply() { + if (ignoreRequest) { + return NULL; + } + meshtastic_NodeInfoLite *node = service.refreshLocalMeshNode(); // should guarantee there is now a position assert(node->has_position); From 3fbe2d771c9eb71f7157cac0cb21cdb09f9fb7a6 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 24 Jul 2023 09:47:16 -0500 Subject: [PATCH 42/57] Hopefully this cancels previous CI runs for a branch (#2642) --- .github/workflows/main_matrix.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 573728ff0..454ffb71a 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -1,4 +1,7 @@ name: CI +concurrency: + group: ${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} on: # # Triggers the workflow on push but only for the master branch push: From 96c6a20e038c15f1f7befce37a2951832678866c Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Mon, 24 Jul 2023 12:33:01 -0500 Subject: [PATCH 43/57] Ensure that MQTT is enabled and log initialization (#2643) --- src/mqtt/MQTT.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/mqtt/MQTT.cpp b/src/mqtt/MQTT.cpp index 50198efca..bbdb65c87 100644 --- a/src/mqtt/MQTT.cpp +++ b/src/mqtt/MQTT.cpp @@ -164,6 +164,8 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) #endif { if (moduleConfig.mqtt.enabled) { + LOG_DEBUG("Initializing MQTT\n"); + assert(!mqtt); mqtt = this; @@ -181,6 +183,14 @@ MQTT::MQTT() : concurrency::OSThread("mqtt"), mqttQueue(MAX_MQTT_QUEUE) if (!moduleConfig.mqtt.proxy_to_client_enabled) pubSub.setCallback(mqttCallback); #endif + + if (moduleConfig.mqtt.proxy_to_client_enabled) { + LOG_INFO("MQTT configured to use client proxy...\n"); + enabled = true; + runASAP = true; + reconnectCount = 0; + publishStatus(); + } // preflightSleepObserver.observe(&preflightSleep); } else { disable(); From 81edf363d72ab5522a058242684ba622b570f0f5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Jul 2023 05:46:11 -0500 Subject: [PATCH 44/57] [create-pull-request] automated change (#2645) Co-authored-by: thebentern --- protobufs | 2 +- src/mesh/generated/meshtastic/portnums.pb.h | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/protobufs b/protobufs index f17298c2b..57bd75ea8 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit f17298c2b093ac0d2536642b508f6cf84771b172 +Subproject commit 57bd75ea8b3c4fe551dcaf1dcd402646878176a8 diff --git a/src/mesh/generated/meshtastic/portnums.pb.h b/src/mesh/generated/meshtastic/portnums.pb.h index e4aaeeb96..089d7b59f 100644 --- a/src/mesh/generated/meshtastic/portnums.pb.h +++ b/src/mesh/generated/meshtastic/portnums.pb.h @@ -54,8 +54,6 @@ typedef enum _meshtastic_PortNum { /* Audio Payloads. Encapsulated codec2 packets. On 2.4 GHZ Bandwidths only for now */ meshtastic_PortNum_AUDIO_APP = 9, - /* Payloads for clients with a network connection proxying MQTT pub/sub to the device */ - meshtastic_PortNum_MQTT_CLIENT_PROXY_APP = 10, /* Provides a 'ping' service that replies to any packet it receives. Also serves as a small example module. */ meshtastic_PortNum_REPLY_APP = 32, From bdcf17a3f77bc575819d06bf4322b8e421bee9cb Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Tue, 25 Jul 2023 16:13:32 -0500 Subject: [PATCH 45/57] Add T-Deck to S3 ota logical branch (#2644) * Add T-Deck to S3 ota logical branch * Revert "Add T-Deck to S3 ota logical branch" This reverts commit d0aef9dc26d6caadadf5769d8a3535f322ad5eed. * Add targets * Get the bat file too --- bin/device-install.bat | 2 +- bin/device-install.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/device-install.bat b/bin/device-install.bat index 4d14193e5..c7d8a10cf 100755 --- a/bin/device-install.bat +++ b/bin/device-install.bat @@ -32,7 +32,7 @@ IF EXIST %FILENAME% IF x%FILENAME:update=%==x%FILENAME% ( %PYTHON% -m esptool --baud 115200 write_flash 0x00 %FILENAME% @REM Account for S3 board's different OTA partition - IF x%FILENAME:s3=%==x%FILENAME% IF x%FILENAME:v3=%==x%FILENAME% ( + IF x%FILENAME:s3=%==x%FILENAME% IF x%FILENAME:v3=%==x%FILENAME% IF x%FILENAME:t-deck=%==x%FILENAME% IF x%FILENAME:wireless-paper=%==x%FILENAME% IF x%FILENAME:wireless-tracker=%==x%FILENAME% ( %PYTHON% -m esptool --baud 115200 write_flash 0x260000 bleota.bin ) else ( %PYTHON% -m esptool --baud 115200 write_flash 0x260000 bleota-s3.bin diff --git a/bin/device-install.sh b/bin/device-install.sh index cd5d6ad59..35d99286d 100755 --- a/bin/device-install.sh +++ b/bin/device-install.sh @@ -50,7 +50,7 @@ if [ -f "${FILENAME}" ] && [ ! -z "${FILENAME##*"update"*}" ]; then "$PYTHON" -m esptool erase_flash "$PYTHON" -m esptool write_flash 0x00 ${FILENAME} # Account for S3 board's different OTA partition - if [ ! -z "${FILENAME##*"s3"*}" ] && [ ! -z "${FILENAME##*"-v3"*}" ]; then + if [ ! -z "${FILENAME##*"s3"*}" ] && [ ! -z "${FILENAME##*"-v3"*}" ] && [ ! -z "${FILENAME##*"t-deck"*}" ] && [ ! -z "${FILENAME##*"wireless-paper"*}" ] && [ ! -z "${FILENAME##*"wireless-tracker"*}" ]; then "$PYTHON" -m esptool write_flash 0x260000 bleota.bin else "$PYTHON" -m esptool write_flash 0x260000 bleota-s3.bin From 86af578df95f738490f34dafd7833a1a3d88aa30 Mon Sep 17 00:00:00 2001 From: Ben Lipsey <117498748+pdxlocations@users.noreply.github.com> Date: Wed, 26 Jul 2023 16:06:31 -0700 Subject: [PATCH 46/57] Preferred units when distance unknown (#2652) * units when distance unknown * replace deleted comment --- src/graphics/Screen.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index d1cc5ad36..b8abec66e 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -835,7 +835,11 @@ static void drawNodeInfo(OLEDDisplay *display, OLEDDisplayUiState *state, int16_ } static char distStr[20]; - strncpy(distStr, "? km", sizeof(distStr)); // might not have location data + if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) { + strncpy(distStr, "? mi", sizeof(distStr)); // might not have location data + } else { + strncpy(distStr, "? km", sizeof(distStr)); + } meshtastic_NodeInfoLite *ourNode = nodeDB.getMeshNode(nodeDB.getNodeNum()); const char *fields[] = {username, distStr, signalStr, lastStr, NULL}; int16_t compassX = 0, compassY = 0; From 0b509c7e79aa6817065b41c564a8788db4b564da Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 27 Jul 2023 06:41:39 -0500 Subject: [PATCH 47/57] Remove concurrency groups for now. They seem to cause CI hangs --- .github/workflows/main_matrix.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 454ffb71a..01535306e 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -1,7 +1,7 @@ name: CI -concurrency: - group: ${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} +#concurrency: +# group: ${{ github.ref }} +# cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} on: # # Triggers the workflow on push but only for the master branch push: From c78238037325b3230edc3727fc6f9b383b7bd3cd Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 27 Jul 2023 07:04:00 -0500 Subject: [PATCH 48/57] Fix semgrep errors --- .github/workflows/build_esp32.yml | 16 ++++++++-------- .github/workflows/build_esp32_s3.yml | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build_esp32.yml b/.github/workflows/build_esp32.yml index 7996a9b1b..8227c3c80 100644 --- a/.github/workflows/build_esp32.yml +++ b/.github/workflows/build_esp32.yml @@ -17,11 +17,11 @@ jobs: uses: ./.github/actions/setup-base - name: Pull web ui - uses: dsaltares/fetch-gh-release-asset@master + uses: dsaltares/fetch-gh-release-asset@1.1.1 with: - repo: "meshtastic/web" - file: "build.tar" - target: "build.tar" + repo: meshtastic/web + file: build.tar + target: build.tar token: ${{ secrets.GITHUB_TOKEN }} - name: Unpack web ui @@ -40,11 +40,11 @@ jobs: run: bin/build-esp32.sh ${{ inputs.board }} - name: Pull OTA Firmware - uses: dsaltares/fetch-gh-release-asset@master + uses: dsaltares/fetch-gh-release-asset@1.1.1 with: - repo: "meshtastic/firmware-ota" - file: "firmware.bin" - target: "release/bleota.bin" + repo: meshtastic/firmware-ota + file: firmware.bin + target: release/bleota.bin token: ${{ secrets.GITHUB_TOKEN }} - name: Get release version string diff --git a/.github/workflows/build_esp32_s3.yml b/.github/workflows/build_esp32_s3.yml index 7f556a991..3a47f0dd3 100644 --- a/.github/workflows/build_esp32_s3.yml +++ b/.github/workflows/build_esp32_s3.yml @@ -17,7 +17,7 @@ jobs: uses: ./.github/actions/setup-base - name: Pull web ui - uses: dsaltares/fetch-gh-release-asset@master + uses: dsaltares/fetch-gh-release-asset@1.1.1 with: repo: "meshtastic/web" file: "build.tar" @@ -38,11 +38,11 @@ jobs: run: bin/build-esp32.sh ${{ inputs.board }} - name: Pull OTA Firmware - uses: dsaltares/fetch-gh-release-asset@master + uses: dsaltares/fetch-gh-release-asset@1.1.1 with: - repo: "meshtastic/firmware-ota" - file: "firmware-s3.bin" - target: "release/bleota-s3.bin" + repo: meshtastic/firmware-ota + file: firmware-s3.bin + target: release/bleota-s3.bin token: ${{ secrets.GITHUB_TOKEN }} - name: Get release version string From 6bd870c454e2221dafc6ab5cbf67d95ac0e03705 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Thu, 27 Jul 2023 07:59:39 -0500 Subject: [PATCH 49/57] I guess we have to use SHAs (lame) --- .github/workflows/build_esp32.yml | 4 ++-- .github/workflows/build_esp32_s3.yml | 10 +++++----- .github/workflows/main_matrix.yml | 2 +- .github/workflows/nightly.yml | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build_esp32.yml b/.github/workflows/build_esp32.yml index 8227c3c80..c9664152e 100644 --- a/.github/workflows/build_esp32.yml +++ b/.github/workflows/build_esp32.yml @@ -17,7 +17,7 @@ jobs: uses: ./.github/actions/setup-base - name: Pull web ui - uses: dsaltares/fetch-gh-release-asset@1.1.1 + uses: dsaltares/fetch-gh-release-asset@a40c8b4a0471f9ab81bdf73a010f74cc51476ad4 with: repo: meshtastic/web file: build.tar @@ -40,7 +40,7 @@ jobs: run: bin/build-esp32.sh ${{ inputs.board }} - name: Pull OTA Firmware - uses: dsaltares/fetch-gh-release-asset@1.1.1 + uses: dsaltares/fetch-gh-release-asset@a40c8b4a0471f9ab81bdf73a010f74cc51476ad4 with: repo: meshtastic/firmware-ota file: firmware.bin diff --git a/.github/workflows/build_esp32_s3.yml b/.github/workflows/build_esp32_s3.yml index 3a47f0dd3..9611dd5b8 100644 --- a/.github/workflows/build_esp32_s3.yml +++ b/.github/workflows/build_esp32_s3.yml @@ -17,11 +17,11 @@ jobs: uses: ./.github/actions/setup-base - name: Pull web ui - uses: dsaltares/fetch-gh-release-asset@1.1.1 + uses: dsaltares/fetch-gh-release-asset@a40c8b4a0471f9ab81bdf73a010f74cc51476ad4 with: - repo: "meshtastic/web" - file: "build.tar" - target: "build.tar" + repo: meshtastic/web + file: build.tar + target: build.tar token: ${{ secrets.GITHUB_TOKEN }} - name: Unpack web ui @@ -38,7 +38,7 @@ jobs: run: bin/build-esp32.sh ${{ inputs.board }} - name: Pull OTA Firmware - uses: dsaltares/fetch-gh-release-asset@1.1.1 + uses: dsaltares/fetch-gh-release-asset@a40c8b4a0471f9ab81bdf73a010f74cc51476ad4 with: repo: meshtastic/firmware-ota file: firmware-s3.bin diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index 01535306e..b4a8a4739 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -49,7 +49,7 @@ jobs: - name: Trunk Check if: ${{ github.event_name != 'workflow_dispatch' }} - uses: trunk-io/trunk-action@v1 + uses: trunk-io/trunk-action@782e83f803ca6e369f035d64c6ba2768174ba61b - name: Check ${{ matrix.board }} run: bin/check-all.sh ${{ matrix.board }} diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index d9d52a2a4..da59bc0fd 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -14,6 +14,6 @@ jobs: uses: actions/checkout@v3 - name: Trunk Check - uses: trunk-io/trunk-action@v1 + uses: trunk-io/trunk-action@782e83f803ca6e369f035d64c6ba2768174ba61b with: trunk-token: ${{ secrets.TRUNK_TOKEN }} From 3d697f8cf40502fbc518c84315d739be7b06a09e Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Fri, 28 Jul 2023 10:39:40 -0500 Subject: [PATCH 50/57] Enable SX126X RX Boosted gain by default (#2663) --- src/mesh/NodeDB.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mesh/NodeDB.cpp b/src/mesh/NodeDB.cpp index 37e19eed1..45b987780 100644 --- a/src/mesh/NodeDB.cpp +++ b/src/mesh/NodeDB.cpp @@ -166,7 +166,7 @@ void NodeDB::installDefaultConfig() config.has_bluetooth = true; config.device.rebroadcast_mode = meshtastic_Config_DeviceConfig_RebroadcastMode_ALL; - config.lora.sx126x_rx_boosted_gain = false; + config.lora.sx126x_rx_boosted_gain = true; config.lora.tx_enabled = true; // FIXME: maybe false in the future, and setting region to enable it. (unset region forces it off) config.lora.override_duty_cycle = false; From ffcc1a0275e40be6deff9da22a96d4448fde89b8 Mon Sep 17 00:00:00 2001 From: GUVWAF <78759985+GUVWAF@users.noreply.github.com> Date: Sat, 29 Jul 2023 14:19:58 +0200 Subject: [PATCH 51/57] RP2040: Enable ExternalNotification and RangeTest Module, set randomSeed (#2664) * Enable ExternalNotification (and RangeTest) Module * Set a random seed at boot --- src/main.cpp | 5 +++++ src/main.h | 2 +- src/modules/Modules.cpp | 6 +++--- src/platform/rp2040/main-rp2040.cpp | 8 ++++++++ variants/rpipico/variant.h | 1 + variants/rpipicow/variant.h | 1 + 6 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 101ef51d8..8559c32f1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -458,6 +458,11 @@ void setup() #ifdef ARCH_NRF52 nrf52Setup(); #endif + +#ifdef ARCH_RP2040 + rp2040Setup(); +#endif + // We do this as early as possible because this loads preferences from flash // but we need to do this after main cpu iniot (esp32setup), because we need the random seed set nodeDB.init(); diff --git a/src/main.h b/src/main.h index b513d6478..301e8a10c 100644 --- a/src/main.h +++ b/src/main.h @@ -68,7 +68,7 @@ extern uint32_t serialSinceMsec; // This will suppress the current delay and instead try to run ASAP. extern bool runASAP; -void nrf52Setup(), esp32Setup(), nrf52Loop(), esp32Loop(), clearBonds(); +void nrf52Setup(), esp32Setup(), nrf52Loop(), esp32Loop(), rp2040Setup(), clearBonds(); meshtastic_DeviceMetadata getDeviceMetadata(); diff --git a/src/modules/Modules.cpp b/src/modules/Modules.cpp index 61de2c265..3f1fed427 100644 --- a/src/modules/Modules.cpp +++ b/src/modules/Modules.cpp @@ -24,7 +24,7 @@ #include "modules/esp32/AudioModule.h" #include "modules/esp32/StoreForwardModule.h" #endif -#if defined(ARCH_ESP32) || defined(ARCH_NRF52) +#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) #include "modules/ExternalNotificationModule.h" #include "modules/RangeTestModule.h" #if (defined(ARCH_ESP32) || defined(ARCH_NRF52)) && !defined(CONFIG_IDF_TARGET_ESP32S2) @@ -81,7 +81,7 @@ void setupModules() storeForwardModule = new StoreForwardModule(); #endif -#if defined(ARCH_ESP32) || defined(ARCH_NRF52) +#if defined(ARCH_ESP32) || defined(ARCH_NRF52) || defined(ARCH_RP2040) externalNotificationModule = new ExternalNotificationModule(); new RangeTestModule(); #endif @@ -92,4 +92,4 @@ void setupModules() // NOTE! This module must be added LAST because it likes to check for replies from other modules and avoid sending extra // acks routingModule = new RoutingModule(); -} +} \ No newline at end of file diff --git a/src/platform/rp2040/main-rp2040.cpp b/src/platform/rp2040/main-rp2040.cpp index 5e24bf03c..1d7f8fe70 100644 --- a/src/platform/rp2040/main-rp2040.cpp +++ b/src/platform/rp2040/main-rp2040.cpp @@ -27,4 +27,12 @@ void getMacAddr(uint8_t *dmac) dmac[2] = src.id[4]; dmac[1] = src.id[3]; dmac[0] = src.id[2]; +} + +void rp2040Setup() +{ + /* Sets a random seed to make sure we get different random numbers on each boot. + Taken from CPU cycle counter and ROSC oscillator, so should be pretty random. + */ + randomSeed(rp2040.hwrand32()); } \ No newline at end of file diff --git a/variants/rpipico/variant.h b/variants/rpipico/variant.h index fb4b9bd75..71d1bd159 100644 --- a/variants/rpipico/variant.h +++ b/variants/rpipico/variant.h @@ -19,6 +19,7 @@ // SDA = 4 // SCL = 5 +#define EXT_NOTIFY_OUT 22 #define BUTTON_PIN 17 #define LED_PIN PIN_LED diff --git a/variants/rpipicow/variant.h b/variants/rpipicow/variant.h index 59f8d2ec2..ac393d4d3 100644 --- a/variants/rpipicow/variant.h +++ b/variants/rpipicow/variant.h @@ -19,6 +19,7 @@ // SDA = 4 // SCL = 5 +#define EXT_NOTIFY_OUT 22 #define BUTTON_PIN 17 #define BATTERY_PIN 26 From b9c9f0f8654fcfd7bf28b9347e0808becce10d96 Mon Sep 17 00:00:00 2001 From: Neil Hao Date: Sat, 29 Jul 2023 05:54:56 -0700 Subject: [PATCH 52/57] nano-g2-ultra (#2660) * 'nano-g2-ultra' * revert overcommit * nano-g2-ultra-fmt * revert overcommit * revert overcommit --------- Co-authored-by: Ben Meadors --- boards/nano-g2-ultra.json | 51 +++++++ variants/nano-g2-ultra/platformio.ini | 18 +++ variants/nano-g2-ultra/variant.cpp | 36 +++++ variants/nano-g2-ultra/variant.h | 197 ++++++++++++++++++++++++++ 4 files changed, 302 insertions(+) create mode 100644 boards/nano-g2-ultra.json create mode 100644 variants/nano-g2-ultra/platformio.ini create mode 100644 variants/nano-g2-ultra/variant.cpp create mode 100644 variants/nano-g2-ultra/variant.h diff --git a/boards/nano-g2-ultra.json b/boards/nano-g2-ultra.json new file mode 100644 index 000000000..7afce178b --- /dev/null +++ b/boards/nano-g2-ultra.json @@ -0,0 +1,51 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DARDUINO_NRF52840_FEATHER -DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"], + ["0x239A", "0x0029"], + ["0x239A", "0x002A"], + ["0x239A", "0x802A"] + ], + "usb_product": "BQ nRF52840", + "mcu": "nrf52840", + "variant": "nano-g2-ultra", + "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", + "svd_path": "nrf52840.svd" + }, + "frameworks": ["arduino"], + "name": "BQ nRF52840", + "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": "https://wiki.uniteng.com/en/meshtastic/nano-g2-ultra", + "vendor": "BQ Consulting" +} diff --git a/variants/nano-g2-ultra/platformio.ini b/variants/nano-g2-ultra/platformio.ini new file mode 100644 index 000000000..c00a9e5ac --- /dev/null +++ b/variants/nano-g2-ultra/platformio.ini @@ -0,0 +1,18 @@ +; First prototype eink/nrf52840/sx1262 device +[env:nano-g2-ultra] +extends = nrf52840_base +board = nano-g2-ultra +debug_tool = jlink + +# add our variants files to the include and src paths +# define build flags for the TFT_eSPI library - NOTE: WE NOT LONGER USE TFT_eSPI, it was for an earlier version of the TTGO eink screens +# -DBUSY_PIN=3 -DRST_PIN=2 -DDC_PIN=28 -DCS_PIN=30 +# add -DCFG_SYSVIEW if you want to use the Segger systemview tool for OS profiling. +build_flags = ${nrf52840_base.build_flags} -Ivariants/nano-g2-ultra -D NANO_G2_ULTRA + -L "${platformio.libdeps_dir}/${this.__env__}/BSEC2 Software Library/src/cortex-m4/fpv4-sp-d16-hard" +build_src_filter = ${nrf52_base.build_src_filter} +<../variants/nano-g2-ultra> +lib_deps = + ${nrf52840_base.lib_deps} + adafruit/Adafruit BusIO@^1.13.2 + lewisxhe/PCF8563_Library@^1.0.1 +;upload_protocol = fs diff --git a/variants/nano-g2-ultra/variant.cpp b/variants/nano-g2-ultra/variant.cpp new file mode 100644 index 000000000..ce5d00886 --- /dev/null +++ b/variants/nano-g2-ultra/variant.cpp @@ -0,0 +1,36 @@ +/* + 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 "nrf.h" +#include "wiring_constants.h" +#include "wiring_digital.h" + +const uint32_t g_ADigitalPinMap[] = { + // P0 - pins 0 and 1 are hardwired for xtal and should never be enabled + 0xff, 0xff, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, + + // P1 + 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47}; + +void initVariant() +{ + // Nothing need to be inited for now +} \ No newline at end of file diff --git a/variants/nano-g2-ultra/variant.h b/variants/nano-g2-ultra/variant.h new file mode 100644 index 000000000..da46f311d --- /dev/null +++ b/variants/nano-g2-ultra/variant.h @@ -0,0 +1,197 @@ +/* + 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_Nano_G2_ +#define _VARIANT_Nano_G2_ + +/** Master clock frequency */ +#define VARIANT_MCK (64000000ul) + +#define USE_LFXO // Board uses 32khz crystal for LF +//#define USE_LFRC // Board uses 32khz RC 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) + +// LEDs +#define PIN_LED1 (-1) +#define PIN_LED2 (-1) +#define PIN_LED3 (-1) + +#define LED_RED PIN_LED3 +#define LED_BLUE PIN_LED1 +#define LED_GREEN PIN_LED2 + +#define LED_BUILTIN LED_BLUE +#define LED_CONN PIN_GREEN + +#define LED_STATE_ON 0 // State when LED is lit +//#define LED_INVERTED 1 + +/* + * Buttons + */ +#define PIN_BUTTON1 (32 + 6) + +#define EXT_NOTIFY_OUT (0 + 4) // Default pin to use for Ext Notify Module. + +/* + * Analog pins + */ +#define PIN_A4 (0 + 2) // Battery ADC + +#define BATTERY_PIN PIN_A4 + +static const uint8_t A4 = PIN_A4; + +#define ADC_RESOLUTION 14 + +/* + * Serial interfaces + */ +#define PIN_SERIAL2_RX (0 + 22) +#define PIN_SERIAL2_TX (0 + 20) + +/** + Wire Interfaces + */ +#define WIRE_INTERFACES_COUNT 1 + +#define PIN_WIRE_SDA (0 + 17) +#define PIN_WIRE_SCL (0 + 15) + +#define PIN_RTC_INT (0 + 14) // Interrupt from the PCF8563 RTC + +/* +External serial flash W25Q16JV_IQ +*/ + +// QSPI Pins +#define PIN_QSPI_SCK (0 + 8) +#define PIN_QSPI_CS (32 + 7) +#define PIN_QSPI_IO0 (0 + 6) // MOSI if using two bit interface +#define PIN_QSPI_IO1 (0 + 26) // MISO if using two bit interface +#define PIN_QSPI_IO2 (32 + 4) // WP if using two bit interface (i.e. not used) +#define PIN_QSPI_IO3 (32 + 2) // HOLD if using two bit interface (i.e. not used) + +// On-board QSPI Flash +#define EXTERNAL_FLASH_DEVICES W25Q16JV_IQ +#define EXTERNAL_FLASH_USE_QSPI + +/* + * Lora radio + */ + +#define USE_SX1262 +#define SX126X_CS (32 + 13) // FIXME - we really should define LORA_CS instead +#define SX126X_DIO1 (32 + 10) +// Note DIO2 is attached internally to the module to an analog switch for TX/RX switching +//#define SX1262_DIO3 \ + (0 + 21) // This is used as an *output* from the sx1262 and connected internally to power the tcxo, do not drive from the main +// CPU? +#define SX126X_BUSY (32 + 11) +#define SX126X_RESET (32 + 15) +#define SX126X_E22 // DIO2 controlls an antenna switch and the TCXO voltage is controlled by DIO3 + +// #define LORA_DISABLE_SENDING // Define this to disable transmission for testing (power testing etc...) + +// #undef SX126X_CS + +/* + * GPS pins + */ + +#define GPS_L76K + +#define PIN_GPS_WAKE (0 + 13) // An output to wake GPS, low means allow sleep, high means force wake +#define PIN_GPS_TX (0 + 9) // This is for bits going TOWARDS the CPU +#define PIN_GPS_RX (0 + 10) // This is for bits going TOWARDS the GPS + +//#define GPS_THREAD_INTERVAL 50 + +#define PIN_SERIAL1_RX PIN_GPS_TX +#define PIN_SERIAL1_TX PIN_GPS_RX + +// PCF8563 RTC Module +#define PCF8563_RTC 0x51 + +/* + * SPI Interfaces + */ +#define SPI_INTERFACES_COUNT 1 + +// For LORA, spi 0 +#define PIN_SPI_MISO (32 + 9) +#define PIN_SPI_MOSI (0 + 11) +#define PIN_SPI_SCK (0 + 12) + +//#define PIN_PWR_EN (0 + 6) + +// To debug via the segger JLINK console rather than the CDC-ACM serial device +// #define USE_SEGGER + +// Battery +// The battery sense is hooked to pin A0 (2) +// it is defined in the anlaolgue pin section of this file +// and has 12 bit resolution +#define BATTERY_SENSE_RESOLUTION_BITS 12 +#define BATTERY_SENSE_RESOLUTION 4096.0 +// Definition of milliVolt per LSB => 3.0V ADC range and 12-bit ADC resolution = 3000mV/4096 +#define VBAT_MV_PER_LSB (0.73242188F) +// Voltage divider value => 100K + 100K voltage divider on VBAT = (100K / (100K + 100K)) +#define VBAT_DIVIDER (0.5F) +// Compensation factor for the VBAT divider +#define VBAT_DIVIDER_COMP (2.0) +// Fixed calculation of milliVolt from compensation value +#define REAL_VBAT_MV_PER_LSB (VBAT_DIVIDER_COMP * VBAT_MV_PER_LSB) +#undef AREF_VOLTAGE +#define AREF_VOLTAGE 3.0 +#define VBAT_AR_INTERNAL AR_INTERNAL_3_0 +#define ADC_MULTIPLIER VBAT_DIVIDER_COMP +#define VBAT_RAW_TO_SCALED(x) (REAL_VBAT_MV_PER_LSB * x) + +#define HAS_RTC 1 + +/** + OLED Screen Model + */ +#define ARDUINO_ARCH_AVR +#define USE_SH1107_128_64 + +#ifdef __cplusplus +} +#endif + +/*---------------------------------------------------------------------------- + * Arduino objects - C++ only + *----------------------------------------------------------------------------*/ + +#endif \ No newline at end of file From 502a6596a329852e14762d53649cb68419ff93da Mon Sep 17 00:00:00 2001 From: Manuel <71137295+mverch67@users.noreply.github.com> Date: Sun, 30 Jul 2023 14:51:26 +0200 Subject: [PATCH 53/57] T deck: support keyboard, trackball and touchscreen (#2665) * add hwid for auto-detection * fix: heltec-wireless-tracker USB serial * T-Deck support * trunk fmt * set FRAMERATE to 1 * fix some defines * trunk fmt * corrected vendor link * T-Deck: support keyboard, trackball & touch screen * T-Watch add touchscreen defs, remove getTouch * fix warnings * getTouch uint16 -> int16 * fix touch x,y * fix I2C port * CannedMsgModule: use entire display height * trunk fmt * fix I2C issue for T-Watch * allow dest selection in canned mode * fix: allow dest selection in canned mode * use tft.setBrightness() to poweroff display * Increased t-watch framerate and added back haptic feedback * add da ref * Move to touched * improved sensitivity and accuracy of touch events * use double tap to send canned message * fix warning * trunk fmt * Remove extra hapticFeedback() --------- Co-authored-by: Ben Meadors --- src/ButtonThread.h | 17 +--- src/commands.h | 2 + src/configuration.h | 9 +- src/detect/ScanI2C.cpp | 4 +- src/detect/ScanI2C.h | 1 + src/detect/ScanI2CTwoWire.cpp | 1 + src/graphics/Screen.cpp | 49 +++++++++ src/graphics/Screen.h | 18 ++-- src/graphics/TFTDisplay.cpp | 38 ++++--- src/graphics/TFTDisplay.h | 6 +- src/input/TouchScreenBase.cpp | 137 ++++++++++++++++++++++++++ src/input/TouchScreenBase.h | 55 +++++++++++ src/input/TouchScreenImpl1.cpp | 68 +++++++++++++ src/input/TouchScreenImpl1.h | 17 ++++ src/input/TrackballInterruptBase.cpp | 78 +++++++++++++++ src/input/TrackballInterruptBase.h | 30 ++++++ src/input/TrackballInterruptImpl1.cpp | 54 ++++++++++ src/input/TrackballInterruptImpl1.h | 16 +++ src/input/UpDownInterruptBase.cpp | 2 +- src/input/cardKbI2cImpl.cpp | 2 +- src/input/kbI2cBase.cpp | 12 ++- src/main.cpp | 18 +++- src/modules/CannedMessageModule.cpp | 125 +++++++++++++++-------- src/modules/CannedMessageModule.h | 1 + src/modules/Modules.cpp | 5 + variants/t-deck/variant.h | 30 ++++-- variants/t-watch-s3/variant.h | 9 +- 27 files changed, 704 insertions(+), 100 deletions(-) create mode 100644 src/input/TouchScreenBase.cpp create mode 100644 src/input/TouchScreenBase.h create mode 100644 src/input/TouchScreenImpl1.cpp create mode 100644 src/input/TouchScreenImpl1.h create mode 100644 src/input/TrackballInterruptBase.cpp create mode 100644 src/input/TrackballInterruptBase.h create mode 100644 src/input/TrackballInterruptImpl1.cpp create mode 100644 src/input/TrackballInterruptImpl1.h diff --git a/src/ButtonThread.h b/src/ButtonThread.h index fcbb73af0..255ab5162 100644 --- a/src/ButtonThread.h +++ b/src/ButtonThread.h @@ -101,23 +101,8 @@ class ButtonThread : public concurrency::OSThread #endif // if (!canSleep) LOG_DEBUG("Suppressing sleep!\n"); // else LOG_DEBUG("sleep ok\n"); -#if defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ST7789_CS) - int x, y = 0; - screen->getTouch(&x, &y); - if (x > 0 && y > 0) { -#ifdef T_WATCH_S3 - drv.setWaveform(0, 75); - drv.setWaveform(1, 0); // end waveform - drv.go(); -#endif - LOG_DEBUG("touch %d %d\n", x, y); - powerFSM.trigger(EVENT_PRESS); - return 150; // Check for next touch every in 150ms - } -#endif - - return 5; + return 50; } private: diff --git a/src/commands.h b/src/commands.h index 7c7595143..03ede5982 100644 --- a/src/commands.h +++ b/src/commands.h @@ -15,4 +15,6 @@ enum class Cmd { PRINT, START_SHUTDOWN_SCREEN, START_REBOOT_SCREEN, + SHOW_PREV_FRAME, + SHOW_NEXT_FRAME }; \ No newline at end of file diff --git a/src/configuration.h b/src/configuration.h index 3e289ef54..aa9064251 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -98,8 +98,9 @@ along with this program. If not, see . // Define if screen should be mirrored left to right // #define SCREEN_MIRROR -// The m5stack I2C Keyboard (also RAK14004) +// I2C Keyboards (M5Stack, RAK14004, T-Deck) #define CARDKB_ADDR 0x5F +#define TDECK_KB_ADDR 0x55 // ----------------------------------------------------------------------------- // SENSOR @@ -173,6 +174,12 @@ along with this program. If not, see . #ifndef HAS_BUTTON #define HAS_BUTTON 0 #endif +#ifndef HAS_TRACKBALL +#define HAS_TRACKBALL 0 +#endif +#ifndef HAS_TOUCHSCREEN +#define HAS_TOUCHSCREEN 0 +#endif #ifndef HAS_TELEMETRY #define HAS_TELEMETRY 0 #endif diff --git a/src/detect/ScanI2C.cpp b/src/detect/ScanI2C.cpp index 75b23f419..996bdf62a 100644 --- a/src/detect/ScanI2C.cpp +++ b/src/detect/ScanI2C.cpp @@ -30,8 +30,8 @@ ScanI2C::FoundDevice ScanI2C::firstRTC() const ScanI2C::FoundDevice ScanI2C::firstKeyboard() const { - ScanI2C::DeviceType types[] = {CARDKB, RAK14004}; - return firstOfOrNONE(2, types); + ScanI2C::DeviceType types[] = {CARDKB, TDECKKB, RAK14004}; + return firstOfOrNONE(3, types); } ScanI2C::FoundDevice ScanI2C::firstAccelerometer() const diff --git a/src/detect/ScanI2C.h b/src/detect/ScanI2C.h index 559cff7ec..418a6bf5e 100644 --- a/src/detect/ScanI2C.h +++ b/src/detect/ScanI2C.h @@ -16,6 +16,7 @@ class ScanI2C RTC_RV3028, RTC_PCF8563, CARDKB, + TDECKKB, RAK14004, PMU_AXP192_AXP2101, BME_680, diff --git a/src/detect/ScanI2CTwoWire.cpp b/src/detect/ScanI2CTwoWire.cpp index 66e092951..30f9e7b7c 100644 --- a/src/detect/ScanI2CTwoWire.cpp +++ b/src/detect/ScanI2CTwoWire.cpp @@ -212,6 +212,7 @@ void ScanI2CTwoWire::scanPort(I2CPort port) } break; + SCAN_SIMPLE_CASE(TDECK_KB_ADDR, TDECKKB, "T-Deck keyboard found\n"); SCAN_SIMPLE_CASE(ST7567_ADDRESS, SCREEN_ST7567, "st7567 display found\n"); #ifdef HAS_NCP5623 SCAN_SIMPLE_CASE(NCP5623_ADDR, NCP5623, "NCP5623 RGB LED found\n"); diff --git a/src/graphics/Screen.cpp b/src/graphics/Screen.cpp index b8abec66e..fbd99d252 100644 --- a/src/graphics/Screen.cpp +++ b/src/graphics/Screen.cpp @@ -31,6 +31,7 @@ along with this program. If not, see . #include "gps/GeoCoord.h" #include "gps/RTC.h" #include "graphics/images.h" +#include "input/TouchScreenImpl1.h" #include "main.h" #include "mesh-pb-constants.h" #include "mesh/Channels.h" @@ -1044,12 +1045,18 @@ void Screen::setup() #endif serialSinceMsec = millis(); +#if HAS_TOUCHSCREEN + touchScreenImpl1 = new TouchScreenImpl1(dispdev.getWidth(), dispdev.getHeight(), dispdev.getTouch); + touchScreenImpl1->init(); +#endif + // Subscribe to status updates powerStatusObserver.observe(&powerStatus->onNewStatus); gpsStatusObserver.observe(&gpsStatus->onNewStatus); nodeStatusObserver.observe(&nodeStatus->onNewStatus); if (textMessageModule) textMessageObserver.observe(textMessageModule); + inputObserver.observe(inputBroker); // Modules can notify screen about refresh MeshModule::observeUIEvents(&uiFrameEventObserver); @@ -1127,6 +1134,12 @@ int32_t Screen::runOnce() handleOnPress(); } break; + case Cmd::SHOW_PREV_FRAME: + handleShowPrevFrame(); + break; + case Cmd::SHOW_NEXT_FRAME: + handleShowNextFrame(); + break; case Cmd::START_BLUETOOTH_PIN_SCREEN: handleStartBluetoothPinScreen(cmd.bluetooth_pin); break; @@ -1420,6 +1433,28 @@ void Screen::handleOnPress() } } +void Screen::handleShowPrevFrame() +{ + // If screen was off, just wake it, otherwise go back to previous frame + // If we are in a transition, the press must have bounced, drop it. + if (ui.getUiState()->frameState == FIXED) { + ui.previousFrame(); + lastScreenTransition = millis(); + setFastFramerate(); + } +} + +void Screen::handleShowNextFrame() +{ + // If screen was off, just wake it, otherwise advance to next frame + // If we are in a transition, the press must have bounced, drop it. + if (ui.getUiState()->frameState == FIXED) { + ui.nextFrame(); + lastScreenTransition = millis(); + setFastFramerate(); + } +} + #ifndef SCREEN_TRANSITION_FRAMERATE #define SCREEN_TRANSITION_FRAMERATE 30 // fps #endif @@ -1857,6 +1892,20 @@ int Screen::handleUIFrameEvent(const UIFrameEvent *event) return 0; } +int Screen::handleInputEvent(const InputEvent *event) +{ + if (showingNormalScreen && moduleFrames.size() == 0) { + LOG_DEBUG("Screen::handleInputEvent from %s\n", event->source); + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { + showPrevFrame(); + } else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { + showNextFrame(); + } + } + + return 0; +} + } // namespace graphics #else graphics::Screen::Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY) {} diff --git a/src/graphics/Screen.h b/src/graphics/Screen.h index 9ebe1c75a..8812b7c70 100644 --- a/src/graphics/Screen.h +++ b/src/graphics/Screen.h @@ -53,6 +53,7 @@ class Screen #include "commands.h" #include "concurrency/LockGuard.h" #include "concurrency/OSThread.h" +#include "input/InputBroker.h" #include "mesh/MeshModule.h" #include "power.h" #include @@ -118,6 +119,8 @@ class Screen : public concurrency::OSThread CallbackObserver(this, &Screen::handleTextMessage); CallbackObserver uiFrameEventObserver = CallbackObserver(this, &Screen::handleUIFrameEvent); + CallbackObserver inputObserver = + CallbackObserver(this, &Screen::handleInputEvent); public: explicit Screen(ScanI2C::DeviceAddress, meshtastic_Config_DisplayConfig_OledType, OLEDDISPLAY_GEOMETRY); @@ -152,8 +155,10 @@ class Screen : public concurrency::OSThread void blink(); - /// Handles a button press. + /// Handle button press, trackball or swipe action) void onPress() { enqueueCmd(ScreenCmd{.cmd = Cmd::ON_PRESS}); } + void showPrevFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_PREV_FRAME}); } + void showNextFrame() { enqueueCmd(ScreenCmd{.cmd = Cmd::SHOW_NEXT_FRAME}); } // Implementation to Adjust Brightness void adjustBrightness(); @@ -301,9 +306,11 @@ class Screen : public concurrency::OSThread // Use this handle to set things like battery status, user count, GPS status, etc. DebugInfo *debug_info() { return &debugInfo; } + // Handle observer events int handleStatusUpdate(const meshtastic::Status *arg); int handleTextMessage(const meshtastic_MeshPacket *arg); int handleUIFrameEvent(const UIFrameEvent *arg); + int handleInputEvent(const InputEvent *arg); /// Used to force (super slow) eink displays to draw critical frames void forceDisplay(); @@ -313,13 +320,6 @@ class Screen : public concurrency::OSThread void setWelcomeFrames(); - void getTouch(int *x, int *y) - { -#if defined(ST7735_CS) || defined(ILI9341_DRIVER) || defined(ST7789_CS) - dispdev.getTouch(x, y); -#endif - }; - protected: /// Updates the UI. // @@ -350,6 +350,8 @@ class Screen : public concurrency::OSThread // Implementations of various commands, called from doTask(). void handleSetOn(bool on); void handleOnPress(); + void handleShowNextFrame(); + void handleShowPrevFrame(); void handleStartBluetoothPinScreen(uint32_t pin); void handlePrint(const char *text); void handleStartFirmwareUpdateScreen(); diff --git a/src/graphics/TFTDisplay.cpp b/src/graphics/TFTDisplay.cpp index 1f67f90c9..4c9196b2f 100644 --- a/src/graphics/TFTDisplay.cpp +++ b/src/graphics/TFTDisplay.cpp @@ -174,7 +174,7 @@ class LGFX : public lgfx::LGFX_Device auto cfg = _light_instance.config(); // Gets a structure for backlight settings. cfg.pin_bl = ST7789_BL; // Pin number to which the backlight is connected - cfg.invert = true; // true to invert the brightness of the backlight + cfg.invert = false; // true to invert the brightness of the backlight // cfg.pwm_channel = 0; _light_instance.config(cfg); @@ -196,7 +196,7 @@ class LGFX : public lgfx::LGFX_Device // cfg.freq = 2500000; // I2C - cfg.i2c_port = 1; + cfg.i2c_port = TOUCH_I2C_PORT; cfg.i2c_addr = TOUCH_SLAVE_ADDRESS; #ifdef SCREEN_TOUCH_USE_I2C1 cfg.pin_sda = I2C_SDA1; @@ -205,7 +205,7 @@ class LGFX : public lgfx::LGFX_Device cfg.pin_sda = I2C_SDA; cfg.pin_scl = I2C_SCL; #endif - cfg.freq = 400000; + // cfg.freq = 400000; _touch_instance.config(cfg); _panel_instance.setTouch(&_touch_instance); @@ -275,6 +275,9 @@ void TFTDisplay::sendCommand(uint8_t com) #endif #ifdef VTFT_CTRL digitalWrite(VTFT_CTRL, LOW); +#endif +#ifndef M5STACK + tft.setBrightness(128); #endif break; } @@ -284,6 +287,9 @@ void TFTDisplay::sendCommand(uint8_t com) #endif #ifdef VTFT_CTRL digitalWrite(VTFT_CTRL, HIGH); +#endif +#ifndef M5STACK + tft.setBrightness(0); #endif break; } @@ -294,6 +300,24 @@ void TFTDisplay::sendCommand(uint8_t com) // Drop all other commands to device (we just update the buffer) } +bool TFTDisplay::hasTouch(void) +{ +#ifndef M5STACK + return tft.touch() != nullptr; +#else + return false; +#endif +} + +bool TFTDisplay::getTouch(int16_t *x, int16_t *y) +{ +#ifndef M5STACK + return tft.getTouch(x, y); +#else + return false; +#endif +} + void TFTDisplay::setDetected(uint8_t detected) { (void)detected; @@ -322,12 +346,4 @@ bool TFTDisplay::connect() return true; } -// Get touch coords from the display -void TFTDisplay::getTouch(int *x, int *y) -{ -#ifndef M5STACK - tft.getTouch(x, y); -#endif -} - #endif \ No newline at end of file diff --git a/src/graphics/TFTDisplay.h b/src/graphics/TFTDisplay.h index 03293d6f4..325765b1f 100644 --- a/src/graphics/TFTDisplay.h +++ b/src/graphics/TFTDisplay.h @@ -22,14 +22,16 @@ class TFTDisplay : public OLEDDisplay // Write the buffer to the display memory virtual void display(void) override; + // Touch screen (static handlers) + static bool hasTouch(void); + static bool getTouch(int16_t *x, int16_t *y); + /** * shim to make the abstraction happy * */ void setDetected(uint8_t detected); - void getTouch(int *x, int *y); - protected: // the header size of the buffer used, e.g. for the SPI command header virtual int getBufferOffset(void) override { return 0; } diff --git a/src/input/TouchScreenBase.cpp b/src/input/TouchScreenBase.cpp new file mode 100644 index 000000000..dad1bb56c --- /dev/null +++ b/src/input/TouchScreenBase.cpp @@ -0,0 +1,137 @@ +#include "TouchScreenBase.h" +#include "main.h" + +#ifndef TIME_LONG_PRESS +#define TIME_LONG_PRESS 400 +#endif + +// move a minimum distance over the screen to detect a "swipe" +#ifndef TOUCH_THRESHOLD_X +#define TOUCH_THRESHOLD_X 30 +#endif + +#ifndef TOUCH_THRESHOLD_Y +#define TOUCH_THRESHOLD_Y 20 +#endif + +TouchScreenBase::TouchScreenBase(const char *name, uint16_t width, uint16_t height) + : concurrency::OSThread(name), _display_width(width), _display_height(height), _first_x(0), _last_x(0), _first_y(0), + _last_y(0), _start(0), _tapped(false), _originName(name) +{ +} + +void TouchScreenBase::init(bool hasTouch) +{ + if (hasTouch) { + LOG_INFO("TouchScreen initialized %d %d\n", TOUCH_THRESHOLD_X, TOUCH_THRESHOLD_Y); + this->setInterval(100); + } else { + disable(); + this->setInterval(UINT_MAX); + } +} + +int32_t TouchScreenBase::runOnce() +{ + TouchEvent e; + e.touchEvent = static_cast(TOUCH_ACTION_NONE); + + // process touch events + int16_t x, y; + bool touched = getTouch(x, y); + if (touched) { + hapticFeedback(); + this->setInterval(20); + _last_x = x; + _last_y = y; + } + if (touched != _touchedOld) { + if (touched) { + _state = TOUCH_EVENT_OCCURRED; + _start = millis(); + _first_x = x; + _first_y = y; + } else { + _state = TOUCH_EVENT_CLEARED; + time_t duration = millis() - _start; + x = _last_x; + y = _last_y; + this->setInterval(50); + + // compute distance + int16_t dx = x - _first_x; + int16_t dy = y - _first_y; + uint16_t adx = abs(dx); + uint16_t ady = abs(dy); + + // swipe horizontal + if (adx > ady && adx > TOUCH_THRESHOLD_X) { + if (0 > dx) { // swipe right to left + e.touchEvent = static_cast(TOUCH_ACTION_LEFT); + LOG_DEBUG("action SWIPE: right to left\n"); + } else { // swipe left to right + e.touchEvent = static_cast(TOUCH_ACTION_RIGHT); + LOG_DEBUG("action SWIPE: left to right\n"); + } + } + // swipe vertical + else if (ady > adx && ady > TOUCH_THRESHOLD_Y) { + if (0 > dy) { // swipe bottom to top + e.touchEvent = static_cast(TOUCH_ACTION_UP); + LOG_DEBUG("action SWIPE: bottom to top\n"); + } else { // swipe top to bottom + e.touchEvent = static_cast(TOUCH_ACTION_DOWN); + LOG_DEBUG("action SWIPE: top to bottom\n"); + } + } + // tap + else { + if (duration > 0 && duration < TIME_LONG_PRESS) { + if (_tapped) { + _tapped = false; + e.touchEvent = static_cast(TOUCH_ACTION_DOUBLE_TAP); + LOG_DEBUG("action DOUBLE TAP(%d/%d)\n", x, y); + } else { + _tapped = true; + } + } else { + _tapped = false; + } + } + } + } + _touchedOld = touched; + + // fire TAP event when no 2nd tap occured within time + if (_tapped && (time_t(millis()) - _start) > TIME_LONG_PRESS - 50) { + _tapped = false; + e.touchEvent = static_cast(TOUCH_ACTION_TAP); + LOG_DEBUG("action TAP(%d/%d)\n", _last_x, _last_y); + } + + // fire LONG_PRESS event without the need for release + if (touched && (time_t(millis()) - _start) > TIME_LONG_PRESS) { + // tricky: prevent reoccurring events and another touch event when releasing + _start = millis() + 30000; + e.touchEvent = static_cast(TOUCH_ACTION_LONG_PRESS); + LOG_DEBUG("action LONG PRESS(%d/%d)\n", _last_x, _last_y); + } + + if (e.touchEvent != TOUCH_ACTION_NONE) { + e.source = this->_originName; + e.x = _last_x; + e.y = _last_y; + onEvent(e); + } + + return interval; +} + +void TouchScreenBase::hapticFeedback() +{ +#ifdef T_WATCH_S3 + drv.setWaveform(0, 75); + drv.setWaveform(1, 0); // end waveform + drv.go(); +#endif +} \ No newline at end of file diff --git a/src/input/TouchScreenBase.h b/src/input/TouchScreenBase.h new file mode 100644 index 000000000..a68c23e99 --- /dev/null +++ b/src/input/TouchScreenBase.h @@ -0,0 +1,55 @@ +#pragma once + +#include "InputBroker.h" +#include "concurrency/OSThread.h" +#include "mesh/NodeDB.h" + +typedef struct _TouchEvent { + const char *source; + char touchEvent; + uint16_t x; + uint16_t y; +} TouchEvent; + +class TouchScreenBase : public Observable, public concurrency::OSThread +{ + public: + explicit TouchScreenBase(const char *name, uint16_t width, uint16_t height); + void init(bool hasTouch); + + protected: + enum TouchScreenBaseStateType { TOUCH_EVENT_OCCURRED, TOUCH_EVENT_CLEARED }; + + enum TouchScreenBaseEventType { + TOUCH_ACTION_NONE, + TOUCH_ACTION_UP, + TOUCH_ACTION_DOWN, + TOUCH_ACTION_LEFT, + TOUCH_ACTION_RIGHT, + TOUCH_ACTION_TAP, + TOUCH_ACTION_DOUBLE_TAP, + TOUCH_ACTION_LONG_PRESS + }; + + virtual int32_t runOnce() override; + + virtual bool getTouch(int16_t &x, int16_t &y) = 0; + virtual void onEvent(const TouchEvent &event) = 0; + + volatile TouchScreenBaseStateType _state = TOUCH_EVENT_CLEARED; + volatile TouchScreenBaseEventType _action = TOUCH_ACTION_NONE; + void hapticFeedback(); + + protected: + uint16_t _display_width; + uint16_t _display_height; + + private: + bool _touchedOld = false; // previous touch state + int16_t _first_x, _last_x; // horizontal swipe direction + int16_t _first_y, _last_y; // vertical swipe direction + time_t _start; // for LONG_PRESS + bool _tapped; // for DOUBLE_TAP + + const char *_originName; +}; diff --git a/src/input/TouchScreenImpl1.cpp b/src/input/TouchScreenImpl1.cpp new file mode 100644 index 000000000..9a7ecd4a2 --- /dev/null +++ b/src/input/TouchScreenImpl1.cpp @@ -0,0 +1,68 @@ +#include "TouchScreenImpl1.h" +#include "InputBroker.h" +#include "configuration.h" + +TouchScreenImpl1 *touchScreenImpl1; + +TouchScreenImpl1::TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTouch)(int16_t *, int16_t *)) + : TouchScreenBase("touchscreen1", width, height), _getTouch(getTouch) +{ +} + +void TouchScreenImpl1::init() +{ +#if !HAS_TOUCHSCREEN + TouchScreenBase::init(false); + return; +#else + TouchScreenBase::init(true); + inputBroker->registerSource(this); +#endif +} + +bool TouchScreenImpl1::getTouch(int16_t &x, int16_t &y) +{ + return _getTouch(&x, &y); +} + +/** + * @brief forward touchscreen event + * + * @param event + * + * The touchscreen events are translated to input events and reversed + */ +void TouchScreenImpl1::onEvent(const TouchEvent &event) +{ + InputEvent e; + e.source = event.source; + switch (event.touchEvent) { + case TOUCH_ACTION_LEFT: { + e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT); + break; + } + case TOUCH_ACTION_RIGHT: { + e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT); + break; + } + case TOUCH_ACTION_UP: { + e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); + break; + } + case TOUCH_ACTION_DOWN: { + e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); + break; + } + case TOUCH_ACTION_DOUBLE_TAP: { + e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); + break; + } + case TOUCH_ACTION_LONG_PRESS: { + e.inputEvent = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_CANCEL); + break; + } + default: + return; + } + this->notifyObservers(&e); +} \ No newline at end of file diff --git a/src/input/TouchScreenImpl1.h b/src/input/TouchScreenImpl1.h new file mode 100644 index 000000000..0c5338459 --- /dev/null +++ b/src/input/TouchScreenImpl1.h @@ -0,0 +1,17 @@ +#pragma once +#include "TouchScreenBase.h" + +class TouchScreenImpl1 : public TouchScreenBase +{ + public: + TouchScreenImpl1(uint16_t width, uint16_t height, bool (*getTouch)(int16_t *, int16_t *)); + void init(void); + + protected: + virtual bool getTouch(int16_t &x, int16_t &y); + virtual void onEvent(const TouchEvent &event); + + bool (*_getTouch)(int16_t *, int16_t *); +}; + +extern TouchScreenImpl1 *touchScreenImpl1; diff --git a/src/input/TrackballInterruptBase.cpp b/src/input/TrackballInterruptBase.cpp new file mode 100644 index 000000000..649e4b362 --- /dev/null +++ b/src/input/TrackballInterruptBase.cpp @@ -0,0 +1,78 @@ +#include "TrackballInterruptBase.h" +#include "configuration.h" + +TrackballInterruptBase::TrackballInterruptBase(const char *name) +{ + this->_originName = name; +} + +void TrackballInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, + char eventDown, char eventUp, char eventLeft, char eventRight, char eventPressed, + void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), void (*onIntRight)(), + void (*onIntPress)()) +{ + this->_pinDown = pinDown; + this->_pinUp = pinUp; + this->_pinLeft = pinLeft; + this->_pinRight = pinRight; + this->_eventDown = eventDown; + this->_eventUp = eventUp; + this->_eventLeft = eventLeft; + this->_eventRight = eventRight; + this->_eventPressed = eventPressed; + + pinMode(pinPress, INPUT_PULLUP); + pinMode(this->_pinDown, INPUT_PULLUP); + pinMode(this->_pinUp, INPUT_PULLUP); + pinMode(this->_pinLeft, INPUT_PULLUP); + pinMode(this->_pinRight, INPUT_PULLUP); + + attachInterrupt(pinPress, onIntPress, RISING); + attachInterrupt(this->_pinDown, onIntDown, RISING); + attachInterrupt(this->_pinUp, onIntUp, RISING); + attachInterrupt(this->_pinLeft, onIntLeft, RISING); + attachInterrupt(this->_pinRight, onIntRight, RISING); + + LOG_DEBUG("Trackball GPIO initialized (%d, %d, %d, %d, %d)\n", this->_pinUp, this->_pinDown, this->_pinLeft, this->_pinRight, + pinPress); +} + +void TrackballInterruptBase::intPressHandler() +{ + InputEvent e; + e.source = this->_originName; + e.inputEvent = this->_eventPressed; + this->notifyObservers(&e); +} + +void TrackballInterruptBase::intDownHandler() +{ + InputEvent e; + e.source = this->_originName; + e.inputEvent = this->_eventDown; + this->notifyObservers(&e); +} + +void TrackballInterruptBase::intUpHandler() +{ + InputEvent e; + e.source = this->_originName; + e.inputEvent = this->_eventUp; + this->notifyObservers(&e); +} + +void TrackballInterruptBase::intLeftHandler() +{ + InputEvent e; + e.source = this->_originName; + e.inputEvent = this->_eventLeft; + this->notifyObservers(&e); +} + +void TrackballInterruptBase::intRightHandler() +{ + InputEvent e; + e.source = this->_originName; + e.inputEvent = this->_eventRight; + this->notifyObservers(&e); +} diff --git a/src/input/TrackballInterruptBase.h b/src/input/TrackballInterruptBase.h new file mode 100644 index 000000000..a82a20cb0 --- /dev/null +++ b/src/input/TrackballInterruptBase.h @@ -0,0 +1,30 @@ +#pragma once + +#include "InputBroker.h" +#include "mesh/NodeDB.h" + +class TrackballInterruptBase : public Observable +{ + public: + explicit TrackballInterruptBase(const char *name); + void init(uint8_t pinDown, uint8_t pinUp, uint8_t pinLeft, uint8_t pinRight, uint8_t pinPress, char eventDown, char eventUp, + char eventLeft, char eventRight, char eventPressed, void (*onIntDown)(), void (*onIntUp)(), void (*onIntLeft)(), + void (*onIntRight)(), void (*onIntPress)()); + void intPressHandler(); + void intDownHandler(); + void intUpHandler(); + void intLeftHandler(); + void intRightHandler(); + + private: + uint8_t _pinDown = 0; + uint8_t _pinUp = 0; + uint8_t _pinLeft = 0; + uint8_t _pinRight = 0; + char _eventDown = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + char _eventUp = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + char _eventLeft = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + char _eventRight = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + char _eventPressed = meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_NONE; + const char *_originName; +}; diff --git a/src/input/TrackballInterruptImpl1.cpp b/src/input/TrackballInterruptImpl1.cpp new file mode 100644 index 000000000..0a73b83b6 --- /dev/null +++ b/src/input/TrackballInterruptImpl1.cpp @@ -0,0 +1,54 @@ +#include "TrackballInterruptImpl1.h" +#include "InputBroker.h" +#include "configuration.h" + +TrackballInterruptImpl1 *trackballInterruptImpl1; + +TrackballInterruptImpl1::TrackballInterruptImpl1() : TrackballInterruptBase("trackball1") {} + +void TrackballInterruptImpl1::init() +{ +#if !HAS_TRACKBALL + // Input device is disabled. + return; +#else + uint8_t pinUp = TB_UP; + uint8_t pinDown = TB_DOWN; + uint8_t pinLeft = TB_LEFT; + uint8_t pinRight = TB_RIGHT; + uint8_t pinPress = TB_PRESS; + + char eventDown = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_DOWN); + char eventUp = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_UP); + char eventLeft = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT); + char eventRight = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT); + char eventPressed = static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_SELECT); + + TrackballInterruptBase::init(pinDown, pinUp, pinLeft, pinRight, pinPress, eventDown, eventUp, eventLeft, eventRight, + eventPressed, TrackballInterruptImpl1::handleIntDown, TrackballInterruptImpl1::handleIntUp, + TrackballInterruptImpl1::handleIntLeft, TrackballInterruptImpl1::handleIntRight, + TrackballInterruptImpl1::handleIntPressed); + inputBroker->registerSource(this); +#endif +} + +void TrackballInterruptImpl1::handleIntDown() +{ + trackballInterruptImpl1->intDownHandler(); +} +void TrackballInterruptImpl1::handleIntUp() +{ + trackballInterruptImpl1->intUpHandler(); +} +void TrackballInterruptImpl1::handleIntLeft() +{ + trackballInterruptImpl1->intLeftHandler(); +} +void TrackballInterruptImpl1::handleIntRight() +{ + trackballInterruptImpl1->intRightHandler(); +} +void TrackballInterruptImpl1::handleIntPressed() +{ + trackballInterruptImpl1->intPressHandler(); +} diff --git a/src/input/TrackballInterruptImpl1.h b/src/input/TrackballInterruptImpl1.h new file mode 100644 index 000000000..36efac6a6 --- /dev/null +++ b/src/input/TrackballInterruptImpl1.h @@ -0,0 +1,16 @@ +#pragma once +#include "TrackballInterruptBase.h" + +class TrackballInterruptImpl1 : public TrackballInterruptBase +{ + public: + TrackballInterruptImpl1(); + void init(); + static void handleIntDown(); + static void handleIntUp(); + static void handleIntLeft(); + static void handleIntRight(); + static void handleIntPressed(); +}; + +extern TrackballInterruptImpl1 *trackballInterruptImpl1; diff --git a/src/input/UpDownInterruptBase.cpp b/src/input/UpDownInterruptBase.cpp index 7c340bab0..ecc3b944a 100644 --- a/src/input/UpDownInterruptBase.cpp +++ b/src/input/UpDownInterruptBase.cpp @@ -23,7 +23,7 @@ void UpDownInterruptBase::init(uint8_t pinDown, uint8_t pinUp, uint8_t pinPress, attachInterrupt(this->_pinDown, onIntDown, RISING); attachInterrupt(this->_pinUp, onIntUp, RISING); - LOG_DEBUG("GPIO initialized (%d, %d, %d)\n", this->_pinDown, this->_pinUp, pinPress); + LOG_DEBUG("Up/down/press GPIO initialized (%d, %d, %d)\n", this->_pinUp, this->_pinDown, pinPress); } void UpDownInterruptBase::intPressHandler() diff --git a/src/input/cardKbI2cImpl.cpp b/src/input/cardKbI2cImpl.cpp index 686f4b5a2..44db1d952 100644 --- a/src/input/cardKbI2cImpl.cpp +++ b/src/input/cardKbI2cImpl.cpp @@ -7,7 +7,7 @@ CardKbI2cImpl::CardKbI2cImpl() : KbI2cBase("cardKB") {} void CardKbI2cImpl::init() { - if (cardkb_found.address != CARDKB_ADDR) { + if (cardkb_found.address != CARDKB_ADDR && cardkb_found.address != TDECK_KB_ADDR) { disable(); return; } diff --git a/src/input/kbI2cBase.cpp b/src/input/kbI2cBase.cpp index 6850eff51..cdffbaf7e 100644 --- a/src/input/kbI2cBase.cpp +++ b/src/input/kbI2cBase.cpp @@ -41,7 +41,7 @@ void write_to_14004(const TwoWire * i2cBus, uint8_t reg, uint8_t data) int32_t KbI2cBase::runOnce() { - if (cardkb_found.address != CARDKB_ADDR) { + if (cardkb_found.address != CARDKB_ADDR && cardkb_found.address != TDECK_KB_ADDR) { // Input device is not detected. return INT32_MAX; } @@ -85,9 +85,9 @@ int32_t KbI2cBase::runOnce() e.kbchar = PrintDataBuf; this->notifyObservers(&e); } - } else { - // m5 cardkb - i2cBus->requestFrom(CARDKB_ADDR, 1); + } else if (kb_model == 0x00 || kb_model == 0x10) { + // m5 cardkb and T-Deck + i2cBus->requestFrom(kb_model == 0x00 ? CARDKB_ADDR : TDECK_KB_ADDR, 1); while (i2cBus->available()) { char c = i2cBus->read(); @@ -132,6 +132,8 @@ int32_t KbI2cBase::runOnce() this->notifyObservers(&e); } } + } else { + LOG_WARN("Unknown kb_model 0x%02x\n", kb_model); } - return 500; + return 300; } diff --git a/src/main.cpp b/src/main.cpp index 8559c32f1..12ee157a5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -96,7 +96,7 @@ ScanI2C::DeviceAddress screen_found = ScanI2C::ADDRESS_NONE; // The I2C address of the cardkb or RAK14004 (if found) ScanI2C::DeviceAddress cardkb_found = ScanI2C::ADDRESS_NONE; -// 0x02 for RAK14004 and 0x00 for cardkb +// 0x02 for RAK14004, 0x00 for cardkb, 0x10 for T-Deck uint8_t kb_model; // The I2C address of the RTC Module (if found) @@ -300,6 +300,15 @@ void setup() #endif #endif +#ifdef T_DECK + // enable keyboard + pinMode(KB_POWERON, OUTPUT); + digitalWrite(KB_POWERON, HIGH); + // There needs to be a delay after power on, give LILYGO-KEYBOARD some startup time + // otherwise keyboard and touch screen will not work + delay(800); +#endif + // Currently only the tbeam has a PMU // PMU initialization needs to be placed before i2c scanning power = new Power(); @@ -372,8 +381,15 @@ void setup() kb_model = 0x02; break; case ScanI2C::DeviceType::CARDKB: + kb_model = 0x00; + break; + case ScanI2C::DeviceType::TDECKKB: + // assign an arbitrary value to distinguish from other models + kb_model = 0x10; + 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\n", kb_info.type); kb_model = 0x00; } } diff --git a/src/modules/CannedMessageModule.cpp b/src/modules/CannedMessageModule.cpp index d3a450371..b788d6a06 100644 --- a/src/modules/CannedMessageModule.cpp +++ b/src/modules/CannedMessageModule.cpp @@ -164,12 +164,21 @@ int CannedMessageModule::handleInputEvent(const InputEvent *event) (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) || (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT))) { LOG_DEBUG("Canned message event (%x)\n", event->kbchar); - if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + // tweak for left/right events generated via trackball/touch with empty kbchar + if (!event->kbchar) { + if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_LEFT)) { + this->payload = 0xb4; + this->destSelect = true; + } else if (event->inputEvent == static_cast(meshtastic_ModuleConfig_CannedMessageConfig_InputEventChar_RIGHT)) { + this->payload = 0xb7; + this->destSelect = true; + } + } else { // pass the pressed key this->payload = event->kbchar; - this->lastTouchMillis = millis(); - validEvent = true; } + this->lastTouchMillis = millis(); + validEvent = true; } if (event->inputEvent == static_cast(ANYKEY)) { LOG_DEBUG("Canned message event any key pressed\n"); @@ -225,7 +234,7 @@ int32_t CannedMessageModule::runOnce() (this->runState == CANNED_MESSAGE_RUN_STATE_INACTIVE)) { return INT32_MAX; } - LOG_DEBUG("Check status\n"); + // LOG_DEBUG("Check status\n"); UIFrameEvent e = {false, true}; if (this->runState == CANNED_MESSAGE_RUN_STATE_SENDING_ACTIVE) { // TODO: might have some feedback of sendig state @@ -300,8 +309,7 @@ int32_t CannedMessageModule::runOnce() this->runState = CANNED_MESSAGE_RUN_STATE_ACTIVE; LOG_DEBUG("MOVE DOWN (%d):%s\n", this->currentMessageIndex, this->getCurrentMessage()); } - } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { - e.frameChanged = true; + } else if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT || this->runState == CANNED_MESSAGE_RUN_STATE_ACTIVE) { switch (this->payload) { case 0xb4: // left if (this->destSelect) { @@ -347,38 +355,49 @@ int32_t CannedMessageModule::runOnce() } } break; - case 0x08: // backspace - if (this->freetext.length() > 0) { - if (this->cursor == this->freetext.length()) { - this->freetext = this->freetext.substring(0, this->freetext.length() - 1); - } else { - this->freetext = this->freetext.substring(0, this->cursor - 1) + - this->freetext.substring(this->cursor, this->freetext.length()); - } - this->cursor--; - } - break; - case 0x09: // tab - if (this->destSelect) { - this->destSelect = false; - } else { - this->destSelect = true; - } - break; default: - if (this->cursor == this->freetext.length()) { - this->freetext += this->payload; - } else { - this->freetext = - this->freetext.substring(0, this->cursor) + this->payload + this->freetext.substring(this->cursor); - } - this->cursor += 1; - if (this->freetext.length() > meshtastic_Constants_DATA_PAYLOAD_LEN) { - this->cursor = meshtastic_Constants_DATA_PAYLOAD_LEN; - this->freetext = this->freetext.substring(0, meshtastic_Constants_DATA_PAYLOAD_LEN); - } break; } + if (this->runState == CANNED_MESSAGE_RUN_STATE_FREETEXT) { + e.frameChanged = true; + switch (this->payload) { + case 0x08: // backspace + if (this->freetext.length() > 0) { + if (this->cursor == this->freetext.length()) { + this->freetext = this->freetext.substring(0, this->freetext.length() - 1); + } else { + this->freetext = this->freetext.substring(0, this->cursor - 1) + + this->freetext.substring(this->cursor, this->freetext.length()); + } + this->cursor--; + } + break; + case 0x09: // tab + if (this->destSelect) { + this->destSelect = false; + } else { + this->destSelect = true; + } + break; + case 0xb4: // left + case 0xb7: // right + // already handled above + break; + default: + if (this->cursor == this->freetext.length()) { + this->freetext += this->payload; + } else { + this->freetext = + this->freetext.substring(0, this->cursor) + this->payload + this->freetext.substring(this->cursor); + } + this->cursor += 1; + if (this->freetext.length() > meshtastic_Constants_DATA_PAYLOAD_LEN) { + this->cursor = meshtastic_Constants_DATA_PAYLOAD_LEN; + this->freetext = this->freetext.substring(0, meshtastic_Constants_DATA_PAYLOAD_LEN); + } + break; + } + } this->lastTouchMillis = millis(); this->notifyObservers(&e); @@ -406,6 +425,11 @@ const char *CannedMessageModule::getNextMessage() { return this->messages[this->getNextIndex()]; } +const char *CannedMessageModule::getMessageByIndex(int index) +{ + return (index >= 0 && index < this->messagesCount) ? this->messages[index] : ""; +} + const char *CannedMessageModule::getNodeName(NodeNum node) { if (node == NODENUM_BROADCAST) { @@ -482,12 +506,31 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st display->setTextAlignment(TEXT_ALIGN_LEFT); display->setFont(FONT_SMALL); display->drawStringf(0 + x, 0 + y, buffer, "To: %s", cannedMessageModule->getNodeName(this->dest)); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL, cannedMessageModule->getPrevMessage()); - display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), y + FONT_HEIGHT_SMALL); - display->setColor(BLACK); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, cannedMessageModule->getCurrentMessage()); - display->setColor(WHITE); - display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 3, cannedMessageModule->getNextMessage()); + int lines = (display->getHeight() / FONT_HEIGHT_SMALL) - 1; + if (lines == 3) { + // static (old) behavior for small displays + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL, cannedMessageModule->getPrevMessage()); + display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, x + display->getWidth(), y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 2, cannedMessageModule->getCurrentMessage()); + display->setColor(WHITE); + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * 3, cannedMessageModule->getNextMessage()); + } else { + // use entire display height for larger displays + int topMsg = (messagesCount > lines && currentMessageIndex >= lines - 1) ? currentMessageIndex - lines + 2 : 0; + for (int i = 0; i < std::min(messagesCount, lines); i++) { + if (i == currentMessageIndex - topMsg) { + display->fillRect(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), x + display->getWidth(), + y + FONT_HEIGHT_SMALL); + display->setColor(BLACK); + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), cannedMessageModule->getCurrentMessage()); + display->setColor(WHITE); + } else { + display->drawString(0 + x, 0 + y + FONT_HEIGHT_SMALL * (i + 1), + cannedMessageModule->getMessageByIndex(topMsg + i)); + } + } + } } } } diff --git a/src/modules/CannedMessageModule.h b/src/modules/CannedMessageModule.h index 5858a473c..4e9dadccf 100644 --- a/src/modules/CannedMessageModule.h +++ b/src/modules/CannedMessageModule.h @@ -30,6 +30,7 @@ class CannedMessageModule : public SinglePortModule, public Observableinit(); #endif +#if HAS_TRACKBALL + trackballInterruptImpl1 = new TrackballInterruptImpl1(); + trackballInterruptImpl1->init(); +#endif #if HAS_SCREEN cannedMessageModule = new CannedMessageModule(); #endif diff --git a/variants/t-deck/variant.h b/variants/t-deck/variant.h index e434cd35d..04f20fa74 100644 --- a/variants/t-deck/variant.h +++ b/variants/t-deck/variant.h @@ -16,8 +16,11 @@ #define TFT_OFFSET_X 0 #define TFT_OFFSET_Y 0 #define SCREEN_ROTATE -#define SCREEN_TRANSITION_FRAMERATE 1 // fps +#define SCREEN_TRANSITION_FRAMERATE 5 + +#define HAS_TOUCHSCREEN 1 #define SCREEN_TOUCH_INT 16 +#define TOUCH_I2C_PORT 0 #define TOUCH_SLAVE_ADDRESS 0x5D // GT911 #define BUTTON_PIN 0 @@ -43,14 +46,25 @@ // keyboard #define I2C_SDA 18 // I2C pins for this board #define I2C_SCL 8 -#define BOARD_POWERON 10 // must be set to HIGH -#define KB_SLAVE_ADDRESS 0x55 -#define KB_BL_PIN 46 // INT, set to INPUT -#define KB_UP 2 -#define KB_DOWN 3 -#define KB_LEFT 1 -#define KB_RIGHT 15 +#define KB_POWERON 10 // must be set to HIGH +#define KB_SLAVE_ADDRESS TDECK_KB_ADDR // 0x55 +#define KB_BL_PIN 46 // not used for now +// trackball +#define HAS_TRACKBALL 1 +#define TB_UP 3 +#define TB_DOWN 15 +#define TB_LEFT 1 +#define TB_RIGHT 2 +#define TB_PRESS BUTTON_PIN + +// microphone +#define ES7210_SCK 47 +#define ES7210_DIN 14 +#define ES7210_LRCK 21 +#define ES7210_MCLK 48 + +// LoRa #define USE_SX1262 #define USE_SX1268 diff --git a/variants/t-watch-s3/variant.h b/variants/t-watch-s3/variant.h index 652696c3f..8c0fc9122 100644 --- a/variants/t-watch-s3/variant.h +++ b/variants/t-watch-s3/variant.h @@ -16,10 +16,13 @@ #define TFT_OFFSET_X 0 #define TFT_OFFSET_Y 0 #define SCREEN_ROTATE -#define SCREEN_TRANSITION_FRAMERATE 1 // fps +#define SCREEN_TRANSITION_FRAMERATE 5 // fps + +#define HAS_TOUCHSCREEN 1 #define SCREEN_TOUCH_INT 16 -#define SCREEN_TOUCH_USE_I2C1 1 -#define TOUCH_SLAVE_ADDRESS 0x38 // GT911 +#define SCREEN_TOUCH_USE_I2C1 +#define TOUCH_I2C_PORT 1 +#define TOUCH_SLAVE_ADDRESS 0x38 #define I2C_SDA1 39 // Used for capacitive touch #define I2C_SCL1 40 // Used for capacitive touch From 97d7a89644dc815fdc2a352843aa50403427df1b Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 30 Jul 2023 07:58:11 -0500 Subject: [PATCH 54/57] Update protobufs --- protobufs | 2 +- src/mesh/generated/meshtastic/mesh.pb.h | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/protobufs b/protobufs index 57bd75ea8..6f88374ec 160000 --- a/protobufs +++ b/protobufs @@ -1 +1 @@ -Subproject commit 57bd75ea8b3c4fe551dcaf1dcd402646878176a8 +Subproject commit 6f88374ec6939fabc3ef79771843c291db063fae diff --git a/src/mesh/generated/meshtastic/mesh.pb.h b/src/mesh/generated/meshtastic/mesh.pb.h index 6007265d5..7fffe9ea7 100644 --- a/src/mesh/generated/meshtastic/mesh.pb.h +++ b/src/mesh/generated/meshtastic/mesh.pb.h @@ -59,6 +59,8 @@ typedef enum _meshtastic_HardwareModel { meshtastic_HardwareModel_TLORA_T3_S3 = 16, /* B&Q Consulting Nano G1 Explorer: https://wiki.uniteng.com/en/meshtastic/nano-g1-explorer */ meshtastic_HardwareModel_NANO_G1_EXPLORER = 17, + /* B&Q Consulting Nano G2 Ultra: https://wiki.uniteng.com/en/meshtastic/nano-g2-ultra */ + meshtastic_HardwareModel_NANO_G2_ULTRA = 18, /* B&Q Consulting Station Edition G1: https://uniteng.com/wiki/doku.php?id=meshtastic:station */ meshtastic_HardwareModel_STATION_G1 = 25, /* RAK11310 (RP2040 + SX1262) */ @@ -228,9 +230,9 @@ typedef enum _meshtastic_Routing_Error { to make sure that critical packets are sent ASAP. In the case of meshtastic that means we want to send protocol acks as soon as possible (to prevent unneeded retransmissions), we want routing messages to be sent next, - then messages marked as reliable and finally ‘background’ packets like periodic position updates. + then messages marked as reliable and finally 'background' packets like periodic position updates. So I bit the bullet and implemented a new (internal - not sent over the air) - field in MeshPacket called ‘priority’. + field in MeshPacket called 'priority'. And the transmission queue in the router object is now a priority queue. */ typedef enum _meshtastic_MeshPacket_Priority { /* Treated as Priority.DEFAULT */ From 76dc80518489a1f4d11e8dd0c70a33bea9bf6859 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 30 Jul 2023 14:07:17 -0500 Subject: [PATCH 55/57] Add Nano-g2-ultra --- .github/workflows/main_matrix.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main_matrix.yml b/.github/workflows/main_matrix.yml index b4a8a4739..09c0635a4 100644 --- a/.github/workflows/main_matrix.yml +++ b/.github/workflows/main_matrix.yml @@ -111,6 +111,7 @@ jobs: - board: t-echo - board: pca10059_diy_eink - board: feather_diy + - board: nano-g2-ultra uses: ./.github/workflows/build_nrf52.yml with: board: ${{ matrix.board }} From 8a49221b7fcb1388ecdc1cc5eb43305f5f875dec Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Sun, 30 Jul 2023 20:17:57 -0500 Subject: [PATCH 56/57] Update version.properties --- version.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.properties b/version.properties index 00ddcfc79..c00681305 100644 --- a/version.properties +++ b/version.properties @@ -1,4 +1,4 @@ [VERSION] major = 2 minor = 1 -build = 21 +build = 22 From ef5e21d3da5a06635180bc96a0ff655d62ff9ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 31 Jul 2023 21:37:55 +0200 Subject: [PATCH 57/57] Enable Trunk on Windows --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 3b489975b..03922dc72 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "editor.formatOnSave": true, - "editor.defaultFormatter": "trunk.io" + "editor.defaultFormatter": "trunk.io", + "trunk.enableWindows": true }