Build a TinyML Weather Station with ESP32-S3 and LoRa

DIY Hardware
Date:October 12, 2026
Topic:
Build a TinyML Weather Station with ESP32-S3 and LoRa
⏱ 3 min read

Most weather stations just phone home. They stream raw temperature, humidity, and pressure readings to a cloud endpoint where someone else runs the model. That architecture works until the cellular module dies, the API key rotates, or the monthly bill arrives. What if the station knew a storm was coming before the gateway even woke up?

Why Edge Inference Changes the Game

Running a TinyML model on the ESP32-S3 lets the node classify weather patterns locally—"rapid pressure drop," "humidity inversion," "clear sky" — without a single byte leaving the MCU. You save bandwidth, slash latency, and keep the device functional during network outages. The Xtensa dual-core at 240 MHz with vector instructions handles a quantized TensorFlow Lite Micro model in under 20 ms. That leaves plenty of duty-cycle budget for LoRa transmission.

Hardware Bill of Materials

ComponentRoleKey Spec
ESP32-S3-DevKitC-1MCU + TensorFlow Lite Micro240 MHz dual-core, 512 KB SRAM
SX1262 LoRa ModuleLong-range radio868/915 MHz, +22 dBm, SPI
BME280Environmental sensorI2C, ±0.5 hPa, ±0.5°C
DS3231 RTCPrecision timekeeping±2 ppm, battery backup
18650 Li-ion + TP4056Power3.7 V, 3000 mAh, solar ready
Custom PCB or PerfboardIntegration2-layer, 1.6 mm

Firmware Architecture

Split the firmware into three FreeRTOS tasks pinned to separate cores. Core 0 owns the radio; Core 1 owns inference. A ring buffer passes feature vectors from the sensor task to the inference task. This prevents SPI transactions from starving the tensor arena.

c
// Core 1: Inference task
void inference_task(void *arg) {
  TfLiteTensor* input = interpreter->input(0);
  TfLiteTensor* output = interpreter->output(0);
  while (1) {
    FeatureVector fv;
    if (xQueueReceive(feature_queue, &fv, portMAX_DELAY)) {
      memcpy(input->data.f, fv.values, sizeof(fv.values));
      interpreter->Invoke();
      uint8_t label = argmax(output->data.f, NUM_CLASSES);
      xQueueSend(label_queue, &label, 0);
    }
  }
}
💡
TipPin the sensor read task to Core 0 alongside LoRa to keep I2C timing deterministic.

Model Training and Quantization

Collect 10,000 labeled samples across seasons. Train a 1D-CNN with two conv layers (16 and 32 filters, kernel 3) and a dense head of 4 classes. Export to TFLite, then apply post-training int8 quantization with a representative dataset. The final .tflite weighs 18 KB—well under the ESP32-S3 SRAM budget. Flash it as a binary blob aligned to 16 bytes for DMA-friendly access.

python
converter = tf.lite.TFLiteConverter.from_keras_model(model)
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.representative_dataset = rep_gen
tflite_quant = converter.convert()
with open('weather_int8.tflite', 'wb') as f:
    f.write(tflite_quant)

LoRa Payload Strategy

Don't send raw sensor JSON. Encode the classified label, confidence, and a 16-bit checksum into 4 bytes using CBOR. Transmit on SF7/125 kHz for 5 km urban range at 5.5 kbps. Duty cycle stays under 1 % even with 10-minute intervals. The gateway decodes CBOR, timestamps, and forwards to InfluxDB.

ByteFieldRange
0Label (0-3)uint8
1Confidenceuint8 (0-255)
2-3CRC16-CCITTuint16

Power Budget Reality Check

Deep sleep current: 18 µA (RTC + ULP). Active cycle: 120 mA for 350 ms (sensor + inference + TX). Average over 10 min = 89 µA. A 3000 mAh cell lasts 3.8 years. Add a 2 W solar panel and TP4056 for perpetual operation. Disable the LDO on the dev kit; feed 3.3 V directly to VDD_SPI to shave 2 mA quiescent.

"

The best network request is the one you never make.

— Pete Warden

Deploy and Iterate

Flash the firmware, mount the enclosure on a pole with the BME280 shielded from direct sun. Watch the gateway dashboard for label distribution. If "rapid pressure drop" spikes falsely, retrain with new negative samples and OTA the updated .tflite via LoRaWAN multicast (fragmented over 50-byte frames). No truck rolls required.

⚠️
WarningKeep the tensor arena in PSRAM if you enable the 8 MB external chip; internal SRAM fragments fast with FreeRTOS stacks.

✦

You now have a weather station that thinks before it speaks. Next step: add a tiny OLED for local debugging, or fuse the model into the ULP coprocessor to push average draw below 50 µA. The silicon is ready. Your dataset is the only limit.

Share𝕏 Twitterin LinkedInin Whatsapp