A Wi-Fi environmental sensor node built on a SparkFun Thing Plus ESP32-C6 and a BME280 breakout, reporting temperature, dew point, humidity, pressure, and battery state of charge to my sensors ingest API. It reuses the batcave station's old prefix and channel numbering, so its live data shows up at the same Sensor Nodes report that a much older weather board used to log to before going dark in 2016.

The interesting part of this project isn't really the sensor -- BME280 breakouts are a commodity part -- it's the firmware: a small, reusable Arduino library (github.com/larsi-org/sensor-node) that handles Wi-Fi provisioning, reconnecting after a move, and reporting over HTTPS, so that wiring up the next sensor node is mostly just picking a sensor and writing a loop().

All firmware source is on GitHub: github.com/larsi-org/sensor-node, MIT licensed. It's structured as a standard Arduino library (clone or symlink it into ~/Arduino/libraries/) with two example sketches -- a minimal placeholder-data BasicNode, and the actual BME280Node this device runs.

Two boards, connected by a single Qwiic cable -- no soldering, no wiring diagram needed:

Channel layout, matching the wire protocol's device/channel addressing (see sensor-node.php):

ChannelReadingUnit
0Temperature°C
1Dew Point°C
2Relative Humidity%
3PressurehPa
15Battery State of Charge%

Channel 15 -- the last of a device's 16 -- is reserved sitewide across every sensor-node device for battery state of charge, leaving 0-14 free for whatever the sketch actually measures.

The firmware is built around a small SensorNode Arduino library rather than a single monolithic sketch, so the Wi-Fi/provisioning/logging plumbing is reusable for whatever sensor gets wired up next. A sketch just calls node.begin() once in setup() and node.log({...}) on whatever schedule it wants in loop():

#include <SensorNode.h>

SensorNode node;

void setup() {
  Serial.begin(115200);
  node.begin();  // connects, or runs the setup portal if it can't
}

void loop() {
  float temperatureC = readTemperature();
  node.log({temperatureC});
  delay(node.config().logIntervalMinutes * 60UL * 1000);  // set via the portal
}

The device has no hardcoded Wi-Fi credentials. If it can't connect to any network it already knows, it opens its own open access point (SensorNode-Setup-XXXX) with a captive setup page: pick a network from a live scan, enter the password, and set a node name, an optional location (e.g. "basement" -- lets two nodes at the same write key share a name but still report distinct network hostnames), device ID, write key, and how often it should report (1–60 minutes). It remembers up to 3 networks, most-recently-added first -- so a node that moves between a couple of locations (a workbench and its final install spot, say) reconnects automatically on the way back instead of needing reprovisioning every time. When the portal does come up because none of them are in range anymore, it's pre-filled with everything except the network password, since the common case is just "this moved somewhere new," not "start over" -- leave the password blank to keep the one already saved for a known network.

Once connected, the device syncs its clock over NTP (needed so it can validate the server's TLS certificate correctly) and posts each reading to log as device|v0,v1,v2,v3, HTTPS only, with the connection verified against a pinned root CA rather than trusting the system's default certificate store. A reading left blank is skipped entirely rather than logged as zero, so a sensor that fails to read on a given cycle doesn't pollute the data with a false reading. See sensor-node.php for the full wire protocol if you want to log your own device to this same API.

A write key is a 16-character, per-station credential -- not a shared API key -- gating what prefix a device is allowed to log to. See sensor-node.php for how to request one and the exact key format if you're building your own client.