Build a fully autonomous environmental logger using the Beetle ESP32-C6, a DFRobot SEN0500 multi-sensor, a W25Q32 SPI NOR flash chip, a rechargeable 3.7V battery, and a custom 3D-printed case from JUSTWAY. Logs temperature, humidity, pressure, altitude, light, and UV index every hour for over a decade — completely power-independent between charges.
The Beetle ESP32-C6 reads 6 environmental parameters from the SEN0500 sensor via I2C and stores each timestamped record on a W25Q32 SPI flash chip. A 4MB flash chip holds ~130, 000 records.
At one reading per hour, that's over 5, 000 days — nearly 15 years of continuous logging without any cloud, WiFi, or cloud dependency.
Why W25Q Flash Instead of SD Card?- Reliability — No filesystem. Raw sectors. No corruption.
- Power — Microamps idle vs milliamps for SD.
- Size — 5mm SOP-8 footprint. No card slot needed.
- Cost — $2 for 4MB vs $4+ for SD + module.
- Longevity — 20+ year retention. SD cards fail unpredictably.
- Simplicity — 4 wires (CS/MOSI/MISO/SCK). No filesystem overhead.
Below is the complete circuit showing the Beetle, sensor, flash chip, charging module, battery, and LED.
Pin Connections Quick ReferenceSee Circuit Diagram above for visual reference.
To keep the build compact and deployment-ready, I housed the Beetle ESP32-C6 and other sensor in a custom 3D-printed enclosure designed specifically for this project.
The case was fabricated by JustWay, a maker-friendly manufacturing service that specializes in rapid prototyping for embedded systems and IoT devices.
The JUSTWAY case is a custom SLA/FDM 3D-printed housing that:
- Protects the PCB from dust, moisture, and light
- Mounts the Beetle, sensor on the outside face, and flash/battery inside
- Provides USB-C access for charging and data offload
- Includes cable routing grooves and sensor mounting posts
- Print in ABS or PETG for UV/temperature stability (not PLA)
- Seal seams with epoxy or silicone if the logger will sit outdoors
- Mount the sensor on the outside face, away from battery heat
- Use silicone thermal pads between battery and case walls
- Mount a small solar panel on the top for trickle charging (optional 100mA 5.5V panel)
In Arduino IDE, go to Boards Manager and add: https://espressif.github.io/arduino-esp32/package_esp32_index.json Select DFRobot Beetle ESP32-C6 from the Boards menu.
Via Library Manager (Ctrl+Shift+I), install:
• DFRobot_EnvironmentalSensor — for the SEN0500
• Adafruit SSD1306 (if using OLED display)
• DFRobot_GDL (if using a TFT screen)
Note: SPI.h, Wire.h, and WiFi.h are built-in.
Arduino CodeLogger Sketch (beetle_sensor_logger.ino)This is the main logging sketch. Upload this to the Beetle ESP32-C6 to start logging environmental data every hour.
/*
* Beetle ESP32-C6 + W25QXX + SEN050X — Sensor Logger
*
* Reads DFRobot Environmental Sensor via I2C and logs to SPI flash every 3s.
*
* Connections:
* W25QXX: CS=GPIO7 MOSI=GPIO22 MISO=GPIO21 SCK=GPIO23
* SEN050X: SDA→GPIO8 SCL→GPIO9 VCC→3.3V GND→GND
*
* Libraries: DFRobot_EnvironmentalSensor by DFRobot
*/
#include <SPI.h>
#include <Wire.h>
#include "DFRobot_EnvironmentalSensor.h"
// ── SPI Flash Pins ────────────────────────────────────────
#define FLASH_CS 7
#define FLASH_MOSI 22
#define FLASH_MISO 21
#define FLASH_SCK 23
#define LED_PIN 15
// ── Flash commands ────────────────────────────────────────
#define CMD_READ 0x03
#define CMD_PAGE_PROG 0x02
#define CMD_SECT_ERASE 0x20
#define CMD_WRITE_EN 0x06
#define CMD_READ_SR1 0x05
#define COUNT_ADDR 0x000000 // sector 0 — header only
#define DATA_ADDR 0x002000 // sector 2 — records (avoids erase clash)
#define STRUCT_VERSION 2 // bump this if Record struct changes
// ── Record struct ─────────────────────────────────────────
struct Record {
uint32_t timestamp;
float tempC;
float tempF;
float humidity;
float pressure_hpa;
float altitude_m;
float light_lx;
float uv_mwcm2;
};
DFRobot_EnvironmentalSensor sensor(SEN050X_DEFAULT_DEVICE_ADDRESS, &Wire);
// ── Flash helpers ─────────────────────────────────────────
inline void csL() { digitalWrite(FLASH_CS, LOW); }
inline void csH() { digitalWrite(FLASH_CS, HIGH); }
uint8_t readSR1() { csL(); SPI.transfer(CMD_READ_SR1); uint8_t r=SPI.transfer(0x00); csH(); return r; }
void waitBusy() { while(readSR1()&1) delay(1); }
void writeEnable() { csL(); SPI.transfer(CMD_WRITE_EN); csH(); }
void flashRead(uint32_t a, uint8_t *b, uint32_t n) {
csL(); SPI.transfer(CMD_READ); SPI.transfer(a>>16); SPI.transfer(a>>8); SPI.transfer(a);
for(uint32_t i=0;i<n;i++) b[i]=SPI.transfer(0x00); csH();
}
void flashWrite(uint32_t a, uint8_t *b, uint32_t n) {
writeEnable(); csL(); SPI.transfer(CMD_PAGE_PROG); SPI.transfer(a>>16); SPI.transfer(a>>8); SPI.transfer(a);
for(uint32_t i=0;i<n;i++) SPI.transfer(b[i]); csH();
}
void eraseSector(uint32_t a) { writeEnable(); csL(); SPI.transfer(CMD_SECT_ERASE); SPI.transfer(a>>16); SPI.transfer(a>>8); SPI.transfer(a); csH(); }
uint32_t readCount() { uint8_t b[4]; flashRead(COUNT_ADDR,b,4); return ((uint32_t)b[0]<<24)|(b[1]<<16)|(b[2]<<8)|b[3]; }
uint8_t readVersion(){ uint8_t v; flashRead(COUNT_ADDR+4,&v,1); return v; }
void writeHeader(uint32_t n) {
eraseSector(COUNT_ADDR); waitBusy();
uint8_t hdr[5];
hdr[0] = (uint8_t)(n >> 24); hdr[1] = (uint8_t)(n >> 16);
hdr[2] = (uint8_t)(n >> 8); hdr[3] = (uint8_t)n;
hdr[4] = STRUCT_VERSION;
flashWrite(COUNT_ADDR, hdr, 5);
waitBusy();
}
void clearAll() { Serial.print(F("Erasing... ")); writeHeader(0); waitBusy(); eraseSector(DATA_ADDR); waitBusy(); Serial.println(F("done")); }
// ── Setup ─────────────────────────────────────────────────
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println(F("\n=== W25QXX + SEN050X Logger ===\n"));
// SPI flash
SPI.begin(FLASH_SCK, FLASH_MISO, FLASH_MOSI, FLASH_CS);
pinMode(FLASH_CS, OUTPUT); csH();
pinMode(LED_PIN, OUTPUT);
Serial.println(F("[OK] SPI flash"));
// I2C sensor
Wire.begin();
Serial.print(F("[ ] Sensor... "));
int tries = 0;
while (sensor.begin() != 0 && tries < 10) { Serial.print('.'); delay(1000); tries++; }
if (tries >= 10) { Serial.println(F("\n[FAIL] Check wiring: SDA→8 SCL→9 VCC→3.3V GND→GND")); while(1) delay(1000); }
Serial.println(F("OK"));
// Storage
uint32_t count = readCount();
uint8_t ver = readVersion();
if (count == 0xFFFFFFFF || ver != STRUCT_VERSION) {
if (ver != STRUCT_VERSION && ver != 0xFF) Serial.println(F("[! ] Struct changed — clearing old data"));
clearAll();
count = 0;
writeHeader(0);
}
Serial.print(F("[ ] Records stored: ")); Serial.println(count);
Serial.print(F("[ ] Record size: ")); Serial.print(sizeof(Record)); Serial.println(F(" bytes\n"));
Serial.print(F("First reading: "));
Serial.print(sensor.getTemperature(TEMP_C)); Serial.print(F("C "));
Serial.print(sensor.getHumidity()); Serial.print(F("% "));
Serial.print(sensor.getAtmospherePressure(HPA)); Serial.println(F("hPa\n"));
Serial.println(F("Logging every 3s...\n"));
}
// ── Loop ──────────────────────────────────────────────────
void loop() {
static uint32_t lastLog = 0;
uint32_t now = millis();
if (now - lastLog >= 3000) { // 1 hour
lastLog = now;
// ── Read sensor ──
Record rec;
rec.timestamp = millis();
rec.tempC = sensor.getTemperature(TEMP_C);
rec.tempF = sensor.getTemperature(TEMP_F);
rec.humidity = sensor.getHumidity();
rec.pressure_hpa = sensor.getAtmospherePressure(HPA);
rec.altitude_m = sensor.getElevation();
rec.light_lx = sensor.getLuminousIntensity();
rec.uv_mwcm2 = sensor.getUltravioletIntensity();
// ── Write to flash ──
uint32_t count = readCount();
uint32_t addr = DATA_ADDR + count * sizeof(Record);
flashWrite(addr, (uint8_t*)&rec, sizeof(Record));
waitBusy();
writeHeader(count + 1);
// Blink LED
digitalWrite(LED_PIN, HIGH);
delay(80);
digitalWrite(LED_PIN, LOW);
// ── Print readings ──
Serial.println(F("-------------------------------"));
Serial.print(F("Temp: "));
Serial.print(rec.tempC);
Serial.println(F(" ℃"));
Serial.print(F("Temp: "));
Serial.print(rec.tempF);
Serial.println(F(" ℉"));
Serial.print(F("Humidity: "));
Serial.print(rec.humidity);
Serial.println(F(" %"));
Serial.print(F("Ultraviolet intensity: "));
Serial.print(rec.uv_mwcm2);
Serial.println(F(" mw/cm2"));
Serial.print(F("LuminousIntensity: "));
Serial.print(rec.light_lx);
Serial.println(F(" lx"));
Serial.print(F("Atmospheric pressure: "));
Serial.print(rec.pressure_hpa);
Serial.println(F(" hpa"));
Serial.print(F("Altitude: "));
Serial.print(rec.altitude_m);
Serial.println(F(" m"));
Serial.print(F("[Record #"));
Serial.print(count + 1);
Serial.println(F("]"));
Serial.println(F("-------------------------------\n"));
}
delay(100);
}Upload this separate sketch to read all data from flash and dump as CSV. Copy the output between the DATA markers into a spreadsheet.
/*
* Beetle ESP32-C6 + W25QXX — Sensor Data READER
*
* Dumps all records stored by beetle_sensor_logger.ino to Serial CSV.
* Output ready for Excel, Google Sheets, or Python.
*
* Connections:
* CS=GPIO7 MOSI=GPIO22 MISO=GPIO21 SCK=GPIO23
*/
#include <SPI.h>
#define FLASH_CS 7
#define FLASH_MOSI 22
#define FLASH_MISO 21
#define FLASH_SCK 23
#define CMD_READ 0x03
#define CMD_READ_SR1 0x05
#define COUNT_ADDR 0x000000
#define DATA_ADDR 0x002000
struct Record {
uint32_t timestamp;
float tempC;
float tempF;
float humidity;
float pressure_hpa;
float altitude_m;
float light_lx;
float uv_mwcm2;
};
inline void csL() { digitalWrite(FLASH_CS, LOW); }
inline void csH() { digitalWrite(FLASH_CS, HIGH); }
void flashRead(uint32_t addr, uint8_t *buf, uint32_t len) {
csL();
SPI.transfer(CMD_READ);
SPI.transfer(addr >> 16); SPI.transfer(addr >> 8); SPI.transfer(addr);
for (uint32_t i = 0; i < len; i++) buf[i] = SPI.transfer(0x00);
csH();
}
uint32_t readCount() {
uint8_t b[4];
flashRead(COUNT_ADDR, b, 4);
return ((uint32_t)b[0]<<24) | (b[1]<<16) | (b[2]<<8) | b[3];
}
void setup() {
Serial.begin(115200);
delay(1000);
SPI.begin(FLASH_SCK, FLASH_MISO, FLASH_MOSI, FLASH_CS);
pinMode(FLASH_CS, OUTPUT); csH();
uint32_t count = readCount();
Serial.println(F("=== W25QXX + SEN050X — Data Dump ==="));
Serial.print(F("Records: ")); Serial.println(count);
Serial.print(F("Record size: ")); Serial.print(sizeof(Record));
Serial.println(F(" bytes"));
Serial.println();
if (count == 0 || count == 0xFFFFFFFF) {
Serial.println(F("No data. Run the logger first."));
return;
}
// CSV header
Serial.println(F("#,TIMESTAMP_MS,TEMP_C,TEMP_F,HUMID_%,PRESS_HPA,ALT_M,LIGHT_LX,UV_MWCM2"));
Serial.println(F("--- DATA START ---"));
uint32_t dump = (count > 1000) ? 1000 : count;
uint32_t start = (count > 1000) ? (count - 1000) : 0;
if (count > 1000) {
Serial.print(F("# Showing last 1000 of "));
Serial.print(count);
Serial.println(F(" records"));
}
Record rec;
for (uint32_t i = start; i < count; i++) {
uint32_t addr = DATA_ADDR + i * sizeof(Record);
flashRead(addr, (uint8_t*)&rec, sizeof(Record));
Serial.print(i + 1);
Serial.print(','); Serial.print(rec.timestamp);
Serial.print(','); Serial.print(rec.tempC, 1);
Serial.print(','); Serial.print(rec.tempF, 1);
Serial.print(','); Serial.print(rec.humidity, 1);
Serial.print(','); Serial.print(rec.pressure_hpa, 1);
Serial.print(','); Serial.print(rec.altitude_m, 1);
Serial.print(','); Serial.print(rec.light_lx, 0);
Serial.print(','); Serial.println(rec.uv_mwcm2, 2);
delay(150); // line-by-line readable pace
}
Serial.println(F("--- DATA END ---"));
Serial.println();
// Stats
Serial.println(F("=== STATS ==="));
float minT=999, maxT=-999, sumT=0;
uint32_t step = (count > 5000) ? count / 5000 : 1;
uint32_t n = 0;
for (uint32_t i = 0; i < count; i += step) {
flashRead(DATA_ADDR + i * sizeof(Record), (uint8_t*)&rec, sizeof(Record));
if (rec.tempC < minT) minT = rec.tempC;
if (rec.tempC > maxT) maxT = rec.tempC;
sumT += rec.tempC;
n++;
}
Serial.print(F("Temp min:")); Serial.print(minT,1);
Serial.print(F(" max:")); Serial.print(maxT,1);
Serial.print(F(" avg:")); Serial.println(sumT/n, 1);
Serial.print(F("Total: ")); Serial.print(count);
Serial.print(F(" records | flash: "));
Serial.print(count * sizeof(Record) / 1024);
Serial.println(F(" KB"));
Serial.println(F("\nCopy lines between DATA START/END into a .csv file."));
}
void loop() { delay(1000); }The logger sleeps between readings. Every hour it wakes, samples, writes, and returns to sleep. The LED blinks for ~80ms on each write — easy to verify from across the room.
Storage Capacity & LifetimeFor a W25Q32 (4MB chip) logging every 1 hour at 36 bytes per record: you get ~5, 400 days = ~14.8 years of lifetime. The practical limit is ~11 years before hitting the 100k erase cycles per sector.
ConclusionThis is a complete, practical environmental logger built from affordable, widely available parts. The Beetle ESP32-C6 is compact and power-efficient. The SEN0500 captures six key metrics. The W25Q32 flash chip stores 130, 000 readings without the complexity of an SD filesystem. The rechargeable battery lets you deploy it anywhere, and the JUSTWAY 3D-printed case keeps it safe from the elements.
Whether you're monitoring a remote weather station, a greenhouse, a server room, or a museum gallery, this logger delivers a decade of historical data at a glance.
No WiFi. No cloud. No subscriptions. Just pure data, stored locally, forever









Comments