ESP32-C6 firmware that lets an existing personal weather station report rain to Rainlog.org without replacing the station.
Some consoles can only upload to Weather Underground, with no way to enter a custom server. This device stands in for Weather Underground: the console uploads to the bridge believing it is talking to WU, and the bridge forwards the reading to Rainlog. It can also relay to the real Weather Underground, so an existing WU station keeps working, once you configure that.
Your console needs no firmware change and no vendor cooperation. It only needs to be pointed at the bridge's WiFi.
Probably not. If your station can upload to a custom server (usually a "Customized" upload setting where you can type a hostname), point it straight at rainlog.org instead. That is simpler, has nothing extra to power, and is what Rainlog's own PWS setup guide recommends. Rainlog accepts both the Weather Underground and Ecowitt upload protocols directly.
This bridge is for stations that cannot enter a custom server at all, such as the AcuRite Iris, where uploading to Weather Underground is the only option the console offers.
It is also still in beta. If you want to try one, email Rainlog first.
1. Get your station credentials from Rainlog.
On rainlog.org, go to Data > View/Edit Gauges, edit your gauge, set its reporting mode to Automatic, and save. The next step does not appear until you have saved.
Back on the gauges list, the gauge now shows a small key icon. Click it. With Weather Underground selected, note two values:
- Station ID, which looks like
Rainlog12345 - Station Key
2. Power on the bridge.
The screen shows a setup message with the bridge's own WiFi name and password.
3. Connect the bridge to your home WiFi.
Join the bridge's WiFi from a phone or laptop. A setup page should open by
itself; if it does not, browse to http://10.41.0.1.
Pick your home network from the list and enter its password. Scan networks refreshes the list if it is empty, and you can also just type the name. Test connection checks the password before you commit to it.
You must also set a new password for the bridge's own WiFi. This is required, not optional: it must be at least 8 characters, the default is rejected, and the bridge will not save until you change it. Write it down, because you need it in the next step and the status screen never shows it again.
Then Save & reboot bridge. The bridge restarts and shows its status screen.
4. Point your weather station at the bridge.
In your console's settings, find where it uploads to Weather Underground and set:
- Station ID: the
Rainlog12345value from step 1 - Station key / password: the station key from step 1
Then join the console to the bridge's WiFi instead of your home WiFi.
Set the protocol to Weather Underground (sometimes called Wunderground). If your console calls this a "Customized" upload, that is fine and expected, just keep the protocol set to Weather Underground.
Do not select the Ecowitt protocol. The bridge only captures Weather Underground uploads. (Rainlog itself accepts Ecowitt, but only for stations uploading to it directly, which do not need a bridge.)
5. Check that it works.
Wait for your console's next upload. The bridge's screen has a Forwarding section whose count rises, and the LED pulses green on each forward. Your readings then appear on rainlog.org.
Do not expect that count to move on every upload. Rainlog accepts one reading per gauge per 5 minutes, so the bridge throttles undated uploads to match. A console that reports every 18 seconds will show its received count climbing steadily while the forwarded count rises only once per 5 minutes. That is working correctly.
To keep uploading to Weather Underground as well, add a relay row on the setup
page. It needs three values: your Rainlog station ID (Rainlog12345), your
WU station ID, and your WU key. A row missing any of them is silently ignored,
so double check all three.
The bridge's screen lists your home WiFi, its own WiFi, and per-device counts, which is usually enough to tell where a problem is. The setup page's Devices tab shows each connected station with any error reason.
- LED dark: normal. The LED is off during healthy operation and only pulses green on a forward.
- LED solid red: an error. Check the screen.
- LED blue: not yet set up, or still connecting.
- Nothing forwarding: confirm the console joined the bridge's WiFi and that its protocol is Weather Underground, not Ecowitt.
You can reach the setup page later from your home network too. Browse to the bridge's home IP shown on its screen and sign in with the bridge's WiFi password.
- BOOT tap: wakes the backlight, which dims after 30 seconds idle.
- BOOT held about 11 seconds: factory reset. Wipes configuration, device names, and stored statistics.
The bridge runs its own access point and joins your home WiFi at the same time, on one radio.
- Bridge side (
10.41.0.1/24, WPA2): your console joins this. The subnet is deliberately off the common home ranges so an overlap with your LAN is very unlikely. Nothing detects one, so a home LAN actually on10.41.0.0/24would misroute the bridge's uplink. - Home side: the bridge's uplink to the internet.
Uploads are captured by impersonating Weather Underground on the bridge side:
net/dns_server.c(UDP :53) answers*.wunderground.comwith the bridge's own address, and does the same for a fixed list of OS connectivity-check hosts so the setup page pops up as a captive portal. That list stays active even once provisioned. Everything else is proxied to the real resolver, except while unprovisioned, when it answers all names.net/capture_server.cserves/weatherstation/updateweatherstation.phpon ports 80 and 443. Some consoles (AcuRite) insist on TLS, hence the HTTPS listener with a self-signed certificate. Seemain/certs/README.mdfor why a private key is committed here on purpose.- It answers
successimmediately and queues the raw query, so a slow uplink never stalls the console. forward/forwarder.csends the reading to Rainlog, appending anrlbridge=<version>marker, and relays to the real WU with the ID and password rewritten when a mapping is configured. Readings timestampednoware throttled to one per gauge per 305 s to stay inside Rainlog's limit; intermediate ones are dropped rather than buffered. Readings carrying a real timestamp are never throttled, and the WU relay is never throttled.
The bridge also NATs for its clients, so the console gets real internet for NTP and vendor services. Some consoles refuse to upload until their clock syncs.
Readings mostly survive a flaky uplink: the forwarder keeps a store-and-forward retry buffer mirrored to flash, so a reboot or an update does not drop an undelivered reading. It drains newest-first, because the WU protocol's rain fields are cumulative and the freshest reading already carries the full total.
The buffer is bounded rather than unlimited, and discards on purpose: 24 entries (oldest evicted first), 30 delivery attempts, and a 6 hour age cap. The flash mirror is also throttled to one write per 15 s, so a crash inside that window loses whatever changed since the last write.
Waveshare ESP32-C6-LCD-1.47. RISC-V single core, WiFi 6, 1.47 inch 172x320 ST7789 LCD.
These units are ESP32-C6FH8 with 8MB flash, despite the Waveshare spec and
demo claiming 4MB. Check yours with esptool flash-id.
Flash size fixes the partition layout and the layouts are not interchangeable,
so there is one image stream per variant, keyed on BOARD_ID (main/board.h).
| Variant | BOARD_ID |
Partitions | Status |
|---|---|---|---|
| C6FH8 (8MB) | esp32-c6fh8-lcd-1.47 |
partitions.csv |
Built and shipping |
| C6FH4 (4MB) | esp32-c6fh4-lcd-1.47 |
partitions-c6fh4.csv |
Staged, untested (no hardware on hand) |
The 4MB layout has no storage partition, so the retry buffer and statistics
would not persist across reboots on that variant.
Shared SPI bus (LCD and SD): SCLK 7, MOSI 6, MISO 5. Single source of truth is
main/board.h.
| Function | GPIO |
|---|---|
| LCD CS / DC / RST | 14 / 15 / 21 |
| LCD backlight | 22 |
| SD CS | 4 |
| RGB LED (WS2812) | 8 |
| BOOT button | 9 |
The onboard WS2812 is RGB wire order on this board, not the usual GRB.
Native ESP-IDF, not Arduino. Every build runs inside Docker against a pinned
espressif/idf image, so no host toolchain is needed.
cp main/config.example.h main/config.h # once, before the first build
./build.sh # build
./build.sh flash # build, flash, serial monitorconfig.h is gitignored and holds only compile-time fallbacks; everything
user-facing is provisioned at runtime. Never commit WiFi passwords or PWS keys.
Flashing defaults to /dev/ttyACM0; override with PORT=/dev/ttyXXX.
./build.sh flash writes the bootloader, partition table, OTA data, and app.
managed_components/ is committed rather than fetched, and dependencies.lock
pins exact versions, so the build never needs network access.
The setup page is a TypeScript + esbuild subproject in web/, built into a
single self-contained main/web/index.html that the firmware embeds.
cd web && npm install && npm run buildThe generated main/web/index.html must be committed: the IDF image has no
Node, so the firmware build never runs npm. CI fails if it is stale.
./fetch-demo.sh downloads the official Waveshare demo (about 60MB, gitignored)
for working ST7789, WS2812, SPI, and SD init sequences.
The bridge checks 2 seconds after its uplink comes up, then backs off by doubling after each check until it settles at a randomized 12 to 24 hour interval (roughly a day of uptime to get there). Updates stream into the inactive slot, are verified against the manifest's sha256 before commit, and reboot. Rollback is enabled: the image is marked valid only about 20 seconds after a good boot, so an update that crash-loops reverts itself.
To cut a release:
- Bump
PROJECT_VERinCMakeLists.txt. It is baked intoesp_app_descand shown on the LCD. ./make-ota.shbuilds and writesdist/rainlog-bridge-<board>-<version>.binplusmanifest-<board>.json.- rsync
dist/to the document root served at/rainlog-bridge-ota/.
Filenames are versioned and immutable. The firmware fetches
manifest-<its own BOARD_ID>.json and rejects a manifest whose board does
not match. A manifest with no board field at all is accepted, for
compatibility with pre-board releases.
OTA replaces the app only. The bootloader and partition table are not touched, so a partition layout change cannot ship over the air and requires a USB reflash.
Scope limits, stated plainly: TLS to the update host validates against the
bundled CA roots, but certificate expiry is not checked, because the build
omits MBEDTLS_HAVE_TIME_DATE. Update integrity rests on the manifest sha256
verified before commit. Secure boot, flash encryption, and anti-rollback are
deliberately out of scope, since none are useful without burning eFuses.
- The two network sides are not routed together for the capture path. The upload endpoint is gated to bridge-side requests, so nothing on the home LAN or the internet can inject a reading.
- The configurator uses one secret, the bridge's own WiFi password. From the
LAN it requires a sign-in that sets a RAM-only session cookie, so a reboot
signs everyone out. HTTP Basic auth with the same password works for
scripting, and no
WWW-Authenticateis ever sent, so browsers never pop a native dialog. - Only the sign-in form throttles failed attempts (1 s penalty). The Basic auth path has no throttle, so password guessing against it from the LAN runs at full speed. Since a correct guess returns the WU upload keys, treat the bridge WiFi password as the real boundary and make it a good one.
- WiFi passwords are never returned by the config API. The per-gauge WU
upload keys are, deliberately:
GET /configincludes them so the page can populate the form, shown behind a Show toggle. They are per-station upload keys rather than network credentials, and the route is already gated. - A private key is committed in
main/certs/, on purpose. Readmain/certs/README.mdbefore concluding otherwise.
MIT, see LICENSE. Three carve-outs it does not cover:
managed_components/joltwallet__littlefs/is vendored third-party code under its own terms.main/net/oui_table.his generated from the public IEEE OUI registry.- The Rainlog name and logo (
main/ui/header_logo.h,tools/icon-512.png,web/src/favicon.png) are branding.
A default build uploads to, and fetches updates from, rainlog.org. A fork for
another service should repoint CFG_RAINLOG_HOST and CFG_OTA_HOST in
main/config.example.h.