Skip to main content
Back to blog

Building a LoRa weather station for my garden

·7 min readOutdoor Tech

I wanted to know what was happening in my garden without running power cables or extending my Wi-Fi outdoors. Temperature, humidity, soil moisture. Simple data, but getting it from 30 meters away through two walls turned out to be an interesting problem.

Wi-Fi was the obvious choice, but I did not want to deal with signal dropouts or a powered outdoor access point. Bluetooth does not have the range. LoRa, on the other hand, sends small packets over long distances on tiny amounts of power. Perfect for a sensor that wakes up every 10 minutes, reads a few values, transmits them, and goes back to sleep.

The hardware

Here is everything I used:

Sender (outdoor):

  • LILYGO T-Beam v1.2: ESP32 + SX1276 LoRa radio + GPS + 18650 battery holder (~$30)
  • BME280 breakout: temperature (-40 to 85°C), humidity (0-100%), barometric pressure via I2C (~$4)
  • Capacitive soil moisture sensor v1.2: analog output, corrosion-resistant (~$3)
  • 6V 1W solar panel (~$5)
  • TP4056 charge module with battery protection (~$1)
  • 18650 lithium cell, 3000mAh (~$5)

Receiver (indoor):

  • Heltec WiFi LoRa 32 V3: ESP32-S3 + SX1262 LoRa + OLED display (~$18)

Total cost: under $70. The T-Beam is overkill for this project since I do not need the GPS, but I had one lying around from experimenting with Meshtastic. A bare ESP32 with an SX1276 module would work fine and cost less.

System overview

graph LR
    A[BME280 + Soil Sensor] -->|I2C / Analog| B[ESP32 T-Beam]
    B -->|LoRa 868MHz| C[Heltec V3 Receiver]
    C -->|HTTP POST| D[InfluxDB]
    E[Solar Panel] -->|TP4056| F[18650 Battery]
    F --> B

The sender reads sensor data, packs it into a LoRa packet, transmits on 868 MHz (the license-free ISM band in Europe), and enters deep sleep. The receiver listens continuously, parses incoming packets, and forwards the data to InfluxDB over Wi-Fi.

The sender sketch

This is a simplified version of what runs on the T-Beam. I use the LoRa library by Sandeep Mistry because it is straightforward for point-to-point communication.

#include <Wire.h>
#include <Adafruit_BME280.h>
#include <LoRa.h>
 
#define LORA_SS 18
#define LORA_RST 23
#define LORA_DIO0 26
#define LORA_FREQ 868E6
 
#define SOIL_PIN 36
#define SLEEP_MINUTES 10
#define US_TO_S 1000000ULL
 
Adafruit_BME280 bme;
 
void setup() {
  Serial.begin(115200);
 
  Wire.begin(21, 22);
  if (!bme.begin(0x76)) {
    Serial.println("BME280 not found");
    goToSleep();
  }
 
  LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
  if (!LoRa.begin(LORA_FREQ)) {
    Serial.println("LoRa init failed");
    goToSleep();
  }
  LoRa.setSpreadingFactor(7);
  LoRa.setSignalBandwidth(125E3);
 
  float temp = bme.readTemperature();
  float hum = bme.readHumidity();
  float pres = bme.readPressure() / 100.0F;
  int soilRaw = analogRead(SOIL_PIN);
  int soilPct = map(soilRaw, 3200, 1400, 0, 100);
  soilPct = constrain(soilPct, 0, 100);
 
  char packet[64];
  snprintf(packet, sizeof(packet),
    "T:%.1f,H:%.1f,P:%.1f,S:%d",
    temp, hum, pres, soilPct);
 
  LoRa.beginPacket();
  LoRa.print(packet);
  LoRa.endPacket();
 
  Serial.println(packet);
  goToSleep();
}
 
void goToSleep() {
  LoRa.sleep();
  esp_sleep_enable_timer_wakeup(SLEEP_MINUTES * 60 * US_TO_S);
  esp_deep_sleep_start();
}
 
void loop() {}

A few things worth noting:

Pin definitions are specific to the T-Beam v1.2. Other ESP32 boards will use different SPI pins for the LoRa module.

Soil moisture calibration is the trickiest part. The map(soilRaw, 3200, 1400, 0, 100) values came from testing my specific sensor. 3200 was the reading in dry air, 1400 was fully submerged in water. Your values will differ. Capacitive sensors are not precision instruments, but they are good enough to tell you "dry," "moist," or "soaked."

Spreading factor 7 gives the shortest airtime and lowest power consumption. For 30 meters through a couple of walls, I do not need the extra range that higher spreading factors provide.

The receiver

The Heltec V3 runs continuously on USB power inside my house. It listens for LoRa packets and forwards them to InfluxDB.

#include <LoRa.h>
#include <WiFi.h>
#include <HTTPClient.h>
 
#define LORA_SS 8
#define LORA_RST 12
#define LORA_DIO0 14
#define LORA_FREQ 868E6
 
const char* ssid = "your-wifi";
const char* password = "your-password";
const char* influxUrl = "http://192.168.1.50:8086/api/v2/write?org=home&bucket=garden";
const char* influxToken = "your-token-here";
 
void setup() {
  Serial.begin(115200);
 
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
  }
 
  LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
  LoRa.begin(LORA_FREQ);
  LoRa.setSpreadingFactor(7);
  LoRa.setSignalBandwidth(125E3);
}
 
void loop() {
  int packetSize = LoRa.parsePacket();
  if (packetSize == 0) return;
 
  String data = "";
  while (LoRa.available()) {
    data += (char)LoRa.read();
  }
 
  float temp, hum, pres;
  int soil;
  sscanf(data.c_str(), "T:%f,H:%f,P:%f,S:%d",
    &temp, &hum, &pres, &soil);
 
  String line = "garden temp=" + String(temp)
    + ",humidity=" + String(hum)
    + ",pressure=" + String(pres)
    + ",soil_moisture=" + String(soil);
 
  HTTPClient http;
  http.begin(influxUrl);
  http.addHeader("Authorization", "Token " + String(influxToken));
  http.addHeader("Content-Type", "text/plain");
  http.POST(line);
  http.end();
}

The receiver formats the data as InfluxDB line protocol and sends it via HTTP POST to the /api/v2/write endpoint. From there, I query it in Grafana. Eventually I want to pull this into Home Assistant for automations, like notifications when the soil gets too dry.

Solar power and the power budget

The math matters here. An 18650 cell holds about 3000mAh at 3.7V. The ESP32 in deep sleep with the LoRa radio powered down draws roughly 10uA. During a wake cycle (read sensors, transmit, go back to sleep), it draws around 100-120mA for about 3-5 seconds.

Per wake cycle: 120mA * 4s = 0.133mAh

Per day (144 cycles at 10-minute intervals): 144 * 0.133 = 19.2mAh active + (10uA * 24h) = 0.24mAh sleep

Total daily draw: roughly 20mAh.

A 3000mAh battery without solar would last about 150 days. With a 6V 1W solar panel feeding through the TP4056 charge controller, the battery stays topped up even on cloudy days in Germany. The panel produces more than enough to cover the 20mAh daily consumption as long as it gets a few hours of indirect light.

The TP4056 handles the charging logic: constant current at 1A until the cell hits 4.2V, then constant voltage until current drops. The version with battery protection (the one with two ICs on the board) prevents over-discharge, which is important since deep-discharging a lithium cell kills it.

Real-world performance

I have been running this setup for about three weeks. Some observations:

Range: 30 meters through two exterior walls is trivial for LoRa at 868 MHz. I tested up to about 200 meters through my neighborhood with trees and fences in the way, and it still worked with RSSI around -100 dBm. I plan to write a deeper post on LoRa radio basics at some point.

Reliability: I have not missed a single transmission at the 30-meter range. The receiver logs every packet with a timestamp. Occasionally a packet arrives with low RSSI if I move the sender to a spot behind the shed, but it still decodes fine.

Battery: The battery has not dropped below 4.0V since I added the solar panel. On purely overcast days it still charges, just slower.

Soil sensor drift: The capacitive sensor readings shift slightly with temperature. The BME280 temperature reading helps me compensate for this in Grafana, but it is not a huge issue for my "is the soil dry" use case.

What I would do differently

Skip the T-Beam. A bare ESP32 dev board plus an SX1276 breakout module would cost half as much and work the same for a stationary sensor. The GPS and battery management on the T-Beam are wasted here.

Add a weatherproof enclosure from the start. I started with the board in a zip-lock bag. It worked for testing but is not a long-term solution. A proper IP65 junction box with cable glands costs about $5 and looks less embarrassing.

Use RadioLib instead of LoRa.h. The LoRa library by Sandeep Mistry works fine for the SX1276, but RadioLib supports a wider range of chips (including the SX1262 on the Heltec V3) and has more active development. If I were starting fresh, I would standardize on RadioLib for both sender and receiver.

Add error handling for Wi-Fi reconnection. The receiver sketch assumes Wi-Fi stays connected. In practice it drops occasionally and needs a reconnect check in the loop.

Sources

Enjoying the blog? Subscribe via RSS to get new posts in your reader.

Subscribe via RSS