Add ESPHome firmware templates, GND pour notes, v2 lessons learned
- firmware/: config + secrets templates committed, flashed config git-ignored - README: document the GND pour on F.Cu/B.Cu and the module keep-out rule areas - README: correct the calibration button wiring (S8 bCal_in, not an MCU GPIO) - README: v2 list - dedicated 5 V power input, decoupling, mounting holes, I2C pull-ups, schematic annotation, RoHS finish, project name, antenna keep-out
This commit is contained in:
1 parent
7cf9f71a15
commit
ac5a556c11
5 files changed
+187
-38
No files matched your search
@@ -9,6 +9,11 @@ _restore_backup_*/
|
|||||||
*.gbr.bak
|
*.gbr.bak
|
||||||
*.drl.bak
|
*.drl.bak
|
||||||
|
|
||||||
|
# Firmware credentials - never commit live keys
|
||||||
|
firmware/esp-sensair.yaml
|
||||||
|
firmware/secrets.yaml
|
||||||
|
**/secrets.yaml
|
||||||
|
|
||||||
# OS / editor noise
|
# OS / editor noise
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*~
|
*~
|
||||||
|
|||||||
@@ -23,20 +23,21 @@ running.
|
|||||||
```
|
```
|
||||||
+5 V
|
+5 V
|
||||||
|
|
|
|
||||||
+-------------------------+---------------------------+
|
+-------------------------+
|
||||||
| | |
|
| |
|
||||||
| +----------v----------+ |
|
| +----------v----------+
|
||||||
| | ESP32-C3 module |--- 3.3 V out --+|
|
| | ESP32-C3 module |--- 3.3 V out --+
|
||||||
| | (onboard 3V3 LDO) | ||
|
| | (onboard 3V3 LDO) | |
|
||||||
+--------------+ | ||
|
+--------------+ | |
|
||||||
| UART 9600 +----------+----------+ ||
|
| UART 9600 +----------+----------+ |
|
||||||
| | ||
|
| | |
|
||||||
+-----v-------+ +------v-------+ v
|
+-----v-------+ +------v-------+ v
|
||||||
| Sensirion S8| | BME280 |<--- I2C (SDA/SCL) --+
|
| Sensirion S8| | BME280 |<--- I2C (SDA/SCL) -+
|
||||||
| NDIR CO2 | | T / RH / P |
|
| NDIR CO2 | | T / RH / P |
|
||||||
+-------------+ +--------------+
|
+-------------+ +--------------+
|
||||||
|
|
||||||
SW1 (calibration button) -- RC debounce (1 k / 100 n) -- ESP32-C3 GPIO
|
SW1 (calibration button) -- RC (1 k / 100 n) -- Sensirion S8 bCal_in (pin 8)
|
||||||
|
5 V enters at the ESP32-C3 devkit's USB-C connector and is passed to the S8 via the 5 V net.
|
||||||
```
|
```
|
||||||
|
|
||||||
## Connections
|
## Connections
|
||||||
@@ -52,6 +53,10 @@ SW1 (calibration button) -- RC debounce (1 k / 100 n) -- ESP32-C3 GPIO
|
|||||||
| `/SCL` | pad 8 | — | pad 3 |
|
| `/SCL` | pad 8 | — | pad 3 |
|
||||||
| `bCal_in` | — | pad 8 (`bCal_in`) | — |
|
| `bCal_in` | — | pad 8 (`bCal_in`) | — |
|
||||||
|
|
||||||
|
The calibration button SW1 is wired to the **S8's own `bCal_in` input**, not to the MCU: pulling
|
||||||
|
`bCal_in` low starts the sensor's background calibration. C1 (100 nF) and R1 (1 kΩ to GND) form the
|
||||||
|
debounce/RC network on that line.
|
||||||
|
|
||||||
Sensirion S8 pins `Alarm_OC`, `PWM_1kHz`, `DVCC_out` and `UART_R/T` are left unconnected.
|
Sensirion S8 pins `Alarm_OC`, `PWM_1kHz`, `DVCC_out` and `UART_R/T` are left unconnected.
|
||||||
|
|
||||||
## Bill of materials
|
## Bill of materials
|
||||||
@@ -105,28 +110,53 @@ All parts are through-hole or plug-in modules on pin headers; hand soldered with
|
|||||||
The Sensirion S8 requires a 5 V supply (its NDIR heater draws current pulses of up to ~150 mA) and
|
The Sensirion S8 requires a 5 V supply (its NDIR heater draws current pulses of up to ~150 mA) and
|
||||||
about 30 s of preheat time before it reports valid CO₂ values.
|
about 30 s of preheat time before it reports valid CO₂ values.
|
||||||
|
|
||||||
|
## Grounding and copper
|
||||||
|
|
||||||
|
Both copper layers carry a **GND copper pour** (one zone on `F.Cu` + `B.Cu`, filled, thermal-relief
|
||||||
|
connection to pads with 0.5 mm gap, 0.5 mm thermal bridge). Four rule-area keep-outs (pads and
|
||||||
|
footprints excluded, copper pour allowed) mark the module areas, including the ESP32-C3 antenna
|
||||||
|
region.
|
||||||
|
|
||||||
## Firmware
|
## Firmware
|
||||||
|
|
||||||
ESPHome, see [`firmware/`](firmware/) for the config and the Home Assistant wiring.
|
ESPHome on `esp32-c3-devkitm-1`, framework **esp-idf**. Config template:
|
||||||
S8 UART: 9600 baud, 8N1. BME280 I²C: standard address `0x76` (or `0x77` depending on the breakout).
|
[`firmware/esp-sensair.yaml.example`](firmware/esp-sensair.yaml.example), secrets template:
|
||||||
|
[`firmware/secrets.yaml.example`](firmware/secrets.yaml.example). The flashed config and
|
||||||
|
`secrets.yaml` are git-ignored — **never commit live keys.**
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|---|---|
|
||||||
|
| I²C | `SDA = GPIO7`, `SCL = GPIO8`, bus scan enabled |
|
||||||
|
| BME280 | I²C address `0x76`, temperature / pressure / humidity, 60 s interval |
|
||||||
|
| Sensirion S8 | UART `TX = GPIO21`, `RX = GPIO20`, 9600 baud, 60 s interval |
|
||||||
|
| Home Assistant | native ESPHome API with noise encryption, OTA updates |
|
||||||
|
| Network | static IP, `fast_connect`, fallback hotspot + captive portal |
|
||||||
|
|
||||||
## Known issues and lessons learned (v1 → v2)
|
## Known issues and lessons learned (v1 → v2)
|
||||||
|
|
||||||
1. **No copper pours.** Neither layer has a ground plane. v2: solid GND pour on the bottom layer
|
1. **Power comes in through the ESP32-C3 devkit's USB-C 5 V pin.** The Sensirion S8 heater draws
|
||||||
with stitching vias around the ESP32-C3 module.
|
current pulses of up to ~150 mA, and that current travels through the module's USB connector, its
|
||||||
2. **No mounting holes.** The board is held by its connectors only. v2: four M2.5 holes.
|
5 V rail and its onboard regulator path. v2: a **dedicated 5 V power input** (2.54 mm header,
|
||||||
3. **No I²C pull-ups populated.** The design relies on the pull-ups of the BME280 breakout.
|
screw terminal or DC barrel jack) wired straight to the 5 V net, independent of the module's
|
||||||
|
USB-C, which then stays a programming/debug connection. Add to that: 220–470 µF bulk plus 100 nF
|
||||||
|
at the S8 supply pins, reverse-polarity protection (series Schottky or P-MOSFET), a jumper to
|
||||||
|
back-feed 5 V from the module's USB for programming-only setups, and a power-good LED. Before
|
||||||
|
changing anything: measure the ripple and droop on the 5 V rail with an oscilloscope while the S8
|
||||||
|
heater fires, so v2 fixes a quantified problem.
|
||||||
|
2. **Single 100 nF decoupling capacitor** for the whole board. v2: 100 nF per supply pin, bulk
|
||||||
|
capacitance at each sensor, and supply stitching vias into the ground pour.
|
||||||
|
3. **No mounting holes.** The board is held by its connectors only. v2: four M2.5 holes.
|
||||||
|
4. **No I²C pull-ups populated.** The design relies on the pull-ups of the BME280 breakout.
|
||||||
v2: add 4.7 kΩ pull-up footprints on `SDA`/`SCL`.
|
v2: add 4.7 kΩ pull-up footprints on `SDA`/`SCL`.
|
||||||
4. **Single 100 nF decoupling capacitor** for the whole board. v2: 100 nF per supply pin plus a
|
|
||||||
10–100 µF bulk capacitor close to the S8 heater supply, which is the actual current-pulse source.
|
|
||||||
5. **Schematic is not annotated** (references `U`, `U`, `U`), so KiCad BOM export is unusable.
|
5. **Schematic is not annotated** (references `U`, `U`, `U`), so KiCad BOM export is unusable.
|
||||||
v2: annotate, set MPNs, and export the BOM from the schematic.
|
v2: annotate, set MPNs, and export the BOM from the schematic.
|
||||||
6. **Leaded HASL** is not RoHS-compliant. v2: HASL lead-free, and ENIG for any impedance-controlled
|
6. **Leaded HASL** is not RoHS-compliant. v2: HASL lead-free, and ENIG for any impedance-controlled
|
||||||
design.
|
design.
|
||||||
7. **Project name contains `+`.** It propagates into the Gerber file names (`esp+sensair-Front.gbr`),
|
7. **Project name contains `+`.** It propagates into the Gerber file names (`esp+sensair-Front.gbr`),
|
||||||
which some fabricator upload portals and drill parsers reject. v2: `esp-sensair`.
|
which some fabricator upload portals and drill parsers reject. v2: `esp-sensair`.
|
||||||
8. **Antenna keep-out** for the ESP32-C3 module should be checked explicitly against the module
|
8. **Antenna keep-out** for the ESP32-C3 module is defined as a rule area (pads and footprints
|
||||||
datasheet (no copper, no silkscreen, no vias under the antenna area).
|
excluded, copper pour allowed). v2: verify it against the module datasheet's antenna keep-out
|
||||||
|
figure, and check the pour's clearance to the antenna region specifically.
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
@@ -141,7 +171,7 @@ S8 UART: 9600 baud, 8N1. BME280 I²C: standard address `0x76` (or `0x77` dependi
|
|||||||
├── gerber/ Gerber X2, Excellon drills, job file, ready-to-upload zip
|
├── gerber/ Gerber X2, Excellon drills, job file, ready-to-upload zip
|
||||||
├── bom/ bill of materials (CSV)
|
├── bom/ bill of materials (CSV)
|
||||||
├── docs/ schematic PDF, layer plots, 3D render
|
├── docs/ schematic PDF, layer plots, 3D render
|
||||||
├── firmware/ ESPHome configuration
|
├── firmware/ ESPHome config templates, secrets templates, notes
|
||||||
├── photos/ photographs of the assembled board
|
├── photos/ photographs of the assembled board
|
||||||
├── LICENSE CERN-OHL-S-2.0 (hardware)
|
├── LICENSE CERN-OHL-S-2.0 (hardware)
|
||||||
└── LICENSE.MIT MIT (firmware config, BOM, docs)
|
└── LICENSE.MIT MIT (firmware config, BOM, docs)
|
||||||
|
|||||||
+45
-17
@@ -1,31 +1,59 @@
|
|||||||
# Firmware
|
# Firmware
|
||||||
|
|
||||||
ESPHome configuration for the ESP32-C3 CO₂ & environment sensor board.
|
ESPHome firmware for the ESP32-C3 CO₂ & environment sensor board.
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
| File | In git | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `esp-sensair.yaml.example` | yes | configuration template, all credentials as `!secret` references |
|
||||||
|
| `secrets.yaml.example` | yes | secrets template with placeholders |
|
||||||
|
| `esp-sensair.yaml` | **no** (git-ignored) | your flashed configuration |
|
||||||
|
| `secrets.yaml` | **no** (git-ignored) | your Wi-Fi, API, OTA and hotspot credentials |
|
||||||
|
|
||||||
|
Setup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp firmware/esp-sensair.yaml.example firmware/esp-sensair.yaml
|
||||||
|
cp firmware/secrets.yaml.example firmware/secrets.yaml
|
||||||
|
# fill in firmware/secrets.yaml
|
||||||
|
esphome run firmware/esp-sensair.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate the Home Assistant API encryption key with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -c "import base64,os; print(base64.b64encode(os.urandom(32)).decode())"
|
||||||
|
```
|
||||||
|
|
||||||
## Target
|
## Target
|
||||||
|
|
||||||
| Item | Value |
|
| Item | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Chip | ESP32-C3 (RISC-V, single core, Wi-Fi + BLE) |
|
| Chip | ESP32-C3 (RISC-V, single core, Wi-Fi) |
|
||||||
| Framework | ESPHome (`esp32` platform, `framework: esp-idf` or `arduino`) |
|
| ESPHome board | `esp32-c3-devkitm-1` |
|
||||||
| Board | this project's board, flashed via the module's UART/USB boot pins |
|
| Framework | ESP-IDF |
|
||||||
|
| Flashing | USB-C on the devkit module (ESPHome OTA after first flash) |
|
||||||
|
|
||||||
## Peripherals
|
## Peripherals
|
||||||
|
|
||||||
| Peripheral | Bus | Notes |
|
| Peripheral | Bus | Pins | Notes |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| Sensirion S8 (NDIR CO₂) | UART, 9600 baud 8N1 | needs 5 V supply, ~30 s preheat; `bCal_in` starts a manual background calibration |
|
| Sensirion S8 (NDIR CO₂) | UART, 9600 baud 8N1 | `TX = GPIO21`, `RX = GPIO20` | 5 V supply, ~30 s preheat; the S8's own `bCal_in` pin carries the on-board calibration button |
|
||||||
| BME280 (T / RH / P) | I²C, address `0x76` (some breakouts `0x77`) | powered from the module's 3.3 V output; pull-ups come from the breakout |
|
| BME280 (T / RH / P) | I²C, address `0x76` | `SDA = GPIO7`, `SCL = GPIO8` | powered from the module's 3.3 V output, pull-ups on the breakout |
|
||||||
| SW1 | GPIO, active low with 1 kΩ / 100 nF RC debounce | calibration button |
|
| SW1 | — | not on the MCU | wired to the S8 `bCal_in` input with a 1 kΩ / 100 nF RC network |
|
||||||
|
|
||||||
## Add here
|
Update interval for both sensors: 60 s.
|
||||||
|
|
||||||
- `esp-sensair.yaml` — the ESPHome configuration actually flashed to the board
|
|
||||||
- `secrets.yaml` — **do not commit** (Wi-Fi credentials, Home Assistant token); keep
|
|
||||||
`secrets.yaml.example` with placeholders
|
|
||||||
- flashing notes: `esptool` / `esphome flash`, boot-mode entry on the module
|
|
||||||
|
|
||||||
## Home Assistant
|
## Home Assistant
|
||||||
|
|
||||||
The device is exposed as native ESPHome entities: `co2`, `temperature`, `relative_humidity`,
|
Exposed as native ESPHome entities: CO₂, temperature, relative humidity, pressure. API connection is
|
||||||
`pressure`, plus the calibration button as a sensor event.
|
encrypted (noise protocol), OTA updates enabled, static IP with a fallback hotspot and captive portal
|
||||||
|
if the Wi-Fi connection fails.
|
||||||
|
|
||||||
|
## Ideas for v2
|
||||||
|
|
||||||
|
- Trigger the S8 background calibration from Home Assistant (`senseair` platform actions) in addition
|
||||||
|
to the hardware button, and log the calibration events.
|
||||||
|
- Automatic baseline calibration window tuned to the room's nightly CO₂ minimum.
|
||||||
|
- Report Wi-Fi signal strength and device uptime as sensors for diagnostics.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# ESPHome configuration for the ESP32-C3 CO2 & environment sensor board.
|
||||||
|
#
|
||||||
|
# This is the SHARED TEMPLATE. Copy it to esp-sensair.yaml (git-ignored) and keep
|
||||||
|
# all credentials in secrets.yaml (git-ignored). Never commit real keys.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# SPDX-FileCopyrightText: 2026 Hannes Gehrmann
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: esp32c3-bme280
|
||||||
|
friendly_name: esp32c3-bme280
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32-c3-devkitm-1
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
|
||||||
|
logger:
|
||||||
|
|
||||||
|
api:
|
||||||
|
encryption:
|
||||||
|
key: !secret api_encryption_key
|
||||||
|
|
||||||
|
ota:
|
||||||
|
- platform: esphome
|
||||||
|
password: !secret ota_password
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: !secret wifi_ssid
|
||||||
|
password: !secret wifi_password
|
||||||
|
fast_connect: true
|
||||||
|
|
||||||
|
manual_ip:
|
||||||
|
static_ip: !secret static_ip
|
||||||
|
gateway: !secret gateway
|
||||||
|
subnet: !secret subnet
|
||||||
|
dns1: !secret dns1
|
||||||
|
|
||||||
|
# Fallback hotspot if the Wi-Fi connection fails
|
||||||
|
ap:
|
||||||
|
ssid: "Esp32C3-Bme280 Fallback Hotspot"
|
||||||
|
password: !secret ap_password
|
||||||
|
|
||||||
|
captive_portal:
|
||||||
|
|
||||||
|
# BME280 on I2C - SDA on GPIO7, SCL on GPIO8 (board nets /SDA, /SCL)
|
||||||
|
i2c:
|
||||||
|
sda: GPIO7
|
||||||
|
scl: GPIO8
|
||||||
|
scan: true
|
||||||
|
|
||||||
|
# Sensirion S8 on UART - 9600 baud, 8N1 (board nets /TX, /RX)
|
||||||
|
uart:
|
||||||
|
tx_pin: GPIO21 # ESP32-C3 TX -> S8 RX
|
||||||
|
rx_pin: GPIO20 # ESP32-C3 RX <- S8 TX
|
||||||
|
baud_rate: 9600
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: bme280_i2c
|
||||||
|
address: 0x76
|
||||||
|
temperature:
|
||||||
|
name: "BME280 Temperature"
|
||||||
|
pressure:
|
||||||
|
name: "BME280 Pressure"
|
||||||
|
humidity:
|
||||||
|
name: "BME280 Humidity"
|
||||||
|
update_interval: 60s
|
||||||
|
|
||||||
|
- platform: senseair
|
||||||
|
co2:
|
||||||
|
name: "Senseair S8 CO2"
|
||||||
|
update_interval: 60s
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Copy to firmware/secrets.yaml (git-ignored) and fill in your own values.
|
||||||
|
#
|
||||||
|
# Generate a Home Assistant API encryption key with:
|
||||||
|
# python3 -c "import base64,os; print(base64.b64encode(os.urandom(32)).decode())"
|
||||||
|
|
||||||
|
wifi_ssid: YourNetworkSSID
|
||||||
|
wifi_password: YourNetworkPassword
|
||||||
|
api_encryption_key: REPLACE_WITH_GENERATED_32_BYTE_BASE64_KEY
|
||||||
|
ota_password: REPLACE_WITH_AN_OTA_PASSWORD
|
||||||
|
ap_password: REPLACE_WITH_A_FALLBACK_HOTSPOT_PASSWORD
|
||||||
|
static_ip: 192.168.1.50
|
||||||
|
gateway: 192.168.1.1
|
||||||
|
subnet: 255.255.255.0
|
||||||
|
dns1: 192.168.1.1
|
||||||
Reference in new issue
Block a user