HELTEC LoRA V4 issues

Hello,

I’m using Heltec V3 (V3.2) modules to remotely control devices.
Everything works perfectly.

I was thinking of upgrading to V4 modules to increase the range.

Indeed, version 4 modules are known for providing more power, and consequently, a greater range.

However, I’m still hesitant about these V4 modules. Let me explain:

I should mention that my tests are done on my desk.

The transmitter is a maximum of 2 meters from the receivers.

So,

  • One V3 module emits a repetitive signal at its maximum power.

  • Opposite it, I have two receivers, one V3 and one V4, with exactly the same code.

The V3 displays an RSSI of approximately 33dBm, while the V4 displays -96dBm!

The V3 is 10 cm from the V4. Same code, same antenna, same power supply… I’m testing another V4 module. Same result.

Strange, isn’t it? How can such a difference be explained?

I’m going a little further with my test. This time, I’m using one of the two V4 modules as a transmitter.

For reception, I’m not changing anything; I’m still using a V3 and a V4. Identical results!

Both V4 modules I have are working.

So, I’m wondering if this commercially available version 4 is really reliable.
I have the impression that it’s displaying incorrect data.

Has anyone else noticed reception problems with such a poor RSSI at short range?

Sincerely,
Eric.

What version f/w are you using in the v4 module?
The Heltec v4’s must be running 1.15.0.

Also 33 dBm isn’t possible as that’s 2 Watts, and no receiver can accept 2 W without melting!

Hi,
I don’t use Meshtastic, but my own sketch.
I ran another test this morning. The transmitter is a V4.2.

Results:

  • V3.2 -26dBm
  • V4.2 -62dBm

Same power supply, same antenna, same code (except for the pin numbers).

See the photo. The V4 is on the left, the V3 on the right.

The black box you can just make out below is the transmitter. It’s located less than 20 cm from both receivers.

-26dBm, I agree with you, that seems impossible. And yet…

Could the problem be with the “RadioLib” library?

I’ve attached my sketch. I should mention that I’m using both cores of the ESP.
Core 0 is dedicated to LoRa only.

Summary

#include <RadioLib.h>
#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
#include <U8g2lib.h>

// — ADAPTATION HELTEC V3.2 —
#define OLED_SDA 17
#define OLED_SCL 18
#define OLED_RST 21 // Parfois inutilisé sur V3 mais défini pour la lib
#define VEXT_CTRL 36

// Pinout LoRa Heltec V3 : NSS: 8, DIO1: 14, NRST: 12, BUSY: 13
SX1262 radio = new Module(8, 14, 12, 13);

// L’écran sur V3 utilise souvent l’adresse 0x3C et les pins 17/18
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, OLED_RST, OLED_SCL, OLED_SDA);
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();

struct ControlData {
uint32_t msgId;
int16_t ailerons, elevator, rudder, flaps, throttle;
bool autoPilot, gear, lights;
int16_t trimRudder, trimElevator;
} rxData;

// Variables de diagnostic
volatile float lastRSSI = 0;
volatile float lastSNR = 0;
volatile uint32_t packetCount = 0;
volatile uint32_t lastPacketTime = 0;
const uint32_t TIMEOUT_MS = 500;

unsigned long lastDebugTime = 0;
SemaphoreHandle_t mutex;

// Prototypes
void debugControlData(const ControlData &data);
void setupLoRaFastMode();
void TaskLoRaReceive(void *pvParameters);
void TaskControlFlight(void *pvParameters);
void TaskDisplay(void *pvParameters);

void setup() {
Serial.begin(115200);

// 1. Activer l’alimentation Vext (OLED + RF)
pinMode(VEXT_CTRL, OUTPUT);
digitalWrite(VEXT_CTRL, LOW);
delay(150); // Un peu plus long pour la stabilité du régulateur V3

mutex = xSemaphoreCreateMutex();

// 2. Configuration LoRa
setupLoRaFastMode();

// 3. Initialisation OLED & PWM
u8g2.begin();
pwm.begin();
pwm.setPWMFreq(50);

// Tâches
xTaskCreatePinnedToCore(TaskLoRaReceive, “LoRaRecv”, 10000, NULL, 1, NULL, 0);
xTaskCreatePinnedToCore(TaskControlFlight, “FlightCtrl”, 10000, NULL, 2, NULL, 1);
xTaskCreatePinnedToCore(TaskDisplay, “Display”, 10000, NULL, 1, NULL, 1);
}

void setupLoRaFastMode() {
Serial.print(F("[LoRa] Init V3.2… "));
int state = radio.begin();

if (state == RADIOLIB_ERR_NONE) {
Serial.println(F(“Succès !”));
radio.setDio2AsRfSwitch(true); // Indispensable sur Heltec
} else {
Serial.print(F("Échec, code "));
Serial.println(state);
while (true)
;
}

// Paramètres haute vitesse
radio.setFrequency(868.0);
radio.setBandwidth(500.0);
radio.setSpreadingFactor(7);
radio.setCodingRate(5);
radio.setPreambleLength(6);
radio.setSyncWord(0x12);
radio.implicitHeader(sizeof(ControlData));
radio.setCRC(false);
radio.setOutputPower(22);
}

void TaskLoRaReceive(void *pvParameters) {
for (;:wink: {
int state = radio.receive((uint8_t *)&rxData, sizeof(ControlData));

if (state == RADIOLIB_ERR_NONE) {
  if (xSemaphoreTake(mutex, portMAX_DELAY)) {
    lastPacketTime = millis();
    lastRSSI = radio.getRSSI();
    lastSNR = radio.getSNR();
    packetCount++;
    xSemaphoreGive(mutex);
  }
}

}
}

void TaskControlFlight(void *pvParameters) {
for (;:wink: {
uint32_t now = millis();

if (xSemaphoreTake(mutex, portMAX_DELAY)) {
  // Calcul du temps écoulé depuis le dernier paquet
  uint32_t timeSinceLastPacket = now - lastPacketTime;

  if (timeSinceLastPacket > TIMEOUT_MS) {
    // ==========================================
    //        MODE FAILSAFE : SIGNAL PERDU
    // ==========================================

    // 1. Moteurs à l'arrêt (150 = arrêt pour la plupart des ESC)
    pwm.setPWM(0, 0, 150);
    pwm.setPWM(1, 0, 150);

    // 2. Gouvernes en position de sécurité
    // On force des valeurs neutres ou de "mise en cercle"
    pwm.setPWM(2, 0, 307);  // Elevator au neutre (ou légèrement cabré)
    pwm.setPWM(3, 0, 350);  // Rudder légèrement incliné pour tourner

    // 3. Optionnel : Sortir le train d'atterrissage si piloté
    // pwm.setPWM(4, 0, 500);

  } else {
    // ==========================================
    //        VOL NORMAL : SIGNAL OK
    // ==========================================

    // Moteurs (0-100%) -> (150-600 PWM)
    int mSpeed = map(rxData.throttle, 0, 4095, 150, 600);
    pwm.setPWM(0, 0, mSpeed);
    pwm.setPWM(1, 0, mSpeed);

    // Axes de vol avec Trims
    // On utilise constrain pour éviter qu'un trim trop fort ne fasse bugger le servo
    int elevatorPos = constrain(rxData.elevator + rxData.trimElevator, 0, 4095);
    int rudderPos = constrain(rxData.rudder + rxData.trimRudder, 0, 4095);

    pwm.setPWM(2, 0, map(elevatorPos, 0, 4095, 200, 500));
    pwm.setPWM(3, 0, map(rudderPos, 0, 4095, 200, 500));
  }

  xSemaphoreGive(mutex);
}

// Fréquence de rafraîchissement des servos (50Hz = 20ms)
vTaskDelay(pdMS_TO_TICKS(20));

}
}

void debugControlData(const ControlData &data) {
Serial.println(F("\n======= [ RECEIVER DEBUG ] ======="));
Serial.printf(“RSSI: %0.2f dBm | SNR: %0.2f | Pkts: %u\n”, lastRSSI, lastSNR, packetCount);
Serial.printf(“STICKS: A:%4d | E:%4d | R:%4d | T:%4d\n”, data.ailerons, data.elevator, data.rudder, data.throttle);
Serial.printf(“FLAGS : AP:%s | GEAR:%s | LGT:%s\n”,
data.autoPilot ? “ON” : “OFF”, data.gear ? “DOWN” : “UP”, data.lights ? “ON” : “OFF”);
Serial.println(F("===================================="));
}

void TaskDisplay(void *pvParameters) {
for (;:wink: {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_6x10_tf);

if (xSemaphoreTake(mutex, portMAX_DELAY)) {
  bool isFailsafe = (millis() - lastPacketTime > TIMEOUT_MS);

  if (isFailsafe) {
    u8g2.drawStr(0, 10, "STATUS: !!! FAILSAFE !!!");
  } else {
    u8g2.drawStr(0, 10, "STATUS: LINK OK");
  }

  u8g2.setCursor(0, 25);
  u8g2.printf("RSSI: %.1f dBm", lastRSSI);

  u8g2.setCursor(0, 40);
  u8g2.printf("SNR: %.1f  PKTS: %u", lastSNR, packetCount);

  u8g2.setCursor(0, 55);
  u8g2.printf("THR: %d%%  GEAR: %s",
              map(rxData.throttle, 0, 4095, 0, 100),
              rxData.gear ? "DOWN" : "UP");

  xSemaphoreGive(mutex);
}
u8g2.sendBuffer();
vTaskDelay(pdMS_TO_TICKS(100));

}
}

void loop() {
vTaskDelete(NULL);
}