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?