From 0d32852d907f8d85b16df83744c681a4175ef585 Mon Sep 17 00:00:00 2001 From: slack-t Date: Tue, 28 Apr 2026 17:17:32 +0200 Subject: [PATCH] Add Heltec Wireless Tracker v1.1 board support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New board: BOARD_HELTEC_TRACKER (0x3D), PRODUCT_HELTEC_TRACKER (0xC4), MODEL_CB (0xCB 433 MHz) / MODEL_CC (0xCC 868/915 MHz) - platformio.ini: new rtnode_heltec_tracker env (QIO PSRAM, 16 MB partitions, Adafruit ST7735+GFX libs) - Boards.h: full pin definitions — SX1262, ST7735S TFT, VEXT, VBAT, GNSS UART - Display.h: ST7735S branch (INITR_MINI160x80_PLUGIN, 160×80 landscape, NVS rotation bypass, no fillScreen on update to avoid soft-SPI flicker) - Power.h / Utilities.h / BoundaryConfig.h / Input.h: Tracker branches - sx126x.cpp: add Tracker to SPI init list; TCXO branch; fix SX1262 errata 15.4 — restore register 0x0736 IQ polarity bit after every SetPacketParams (was missing from this fork — caused LED always-on via spurious preamble IRQ and silent RX demodulation failure) - GNSS.h: UC6580 UART driver (VEXT gate, 115200 baud, NMEA parse, gnss_state_t) - flash.py / extra_script.py: tracker board profile and provisioning entry - docs: manifest-tracker.json + manifest-tracker-full.json; web flasher UI entry Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 1 + Boards.h | 77 ++++++++++++++++++- BoundaryConfig.h | 7 +- Display.h | 63 +++++++++++++++- GNSS.h | 126 ++++++++++++++++++++++++++++++++ Input.h | 2 + Power.h | 27 ++++++- RNode_Firmware.ino | 2 +- Utilities.h | 13 +++- docs/index.html | 33 ++++++++- docs/manifest-tracker-full.json | 14 ++++ docs/manifest-tracker.json | 14 ++++ extra_script.py | 2 + flash.py | 20 ++++- platformio.ini | 27 +++++++ sx126x.cpp | 21 +++++- 16 files changed, 428 insertions(+), 21 deletions(-) create mode 100644 GNSS.h create mode 100644 docs/manifest-tracker-full.json create mode 100644 docs/manifest-tracker.json diff --git a/.gitignore b/.gitignore index cc93a9e..95ea05e 100755 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ Console/build build/* .pio/* .vscode/* +.gitnexus diff --git a/Boards.h b/Boards.h index 5dd8171..ca24e17 100755 --- a/Boards.h +++ b/Boards.h @@ -99,10 +99,15 @@ #define BOARD_HELTEC32_V4 0x3F #define MODEL_C8 0xC8 // Heltec Lora32 v3, 850-950 MHz, 28dBm - #define PRODUCT_HELTEC_T114 0xC2 // Heltec Mesh Node T114 - #define BOARD_HELTEC_T114 0x3C - #define MODEL_C6 0xC6 // Heltec Mesh Node T114, 470-510 MHz - #define MODEL_C7 0xC7 // Heltec Mesh Node T114, 863-928 MHz + #define PRODUCT_HELTEC_T114 0xC2 // Heltec Mesh Node T114 + #define BOARD_HELTEC_T114 0x3C + #define MODEL_C6 0xC6 // Heltec Mesh Node T114, 470-510 MHz + #define MODEL_C7 0xC7 // Heltec Mesh Node T114, 863-928 MHz + + #define PRODUCT_HELTEC_TRACKER 0xC4 // Heltec Wireless Tracker v1.1 + #define BOARD_HELTEC_TRACKER 0x43 + #define MODEL_CB 0xCB // Heltec Wireless Tracker, 433/470 MHz + #define MODEL_CC 0xCC // Heltec Wireless Tracker, 868/915 MHz #define PRODUCT_TECHO 0x15 // LilyGO T-Echo devices #define BOARD_TECHO 0x44 @@ -161,6 +166,7 @@ #define HAS_SLEEP false #define HAS_LORA_PA false #define HAS_LORA_LNA false + #define HAS_GNSS false #define PIN_DISP_SLEEP -1 #define VALIDATE_FIRMWARE true @@ -457,6 +463,69 @@ const int pin_miso = 11; const int pin_sclk = 9; + #elif BOARD_MODEL == BOARD_HELTEC_TRACKER + #define IS_ESP32S3 true + #define HAS_DISPLAY true + #define HAS_BLUETOOTH false + #ifdef BOUNDARY_MODE + #define HAS_BLE false + #else + #define HAS_BLE true + #endif + #define HAS_WIFI true + #define HAS_PMU true + #define HAS_CONSOLE true + #define HAS_EEPROM true + #define HAS_INPUT true + #define HAS_SLEEP true + #define HAS_GNSS true + #define PIN_WAKEUP GPIO_NUM_0 + #define WAKEUP_LEVEL 0 + // VEXT (GPIO3, active LOW) powers TFT and GNSS module + #define Vext GPIO_NUM_3 + + const int pin_btn_usr1 = 0; + const int pin_led_rx = 18; + const int pin_led_tx = 18; + + #define MODEM SX1262 + #define HAS_TCXO true + const int pin_tcxo_enable = -1; + #define HAS_BUSY true + #define DIO2_AS_RF_SWITCH true + + #define PA_MAX_OUTPUT 22 + + // SX1262 LoRa (same pins as V3/V4) + const int pin_cs = 8; + const int pin_busy = 13; + const int pin_dio = 14; + const int pin_reset = 12; + const int pin_mosi = 10; + const int pin_miso = 11; + const int pin_sclk = 9; + + // ST7735S 0.96" 80×160 TFT (software SPI on dedicated pins) + #define TRACKER_TFT_BL 21 + #define TRACKER_TFT_DC 40 + #define TRACKER_TFT_CS 38 + #define TRACKER_TFT_RST 39 + #define TRACKER_TFT_MOSI 42 + #define TRACKER_TFT_SCK 41 + + const int DISPLAY_BL_PIN = TRACKER_TFT_BL; + const int DISPLAY_DC = TRACKER_TFT_DC; + const int DISPLAY_CS = TRACKER_TFT_CS; + const int DISPLAY_RST = TRACKER_TFT_RST; + const int DISPLAY_MOSI = TRACKER_TFT_MOSI; + const int DISPLAY_CLK = TRACKER_TFT_SCK; + + // UC6580 GNSS (UART2) + #define TRACKER_GNSS_TX 33 + #define TRACKER_GNSS_RX 34 + #define TRACKER_GNSS_RST 35 + #define TRACKER_GNSS_PPS 36 + #elif BOARD_MODEL == BOARD_RNODE_NG_20 #define HAS_DISPLAY true #define HAS_BLUETOOTH true diff --git a/BoundaryConfig.h b/BoundaryConfig.h index a70332f..54cf3a8 100755 --- a/BoundaryConfig.h +++ b/BoundaryConfig.h @@ -70,7 +70,7 @@ static const int BW_OPTIONS_COUNT = sizeof(BW_OPTIONS_HZ) / sizeof(BW_OPTIONS_HZ static uint8_t config_default_display_rotation() { #if BOARD_MODEL == BOARD_LORA32_V2_1 || BOARD_MODEL == BOARD_TBEAM || BOARD_MODEL == BOARD_RAK4631 return 0; - #elif BOARD_MODEL == BOARD_HELTEC32_V2 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC_T114 || BOARD_MODEL == BOARD_TBEAM_S_V1 + #elif BOARD_MODEL == BOARD_HELTEC32_V2 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC_TRACKER || BOARD_MODEL == BOARD_HELTEC_T114 || BOARD_MODEL == BOARD_TBEAM_S_V1 return 1; #else return 3; @@ -779,9 +779,14 @@ void config_portal_start() { stat_area.println(""); stat_area.println("Open browser"); stat_area.println("http://10.0.0.1"); + #if BOARD_MODEL == BOARD_HELTEC_TRACKER + display.fillScreen(SSD1306_BLACK); + display.drawBitmap(8, 0, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK); + #else display.clearDisplay(); display.drawBitmap(0, 0, stat_area.getBuffer(), stat_area.width(), stat_area.height(), SSD1306_WHITE, SSD1306_BLACK); display.display(); + #endif } #endif // Headless: LED ramp will be driven from the WCC portal loop diff --git a/Display.h b/Display.h index ece9b75..1d92ce0 100755 --- a/Display.h +++ b/Display.h @@ -22,6 +22,8 @@ #elif BOARD_MODEL == BOARD_HELTEC_T114 #include "ST7789.h" #define COLOR565(r, g, b) (((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3)) + #elif BOARD_MODEL == BOARD_HELTEC_TRACKER + #include #elif BOARD_MODEL == BOARD_TBEAM_S_V1 #include #else @@ -153,6 +155,11 @@ extern BoundaryState boundary_state; ST7789Spi display(&SPI1, DISPLAY_RST, DISPLAY_DC, DISPLAY_CS); #define SSD1306_WHITE ST77XX_WHITE #define SSD1306_BLACK ST77XX_BLACK +#elif BOARD_MODEL == BOARD_HELTEC_TRACKER + // Software SPI on dedicated GPIO-matrix pins, separate from LoRa SPI bus + Adafruit_ST7735 display = Adafruit_ST7735(DISPLAY_CS, DISPLAY_DC, DISPLAY_MOSI, DISPLAY_CLK, DISPLAY_RST); + #define SSD1306_WHITE ST77XX_WHITE + #define SSD1306_BLACK ST77XX_BLACK #elif BOARD_MODEL == BOARD_TBEAM_S_V1 Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire, -1); #define SSD1306_WHITE SH110X_WHITE @@ -238,6 +245,14 @@ void update_area_positions() { p_as_x = 126; p_as_y = p_ad_y; } + #elif BOARD_MODEL == BOARD_HELTEC_TRACKER + // 160×80 landscape: two 64×64 canvases side by side, vertically centered (8px top margin) + if (disp_mode == DISP_MODE_LANDSCAPE) { + p_ad_x = 16; // disp area: left half (80px) → center 64px → offset 8px, +8px left margin + p_ad_y = 8; // (80-64)/2 + p_as_x = 80; // stat area: right half starts at x=80 + p_as_y = 8; + } #elif BOARD_MODEL == BOARD_TECHO if (disp_mode == DISP_MODE_PORTRAIT) { p_ad_x = 61; @@ -271,6 +286,11 @@ uint8_t display_contrast = 0x00; } #elif BOARD_MODEL == BOARD_HELTEC_T114 void set_contrast(ST7789Spi *display, uint8_t value) { } +#elif BOARD_MODEL == BOARD_HELTEC_TRACKER + void set_contrast(Adafruit_ST7735 *display, uint8_t value) { + // BL is active HIGH: higher PWM duty = brighter. + analogWrite(DISPLAY_BL_PIN, value == 0 ? 0 : map(value, 1, 255, 40, 255)); + } #elif BOARD_MODEL == BOARD_TECHO void set_contrast(void *display, uint8_t value) { if (value == 0) { analogWrite(pin_backlight, 0); } @@ -352,6 +372,14 @@ bool display_init() { #elif BOARD_MODEL == BOARD_HELTEC_T114 pinMode(PIN_T114_TFT_EN, OUTPUT); digitalWrite(PIN_T114_TFT_EN, LOW); + #elif BOARD_MODEL == BOARD_HELTEC_TRACKER + // VEXT is active HIGH (GPIO3 HIGH = peripheral power on) + pinMode(Vext, OUTPUT); + digitalWrite(Vext, HIGH); + // BL is active HIGH — claim LEDC channel at a visible duty before any + // set_contrast() call can fire and override it. + analogWrite(DISPLAY_BL_PIN, 200); + delay(50); #elif BOARD_MODEL == BOARD_TECHO display.init(0, true, 10, false, displaySPI, SPISettings(4000000, MSBFIRST, SPI_MODE0)); display.setPartialWindow(0, 0, DISP_W, DISP_H); @@ -418,6 +446,10 @@ bool display_init() { // set white as default pixel colour for Heltec T114 display.setRGB(COLOR565(0xFF, 0xFF, 0xFF)); if (false) { + #elif BOARD_MODEL == BOARD_HELTEC_TRACKER + display.initR(INITR_MINI160x80_PLUGIN); // heltec_tracker uses PLUGIN variant + display.fillScreen(SSD1306_BLACK); + if (false) { #elif BOARD_MODEL == BOARD_TBEAM_S_V1 if (!display.begin(display_address, true)) { #else @@ -425,7 +457,17 @@ bool display_init() { #endif return false; } else { - set_contrast(&display, display_contrast); + // Tracker backlight was already set in the VEXT block above; + // calling set_contrast(display_contrast=0) here would reset LEDC to 0%. + #if BOARD_MODEL != BOARD_HELTEC_TRACKER + set_contrast(&display, display_contrast); + #endif + // Tracker rotation is fixed by hardware; never load a stale NVS value. + #if BOARD_MODEL == BOARD_HELTEC_TRACKER + disp_mode = DISP_MODE_LANDSCAPE; + display.setRotation(3); + if (false) { // structural dummy — balances the closing } of the else block below + #else if (display_rotation != 0xFF) { if (display_rotation == 0 || display_rotation == 2) { disp_mode = DISP_MODE_LANDSCAPE; @@ -434,6 +476,7 @@ bool display_init() { } display.setRotation(display_rotation); } else { + #endif #if BOARD_MODEL == BOARD_RNODE_NG_20 disp_mode = DISP_MODE_PORTRAIT; display.setRotation(3); @@ -464,6 +507,9 @@ bool display_init() { #elif BOARD_MODEL == BOARD_HELTEC32_V4 disp_mode = DISP_MODE_PORTRAIT; display.setRotation(1); + #elif BOARD_MODEL == BOARD_HELTEC_TRACKER + disp_mode = DISP_MODE_LANDSCAPE; + display.setRotation(3); // INITR_MINI160x80_PLUGIN: rot3 = 160w × 80h landscape, correct origin #elif BOARD_MODEL == BOARD_HELTEC_T114 disp_mode = DISP_MODE_PORTRAIT; display.setRotation(1); @@ -519,6 +565,15 @@ bool display_init() { fillRect(p_as_x, p_as_y, 128, 128, SSD1306_BLACK); pinMode(PIN_T114_TFT_BLGT, OUTPUT); digitalWrite(PIN_T114_TFT_BLGT, LOW); + #elif BOARD_MODEL == BOARD_HELTEC_TRACKER + // Fresh EEPROM (erased flash) returns 0 from the NVS layer, which would + // call set_contrast(0) and turn the backlight off. Treat 0 as "not yet + // configured" and fall back to full brightness so first boot is visible. + if (display_intensity == 0) { + display_intensity = 0xFF; + display_unblank_intensity = 0xFF; + } + set_contrast(&display, display_intensity); #endif return true; @@ -1257,6 +1312,8 @@ void update_display(bool blank = false) { #if BOARD_MODEL == BOARD_HELTEC_T114 display.clear(); display.display(); + #elif BOARD_MODEL == BOARD_HELTEC_TRACKER + display.fillScreen(SSD1306_BLACK); #elif BOARD_MODEL != BOARD_TDECK && BOARD_MODEL != BOARD_TECHO display.clearDisplay(); display.display(); @@ -1277,6 +1334,8 @@ void update_display(bool blank = false) { #if BOARD_MODEL == BOARD_HELTEC_T114 display.clear(); + #elif BOARD_MODEL == BOARD_HELTEC_TRACKER + // drawBitmap is fully opaque; no fill needed — avoids slow full-screen SPI writes #elif BOARD_MODEL != BOARD_TDECK && BOARD_MODEL != BOARD_TECHO display.clearDisplay(); #endif @@ -1302,7 +1361,7 @@ void update_display(bool blank = false) { last_epd_refresh = millis(); epd_blanked = false; } - #elif BOARD_MODEL != BOARD_TDECK + #elif BOARD_MODEL != BOARD_TDECK && BOARD_MODEL != BOARD_HELTEC_TRACKER display.display(); #endif diff --git a/GNSS.h b/GNSS.h new file mode 100644 index 0000000..41deedb --- /dev/null +++ b/GNSS.h @@ -0,0 +1,126 @@ +// GNSS.h — UC6580 GNSS driver for Heltec Wireless Tracker v1.1 +// Phase 1: read NMEA from UART2, parse into gnss_state, log to Serial. +// Phase 2 (future): feed lat/lon into Advertise.h position beacons. + +#pragma once + +#if HAS_GNSS + +#include + +// ── State ─────────────────────────────────────────────────────────────────── + +struct GnssState { + bool fix; + uint8_t satellites; + double latitude; // degrees, positive = North + double longitude; // degrees, positive = East + float altitude; // metres MSL + uint8_t hour, minute, second; + bool ready; // true after first valid fix +}; + +GnssState gnss_state = {}; + +// ── Internal parser state ──────────────────────────────────────────────────── + +#define GNSS_UART Serial2 +#define GNSS_BAUD 115200 +#define GNSS_BUF_SIZE 128 + +static char _gnss_buf[GNSS_BUF_SIZE]; +static uint8_t _gnss_buf_pos = 0; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +static double _parse_nmea_coord(const char *field, const char *hemi) { + // NMEA format: DDDMM.MMMM or DDMM.MMMM + if (!field || field[0] == '\0') return 0.0; + double raw = atof(field); + int deg = (int)(raw / 100); + double mins = raw - deg * 100.0; + double val = deg + mins / 60.0; + if (hemi && (hemi[0] == 'S' || hemi[0] == 'W')) val = -val; + return val; +} + +static uint8_t _tok(const char *sentence, uint8_t idx, char *out, uint8_t maxlen) { + const char *p = sentence; + uint8_t cur = 0; + while (*p && cur < idx) { if (*p++ == ',') cur++; } + uint8_t n = 0; + while (*p && *p != ',' && *p != '*' && n < maxlen - 1) out[n++] = *p++; + out[n] = '\0'; + return n; +} + +// Parse $GNGGA or $GPGGA: time, lat, lon, fix, sats, alt +static void _parse_gga(const char *s) { + char f[16]; + + _tok(s, 1, f, sizeof(f)); + if (strlen(f) >= 6) { + gnss_state.hour = (f[0]-'0')*10 + (f[1]-'0'); + gnss_state.minute = (f[2]-'0')*10 + (f[3]-'0'); + gnss_state.second = (f[4]-'0')*10 + (f[5]-'0'); + } + + char lat[12], latH[2], lon[12], lonH[2]; + _tok(s, 2, lat, sizeof(lat)); + _tok(s, 3, latH, sizeof(latH)); + _tok(s, 4, lon, sizeof(lon)); + _tok(s, 5, lonH, sizeof(lonH)); + + char fixQ[2], sats[4], alt[12]; + _tok(s, 6, fixQ, sizeof(fixQ)); + _tok(s, 7, sats, sizeof(sats)); + _tok(s, 9, alt, sizeof(alt)); + + gnss_state.fix = (fixQ[0] >= '1'); + gnss_state.satellites = (uint8_t)atoi(sats); + gnss_state.latitude = _parse_nmea_coord(lat, latH); + gnss_state.longitude = _parse_nmea_coord(lon, lonH); + gnss_state.altitude = atof(alt); + + if (gnss_state.fix) gnss_state.ready = true; +} + +static void _process_sentence(const char *s) { + // Only handle GGA for now (position + fix quality) + if (strncmp(s + 1, "GNGGA", 5) == 0 || strncmp(s + 1, "GPGGA", 5) == 0) { + _parse_gga(s); + } +} + +// ── Public API ─────────────────────────────────────────────────────────────── + +void gnss_init() { + // Hard-reset UC6580 then release + pinMode(TRACKER_GNSS_RST, OUTPUT); + digitalWrite(TRACKER_GNSS_RST, LOW); + delay(10); + digitalWrite(TRACKER_GNSS_RST, HIGH); + delay(100); + + GNSS_UART.begin(GNSS_BAUD, SERIAL_8N1, TRACKER_GNSS_RX, TRACKER_GNSS_TX); +} + +// Call from the main loop; non-blocking, accumulates characters. +void gnss_update() { + while (GNSS_UART.available()) { + char c = (char)GNSS_UART.read(); + if (c == '$') { + _gnss_buf_pos = 0; + } + if (_gnss_buf_pos < GNSS_BUF_SIZE - 1) { + _gnss_buf[_gnss_buf_pos++] = c; + } + if (c == '\n' && _gnss_buf_pos > 6) { + _gnss_buf[_gnss_buf_pos] = '\0'; + _process_sentence(_gnss_buf); + _gnss_buf_pos = 0; + } + } +} + +#endif // HAS_GNSS diff --git a/Input.h b/Input.h index 3eae259..6e6e3ca 100755 --- a/Input.h +++ b/Input.h @@ -93,7 +93,9 @@ #if HAS_DISPLAY if (disp_ready) { display.fillScreen(SSD1306_WHITE); + #if BOARD_MODEL != BOARD_HELTEC_TRACKER display.display(); + #endif } #endif headless_led_fast_blink(); diff --git a/Power.h b/Power.h index 7ce6d8a..059f0da 100755 --- a/Power.h +++ b/Power.h @@ -147,6 +147,23 @@ float pmu_temperature = PMU_TEMP_MIN-1; bool bat_voltage_dropping = false; float bat_delay_v = 0; float bat_state_change_v = 0; +#elif BOARD_MODEL == BOARD_HELTEC_TRACKER + #define BAT_V_MIN 3.05 + #define BAT_V_MAX 4.0 + #define BAT_V_CHG 4.48 + #define BAT_V_FLOAT 4.33 + #define BAT_SAMPLES 7 + const uint8_t pin_vbat = 1; + const uint8_t pin_ctrl = 2; + float bat_p_samples[BAT_SAMPLES]; + float bat_v_samples[BAT_SAMPLES]; + uint8_t bat_samples_count = 0; + int bat_discharging_samples = 0; + int bat_charging_samples = 0; + int bat_charged_samples = 0; + bool bat_voltage_dropping = false; + float bat_delay_v = 0; + float bat_state_change_v = 0; #elif BOARD_MODEL == BOARD_HELTEC_T114 #define BAT_V_MIN 3.15 #define BAT_V_MAX 4.165 @@ -202,9 +219,9 @@ void measure_temperature() { } void measure_battery() { - #if BOARD_MODEL == BOARD_RNODE_NG_21 || BOARD_MODEL == BOARD_LORA32_V2_1 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC_T114 || BOARD_MODEL == BOARD_TECHO + #if BOARD_MODEL == BOARD_RNODE_NG_21 || BOARD_MODEL == BOARD_LORA32_V2_1 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC_TRACKER || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC_T114 || BOARD_MODEL == BOARD_TECHO battery_installed = true; - #if BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 + #if BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC_TRACKER battery_indeterminate = false; #else battery_indeterminate = true; @@ -214,6 +231,8 @@ void measure_battery() { float battery_measurement = (float)(analogRead(pin_vbat)) * 0.0041; #elif BOARD_MODEL == BOARD_HELTEC32_V4 float battery_measurement = (float)(analogRead(pin_vbat)) * 0.00418; + #elif BOARD_MODEL == BOARD_HELTEC_TRACKER + float battery_measurement = (float)(analogRead(pin_vbat)) * 0.00418; #elif BOARD_MODEL == BOARD_T3S3 float battery_measurement = (float)(analogRead(pin_vbat)) / 4095.0*6.7828; #elif BOARD_MODEL == BOARD_HELTEC_T114 @@ -439,6 +458,10 @@ bool init_pmu() { pinMode(pin_ctrl,OUTPUT); digitalWrite(pin_ctrl, HIGH); return true; + #elif BOARD_MODEL == BOARD_HELTEC_TRACKER + pinMode(pin_ctrl,OUTPUT); + digitalWrite(pin_ctrl, HIGH); + return true; #elif BOARD_MODEL == BOARD_HELTEC_T114 pinMode(pin_ctrl,OUTPUT); digitalWrite(pin_ctrl, HIGH); diff --git a/RNode_Firmware.ino b/RNode_Firmware.ino index f06908a..d82170c 100755 --- a/RNode_Firmware.ino +++ b/RNode_Firmware.ino @@ -364,7 +364,7 @@ void setup() { boot_seq(); #endif - #if BOARD_MODEL != BOARD_RAK4631 && BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_TECHO && BOARD_MODEL != BOARD_T3S3 && BOARD_MODEL != BOARD_TBEAM_S_V1 && BOARD_MODEL != BOARD_HELTEC32_V4 && BOARD_MODEL != BOARD_HELTEC32_V3 + #if BOARD_MODEL != BOARD_RAK4631 && BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_TECHO && BOARD_MODEL != BOARD_T3S3 && BOARD_MODEL != BOARD_TBEAM_S_V1 && BOARD_MODEL != BOARD_HELTEC32_V4 && BOARD_MODEL != BOARD_HELTEC32_V3 && BOARD_MODEL != BOARD_HELTEC_TRACKER // Some boards need to wait until the hardware UART is set up before booting // the full firmware. In the case of the RAK4631, Heltec T114, and Heltec V3, // the line below will wait until a serial connection is actually established diff --git a/Utilities.h b/Utilities.h index f141f08..c4ad58b 100755 --- a/Utilities.h +++ b/Utilities.h @@ -324,6 +324,13 @@ extern RNS::Reticulum reticulum; void led_tx_off() { digitalWrite(pin_led_tx, LOW); } void led_id_on() { } void led_id_off() { } + #elif BOARD_MODEL == BOARD_HELTEC_TRACKER + void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } + void led_rx_off() { digitalWrite(pin_led_rx, LOW); } + void led_tx_on() { digitalWrite(pin_led_tx, HIGH); } + void led_tx_off() { digitalWrite(pin_led_tx, LOW); } + void led_id_on() { } + void led_id_off() { } #elif BOARD_MODEL == BOARD_LORA32_V2_1 void led_rx_on() { digitalWrite(pin_led_rx, HIGH); } void led_rx_off() { digitalWrite(pin_led_rx, LOW); } @@ -388,7 +395,7 @@ extern RNS::Reticulum reticulum; // ── Headless LED indicators (for Heltec V4 without OLED) ───────────────── // Uses LEDC PWM for smooth ramp effects on pin_led_tx (GPIO 35) -#if BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC32_V3 +#if BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC_TRACKER #define HEADLESS_LED_CHANNEL 0 bool headless_led_pwm_attached = false; @@ -1723,7 +1730,7 @@ bool eeprom_product_valid() { #if PLATFORM == PLATFORM_AVR if (rval == PRODUCT_RNODE || rval == PRODUCT_HMBRW) { #elif PLATFORM == PLATFORM_ESP32 - if (rval == PRODUCT_RNODE || rval == BOARD_RNODE_NG_20 || rval == BOARD_RNODE_NG_21 || rval == PRODUCT_HMBRW || rval == PRODUCT_TBEAM || rval == PRODUCT_T32_10 || rval == PRODUCT_T32_20 || rval == PRODUCT_T32_21 || rval == PRODUCT_H32_V2 || rval == PRODUCT_H32_V3 || rval == PRODUCT_H32_V4 || rval == PRODUCT_TDECK_V1 || rval == PRODUCT_TBEAM_S_V1 || rval == PRODUCT_XIAO_S3) { + if (rval == PRODUCT_RNODE || rval == BOARD_RNODE_NG_20 || rval == BOARD_RNODE_NG_21 || rval == PRODUCT_HMBRW || rval == PRODUCT_TBEAM || rval == PRODUCT_T32_10 || rval == PRODUCT_T32_20 || rval == PRODUCT_T32_21 || rval == PRODUCT_H32_V2 || rval == PRODUCT_H32_V3 || rval == PRODUCT_H32_V4 || rval == PRODUCT_HELTEC_TRACKER || rval == PRODUCT_TDECK_V1 || rval == PRODUCT_TBEAM_S_V1 || rval == PRODUCT_XIAO_S3) { #elif PLATFORM == PLATFORM_NRF52 if (rval == PRODUCT_RAK4631 || rval == PRODUCT_HELTEC_T114 || rval == PRODUCT_TECHO || rval == PRODUCT_HMBRW) { #else @@ -1773,6 +1780,8 @@ bool eeprom_model_valid() { if (model == MODEL_C5 || model == MODEL_CA) { #elif BOARD_MODEL == BOARD_HELTEC32_V4 if (model == MODEL_C8) { + #elif BOARD_MODEL == BOARD_HELTEC_TRACKER + if (model == MODEL_CB || model == MODEL_CC) { #elif BOARD_MODEL == BOARD_HELTEC_T114 if (model == MODEL_C6 || model == MODEL_C7) { #elif BOARD_MODEL == BOARD_RAK4631 diff --git a/docs/index.html b/docs/index.html index 8908046..49554bf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -312,6 +312,19 @@

RTNode Firmware Flasher

Connect your RTNode via USB, then click Detect to identify the board automatically.

+
+ + +
+

+ The Wireless Tracker v1.1 has PSRAM like the V4 and cannot be auto-detected. + Select it manually before flashing. +

@@ -426,6 +439,13 @@

RTNode Firmware Flasher

updateAddr: 0x10000, fullAddr: 0x0, }, + tracker: { + label: 'Heltec Wireless Tracker v1.1', + updateName: 'rtnode_heltec_tracker.bin', + fullName: 'rtnode_heltec_tracker_merged.bin', + updateAddr: 0x10000, + fullAddr: 0x0, + }, }; function getBinUrl(fw, isUpdate) { @@ -452,6 +472,9 @@

RTNode Firmware Flasher

})(); // ── Detection — mirrors detect_board() in flash.py ───────────────────────── + // NOTE: The Wireless Tracker v1.1 also has PSRAM and appears identical to V4 + // during detection. Users must manually select "Tracker" from the board menu + // if auto-detect picks V4 on a Tracker device. function detectBoard(chipName, features) { if (chipName.includes('ESP32-S3')) { const hasPsram = features.some(f => String(f).toUpperCase().includes('PSRAM')); @@ -461,9 +484,10 @@

RTNode Firmware Flasher

} // ── UI refs ──────────────────────────────────────────────────────────────── - const detectBtn = document.getElementById('detect-btn'); - const flashBtn = document.getElementById('flash-btn'); + const detectBtn = document.getElementById('detect-btn'); + const flashBtn = document.getElementById('flash-btn'); const versionSelect = document.getElementById('version-select'); + const boardOverride = document.getElementById('board-override'); const flashDesc = document.getElementById('flash-desc'); const modeUpdate = document.getElementById('mode-update'); @@ -547,13 +571,14 @@

RTNode Firmware Flasher

const features = await loader.chip.getChipFeatures(loader); const flashSizeKb = await loader.getFlashSize(); const flashSize = flashSizeKb >= 1024 ? (flashSizeKb / 1024) + 'MB' : flashSizeKb + 'KB'; - const boardKey = detectBoard(chipName, features); + const autoKey = detectBoard(chipName, features); + const boardKey = boardOverride.value || autoKey; detectedFw = FIRMWARE[boardKey]; log(`Chip: ${chipName}`); log(`Features: ${features.join(', ')}`); log(`Flash size: ${flashSize}`); - log(`Board: ${detectedFw.label}`); + log(`Board: ${detectedFw.label}${boardOverride.value ? ' (manually selected)' : ' (auto-detected)'}`); setBadge('ok', `✓ ${detectedFw.label} — Flash: ${flashSize} — Ready to flash`); flashBtn.disabled = false; diff --git a/docs/manifest-tracker-full.json b/docs/manifest-tracker-full.json new file mode 100644 index 0000000..b36a434 --- /dev/null +++ b/docs/manifest-tracker-full.json @@ -0,0 +1,14 @@ +{ + "name": "RTNode - Heltec Wireless Tracker v1.1 (Full Install)", + "builds": [ + { + "chipFamily": "ESP32-S3", + "parts": [ + { + "path": "firmware/rtnode_heltec_tracker_merged.bin", + "offset": 0 + } + ] + } + ] +} diff --git a/docs/manifest-tracker.json b/docs/manifest-tracker.json new file mode 100644 index 0000000..9eb80a1 --- /dev/null +++ b/docs/manifest-tracker.json @@ -0,0 +1,14 @@ +{ + "name": "RTNode - Heltec Wireless Tracker v1.1 (Update)", + "builds": [ + { + "chipFamily": "ESP32-S3", + "parts": [ + { + "path": "firmware/rtnode_heltec_tracker.bin", + "offset": 65536 + } + ] + } + ] +} diff --git a/extra_script.py b/extra_script.py index 417b680..f98e533 100755 --- a/extra_script.py +++ b/extra_script.py @@ -121,6 +121,8 @@ def device_provision(env): env.Execute("rnodeconf --product b1 --model b9 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) elif variant in ("heltec32v4", "heltec_v4"): env.Execute("rnodeconf --product b1 --model b9 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) + elif variant in ("heltec_tracker",): + env.Execute("rnodeconf --product c4 --model cc --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) elif variant in ("rak4631", "rak4631_local"): env.Execute("rnodeconf --product 10 --model 12 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) elif variant in ("heltec_t114", "heltec_t114_local"): diff --git a/flash.py b/flash.py index dd27777..0eead5a 100755 --- a/flash.py +++ b/flash.py @@ -110,6 +110,22 @@ }, }, }, + # Tracker is ESP32-S3FN8 (embedded flash, NO PSRAM) — must use DIO flash mode. + # Always select explicitly: python flash.py --board tracker + "tracker": { + "name": "Heltec Wireless Tracker v1.1", + "chip": "ESP32-S3", + "baud_rate": "921600", + "flash_mode": "dio", + "flash_variants": { + "8MB": { + "pio_env": "rtnode_heltec_tracker", + "build_dir": ".pio/build/rtnode_heltec_tracker", + "firmware_bin": "rtnode_heltec_tracker.bin", + "merged_bin": "rtnode_heltec_tracker_merged.bin", + }, + }, + }, } DEFAULT_BOARD = "v4" @@ -1233,8 +1249,8 @@ def main(): Erase flash first, then do a full flash. """, ) - parser.add_argument("--board", choices=["v3", "v4"], default=None, - help="Target board: v3 (Heltec V3) or v4 (Heltec V4). " + parser.add_argument("--board", choices=["v3", "v4", "tracker"], default=None, + help="Target board: v3 (Heltec V3), v4 (Heltec V4), or tracker (Heltec Wireless Tracker v1.1). " "Auto-detected from connected device if omitted.") parser.add_argument("--file", "-f", help="Path to firmware binary to flash") parser.add_argument("--port", "-p", help="Serial port (auto-detected if omitted)") diff --git a/platformio.ini b/platformio.ini index f694ee6..a0a49f0 100755 --- a/platformio.ini +++ b/platformio.ini @@ -353,6 +353,33 @@ lib_deps = ${env.lib_deps} XPowersLib@^0.2.1 +[env:rtnode_heltec_tracker] +platform = espressif32 +board = esp32-s3-devkitc-1 +custom_variant = heltec_tracker +board_build.filesystem = littlefs +; ESP32-S3FN8: 8 MB embedded flash, NO PSRAM — must use DIO like V3 (embedded flash +; does not support QIO; bootloader WDT-crashes if qio is set) +board_upload.flash_size = 8MB +board_upload.maximum_size = 8388608 +board_build.partitions = default_8MB.csv +board_build.flash_mode = dio +monitor_speed = 115200 +build_flags = + ${env.build_flags} + -DBOARD_MODEL=BOARD_HELTEC_TRACKER + -DARDUINO_USB_CDC_ON_BOOT=1 + -DBOUNDARY_MODE + -DRNS_USE_TLSF=1 + -DRNS_USE_ALLOCATOR=1 + -DBOUNDARY_TCP_MODE=0 + -DBOUNDARY_TCP_PORT=4242 +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + adafruit/Adafruit ST7735 and ST7789 Library@^1.10.4 +monitor_filters = esp32_exception_decoder + [env:rtnode_heltec_v4] platform = espressif32 board = esp32-s3-devkitc-1 diff --git a/sx126x.cpp b/sx126x.cpp index 7296fd4..350baa4 100755 --- a/sx126x.cpp +++ b/sx126x.cpp @@ -135,7 +135,7 @@ bool sx126x::preInit() { pinMode(_ss, OUTPUT); digitalWrite(_ss, HIGH); - #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_XIAO_S3 + #if BOARD_MODEL == BOARD_T3S3 || BOARD_MODEL == BOARD_HELTEC32_V3 || BOARD_MODEL == BOARD_HELTEC32_V4 || BOARD_MODEL == BOARD_HELTEC_TRACKER || BOARD_MODEL == BOARD_TDECK || BOARD_MODEL == BOARD_XIAO_S3 SPI.begin(pin_sclk, pin_miso, pin_mosi, pin_cs); #elif BOARD_MODEL == BOARD_TECHO SPI.setPins(pin_miso, pin_sclk, pin_mosi); @@ -280,9 +280,22 @@ void sx126x::setPacketParams(long preamble_symbols, uint8_t headermode, uint8_t buf[4] = crc; buf[5] = 0x00; // standard IQ setting (no inversion) buf[6] = 0x00; // unused params - buf[7] = 0x00; - buf[8] = 0x00; + buf[7] = 0x00; + buf[8] = 0x00; executeOpcode(OP_PACKET_PARAMS_6X, buf, 9); + + // SX1262 errata section 15.4: IQ polarity is inverted compared to + // SX1276. The SetPacketParams command resets register 0x0736 to an + // incorrect default. For standard IQ (no inversion), bit 2 must be + // SET after every SetPacketParams call. For inverted IQ, bit 2 must + // be CLEARED. Without this fix, LoRa RX demodulation fails silently + // while TX continues to work. + uint8_t iqreg = readRegister(0x0736); + if (buf[5] == 0x00) { + writeRegister(0x0736, iqreg | 0x04); + } else { + writeRegister(0x0736, iqreg & ~0x04); + } } void sx126x::reset(void) { @@ -676,6 +689,8 @@ void sx126x::enableTCXO() { uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF}; #elif BOARD_MODEL == BOARD_HELTEC32_V4 uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF}; + #elif BOARD_MODEL == BOARD_HELTEC_TRACKER + uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF}; #endif executeOpcode(OP_DIO3_TCXO_CTRL_6X, buf, 4); #endif