diff --git a/E-Paper-Segment-Code.zip b/E-Paper-Segment-Code.zip new file mode 100644 index 0000000..7d17c7a Binary files /dev/null and b/E-Paper-Segment-Code.zip differ diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md new file mode 100644 index 0000000..40e65f2 --- /dev/null +++ b/PROJECT_MEMORY.md @@ -0,0 +1,292 @@ +# 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%` | + +### 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 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 = % +- [ ] **Add humidity sensor** — wire up an external I²C sensor (SHT40, BME280, etc.) and show on display line 2 +- [ ] **Configure Matter/Thread** — enable OpenThread / Matter components in `sdkconfig` +- [ ] **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 af7b664..35d62dc 100644 --- a/README.md +++ b/README.md @@ -12,3 +12,17 @@ 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 deleted file mode 100644 index 0cf6cc6..0000000 Binary files a/datasheets/1.9inch_Segment_e-Paper_Specification.pdf and /dev/null differ diff --git a/firmware/main/CMakeLists.txt b/firmware/main/CMakeLists.txt index 21eab8a..8be0c65 100644 --- a/firmware/main/CMakeLists.txt +++ b/firmware/main/CMakeLists.txt @@ -1,2 +1,3 @@ idf_component_register(SRCS "temphummon.c" - INCLUDE_DIRS ".") + INCLUDE_DIRS "." + PRIV_REQUIRES esp_driver_tsens esp_driver_i2c esp_driver_gpio) diff --git a/firmware/main/bitprobe.c b/firmware/main/bitprobe.c new file mode 100644 index 0000000..ac4ca77 --- /dev/null +++ b/firmware/main/bitprobe.c @@ -0,0 +1,226 @@ +/** + * dpfinder — ESP32-H2 Zero + 1.9" Segment E-Paper (IST7134 via I²C) + * + * Targeted decimal-point / % tester, guided by the manual: + * "The decimal point and % are the 5th place in the 4th, 8th, and 10th + * positions respectively." (positions = vertical columns, L→R, T→B) + * + * We don't trust our model for exactly which BYTE is column 4/8/10, so we + * just test bit-5 (0x20) at a handful of candidate bytes on a clear display + * (a few "8" digits for context) and the human reports which shows a decimal + * point or %. + * + * 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 + +/* Digit "1" = minimal glyph (0x1f sets bits 0-4 only; bit 5 stays DARK so + * an added bit-5 decimal point is clearly visible). */ +static const uint8_t DIG1[2] = {0x1f, 0x00}; + +/* The 6 digit positions (byte pair). */ +static const uint8_t POS[6][2] = { + {1, 2}, {3, 4}, {5, 6}, {7, 8}, {9, 10}, {11, 12}, +}; + +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 a readout of "1"s (one in each position) + one extra bit. + * Base "1" leaves bit5 dark, so a decimal/% bit at bit5 is clearly seen. */ +static void show_1s_plus(uint8_t frame_byte, uint8_t phys_bit, int tag) +{ + uint8_t f[FRAME_LEN]; + memset(f, 0, FRAME_LEN); + for (int p = 0; p < 6; p++) { + f[POS[p][0]] = DIG1[0]; + f[POS[p][1]] = DIG1[1]; + } + f[frame_byte] |= (uint8_t)(1u << phys_bit); + ESP_LOGI("dpfinder", "TEST %d: byte %d bit %d", tag, frame_byte, phys_bit); + + 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("dpfinder", "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("dpfinder", "no BOOT press in %d ms, auto-advancing", 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("dpfinder", "== Test decimal/% at bit5 (0x20) of candidate bytes =="); + + /* Phase 0: plain all 1s, no extra bit (reference). */ + { + uint8_t f[FRAME_LEN]; + memset(f, 0, FRAME_LEN); + for (int p = 0; p < 6; p++) { + f[POS[p][0]] = DIG1[0]; + f[POS[p][1]] = DIG1[1]; + } + 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); + ESP_LOGI("dpfinder", "REF: all 1s (no extras)"); + wait_for_advance(); + } + + /* Bit 5 (=0x20) at each candidate byte, 0-indexed: 3,4,7,8,9,10 */ + const int cand_byte[] = {3, 4, 7, 8, 9, 10}; + for (int i = 0; i < (int)(sizeof(cand_byte)/sizeof(cand_byte[0])); i++) { + show_1s_plus(cand_byte[i], 5, i + 1); + wait_for_advance(); + } + + ESP_LOGI("dpfinder", "== 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 7b66f33..a52362b 100644 --- a/firmware/main/temphummon.c +++ b/firmware/main/temphummon.c @@ -1,6 +1,192 @@ -#include +/** + * 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" -void app_main(void) -{ +#define PIN_SDA GPIO_NUM_4 +#define PIN_SCL GPIO_NUM_5 +#define PIN_RST GPIO_NUM_10 +#define PIN_BUSY GPIO_NUM_11 +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] */ + +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] = 0x05; +} + +/* 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<