diff --git a/E-Paper-Segment-Code.zip b/E-Paper-Segment-Code.zip deleted file mode 100644 index 7d17c7a..0000000 Binary files a/E-Paper-Segment-Code.zip and /dev/null differ diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md deleted file mode 100644 index 2f2ce05..0000000 --- a/PROJECT_MEMORY.md +++ /dev/null @@ -1,391 +0,0 @@ -# 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. - -### ⚙️ Two firmware builds: app vs. bit-probe - -`firmware/main/CMakeLists.txt` selects which source compiles. To switch: -- **App** (`temphummon.c`): `SRCS "temphummon.c"` — the normal temperature/humidity display -- **Probe** (`bitprobe.c`): `SRCS "bitprobe.c"` — interactive bit-field explorer (BOOT-button - advance). Files `dpfinder`/`iconfinder` were folded into `bitprobe.c` across sessions. - -The project name is always `temphummon` (even when bitprobe is compiled), so the ELF/bin are -named `temphummon.*` regardless. See the serial-monitor workflow below for how to watch output. - -### 🖥️ Serial monitor workflow (headless container) - -`idf.py monitor` needs a real TTY and fails in this container. Use a detached `tmux` -session instead (tmux was installed: -`apt-get install -y -qq tmux`): - -```bash -PYENV=/nix/store/h1az2v5l5jx81jdqfzk5q6wnv40cgq4y-python3-3.13.12-env -IDF_MON=/nix/store/4d83mx8ff4ffpcj0x8r4l2x3qd8z3z8k-source/tools/idf_monitor.py -tmux new-session -d -s mon "cd /src/firmware && $PYENV/bin/python $IDF_MON \ - -p /dev/ttyACM0 -b 115200 --toolchain-prefix riscv32-esp-elf- --target esp32h2 \ - --revision 0 --decode-panic backtrace build/temphummon.elf build/bootloader/bootloader.elf \ - 2>&1 | tee /tmp/monitor.log" -# read live log: tr -d '\000' < /tmp/monitor.log | tail -20 -# stop: tmux kill-session -t mon -``` - -To hard-reset the board (re-init a wedged panel) without reflashing: -```bash -$PYENV/bin/python -m esptool --chip esp32h2 -p /dev/ttyACM0 \ - --before=default-reset --after=hard-reset chip-id -``` - -The flash tool needs the port free — kill the tmux monitor first (`tmux kill-session -t mon`). - -### ⚠️ Black-control bits (panel wedging) & I²C timeout - -Probing found several bits that flip the whole panel BLACK and wedge it until a hard -re-init (RST pulse / board power-cycle): `f[2]` b4, `f[4]` b7, `f[13]` b1 (and `f[8]` b4 -caused an I²C hardware timeout). Avoid touching these when composing frames. A single -`epd_write()` does not always clear the wedge — use `epd_lut_GC()` clear or reset the board. - ---- - -## Build State - -### ✅ Successful build - -The firmware compiles and links successfully for ESP32-H2 (RISC-V target). -Current `temphummon.c` build size: `0x2c580` bytes (83% free). - -### Firmware behavior - -Current firmware (`main/temphummon.c`): -- Initializes GPIOs (RST output, BUSY input) and I²C bus -- Initializes display (RST pulse → `0x2B 0xA7 0xE0` power/boost/TSON → temp comp) -- Clears display with **GC (full global refresh) LUT**, then updates with - **DU_WB (partial) LUT** every second -- 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 - (`render_temp()` / `render_humidity()` helpers) -- Prints status via ESP_LOG (`temphummon: T: 27.6°C` every 1s) -- Bit 1 of `f[13]` (`0x02`) is NOT used for °C (leaves the degree circle incomplete) - — `0x05` = °C (bits 0+2) is the correct full-degree rendering - -### Internal temp sensor — accuracy/resolution notes (verified) - -The internal CPU die sensor is **live** (a CPU-stress test during bring-up -raised its reading 27.6 → 28.6°C and held it, confirming the die heats). It -measures the **silicon die**, NOT ambient air — blowing on it barely changes it. -The readout is **coarsely quantized with a per-chip calibration offset**: values -appear stuck in ~1°C steps and always end in `.6` (e.g. 27.6 / 28.6). This is -an inherent limitation of the ESP32-H2 internal sensor, not a firmware bug. -For a real ambient thermometer, wire an **external I²C sensor** (BME280/SHT40) -instead — see TODO. - -⚠️ The display uses a dot-matrix glyph style. A lone bit does not always -produce a clean segment; the erase flash (white frame) is normal before each -partial update. - -### ✅ 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 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 .#`): - -| 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 diff --git a/README.md b/README.md index 35d62dc..af7b664 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,3 @@ Based on the ESP32-H2 and Waveshare Segment E-Paper display. | [1.9inch Segment E-Paper Module](https://www.waveshare.com/product/displays/e-paper/1.9inch-segment-e-paper-module.htm) | 9.47 € | [wiki](https://www.waveshare.com/wiki/1.9inch_Segment_e-Paper_Module_Manual) | Matter over Thread Temperature and humidity monitor - -## 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 | - -> 📖 **Project Memory**: See [`PROJECT_MEMORY.md`](PROJECT_MEMORY.md) for environment setup, -> Nix dev shell usage, build status, hardware details, and next steps. diff --git a/datasheets/1.9inch_Segment_e-Paper_Specification.pdf b/datasheets/1.9inch_Segment_e-Paper_Specification.pdf new file mode 100644 index 0000000..0cf6cc6 Binary files /dev/null and b/datasheets/1.9inch_Segment_e-Paper_Specification.pdf differ diff --git a/firmware/main/CMakeLists.txt b/firmware/main/CMakeLists.txt index 8be0c65..21eab8a 100644 --- a/firmware/main/CMakeLists.txt +++ b/firmware/main/CMakeLists.txt @@ -1,3 +1,2 @@ idf_component_register(SRCS "temphummon.c" - INCLUDE_DIRS "." - PRIV_REQUIRES esp_driver_tsens esp_driver_i2c esp_driver_gpio) + INCLUDE_DIRS ".") diff --git a/firmware/main/bitprobe.c b/firmware/main/bitprobe.c deleted file mode 100644 index 369d019..0000000 --- a/firmware/main/bitprobe.c +++ /dev/null @@ -1,218 +0,0 @@ -/** - * iconfinder2 — ESP32-H2 Zero + 1.9" Segment E-Paper (IST7134 via I²C) - * - * Hunt the BLUETOOTH and BATTERY icons specifically. - * Per user: they are at the "3rd and 4th position on the 13th bit" — - * i.e. frame byte 13 (the °C/°F byte), bits 3 and 4. - * - * We probe byte 13 bits 0..7 one at a time on a BLANK panel so any icon is - * unmistakable. (We already know: bit0+bit2 = °C letter; bit1 = black - * control, so it's guarded.) - * - * Wiring (GPIO4→SDA, GPIO5→SCL, GPIO10→RST, GPIO11→BUSY, 3V3→VCC, GND→GND) - */ -#include -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "driver/i2c_master.h" -#include "driver/gpio.h" -#include "esp_log.h" -#include "esp_timer.h" - -#define PIN_SDA GPIO_NUM_4 -#define PIN_SCL GPIO_NUM_5 -#define PIN_RST GPIO_NUM_10 -#define PIN_BUSY GPIO_NUM_11 -#define PIN_BOOT GPIO_NUM_9 - -#define FRAME_LEN 15 -#define ADVANCE_MODE 1 -#define AUTO_HOLD_MS 3000 -#define BOOT_TIMEOUT_MS 0 - -static i2c_master_bus_handle_t bus; -static i2c_master_dev_handle_t cmd_dev, dat_dev; - -static void epd_reset(void) -{ - gpio_set_level(PIN_RST, 1); vTaskDelay(pdMS_TO_TICKS(200)); - gpio_set_level(PIN_RST, 0); vTaskDelay(pdMS_TO_TICKS(20)); - gpio_set_level(PIN_RST, 1); vTaskDelay(pdMS_TO_TICKS(200)); -} - -static void epd_wait_busy(void) -{ - while (gpio_get_level(PIN_BUSY) == 0) vTaskDelay(pdMS_TO_TICKS(1)); - vTaskDelay(pdMS_TO_TICKS(10)); -} - -static void epd_lut_GC(void) -{ - uint8_t lut[] = {0x82, 0x20, 0x00, 0xA0, 0x80, 0x40, 0x63}; - i2c_master_transmit(cmd_dev, lut, sizeof(lut), pdMS_TO_TICKS(100)); -} -static void epd_lut_DU_WB(void) -{ - uint8_t lut[] = {0x82, 0x80, 0x00, 0xC0, 0x80, 0x80, 0x62}; - i2c_master_transmit(cmd_dev, lut, sizeof(lut), pdMS_TO_TICKS(100)); -} - -static void epd_temperature(void) -{ - uint8_t a[] = {0x7E, 0x81, 0xB4}; - i2c_master_transmit(cmd_dev, a, sizeof(a), pdMS_TO_TICKS(100)); - vTaskDelay(pdMS_TO_TICKS(10)); - uint8_t b[] = {0xE7, 0x0E}; - i2c_master_transmit(cmd_dev, b, sizeof(b), pdMS_TO_TICKS(100)); -} - -static void epd_init(void) -{ - epd_reset(); - vTaskDelay(pdMS_TO_TICKS(100)); - uint8_t p[] = {0x2B, 0xA7, 0xE0}; - i2c_master_transmit(cmd_dev, p, sizeof(p), pdMS_TO_TICKS(100)); - vTaskDelay(pdMS_TO_TICKS(10)); - epd_temperature(); -} - -static void epd_write(const uint8_t *frame, uint8_t extra_byte) -{ - uint8_t w[] = {0xAC, 0x2B, 0x40, 0xA9, 0xA8}; - i2c_master_transmit(cmd_dev, w, sizeof(w), pdMS_TO_TICKS(100)); - i2c_master_transmit(dat_dev, frame, FRAME_LEN, pdMS_TO_TICKS(100)); - i2c_master_transmit(dat_dev, &extra_byte, 1, pdMS_TO_TICKS(100)); - uint8_t d[] = {0xAB, 0xAA, 0xAF}; - i2c_master_transmit(cmd_dev, d, sizeof(d), pdMS_TO_TICKS(100)); - epd_wait_busy(); - uint8_t s[] = {0xAE, 0x28, 0xAD}; - i2c_master_transmit(cmd_dev, s, sizeof(s), pdMS_TO_TICKS(100)); -} - -/* Show byte13 with ONLY the given bit set (blank elsewhere). */ -static void show_b13(uint8_t value, int tag, const char *note) -{ - uint8_t f[FRAME_LEN]; - memset(f, 0, FRAME_LEN); - f[13] = value; - ESP_LOGI("icon2", "T%d: f[13]=0x%02x (%s)", tag, value, note); - - uint8_t off[FRAME_LEN]; - memset(off, 0, FRAME_LEN); - epd_lut_GC(); - epd_write(off, 0x00); - epd_lut_DU_WB(); - epd_write(f, 0x00); -} - -static void boot_init(void) -{ - gpio_config_t bc = { - .pin_bit_mask = (1ULL << PIN_BOOT), .mode = GPIO_MODE_INPUT, - .pull_up_en = GPIO_PULLUP_ENABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, - .intr_type = GPIO_INTR_DISABLE, - }; - gpio_config(&bc); -} - -static void wait_for_advance(void) -{ - if (ADVANCE_MODE == 0) { vTaskDelay(pdMS_TO_TICKS(AUTO_HOLD_MS)); return; } - ESP_LOGI("icon2", "press BOOT to advance next..."); - int64_t t_start = esp_timer_get_time(); - for (;;) { - if (gpio_get_level(PIN_BOOT) == 0) { - vTaskDelay(pdMS_TO_TICKS(30)); - while (gpio_get_level(PIN_BOOT) == 0) vTaskDelay(pdMS_TO_TICKS(10)); - vTaskDelay(pdMS_TO_TICKS(30)); - break; - } - if (BOOT_TIMEOUT_MS > 0 && - esp_timer_get_time() - t_start > BOOT_TIMEOUT_MS * 1000LL) { - ESP_LOGW("icon2", "no BOOT press in %d ms", BOOT_TIMEOUT_MS); - break; - } - vTaskDelay(pdMS_TO_TICKS(20)); - } -} - -void app_main(void) -{ - i2c_master_bus_config_t bc = { - .i2c_port = -1, .sda_io_num = PIN_SDA, .scl_io_num = PIN_SCL, - .clk_source = I2C_CLK_SRC_DEFAULT, .glitch_ignore_cnt = 7, - .flags.enable_internal_pullup = 1, - }; - ESP_ERROR_CHECK(i2c_new_master_bus(&bc, &bus)); - i2c_device_config_t cc = { - .dev_addr_length = I2C_ADDR_BIT_LEN_7, .device_address = 0x3C, .scl_speed_hz = 100000, - }; - i2c_device_config_t dc = { - .dev_addr_length = I2C_ADDR_BIT_LEN_7, .device_address = 0x3D, .scl_speed_hz = 100000, - }; - ESP_ERROR_CHECK(i2c_master_bus_add_device(bus, &cc, &cmd_dev)); - ESP_ERROR_CHECK(i2c_master_bus_add_device(bus, &dc, &dat_dev)); - - gpio_config_t rc = { - .pin_bit_mask = (1ULL << PIN_RST), .mode = GPIO_MODE_OUTPUT, - .pull_up_en = GPIO_PULLUP_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, - .intr_type = GPIO_INTR_DISABLE, - }; - gpio_config(&rc); gpio_set_level(PIN_RST, 1); - gpio_config_t busy_cfg = { - .pin_bit_mask = (1ULL << PIN_BUSY), .mode = GPIO_MODE_INPUT, - .pull_up_en = GPIO_PULLUP_DISABLE, .pull_down_en = GPIO_PULLDOWN_DISABLE, - .intr_type = GPIO_INTR_DISABLE, - }; - gpio_config(&busy_cfg); - - boot_init(); - epd_init(); - - ESP_LOGI("icon2", "== Probe f[13] bits for Bluetooth (3) & power (4) icons =="); - - /* Single bits of byte 13 (skip bit1 = known black control). */ - show_b13(0x01, 1, "bit0 (C stroke)"); - wait_for_advance(); - - show_b13(0x02, 2, "bit1 (SKIPPED-guard, black ctrl)"); - wait_for_advance(); // just wait, we already know it blackens - - show_b13(0x04, 3, "bit2 (C stroke)"); - wait_for_advance(); - - show_b13(0x08, 4, "bit3 = BLUETOOTH?"); - wait_for_advance(); - - show_b13(0x10, 5, "bit4 = POWER/BATTERY?"); - wait_for_advance(); - - show_b13(0x20, 6, "bit5"); - wait_for_advance(); - - show_b13(0x40, 7, "bit6"); - wait_for_advance(); - - show_b13(0x80, 8, "bit7"); - wait_for_advance(); - - /* Combo reference: full °C (0x05) and, if icons exist, try adding them. */ - show_b13(0x05, 9, "0x05 = °C full"); - wait_for_advance(); - - show_b13(0x05 | 0x08, 10, "0x0D = °C + BT?"); - wait_for_advance(); - - show_b13(0x05 | 0x10, 11, "0x15 = °C + bat?"); - wait_for_advance(); - - show_b13(0x05 | 0x08 | 0x10, 12, "0x1D = °C + BT + bat?"); - wait_for_advance(); - - ESP_LOGI("icon2", "== Done. =="); - uint8_t off[FRAME_LEN]; - memset(off, 0, FRAME_LEN); - epd_lut_DU_WB(); - epd_write(off, 0x00); - while (1) vTaskDelay(pdMS_TO_TICKS(1000)); -} diff --git a/firmware/main/temphummon.c b/firmware/main/temphummon.c index 9c3072c..7b66f33 100644 --- a/firmware/main/temphummon.c +++ b/firmware/main/temphummon.c @@ -1,217 +1,6 @@ -/** - * temphummon — ESP32-H2 + 1.9" Segment E-Paper (IST7134 via I²C) - * Shows internal CPU temperature on top line + fixed humidity on bottom. - * - * Frame layout (empirically verified, see PROJECT_MEMORY.md): - * f[0] = leading "1" glyph (bits 0-4) - * f[1],f[2] = top-line tens digit - * f[3],f[4] = top-line ones digit - * f[5],f[6] = bottom-line tens digit - * f[7],f[8] = bottom-line ones digit - * f[9],f[10] = bottom-line tenths digit - * f[11],f[12] = top-line tenths digit - * f[13] = °C (0x05) / °F (0x06) letter - * - * DECIMAL POINT / % (the hard-won bits!): - * f[4] bit5 = top decimal (after top ones) -> "188.8" - * f[8] bit5 = bottom decimal (after bottom ones) -> "88.8" - * f[10] bit5 = percent sign (after bottom tenths) -> "88.8%" - * - * Wiring: GPIO4→SDA, GPIO5→SCL, GPIO10→RST, GPIO11→BUSY, 3V3→VCC, GND→GND - */ -#include -#include -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "driver/i2c_master.h" -#include "driver/gpio.h" -#include "driver/temperature_sensor.h" -#include "esp_log.h" +#include -#define PIN_SDA GPIO_NUM_4 -#define PIN_SCL GPIO_NUM_5 -#define PIN_RST GPIO_NUM_10 -#define PIN_BUSY GPIO_NUM_11 +void app_main(void) +{ -static i2c_master_bus_handle_t bus; -static i2c_master_dev_handle_t cmd_dev, dat_dev; -static temperature_sensor_handle_t tsens; - -/* Digit glyphs from Waveshare reference (byte pair per digit). */ -static const uint8_t DIG[10][2] = { - {0xbf,0x1f},{0x1f,0x00},{0xfd,0x17},{0xf5,0x1f},{0x47,0x1f}, - {0xf7,0x1d},{0xff,0x1d},{0x21,0x1f},{0xff,0x1f},{0xf7,0x1f}, -}; -static const uint8_t DIG_L[10][2] = { - {0xbf,0x1f},{0x1f,0x00},{0xfd,0x37},{0xf5,0x1f},{0x47,0x1f}, - {0xf7,0x1d},{0xff,0x3d},{0x21,0x1f},{0xff,0x3f},{0xf7,0x1f}, -}; - -/* Decimal point & % bits (0x20) */ -#define BIT_DP_TOP 0x20 /* f[4] */ -#define BIT_DP_BOT 0x20 /* f[8] */ -#define BIT_PERCENT 0x20 /* f[10] */ - -/* Status icons in f[13] (empirically verified, see PROJECT_MEMORY.md) */ -#define ICON_DEGC 0x05 /* °C (degree + C) */ -#define ICON_DEGF 0x06 /* °F */ -#define ICON_BT 0x08 /* Bluetooth */ -#define ICON_BAT 0x10 /* battery/power */ -#define ICON_BT_BAT 0x1D /* °C + Bluetooth + battery */ - -/* Debug: burn CPU for a while to stress the die and verify the internal - * temp sensor responds. Set to 1 to spin in a busy loop for STRESS_MS - * after each read (should raise/perturb the die temp). - * Verified: die rises 27.6 -> 28.6°C under load (sensor is live). */ -#define CPU_STRESS_TEST 0 -#define STRESS_MS 3000 - -static void epd_wait_busy(void) { - /* BUSY_N: LOW = busy, HIGH = ready */ - while (gpio_get_level(PIN_BUSY) == 0) vTaskDelay(1); - vTaskDelay(pdMS_TO_TICKS(10)); -} - -static void epd_lut_GC(void) { - uint8_t lut[] = {0x82,0x20,0x00,0xA0,0x80,0x40,0x63}; - i2c_master_transmit(cmd_dev, lut, sizeof(lut), pdMS_TO_TICKS(100)); -} -static void epd_lut_DU_WB(void) { - uint8_t lut[] = {0x82,0x80,0x00,0xC0,0x80,0x80,0x62}; - i2c_master_transmit(cmd_dev, lut, sizeof(lut), pdMS_TO_TICKS(100)); -} - -static void epd_temperature(void) { - uint8_t a[] = {0x7E,0x81,0xB4}; - i2c_master_transmit(cmd_dev, a, sizeof(a), pdMS_TO_TICKS(100)); - vTaskDelay(pdMS_TO_TICKS(10)); - uint8_t b[] = {0xE7,0x0E}; - i2c_master_transmit(cmd_dev, b, sizeof(b), pdMS_TO_TICKS(100)); -} - -static void epd_write(const uint8_t *frame, bool clear) { - uint8_t w[] = {0xAC,0x2B,0x40,0xA9,0xA8}; - i2c_master_transmit(cmd_dev, w, 5, pdMS_TO_TICKS(100)); - i2c_master_transmit(dat_dev, frame, 15, pdMS_TO_TICKS(100)); - uint8_t e = clear ? 0x03 : 0x00; - i2c_master_transmit(dat_dev, &e, 1, pdMS_TO_TICKS(100)); - uint8_t d[] = {0xAB,0xAA,0xAF}; - i2c_master_transmit(cmd_dev, d, 3, pdMS_TO_TICKS(100)); - epd_wait_busy(); - uint8_t s[] = {0xAE,0x28,0xAD}; - i2c_master_transmit(cmd_dev, s, 3, pdMS_TO_TICKS(100)); -} - -static void epd_init(void) { - // Reset - gpio_set_level(PIN_RST, 1); vTaskDelay(pdMS_TO_TICKS(200)); - gpio_set_level(PIN_RST, 0); vTaskDelay(pdMS_TO_TICKS(20)); - gpio_set_level(PIN_RST, 1); vTaskDelay(pdMS_TO_TICKS(200)); - // Power on - uint8_t p[] = {0x2B,0xA7,0xE0}; - i2c_master_transmit(cmd_dev, p, 3, pdMS_TO_TICKS(100)); - vTaskDelay(pdMS_TO_TICKS(10)); - epd_temperature(); -} - -/* Render a value (0-199.9) to the top line: [lead1 ][tens][ones].[tenths] */ -static void render_temp(uint8_t *f, float t) { - if (t < 0) t = 0; - if (t > 199.9) t = 199.9; - - int hi = (int)t / 100; - int te = ((int)t / 10) % 10; - int on = (int)t % 10; - int tn = (int)(t * 10) % 10; - - // tens (top) - if (hi || te) { f[1] = DIG[te][0]; f[2] = DIG[te][1]; } - // ones (top) + DECIMAL (f[4] bit5) - f[3] = DIG[on][0]; - f[4] = DIG[on][1] | BIT_DP_TOP; - // tenths (top) - f[11] = DIG_L[tn][0]; - f[12] = DIG_L[tn][1]; - // leading "1" - if (hi) { - f[0] |= 0x1f; /* left column of the leading "1" glyph */ - } - // °C - f[13] = ICON_DEGC; -} - -/* Render a value (0-99.9) to the bottom line: [tens][ones].[tenths]% */ -static void render_humidity(uint8_t *f, float h) { - if (h < 0) h = 0; - if (h > 99.9) h = 99.9; - - int te = ((int)h / 10) % 10; - int on = (int)h % 10; - int tn = (int)(h * 10) % 10; - - // tens (bottom) - if (te) { f[5] = DIG[te][0]; f[6] = DIG[te][1]; } - // ones (bottom) + DECIMAL (f[8] bit5) - f[7] = DIG[on][0]; - f[8] = DIG[on][1] | BIT_DP_BOT; - // tenths (bottom) + PERCENT (f[10] bit5) - f[9] = DIG_L[tn][0]; - f[10] = DIG_L[tn][1] | BIT_PERCENT; -} - -void app_main(void) { - // Init I²C - i2c_master_bus_config_t bc = {.i2c_port=-1,.sda_io_num=PIN_SDA,.scl_io_num=PIN_SCL, - .clk_source=I2C_CLK_SRC_DEFAULT,.glitch_ignore_cnt=7,.flags.enable_internal_pullup=1}; - ESP_ERROR_CHECK(i2c_new_master_bus(&bc, &bus)); - i2c_device_config_t cc = {.dev_addr_length=I2C_ADDR_BIT_LEN_7,.device_address=0x3C,.scl_speed_hz=100000}; - i2c_device_config_t dc = {.dev_addr_length=I2C_ADDR_BIT_LEN_7,.device_address=0x3D,.scl_speed_hz=100000}; - ESP_ERROR_CHECK(i2c_master_bus_add_device(bus, &cc, &cmd_dev)); - ESP_ERROR_CHECK(i2c_master_bus_add_device(bus, &dc, &dat_dev)); - - // Init GPIO - gpio_config_t rc = {.pin_bit_mask=(1ULL<