LILYGO TTGO LoRa32 board with integrated OLED display and whip antenna mounted outdoors on a fence post

Intermediate

Build a LoRa Sensor Node

WirelessIoTSensors

Talk to a sensor a mile away with no router, no SIM card, and no monthly bill -- real long-range wireless, one board at a time.

4 real parts included

Total for this kit: 1,521.00 ฿

Doesn't include any order discount you may get at checkout.

What you'll need

Project Goal

This project puts a genuine long-range radio in your hands: a LoRa node that can send and receive data over distances measured in kilometers rather than meters, without relying on Wi-Fi, a cellular SIM, or a monthly data bill. Be clear-eyed about scope going in: this kit is a single node. A LoRa link needs two ends to actually demonstrate communication -- either a second LoRa board (a second LILYGO TTGO LoRa32, or any compatible LoRa radio) or a LoRaWAN gateway on the receiving end. This build gets one fully working, transmit-and-receive-capable node ready to talk; pairing it with a second node is what completes an actual link.

What the Controller Does

The LILYGO TTGO TZGO LoRa32 combines an ESP32 microcontroller, a SX127x-family LoRa radio, and a small onboard OLED display on a single board. The ESP32 runs your program, hands data to the LoRa radio to transmit, and reads back whatever the radio receives -- while the OLED gives you a live, no-computer-needed view of what the radio is doing.

What the LoRa Radio Does

Unlike Wi-Fi or Bluetooth, the onboard LoRa radio uses chirp spread spectrum (CSS) modulation -- it spreads each bit of data across a wide, slowly-swept frequency chirp instead of a short, information-dense burst. That trade is exactly what gives LoRa its range advantage: it sacrifices bandwidth (LoRa is slow, typically well under a few kilobits per second) in exchange for a radio link that stays decodable at power levels and distances where Wi-Fi has long since dropped out.

What the OLED Display Does

The built-in display exists so you can see the radio's status without a serial monitor plugged in -- connection state, last packet sent or received, signal strength (RSSI), and signal-to-noise ratio (SNR) are all useful to show directly on the board itself, especially once you take the node away from your desk to test real-world range.

Frequency Bands and Regional Regulations

LoRa radios are manufactured for specific frequency bands -- commonly 433MHz, 868MHz, or 915MHz depending on region -- and that choice isn't arbitrary: it follows each region's radio spectrum regulations for unlicensed low-power devices. Confirm which band your specific board variant covers, and make sure it matches your region's rules. This matters again the moment you add a second node: both ends of a link must operate on the same band to talk to each other at all.

Software and Library Concepts

The LoRa radio's raw send/receive capability is just a physical layer -- it moves bytes across the air, nothing more. To build anything useful, you still need a data protocol on top of it: something as simple as a custom packet format with a sender ID and payload, or a full framework like LoRaWAN if you want built-in addressing, acknowledgements, and (for a gateway-based setup) integration with existing LoRaWAN network infrastructure. Community libraries for the SX127x radio family handle the low-level radio configuration; your own code defines what the bytes you send and receive actually mean.

Testing and Troubleshooting

  • No signal received at all between two nodes -- first confirm both boards are configured for the exact same frequency band; a 915MHz node and an 868MHz node cannot hear each other no matter how well everything else is wired or coded.
  • Range seems disappointing when testing indoors -- this is expected, not a fault. LoRa's real range advantage shows up outdoors with a clear line of sight; walls, floors, and building materials attenuate it heavily, so don't judge range from a short indoor test.
  • Packets arrive intermittently or drop randomly -- check that the antenna's SMA connector is fully seated on both boards; a loose antenna connection is one of the most common real-world causes of flaky LoRa performance.
  • One board seems to transmit but never receives anything back -- verify you actually have a second node running and listening; a single board with nothing on the other end will never show incoming packets, by design.

Optional Future Upgrade: LoRaWAN Gateway Integration

Once you have two nodes talking directly to each other with a simple custom packet scheme, a natural next step is registering a node against a LoRaWAN gateway and network server instead, which adds standardized addressing, encryption, and the ability to forward your sensor data on to a broader network rather than just to a second board sitting next to it.

Wiring

  • All connections are pre-wired onboard the LILYGO TTGO LoRa32 V1 — no breadboard wiring is required for this project; the LoRa radio (SCK=GPIO5, MISO=GPIO19, MOSI=GPIO27, CS/SS=GPIO18, RST=GPIO14, DIO0=GPIO26) and the OLED (SDA=GPIO4, SCL=GPIO15, RST=GPIO16, I2C address 0x3C) are fixed onboard connections
  • USB-C cable → PC/power only, for programming and power

This pinout is for the common TTGO LoRa32 V1 board — V2/V2.1 revisions wire the OLED and LoRa module slightly differently, so confirm against your specific board's documentation or silkscreen if you have a different revision before assuming these pins. You must set `LORA_FREQUENCY` to match both your region and the frequency your physical LoRa module was built for (e.g. 915MHz US, 868MHz EU, 433MHz Asia) — sending on the wrong band can be illegal and definitely won't be heard by other hardware. To make this useful, flash a second LoRa32 board with matching receive code (`LoRa.parsePacket()`/`LoRa.read()`), or pair it with a "Solar-Powered Remote Sensor Node" project as a natural receive-side counterpart.

Starter Code

#include <SPI.h>
#include <LoRa.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// TTGO LoRa32 V1 onboard pinout - fixed on this board. V2/V2.1 boards wire
// these differently - check your specific board's docs/silkscreen if unsure.
#define LORA_SCK  5
#define LORA_MISO 19
#define LORA_MOSI 27
#define LORA_SS   18
#define LORA_RST  14
#define LORA_DIO0 26

#define OLED_SDA 4
#define OLED_SCL 15
#define OLED_RST 16
#define OLED_ADDRESS 0x3C

// Set this to match your LoRa module's region/frequency band, e.g.
// 915E6 (US), 868E6 (EU), 433E6 (Asia) - the wrong band can be illegal
// in your region and won't talk to other hardware correctly.
#define LORA_FREQUENCY 915E6

Adafruit_SSD1306 display(128, 64, &Wire, OLED_RST);

uint32_t packetCounter = 0;
unsigned long lastSend = 0;
const unsigned long SEND_INTERVAL_MS = 5000;

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

  // The onboard OLED needs a manual reset pulse before Wire/display init
  pinMode(OLED_RST, OUTPUT);
  digitalWrite(OLED_RST, LOW);
  delay(20);
  digitalWrite(OLED_RST, HIGH);

  Wire.begin(OLED_SDA, OLED_SCL);
  if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDRESS)) {
    Serial.println("SSD1306 init failed");
    while (true) delay(1000);
  }
  display.setTextColor(SSD1306_WHITE);

  SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_SS);
  LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);

  if (!LoRa.begin(LORA_FREQUENCY)) {
    Serial.println("LoRa init failed - check wiring/frequency");
    while (true) delay(1000);
  }

  Serial.println("LoRa Sensor Node ready");
}

void loop() {
  if (millis() - lastSend < SEND_INTERVAL_MS) return;
  lastSend = millis();

  unsigned long uptime = millis() / 1000;

  char packet[48];
  snprintf(packet, sizeof(packet), "count=%lu,uptime=%lus", packetCounter, uptime);

  LoRa.beginPacket();
  LoRa.print(packet);
  LoRa.endPacket();

  Serial.printf("Sent packet #%lu, uptime %lus\n", packetCounter, uptime);

  display.clearDisplay();
  display.setTextSize(2);
  display.setCursor(0, 0);
  display.println("LoRa TX");
  display.setTextSize(1);
  display.setCursor(0, 24);
  display.printf("Packet: %lu\n", packetCounter);
  display.printf("Uptime: %lus\n", uptime);
  display.display();

  packetCounter++;
}

Not sure about a step? Ask us