SX126x Heltec LoRa

Hi ESPHome,

Short version
I need help with LORA configuration.

I have for the last 10 months been digging into ESPHome like crazy, I love it that I can easily create my own ESP32 devices that can get data from different sensors (temp, humidity, CO2, binary door, reversible fan), with/without a battery, so many things I can do.

In short ESPHome rocks!

In ESPHome 2025.7.0 SX126x Component was added to the list and my heart jumped into hypermode, I finally would be able to utilize and create my 1-10km away sensors from around the neighborhood.
Quickly bought one more Heltec Lora v3 (had one in storage) and started configuring.
After lots of tinkering, ChatGPG, CoPilot and Gemini I ended up with a working solution that I later improved on and on.

From here on out it will go down in proper acronyms, units and I ask you all for a better knowing and getting things right. I need help.

Sadly the receiver side never goes better then -111.0 dBm with SNR 8.25dB.
My working solution gives me -68.0 dBm on the sender side with SNR 7.75 dB.
But this is when the devices are 10-15cm away from each other.
As soon as I put my receiver where it suppose to be and using the MikroTik 868 Omni antenna.
I have tried with different chips/ESP32 and they all have the same issue.
I have connected the antenna connector all the time (no burnt chip).

My configs

Sender Heltec Lora V3:

substitutions:
  name_s01: esp32-lora-sender-01
  friendly_name: ESP32-Lora-Sender-01

esphome:
  name: ${name_s01}
  name_add_mac_suffix: false
  friendly_name: ${friendly_name}
  on_boot:
    priority: 1000
    then:
      - output.turn_on: vext_power

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf

logger:
  hardware_uart: UART0
  level: debug

api:
  encryption:
    key: !secret esp32-lora-sender-01-encryption-key

ota:
  - platform: esphome
    password: !secret esp32-lora-sender-01-ota-password

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "Esp32-Lora-Sender-01"
    password: !secret esp32-lora-sender-01-ap-password

captive_portal:

spi:
  clk_pin: GPIO9
  mosi_pin: GPIO10
  miso_pin: GPIO11

sx126x:
  cs_pin: GPIO8
  rst_pin: GPIO12
  busy_pin: GPIO13
  dio1_pin: GPIO14
  rf_switch: True
  id: lora
  hw_version: sx1262
  modulation: LORA
  frequency: 868800000 #868800000
  crc_enable: true
  #rx_start: true
  tcxo_voltage: 1_8V
  tcxo_delay: 30ms
  sync_value: [0x14, 0x24]
  spreading_factor: 9         # Higher SF = better range and SNR
  pa_power: 22                 # Max out transmit power
  bandwidth: 125_0kHz          # Narrower bandwidth = better sensitivity
  coding_rate: CR_4_5          # Stronger error correction
  preamble_size: 12
  on_packet:
    then:
      - lambda: |-
          id(rssi_value) = rssi;
          id(snr_value) = snr;
          ESP_LOGD("lambda", "RSSI: %.2f dBm", rssi);
          ESP_LOGD("lambda", "SNR: %.2f dB", snr);

packet_transport:
  platform: sx126x
  # encryption: !secret encryption_key
  # rolling_code_enable: true
  #ping_pong_enable: true
  sensors:
    - esp32_lora_sender_01_temperature
    - esp32_lora_sender_01_humidity
    - esp32_lora_sender_01_rssi
    - esp32_lora_sender_01_snr

i2c:
  # Separate I2C bus for the OLED display
  - id: oled_i2c_bus
    sda: GPIO17
    scl: GPIO18
    scan: True

  # Separate I2C bus for the SHT3XD sensor
  - id: sht3xd_i2c_bus
    sda: GPIO41
    scl: GPIO42
    scan: true

output:
  - platform: gpio
    pin:
      number: GPIO36
      inverted: true
    id: vext_power

sensor:
  - platform: sht3xd
    i2c_id: sht3xd_i2c_bus # Using the dedicated sensor bus
    address: 0x44
    update_interval: 10s
    temperature:
      name: "Temperature"
      id: esp32_lora_sender_01_temperature
      unit_of_measurement: "°C"
      accuracy_decimals: 2
      on_value:
        then:
          - lambda: |-
              ESP_LOGI("LoRa", "Temperature to transmit: %.2f °C", x);
    humidity:
      name: "Humidity"
      id: esp32_lora_sender_01_humidity
      unit_of_measurement: "%"
      accuracy_decimals: 2
      on_value:
        then:
          - lambda: |-
              ESP_LOGI("LoRa", "Humidity to transmit: %.2f %%", x);

  - platform: template
    name: "LoRa RSSI"
    id: esp32_lora_sender_01_rssi
    unit_of_measurement: "dBm"
    accuracy_decimals: 1
    lambda: |-
      return id(rssi_value);

  - platform: template
    name: "LoRa SNR"
    id: esp32_lora_sender_01_snr
    unit_of_measurement: "dB"
    accuracy_decimals: 2
    lambda: |-
      return id(snr_value);

  - platform: adc
    pin: GPIO1
    id: battery_voltage
    update_interval: 30s
    attenuation: 12db
    filters:
      - lambda: |-
          float raw_adc = x;
          float voltage = raw_adc * 4.1;  // Instead of 3.3
          int percent = int((voltage - 2.5) * 100.0 / (4.2 - 2.5));
          percent = std::max(0, std::min(percent, 100));
          id(battery_percent) = percent;

          ESP_LOGI("battery", "Raw ADC: %.4f", raw_adc);
          ESP_LOGI("battery", "Calculated Voltage: %.2f V", voltage);
          ESP_LOGI("battery", "Battery Percent: %d %%", percent);
          return voltage;

button:
  - platform: restart
    name: "Restart device"

font:
  - file: "arial.ttf"
    id: my_font
    size: 11

globals:
  - id: rssi_value
    type: float
    initial_value: '-210.0'
  - id: snr_value
    type: float
    initial_value: '-20.0'
  - id: last_tx
    type: bool
    restore_value: no
    initial_value: "false"
  - id: last_rx
    type: bool
    restore_value: no
    initial_value: "false"
  - id: battery_percent
    type: int
    initial_value: '0'

display:
  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    i2c_id: oled_i2c_bus
    address: 0x3C
    reset_pin: GPIO21
    rotation: 0
    update_interval: 1s
    lambda: |-
      // RSSI / SNR
      it.print(0, 0, id(my_font), "RSSI:");
      it.printf(50, 0, id(my_font), "%.1f dBm", id(rssi_value));

      it.print(0, 11, id(my_font), "SNR:");
      it.printf(50, 11, id(my_font), "%.1f dB", id(snr_value));

      // Temp / Humidity
      it.print(0, 22, id(my_font), "Temp:");
      it.printf(50, 22, id(my_font), "%.1f °C", id(esp32_lora_sender_01_temperature).state);

      it.print(0, 33, id(my_font), "Hum:");
      it.printf(50, 33, id(my_font), "%.1f %%", id(esp32_lora_sender_01_humidity).state);

      it.print(0, 44, id(my_font), "Battery:");
      it.printf(50, 44, id(my_font), "%d %%", id(battery_percent));

Receiver Heltec Lora V4 (I’ve tried V3 also, same result):

substitutions:
  name_r02: esp32-lora-receiver-05
  friendly_name: ESP32 Lora Receiver 05

esphome:
  name: ${name_r02}
  name_add_mac_suffix: false
  friendly_name: ${friendly_name}
  on_boot:
    priority: 1000
    then:
      - output.turn_on: vext_power
      - delay: 500ms

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf

# Enable Home Assistant API
api:
  encryption:
    key: !secret esp32-lora-receiver-05-encryption-key

ota:
  - platform: esphome
    password: !secret esp32-lora-receiver-05-ota-password

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Esp32-Lora-Receiver-01"
    password: !secret esp32-lora-receiver-05-ap-password

logger:
  hardware_uart: UART0
  level: debug

captive_portal:

# SPI for SX126x LoRa
spi:
  clk_pin: GPIO9
  mosi_pin: GPIO10
  miso_pin: GPIO11

sx126x:
  cs_pin: GPIO8
  rst_pin: GPIO12
  busy_pin: GPIO13
  dio1_pin: GPIO14
  rf_switch: true
  rx_start: true
  id: lora
  hw_version: sx1262
  modulation: LORA
  frequency: 868800000
  crc_enable: true
  tcxo_voltage: 1_8V
  tcxo_delay: 30ms
  sync_value: [0x14, 0x24]
  spreading_factor: 9         # Higher SF = better range and SNR
  pa_power: 22                 # Max out transmit power
  bandwidth: 125_0kHz          # Narrower bandwidth = better sensitivity
  coding_rate: CR_4_5          # Stronger error correction
  preamble_size: 12
  on_packet:
    then:
      - lambda: |-
          id(rssi_value) = rssi;
          id(snr_value) = snr;
          id(lora_rssi_sensor).update();
          id(lora_snr_sensor).update();
          id(last_packet_time) = (int)(millis() / 1000);  // record packet time
          ESP_LOGI("LoRa", "RSSI: %.2f dBm", rssi);
          ESP_LOGI("LoRa", "SNR: %.2f dB", snr);
#            ESP_LOGI("LoRa", "Raw hex: %s", format_hex(x).c_str());

packet_transport:
  platform: sx126x
  update_interval: 10s
  providers:
    - name: esp32-lora-sender-01
      # encryption: !secret encryption_key  # if you use encryption
      # ping_pong_enable: true
      # rolling_code_enable: true

sensor:
  - platform: packet_transport
    provider: esp32-lora-sender-01
    name: "Temperature"
    id: esp32_lora_sender_01_temperature
    internal: false
    unit_of_measurement: "°C"
    accuracy_decimals: 2
    filters:
      - heartbeat: 30s
    on_value:
      then:
        - lambda: |-
            ESP_LOGI("LoRaSensor", "📡 New temperature value received!");
            ESP_LOGI("LoRaSensor", "Remote ID: esp32_lora_sender_01_temperature");
            ESP_LOGI("LoRaSensor", "Temperature: %.2f °C", x);

  - platform: packet_transport
    provider: esp32-lora-sender-01
    name: "Humidity"
    id: esp32_lora_sender_01_humidity
    internal: false
    unit_of_measurement: "%"
    accuracy_decimals: 2

  - platform: packet_transport
    provider: esp32-lora-sender-01
    name: "Sender RSSI"
    id: esp32_lora_sender_01_rssi
    internal: false
    unit_of_measurement: "dBm"
    accuracy_decimals: 1

  - platform: packet_transport
    provider: esp32-lora-sender-01
    name: "Sender SNR"
    id: esp32_lora_sender_01_snr
    internal: false
    unit_of_measurement: "dB"
    accuracy_decimals: 2

  - platform: template
    name: "LoRa RSSI"
    id: lora_rssi_sensor
    unit_of_measurement: "dBm"
    accuracy_decimals: 1
    update_interval: never
    lambda: |-
      return id(rssi_value);

  - platform: template
    name: "LoRa SNR"
    id: lora_snr_sensor
    unit_of_measurement: "dB"
    accuracy_decimals: 2
    update_interval: never
    lambda: |-
      return id(snr_value);

button:
  - platform: restart
    name: "Restart device"

output:
  - platform: gpio
    pin:
      number: GPIO36
      inverted: false
    id: vext_power


globals:
  - id: rssi_value
    type: float
    initial_value: '-200.0'
  - id: snr_value
    type: float
    initial_value: '-10.0'
  - id: last_packet_time
    type: int
    initial_value: '0'

interval:
  - interval: 10s
    then:
      - lambda: |-
          // Get current uptime in seconds
          int now_sec = (int)(millis() / 1000);
          // If more than 30 seconds since last packet, force RX
          if (now_sec - id(last_packet_time) > 30) {
            ESP_LOGW("LoRa", "No packet in >30s, forcing RX mode");
            id(lora).set_mode_rx();
          }
          
i2c:
  - id: oled_i2c_bus
    sda: GPIO17
    scl: GPIO18
    scan: true

font:
  - file: "arial.ttf"
    id: my_font
    size: 10

display:
  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    i2c_id: oled_i2c_bus
    reset_pin: GPIO21
    address: 0x3C
    rotation: 0
    update_interval: 1s
    lambda: |-
      it.printf(0, 0, id(my_font), "RSSI: %.1f dBm", id(rssi_value));
      it.printf(0, 12, id(my_font), "SNR:  %.1f dB", id(snr_value));
      it.printf(0, 24, id(my_font), "Temp: %.1f C", id(esp32_lora_sender_01_temperature).state);
      it.printf(0, 36, id(my_font), "Hum:  %.1f %%", id(esp32_lora_sender_01_humidity).state);
      it.printf(0, 48, id(my_font), "Last RX: %ds ago", 
                (int)((millis()/1000) - id(last_packet_time)));

I’m sure you’ll find a lot of things that can be tweaked in a different way, but for now my issue after live testing and walking away from where the receiver is I get maybe 100 meter distance of receiving/sending.

Anyone up for the challenge and can tell me where I have configured my LORA/sx1262 wrong?

Try this in Heltec v4 reciver:

 on_boot:
    - priority: 1000
      then:
        - lambda: |-
            pinMode(2, OUTPUT);
            digitalWrite(2, HIGH);     // FEM_EN
            pinMode(46, OUTPUT);
            digitalWrite(46, HIGH);    // FEM_PA_TX
            delay(50);
#for display
            pinMode(36, OUTPUT);
            digitalWrite(36, LOW);
            delay(100);

Hello community,

Same hardware and same problem:

  • 2x Heltec LORA WIFI V4
  • Heltec original antennas tested or 868Mhz 10DBi antenna or LilyGo 868Mhz
  • Point-to-point communication
  • espHome firmware
  • Identical LORA settings on both cards:
  • GPIO mapping in accordance with Heltc documentation.

It is impossible to go beyond a few metres.
I admit I’m a bit desperate, and I thought I wasn’t the only one in this situation, but I can’t find anything else other than this thread that discusses the same problem.

Thank you in advance for any useful ideas or information.

Config:

esphome:
  name: device2
  friendly_name: device2
  on_boot:
    priority: 1000
    then:
      - output.turn_on: fem_en
      - output.turn_on: v_fem
      - output.turn_off: fem_pa
      - delay: 50ms

esp32:
  variant: esp32s3
  flash_size: 8MB
  cpu_frequency: 240MHz
  framework:
    type: esp-idf

output:
  - platform: gpio
    id: fem_en
    pin: GPIO2

  - platform: gpio
    id: v_fem
    pin: GPIO7

  - platform: gpio
    id: fem_pa
    pin: GPIO46

spi:
  clk_pin: GPIO9
  mosi_pin: GPIO10
  miso_pin: GPIO11

sx126x:
  dio1_pin: GPIO14
  cs_pin: GPIO8
  busy_pin: GPIO13
  rst_pin: GPIO12

  hw_version: sx1262  #sx1261 as detected is not working
  rf_switch: true
  pa_power: 20  # 13 work well with Lyligo T3S3 LORA
  frequency: 868300000
  modulation: LORA
  bandwidth: 250_0kHz   #Tryed 125, 250 without significant changes
  spreading_factor: 11   #Tryed 8,11,12
  coding_rate: CR_4_5
  preamble_size: 8  #idem, with differents values
  crc_enable: true
  sync_value: [0x14, 0x24]
  tcxo_voltage: 1_8V
  tcxo_delay: 30ms
  rx_start: true

script:
  - id: sx_send_raw
    parameters:
      payload: std::vector<uint8_t>
    then:
      **- output.turn_on: fem_pa**
      **- delay: 2ms**
      - sx126x.send_packet:
          data: !lambda |-
            return payload;
      **- delay: 2ms**
      **- output.turn_off: fem_pa**

LOG:

[01:32:52.832][C][sx126x:478]: SX126x:
[01:32:52.835][C][sx126x:479]:   CS Pin: GPIO8
[01:32:52.836][C][sx126x:480]:   BUSY Pin: GPIO13
[01:32:52.839][C][sx126x:481]:   RST Pin: GPIO12
[01:32:52.841][C][sx126x:482]:   DIO1 Pin: GPIO14
[01:32:52.845][C][sx126x:483]:   **HW Version: SX1261 V2D 2D02**
[01:32:52.845][C][sx126x:483]:   Frequency: 868300000 Hz
[01:32:52.845][C][sx126x:483]:   Bandwidth: 250000 Hz
[01:32:52.845][C][sx126x:483]:   PA Power: 20 dBm
[01:32:52.845][C][sx126x:483]:   PA Ramp: 40 us
[01:32:52.845][C][sx126x:483]:   Payload Length: 0
[01:32:52.845][C][sx126x:483]:   CRC Enable: TRUE
[01:32:52.845][C][sx126x:483]:   Rx Start: TRUE
[01:32:52.860][C][sx126x:522]:   Modulation: LORA
[01:32:52.860][C][sx126x:522]:   Spreading Factor: 11
[01:32:52.860][C][sx126x:522]:   Coding Rate: 4/5
[01:32:52.860][C][sx126x:522]:   Preamble Size: 8
[01:32:52.861][C][sx126x:530]:   Sync Value: 0x1424

[01:34:45.253][D][button:023]: 'LoRa: Send Ping Now' Pressed.
[01:34:45.527][D][sensor:133]: 'LoRa TX Count': Sending state 381.00000  with 0 decimals of accuracy
[01:34:45.842][D][lora:132]: RX len=13 **rssi=-111.0 snr=-19.50** HEX=50 4F 4E 47 3A 67 61 72 61 67 65 3A 34  ASCII='PONG:device2:4'
[01:34:45.842][D][sensor:133]: 'LoRa RSSI Main': Sending state -111.00000 dBm with 1 decimals of accuracy
[01:34:45.846][D][sensor:133]: 'LoRa SNR Main': Sending state -19.50000 dB with 1 decimals of accuracy
[01:34:45.850][D][sensor:133]: 'LoRa RX Count': Sending state 185.00000  with 0 decimals of accuracy
[01:34:45.851][D][text_sensor:087]: 'LoRa Payload (brut)': Sending state 'PONG:device2:4'
[01:34:45.854][D][sensor:133]: 'LoRa RTT device2': Sending state 598.00000 ms with 0 decimals of accuracy

Yep !
RSSI and SNR finally at expected values.
Not to mention that there are probably still some areas for improvement.
But if it helps, here’s what I did:

Not forgetting, for their courage and willingness to share their expertise, my sincere gratitude to all those who have been involved, directly or indirectly, in the development of HomeAssistant, espHome and this grand idea of implementing LORA support !

# HELTEC WIFI LORA V4
output:
  - platform: gpio
    pin: GPIO2
    id: fem_csd
  - platform: gpio
    pin: GPIO7
    id: vfem_pwr
  - platform: gpio
    pin: GPIO46
    id: fem_mode

esphome:
  name: XYZ
  friendly_name: device XYZ
  on_boot:
    priority: 600
    then:
      - output.turn_on: fem_csd     # SET HIGH
      - output.turn_on: vfem_pwr    # SET HIGH
      - output.turn_on: fem_mode    # SET HIGH (Mode Gain ON)
      
esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf

# Enable logging
logger:

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  power_save_mode: none
  manual_ip:
    static_ip: W.X.Y.Z
    subnet: 255.255.255.0
    gateway: 0.0.0.0
    dns1: 0.0.0.0

api:
  encryption:
    key: !secret api_key_XYZ

ota:
  platform: esphome
  password: !secret ota_XYZ

# SPI bus for LoRa radio
spi:
  clk_pin: GPIO9
  mosi_pin: GPIO10
  miso_pin: GPIO11

# LoRa Radio Configuration (SX1262)
sx126x:
  dio1_pin: GPIO14
  cs_pin: GPIO8
  busy_pin: GPIO13
  rst_pin: GPIO12
  pa_power: 17
  frequency: 868100000
  bandwidth: 125_0kHz
  crc_enable: true
  modulation: LORA
  rx_start: true
  hw_version: sx1262       # And not sx1261 as shown in the log ;-)
  rf_switch: true
  preamble_size: 8
  spreading_factor: 9
  coding_rate: CR_4_5
  tcxo_voltage: 1_8V
  tcxo_delay: 15ms         # At least 5ms

Result:

Now:
[01:40:18.702][D][sensor:135]: 'LORA SNR XYZ': Sending state 13.75000 dB with 1 decimals of accuracy
[01:40:18.712][D][sensor:135]: 'LORA RSSI XYZ': Sending state -47.00000 dBm with 1 decimals of accuracy

Previously:
[01:34:45.842][D][sensor:133]: 'LoRa RSSI Main': Sending state -111.00000 dBm with 1 decimals of accuracy
[01:34:45.846][D][sensor:133]: 'LoRa SNR Main': Sending state -19.50000 dB with 1 decimals of accuracy