Small enclosure and breadboard with ESP32-C3, AHT20, SD card module and DS3231 RTC, laptop showing logged data graph

Intermediate

Build a Wireless Environmental Data Logger

IoTSensorsWireless

Combine environmental sensing, wireless connectivity and periodic data logging to create a practical monitoring node.

8 real parts included

Total for this kit: 1,768.00 ฿

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

What you'll need

Project Goal

This project combines everything from the beginner sensor project with wireless connectivity, turning a single readout into a real monitoring node that can report data without a permanent wired connection to a computer.

Choosing a Sampling Interval

Decide how often the sensor actually needs to be read before writing any code. A room-temperature node might reasonably sample once a minute; a fast-changing measurement might need seconds. Sampling far more often than the data actually changes just wastes power and, on a shared wireless network, unnecessary airtime.

Reading the Sensor

This part is the same as any other sensor project: read the current value on each cycle. What's new here is what happens to that value afterward.

Local vs Remote Logging

Local logging means storing readings on the device itself (in memory or on local storage) for later retrieval. Remote logging means sending each reading somewhere else over the network as it's taken. The two aren't mutually exclusive -- a robust node often keeps a small local buffer specifically so it has somewhere to hold data if the remote connection is temporarily unavailable.

Wireless Options

Wi-Fi is the most straightforward option if a network is already available where the node will live and power isn't extremely constrained. Other wireless options exist for longer range or lower power at the cost of more setup complexity -- the right choice depends on your specific range, power and infrastructure constraints, not on which option is most familiar.

Retry Behavior

A wireless connection will occasionally fail to send -- plan for that from the start rather than treating it as a rare edge case. A simple, workable approach: if a send fails, hold the reading (or a short buffer of recent readings) and retry on the next cycle rather than silently discarding it.

Timestamps

Every logged reading should carry a timestamp of when it was actually taken, not when it happened to be received or logged remotely -- the two can drift apart, especially if retries are involved.

Power Considerations

Wireless radios draw meaningfully more power than the controller alone, especially while actively transmitting. If the node will run on battery, minimizing how long the radio stays active per cycle (send quickly, then return to a low-power state) matters far more than almost any other optimization you could make.

Data Integrity

Sanity-check readings before logging them -- a sensor returning an implausible value (well outside its rated range) is more likely to indicate a wiring or read error than a genuine measurement, and logging it uncritically pollutes your data.

Optional Upgrade: Battery Operation

Once the wired, mains-powered version is reliable, moving to battery power is a reasonable next step -- revisit the power considerations above specifically, since battery life becomes the primary constraint once you make that change.

This guide describes the concepts a wireless logging node needs. It does not assume or claim any specific cloud service or dashboard integration is already built -- treat that as a separate, later addition once the logging itself is proven reliable.

Wiring

  • AHT20 VIN → 3.3V, GND → GND, SDA → GPIO8, SCL → GPIO9 (I2C bus, address 0x38)
  • DS3231 VCC → 3.3V, GND → GND, SDA → GPIO8, SCL → GPIO9 — same I2C bus as the AHT20 (address 0x68, no conflict); leave SQW/32K unconnected
  • SD card module VCC → 5V (most breakout SD modules have an onboard 3.3V regulator and level shifting; if yours is a bare 3.3V-only module, use 3.3V instead — check its silkscreen)
  • SD card module GND → GND
  • SD card module CS → GPIO7 (`SD_CS_PIN`)
  • SD card module SCK → GPIO4 (`SD_SCK_PIN`)
  • SD card module MOSI/DI → GPIO6 (`SD_MOSI_PIN`)
  • SD card module MISO/DO → GPIO5 (`SD_MISO_PIN`)

The ESP32-C3 has no fixed SPI pins — its GPIO matrix lets you route SPI to almost any free pins, which is why the code calls `SPI.begin()` explicitly with the four pin numbers above rather than relying on a default; adjust the `#define`s if you wire it differently. On first upload, the DS3231 needs its clock set once from the sketch's compile time (done in `setup()` below) — after that its coin-cell backup keeps time through power cycles. Bring the SD card up last in `setup()` (after I2C) so a missing/bad card doesn't block sensor logging entirely; the "Wireless" name in this project's title is really about the ESP32-C3's built-in Wi-Fi being available for a future upgrade — adding periodic upload with `WiFi.begin()` and an HTTP client is a natural next step once local logging is solid.

Wiring Diagram

VIN GND SDA → GPIO8 SCL → GPIO9 VCC GND SDA → GPIO8 SCL → GPIO9 VCC GND CS → GPIO7 SCK → GPIO4 MOSI/DI → GPIO6 MISO/DO → GPIO5 ESP32-C3 SuperMini AHT20 DS3231 SD card module 3.3V GND 5V

Starter Code

#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <RTClib.h>
#include <Adafruit_AHTX0.h>

// I2C — ESP32-C3/S3 Arduino-ESP32 default pins
#define SDA_PIN 8
#define SCL_PIN 9

// SPI for the SD module — adjust to match how you wired it
#define SD_CS_PIN   7
#define SD_SCK_PIN  4
#define SD_MOSI_PIN 6
#define SD_MISO_PIN 5

const char* LOG_FILE = "/envlog.csv";
const unsigned long LOG_INTERVAL_MS = 60000; // once a minute

RTC_DS3231 rtc;
Adafruit_AHTX0 aht;
sensors_event_t humidityEvent, tempEvent;

unsigned long lastLog = 0;

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

  Wire.begin(SDA_PIN, SCL_PIN);

  if (!rtc.begin()) {
    Serial.println("DS3231 not found — check wiring.");
    while (1) delay(1000);
  }
  if (rtc.lostPower()) {
    Serial.println("RTC lost power, setting time from compile time.");
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
  }

  if (!aht.begin()) {
    Serial.println("AHT20 not found — check wiring.");
    while (1) delay(1000);
  }

  SPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);
  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("SD card init failed — check wiring/card.");
    while (1) delay(1000);
  }

  // Write a CSV header if the file doesn't exist yet
  if (!SD.exists(LOG_FILE)) {
    File f = SD.open(LOG_FILE, FILE_WRITE);
    if (f) {
      f.println("timestamp,temperature_c,humidity_pct");
      f.close();
    }
  }

  Serial.println("Logger ready.");
}

void loop() {
  unsigned long now = millis();
  if (now - lastLog < LOG_INTERVAL_MS) return;
  lastLog = now;

  DateTime ts = rtc.now();
  aht.getEvent(&humidityEvent, &tempEvent);

  char timestamp[20];
  snprintf(timestamp, sizeof(timestamp), "%04d-%02d-%02d %02d:%02d:%02d",
           ts.year(), ts.month(), ts.day(), ts.hour(), ts.minute(), ts.second());

  File f = SD.open(LOG_FILE, FILE_WRITE);
  if (f) {
    f.print(timestamp);
    f.print(",");
    f.print(tempEvent.temperature);
    f.print(",");
    f.println(humidityEvent.relative_humidity);
    f.close();
    Serial.printf("Logged: %s, %.1fC, %.1f%%\n", timestamp, tempEvent.temperature, humidityEvent.relative_humidity);
  } else {
    Serial.println("Failed to open log file for writing.");
  }
}

Not sure about a step? Ask us