Heltec V3 – Dual-Core LoRa Receiver with Queue Management
Advanced LoRa receiver for Heltec V3 (ESP32-S3) with dual-core architecture – FreeRTOS inter-core communication, message queuing, CRC validation, and offline message storage with automatic retry on WiFi reconnect.
Overview
The Heltec V3 (ESP32-S3) is a powerful dual-core microcontroller with built-in LoRa radio and OLED display. This project demonstrates a production-ready LoRa receiver that leverages both cores:
- Core 1 (LoRa Reception) – Dedicated to receiving and validating LoRa packets.
- Core 0 (WiFi/HTTP) – Handles internet connectivity and API communication.
Key features:
- ✅ Dual-core architecture – Real-time LoRa reception on one core, network tasks on the other.
- ✅ FreeRTOS inter-core queue – Thread-safe message passing between cores.
- ✅ CRC validation – Only valid packets are processed and displayed.
- ✅ Offline message queuing – Stores messages when WiFi is disconnected.
- ✅ Automatic retry – Sends queued messages when WiFi reconnects.
System Architecture
Two Queue Systems
1. Inter-Core FreeRTOS Queue (loraQueue)
| Property | Value |
|---|---|
| Type | FreeRTOS QueueHandle_t |
| Purpose | Communication between Core 1 → Core 0 |
| Location | Global variable in main.ino |
| Size | 10 messages |
| Thread-safe | ✅ Yes (FreeRTOS managed) |
| Access | xQueueSend() / xQueueReceive() |
2. HTTP API Client Queue (_messageQueue)
| Property | Value |
|---|---|
| Type | std::vector<QueuedMessage> |
| Purpose | Store messages when WiFi is disconnected |
| Location | Private member of HTTPAPIClient class |
| Size | 20 messages |
| Thread-safe | ❌ No (only accessed from Core 0) |
| Access | addToQueue() / processQueue() |
Data Flow Diagram
Step-by-Step Flow
When WiFi is Disconnected
LoRa Packet → Core 1 (CRC) → xQueueSend(loraQueue)
→ Core 0 (WiFi Task) → xQueueReceive(loraQueue)
→ apiClient.sendMessage()
→ isConnected() == false
→ addToQueue() → _messageQueue (stored)
When WiFi is Connected
LoRa Packet → Core 1 (CRC) → xQueueSend(loraQueue)
→ Core 0 (WiFi Task) → xQueueReceive(loraQueue)
→ apiClient.sendMessage()
→ isConnected() == true
→ sendToAPI() → Internet
When WiFi Reconnects (Process Queue First)
1. processQueue() → Sends all queued messages from _messageQueue
2. isSendConnectionGreeting() → Sends "Device online! Queue: 0 pending"
Why process queue first:
- ✅ Sends all queued historical data from when device was offline.
- ✅ Clears backlog before sending new status information.
- ✅ More important data (sensor readings) gets priority over administrative messages.
- ✅ Greeting reflects the current state after backlog is cleared.
Code Examples
FreeRTOS Queue Declaration (main.ino)
// Thread-safe queues for inter-core communication
QueueHandle_t loraQueue;
void setup() {
// Create queue with 10 message slots
loraQueue = xQueueCreate(10, sizeof(LoRaMessage));
}
// LoRa Reception Task (Core 1)
void loraReceptionTask(void *pvParameters) {
while (1) {
LoRaMessage loraMsg;
loraMsg.message = str;
loraMsg.isValid = (state == RADIOLIB_ERR_NONE);
loraMsg.rssi = radio.getRSSI();
loraMsg.snr = radio.getSNR();
// Send to inter-core queue
xQueueSend(loraQueue, &loraMsg, portMAX_DELAY);
vTaskDelay(50 / portTICK_PERIOD_MS);
}
}
// WiFi HTTP Task (Core 0)
void wifiHTTPTask(void *pvParameters) {
while (1) {
LoRaMessage loraMsg;
// Receive from inter-core queue
if (xQueueReceive(loraQueue, &loraMsg, 0) == pdTRUE) {
// Pass to API client
apiClient.sendMessage(loraMsg.message, loraMsg.isValid,
loraMsg.rssi, loraMsg.snr);
// Check queue status
UBaseType_t queueBefore = uxQueueMessagesWaiting(loraQueue) + 1;
UBaseType_t queueAfter = uxQueueMessagesWaiting(loraQueue);
}
// Process offline queue
apiClient.processQueue();
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
HTTP API Client Queue (HTTPAPIClient.cpp)
bool HTTPAPIClient::sendMessage(const String& message, bool isValid,
float rssi, float snr) {
if (!isConnected()) {
// WiFi OFF: Store in internal API client queue
Serial.println("📡 WiFi disconnected - queuing valid message");
if (isValid) {
return addToQueue(message, isValid, rssi, snr);
}
return false;
} else {
// WiFi ON: Send immediately
return sendToAPI(message, isValid, rssi, snr);
}
}
bool HTTPAPIClient::addToQueue(const String& message, bool isValid,
float rssi, float snr) {
if (_messageQueue.size() >= _maxQueueSize) {
Serial.println("⚠️ Message queue full, removing oldest");
_messageQueue.erase(_messageQueue.begin());
}
QueuedMessage qMsg;
qMsg.message = message;
qMsg.isValid = isValid;
qMsg.rssi = rssi;
qMsg.snr = snr;
qMsg.timestamp = millis();
_messageQueue.push_back(qMsg);
return true;
}
void HTTPAPIClient::processQueue() {
if (!isConnected()) {
return; // Wait for WiFi to reconnect
}
if (_messageQueue.empty()) {
return; // No queued messages
}
Serial.printf("📤 Processing %d queued messages...\n", _messageQueue.size());
for (auto &msg : _messageQueue) {
sendToAPI(msg.message, msg.isValid, msg.rssi, msg.snr);
delay(100); // Small delay between messages
}
_messageQueue.clear();
}
HTTPAPIClient.h – Queue Declaration
class HTTPAPIClient {
private:
struct QueuedMessage {
String message;
bool isValid;
float rssi;
float snr;
unsigned long timestamp;
};
std::vector<QueuedMessage> _messageQueue;
const int _maxQueueSize = 20;
};
Key Points
| Concept | Explanation |
|---|---|
| No Loopback | Messages never go back to loraQueue from the API client. |
| Two-Stage Queuing | Stage 1: loraQueue (inter-core communication). Stage 2: _messageQueue (offline storage). |
| Different Purposes | loraQueue: Real-time message passing between cores. _messageQueue: Persistent storage when offline. |
| Thread Safety | loraQueue is thread-safe (FreeRTOS managed). _messageQueue is only accessed from Core 0. |
| Priority | Process queue before sending greeting to show accurate queue status. |
Key Achievements
- ✅ Dual-core LoRa reception – Real-time packet processing on dedicated core.
- ✅ FreeRTOS inter-core queue – Thread-safe message passing.
- ✅ CRC validation – Only valid packets are processed.
- ✅ Offline message storage – Messages stored when WiFi is disconnected.
- ✅ Automatic retry – Queued messages sent when WiFi reconnects.
- ✅ Accurate status reporting – Process queue before connection greeting.
Technology Stack
| Category | Technology |
|---|---|
| MCU | ESP32-S3 (Heltec V3) |
| LoRa | SX1276 (RadioLib) |
| RTOS | FreeRTOS (ESP-IDF) |
| Queues | FreeRTOS Queue, std::vector |
| WiFi | ESP32 WiFi library |
| HTTP | HTTPClient (Arduino) |
| Display | SSD1306 (OLED) |
Related Projects
- Embedded Systems Engineering – Main embedded systems page.
- Heltec V3 LoRa Receiver – Base LoRa receiver.
This dual-core LoRa receiver is part of my broader Embedded Systems Engineering practice. For a detailed technical walkthrough or custom firmware design, feel free to reach out.