Back to Projects

Encrypted Off-Grid Communications (LoRa)

ESP32 LoRa nodes with encrypted payloads, ACK/retry delivery, OLED status, and a browser chat UI served from the device over WiFi and WebSockets.

Why I built this

I wrote the firmware and software for the LoRa nodes and the embedded web server chat application myself, designed the hardware, assembled the boards, and 3D-printed casings for each node. Off-grid situations still need trustworthy comms when cellular or the internet is missing, unreliable, or not appropriate: remote sites, crowded events, disaster response, or small teams that want private radio-backed messaging without infrastructure. LoRa is a strong fit for low-power, long-range links, so I built a complete prototype that pairs robust radio behavior with a familiar chat experience in the browser.

Tech stack

C/C++ESP32LoRaSX1262PlatformIOWebSocketsRadioLib

How I built it

Overview Each node runs modular C++ firmware (PlatformIO) on ESP32-class hardware with an SX1262 LoRa transceiver. The device brings up a WiFi access point, serves a responsive web UI over HTTP, and uses WebSockets for real-time chat and delivery status. Payloads are encrypted before they go over LoRa; an acknowledgment and retry path reports success, failure after retries, or pending state back to the UI. A small OLED shows AP SSID/password, assigned IP, connected WiFi clients, and snippets of recent LoRa TX/RX so you can operate without a phone at a glance. A GPIO button can fire a predefined "alive" ping across the link. Hardware layout, tested boards, components per node, and LoRa/WebSocket framing (with diagrams) are documented in the Hardware architecture and Communication protocols sections below. Software architecture The codebase splits into managers: lora_manager configures the radio via RadioLib, formats packets, handles interrupt-driven reception, maintains an outgoing queue, and drives ACK timeouts/retries; web_manager hosts AsyncWebServer on port 80, serves the chat page, and manages JSON over WebSocket at /ws; display_manager drives U8g2 with states for boot, AP details, IP ready, chat context, and RX alerts; encryption helpers XOR-encrypt message bodies and represent ciphertext as HEX on the wire. ArduinoJson serializes WebSocket messages. WiFi stack and async web work sit on FreeRTOS alongside a lean Arduino loop, with LoRa events surfaced from ISR context safely.

Hardware architecture

Node configuration diagram: ESP32 connected to OLED, SX1262 LoRa transceiver, and LiPo battery
Node configuration

The system is designed primarily for ESP32-based boards with integrated LoRa transceivers and OLED displays but will work with any ESP32 board connected with an SX1262 LoRa module over SPI and an OLED display over I2C.

Tested boards

  • Heltec WiFi LoRa 32 V3 (ESP32-S3, SX1262 LoRa, 0.96" OLED)
  • Seeed Studio XIAO ESP32S3 with WIO SX1262 LoRa module attachment (will require an external OLED over I2C for full functionality)

Components per node

  • ESP32 microcontroller
  • SX1262 LoRa transceiver
  • OLED display (SSD1306/SH1106)
  • WiFi antenna
  • LoRa antenna
  • Input button (typically GPIO 0 on ESP32 dev boards)

Communication protocols

Communication cycle diagram: LoRa data packet from node 1 to node 2 and ACK packet back, showing packet structure
LoRa communication cycle (data and ACK)

LoRa protocol

  • Data packet: MY_DEVICE_ID:LORA_PACKET_PREFIX:currentLoRaMessageId:ENCRYPTED_MESSAGE

    Example: BigNode:P:123:AABBCCDD (where AABBCCDD is the HEX of XOR-encrypted "hello")

  • ACK packet: MY_DEVICE_ID:LORA_ACK_PREFIX:receivedMessageId

    Example: PhoneNode:A:123

  • Configured for 915 MHz, SF7, 125 kHz bandwidth (legal configuration for Australia).

WiFi / web protocol

  • HTTP: serves the main HTML page.
  • WebSocket (JSON):

Client to server (sending message)

{"text": "message_content", "local_id": "local_msg_123"}

Server to client (identity)

{"type": "system", "event": "identity", "deviceId": "BigNode", "boardName": "BigNode"}

Server to client (received message)

{"sender": "PhoneNode", "text": "decrypted_message"}

Server to client (ACK status)

{"type": "ack_status", "local_id": "local_msg_123", "lora_msg_id": 456, "status": "acked" | "failed_ack" | "pending_ack"}