ESPHome Heltec HTIT Tracker (TFT, GNSS, LoRa)

I recently got a Heltec HTIT Tracker based on ESP32S3 with a 0.96 160x80 TFT, multi-system GNSS and LoRa
https://docs.heltec.org/en/node/esp32/wireless_tracker/index.html
I was astonished that there was no ready-made ESPHome yaml around, so I started nearly from scratch with this device.

Because I took several loooooong evenings to get this done, I thought why not share here for others :slight_smile:

Most trickiest for me was the TFT display. It does not suffice to define the display model st7735 with spi. Looking up schematics I found that also 2 GPIOs are needed to turn the TFT on.

The gps part using uart was straight forward.
I have no interest in LoRa so this is not available in following esphome yaml.

The display shows GNSS data
20240629_163533

Full yaml config:

substitutions:
  name: "heltec-htit-tracker"
  friendly_name: ESPHome Heltec HTIT Tracker

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  name_add_mac_suffix: false
  platformio_options:
    board_build.flash_mode: dio
  project:
    name: esphome.web
    version: '1.0'

esp32: # Heltec HTIT Tracker v1.1 = ESP32-S3FN8
  board: esp32-s3-devkitc-1
  framework:
    type: arduino # gps requires arduino
    #type: esp-idf # recommended for variants of the ESP32 like this ESP32S3
    #sdkconfig_options:
    #  COMPILER_OPTIMIZATION_SIZE: y
    #  COMPILER_OPTIMIZATION_PERFORMANCE: y

# Enable logging
logger:
  level: DEBUG
  logs:
    component: ERROR
    ili9xxx: INFO
    uart_debug: DEBUG
    gps: DEBUG

# Enable Home Assistant API
api:

# Allow Over-The-Air updates
ota:
  - platform: esphome

# Allow provisioning Wi-Fi via serial
improv_serial:

wifi:
  networks:
  - ssid: !secret wifi_ssid
    password: !secret wifi_password

  # Set up a wifi access point
  # ap: {}

# In combination with the `ap` this allows the user
# to provision wifi credentials to the device via WiFi AP.
# captive_portal:

dashboard_import:
  package_import_url: github://esphome/firmware/esphome-web/esp32s3.yaml@v2
  import_full_config: true

# Sets up Bluetooth LE (Only on ESP32) to allow the user
# to provision wifi credentials to the device.
esp32_improv:
  authorizer: none

# To have a "next url" for improv serial
web_server:

#############################################################
################## TFT display ##############################

font:
  - file: "gfonts://Roboto Mono" # https://fonts.google.com/specimen/Roboto+Mono
    id: my_font
    size: 20
color:
  - id: white
    red: 100%
    green: 100%
    blue: 100%
  - id: red
    red: 100%
    green: 0%
    blue: 0%
  - id: yellow
    red: 100%
    green: 100%
    blue: 0%

# see https://esphome.io/components/display/ili9xxx#ili9xxx
display:
  - platform: ili9xxx
    model: st7735
    id: st7735
    # from HT_st7735.h [1]
    # #define ST7735_DC_Pin    40 // schematics:   MTDO --- RS
    dc_pin: GPIO40
    # #define ST7735_REST_Pin  39 // schematics:   MTCK --- RES
    reset_pin: GPIO39
    # #define ST7735_CS_Pin    38 // schematics: GPIO38 --- CS
    cs_pin: GPIO38
    #define ST7735_IS_160X80 1
    #define ST7735_XSTART 1
    #define ST7735_YSTART 26
    #define ST7735_WIDTH  160
    #define ST7735_HEIGHT 80
    #define ST7735_ROTATION (ST7735_MADCTL_MY | ST7735_MADCTL_MV | ST7735_MADCTL_BGR)
    color_order: bgr # corresponds to ST7735_MADCTL_BGR
    invert_colors: true # usually you want text on black background
    dimensions:
      width: 160
      height: 80
      offset_width: 1
      offset_height: 26
    transform:
      swap_xy: true   # corresponds to ST7735_MADCTL_MV
      mirror_y: true  # corresponds to ST7735_MADCTL_MY
      mirror_x: false
    data_rate: 40MHz
    auto_clear_enabled: true # else text would draw over old text
    lambda: |-
      // Display the string "Hello World!" at [0,10]
      //it.print(0, 10, id(my_font), "Hello World!");
      // Display some GNSS values
      it.printf(0,  0, id(my_font), id(white), "Sats %.0f  ", id(htit_gnss_satellites).state);
      it.printf(0, 20, id(my_font), id(white), " LAT %.2f °", id(htit_gnss_latitude).state);
      it.printf(0, 40, id(my_font), id(white), "LONG %.2f °", id(htit_gnss_longitude).state);
      it.printf(0, 60, id(my_font), id(white), " ALT %.1f m", id(htit_gnss_altitude).state);

# display st7735 requires spi
spi:
  - id: spi_st7735
    # from HT_st7735.h [1]
    # #define ST7735_SCLK_Pin 41 // schematics: MTDI --- SCLK --- SCL
    clk_pin: GPIO41
    # #define ST7735_MOSI_Pin 42 // schematics: MTMS --- SDIN --- SDA
    mosi_pin: GPIO42
    #miso_pin: GPIOxx // not needed

# display st7735 requires 2 GPIOs set to HIGH to turn on TFT
switch:
  # from HT_st7735.h [1]
  # #define ST7735_LED_K_Pin 21
  - platform: gpio
    pin: GPIO21
    id: led_k_pin
    restore_mode: ALWAYS_ON
  # #define ST7735_VTFT_CTRL_Pin  3
  - platform: gpio
    pin: GPIO3 # it's safe to ignore_strapping_warning
    id: vtft_ctrl_pin # schematics: Vext_Ctrl
    restore_mode: ALWAYS_ON

######################################################
################## GNSS ##############################

uart: # GNSS requires Vext_Ctrl to be HIGH
  id: uart_gnss
  # it's safe to ignore: WARNING GPIO33/34 is used by the PSRAM interface on ESP32-S3R8 / ESP32-S3R8V and should be avoided on these models
  tx_pin: GPIO34 # schematics_ GPIO34 --- GNSS_RX --- RX (tx_pin of ESP = RX of GNSS chip)
  rx_pin: GPIO33 # schematics_ GPIO33 --- GNSS_TX --- TX (rx_pin of ESP = TX of GNSS chip)
  baud_rate: 115200
  debug:
    direction: BOTH
    dummy_receiver: false # true if no UART device component (=gps) is configured for the UART bus (yet)
    after:
      delimiter: "\n"
    sequence:
      - lambda: UARTDebug::log_string(direction, bytes);

gps:
  latitude:
    name: "Latitude"
    id: htit_gnss_latitude
  longitude:
    name: "Longitude"
    id: htit_gnss_longitude
  altitude:
    name: "Altitude"
    id: htit_gnss_altitude
  speed:
    name: "Speed"
    id: htit_gnss_speed
  course:
    name: "Course"
    id: htit_gnss_course
  satellites:
    name: "Satellites"
    id: htit_gnss_satellites
time:
  - platform: gps
    id: gnss_time

# Some helper functions to restart ESPHome from HA
button:
- platform: restart
  name: HTIT Restart
- platform: safe_mode
  name: HTIT Safe Mode Boot

######################################################
################## References ########################
# [1] https://github.com/HelTecAutomation/Heltec_ESP32/blob/35c3adf9261fa005714ab6227a02a0e1171d3a7f/src/HT_st7735.h
3 Likes

Thank you for your sharing. I have the same tracker and I tried a lot of things with Arduino IDE. But your solution with ESP Home look better :+1:

1 Like

The SX1262 is now also supported by ESPHome: SX126x Component — ESPHome
currently in beta. i’m trying now to get LoRa running

Listening…I have interest in LoRa as well. Thanks!

@drirwin what does this?

esphome:
  platformio_options:
    board_build.flash_mode: dio

here is my config i currently have. still gets some more updates the next days:

# Documentation
# https://heltec.org/project/wireless-tracker/
# https://docs.heltec.org/en/node/esp32/wireless_tracker/index.html
# https://resource.heltec.cn/download/Wireless_Tracker/Wireless%20Tracker1.1.pdf
# https://resource.heltec.cn/download/Wireless_Tracker/Wireless_Tacker1.1/HTIT-Tracker_V0.5.pdf

# Datasheets
# USB-UART (CP2102): https://www.silabs.com/documents/public/data-sheets/CP2102-9.pdf
# Battery Charger (TP4054): https://www.laskakit.cz/user/related_files/tp4054.pdf
# Microcontroller (ESP32-S3FN8): 
# Display (ST7735): https://www.displayfuture.com/Display/datasheet/controller/ST7735.pdf
# LoRa (SX1262): https://semtech.my.salesforce.com/sfc/p/#E0000000JelG/a/RQ000008nKCH/hp2iKwMDKWl34g1D3LBf_zC7TGBRIo2ff5LMnS8r19s
# GNSS (UC6580): https://pt.unicore.com/uploads/file/uc6580-datasheet-en-r1-1.pdf

substitutions:
  name: "heltec-htit-tracker"
  friendly_name: "Heltec HTIT Tracker"

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  #platformio_options:
  #  board_build.flash_mode: dio

esp32:
  board: esp32-s3-devkitc-1
  framework:
    #type: arduino
    type: esp-idf # GPS does NOT support ESP-IDF yet
    version: 5.4.2
    platform_version: 54.03.21

# Enable logging
logger:
  #baud_rate: 0
#  level: VERY_VERBOSE

# Enable Home Assistant API
api:
  encryption:
    key: "RM7K+/od/u4DwpYI/2uIAGjyxoteIbjMwFJSLsoF4Ck="

ota:
  - platform: esphome
    password: "a07f0ecafb9903e33bc48562d36e3616"

wifi:
  #power_save_mode: HIGH
  #fast_connect: true

  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Heltec Fallback Hotspot"
    password: "7oMJPdiHIgb9"

captive_portal:

external_components:
  - source: github://pr#9021
    components: [adc]
    refresh: 0s
  - source: github://pr#9728 # GPS works with ESP-IDF
    components: [gps]
    refresh: 0s

# https://esphome.io/components/time/
time:
  # https://esphome.io/components/time/homeassistant
  - platform: homeassistant
    id: homeassistant_time
  # https://esphome.io/components/time/gps
  - platform: gps
    id: gnss_time

# https://esphome.io/components/deep_sleep
#deep_sleep:
#  id: deep_sleep_id
#  run_duration: 30s
#  sleep_duration: 14.5min

# Light
# https://esphome.io/components/output/
output:
  # https://esphome.io/components/output/gpio
  - platform: gpio
    id: light_output
    pin: GPIO18 # LED
  - platform: gpio
    id: gnss_rst_output
    pin: GPIO35 # GNSS_RST
    inverted: true

# Light
# https://esphome.io/components/light/
light:
  # https://esphome.io/components/light/binary
  - platform: binary
    id: led
    name: "LED"
    output: light_output

# https://esphome.io/components/sensor/
sensor:
  # https://esphome.io/components/sensor/uptime
  - platform: uptime
    id: last_boot
    type: timestamp
    name: "Last Boot (Uptime)"
  # BAT Voltage
  # https://esphome.io/components/sensor/adc
  - platform: adc
    id: bat_voltage
    name: "BAT Voltage"
    pin: GPIO1 # ADC_IN
    attenuation: 12db
    accuracy_decimals: 3
    samples: 3
    update_interval: 1min
    filters:
      - multiply: 4.8837

# Display
# https://esphome.io/components/font
font:
  - file: "gfonts://Roboto Mono" # https://fonts.google.com/specimen/Roboto+Mono
    id: my_roboto
    size: 16

# Display
# https://esphome.io/components/display/#color
color:
  - id: white
    red: 100%
    green: 100%
    blue: 100%
  - id: red
    red: 100%
    green: 0%
    blue: 0%
  - id: green
    red: 00%
    green: 100%
    blue: 0%
  - id: blue
    red: 0%
    green: 0%
    blue: 100%

# https://esphome.io/components/spi
spi:
  # Display
  - id: spi_st7735
    clk_pin: GPIO41 # SCLK
    mosi_pin: GPIO42 # SDIN
    #miso_pin: GPIOxx // not needed
  # LoRa
  - id: spi_sx126x
    clk_pin: GPIO9 # LoRa_SCK
    mosi_pin: GPIO10 # LoRa_MOSI
    miso_pin: GPIO11 # LoRa_MISO

# Display
# https://esphome.io/components/display/
display:
  # https://esphome.io/components/display/ili9xxx
  - platform: ili9xxx
    spi_id: spi_st7735
    model: st7735
    id: st7735
    cs_pin: GPIO38 # CS
    reset_pin: GPIO39 # RES
    dc_pin: GPIO40 # RS
    #define ST7735_ROTATION (ST7735_MADCTL_MY | ST7735_MADCTL_MV | ST7735_MADCTL_BGR)
    color_order: bgr # corresponds to ST7735_MADCTL_BGR
    invert_colors: true # usually you want text on black background
    dimensions:
      width: 160
      height: 80
      offset_width: 1
      offset_height: 26
    transform:
      swap_xy: true   # corresponds to ST7735_MADCTL_MV
      mirror_y: true  # corresponds to ST7735_MADCTL_MY
      mirror_x: false
    data_rate: 40MHz
    auto_clear_enabled: true # else text would draw over old text
    lambda: |-
      // Display the string "Hello World!" at [0,10]
      //it.print(0, 10, id(my_roboto), "Hello World!");
      // Display some GNSS values
      it.printf(0,  0, id(my_roboto), id(red), "Sats %.0f  ", id(htit_gnss_satellites).state);
      it.printf(0, 20, id(my_roboto), id(green), "LA %.4f °", id(htit_gnss_latitude).state);
      it.printf(0, 40, id(my_roboto), id(blue), "LO %.4f °", id(htit_gnss_longitude).state);
      it.printf(0, 60, id(my_roboto), id(white), "H %.1f m", id(htit_gnss_altitude).state);

# display st7735 requires 2 GPIOs set to HIGH to turn on TFT
# https://esphome.io/components/switch/
switch:
  # Power
  # https://esphome.io/components/switch/gpio
  - platform: gpio
    id: power
    name: "Power (Display & GNSS)"
    restore_mode: ALWAYS_ON
    pin:
      number: GPIO3 # Vext_Ctrl # it's safe to ignore_strapping_warning
      mode: #OUTPUT
        output: true
        pulldown: true
  # Display Status
  # https://esphome.io/components/switch/gpio
  - platform: gpio
    id: display_status
    name: "Display Status"
    restore_mode: RESTORE_DEFAULT_ON
    pin:
      number: GPIO21 # LED_K
      mode: OUTPUT
  # Charge Control
  # https://esphome.io/components/switch/gpio
  - platform: gpio
    id: charge_control
    name: "Charge Control"
    restore_mode: RESTORE_DEFAULT_ON
    pin:
      number: GPIO2 # ADC_Ctrl
      mode: OUTPUT
        #output: true
        #pulldown: true

# LoRa
# https://esphome.io/components/sx126x
sx126x:
  spi_id: spi_sx126x
  hw_version: sx1262
  cs_pin: GPIO8 # LoRa_NSS
  rst_pin: GPIO12 # LoRa_RST
  busy_pin: GPIO13 # LoRa_BUSY
  dio1_pin: GPIO14 # DIO1
  frequency: 863000000
  modulation: LORA
  rf_switch: true
  # Optional
  pa_power: 3 # -3 - 22 (Default: 17)
  #pa_ramp: 40us # 10us, 20us, 40us, 80us, 200us, 800us, 1700us, 3400us (Default: 40us) # TXRMP
  #rx_start: true # Default: true
  #tcxo_delay: 5ms # (Default: 5ms)
  tcxo_voltage: 1_7V # NONE (Default), 1_6V, 1_7V (Best), 1_8V, 2_2V, 2_4V, 2_7V, 3_0V, 3_3V
  # LoRa only
  #bandwidth: 125_0kHz # 7_8kHz, 10_4kHz, 15_6kHz, 20_8kHz, 31_3kHz, 41_7kHz, 62_5kHz, 125_0kHz (Default), 250_0kHz, 500_0kHz
  #payload_length: 0 # 0 - 256 (Default: 0) (spreading_factor=6 => >0)
  crc_enable: true # Default: false
  #preamble_size: 8 # Minimum: 6 (Default: 8)
  #spreading_factor: 7 # 6 - 12 (Default: 7)
  #coding_rate: CR_4_5 # CR_4_5 (Default), CR_4_6, CR_4_7, CR_4_8
  #sync_value: [0x14, 0x24] # Private (Default): [0x14, 0x24] / LoRaWAN: [0x34, 0x44]
  on_packet:
    then:
      - lambda: |-
          ESP_LOGD("lambda", "packet %s", format_hex(x).c_str());
          ESP_LOGD("lambda", "rssi %.2f", rssi);
          ESP_LOGD("lambda", "snr %.2f", snr);

# GNSS
# https://esphome.io/components/uart
uart: # GNSS requires Vext_Ctrl to be HIGH
  id: uart_gnss
  # it's safe to ignore: WARNING GPIO33/34 is used by the PSRAM interface on ESP32-S3R8 / ESP32-S3R8V and should be avoided on these models
  tx_pin: GPIO34 # GNSS_RX
  rx_pin: GPIO33 # GNSS_TX
  baud_rate: 115200
#  debug:
#    direction: RX #BOTH
#    #dummy_receiver: false # true if no UART device component (=gps) is configured for the UART bus (yet)
#    after:
#      #bytes: 150
#      #timeout: 100ms
#      delimiter: "\r\n"
#    sequence:
#      - lambda: UARTDebug::log_string(direction, bytes);

# GNSS
# https://esphome.io/components/gps
gps:
  latitude:
    name: "Latitude"
    id: htit_gnss_latitude
  longitude:
    name: "Longitude"
    id: htit_gnss_longitude
  altitude:
    name: "Altitude"
    id: htit_gnss_altitude
  speed:
    name: "Speed"
    id: htit_gnss_speed
  course:
    name: "Course"
    id: htit_gnss_course
  satellites:
    name: "Satellites"
    id: htit_gnss_satellites
  hdop:
    name: "HDOP"
    id: htit_gnss_hdop
  update_interval: 5s

# https://esphome.io/components/button/
button:
  # https://esphome.io/components/button/restart
  - platform: restart
    name: "Restart"
    # CHIP_PU/EN / RST Button
  # https://esphome.io/components/button/shutdown
  - platform: shutdown
    name: "Shutdown"
  # https://esphome.io/components/button/safe_mode
  - platform: safe_mode
    name: "Safe Mode Boot"
  # https://esphome.io/components/button/output
  - platform: output
    id: reset_gnss
    name: "Reset GNSS"
    output: gnss_rst_output
    duration: 1ms
    entity_category: config
  # https://esphome.io/components/button/template
  # For testing only
  - platform: template
    name: "Transmit Packet"
    on_press:
      then:
        - logger.log: "Tansmitting..."
        - sx126x.send_packet:
            data: [0xC5, 0x51, 0x78, 0x82, 0xB7, 0xF9, 0x9C, 0x5C]
        - logger.log: "Tansmitted"

# https://esphome.io/components/binary_sensor/
binary_sensor:
  # https://esphome.io/components/binary_sensor/gpio
  - platform: gpio
    id: user_button
    name: "USER Button"
    pin:
      number: GPIO0 # USER_Key
      mode: INPUT_PULLUP
      inverted: true
    on_press:
      then:
        - logger.log: "USER Button wurde gedrückt"

# https://esphome.io/components/packet_transport/
packet_transport:
  - platform: sx126x
    update_interval: 5min # Default: 15sec
    ping_pong_enable: true
    #ping_pong_recycle_time: 10min
    rolling_code_enable: true
    encryption: !secret lora_encryption_key
    providers: 
      - name: "lora"
        encryption: !secret lora_encryption_key
    sensors:
      - htit_gnss_latitude
      - htit_gnss_longitude
      - htit_gnss_altitude
      - htit_gnss_speed
      - htit_gnss_course
      - htit_gnss_satellites
      - htit_gnss_hdop
      - id: bat_voltage
        broadcast_id: remote_bat_voltage