From e2522687548783bbaf4d5979170076d801766d7f Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Thu, 30 Apr 2026 20:28:19 +1000 Subject: [PATCH 01/11] initial successful build and flash --- boards/lilygo_techo_card.json | 50 +++++ src/helpers/ui/SSD1306Display.cpp | 15 +- src/helpers/ui/SSD1306Display.h | 14 +- variants/lilygo_techo_card/TechoCardBoard.cpp | 149 ++++++++++++++ variants/lilygo_techo_card/TechoCardBoard.h | 63 ++++++ variants/lilygo_techo_card/platformio.ini | 138 +++++++++++++ variants/lilygo_techo_card/target.cpp | 46 +++++ variants/lilygo_techo_card/target.h | 32 +++ variants/lilygo_techo_card/variant.cpp | 11 + variants/lilygo_techo_card/variant.h | 194 ++++++++++++++++++ 10 files changed, 707 insertions(+), 5 deletions(-) create mode 100644 boards/lilygo_techo_card.json create mode 100644 variants/lilygo_techo_card/TechoCardBoard.cpp create mode 100644 variants/lilygo_techo_card/TechoCardBoard.h create mode 100644 variants/lilygo_techo_card/platformio.ini create mode 100644 variants/lilygo_techo_card/target.cpp create mode 100644 variants/lilygo_techo_card/target.h create mode 100644 variants/lilygo_techo_card/variant.cpp create mode 100644 variants/lilygo_techo_card/variant.h diff --git a/boards/lilygo_techo_card.json b/boards/lilygo_techo_card.json new file mode 100644 index 0000000000..a57f88b455 --- /dev/null +++ b/boards/lilygo_techo_card.json @@ -0,0 +1,50 @@ +{ + "build": { + "arduino": { + "ldscript": "nrf52840_s140_v6.ld" + }, + "core": "nRF5", + "cpu": "cortex-m4", + "extra_flags": "-DNRF52840_XXAA", + "f_cpu": "64000000L", + "hwids": [ + ["0x239A", "0x8029"] + ], + "usb_product": "T-Echo Card", + "mcu": "nrf52840", + "variant": "lilygo_techo_card", + "variants_dir": "variants_bsp", + "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", + "openocd_target": "nrf52840" + }, + "frameworks": ["arduino"], + "name": "LilyGo T-Echo Card (nRF52840, SX1262, 4MB Flash)", + "upload": { + "maximum_ram_size": 248832, + "maximum_size": 815104, + "speed": 115200, + "protocol": "nrfutil", + "protocols": ["nrfutil", "jlink", "cmsis-dap"], + "native_usb": true, + "use_1200bps_touch": true, + "require_upload_port": true, + "wait_for_upload_port": true + }, + "url": "https://github.com/Xinyuan-LilyGO/T-Echo-Card", + "vendor": "LILYGO" +} \ No newline at end of file diff --git a/src/helpers/ui/SSD1306Display.cpp b/src/helpers/ui/SSD1306Display.cpp index 464b2642a0..ae4e2b84e3 100644 --- a/src/helpers/ui/SSD1306Display.cpp +++ b/src/helpers/ui/SSD1306Display.cpp @@ -14,7 +14,18 @@ bool SSD1306Display::begin() { #ifdef DISPLAY_ROTATION display.setRotation(DISPLAY_ROTATION); #endif - return display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false) && i2c_probe(Wire, DISPLAY_ADDRESS); + bool ok = display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false) && i2c_probe(Wire, DISPLAY_ADDRESS); + + // Apply vertical display offset for panels smaller than the GDDRAM + // (e.g. 72×40 panel in a 128×64 GDDRAM — visible area starts at row 24) + #ifdef OLED_DISPLAY_OFFSET + if (ok) { + display.ssd1306_command(SSD1306_SETDISPLAYOFFSET); + display.ssd1306_command(OLED_DISPLAY_OFFSET); + } + #endif + + return ok; } void SSD1306Display::turnOn() { @@ -90,4 +101,4 @@ uint16_t SSD1306Display::getTextWidth(const char* str) { void SSD1306Display::endFrame() { display.display(); -} +} \ No newline at end of file diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h index d843da85b2..3d3855d677 100644 --- a/src/helpers/ui/SSD1306Display.h +++ b/src/helpers/ui/SSD1306Display.h @@ -15,6 +15,14 @@ #define DISPLAY_ADDRESS 0x3C #endif +#ifndef OLED_WIDTH + #define OLED_WIDTH 128 +#endif + +#ifndef OLED_HEIGHT + #define OLED_HEIGHT 64 +#endif + class SSD1306Display : public DisplayDriver { Adafruit_SSD1306 display; bool _isOn; @@ -23,8 +31,8 @@ class SSD1306Display : public DisplayDriver { bool i2c_probe(TwoWire& wire, uint8_t addr); public: - SSD1306Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64), - display(128, 64, &Wire, PIN_OLED_RESET), + SSD1306Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(OLED_WIDTH, OLED_HEIGHT), + display(OLED_WIDTH, OLED_HEIGHT, &Wire, PIN_OLED_RESET), _peripher_power(peripher_power) { _isOn = false; @@ -45,4 +53,4 @@ class SSD1306Display : public DisplayDriver { void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; uint16_t getTextWidth(const char* str) override; void endFrame() override; -}; +}; \ No newline at end of file diff --git a/variants/lilygo_techo_card/TechoCardBoard.cpp b/variants/lilygo_techo_card/TechoCardBoard.cpp new file mode 100644 index 0000000000..ba95deff2b --- /dev/null +++ b/variants/lilygo_techo_card/TechoCardBoard.cpp @@ -0,0 +1,149 @@ +#include "TechoCardBoard.h" +#include "variant.h" +#include + +void TechoCardBoard::begin() { + NRF52BoardDCDC::begin(); + Serial.begin(115200); + + // RT9080 3V3 rail: clean reset cycle (from Meshtastic PR #10267) + // Toggling EN HIGH→LOW→HIGH forces a clean power-on, preventing + // brown-out when LoRa TX fires at full power. + #if PIN_OLED_EN >= 0 + pinMode(PIN_OLED_EN, OUTPUT); + digitalWrite(PIN_OLED_EN, HIGH); + delay(100); + digitalWrite(PIN_OLED_EN, LOW); + delay(100); + digitalWrite(PIN_OLED_EN, HIGH); + delay(100); + #endif + + // Park peripheral enable pins LOW before setup runs + #if defined(HAS_GPS) && PIN_GPS_EN >= 0 + pinMode(PIN_GPS_EN, OUTPUT); + digitalWrite(PIN_GPS_EN, LOW); + #endif + #if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0 + pinMode(PIN_GPS_RF_EN, OUTPUT); + digitalWrite(PIN_GPS_RF_EN, LOW); + #endif + #if defined(HAS_BUZZER) && PIN_BUZZER >= 0 + pinMode(PIN_BUZZER, OUTPUT); + digitalWrite(PIN_BUZZER, LOW); + #endif + #if defined(HAS_SPEAKER) + pinMode(PIN_SPK_EN, OUTPUT); + digitalWrite(PIN_SPK_EN, LOW); + #if PIN_SPK_EN2 >= 0 + pinMode(PIN_SPK_EN2, OUTPUT); + digitalWrite(PIN_SPK_EN2, LOW); + #endif + #endif + + // Enable GPS power after rail stabilises + #if defined(HAS_GPS) && PIN_GPS_EN >= 0 + delay(10); + digitalWrite(PIN_GPS_EN, HIGH); + #endif + #if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0 + digitalWrite(PIN_GPS_RF_EN, HIGH); + #endif + + // Initialise GPS UART + #if defined(HAS_GPS) + Serial1.setPins(PIN_GPS_RX, PIN_GPS_TX); + Serial1.begin(GPS_BAUDRATE); + #endif + + pinMode(PIN_VBAT_READ, INPUT); + pinMode(PIN_USER_BTN, INPUT); + + // Initialise I2C — must be done before display.begin() is called from main.cpp + Wire.begin(); + Wire.setClock(400000); + + // Initialise WS2812 NeoPixels (all off at boot) + #if defined(HAS_RGB_LED) + _pixel_power.begin(); + _pixel_power.clear(); + _pixel_power.show(); + + _pixel_notify.begin(); + _pixel_notify.clear(); + _pixel_notify.show(); + + _pixel_pairing.begin(); + _pixel_pairing.clear(); + _pixel_pairing.show(); + #endif +} + +void TechoCardBoard::enableGPS(bool enable) { + #if defined(HAS_GPS) && PIN_GPS_EN >= 0 + digitalWrite(PIN_GPS_EN, enable ? HIGH : LOW); + #endif + #if defined(HAS_GPS) && PIN_GPS_RF_EN >= 0 + digitalWrite(PIN_GPS_RF_EN, enable ? HIGH : LOW); + #endif +} + +void TechoCardBoard::enableSpeaker(bool enable) { + #if defined(HAS_SPEAKER) + digitalWrite(PIN_SPK_EN, enable ? HIGH : LOW); + #if PIN_SPK_EN2 >= 0 + digitalWrite(PIN_SPK_EN2, enable ? HIGH : LOW); + #endif + #endif +} + +void TechoCardBoard::setLED(uint8_t r, uint8_t g, uint8_t b) { + #if defined(HAS_RGB_LED) + uint32_t color = Adafruit_NeoPixel::Color(r, g, b); + _pixel_power.setPixelColor(0, color); + _pixel_power.show(); + _pixel_notify.setPixelColor(0, color); + _pixel_notify.show(); + _pixel_pairing.setPixelColor(0, color); + _pixel_pairing.show(); + #else + (void)r; (void)g; (void)b; + #endif +} + +void TechoCardBoard::ledOff() { + setLED(0, 0, 0); +} + +void TechoCardBoard::setStatusLED(uint8_t led_index, uint32_t color) { + #if defined(HAS_RGB_LED) + switch (led_index) { + case 0: + _pixel_power.setPixelColor(0, color); + _pixel_power.show(); + break; + case 1: + _pixel_notify.setPixelColor(0, color); + _pixel_notify.show(); + break; + case 2: + _pixel_pairing.setPixelColor(0, color); + _pixel_pairing.show(); + break; + } + #else + (void)led_index; (void)color; + #endif +} + +void TechoCardBoard::buzz(uint16_t freq_hz, uint16_t duration_ms) { + #if defined(HAS_BUZZER) && PIN_BUZZER >= 0 + if (freq_hz == 0 || duration_ms == 0) { + noTone(PIN_BUZZER); + return; + } + tone(PIN_BUZZER, freq_hz, duration_ms); + #else + (void)freq_hz; (void)duration_ms; + #endif +} \ No newline at end of file diff --git a/variants/lilygo_techo_card/TechoCardBoard.h b/variants/lilygo_techo_card/TechoCardBoard.h new file mode 100644 index 0000000000..727c4bbfde --- /dev/null +++ b/variants/lilygo_techo_card/TechoCardBoard.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include +#include "variant.h" + +#if defined(HAS_RGB_LED) + #include +#endif + +class TechoCardBoard : public NRF52BoardDCDC { +private: + #if defined(HAS_RGB_LED) + Adafruit_NeoPixel _pixel_power = Adafruit_NeoPixel(1, PIN_RGB_LED_1, NEO_GRB + NEO_KHZ800); + Adafruit_NeoPixel _pixel_notify = Adafruit_NeoPixel(1, PIN_RGB_LED_2, NEO_GRB + NEO_KHZ800); + Adafruit_NeoPixel _pixel_pairing = Adafruit_NeoPixel(1, PIN_RGB_LED_3, NEO_GRB + NEO_KHZ800); + #endif + +public: + TechoCardBoard() : NRF52Board("TECHO_CARD_OTA") {} + + void begin(); + + uint16_t getBattMilliVolts() override { + int adcvalue = 0; + analogReadResolution(12); + analogReference(AR_INTERNAL_3_0); + pinMode(PIN_BAT_CTL, OUTPUT); + pinMode(PIN_VBAT_READ, INPUT); + digitalWrite(PIN_BAT_CTL, HIGH); + + delay(10); + adcvalue = analogRead(PIN_VBAT_READ); + digitalWrite(PIN_BAT_CTL, LOW); + + return (uint16_t)((float)adcvalue * MV_LSB * ADC_MULTIPLIER); + } + + const char* getManufacturerName() const override { + return "LilyGo T-Echo Card"; + } + + void powerOff() override { + sd_power_system_off(); + } + + // GPS power control + void enableGPS(bool enable); + + // Speaker power control + void enableSpeaker(bool enable); + + // RGB LEDs — all three to same colour + void setLED(uint8_t r, uint8_t g, uint8_t b); + void ledOff(); + + // Per-LED status control (0=power, 1=notify, 2=pairing) + void setStatusLED(uint8_t led_index, uint32_t color); + + // Buzzer + void buzz(uint16_t freq_hz, uint16_t duration_ms); +}; \ No newline at end of file diff --git a/variants/lilygo_techo_card/platformio.ini b/variants/lilygo_techo_card/platformio.ini new file mode 100644 index 0000000000..42a7d73fd3 --- /dev/null +++ b/variants/lilygo_techo_card/platformio.ini @@ -0,0 +1,138 @@ +; ============================================================================= +; LilyGo T-Echo Card — nRF52840 + SX1262 + SSD1306 OLED (72x40) + L76K GPS +; ============================================================================= + +[lilygo_techo_card] +extends = nrf52_base +board = lilygo_techo_card +platform_packages = framework-arduinoadafruitnrf52 +board_build.ldscript = boards/nrf52840_s140_v6.ld +; Point FrameworkArduinoVariant at a directory containing ONLY variant.h/cpp. +; Without this, PlatformIO tries to compile TechoCardBoard.cpp and target.cpp +; as part of the framework variant, which fails because MeshCore.h and +; RadioLib.h aren't on the BSP include path. +board_build.variants_dir = variants_bsp +build_flags = ${nrf52_base.build_flags} + -I src/helpers/nrf52 + -I lib/nrf52/s140_nrf52_6.1.1_API/include + -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 + -I variants/lilygo_techo_card + -D LILYGO_TECHO_CARD + -D RADIO_CLASS=CustomSX1262 + -D WRAPPER_CLASS=CustomSX1262Wrapper + -D LORA_TX_POWER=22 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D DISPLAY_CLASS=SSD1306Display + -D PIN_BUZZER=38 + -D DISABLE_DIAGNOSTIC_OUTPUT +build_src_filter = ${nrf52_base.build_src_filter} + + + + + + + +<../variants/lilygo_techo_card> +lib_deps = + ${nrf52_base.lib_deps} + stevemarple/MicroNMEA @ ^2.0.6 + adafruit/Adafruit NeoPixel @ ^1.12.3 + adafruit/Adafruit SSD1306 @ ^2.5.12 + adafruit/Adafruit GFX Library @ ^1.11.11 + adafruit/Adafruit BusIO @ ^1.16.2 + end2endzone/NonBlockingRtttl @ ^1.3.0 + +debug_tool = jlink +upload_protocol = nrfutil + +; ============================================================================= +; Build Environments +; ============================================================================= + +; --- BLE Companion Radio --- +[env:techo_card_companion_radio_ble] +extends = lilygo_techo_card +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${lilygo_techo_card.build_flags} + -I examples/companion_radio + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=234567 + -D OFFLINE_QUEUE_SIZE=256 + -D AUTO_OFF_MILLIS=0 + -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 + -D MESH_DEBUG=1 +build_src_filter = ${lilygo_techo_card.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${lilygo_techo_card.lib_deps} + densaugeo/base64 @ ~1.4.0 + +; --- USB Companion Radio --- +[env:techo_card_companion_radio_usb] +extends = lilygo_techo_card +board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld +board_upload.maximum_size = 712704 +build_flags = + ${lilygo_techo_card.build_flags} + -I examples/companion_radio + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D AUTO_OFF_MILLIS=0 +; -D BLE_PIN_CODE=123456 +; -D BLE_DEBUG_LOGGING=1 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${lilygo_techo_card.build_src_filter} + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${lilygo_techo_card.lib_deps} + densaugeo/base64 @ ~1.4.0 + +; --- Repeater --- +[env:techo_card_repeater] +extends = lilygo_techo_card +build_flags = + ${lilygo_techo_card.build_flags} + -D ADVERT_NAME='"T-Echo Card Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${lilygo_techo_card.build_src_filter} + +<../examples/simple_repeater> + +; --- Room Server --- +[env:techo_card_room_server] +extends = lilygo_techo_card +build_flags = + ${lilygo_techo_card.build_flags} + -D ADVERT_NAME='"T-Echo Card Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +; -D MESH_PACKET_LOGGING=1 +; -D MESH_DEBUG=1 +build_src_filter = ${lilygo_techo_card.build_src_filter} + +<../examples/simple_room_server> + +; --- Sensor Node --- +[env:techo_card_sensor] +extends = lilygo_techo_card +build_flags = + ${lilygo_techo_card.build_flags} + -D ADVERT_NAME='"T-Echo Card Sensor"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 +build_src_filter = ${lilygo_techo_card.build_src_filter} + +<../examples/simple_sensor> \ No newline at end of file diff --git a/variants/lilygo_techo_card/target.cpp b/variants/lilygo_techo_card/target.cpp new file mode 100644 index 0000000000..2e3af8dc50 --- /dev/null +++ b/variants/lilygo_techo_card/target.cpp @@ -0,0 +1,46 @@ +#include +#include "target.h" +#include +#include + +TechoCardBoard board; + +RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY, SPI); + +WRAPPER_CLASS radio_driver(radio, board); + +SensorManager sensors = SensorManager(); + +VolatileRTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#ifdef DISPLAY_CLASS + DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +bool radio_init() { + // board.begin() and display.begin() are called by main.cpp before this. + // radio_init() should ONLY initialise the radio — matching Meshpocket pattern. + return radio.std_init(&SPI); +} + +uint32_t radio_get_rng_seed() { + return radio.random(0x7FFFFFFF); +} + +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr) { + radio.setFrequency(freq); + radio.setSpreadingFactor(sf); + radio.setBandwidth(bw); + radio.setCodingRate(cr); +} + +void radio_set_tx_power(int8_t dbm) { + radio.setOutputPower(dbm); +} + +mesh::LocalIdentity radio_new_identity() { + RadioNoiseListener rng(radio); + return mesh::LocalIdentity(&rng); +} \ No newline at end of file diff --git a/variants/lilygo_techo_card/target.h b/variants/lilygo_techo_card/target.h new file mode 100644 index 0000000000..70239b7461 --- /dev/null +++ b/variants/lilygo_techo_card/target.h @@ -0,0 +1,32 @@ +#pragma once + +#define RADIOLIB_STATIC_ONLY 1 +#include +#include +#include +#include +#include +#include +#include "TechoCardBoard.h" + +#ifdef DISPLAY_CLASS +#include +#include +#endif + +extern TechoCardBoard board; +extern WRAPPER_CLASS radio_driver; +extern AutoDiscoverRTCClock rtc_clock; + +#ifdef DISPLAY_CLASS + extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; +#endif + +extern SensorManager sensors; + +bool radio_init(); +uint32_t radio_get_rng_seed(); +void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); +void radio_set_tx_power(int8_t dbm); +mesh::LocalIdentity radio_new_identity(); \ No newline at end of file diff --git a/variants/lilygo_techo_card/variant.cpp b/variants/lilygo_techo_card/variant.cpp new file mode 100644 index 0000000000..3b45770d42 --- /dev/null +++ b/variants/lilygo_techo_card/variant.cpp @@ -0,0 +1,11 @@ +#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 32.768 kHz crystal (LFXO) + 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}; \ No newline at end of file diff --git a/variants/lilygo_techo_card/variant.h b/variants/lilygo_techo_card/variant.h new file mode 100644 index 0000000000..bd08bd2e03 --- /dev/null +++ b/variants/lilygo_techo_card/variant.h @@ -0,0 +1,194 @@ +/* + * variant.h — LilyGo T-Echo Card pin definitions + * + * nRF52840 + SX1262 (HPB16B3) + SSD1315 OLED (72×40) + L76K GPS + * + MAX98357 Speaker + MP34DT05 PDM Mic + ICM20948 IMU + BQ25896 + Solar + * + * Cross-referenced against: + * - LilyGo official: T-Echo-Card/libraries/private_library/t_echo_card_config.h + * - Meshtastic PR #10267 (caveman99) + */ + +#pragma once + +#include "WVariant.h" + +//////////////////////////////////////////////////////////////////////////////// +// Low frequency clock source + +#define USE_LFXO // 32.768 kHz crystal +#define VARIANT_MCK (64000000ul) + +//////////////////////////////////////////////////////////////////////////////// +// Power / Battery + +#define PIN_VBAT_READ 2 // (0, 2) = AIN0 +#define BATTERY_ADC_AIN 0 // nRF SAADC AIN channel number + +// Gated voltage divider: drive HIGH before ADC read, LOW after +#define PIN_BAT_CTL 31 // (0, 31) +#define ADC_MULTIPLIER (2.0F) + +#define MV_LSB (3000.0F / 4096.0F) + +#define ADC_RESOLUTION (14) +#define BATTERY_SENSE_RES (12) +#define AREF_VOLTAGE (3.0) + +//////////////////////////////////////////////////////////////////////////////// +// Pin counts + +#define PINS_COUNT (48) +#define NUM_DIGITAL_PINS (48) +#define NUM_ANALOG_INPUTS (1) +#define NUM_ANALOG_OUTPUTS (0) + +//////////////////////////////////////////////////////////////////////////////// +// UART — GPS (L76K) + +#define PIN_SERIAL1_RX 21 // (0, 21) — GPS TX → nRF RX +#define PIN_SERIAL1_TX 19 // (0, 19) — nRF TX → GPS RX + +//////////////////////////////////////////////////////////////////////////////// +// I2C (shared: OLED, IMU ICM20948) + +#define WIRE_INTERFACES_COUNT (1) +#define PIN_WIRE_SDA 36 // (1, 4) +#define PIN_WIRE_SCL 34 // (1, 2) + +//////////////////////////////////////////////////////////////////////////////// +// LEDs — WS2812 addressable (no plain GPIO LED) + +#define LED_BUILTIN 39 // WS2812 data 1 (1, 7) +#define PIN_LED LED_BUILTIN +#define LED_RED LED_BUILTIN +#define LED_BLUE (-1) // Prevents Bluefruit flashing during advertising +#define PIN_STATUS_LED LED_BUILTIN +#define LED_STATE_ON 1 + +// Three independent WS2812s on separate data lines +#define HAS_RGB_LED 1 +#define PIN_RGB_LED_1 39 // (1, 7) — power/charge +#define PIN_RGB_LED_2 44 // (1, 12) — notification +#define PIN_RGB_LED_3 28 // (0, 28) — BLE pairing +#define PIN_NEOPIXEL PIN_RGB_LED_1 +#define NUM_NEOPIXELS 3 + +//////////////////////////////////////////////////////////////////////////////// +// Buttons + +#define PIN_BUTTON1 42 // (1, 10) — orange front button +#define BUTTON_PIN PIN_BUTTON1 +#define PIN_USER_BTN BUTTON_PIN +// Boot button: P0.24 (hardware only, used for DFU) + +//////////////////////////////////////////////////////////////////////////////// +// SPI — LoRa + +#define SPI_INTERFACES_COUNT (1) + +#define PIN_SPI_MISO 17 // (0, 17) +#define PIN_SPI_MOSI 15 // (0, 15) +#define PIN_SPI_SCK 13 // (0, 13) + +//////////////////////////////////////////////////////////////////////////////// +// SX1262 LoRa Radio (HPB16B3 / S62F module) + +#define USE_SX1262 +#define SX126X_CS 11 // (0, 11) +#define SX126X_DIO1 40 // (1, 8) +#define SX126X_BUSY 14 // (0, 14) +#define SX126X_RESET 7 // (0, 7) +#define SX126X_DIO2_AS_RF_SWITCH true +#define SX126X_DIO3_TCXO_VOLTAGE 1.8 + +#define P_LORA_NSS SX126X_CS +#define P_LORA_DIO_1 SX126X_DIO1 +#define P_LORA_RESET SX126X_RESET +#define P_LORA_BUSY SX126X_BUSY +#define P_LORA_SCLK PIN_SPI_SCK +#define P_LORA_MISO PIN_SPI_MISO +#define P_LORA_MOSI PIN_SPI_MOSI + +// RF switch control lines (may be needed in addition to DIO2) +#define LORA_RF_VC1 27 // (0, 27) +#define LORA_RF_VC2 33 // (1, 1) + +//////////////////////////////////////////////////////////////////////////////// +// OLED Display — SSD1315 (SSD1306-compatible), 72×40, I2C +// +// Physical panel is 72×40 within 128×64 GDDRAM. +// Visible window: columns 28–99, pages 3–7 (rows 24–63). +// SETDISPLAYOFFSET = 24 maps page 0 writes to the visible area. + +#define HAS_OLED 1 +#define OLED_I2C_ADDR 0x3C +#define OLED_WIDTH 72 +#define OLED_HEIGHT 40 +#define OLED_DISPLAY_OFFSET 24 + +// RT9080 enable — controls 3V3 rail (OLED, GPS, LoRa, sensors) +#define PIN_OLED_EN 30 // (0, 30) +#define PIN_OLED_RESET (-1) + +//////////////////////////////////////////////////////////////////////////////// +// GPS — L76K Multi-GNSS + +#define HAS_GPS 1 +#define GPS_BAUDRATE 9600 +#define PIN_GPS_TX 19 // nRF TX → GPS RX +#define PIN_GPS_RX 21 // nRF RX ← GPS TX +#define PIN_GPS_EN 47 // (1, 15) +#define PIN_GPS_WAKEUP 25 // (0, 25) +#define PIN_GPS_1PPS 23 // (0, 23) +#define PIN_GPS_RF_EN 29 // (0, 29) + +//////////////////////////////////////////////////////////////////////////////// +// Speaker — MAX98357 I2S Class-D Mono Amp + +#define HAS_SPEAKER 1 +#define PIN_SPK_EN 43 // (1, 11) +#define PIN_SPK_EN2 3 // (0, 3) +#define PIN_SPK_BCLK 16 // (0, 16) +#define PIN_SPK_DATA 20 // (0, 20) +#define PIN_SPK_LRCK 22 // (0, 22) + +//////////////////////////////////////////////////////////////////////////////// +// Microphone — MP34DT05 Digital MEMS PDM + +#define HAS_MICROPHONE 1 +#define PIN_MIC_CLK 35 // (1, 3) +#define PIN_MIC_DATA 37 // (1, 5) + +//////////////////////////////////////////////////////////////////////////////// +// Buzzer + +#define HAS_BUZZER 1 +#define PIN_BUZZER 38 // (1, 6) + +//////////////////////////////////////////////////////////////////////////////// +// IMU — ICM20948 + +#define HAS_IMU 1 +#define IMU_I2C_ADDR 0x68 + +//////////////////////////////////////////////////////////////////////////////// +// NFC — nRF52840 NFC-A (dedicated P0.09/P0.10) + +#define HAS_NFC 1 + +//////////////////////////////////////////////////////////////////////////////// +// External Flash — ZD25WQ32CEIGR 4MB QSPI + +#define HAS_EXT_FLASH 1 +#define PIN_QSPI_SCK 4 // (0, 4) +#define PIN_QSPI_CS 12 // (0, 12) +#define PIN_QSPI_IO0 6 // (0, 6) +#define PIN_QSPI_IO1 8 // (0, 8) +#define PIN_QSPI_IO2 41 // (1, 9) +#define PIN_QSPI_IO3 26 // (0, 26) + +//////////////////////////////////////////////////////////////////////////////// +// No dedicated RTC chip — time from GPS or BLE companion sync + +#define HAS_RTC 0 \ No newline at end of file From 1f731b53e7016c183ee7efcb92ffab871d22e7fa Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:55:50 +1000 Subject: [PATCH 02/11] font and display & ui fixes; reverts now unused ssd1306display files to dev baseline --- examples/companion_radio/ui-new/UITask.cpp | 39 ++- src/helpers/ui/SSD1306Display.cpp | 13 +- src/helpers/ui/SSD1306Display.h | 12 +- src/helpers/ui/U8g2Display.h | 127 ++++++++ variants/lilygo_techo_card/TechoCardBoard.cpp | 46 +-- variants/lilygo_techo_card/TechoCardBoard.h | 4 +- .../lilygo_techo_card/TechoCardHomeScreen.h | 286 ++++++++++++++++++ variants/lilygo_techo_card/platformio.ini | 25 +- variants/lilygo_techo_card/target.h | 8 +- variants/lilygo_techo_card/variant.h | 14 +- 10 files changed, 497 insertions(+), 77 deletions(-) create mode 100644 src/helpers/ui/U8g2Display.h create mode 100644 variants/lilygo_techo_card/TechoCardHomeScreen.h diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 49c75a5b8e..970b688eee 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -5,6 +5,9 @@ #ifdef WIFI_SSID #include #endif +#if defined(LILYGO_TECHO_CARD) + #include "TechoCardHomeScreen.h" +#endif #ifndef AUTO_OFF_MILLIS #define AUTO_OFF_MILLIS 15000 // 15 seconds @@ -52,6 +55,15 @@ class SplashScreen : public UIScreen { } int render(DisplayDriver& display) override { +#if defined(LILYGO_TECHO_CARD) + // Text-only splash for 72×40 OLED — no room for 128px logo + display.setColor(DisplayDriver::GREEN); + display.setTextSize(1); + display.drawTextCentered(display.width()/2, 2, "MeshCore"); + display.setColor(DisplayDriver::LIGHT); + display.drawTextCentered(display.width()/2, 14, _version_info); + display.drawTextCentered(display.width()/2, 26, FIRMWARE_BUILD_DATE); +#else // meshcore logo display.setColor(DisplayDriver::BLUE); int logoWidth = 128; @@ -72,6 +84,7 @@ class SplashScreen : public UIScreen { display.setTextSize(1); display.drawTextCentered(display.width()/2, 48, FIRMWARE_BUILD_DATE); +#endif return 1000; } @@ -585,7 +598,11 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no _alert_expiry = 0; splash = new SplashScreen(this); +#if defined(LILYGO_TECHO_CARD) + home = new TechoCardHomeScreen(this, &rtc_clock, node_prefs); +#else home = new HomeScreen(this, &rtc_clock, sensors, node_prefs); +#endif msg_preview = new MsgPreviewScreen(this, &rtc_clock); setCurrScreen(splash); } @@ -747,6 +764,26 @@ void UITask::loop() { c = handleTripleClick(KEY_SELECT); } #endif +#if defined(PIN_BOOT_BTN) + // Second navigation button (C / Boot on T-Echo Card) + torch + { + static MomentaryButton boot_btn(PIN_BOOT_BTN, LONG_PRESS_MILLIS, true); + static bool _boot_btn_ready = false; + if (!_boot_btn_ready) { boot_btn.begin(); _boot_btn_ready = true; } + static bool torch_on = false; + int ev2 = boot_btn.check(); + if (ev2 == BUTTON_EVENT_CLICK && c == 0) { + c = checkDisplayOn(KEY_PREV); + } else if (ev2 == BUTTON_EVENT_DOUBLE_CLICK) { + torch_on = !torch_on; + if (torch_on) { + board.setLED(255, 255, 255); + } else { + board.ledOff(); + } + } + } +#endif #if defined(PIN_USER_BTN_ANA) if (abs(millis() - _analogue_pin_read_millis) > 10) { int ev = analog_btn.check(); @@ -928,4 +965,4 @@ void UITask::toggleBuzzer() { showAlert(buzzer.isQuiet() ? "Buzzer: OFF" : "Buzzer: ON", 800); _next_refresh = 0; // trigger refresh #endif -} +} \ No newline at end of file diff --git a/src/helpers/ui/SSD1306Display.cpp b/src/helpers/ui/SSD1306Display.cpp index ae4e2b84e3..07e000967e 100644 --- a/src/helpers/ui/SSD1306Display.cpp +++ b/src/helpers/ui/SSD1306Display.cpp @@ -14,18 +14,7 @@ bool SSD1306Display::begin() { #ifdef DISPLAY_ROTATION display.setRotation(DISPLAY_ROTATION); #endif - bool ok = display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false) && i2c_probe(Wire, DISPLAY_ADDRESS); - - // Apply vertical display offset for panels smaller than the GDDRAM - // (e.g. 72×40 panel in a 128×64 GDDRAM — visible area starts at row 24) - #ifdef OLED_DISPLAY_OFFSET - if (ok) { - display.ssd1306_command(SSD1306_SETDISPLAYOFFSET); - display.ssd1306_command(OLED_DISPLAY_OFFSET); - } - #endif - - return ok; + return display.begin(SSD1306_SWITCHCAPVCC, DISPLAY_ADDRESS, true, false) && i2c_probe(Wire, DISPLAY_ADDRESS); } void SSD1306Display::turnOn() { diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h index 3d3855d677..5995a2aeb4 100644 --- a/src/helpers/ui/SSD1306Display.h +++ b/src/helpers/ui/SSD1306Display.h @@ -15,14 +15,6 @@ #define DISPLAY_ADDRESS 0x3C #endif -#ifndef OLED_WIDTH - #define OLED_WIDTH 128 -#endif - -#ifndef OLED_HEIGHT - #define OLED_HEIGHT 64 -#endif - class SSD1306Display : public DisplayDriver { Adafruit_SSD1306 display; bool _isOn; @@ -31,8 +23,8 @@ class SSD1306Display : public DisplayDriver { bool i2c_probe(TwoWire& wire, uint8_t addr); public: - SSD1306Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(OLED_WIDTH, OLED_HEIGHT), - display(OLED_WIDTH, OLED_HEIGHT, &Wire, PIN_OLED_RESET), + SSD1306Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64), + display(128, 64, &Wire, PIN_OLED_RESET), _peripher_power(peripher_power) { _isOn = false; diff --git a/src/helpers/ui/U8g2Display.h b/src/helpers/ui/U8g2Display.h new file mode 100644 index 0000000000..5d736dc3fe --- /dev/null +++ b/src/helpers/ui/U8g2Display.h @@ -0,0 +1,127 @@ +#pragma once + +#include "DisplayDriver.h" +#include +#include + +#ifndef DISPLAY_ADDRESS + #define DISPLAY_ADDRESS 0x3C +#endif + +#ifndef OLED_WIDTH + #define OLED_WIDTH 72 +#endif + +#ifndef OLED_HEIGHT + #define OLED_HEIGHT 40 +#endif + +class U8g2Display : public DisplayDriver { + // U8g2 constructor for SSD1306/SSD1315 72×40 panel — handles all + // GDDRAM column/page offsets, SETMULTIPLEX, SETDISPLAYOFFSET internally + U8G2_SSD1306_72X40_ER_F_HW_I2C _u8g2; + bool _isOn; + uint8_t _drawColor; + + // Font metrics for current font (cached on setTextSize) + uint8_t _fontAscent; + uint8_t _fontHeight; + + void applyFont(int sz) { + if (sz >= 2) { + _u8g2.setFont(u8g2_font_5x7_mr); // 5×7 — "large" for this display + } else { + _u8g2.setFont(u8g2_font_5x7_mr); + } + _fontAscent = _u8g2.getAscent(); + _fontHeight = _u8g2.getAscent() - _u8g2.getDescent(); + } + +public: + U8g2Display() : DisplayDriver(OLED_WIDTH, OLED_HEIGHT), + _u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE), + _isOn(false), _drawColor(1), _fontAscent(5), _fontHeight(6) {} + + bool begin() { + // Wire must already be initialised by board.begin() before this is called + bool ok = _u8g2.begin(); + if (ok) { + _u8g2.setI2CAddress(DISPLAY_ADDRESS * 2); // U8g2 uses 8-bit address + _u8g2.setFontPosTop(); // y coordinate = top of text, not baseline + _u8g2.setFontMode(1); // transparent background + applyFont(1); // default to compact font + _isOn = true; + } + return ok; + } + + bool isOn() override { return _isOn; } + + void turnOn() override { + _u8g2.setPowerSave(0); + _isOn = true; + } + + void turnOff() override { + _u8g2.setPowerSave(1); + _isOn = false; + } + + void clear() override { + _u8g2.clearBuffer(); + _u8g2.sendBuffer(); + } + + void startFrame(Color bkg = DARK) override { + _u8g2.clearBuffer(); + _drawColor = 1; + _u8g2.setDrawColor(1); + applyFont(1); + } + + void setTextSize(int sz) override { + applyFont(sz); + } + + void setColor(Color c) override { + _drawColor = (c != DARK) ? 1 : 0; + _u8g2.setDrawColor(_drawColor); + } + + void setCursor(int x, int y) override { + _cursorX = x; + _cursorY = y; + } + + void print(const char* str) override { + _u8g2.setDrawColor(_drawColor); + _u8g2.drawStr(_cursorX, _cursorY, str); + } + + void fillRect(int x, int y, int w, int h) override { + _u8g2.setDrawColor(_drawColor); + _u8g2.drawBox(x, y, w, h); + } + + void drawRect(int x, int y, int w, int h) override { + _u8g2.setDrawColor(_drawColor); + _u8g2.drawFrame(x, y, w, h); + } + + void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override { + _u8g2.setDrawColor(1); + _u8g2.drawXBM(x, y, w, h, bits); + } + + uint16_t getTextWidth(const char* str) override { + return _u8g2.getStrWidth(str); + } + + void endFrame() override { + _u8g2.sendBuffer(); + } + +private: + int _cursorX = 0; + int _cursorY = 0; +}; diff --git a/variants/lilygo_techo_card/TechoCardBoard.cpp b/variants/lilygo_techo_card/TechoCardBoard.cpp index ba95deff2b..9df5cf6f41 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.cpp +++ b/variants/lilygo_techo_card/TechoCardBoard.cpp @@ -63,19 +63,15 @@ void TechoCardBoard::begin() { Wire.begin(); Wire.setClock(400000); - // Initialise WS2812 NeoPixels (all off at boot) + // Initialise WS2812 NeoPixel chain (all off at boot) + // Force data line LOW before init to prevent stray HIGH latching green #if defined(HAS_RGB_LED) - _pixel_power.begin(); - _pixel_power.clear(); - _pixel_power.show(); - - _pixel_notify.begin(); - _pixel_notify.clear(); - _pixel_notify.show(); - - _pixel_pairing.begin(); - _pixel_pairing.clear(); - _pixel_pairing.show(); + pinMode(PIN_RGB_LED_1, OUTPUT); + digitalWrite(PIN_RGB_LED_1, LOW); + delayMicroseconds(300); // WS2812 reset pulse is ~280µs + _pixels.begin(); + _pixels.clear(); + _pixels.show(); #endif } @@ -100,12 +96,10 @@ void TechoCardBoard::enableSpeaker(bool enable) { void TechoCardBoard::setLED(uint8_t r, uint8_t g, uint8_t b) { #if defined(HAS_RGB_LED) uint32_t color = Adafruit_NeoPixel::Color(r, g, b); - _pixel_power.setPixelColor(0, color); - _pixel_power.show(); - _pixel_notify.setPixelColor(0, color); - _pixel_notify.show(); - _pixel_pairing.setPixelColor(0, color); - _pixel_pairing.show(); + for (int i = 0; i < NUM_NEOPIXELS; i++) { + _pixels.setPixelColor(i, color); + } + _pixels.show(); #else (void)r; (void)g; (void)b; #endif @@ -117,19 +111,9 @@ void TechoCardBoard::ledOff() { void TechoCardBoard::setStatusLED(uint8_t led_index, uint32_t color) { #if defined(HAS_RGB_LED) - switch (led_index) { - case 0: - _pixel_power.setPixelColor(0, color); - _pixel_power.show(); - break; - case 1: - _pixel_notify.setPixelColor(0, color); - _pixel_notify.show(); - break; - case 2: - _pixel_pairing.setPixelColor(0, color); - _pixel_pairing.show(); - break; + if (led_index < NUM_NEOPIXELS) { + _pixels.setPixelColor(led_index, color); + _pixels.show(); } #else (void)led_index; (void)color; diff --git a/variants/lilygo_techo_card/TechoCardBoard.h b/variants/lilygo_techo_card/TechoCardBoard.h index 727c4bbfde..134da85756 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.h +++ b/variants/lilygo_techo_card/TechoCardBoard.h @@ -12,9 +12,7 @@ class TechoCardBoard : public NRF52BoardDCDC { private: #if defined(HAS_RGB_LED) - Adafruit_NeoPixel _pixel_power = Adafruit_NeoPixel(1, PIN_RGB_LED_1, NEO_GRB + NEO_KHZ800); - Adafruit_NeoPixel _pixel_notify = Adafruit_NeoPixel(1, PIN_RGB_LED_2, NEO_GRB + NEO_KHZ800); - Adafruit_NeoPixel _pixel_pairing = Adafruit_NeoPixel(1, PIN_RGB_LED_3, NEO_GRB + NEO_KHZ800); + Adafruit_NeoPixel _pixels = Adafruit_NeoPixel(NUM_NEOPIXELS, PIN_RGB_LED_1, NEO_GRB + NEO_KHZ800); #endif public: diff --git a/variants/lilygo_techo_card/TechoCardHomeScreen.h b/variants/lilygo_techo_card/TechoCardHomeScreen.h new file mode 100644 index 0000000000..8466da8b38 --- /dev/null +++ b/variants/lilygo_techo_card/TechoCardHomeScreen.h @@ -0,0 +1,286 @@ +// ============================================================================= +// TechoCardHomeScreen — 72×40 OLED home screen for LilyGo T-Echo Card +// +// Four-line layout using U8g2's 4×6 tom_thumb font (18 chars × 4 lines). +// U8g2's native SSD1306_72X40_ER support handles all GDDRAM offset mapping. +// +// Two-button navigation: A (pin 42) = next page / long-press activate +// C (pin 24) = previous page +// +// Pages: STATUS → RADIO → BLE → ADVERT → GPS → HIBERNATE +// ============================================================================= +#pragma once + +#include +#include +#include +#include +#include "MyMesh.h" +#include "UITask.h" + +class TechoCardHomeScreen : public UIScreen { + enum Page { + STATUS, + RADIO, +#ifdef BLE_PIN_CODE + BLE, +#endif + ADVERT, +#if ENV_INCLUDE_GPS == 1 + GPS, +#endif + HIBERNATE, + PAGE_COUNT + }; + + UITask* _task; + mesh::RTCClock* _rtc; + NodePrefs* _prefs; + uint8_t _page; + bool _shutdown_init; + unsigned long _shutdown_at; + + // Four lines at 9px spacing within 40px display. + // U8g2 handles panel offset natively — y=0 is the true visible top. + static const int Y0 = 2; + static const int Y1 = 11; + static const int Y2 = 20; + static const int Y3 = 29; + + int battPercent() { + uint16_t mv = _task->getBattMilliVolts(); + if (mv == 0) return 0; + int pct = ((int)mv - 3000) * 100 / 1200; + if (pct < 0) pct = 0; + if (pct > 100) pct = 100; + return pct; + } + +public: + TechoCardHomeScreen(UITask* task, mesh::RTCClock* rtc, NodePrefs* prefs) + : _task(task), _rtc(rtc), _prefs(prefs), + _page(STATUS), _shutdown_init(false), _shutdown_at(0) {} + + void cancelEditing() { _shutdown_init = false; } + + int render(DisplayDriver& display) override { + char tmp[32]; + display.setTextSize(1); + + switch (_page) { + + // ----- STATUS ----- + case STATUS: { + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, Y0); + char filtered_name[sizeof(_prefs->node_name)]; + display.translateUTF8ToBlocks(filtered_name, _prefs->node_name, + sizeof(filtered_name)); + display.print(filtered_name); + + snprintf(tmp, sizeof(tmp), "%d%%", battPercent()); + display.drawTextRightAlign(display.width() - 1, Y0, tmp); + + display.setColor(DisplayDriver::YELLOW); + display.setCursor(0, Y1); + snprintf(tmp, sizeof(tmp), "MSG: %d", _task->getMsgCount()); + display.print(tmp); + + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, Y2); + if (_task->hasConnection()) { + display.print("Connected"); + } else if (_task->isSerialEnabled()) { + display.print("BLE: On"); + } else { + display.print("BLE: Off"); + } + break; + } + + // ----- RADIO ----- + case RADIO: { + display.setColor(DisplayDriver::YELLOW); + display.setCursor(0, Y0); + snprintf(tmp, sizeof(tmp), "%.1f MHz SF%d", + _prefs->freq, _prefs->sf); + display.print(tmp); + + display.setCursor(0, Y1); + snprintf(tmp, sizeof(tmp), "BW %.0f CR %d", + _prefs->bw, _prefs->cr); + display.print(tmp); + + display.setCursor(0, Y2); + snprintf(tmp, sizeof(tmp), "TX: %d dBm", + _prefs->tx_power_dbm); + display.print(tmp); + + display.setCursor(0, Y3); + snprintf(tmp, sizeof(tmp), "Noise floor: %d", + radio_driver.getNoiseFloor()); + display.print(tmp); + break; + } + +#ifdef BLE_PIN_CODE + // ----- BLE ----- + case BLE: { + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, Y0); + display.print(_task->isSerialEnabled() ? "BLE: ON" : "BLE: OFF"); + + display.setColor(DisplayDriver::YELLOW); + display.setCursor(0, Y1); + snprintf(tmp, sizeof(tmp), "PIN: %lu", + (unsigned long)the_mesh.getBLEPin()); + display.print(tmp); + + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, Y3); + display.print("Hold A: toggle"); + break; + } +#endif + + // ----- ADVERT ----- + case ADVERT: { + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, Y0); + display.print("Advert"); + + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, Y2); + display.print("Hold A: send"); + break; + } + +#if ENV_INCLUDE_GPS == 1 + // ----- GPS ----- + case GPS: { + LocationProvider* loc = sensors.getLocationProvider(); + + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, Y0); + if (!_prefs->gps_enabled) { + display.print("GPS: OFF"); + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, Y2); + display.print("Hold A: toggle"); + break; + } + + display.print("GPS: ON"); + if (loc) { + snprintf(tmp, sizeof(tmp), "Sats: %d", + loc->satellitesCount()); + display.drawTextRightAlign(display.width() - 1, Y0, tmp); + + display.setColor(DisplayDriver::YELLOW); + display.setCursor(0, Y1); + display.print(loc->isValid() ? "Fix: 3D" : "No fix"); + + if (loc->isValid()) { + display.setCursor(0, Y2); + snprintf(tmp, sizeof(tmp), "%.4f, %.4f", + loc->getLatitude() / 1000000.0, + loc->getLongitude() / 1000000.0); + display.print(tmp); + } + } + + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, Y3); + display.print("Hold A: toggle"); + break; + } +#endif + + // ----- HIBERNATE ----- + case HIBERNATE: { + if (_shutdown_init) { + display.setColor(DisplayDriver::RED); + display.setCursor(0, Y1); + display.print("Shutting down..."); + return 200; + } + + display.setColor(DisplayDriver::YELLOW); + display.setCursor(0, Y0); + display.print("Hibernate"); + + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, Y2); + display.print("Hold A: sleep"); + break; + } + } // switch + + return 5000; + } + + bool handleInput(char c) override { + if (_shutdown_init) { + _shutdown_init = false; + return true; + } + + if (c == KEY_NEXT || c == 'd') { + _page = (_page + 1) % PAGE_COUNT; + return true; + } + if (c == KEY_PREV || c == 'a') { + _page = (_page + PAGE_COUNT - 1) % PAGE_COUNT; + return true; + } + + if (c == KEY_ENTER) { + switch (_page) { +#ifdef BLE_PIN_CODE + case BLE: + if (_task->isSerialEnabled()) { + _task->disableSerial(); + _task->showAlert("BLE Off", 800); + } else { + _task->enableSerial(); + _task->showAlert("BLE On", 800); + } + return true; +#endif + + case ADVERT: + _task->notify(UIEventType::ack); + if (the_mesh.advert()) { + _task->showAlert("Sent!", 800); + } else { + _task->showAlert("Failed", 800); + } + return true; + +#if ENV_INCLUDE_GPS == 1 + case GPS: + _task->toggleGPS(); + return true; +#endif + + case HIBERNATE: + _shutdown_init = true; + _shutdown_at = millis() + 500; + return true; + + default: + return false; + } + } + + return false; + } + + void poll() override { + if (_shutdown_init && millis() >= _shutdown_at) { + if (!_task->isButtonPressed()) { + _task->shutdown(); + } + } + } +}; \ No newline at end of file diff --git a/variants/lilygo_techo_card/platformio.ini b/variants/lilygo_techo_card/platformio.ini index 42a7d73fd3..13df5a8ed5 100644 --- a/variants/lilygo_techo_card/platformio.ini +++ b/variants/lilygo_techo_card/platformio.ini @@ -23,12 +23,14 @@ build_flags = ${nrf52_base.build_flags} -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 - -D DISPLAY_CLASS=SSD1306Display + -D USE_U8G2_DISPLAY + -D DISPLAY_CLASS=U8g2Display -D PIN_BUZZER=38 + -D PIN_BOOT_BTN=24 + -D ENV_INCLUDE_GPS=1 -D DISABLE_DIAGNOSTIC_OUTPUT build_src_filter = ${nrf52_base.build_src_filter} + - + + +<../variants/lilygo_techo_card> lib_deps = @@ -39,15 +41,12 @@ lib_deps = adafruit/Adafruit GFX Library @ ^1.11.11 adafruit/Adafruit BusIO @ ^1.16.2 end2endzone/NonBlockingRtttl @ ^1.3.0 + olikraus/U8g2 @ ^2.35.19 debug_tool = jlink upload_protocol = nrfutil -; ============================================================================= -; Build Environments -; ============================================================================= -; --- BLE Companion Radio --- [env:techo_card_companion_radio_ble] extends = lilygo_techo_card board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld @@ -58,12 +57,12 @@ build_flags = -I examples/companion_radio/ui-new -D MAX_CONTACTS=350 -D MAX_GROUP_CHANNELS=40 - -D BLE_PIN_CODE=234567 + -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=256 -D AUTO_OFF_MILLIS=0 - -D BLE_DEBUG_LOGGING=1 +; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 +; -D MESH_DEBUG=1 build_src_filter = ${lilygo_techo_card.build_src_filter} + +<../examples/companion_radio/*.cpp> @@ -72,7 +71,7 @@ lib_deps = ${lilygo_techo_card.lib_deps} densaugeo/base64 @ ~1.4.0 -; --- USB Companion Radio --- + [env:techo_card_companion_radio_usb] extends = lilygo_techo_card board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld @@ -96,7 +95,7 @@ lib_deps = ${lilygo_techo_card.lib_deps} densaugeo/base64 @ ~1.4.0 -; --- Repeater --- + [env:techo_card_repeater] extends = lilygo_techo_card build_flags = @@ -111,7 +110,7 @@ build_flags = build_src_filter = ${lilygo_techo_card.build_src_filter} +<../examples/simple_repeater> -; --- Room Server --- + [env:techo_card_room_server] extends = lilygo_techo_card build_flags = @@ -126,7 +125,7 @@ build_flags = build_src_filter = ${lilygo_techo_card.build_src_filter} +<../examples/simple_room_server> -; --- Sensor Node --- + [env:techo_card_sensor] extends = lilygo_techo_card build_flags = diff --git a/variants/lilygo_techo_card/target.h b/variants/lilygo_techo_card/target.h index 70239b7461..29f8b51fea 100644 --- a/variants/lilygo_techo_card/target.h +++ b/variants/lilygo_techo_card/target.h @@ -10,8 +10,12 @@ #include "TechoCardBoard.h" #ifdef DISPLAY_CLASS -#include -#include + #if defined(USE_U8G2_DISPLAY) + #include + #else + #include + #endif + #include #endif extern TechoCardBoard board; diff --git a/variants/lilygo_techo_card/variant.h b/variants/lilygo_techo_card/variant.h index bd08bd2e03..feacfe6894 100644 --- a/variants/lilygo_techo_card/variant.h +++ b/variants/lilygo_techo_card/variant.h @@ -58,19 +58,23 @@ //////////////////////////////////////////////////////////////////////////////// // LEDs — WS2812 addressable (no plain GPIO LED) +// The BSP drives LED_BUILTIN via digitalWrite for BLE status — if pointed at +// the WS2812 data pin (39), it holds the line HIGH and all LEDs glow green. +// Point at an unused GPIO (46 = P1.14) so the BSP toggles harmlessly. -#define LED_BUILTIN 39 // WS2812 data 1 (1, 7) +#define LED_BUILTIN 46 // Unused GPIO — keeps BSP happy #define PIN_LED LED_BUILTIN #define LED_RED LED_BUILTIN #define LED_BLUE (-1) // Prevents Bluefruit flashing during advertising #define PIN_STATUS_LED LED_BUILTIN #define LED_STATE_ON 1 -// Three independent WS2812s on separate data lines +// WS2812 RGB LEDs — 3 LEDs daisy-chained on a single data line (pin 39) +// Hardware verified: all three light when pin 39 is driven HIGH. +// Meshtastic PR #10267 mapped them as separate GPIOs (39, 44, 28) but +// testing confirms they're chained. #define HAS_RGB_LED 1 -#define PIN_RGB_LED_1 39 // (1, 7) — power/charge -#define PIN_RGB_LED_2 44 // (1, 12) — notification -#define PIN_RGB_LED_3 28 // (0, 28) — BLE pairing +#define PIN_RGB_LED_1 39 // (1, 7) — chain data in #define PIN_NEOPIXEL PIN_RGB_LED_1 #define NUM_NEOPIXELS 3 From 28b4720a17cf2411237ae24086cf00572f02e2f1 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:31:53 +1000 Subject: [PATCH 03/11] ui fixes; gps toggle on off now working --- variants/lilygo_techo_card/TechoCardHomeScreen.h | 4 ++-- variants/lilygo_techo_card/platformio.ini | 2 ++ variants/lilygo_techo_card/target.cpp | 11 ++++++++--- variants/lilygo_techo_card/target.h | 4 ++-- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/variants/lilygo_techo_card/TechoCardHomeScreen.h b/variants/lilygo_techo_card/TechoCardHomeScreen.h index 8466da8b38..352347bd64 100644 --- a/variants/lilygo_techo_card/TechoCardHomeScreen.h +++ b/variants/lilygo_techo_card/TechoCardHomeScreen.h @@ -117,7 +117,7 @@ class TechoCardHomeScreen : public UIScreen { display.print(tmp); display.setCursor(0, Y3); - snprintf(tmp, sizeof(tmp), "Noise floor: %d", + snprintf(tmp, sizeof(tmp), "NF: %d", radio_driver.getNoiseFloor()); display.print(tmp); break; @@ -172,7 +172,7 @@ class TechoCardHomeScreen : public UIScreen { display.print("GPS: ON"); if (loc) { - snprintf(tmp, sizeof(tmp), "Sats: %d", + snprintf(tmp, sizeof(tmp), "S: %d", loc->satellitesCount()); display.drawTextRightAlign(display.width() - 1, Y0, tmp); diff --git a/variants/lilygo_techo_card/platformio.ini b/variants/lilygo_techo_card/platformio.ini index 13df5a8ed5..cbd08bac62 100644 --- a/variants/lilygo_techo_card/platformio.ini +++ b/variants/lilygo_techo_card/platformio.ini @@ -28,9 +28,11 @@ build_flags = ${nrf52_base.build_flags} -D PIN_BUZZER=38 -D PIN_BOOT_BTN=24 -D ENV_INCLUDE_GPS=1 + -D ENV_SKIP_GPS_DETECT -D DISABLE_DIAGNOSTIC_OUTPUT build_src_filter = ${nrf52_base.build_src_filter} + + + + +<../variants/lilygo_techo_card> lib_deps = diff --git a/variants/lilygo_techo_card/target.cpp b/variants/lilygo_techo_card/target.cpp index 2e3af8dc50..98ed6aa6e1 100644 --- a/variants/lilygo_techo_card/target.cpp +++ b/variants/lilygo_techo_card/target.cpp @@ -9,14 +9,19 @@ RADIO_CLASS radio = new Module(P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BU WRAPPER_CLASS radio_driver(radio, board); -SensorManager sensors = SensorManager(); - VolatileRTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); +#if ENV_INCLUDE_GPS + MicroNMEALocationProvider gps(Serial1, &rtc_clock); + EnvironmentSensorManager sensors(gps); +#else + EnvironmentSensorManager sensors; +#endif + #ifdef DISPLAY_CLASS DISPLAY_CLASS display; - MomentaryButton user_btn(PIN_USER_BTN, 1000, true); + MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true); #endif bool radio_init() { diff --git a/variants/lilygo_techo_card/target.h b/variants/lilygo_techo_card/target.h index 29f8b51fea..a1a95a512a 100644 --- a/variants/lilygo_techo_card/target.h +++ b/variants/lilygo_techo_card/target.h @@ -5,7 +5,7 @@ #include #include #include -#include +#include #include #include "TechoCardBoard.h" @@ -27,7 +27,7 @@ extern AutoDiscoverRTCClock rtc_clock; extern MomentaryButton user_btn; #endif -extern SensorManager sensors; +extern EnvironmentSensorManager sensors; bool radio_init(); uint32_t radio_get_rng_seed(); From f525e6a2e4d9f89255d4afd114098e889d52f738 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:53:21 +1000 Subject: [PATCH 04/11] =?UTF-8?q?UITask.cpp=20=E2=80=94=20MsgPreviewScreen?= =?UTF-8?q?=20fully=20guarded=20out=20for=20T-Echo=20Card.=20The=20=5Fmsgc?= =?UTF-8?q?ount=20still=20updates=20and=20the=20display=20still=20wakes=20?= =?UTF-8?q?on=20new=20messages,=20but=20it=20stays=20on=20the=20home=20scr?= =?UTF-8?q?een=20instead=20of=20switching=20to=20a=20preview=20that's=20un?= =?UTF-8?q?readable=20at=2072=C3=9740.=20Also=20saves=20a=20bit=20of=20nRF?= =?UTF-8?q?52=20heap=20by=20skipping=20the=20allocation.=20TechoCardHomeSc?= =?UTF-8?q?reen.h=20=E2=80=94=20Battery=20%=20moved=20from=20Y0=20down=20t?= =?UTF-8?q?o=20Y1,=20right-aligned=20opposite=20the=20MSG=20count.=20Node?= =?UTF-8?q?=20name=20now=20has=20the=20full=20top=20line=20to=20itself.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/companion_radio/ui-new/UITask.cpp | 4 ++++ variants/lilygo_techo_card/TechoCardHomeScreen.h | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 970b688eee..6574305dda 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -603,7 +603,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #else home = new HomeScreen(this, &rtc_clock, sensors, node_prefs); #endif +#if !defined(LILYGO_TECHO_CARD) msg_preview = new MsgPreviewScreen(this, &rtc_clock); +#endif setCurrScreen(splash); } @@ -652,8 +654,10 @@ void UITask::msgRead(int msgcount) { void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { _msgcount = msgcount; +#if !defined(LILYGO_TECHO_CARD) ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text); setCurrScreen(msg_preview); +#endif if (_display != NULL) { if (!_display->isOn() && !hasConnection()) { diff --git a/variants/lilygo_techo_card/TechoCardHomeScreen.h b/variants/lilygo_techo_card/TechoCardHomeScreen.h index 352347bd64..313489cd38 100644 --- a/variants/lilygo_techo_card/TechoCardHomeScreen.h +++ b/variants/lilygo_techo_card/TechoCardHomeScreen.h @@ -78,14 +78,14 @@ class TechoCardHomeScreen : public UIScreen { sizeof(filtered_name)); display.print(filtered_name); - snprintf(tmp, sizeof(tmp), "%d%%", battPercent()); - display.drawTextRightAlign(display.width() - 1, Y0, tmp); - display.setColor(DisplayDriver::YELLOW); display.setCursor(0, Y1); snprintf(tmp, sizeof(tmp), "MSG: %d", _task->getMsgCount()); display.print(tmp); + snprintf(tmp, sizeof(tmp), "%d%%", battPercent()); + display.drawTextRightAlign(display.width() - 1, Y1, tmp); + display.setColor(DisplayDriver::LIGHT); display.setCursor(0, Y2); if (_task->hasConnection()) { From d52190583d7f59b0c8b404e00bf394025506be8a Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:57:34 +1000 Subject: [PATCH 05/11] remove .DS_Store, add to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a0ad5f6ea9..0b02ba18d7 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ compile_commands.json .venv/ venv/ platformio.local.ini +.DS_Store From 218f1ea0555e68b414c35058dd93872dd3f64b01 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:58:13 +1000 Subject: [PATCH 06/11] removed em dashes and replaced with -- --- examples/companion_radio/ui-new/UITask.cpp | 2 +- variants/lilygo_techo_card/TechoCardBoard.cpp | 2 +- variants/lilygo_techo_card/TechoCardBoard.h | 2 +- .../lilygo_techo_card/TechoCardHomeScreen.h | 6 +-- variants/lilygo_techo_card/platformio.ini | 2 +- variants/lilygo_techo_card/target.cpp | 2 +- variants/lilygo_techo_card/variant.cpp | 2 +- variants/lilygo_techo_card/variant.h | 44 +++++++++---------- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 6574305dda..bc26adec6e 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -56,7 +56,7 @@ class SplashScreen : public UIScreen { int render(DisplayDriver& display) override { #if defined(LILYGO_TECHO_CARD) - // Text-only splash for 72×40 OLED — no room for 128px logo + // Text-only splash for 72x40 OLED -- no room for 128px logo display.setColor(DisplayDriver::GREEN); display.setTextSize(1); display.drawTextCentered(display.width()/2, 2, "MeshCore"); diff --git a/variants/lilygo_techo_card/TechoCardBoard.cpp b/variants/lilygo_techo_card/TechoCardBoard.cpp index 9df5cf6f41..53bf37a894 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.cpp +++ b/variants/lilygo_techo_card/TechoCardBoard.cpp @@ -59,7 +59,7 @@ void TechoCardBoard::begin() { pinMode(PIN_VBAT_READ, INPUT); pinMode(PIN_USER_BTN, INPUT); - // Initialise I2C — must be done before display.begin() is called from main.cpp + // Initialise I2C -- must be done before display.begin() is called from main.cpp Wire.begin(); Wire.setClock(400000); diff --git a/variants/lilygo_techo_card/TechoCardBoard.h b/variants/lilygo_techo_card/TechoCardBoard.h index 134da85756..5eb1c917dd 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.h +++ b/variants/lilygo_techo_card/TechoCardBoard.h @@ -49,7 +49,7 @@ class TechoCardBoard : public NRF52BoardDCDC { // Speaker power control void enableSpeaker(bool enable); - // RGB LEDs — all three to same colour + // RGB LEDs -- all three to same colour void setLED(uint8_t r, uint8_t g, uint8_t b); void ledOff(); diff --git a/variants/lilygo_techo_card/TechoCardHomeScreen.h b/variants/lilygo_techo_card/TechoCardHomeScreen.h index 313489cd38..2d557e068d 100644 --- a/variants/lilygo_techo_card/TechoCardHomeScreen.h +++ b/variants/lilygo_techo_card/TechoCardHomeScreen.h @@ -1,7 +1,7 @@ // ============================================================================= -// TechoCardHomeScreen — 72×40 OLED home screen for LilyGo T-Echo Card +// TechoCardHomeScreen -- 72x40 OLED home screen for LilyGo T-Echo Card // -// Four-line layout using U8g2's 4×6 tom_thumb font (18 chars × 4 lines). +// Four-line layout using U8g2's 4x6 tom_thumb font (18 chars x 4 lines). // U8g2's native SSD1306_72X40_ER support handles all GDDRAM offset mapping. // // Two-button navigation: A (pin 42) = next page / long-press activate @@ -41,7 +41,7 @@ class TechoCardHomeScreen : public UIScreen { unsigned long _shutdown_at; // Four lines at 9px spacing within 40px display. - // U8g2 handles panel offset natively — y=0 is the true visible top. + // U8g2 handles panel offset natively -- y=0 is the true visible top. static const int Y0 = 2; static const int Y1 = 11; static const int Y2 = 20; diff --git a/variants/lilygo_techo_card/platformio.ini b/variants/lilygo_techo_card/platformio.ini index cbd08bac62..dfdffda3a3 100644 --- a/variants/lilygo_techo_card/platformio.ini +++ b/variants/lilygo_techo_card/platformio.ini @@ -1,5 +1,5 @@ ; ============================================================================= -; LilyGo T-Echo Card — nRF52840 + SX1262 + SSD1306 OLED (72x40) + L76K GPS +; LilyGo T-Echo Card -- nRF52840 + SX1262 + SSD1306 OLED (72x40) + L76K GPS ; ============================================================================= [lilygo_techo_card] diff --git a/variants/lilygo_techo_card/target.cpp b/variants/lilygo_techo_card/target.cpp index 98ed6aa6e1..bae3c17c1e 100644 --- a/variants/lilygo_techo_card/target.cpp +++ b/variants/lilygo_techo_card/target.cpp @@ -26,7 +26,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); bool radio_init() { // board.begin() and display.begin() are called by main.cpp before this. - // radio_init() should ONLY initialise the radio — matching Meshpocket pattern. + // radio_init() should ONLY initialise the radio -- matching Meshpocket pattern. return radio.std_init(&SPI); } diff --git a/variants/lilygo_techo_card/variant.cpp b/variants/lilygo_techo_card/variant.cpp index 3b45770d42..4cba928f16 100644 --- a/variants/lilygo_techo_card/variant.cpp +++ b/variants/lilygo_techo_card/variant.cpp @@ -4,7 +4,7 @@ #include "wiring_digital.h" const uint32_t g_ADigitalPinMap[] = { - // P0 — pins 0 and 1 are hardwired for 32.768 kHz crystal (LFXO) + // P0 -- pins 0 and 1 are hardwired for 32.768 kHz crystal (LFXO) 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 diff --git a/variants/lilygo_techo_card/variant.h b/variants/lilygo_techo_card/variant.h index feacfe6894..d45a55c74c 100644 --- a/variants/lilygo_techo_card/variant.h +++ b/variants/lilygo_techo_card/variant.h @@ -1,7 +1,7 @@ /* - * variant.h — LilyGo T-Echo Card pin definitions + * variant.h -- LilyGo T-Echo Card pin definitions * - * nRF52840 + SX1262 (HPB16B3) + SSD1315 OLED (72×40) + L76K GPS + * nRF52840 + SX1262 (HPB16B3) + SSD1315 OLED (72x40) + L76K GPS * + MAX98357 Speaker + MP34DT05 PDM Mic + ICM20948 IMU + BQ25896 + Solar * * Cross-referenced against: @@ -44,10 +44,10 @@ #define NUM_ANALOG_OUTPUTS (0) //////////////////////////////////////////////////////////////////////////////// -// UART — GPS (L76K) +// UART -- GPS (L76K) -#define PIN_SERIAL1_RX 21 // (0, 21) — GPS TX → nRF RX -#define PIN_SERIAL1_TX 19 // (0, 19) — nRF TX → GPS RX +#define PIN_SERIAL1_RX 21 // (0, 21) -- GPS TX → nRF RX +#define PIN_SERIAL1_TX 19 // (0, 19) -- nRF TX → GPS RX //////////////////////////////////////////////////////////////////////////////// // I2C (shared: OLED, IMU ICM20948) @@ -57,37 +57,37 @@ #define PIN_WIRE_SCL 34 // (1, 2) //////////////////////////////////////////////////////////////////////////////// -// LEDs — WS2812 addressable (no plain GPIO LED) -// The BSP drives LED_BUILTIN via digitalWrite for BLE status — if pointed at +// LEDs -- WS2812 addressable (no plain GPIO LED) +// The BSP drives LED_BUILTIN via digitalWrite for BLE status -- if pointed at // the WS2812 data pin (39), it holds the line HIGH and all LEDs glow green. // Point at an unused GPIO (46 = P1.14) so the BSP toggles harmlessly. -#define LED_BUILTIN 46 // Unused GPIO — keeps BSP happy +#define LED_BUILTIN 46 // Unused GPIO -- keeps BSP happy #define PIN_LED LED_BUILTIN #define LED_RED LED_BUILTIN #define LED_BLUE (-1) // Prevents Bluefruit flashing during advertising #define PIN_STATUS_LED LED_BUILTIN #define LED_STATE_ON 1 -// WS2812 RGB LEDs — 3 LEDs daisy-chained on a single data line (pin 39) +// WS2812 RGB LEDs -- 3 LEDs daisy-chained on a single data line (pin 39) // Hardware verified: all three light when pin 39 is driven HIGH. // Meshtastic PR #10267 mapped them as separate GPIOs (39, 44, 28) but // testing confirms they're chained. #define HAS_RGB_LED 1 -#define PIN_RGB_LED_1 39 // (1, 7) — chain data in +#define PIN_RGB_LED_1 39 // (1, 7) -- chain data in #define PIN_NEOPIXEL PIN_RGB_LED_1 #define NUM_NEOPIXELS 3 //////////////////////////////////////////////////////////////////////////////// // Buttons -#define PIN_BUTTON1 42 // (1, 10) — orange front button +#define PIN_BUTTON1 42 // (1, 10) -- orange front button #define BUTTON_PIN PIN_BUTTON1 #define PIN_USER_BTN BUTTON_PIN // Boot button: P0.24 (hardware only, used for DFU) //////////////////////////////////////////////////////////////////////////////// -// SPI — LoRa +// SPI -- LoRa #define SPI_INTERFACES_COUNT (1) @@ -119,9 +119,9 @@ #define LORA_RF_VC2 33 // (1, 1) //////////////////////////////////////////////////////////////////////////////// -// OLED Display — SSD1315 (SSD1306-compatible), 72×40, I2C +// OLED Display -- SSD1315 (SSD1306-compatible), 72x40, I2C // -// Physical panel is 72×40 within 128×64 GDDRAM. +// Physical panel is 72x40 within 128x64 GDDRAM. // Visible window: columns 28–99, pages 3–7 (rows 24–63). // SETDISPLAYOFFSET = 24 maps page 0 writes to the visible area. @@ -131,12 +131,12 @@ #define OLED_HEIGHT 40 #define OLED_DISPLAY_OFFSET 24 -// RT9080 enable — controls 3V3 rail (OLED, GPS, LoRa, sensors) +// RT9080 enable -- controls 3V3 rail (OLED, GPS, LoRa, sensors) #define PIN_OLED_EN 30 // (0, 30) #define PIN_OLED_RESET (-1) //////////////////////////////////////////////////////////////////////////////// -// GPS — L76K Multi-GNSS +// GPS -- L76K Multi-GNSS #define HAS_GPS 1 #define GPS_BAUDRATE 9600 @@ -148,7 +148,7 @@ #define PIN_GPS_RF_EN 29 // (0, 29) //////////////////////////////////////////////////////////////////////////////// -// Speaker — MAX98357 I2S Class-D Mono Amp +// Speaker -- MAX98357 I2S Class-D Mono Amp #define HAS_SPEAKER 1 #define PIN_SPK_EN 43 // (1, 11) @@ -158,7 +158,7 @@ #define PIN_SPK_LRCK 22 // (0, 22) //////////////////////////////////////////////////////////////////////////////// -// Microphone — MP34DT05 Digital MEMS PDM +// Microphone -- MP34DT05 Digital MEMS PDM #define HAS_MICROPHONE 1 #define PIN_MIC_CLK 35 // (1, 3) @@ -171,18 +171,18 @@ #define PIN_BUZZER 38 // (1, 6) //////////////////////////////////////////////////////////////////////////////// -// IMU — ICM20948 +// IMU -- ICM20948 #define HAS_IMU 1 #define IMU_I2C_ADDR 0x68 //////////////////////////////////////////////////////////////////////////////// -// NFC — nRF52840 NFC-A (dedicated P0.09/P0.10) +// NFC -- nRF52840 NFC-A (dedicated P0.09/P0.10) #define HAS_NFC 1 //////////////////////////////////////////////////////////////////////////////// -// External Flash — ZD25WQ32CEIGR 4MB QSPI +// External Flash -- ZD25WQ32CEIGR 4MB QSPI #define HAS_EXT_FLASH 1 #define PIN_QSPI_SCK 4 // (0, 4) @@ -193,6 +193,6 @@ #define PIN_QSPI_IO3 26 // (0, 26) //////////////////////////////////////////////////////////////////////////////// -// No dedicated RTC chip — time from GPS or BLE companion sync +// No dedicated RTC chip -- time from GPS or BLE companion sync #define HAS_RTC 0 \ No newline at end of file From f77d42236b63bcdb4483eb79211913a009fafff0 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 1 May 2026 00:01:19 +1000 Subject: [PATCH 07/11] revert whitespace-only changes to SSD1306Display --- src/helpers/ui/SSD1306Display.cpp | 2 +- src/helpers/ui/SSD1306Display.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/helpers/ui/SSD1306Display.cpp b/src/helpers/ui/SSD1306Display.cpp index 07e000967e..464b2642a0 100644 --- a/src/helpers/ui/SSD1306Display.cpp +++ b/src/helpers/ui/SSD1306Display.cpp @@ -90,4 +90,4 @@ uint16_t SSD1306Display::getTextWidth(const char* str) { void SSD1306Display::endFrame() { display.display(); -} \ No newline at end of file +} diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h index 5995a2aeb4..d843da85b2 100644 --- a/src/helpers/ui/SSD1306Display.h +++ b/src/helpers/ui/SSD1306Display.h @@ -45,4 +45,4 @@ class SSD1306Display : public DisplayDriver { void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; uint16_t getTextWidth(const char* str) override; void endFrame() override; -}; \ No newline at end of file +}; From 877606a460edcf61856ba010b781b75ff964a72f Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 1 May 2026 00:10:25 +1000 Subject: [PATCH 08/11] fixed torch statusled uitask --- examples/companion_radio/ui-new/UITask.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index bc26adec6e..d130b69be6 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -56,7 +56,7 @@ class SplashScreen : public UIScreen { int render(DisplayDriver& display) override { #if defined(LILYGO_TECHO_CARD) - // Text-only splash for 72x40 OLED -- no room for 128px logo + // Text-only splash for 72×40 OLED -- no room for 128px logo display.setColor(DisplayDriver::GREEN); display.setTextSize(1); display.drawTextCentered(display.width()/2, 2, "MeshCore"); @@ -781,9 +781,10 @@ void UITask::loop() { } else if (ev2 == BUTTON_EVENT_DOUBLE_CLICK) { torch_on = !torch_on; if (torch_on) { - board.setLED(255, 255, 255); + // Single LED only -- driving all three white exceeds RT9080 current budget and reboots + board.setStatusLED(0, 0xFFFFFF); } else { - board.ledOff(); + board.setStatusLED(0, 0); } } } From a4d2bae8b4f5f55e2af2519321750768aae6126e Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 1 May 2026 10:08:01 +1000 Subject: [PATCH 09/11] T-Echo Card: compass, battery page, GPS fixes, MCU temp telemetry, idle sleep. New file: - GPSStreamCounter.h: Stream wrapper counting NMEA sentences for live baud-rate confirmation on the GPS home screen page. Variant fixes: - variant.h: swap GPS RX/TX pins (vendor labels are chip-perspective, not nRF-perspective); #ifndef guards on buzzer defines. - target.cpp/h: wire GPSStreamCounter around Serial1. - TechoCardBoard: getMCUTemperature() via sd_temp_get (SoftDevice-safe), BQ25896 charger I2C API, ICM20948/AK09916 compass init/read/sleep. - TechoCardHomeScreen.h: COMPASS + BATTERY pages, NMEA rate when no GPS fix, battPercent() recalibrated to 4.16V full-charge ceiling. Cross-platform (all guarded behind LILYGO_TECHO_CARD or NRF52_PLATFORM): - MyMesh.cpp: MCU die temp in both telemetry paths; hasPendingWork(). - MyMesh.h: hasPendingWork() decl; GPS enable-at-boot (#if TECHO_CARD). - main.cpp: board.sleep(0) when idle (#if NRF52_PLATFORM). - UITask.cpp: _version_info[24]; double-click screen toggle and GPS hardware enable/disable (#if LILYGO_TECHO_CARD). --- examples/companion_radio/MyMesh.cpp | 11 ++ examples/companion_radio/MyMesh.h | 12 +- examples/companion_radio/main.cpp | 8 +- examples/companion_radio/ui-new/UITask.cpp | 22 ++- variants/lilygo_techo_card/GPSStreamCounter.h | 76 ++++++++ variants/lilygo_techo_card/TechoCardBoard.cpp | 166 ++++++++++++++++++ variants/lilygo_techo_card/TechoCardBoard.h | 21 +++ .../lilygo_techo_card/TechoCardHomeScreen.h | 111 +++++++++++- variants/lilygo_techo_card/target.cpp | 3 +- variants/lilygo_techo_card/target.h | 8 + variants/lilygo_techo_card/variant.h | 12 +- 11 files changed, 439 insertions(+), 11 deletions(-) create mode 100644 variants/lilygo_techo_card/GPSStreamCounter.h diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index e8c1914bad..6dca77e1bd 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -642,6 +642,7 @@ uint8_t MyMesh::onContactRequest(const ContactInfo &contact, uint32_t sender_tim if (permissions & TELEM_PERM_BASE) { // only respond if base permission bit is set telemetry.reset(); telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + { float t = board.getMCUTemperature(); if (!isnan(t)) telemetry.addTemperature(TELEM_CHANNEL_SELF, t); } // query other sensors -- target specific sensors.querySensors(permissions, telemetry); @@ -1613,6 +1614,7 @@ void MyMesh::handleCmdFrame(size_t len) { } else if (cmd_frame[0] == CMD_SEND_TELEMETRY_REQ && len == 4) { // 'self' telemetry request telemetry.reset(); telemetry.addVoltage(TELEM_CHANNEL_SELF, (float)board.getBattMilliVolts() / 1000.0f); + { float t = board.getMCUTemperature(); if (!isnan(t)) telemetry.addTemperature(TELEM_CHANNEL_SELF, t); } // query other sensors -- target specific sensors.querySensors(0xFF, telemetry); @@ -2191,3 +2193,12 @@ bool MyMesh::advert() { return false; } } + +// To check if there is pending work +bool MyMesh::hasPendingWork() const { + #if defined(WITH_BRIDGE) + if (bridge.isRunning()) return true; // bridge needs WiFi radio, can't sleep + #endif + // If getOutboundTotal() is not available, use: _mgr->getOutboundCount(0xFFFFFFFF) > 0 + return _mgr->getOutboundTotal() > 0; +} \ No newline at end of file diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index aeff591cf4..9a0390c727 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -174,9 +174,19 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { sprintf(interval_str, "%u", _prefs.gps_interval); sensors.setSettingValue("gps_interval", interval_str); } + #if defined(LILYGO_TECHO_CARD) + // Power the L76K down at boot if GPS is persisted as off. + // Without this, board.begin() drives PIN_GPS_EN HIGH unconditionally + // and the chip stays powered until the user manually toggles GPS + // twice through the menu. + board.enableGPS(_prefs.gps_enabled); + #endif } #endif + // To check if there is pending work + bool hasPendingWork() const; + private: void writeOKFrame(); void writeErrFrame(uint8_t err_code); @@ -249,4 +259,4 @@ class MyMesh : public BaseChatMesh, public DataStoreHost { AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table }; -extern MyMesh the_mesh; +extern MyMesh the_mesh; \ No newline at end of file diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 876dc9c33c..70b0a9c870 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -229,4 +229,10 @@ void loop() { ui_task.loop(); #endif rtc_clock.tick(); -} + + if (!the_mesh.hasPendingWork()) { + #if defined(NRF52_PLATFORM) + board.sleep(0); // nrf ignores seconds param, sleeps whenever possible + #endif + } +} \ No newline at end of file diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index d130b69be6..9472dff115 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -37,7 +37,7 @@ class SplashScreen : public UIScreen { UITask* _task; unsigned long dismiss_after; - char _version_info[12]; + char _version_info[24]; public: SplashScreen(UITask* task) : _task(task) { @@ -763,7 +763,21 @@ void UITask::loop() { } else if (ev == BUTTON_EVENT_LONG_PRESS) { c = handleLongPress(KEY_ENTER); } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { + #if defined(LILYGO_TECHO_CARD) + // Toggle screen on/off for battery saving + if (_display != NULL) { + if (_display->isOn()) { + _display->turnOff(); + } else { + _display->turnOn(); + _auto_off = millis() + AUTO_OFF_MILLIS; + _next_refresh = 0; + } + } + c = 0; // consume event + #else c = handleDoubleClick(KEY_PREV); + #endif } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { c = handleTripleClick(KEY_SELECT); } @@ -947,6 +961,12 @@ void UITask::toggleGPS() { _node_prefs->gps_enabled = 1; notify(UIEventType::ack); } + #if defined(LILYGO_TECHO_CARD) + // Actually power the L76K down/up at the hardware level. + // Without this, toggling GPS off only stops reading position data + // while the chip itself stays powered (~25mA draw). + board.enableGPS(_node_prefs->gps_enabled); + #endif the_mesh.savePrefs(); showAlert(_node_prefs->gps_enabled ? "GPS: Enabled" : "GPS: Disabled", 800); _next_refresh = 0; diff --git a/variants/lilygo_techo_card/GPSStreamCounter.h b/variants/lilygo_techo_card/GPSStreamCounter.h new file mode 100644 index 0000000000..dbe9884658 --- /dev/null +++ b/variants/lilygo_techo_card/GPSStreamCounter.h @@ -0,0 +1,76 @@ +#pragma once + +#include + +// Transparent Stream wrapper that counts NMEA sentences (newline-delimited) +// flowing from the GPS serial port to the MicroNMEA parser. +// +// Usage: Instead of MicroNMEALocationProvider gps(Serial2, &rtc_clock); +// Use: GPSStreamCounter gpsStream(Serial2); +// MicroNMEALocationProvider gps(gpsStream, &rtc_clock); +// +// Every read() call passes through to the underlying stream; when a '\n' +// is seen the sentence counter increments. This lets the UI display a +// live "nmea" count so users can confirm the baud rate is correct and +// the GPS module is actually sending data. + +class GPSStreamCounter : public Stream { +public: + GPSStreamCounter(Stream& inner) + : _inner(inner), _sentences(0), _sentences_snapshot(0), + _last_snapshot(0), _sentences_per_sec(0) {} + + // --- Stream read interface (passes through) --- + int available() override { return _inner.available(); } + int peek() override { return _inner.peek(); } + + int read() override { + int c = _inner.read(); + if (c == '\n') { + _sentences++; + } + return c; + } + + // --- Stream write interface (pass through for NMEA commands if needed) --- + size_t write(uint8_t b) override { return _inner.write(b); } + + // Required override on Adafruit nRF52 BSP where Stream::flush() is pure virtual. + // No-op equivalent on ESP32 cores that provide a default implementation. + void flush() override { _inner.flush(); } + + // --- Sentence counting API --- + + // Total sentences received since boot (or last reset) + uint32_t getSentenceCount() const { return _sentences; } + + // Sentences received per second (updated each time you call it, + // with a 1-second rolling window) + uint16_t getSentencesPerSec() { + unsigned long now = millis(); + unsigned long elapsed = now - _last_snapshot; + if (elapsed >= 1000) { + uint32_t delta = _sentences - _sentences_snapshot; + // Scale to per-second if interval wasn't exactly 1000ms + _sentences_per_sec = (uint16_t)((delta * 1000UL) / elapsed); + _sentences_snapshot = _sentences; + _last_snapshot = now; + } + return _sentences_per_sec; + } + + // Reset all counters (e.g. when GPS hardware power cycles) + void resetCounters() { + _sentences = 0; + _sentences_snapshot = 0; + _sentences_per_sec = 0; + _last_snapshot = millis(); + } + +private: + Stream& _inner; + volatile uint32_t _sentences; + uint32_t _sentences_snapshot; + unsigned long _last_snapshot; + uint16_t _sentences_per_sec; +}; diff --git a/variants/lilygo_techo_card/TechoCardBoard.cpp b/variants/lilygo_techo_card/TechoCardBoard.cpp index 53bf37a894..6b24ac0251 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.cpp +++ b/variants/lilygo_techo_card/TechoCardBoard.cpp @@ -1,6 +1,7 @@ #include "TechoCardBoard.h" #include "variant.h" #include +#include void TechoCardBoard::begin() { NRF52BoardDCDC::begin(); @@ -84,6 +85,22 @@ void TechoCardBoard::enableGPS(bool enable) { #endif } +float TechoCardBoard::getMCUTemperature() { + // SoftDevice owns the TEMP peripheral -- direct register access hard faults. + // Use sd_temp_get() when SoftDevice is enabled. + int32_t temp; + uint8_t sd_en = 0; + sd_softdevice_is_enabled(&sd_en); + if (sd_en) { + if (sd_temp_get(&temp) == NRF_SUCCESS) { + return temp * 0.25f; + } + return NAN; + } + // SoftDevice off -- fall back to parent's direct register access + return NRF52Board::getMCUTemperature(); +} + void TechoCardBoard::enableSpeaker(bool enable) { #if defined(HAS_SPEAKER) digitalWrite(PIN_SPK_EN, enable ? HIGH : LOW); @@ -130,4 +147,153 @@ void TechoCardBoard::buzz(uint16_t freq_hz, uint16_t duration_ms) { #else (void)freq_hz; (void)duration_ms; #endif +} + +// ============================================================================= +// BQ25896 Charger IC (I2C address 0x6B) +// ============================================================================= + +#define BQ25896_ADDR 0x6B + +bool TechoCardBoard::probeCharger() { + if (!_chargerProbed) { + Wire.beginTransmission(BQ25896_ADDR); + _chargerPresent = (Wire.endTransmission() == 0); + _chargerProbed = true; + if (!_chargerPresent) { + Serial.println("BQ25896: not found at 0x6B"); + } + } + return _chargerPresent; +} + +uint8_t TechoCardBoard::readChargerReg(uint8_t reg) { + if (!probeCharger()) return 0; + Wire.beginTransmission(BQ25896_ADDR); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) return 0; + Wire.requestFrom((uint8_t)BQ25896_ADDR, (uint8_t)1); + return Wire.available() ? Wire.read() : 0; +} + +void TechoCardBoard::writeChargerReg(uint8_t reg, uint8_t val) { + Wire.beginTransmission(BQ25896_ADDR); + Wire.write(reg); + Wire.write(val); + Wire.endTransmission(); +} + +void TechoCardBoard::enableChargerADC() { + uint8_t reg02 = readChargerReg(0x02); + reg02 |= 0xC0; // CONV_RATE=1 (continuous) + CONV_START=1 + writeChargerReg(0x02, reg02); +} + +uint8_t TechoCardBoard::getChargeStatus() { + return (readChargerReg(0x0B) >> 3) & 0x03; +} + +uint16_t TechoCardBoard::getChargerBattMV() { + return 2304 + (readChargerReg(0x0E) & 0x7F) * 20; +} + +uint8_t TechoCardBoard::getChargerTSPCT() { + return 21 + (readChargerReg(0x10) & 0x7F); +} + +// ============================================================================= +// ICM20948 / AK09916 Compass +// +// Enable I2C bypass on the ICM20948 so the AK09916 magnetometer at 0x0C +// appears directly on Wire. Then set continuous measurement mode. +// ============================================================================= + +#define ICM20948_ADDR 0x68 +#define AK09916_ADDR 0x0C + +static uint8_t _i2c_rd(uint8_t addr, uint8_t reg) { + Wire.beginTransmission(addr); + Wire.write(reg); + if (Wire.endTransmission(false) != 0) return 0; + Wire.requestFrom(addr, (uint8_t)1); + return Wire.available() ? Wire.read() : 0; +} + +static void _i2c_wr(uint8_t addr, uint8_t reg, uint8_t val) { + Wire.beginTransmission(addr); + Wire.write(reg); + Wire.write(val); + Wire.endTransmission(); +} + +bool TechoCardBoard::initCompass() { + if (_compassReady) return true; + + // Bank 0 + _i2c_wr(ICM20948_ADDR, 0x7F, 0x00); + + // Check WHO_AM_I (expect 0xEA) + if (_i2c_rd(ICM20948_ADDR, 0x00) != 0xEA) return false; + + // Wake up: auto clock, not sleep + _i2c_wr(ICM20948_ADDR, 0x06, 0x01); + delay(10); + + // Enable I2C bypass so AK09916 is directly accessible + _i2c_wr(ICM20948_ADDR, 0x0F, 0x02); + delay(5); + + // Check AK09916 WHO_AM_I (expect 0x09) + if (_i2c_rd(AK09916_ADDR, 0x01) != 0x09) return false; + + // Power down first, then continuous mode 4 (100 Hz) + _i2c_wr(AK09916_ADDR, 0x31, 0x00); + delay(1); + _i2c_wr(AK09916_ADDR, 0x31, 0x08); + delay(10); + + _compassReady = true; + return true; +} + +bool TechoCardBoard::readMag(int16_t& mx, int16_t& my, int16_t& mz) { + if (!_compassReady) return false; + + // Check data ready (ST1 bit 0) + if (!(_i2c_rd(AK09916_ADDR, 0x10) & 0x01)) return false; + + // Burst read 6 data bytes + ST2 (must read ST2 to complete cycle) + Wire.beginTransmission(AK09916_ADDR); + Wire.write(0x11); + if (Wire.endTransmission(false) != 0) return false; + Wire.requestFrom((uint8_t)AK09916_ADDR, (uint8_t)7); + if (Wire.available() < 7) return false; + + uint8_t buf[7]; + for (int i = 0; i < 7; i++) buf[i] = Wire.read(); + + mx = (int16_t)(buf[1] << 8 | buf[0]); + my = (int16_t)(buf[3] << 8 | buf[2]); + mz = (int16_t)(buf[5] << 8 | buf[4]); + // buf[6] = ST2, read to unlatch + + return true; +} + +// Power down the AK09916 magnetometer and put the ICM20948 itself to sleep. +// Saves ~3-4mA when not actively viewing the compass page. +// Next call to initCompass() will fully re-initialise the chain. +void TechoCardBoard::sleepCompass() { + if (!_compassReady) return; + + // Bank 0 (in case we drifted) + _i2c_wr(ICM20948_ADDR, 0x7F, 0x00); + + // AK09916 CNTL2 = 0x00 -- power-down mode (stops continuous measurement) + _i2c_wr(AK09916_ADDR, 0x31, 0x00); + + // ICM20948 PWR_MGMT_1 = 0x40 -- SLEEP bit set + _i2c_wr(ICM20948_ADDR, 0x06, 0x40); + + _compassReady = false; } \ No newline at end of file diff --git a/variants/lilygo_techo_card/TechoCardBoard.h b/variants/lilygo_techo_card/TechoCardBoard.h index 5eb1c917dd..4b4f346d88 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.h +++ b/variants/lilygo_techo_card/TechoCardBoard.h @@ -39,6 +39,8 @@ class TechoCardBoard : public NRF52BoardDCDC { return "LilyGo T-Echo Card"; } + float getMCUTemperature() override; + void powerOff() override { sd_power_system_off(); } @@ -58,4 +60,23 @@ class TechoCardBoard : public NRF52BoardDCDC { // Buzzer void buzz(uint16_t freq_hz, uint16_t duration_ms); + + // BQ25896 charger IC (0x6B) + bool probeCharger(); // check if BQ25896 responds on I2C + uint8_t readChargerReg(uint8_t reg); + void writeChargerReg(uint8_t reg, uint8_t val); + void enableChargerADC(); // start continuous ADC conversion + uint8_t getChargeStatus(); // 0=none, 1=pre, 2=fast, 3=done + uint16_t getChargerBattMV(); // battery voltage from charger ADC + uint8_t getChargerTSPCT(); // thermistor voltage as % of REGN + + // ICM20948 / AK09916 compass (0x68 bypass to 0x0C) + bool initCompass(); + bool readMag(int16_t& mx, int16_t& my, int16_t& mz); + void sleepCompass(); // power down magnetometer + put ICM20948 in sleep mode + +private: + bool _compassReady = false; + bool _chargerProbed = false; + bool _chargerPresent = false; }; \ No newline at end of file diff --git a/variants/lilygo_techo_card/TechoCardHomeScreen.h b/variants/lilygo_techo_card/TechoCardHomeScreen.h index 2d557e068d..4cbea4b7a2 100644 --- a/variants/lilygo_techo_card/TechoCardHomeScreen.h +++ b/variants/lilygo_techo_card/TechoCardHomeScreen.h @@ -7,10 +7,11 @@ // Two-button navigation: A (pin 42) = next page / long-press activate // C (pin 24) = previous page // -// Pages: STATUS → RADIO → BLE → ADVERT → GPS → HIBERNATE +// Pages: STATUS -> RADIO -> BLE -> ADVERT -> GPS -> COMPASS -> BATTERY -> HIBERNATE // ============================================================================= #pragma once +#include #include #include #include @@ -29,6 +30,8 @@ class TechoCardHomeScreen : public UIScreen { #if ENV_INCLUDE_GPS == 1 GPS, #endif + COMPASS, + BATTERY, HIBERNATE, PAGE_COUNT }; @@ -40,6 +43,12 @@ class TechoCardHomeScreen : public UIScreen { bool _shutdown_init; unsigned long _shutdown_at; + // Compass state + bool _compassInitDone; + bool _compassOK; + float _lastHeading; + int16_t _lastMx, _lastMy, _lastMz; + // Four lines at 9px spacing within 40px display. // U8g2 handles panel offset natively -- y=0 is the true visible top. static const int Y0 = 2; @@ -50,16 +59,29 @@ class TechoCardHomeScreen : public UIScreen { int battPercent() { uint16_t mv = _task->getBattMilliVolts(); if (mv == 0) return 0; - int pct = ((int)mv - 3000) * 100 / 1200; + int pct = ((int)mv - 3000) * 100 / 1160; if (pct < 0) pct = 0; if (pct > 100) pct = 100; return pct; } + const char* cardinal(float deg) { + if (deg >= 337.5f || deg < 22.5f) return "N"; + if (deg < 67.5f) return "NE"; + if (deg < 112.5f) return "E"; + if (deg < 157.5f) return "SE"; + if (deg < 202.5f) return "S"; + if (deg < 247.5f) return "SW"; + if (deg < 292.5f) return "W"; + return "NW"; + } + public: TechoCardHomeScreen(UITask* task, mesh::RTCClock* rtc, NodePrefs* prefs) : _task(task), _rtc(rtc), _prefs(prefs), - _page(STATUS), _shutdown_init(false), _shutdown_at(0) {} + _page(STATUS), _shutdown_init(false), _shutdown_at(0), + _compassInitDone(false), _compassOK(false), + _lastHeading(0), _lastMx(0), _lastMy(0), _lastMz(0) {} void cancelEditing() { _shutdown_init = false; } @@ -186,6 +208,13 @@ class TechoCardHomeScreen : public UIScreen { loc->getLatitude() / 1000000.0, loc->getLongitude() / 1000000.0); display.print(tmp); + } else { + // No fix yet -- show NMEA sentence rate to confirm the chip is talking. + // If this stays at 0, GPS is silent (baud rate wrong, RF off, etc). + display.setCursor(0, Y2); + snprintf(tmp, sizeof(tmp), "NMEA: %u/s", + (unsigned)gpsStream.getSentencesPerSec()); + display.print(tmp); } } @@ -196,6 +225,82 @@ class TechoCardHomeScreen : public UIScreen { } #endif + // ----- COMPASS ----- + case COMPASS: { + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, Y0); + display.print("Compass"); + + if (!_compassInitDone) { + _compassOK = board.initCompass(); + _compassInitDone = true; + } + + if (!_compassOK) { + display.setColor(DisplayDriver::RED); + display.setCursor(0, Y2); + display.print("IMU not found"); + break; + } + + int16_t mx, my, mz; + if (board.readMag(mx, my, mz)) { + _lastMx = mx; _lastMy = my; _lastMz = mz; + _lastHeading = atan2f((float)my, (float)mx) * 180.0f / (float)M_PI; + if (_lastHeading < 0) _lastHeading += 360.0f; + } + + display.setColor(DisplayDriver::YELLOW); + display.setCursor(0, Y1); + snprintf(tmp, sizeof(tmp), "%.0f %s", + _lastHeading, cardinal(_lastHeading)); + display.print(tmp); + + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, Y2); + snprintf(tmp, sizeof(tmp), "X:%d Y:%d", _lastMx, _lastMy); + display.print(tmp); + + #if ENV_INCLUDE_GPS == 1 + { + LocationProvider* loc = sensors.getLocationProvider(); + if (loc && loc->isValid()) { + display.setCursor(0, Y3); + snprintf(tmp, sizeof(tmp), "%.4f,%.4f", + loc->getLatitude() / 1000000.0, + loc->getLongitude() / 1000000.0); + display.print(tmp); + } + } + #endif + return 500; // fast refresh for compass + } + + // ----- BATTERY ----- + case BATTERY: { + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, Y0); + display.print("Battery"); + + uint16_t mv = _task->getBattMilliVolts(); + snprintf(tmp, sizeof(tmp), "%d%%", battPercent()); + display.drawTextRightAlign(display.width() - 1, Y0, tmp); + + display.setColor(DisplayDriver::YELLOW); + display.setCursor(0, Y1); + snprintf(tmp, sizeof(tmp), "%d.%02dV", mv / 1000, (mv % 1000) / 10); + display.print(tmp); + + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, Y2); + { + float dieTemp = board.getMCUTemperature(); + snprintf(tmp, sizeof(tmp), "Temp: %.0fC", dieTemp); + display.print(tmp); + } + break; + } + // ----- HIBERNATE ----- case HIBERNATE: { if (_shutdown_init) { diff --git a/variants/lilygo_techo_card/target.cpp b/variants/lilygo_techo_card/target.cpp index bae3c17c1e..0fb27d2249 100644 --- a/variants/lilygo_techo_card/target.cpp +++ b/variants/lilygo_techo_card/target.cpp @@ -13,7 +13,8 @@ VolatileRTCClock fallback_clock; AutoDiscoverRTCClock rtc_clock(fallback_clock); #if ENV_INCLUDE_GPS - MicroNMEALocationProvider gps(Serial1, &rtc_clock); + GPSStreamCounter gpsStream(Serial1); + MicroNMEALocationProvider gps(gpsStream, &rtc_clock); EnvironmentSensorManager sensors(gps); #else EnvironmentSensorManager sensors; diff --git a/variants/lilygo_techo_card/target.h b/variants/lilygo_techo_card/target.h index a1a95a512a..19e41434e5 100644 --- a/variants/lilygo_techo_card/target.h +++ b/variants/lilygo_techo_card/target.h @@ -9,6 +9,10 @@ #include #include "TechoCardBoard.h" +#if ENV_INCLUDE_GPS +#include "GPSStreamCounter.h" +#endif + #ifdef DISPLAY_CLASS #if defined(USE_U8G2_DISPLAY) #include @@ -29,6 +33,10 @@ extern AutoDiscoverRTCClock rtc_clock; extern EnvironmentSensorManager sensors; +#if ENV_INCLUDE_GPS +extern GPSStreamCounter gpsStream; +#endif + bool radio_init(); uint32_t radio_get_rng_seed(); void radio_set_params(float freq, float bw, uint8_t sf, uint8_t cr); diff --git a/variants/lilygo_techo_card/variant.h b/variants/lilygo_techo_card/variant.h index d45a55c74c..c60c7684bb 100644 --- a/variants/lilygo_techo_card/variant.h +++ b/variants/lilygo_techo_card/variant.h @@ -46,8 +46,8 @@ //////////////////////////////////////////////////////////////////////////////// // UART -- GPS (L76K) -#define PIN_SERIAL1_RX 21 // (0, 21) -- GPS TX → nRF RX -#define PIN_SERIAL1_TX 19 // (0, 19) -- nRF TX → GPS RX +#define PIN_SERIAL1_RX 19 // (0, 19) -- GPS TX -> nRF RX +#define PIN_SERIAL1_TX 21 // (0, 21) -- nRF TX -> GPS RX //////////////////////////////////////////////////////////////////////////////// // I2C (shared: OLED, IMU ICM20948) @@ -140,8 +140,8 @@ #define HAS_GPS 1 #define GPS_BAUDRATE 9600 -#define PIN_GPS_TX 19 // nRF TX → GPS RX -#define PIN_GPS_RX 21 // nRF RX ← GPS TX +#define PIN_GPS_TX 21 // nRF TX -> GPS RX (vendor GPS_UART_RX / P0.21) +#define PIN_GPS_RX 19 // nRF RX <- GPS TX (vendor GPS_UART_TX / P0.19) #define PIN_GPS_EN 47 // (1, 15) #define PIN_GPS_WAKEUP 25 // (0, 25) #define PIN_GPS_1PPS 23 // (0, 23) @@ -167,8 +167,12 @@ //////////////////////////////////////////////////////////////////////////////// // Buzzer +#ifndef HAS_BUZZER #define HAS_BUZZER 1 +#endif +#ifndef PIN_BUZZER #define PIN_BUZZER 38 // (1, 6) +#endif //////////////////////////////////////////////////////////////////////////////// // IMU -- ICM20948 From 2557f3788ff22ea9f78e782d5518a81d04ad7b30 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Fri, 1 May 2026 11:57:53 +1000 Subject: [PATCH 10/11] added triple click A button for buzzer silence to uitask; changed auto off milis to 60 seconds instead of default 15 in platformio; compass fix and calibration fix in techoboardhome --- examples/companion_radio/ui-new/UITask.cpp | 5 + variants/lilygo_techo_card/TechoCardBoard.cpp | 52 +++++- variants/lilygo_techo_card/TechoCardBoard.h | 18 ++ .../lilygo_techo_card/TechoCardHomeScreen.h | 173 +++++++++++++++--- variants/lilygo_techo_card/platformio.ini | 2 +- 5 files changed, 217 insertions(+), 33 deletions(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 9472dff115..e0a797f73f 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -779,7 +779,12 @@ void UITask::loop() { c = handleDoubleClick(KEY_PREV); #endif } else if (ev == BUTTON_EVENT_TRIPLE_CLICK) { + #if defined(LILYGO_TECHO_CARD) + toggleBuzzer(); + c = 0; // consume event + #else c = handleTripleClick(KEY_SELECT); + #endif } #endif #if defined(PIN_BOOT_BTN) diff --git a/variants/lilygo_techo_card/TechoCardBoard.cpp b/variants/lilygo_techo_card/TechoCardBoard.cpp index 6b24ac0251..cf9c601fbe 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.cpp +++ b/variants/lilygo_techo_card/TechoCardBoard.cpp @@ -2,6 +2,8 @@ #include "variant.h" #include #include +#include +using namespace Adafruit_LittleFS_Namespace; void TechoCardBoard::begin() { NRF52BoardDCDC::begin(); @@ -246,11 +248,8 @@ bool TechoCardBoard::initCompass() { // Check AK09916 WHO_AM_I (expect 0x09) if (_i2c_rd(AK09916_ADDR, 0x01) != 0x09) return false; - // Power down first, then continuous mode 4 (100 Hz) + // Leave in power-down -- readMag triggers single measurements on demand _i2c_wr(AK09916_ADDR, 0x31, 0x00); - delay(1); - _i2c_wr(AK09916_ADDR, 0x31, 0x08); - delay(10); _compassReady = true; return true; @@ -259,8 +258,16 @@ bool TechoCardBoard::initCompass() { bool TechoCardBoard::readMag(int16_t& mx, int16_t& my, int16_t& mz) { if (!_compassReady) return false; - // Check data ready (ST1 bit 0) - if (!(_i2c_rd(AK09916_ADDR, 0x10) & 0x01)) return false; + // Single-measurement mode: trigger one fresh measurement per call. + // Continuous mode gets disrupted by OLED I2C display writes sharing + // the bus through ICM20948 bypass, causing stale data. + _i2c_wr(AK09916_ADDR, 0x31, 0x01); // single measurement trigger + + // Wait for data ready (measurement takes ~7.2ms) + for (int i = 0; i < 20; i++) { + if (_i2c_rd(AK09916_ADDR, 0x10) & 0x01) break; + delay(1); + } // Burst read 6 data bytes + ST2 (must read ST2 to complete cycle) Wire.beginTransmission(AK09916_ADDR); @@ -296,4 +303,37 @@ void TechoCardBoard::sleepCompass() { _i2c_wr(ICM20948_ADDR, 0x06, 0x40); _compassReady = false; +} + +// ============================================================================= +// Compass calibration persistence +// ============================================================================= + +#define COMPASS_CAL_FILE "/compass_cal" + +bool TechoCardBoard::loadCalibration() { + // InternalFS must already be initialised (done in main.cpp setup) + File file = InternalFS.open(COMPASS_CAL_FILE, FILE_O_READ); + if (file) { + int n = file.read((uint8_t*)&_cal, sizeof(_cal)); + file.close(); + if (n == (int)sizeof(_cal) && _cal.magic == COMPASS_CAL_MAGIC) { + return true; + } + } + // No valid calibration -- reset to identity (no correction) + _cal = { 0, 0, 0, 1.0f, 1.0f, 1.0f, 0 }; + return false; +} + +bool TechoCardBoard::saveCalibration(const CompassCalibration& cal) { + _cal = cal; + _cal.magic = COMPASS_CAL_MAGIC; + // Direct-write pattern: remove then create (nRF52 LittleFS compatible) + InternalFS.remove(COMPASS_CAL_FILE); + File file = InternalFS.open(COMPASS_CAL_FILE, FILE_O_WRITE); + if (!file) return false; + file.write((const uint8_t*)&_cal, sizeof(_cal)); + file.close(); + return true; } \ No newline at end of file diff --git a/variants/lilygo_techo_card/TechoCardBoard.h b/variants/lilygo_techo_card/TechoCardBoard.h index 4b4f346d88..ae432682b7 100644 --- a/variants/lilygo_techo_card/TechoCardBoard.h +++ b/variants/lilygo_techo_card/TechoCardBoard.h @@ -9,6 +9,17 @@ #include #endif +// Hard-iron offsets + soft-iron axis scaling. +// Computed by on-device calibration (rotate slowly for ~20 seconds). +// Persisted to /compass_cal on InternalFS. +#define COMPASS_CAL_MAGIC 0xCA1B0000 + +struct CompassCalibration { + int16_t off_x, off_y, off_z; // hard-iron offsets (raw ADC counts) + float scale_x, scale_y, scale_z; // soft-iron per-axis scale factors + uint32_t magic; // COMPASS_CAL_MAGIC when valid +}; + class TechoCardBoard : public NRF52BoardDCDC { private: #if defined(HAS_RGB_LED) @@ -75,8 +86,15 @@ class TechoCardBoard : public NRF52BoardDCDC { bool readMag(int16_t& mx, int16_t& my, int16_t& mz); void sleepCompass(); // power down magnetometer + put ICM20948 in sleep mode + // Compass calibration (persisted to InternalFS) + bool loadCalibration(); // call after InternalFS.begin() + bool saveCalibration(const CompassCalibration& cal); + bool isCalibrated() const { return _cal.magic == COMPASS_CAL_MAGIC; } + const CompassCalibration& getCalibration() const { return _cal; } + private: bool _compassReady = false; bool _chargerProbed = false; bool _chargerPresent = false; + CompassCalibration _cal = { 0, 0, 0, 1.0f, 1.0f, 1.0f, 0 }; }; \ No newline at end of file diff --git a/variants/lilygo_techo_card/TechoCardHomeScreen.h b/variants/lilygo_techo_card/TechoCardHomeScreen.h index 4cbea4b7a2..78c789eb72 100644 --- a/variants/lilygo_techo_card/TechoCardHomeScreen.h +++ b/variants/lilygo_techo_card/TechoCardHomeScreen.h @@ -49,6 +49,18 @@ class TechoCardHomeScreen : public UIScreen { float _lastHeading; int16_t _lastMx, _lastMy, _lastMz; + // Compass calibration state + bool _calMode; + unsigned long _calStart; + uint16_t _calCount; + int16_t _calMinX, _calMaxX; + int16_t _calMinY, _calMaxY; + int16_t _calMinZ, _calMaxZ; + + // Diagnostic counters (temporary) + uint16_t _magOk; + uint16_t _magFail; + // Four lines at 9px spacing within 40px display. // U8g2 handles panel offset natively -- y=0 is the true visible top. static const int Y0 = 2; @@ -81,7 +93,12 @@ class TechoCardHomeScreen : public UIScreen { : _task(task), _rtc(rtc), _prefs(prefs), _page(STATUS), _shutdown_init(false), _shutdown_at(0), _compassInitDone(false), _compassOK(false), - _lastHeading(0), _lastMx(0), _lastMy(0), _lastMz(0) {} + _lastHeading(0), _lastMx(0), _lastMy(0), _lastMz(0), + _calMode(false), _calStart(0), _calCount(0), + _calMinX(0), _calMaxX(0), + _calMinY(0), _calMaxY(0), + _calMinZ(0), _calMaxZ(0), + _magOk(0), _magFail(0) {} void cancelEditing() { _shutdown_init = false; } @@ -203,9 +220,14 @@ class TechoCardHomeScreen : public UIScreen { display.print(loc->isValid() ? "Fix: 3D" : "No fix"); if (loc->isValid()) { + display.setColor(DisplayDriver::LIGHT); display.setCursor(0, Y2); - snprintf(tmp, sizeof(tmp), "%.4f, %.4f", - loc->getLatitude() / 1000000.0, + snprintf(tmp, sizeof(tmp), "%.4f", + loc->getLatitude() / 1000000.0); + display.print(tmp); + + display.setCursor(0, Y3); + snprintf(tmp, sizeof(tmp), "%.4f", loc->getLongitude() / 1000000.0); display.print(tmp); } else { @@ -215,27 +237,102 @@ class TechoCardHomeScreen : public UIScreen { snprintf(tmp, sizeof(tmp), "NMEA: %u/s", (unsigned)gpsStream.getSentencesPerSec()); display.print(tmp); + + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, Y3); + display.print("Hold A: toggle"); } } - - display.setColor(DisplayDriver::LIGHT); - display.setCursor(0, Y3); - display.print("Hold A: toggle"); break; } #endif // ----- COMPASS ----- case COMPASS: { - display.setColor(DisplayDriver::GREEN); - display.setCursor(0, Y0); - display.print("Compass"); - if (!_compassInitDone) { _compassOK = board.initCompass(); + board.loadCalibration(); _compassInitDone = true; } + // --- Calibration mode --- + if (_calMode) { + int16_t mx, my, mz; + if (_compassOK && board.readMag(mx, my, mz)) { + if (_calCount == 0) { + _calMinX = _calMaxX = mx; + _calMinY = _calMaxY = my; + _calMinZ = _calMaxZ = mz; + } else { + if (mx < _calMinX) _calMinX = mx; + if (mx > _calMaxX) _calMaxX = mx; + if (my < _calMinY) _calMinY = my; + if (my > _calMaxY) _calMaxY = my; + if (mz < _calMinZ) _calMinZ = mz; + if (mz > _calMaxZ) _calMaxZ = mz; + } + _calCount++; + } + + int spreadX = _calMaxX - _calMinX; + int spreadY = _calMaxY - _calMinY; + int spreadZ = _calMaxZ - _calMinZ; + unsigned long elapsed = millis() - _calStart; + bool adequate = (spreadX >= 100 && spreadY >= 100 && _calCount >= 150); + bool timeout = (elapsed >= 30000); + + if (adequate || (timeout && spreadX >= 50 && spreadY >= 50)) { + // Compute and save calibration + CompassCalibration cal; + cal.off_x = (_calMinX + _calMaxX) / 2; + cal.off_y = (_calMinY + _calMaxY) / 2; + cal.off_z = (_calMinZ + _calMaxZ) / 2; + float avgRange = ((float)spreadX + (float)spreadY) / 2.0f; + cal.scale_x = (spreadX > 0) ? avgRange / (float)spreadX : 1.0f; + cal.scale_y = (spreadY > 0) ? avgRange / (float)spreadY : 1.0f; + cal.scale_z = (spreadZ > 30) ? avgRange / (float)spreadZ : 1.0f; + cal.magic = COMPASS_CAL_MAGIC; + board.saveCalibration(cal); + _calMode = false; + _task->showAlert("Cal saved!", 800); + return 500; + } + + if (timeout) { + _calMode = false; + _task->showAlert("Try again", 800); + return 500; + } + + // Calibration progress display + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, Y0); + display.print("Calibrate"); + + display.setColor(DisplayDriver::YELLOW); + display.setCursor(0, Y1); + display.print("Rotate slowly..."); + + display.setColor(DisplayDriver::LIGHT); + display.setCursor(0, Y2); + snprintf(tmp, sizeof(tmp), "Samples: %u", _calCount); + display.print(tmp); + + display.setCursor(0, Y3); + snprintf(tmp, sizeof(tmp), "X:%d Y:%d", spreadX, spreadY); + display.print(tmp); + + return 100; // fast sample collection + } + + // --- Normal compass display --- + display.setColor(DisplayDriver::GREEN); + display.setCursor(0, Y0); + display.print("Compass"); + if (board.isCalibrated()) { + display.drawTextRightAlign(display.width() - 1, Y0, "CAL"); + } + if (!_compassOK) { display.setColor(DisplayDriver::RED); display.setCursor(0, Y2); @@ -245,9 +342,27 @@ class TechoCardHomeScreen : public UIScreen { int16_t mx, my, mz; if (board.readMag(mx, my, mz)) { - _lastMx = mx; _lastMy = my; _lastMz = mz; - _lastHeading = atan2f((float)my, (float)mx) * 180.0f / (float)M_PI; + _magOk++; + // Exponential moving average: 7/8 old + 1/8 new (settles in ~2s) + if (_magOk == 1) { + _lastMx = mx; _lastMy = my; _lastMz = mz; + } else { + _lastMx = (_lastMx * 7 + mx + 4) >> 3; + _lastMy = (_lastMy * 7 + my + 4) >> 3; + _lastMz = (_lastMz * 7 + mz + 4) >> 3; + } + float cx = (float)_lastMx; + float cy = (float)_lastMy; + if (board.isCalibrated()) { + const CompassCalibration& cal = board.getCalibration(); + cx = ((float)_lastMx - cal.off_x) * cal.scale_x; + cy = ((float)_lastMy - cal.off_y) * cal.scale_y; + } + // Y axis is inverted relative to compass convention on this PCB + _lastHeading = atan2f(-cy, cx) * 180.0f / (float)M_PI; if (_lastHeading < 0) _lastHeading += 360.0f; + } else { + _magFail++; } display.setColor(DisplayDriver::YELLOW); @@ -261,19 +376,11 @@ class TechoCardHomeScreen : public UIScreen { snprintf(tmp, sizeof(tmp), "X:%d Y:%d", _lastMx, _lastMy); display.print(tmp); - #if ENV_INCLUDE_GPS == 1 - { - LocationProvider* loc = sensors.getLocationProvider(); - if (loc && loc->isValid()) { - display.setCursor(0, Y3); - snprintf(tmp, sizeof(tmp), "%.4f,%.4f", - loc->getLatitude() / 1000000.0, - loc->getLongitude() / 1000000.0); - display.print(tmp); - } - } - #endif - return 500; // fast refresh for compass + display.setCursor(0, Y3); + snprintf(tmp, sizeof(tmp), "Z:%d", _lastMz); + display.print(tmp); + + return 250; // smooth readable refresh } // ----- BATTERY ----- @@ -330,6 +437,13 @@ class TechoCardHomeScreen : public UIScreen { return true; } + // Any input during calibration cancels it + if (_calMode) { + _calMode = false; + _task->showAlert("Cancelled", 500); + return true; + } + if (c == KEY_NEXT || c == 'd') { _page = (_page + 1) % PAGE_COUNT; return true; @@ -368,6 +482,13 @@ class TechoCardHomeScreen : public UIScreen { return true; #endif + case COMPASS: + if (!_compassOK) return false; + _calMode = true; + _calStart = millis(); + _calCount = 0; + return true; + case HIBERNATE: _shutdown_init = true; _shutdown_at = millis() + 500; diff --git a/variants/lilygo_techo_card/platformio.ini b/variants/lilygo_techo_card/platformio.ini index dfdffda3a3..34ced774d3 100644 --- a/variants/lilygo_techo_card/platformio.ini +++ b/variants/lilygo_techo_card/platformio.ini @@ -61,7 +61,7 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D BLE_PIN_CODE=123456 -D OFFLINE_QUEUE_SIZE=256 - -D AUTO_OFF_MILLIS=0 + -D AUTO_OFF_MILLIS=60000 ; -D BLE_DEBUG_LOGGING=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 From 600928aac142425ea78df5f8ffaef230588bbd77 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Sat, 2 May 2026 02:31:50 +1000 Subject: [PATCH 11/11] Added -D AUTO_SHUTDOWN_MILLIVOLTS=3000 for better battery polling --- variants/lilygo_techo_card/platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/variants/lilygo_techo_card/platformio.ini b/variants/lilygo_techo_card/platformio.ini index 34ced774d3..7aeaca6993 100644 --- a/variants/lilygo_techo_card/platformio.ini +++ b/variants/lilygo_techo_card/platformio.ini @@ -30,6 +30,7 @@ build_flags = ${nrf52_base.build_flags} -D ENV_INCLUDE_GPS=1 -D ENV_SKIP_GPS_DETECT -D DISABLE_DIAGNOSTIC_OUTPUT + -D AUTO_SHUTDOWN_MILLIVOLTS=3000 build_src_filter = ${nrf52_base.build_src_filter} + +