329 lines
12 KiB
Markdown
329 lines
12 KiB
Markdown
# Project Memory — temphummon
|
||
|
||
Temperature and humidity monitor using Matter over Thread.
|
||
Based on the **ESP32-H2 Zero** and **Waveshare 1.9" Segment E-Paper** display.
|
||
|
||
---
|
||
|
||
## Quick Reference
|
||
|
||
| Item | Value |
|
||
|---|---|
|
||
| MCU | ESP32-H2 (RISC-V, IEEE 802.15.4 / Thread) |
|
||
| IDF version | v6.0.1 |
|
||
| Chip target | `esp32h2` |
|
||
| Build tool | `idf.py` (via Nix dev shell) |
|
||
| Main source | `firmware/main/temphummon.c` |
|
||
| sdkconfig | `firmware/sdkconfig` |
|
||
| Datasheets | `datasheets/` |
|
||
| CAD files | `cad/` |
|
||
| BOM | See `README.md` |
|
||
| Reference code | `E-Paper-Segment-Code.zip` (Waveshare, in project root) |
|
||
|
||
---
|
||
|
||
## Wiring (ESP32-H2-Zero ↔ E-Paper Module)
|
||
|
||
Fixed and tested — display does NOT overheat with correct wiring.
|
||
|
||
| ESP32-H2-Zero | E-Paper Module | Wire color |
|
||
|:---|:---|:---:|
|
||
| GPIO4 | SDA | 🟢 Green |
|
||
| GPIO5 | SCL | 🟡 Yellow |
|
||
| GPIO10 | RST | 🟠 Orange |
|
||
| GPIO11 | BUSY | 🔵 Blue |
|
||
| 3V3 | VCC | 🔴 Red |
|
||
| GND | GND | ⚫ Black |
|
||
|
||
**Note:** The module version has its own level shifters and LDO — safe to connect directly. The bare panel (without PCB) has different pinout.
|
||
|
||
---
|
||
|
||
## Important: Display Interface = I²C, not SPI
|
||
|
||
The IST7134 driver IC on the 1.9" Segment E-Paper uses **I²C** communication, NOT SPI. The Waveshare reference code confirms this.
|
||
|
||
### I²C Protocol Details
|
||
|
||
| Parameter | Value |
|
||
|:---|:---:|
|
||
| Command address | `0x3C` (A0=0, 7-bit) |
|
||
| Data address | `0x3D` (A0=1, 7-bit) |
|
||
| Speed | 100 kHz |
|
||
| Pull-ups | Internal (enabled in firmware) |
|
||
|
||
The A0 bit is encoded in the I²C address itself: `0x3C` for commands, `0x3D` for data. This is handled by using two separate `i2c_master_dev_handle_t` handles in the firmware.
|
||
|
||
### Display Data Format
|
||
|
||
- 15 bytes per frame
|
||
- 91 segment outputs + 2 background + 1 VCOM (94 bits ≈ 12 bytes, padded to 15)
|
||
- Digit patterns (0-9) from Waveshare reference code
|
||
- Two SRAM banks used: first for image data, second for control
|
||
|
||
### Frame Layout (EMPIRICALLY VERIFIED with bit-probe)
|
||
|
||
The digits are **dot-matrix style** (each bit = one dot / short stroke of a column),
|
||
NOT classic one-bit-per-7-segment. Each digit glyph maps to a **byte pair**. The two
|
||
lines interleave: top-tens, top-ones, bottom-tens, bottom-ones, bottom-tenths, top-tenths.
|
||
|
||
| Frame byte(s) | Display location | Position label |
|
||
|:---|:---|:---|
|
||
| `f[0]` bits 0-4 | Top line, **leading "1"** (hundreds) | — |
|
||
| `f[1], f[2]` | Top line, **tens digit** | pos 1 |
|
||
| `f[3], f[4]` | Top line, **ones digit** | pos 2 |
|
||
| `f[5], f[6]` | **Bottom** line, **tens digit** | pos 3 |
|
||
| `f[7], f[8]` | Bottom line, **ones digit** | pos 4 |
|
||
| `f[9], f[10]` | Bottom line, **tenths digit** | pos 5 |
|
||
| `f[11], f[12]` | Top line, **tenths digit** | pos 6 |
|
||
| `f[13]` | °C / °F letter (0x05 = °C) | — |
|
||
| `f[14]` | Padding / unknown | — |
|
||
|
||
**→ Top line renders as `[1][tens][ones].[tenths] °C`** e.g. `188.8 °C`
|
||
**→ Bottom line renders as `[tens][ones].[tenths] %`** e.g. `88.8 %`
|
||
|
||
### Decimal point & % (THE missing piece — now found! 🎯)
|
||
|
||
All three are **bit 5 (0x20) of the SECOND byte of the pair**, matching the manual:
|
||
"decimal point and % are the 5th place in the 4th, 8th, and 10th positions."
|
||
|
||
| Symbol | Frame byte | Bit | Where it appears |
|
||
|:---|:---|:---|:---|
|
||
| **Decimal point (top)** | `f[4]` | bit 5 (0x20) | after top-line **ones** → `188.8` |
|
||
| **Decimal point (bottom)** | `f[8]` | bit 5 (0x20) | after bottom-line **ones** → `88.8` |
|
||
| **% sign (humidity)** | `f[10]` | bit 5 (0x20) | after bottom-line **tenths** → `88.8%` |
|
||
|
||
### Bluetooth / power icons (FOUND! 🎯 — in f[13], not a special byte)
|
||
|
||
All status icons live in **`f[13]`**, the same byte as the °C/°F letter. The
|
||
manual's "3rd and 4th position on the 13th bit" = bits 3 and 4 of `f[13]`.
|
||
Probing on a blank panel confirmed each bit, and combos compose cleanly.
|
||
|
||
| f[13] value | Bits | Displays |
|
||
|:---|:---|:---|
|
||
| `0x01` | bit0 | part of C letter (standalone = black-control risk) |
|
||
| `0x02` | bit1 | degree sign ° line |
|
||
| `0x04` | bit2 | C/F letter strokes |
|
||
| **`0x05`** | bits0+2 | **°C** (degree + C) |
|
||
| **`0x06`** | bits1+2 | **°F** (per manual) |
|
||
| **`0x08`** | bit3 | **Bluetooth icon** |
|
||
| **`0x10`** | bit4 | **Battery/power icon** |
|
||
| **`0x0D`** | 0x05+0x08 | °C + Bluetooth |
|
||
| **`0x15`** | 0x05+0x10 | °C + battery |
|
||
| **`0x1D`** | 0x05+0x08+0x10 | **°C + Bluetooth + battery** |
|
||
| bits 5-7 | — | unused |
|
||
|
||
All compositions verified live on the panel. (The earlier "no icon found"
|
||
conclusion was wrong - the icons were in f[13] bits 3/4, which the first
|
||
probe had not isolated.)
|
||
|
||
### Planned status-icon behavior (for future battery + Matter work)
|
||
|
||
- **Battery icon** (`f[13]` bit4, 0x10): to turn ON when the device battery
|
||
is **nearly empty** (low-battery warning). Requires adding a battery/gas-
|
||
gauge circuit (e.g. ADC divider on VBAT, or an I²C fuel gauge) later.
|
||
- **Bluetooth icon** (`f[13]` bit3, 0x08): to turn ON when the **Matter/
|
||
Thread network join/connection is successful**, OFF when not connected.
|
||
Requires the Matter cluster + OpenThread bring-up (see TODO).
|
||
- Both compose with °C via OR: `f[13] = ICON_DEGC | (low_bat ? ICON_BAT : 0) |
|
||
(matter_ok ? ICON_BT : 0)` once those signals exist.
|
||
|
||
### Verified glyph segment map (from probing)
|
||
|
||
For a digit byte-pair, the **first byte** = left column + horizontal bars, the
|
||
**second byte** = right column (bits 0-3) + decimal/control (bit 5+).
|
||
Example (pos 1, top tens): byte1 bit5 = top bar, bit6 = middle bar, bit7 = bottom
|
||
bar; byte2 bits 0-3 = right-side dots/bars.
|
||
|
||
⚠️ **Control bits** (probed): second-byte bit 4 of some pairs and byte13 bit1
|
||
flip the whole panel BLACK and wedge the driver until a hard re-init (RST pulse).
|
||
Avoid setting them, or re-init after. (bitprobe had to power-cycle the board.)
|
||
|
||
### Initialization Sequence
|
||
|
||
```
|
||
1. Hardware reset (RST pin: high → low → high with delays)
|
||
2. POWER_ON command (0x2B)
|
||
3. Boost + TSON (0xA7, 0xE0)
|
||
4. Temperature compensation based on ambient temp
|
||
```
|
||
|
||
### Display Refresh Sequence
|
||
|
||
```
|
||
1. Wake from sleep (0xAC)
|
||
2. Power on (0x2B)
|
||
3. Set RAM address (0x40)
|
||
4. Open first SRAM (0xA9), close (0xA8)
|
||
5. Write 15 bytes of image data
|
||
6. Write extra byte (0x00 or 0x03)
|
||
7. Open second SRAM (0xAB), close (0xAA)
|
||
8. Display on (0xAF)
|
||
9. Wait for BUSY pin
|
||
10. Display off (0xAE), HV off (0x28), sleep (0xAD)
|
||
```
|
||
|
||
### Waveform LUTs
|
||
|
||
| Function | Bytes sent | Use case |
|
||
|:---|:---|:---|
|
||
| `epd_lut_DU_WB()` | `82 80 00 C0 80 80 62` | Partial update (white extinction + black out) |
|
||
| `epd_lut_GC()` | `82 20 00 A0 80 40 63` | Full global refresh |
|
||
| `epd_lut_5S()` | `82 28 20 A8 A0 50 65` | Boot waveform (better ghosting) |
|
||
|
||
---
|
||
|
||
## Nix Development Shell
|
||
|
||
The project uses **Nix flakes** to provision the ESP-IDF toolchain.
|
||
|
||
### How to enter the dev shell
|
||
|
||
```bash
|
||
export PATH="/root/.nix-profile/bin:$PATH"
|
||
cd /src && nix develop --command bash
|
||
```
|
||
|
||
### One-off commands (no interactive shell needed)
|
||
|
||
```bash
|
||
cd /src/firmware && nix develop /src --no-write-lock-file --command idf.py build
|
||
```
|
||
|
||
### Nix details
|
||
|
||
- **Flake**: `/src/flake.nix`
|
||
- **Lock**: `/src/flake.lock`
|
||
- **Nix binary**: `/root/.nix-profile/bin/nix` (also at `/nix/store/irfrbndi76zhkvqsfhmsn4a99iafck29-nix-2.35.2/bin/nix`)
|
||
- **Nix config**: `/etc/nix/nix.conf` (has `experimental-features = nix-command flakes`)
|
||
- **Dev shell inputs**: `esp-idf`, `git`, `minicom`, `usbutils`, `gawk`, `coreutils`, `cmake`, `ninja`
|
||
|
||
### ⚠️ Note: Custom nixpkgs-esp-dev fork
|
||
|
||
The flake uses a **custom fork** of `nixpkgs-esp-dev`:
|
||
```
|
||
nixpkgs-esp-dev.url = "github:dvdvgt/nixpkgs-esp-dev/update-v6.0.1";
|
||
```
|
||
|
||
This fork has **few/no cached binary substitutes**, so `nix develop` compiles the entire ESP-IDF toolchain from source. This takes **a long time** on first run but we're keeping it for now since mainline `mirrexagon/nixpkgs-esp-dev` doesn't support ESP-IDF v6.0.1 yet.
|
||
|
||
---
|
||
|
||
## Build State
|
||
|
||
### ✅ Successful build
|
||
|
||
The firmware compiles and links successfully for ESP32-H2 (RISC-V target). Size: `0x2b1b0` bytes (83% free).
|
||
|
||
### Firmware behavior
|
||
|
||
Current firmware (`main/temphummon.c`):
|
||
- Initializes GPIOs (RST output, BUSY input) and I²C bus
|
||
- Initializes display (power on, boost, temperature compensation)
|
||
- Clears display (full refresh cycle)
|
||
- Reads internal CPU temperature, renders on top line as `188.8°C` with
|
||
correct **decimal point** (`f[4]` bit5) and leading "1" (`f[0]`)
|
||
- Renders fixed humidity sample on bottom line as `88.8%` (tens/ones/tenths
|
||
+ decimal `f[8]` bit5 + **percent sign** `f[10]` bit5), updates every 1s
|
||
- Prints status via ESP_LOG
|
||
|
||
### ✅ Hardware bringup (confirmed working)
|
||
|
||
- **ESP32-H2 Zero** detected as rev v1.2 at 96 MHz
|
||
- **Flashing** via USB-Serial/JTAG works at 460800 baud
|
||
- **Display driver** (IST7134 via I²C at 0x3C/0x3D) works:
|
||
- I²C communication succeeds, BUSY handshake works
|
||
- Display clears properly
|
||
- Digits 0–9 show correctly in sequence
|
||
- All-black font pattern renders
|
||
- Deep sleep mode works without errors
|
||
- **No hardware issues** — wiring, level shifting, LDO all correct
|
||
|
||
### Flashing (requires serial connected)
|
||
|
||
```bash
|
||
cd /src/firmware && nix develop /src --no-write-lock-file --command idf.py -p <PORT> flash monitor
|
||
```
|
||
|
||
Or via the flake app:
|
||
```bash
|
||
nix run .#flash
|
||
```
|
||
|
||
---
|
||
|
||
## Hardware Info
|
||
|
||
### ESP32-H2 Zero (Waveshare)
|
||
|
||
- **SoC**: ESP32-H2 (single-core RISC-V 32-bit, up to 96 MHz)
|
||
- **Radio**: IEEE 802.15.4 (Thread, Zigbee) + Bluetooth 5 (LE)
|
||
- **Flash**: 4 MB
|
||
- **PSRAM**: Not present on this board
|
||
- **Schematic**: `datasheets/ESP32-H2-Zero-Schematic.pdf`
|
||
- **Datasheet**: `datasheets/Esp32-h2_datasheet_en.pdf`
|
||
- **TRM**: `datasheets/Esp32-h2_technical_reference_manual_en.pdf`
|
||
- **Wiki**: https://docs.waveshare.com/ESP32-H2-Zero
|
||
|
||
### 1.9" Segment E-Paper Module (Waveshare)
|
||
|
||
- **Driver IC**: IST7134 (datasheet: `datasheets/IST7134.pdf`)
|
||
- **Interface**: I²C
|
||
- **Module schematic**: `datasheets/1.9inch_Segment_e-Paper_Module01.pdf`
|
||
- **Display manual**: `datasheets/1.9inch Segment e-Paper V1.1.pdf`
|
||
- **Module wiki**: https://www.waveshare.com/wiki/1.9inch_Segment_e-Paper_Module_Manual
|
||
- **Reference code**: `E-Paper-Segment-Code.zip` (in project root)
|
||
|
||
#### Module board features
|
||
|
||
- **NDC7002N** dual N-channel MOSFETs for level shifting
|
||
- **RT9193-33** LDO (3.3V regulator)
|
||
- **AO3401** P-MOSFET power switch (on by default without external EN)
|
||
- Two **6-pin male headers**: SDA, SCL, RST, BUSY, VCC, GND
|
||
- Supports 3.3V or 5V input
|
||
|
||
### Other components
|
||
|
||
- **WS2812B RGB LED**: `datasheets/XL-0807RGBC-WS2812B.pdf` (on-board, on GPIO8)
|
||
|
||
---
|
||
|
||
## Next Steps (TODO)
|
||
|
||
- [x] **Board bringup** — flashed and tested the display driver on real hardware
|
||
- [x] **Port Waveshare digit lookup table** — all 10 digits (0–9) verified working on the display
|
||
- [x] **Internal temperature sensor** — reads ESP32-H2 internal temp sensor, shows on display line 1, updates every 1s
|
||
- [x] **Find decimal point & % bits** — probing session mapped them: `f[4]/f[8]` bit5 = decimals, `f[10]` bit5 = %
|
||
- [x] **Find Bluetooth & battery icons** — `f[13]` bit3 = Bluetooth, bit4 = battery; combos with °C work (0x0D/0x15/0x1D)
|
||
- [ ] **Add humidity sensor** — wire up an external I²C sensor (SHT40, BME280, etc.) and show on display line 2
|
||
- [ ] **Add battery + low-battery icon** — battery circuit/gauge; drive `f[13]` bit4 (battery icon) when nearly empty
|
||
- [ ] **Configure Matter/Thread** — enable OpenThread / Matter components in `sdkconfig`; drive `f[13]` bit3 (Bluetooth icon) on successful join
|
||
- [ ] **Implement Matter cluster** — temperature and humidity measurement clusters
|
||
- [ ] **Implement low-power operation** — deep sleep between measurements
|
||
|
||
---
|
||
|
||
## Flake Nix Apps
|
||
|
||
The flake defines these convenience apps (`nix run .#<name>`):
|
||
|
||
| App | Description |
|
||
|---|---|
|
||
| `build` | Build firmware (`idf.py build`) |
|
||
| `flash` | Flash to auto-detected serial port |
|
||
| `monitor` | Open serial monitor |
|
||
| `menuconfig` | Open `idf.py menuconfig` |
|
||
| `clean` | `idf.py clean` |
|
||
| `full` | Build → Flash → Monitor pipeline |
|
||
| `run-agent` | Run pi agent container |
|
||
| `build-agent` | Build pi agent container image |
|
||
|
||
---
|
||
|
||
## Environment Notes
|
||
|
||
- Container: Debian-based (from `Containerfile`), runs as root
|
||
- Working directory: `/src` (mounted volume)
|
||
- Nix is installed but NOT in default PATH — must use `/root/.nix-profile/bin/nix`
|
||
- `.envrc` contains `use flake` (for direnv, not active in this environment)
|
||
- `direnv` state: `.direnv/` directory exists
|