For CircuitPython Day 2026 I wanted to create an E-ink CO2 monitoring project for a friend.
They wanted to be able to see how long things were in bad ranges after the fact, mostly not looking at the data until a problem occurred and was rectified, like a post-mortem of their setup. There's no internet over wifi (just phone data), and they prefer / are used to using Bluetooth on the phone with another data logger.
To make it more exciting for me I wanted to play with a few technologies, namely an *ESP-Now data collector, Offline SSL WiFi + Web-BLE control/analysis page, and Web-AI thrown in for local offline model use in the browser!*
*Web-AI and the Prompt API are limited to Chrome currently, and Web-MCP is a time restricted origin-trial for now.
Hardware / Parts Used
It was appropriate timing as Adafruit have just released the Sensirion SCD43 (most accurate so far) along with a plethora of E-ink panels + displays, my most recent acquisition being the Quad colour 3.52" panel.
Alternative components
This project was designed for two displays, the 2.9" or 3.52" e-inks, but anything should work (adjusting display layout a bit).
The Host controller can be anything really, all transports (BLE/WiFi/esp-now) should be optional (Espressif WiFi for espnow), but this was set up for the Feather C6 or S3, combined with the E-ink Feather Friend.
The remote sensor nodes are a similar spec, without displays or e-ink feather friends.
Sensor-wise, the code expects a Sensirion SCD4x series (40/41/43) or SCD30 Carbon dioxide sensor, or alternatively a combined Air Quality CO2 sensor like the SEN66. -# (The SEN63c is not recommended for remote nodes, but is fine for the host controller where it's more about approximate household values rather than more accurate data)
How does it work and what does it do?
So this project is an ESP-Now based communication device (less bandwidth and no real need for WiFi AP dependency although can host one if needed), with it's own offline sensor and SD card, and acts as a data collector for remote sensor nodes.
The devices are set up with a wifi AP to begin with (and a captive portal) allowing setting of the device name and sensor polling frequency. If the esp-now based host controller cannot be accessed the the nodes create the setup wifi AP again (@tyeth remember to check what happens if host goes offline and returns - they happily resume but more testing worth doing).
The main host controller is expected to be powered (ideally with battery backup) and displays it's data along with the remote nodes, compared against high_warn / high_bad / low_warn and low_bad thresholds for each metric (co2, RH, temp, particles, battery).
So how do we view the data on a bigger screen, or historical data?
There's a GitHub Pages site mirrored on the host controllers Wifi AP captive portal. It supports https and http locally, so that web-AI is supported* [https is mandated by Browsers for certain technologies like Web-BLE or phone sensor access].
This is a demo link that prepopulates data files into the page (no syncing from device needed):
https://tyeth.github.io/deepsleep_espnow_wifi_and_ble_env_collector/webapp/?demo=1
The host has the page available served over the captive wifi portal just in case no internet is around, and the needed plotly.js is stored in flash or on the SD card for easy serving.
Lastly pyodide is available with access to the data and sqlite3 and numpy, but being 10mb it's an SD or online only option.
The final treat on the page is the use of the experimental Web-AI Prompt API in Google Chrome, allowing the downloading of a model (nano-flash or Gemma4*) and then interrogating your data via the locally run model in the browser.
Offline web page resources (for graphs and AI analysis)
What's clever is that by visiting the webpage from github first, you can get the https support allowing Web-AI, download the Model if needed along with all the JS libraries super quickly.
Then you switch to the Sensors WiFi network (no internet) and the page remains working. It's also possible to just visit the webpage initially, but then the JS files will be slowly served from the ESP32.
The page still allows accessing the esp32 host controller on a known fixed address (192.168.4.1) via an openly free SSL certificate from 192dot168dot4dot1.gundryconsultancy.com which is updated by LetsEncrypyt every 60days. Of course you can also use BLE instead to transfer the data from host.
Poor forgetful little computer... Keeping Time and new SSL certs updated
Because the Host controller device boots with no time, and maybe an out of date SSL certificate, the phone or PC provide updated time (and optionally SSL certs) simply by connecting to the website and then using web-ble or the wifi REST API to connect to the host controller. At that point all the stored data with <2020 timestamps get synced with the relative offset to current time giving correct dates.
The days of data are also synced to the browser in the same way, over BLE or WiFi API, saved to localstorage.
It would be relatively cheap and easy to include a Real-Time Clock (RTC) as they have battery backups and would retain time after power loss. As it is, the first visit to the portal page syncs time from the browser 🧠
Room for improvement
Alas the phone (android) doesn't support Web-AI yet* (ML-Kit promises in future), and other browsers have so far refused the temptation of the Prompt API (with good reasons), so if no internet exists then a Laptop is required to use the Web-AI model (and have downloaded a model previously or be briefly online).
The Nano-flash model was at times really quick and effective, although pretty inconsistent and at times ran for minutes before a response. Gemma4 model will likely be much better (will update after testing).
Of course as models get better there will eventually be something worth running locally on the esp32.
For kicks pretty early on I wanted to think about this structured output schema and then move onto Tool calls from the agents responses, and it worked surprisingly well (not to spec / hacky).
https://github.com/tyeth/deepsleep_espnow_wifi_and_ble_env_collector/compare/main...ai-tool-loop
Longer term, it would be nice if the agent had access to more tooling (Web-MCP), like the whole of pyodide, and the ability to reuse the graphs to highlight a related region of interest. Sadly it probably wants a sidebar or floating widget so the "Ask" and response summary is always available...
Lastly CircuitPython itself comes with some occasional gotchas, community contributions (esp-now), bleeding edge features (like BLE improvements), and heavy memory usage woes if using graphics and radio, especially when you're a glutton for punishment like myself and attempt an SSL server + BLE + espnow + display!
🪞🔫🦶🤓
I ended up compiling circuitpython with extra flags for memory radio buffer size, adding SSL session tickets for cheaper connection resuming, turning off BLE workflow in settings.toml, and using the new supervisor.unsafe_disable_usb_storage to allow removing MSC write permission from the PC and restoring to the MCU (to save settings changes etc).
To Be Continued...
I love the optimisation of running things via local models, avoid burning the world unnecessarily and all that, but clearly there's work to be done. I figure by next year it'll be worth revisiting the mailing list - Built-in AI: Early Preview Program (Chrome) for some mobile-centric ML-Kit stuff.
Well this was also a massive excuse to practise using my CNC for more complex toolpaths, mixing Wood and 3d printing, and attempting to probe workpieces.
There will be a follow up post showing the enclosure / composite design, and in the mean-time here's the latest yak shaving mission en-route, in my tangential quest for CNC probing accuracy and convenience:
This page (CircuitPython Day 2026 - ESP-Now offline sensor + remote data collector, WiFi + Web-BLE control/analysis page, and Web-AI thrown in for local offline model use in the browser!) was last updated on September 16, 2026.
Text editor powered by tinymce.