{"id":16787202,"url":"https://github.com/tech1k/helloesp","last_synced_at":"2026-04-26T02:02:31.443Z","repository":{"id":41103677,"uuid":"508074411","full_name":"Tech1k/helloesp","owner":"Tech1k","description":"A public website hosted on an ESP32.","archived":false,"fork":false,"pushed_at":"2026-04-21T07:08:35.000Z","size":2183,"stargazers_count":123,"open_issues_count":0,"forks_count":7,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-04-21T09:21:56.115Z","etag":null,"topics":["arduino","bme280","ccs811","cloudflare-workers","esp32","espressif","iot","microcontroller","oled","platformio","webserver","website","websocket"],"latest_commit_sha":null,"homepage":"https://helloesp.com","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Tech1k.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":"tech1k","custom":["https://kristiankramer.dev/donate","https://www.buymeacoffee.com/kristiankramer"]}},"created_at":"2022-06-27T22:02:38.000Z","updated_at":"2026-04-21T09:17:48.000Z","dependencies_parsed_at":"2022-07-08T19:50:23.718Z","dependency_job_id":"63f8997c-8040-4e40-a020-aeea6416f524","html_url":"https://github.com/Tech1k/helloesp","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Tech1k/helloesp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tech1k%2Fhelloesp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tech1k%2Fhelloesp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tech1k%2Fhelloesp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tech1k%2Fhelloesp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Tech1k","download_url":"https://codeload.github.com/Tech1k/helloesp/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Tech1k%2Fhelloesp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32283294,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-25T18:29:39.964Z","status":"online","status_checked_at":"2026-04-26T02:00:05.962Z","response_time":129,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["arduino","bme280","ccs811","cloudflare-workers","esp32","espressif","iot","microcontroller","oled","platformio","webserver","website","websocket"],"created_at":"2024-10-13T08:14:27.271Z","updated_at":"2026-04-26T02:02:31.438Z","avatar_url":"https://github.com/Tech1k.png","language":"HTML","readme":"# HelloESP\n\n[![HelloESP status](https://helloesp.com/status.svg)](https://helloesp.com)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/license/mit/)\n[![Build](https://github.com/Tech1k/helloesp/actions/workflows/build.yml/badge.svg)](https://github.com/Tech1k/helloesp/actions/workflows/build.yml)\n[![PlatformIO](https://img.shields.io/badge/PlatformIO-Arduino-orange)](https://platformio.org/)\n\nA public website running entirely on a single ESP32 with 520 KB of RAM. Every page, every sensor reading, every guestbook entry is served by the microcontroller itself. There is no backend server.\n\n**Live at [helloesp.com](https://helloesp.com)**\n\n![HelloESP in its display frame](.github/assets/hero.jpg)\n\n---\n\n## Why?\n\nBecause it's fun to see how far a $10 microcontroller can go. The last version ran until 2023 on an ESP32 behind a Cloudflare tunnel. It eventually went offline and the domain lapsed.\n\nYears later I came back to it. The internet had gotten heavier in the meantime: more of everything, most of it wasteful. This whole website weighs less than a single phone wallpaper. I wanted to see what a single small chip could still do against all that. The domain was available again, which felt like permission.\n\nThis time it stays up. If it goes down, I fix it. That's the point now.\n\n## How it works\n\nThe ESP32 holds a persistent outbound WebSocket to a Cloudflare Worker. When a browser hits `helloesp.com`, the Worker relays the request over that socket and streams the response back. The ESP never accepts an inbound TCP connection from the internet, only from the LAN.\n\n```\n Browser ──HTTPS──▶ Cloudflare Worker ──WSS──▶ ESP32 (on your home network)\n                         ▲                        │\n                         └────── response ────────┘\n```\n\nResponses larger than a single WebSocket frame are chunked and base64-encoded. Admin endpoints return 404 through the relay and are only reachable on the LAN. A second shared secret (HMAC) can be enabled so a leaked Worker secret alone cannot impersonate the device.\n\nThe same WebSocket also carries **live push events** the other direction: every 5 seconds the device pushes sensor stats, and every tracked public request triggers a console event. The Worker fans these out to connected browsers via Server-Sent Events (`/_stream`), so the homepage ticks in real time without polling.\n\nWithout the Worker, the site still runs on LAN via mDNS at `http://helloesp.local`.\n\n## Hardware\n\n| Part | Role |\n|---|---|\n| ESP32 DOIT DevKit V1 | MCU, 520 KB RAM, dual-core Xtensa |\n| BME280 | Temperature, humidity, barometric pressure |\n| CCS811 | CO₂, VOC |\n| SSD1306 128×64 | OLED, rotating info pages with burn-in shift |\n| Micro SD card (FAT32, ≤32 GB) | Filesystem: HTML, images, logs, config |\n| Status LED | Blinks on every public visit |\n| Notification LED | Lights when the guestbook has pending entries |\n\n### Wiring\n\n| Signal | ESP32 Pin |\n|---|---|\n| I²C SDA (BME280, CCS811, OLED) | GPIO 21 |\n| I²C SCL (BME280, CCS811, OLED) | GPIO 22 |\n| SD CS | GPIO 5 |\n| SD MOSI | GPIO 23 |\n| SD MISO | GPIO 19 |\n| SD SCK | GPIO 18 |\n| Status LED | GPIO 33 |\n| Notification LED | GPIO 32 |\n| CCS811 WAKE | GND |\n\nI²C addresses: BME280 `0x76`, CCS811 `0x5A`, SSD1306 `0x3C`.\n\n## Features\n\n**Frontend**\n- Live dashboard: 12 sensor/system metrics, trend arrows, degraded-sensor indicators\n- Real-time SSE updates (push every 5s from the device), with graceful fallback to 30s polling\n- Live connection indicator (pulsing green dot) and a \"Right now\" request ticker on the homepage\n- Historical CSV charts with day/week/month switcher; weekly/monthly/yearly aggregate archives\n- Hall of Fame (lifetime extremes: peak CO₂, temp range, busiest day, longest uptime) and year-over-year monthly visitor deltas on `/history`\n- Visitor country map, request-rate chart, changelog, photo carousel\n- `/console` live feed: last 50 public requests with country flags, updated instantly via SSE\n- Outdoor weather context (via Cloudflare Worker proxy to Open-Meteo)\n- Guestbook with submission, moderation queue, rate limiting\n- Dark mode, responsive, SRI-pinned CDN scripts, no tracking\n\n**Firmware**\n- Full async HTTP server over WebSocket-relayed traffic\n- Non-blocking WebSocket client with exponential backoff (5 s → 300 s cap)\n- Atomic writes for all critical files (`tmp → bak → rename`)\n- Period aggregation: weekly, monthly, yearly checkpoints\n- CSV sensor logging every 5 minutes\n- NTP with three-server failover and bounded boot retry\n- WiFi runtime watchdog: reboots if disconnected \u003e10 min\n- Heap safety net: reboots at \u003c20 KB free rather than hang\n- Event-driven SSE push for sensor stats (5s) and console entries (on each request)\n- Admin panel (LAN-only): OTA updates, file manager, full SD backup/restore, R2 backup health + liveness test, SMTP2GO test email, self-test, sensor + error log viewers, device health sparklines (heap + RSSI), Worker link status, data management (reset counters / export state), maintenance mode toggle\n- OLED boot sequence, rotating runtime pages, burn-in protection\n\n**Edge**\n- Cloudflare Worker (Durable Object) with per-IP rate limiting, 8 KB POST cap\n- SSE fanout hub: multiple browser viewers, constant ESP load\n- Maintenance mode with auto-expiring window and dedicated 503 page\n- Auto-retry + pulsing indicator on all error pages (offline / timeout / maintenance)\n- Hourly outdoor-weather refresh cached in the Durable Object\n- Embeddable live status badges at `/status.svg` (and `/status-wide.svg`)\n- Optional HMAC challenge-response device auth\n- Optional SMTP2GO integration for guestbook-pending alerts, dead-man's-switch (device silent \u003eN hours), backup failures, and overdue-backup warnings\n- Optional daily off-site backups to Cloudflare R2 with GFS rotation (7 daily + 4 weekly + 12 monthly + yearly) and sha256 integrity manifests\n- Security headers, no-cache list for dynamic endpoints\n- RSS feeds (`/changelog.rss`, `/guestbook.rss`), `sitemap.xml`, `robots.txt`, `.well-known/security.txt`\n\n### Embeddable status badges\n\nFour variants of the compact badge (shields-compatible, edge-cached 60s):\n\n```markdown\n![](https://helloesp.com/status.svg)                    # default: uptime\n![](https://helloesp.com/status.svg?metric=visits)      # visit count\n![](https://helloesp.com/status.svg?metric=temp)        # current temperature\n![](https://helloesp.com/status.svg?metric=online)      # online/offline pulse\n```\n\nPlus a wide stat card with multiple live metrics:\n\n```markdown\n![](https://helloesp.com/status-wide.svg)\n```\n\nBoth pull from the Worker's cached sensor state. Zero ESP load regardless of how many pages embed them. State-aware colors (live, stale, offline, maintenance).\n\n## Setup\n\n### 1. Firmware\n\n```bash\ngit clone https://github.com/Tech1k/helloesp.git\ncd helloesp\npio run -t upload\n```\n\nRequires [PlatformIO](https://platformio.org/).\n\n### 2. SD card\n\nFormat a FAT32 SD card (≤32 GB) and copy the contents of `data/` to the root. The `data/` folder is **not** flashed. It lives on the SD card.\n\n### 3. Config\n\nRename `config.example.txt` to `config.txt` on the SD card and fill in:\n\n```\nwifi_ssid=YOUR_SSID\nwifi_pass=YOUR_PASSWORD\nadmin_user=admin\nadmin_pass=your-admin-password\ntimezone=MST7MDT,M3.2.0,M11.1.0\n```\n\nTimezone is a POSIX TZ string. Common US examples are listed in the file. Leave `worker_url`, `worker_key`, and `device_key` blank to run LAN-only.\n\n### 4. Cloudflare Worker (optional, for public access)\n\n```bash\n# Generate a shared secret; don't pick one by hand\nopenssl rand -hex 32\n```\n\nThe same value goes in two places:\n- `worker_key` in the SD `config.txt`\n- Worker secret `WORKER_SECRET`\n\nThen deploy:\n\n```bash\ncd worker\nwrangler deploy\nwrangler secret put WORKER_SECRET   # paste the value from openssl\n```\n\nAdd `worker_url` to `config.txt` (e.g. `helloesp.com`). The ESP will connect on next boot.\n\n### 5. HMAC device auth (optional)\n\nAdds a second secret so a leaked `WORKER_SECRET` alone cannot impersonate the device. On every reconnect, the Worker sends a random nonce and the ESP signs it with the device key.\n\n```bash\nopenssl rand -hex 32\n```\n\nSet the same value as `device_key` in `config.txt` and:\n\n```bash\nwrangler secret put HMAC_SECRET\n```\n\nIf either side is unset, HMAC is disabled and auth falls back to `WORKER_SECRET` only.\n\n### 6. Email alerts via SMTP2GO (optional)\n\nConfiguring [SMTP2GO](https://smtp2go.com/) once unlocks every alert channel the Worker uses:\n\n- Guestbook moderation notifications (throttled 1/5min)\n- Dead-man's-switch: device silent for longer than `DEADMAN_HOURS` (default 6) + recovery email when it comes back\n- Backup failures (throttled 1/hr) and overdue-backup warnings (\u003e48 h since last success, 1/day)\n- Manual test email from the admin panel\n\n```bash\nwrangler secret put SMTP2GO_KEY\nwrangler secret put NOTIFY_EMAIL\nwrangler secret put NOTIFY_FROM    # optional, e.g. \"HelloESP \u003cno-reply@yourdomain\u003e\"\nwrangler secret put DEADMAN_HOURS  # optional, default 6, accepts fractional hours\n```\n\nThe ESP never blocks on outbound HTTPS; all email sending lives on the Worker. If any required secret is unset, the corresponding alerts silently no-op.\n\n### 7. Off-site backups to R2 (optional)\n\nFull SD snapshots go to Cloudflare R2 once a day at 4 AM local. Excludes `config.txt`, `*.tmp`/`*.bak` files, and `/logs`. Each backup lives at `state/YYYY-MM-DD/` with a sha256 manifest; `state/latest.json` is the atomic commit pointer.\n\n```bash\nwrangler r2 bucket create helloesp-backup\nwrangler deploy\n```\n\nThe binding is declared in `wrangler.toml`. If the binding isn't present, the Worker falls back to emailing the bundle as an attachment (if SMTP2GO is configured) so you don't silently lose backups.\n\n**Rotation (Grandfather-Father-Son).** Keeps 7 daily + 4 weekly (Sundays) + 12 monthly (1st of month) + every Jan 1 forever. Anything outside those rules older than 8 days is pruned. A prefix guard refuses to delete anything not matching `state/YYYY-MM-DD/`.\n\n**Storage math.** ~1.4 MB per snapshot × ~23 retained snapshots ≈ 32 MB/year. Well under R2's 10 GB free tier.\n\n**Alerting.** No email on success. A single email fires if a backup fails (throttled 1/hr) or if no successful backup has been committed for \u003e48 h (once per day).\n\n### Restoring from an R2 backup\n\n1. Format a new FAT32 SD card (≤32 GB).\n2. Copy the `data/` folder from this repo to the SD root. HTML/CSS/JS/images are deploy-recreatable and not in the backup.\n3. Open the R2 bucket. Read `state/latest.json` for the most recent snapshot date.\n4. Download every object under `state/{date}/`, preserving subdirectories (e.g. `stats/weekly/2026-W16.json` → `/stats/weekly/2026-W16.json` on SD).\n5. Recreate `config.txt` from your own records. Secrets aren't in the backup on purpose.\n6. Insert the SD card and boot.\n\nFor single-file fixes, download one object from R2 and drop it in via the admin file manager. No full restore needed.\n\n## Security\n\n- `config.txt` is in `.gitignore`. Do not commit it. Treat `WORKER_SECRET` and `device_key` like passwords.\n- Admin endpoints (`/admin`, `/admin/*`, `/_upload`, `/_ota`, `/guestbook/pending`, `/guestbook/moderate`) return 404 to any request arriving through the Worker. They are only reachable from the LAN.\n- Admin Basic Auth uses a per-IP lockout (5 failed attempts → 10-minute block).\n- Worker enforces 60 req/min per IP, caps POST bodies at 8 KB, and strips hop-by-hop headers.\n- CDN scripts are pinned with SHA-384 SRI hashes.\n- Guestbook input is stripped of control bytes and CSV-breaking characters before storage; output is JSON- and HTML-escaped on both sides.\n\n## Limitations\n\nIf you're thinking about building your own, here's what to expect:\n\n- **Concurrency is modest.** Roughly 5 simultaneous requests before latency is noticeable. Cloudflare's edge absorbs bursts of static content; dynamic endpoints hit the chip.\n- **No HTTPS origin.** TLS terminates at the Worker. LAN access is HTTP only, so don't treat the admin panel as secure without a VPN or physical access.\n- **Single point of failure.** One chip, one WiFi link, one SD card. A power blip takes the site down until reboot (usually under 30 seconds).\n- **SD wear.** CSV logging every 5 min plus guestbook writes is a few hundred writes per day. Consumer SD cards will last years, not decades; swap annually if the site matters.\n- **Memory is tight.** 520 KB total, ~180 KB free at idle. Large responses or many concurrent frames can OOM; a heap watchdog reboots at under 20 KB free rather than hang.\n\n## Repository layout\n\n```\nsrc/main.cpp          Firmware (Arduino framework)\ndata/                 SD card contents: HTML, CSS, JS, images, favicon SVG\nworker/worker.js      Cloudflare Worker + Durable Object (relay, SSE, weather, badges)\nworker/wrangler.toml  Worker config\nplatformio.ini        Build config\n.github/workflows/    CI (PlatformIO build on push + PR)\n.github/assets/       README hero image\n```\n\n## Credits\n\n- [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer)\n- [AsyncTCP](https://github.com/me-no-dev/AsyncTCP)\n- [Adafruit BME280](https://github.com/adafruit/Adafruit_BME280_Library)\n- [Adafruit CCS811](https://github.com/adafruit/Adafruit_CCS811_Library)\n- [Adafruit SSD1306](https://github.com/adafruit/Adafruit_SSD1306) + [GFX](https://github.com/adafruit/Adafruit-GFX-Library)\n- [Uptime Library](https://github.com/YiannisBourkelis/Uptime-Library)\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n","funding_links":["https://github.com/sponsors/tech1k","https://kristiankramer.dev/donate","https://www.buymeacoffee.com/kristiankramer"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftech1k%2Fhelloesp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftech1k%2Fhelloesp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftech1k%2Fhelloesp/lists"}