Compare commits
7 Commits
bf91dc1af6
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b34754c77 | |||
| 58948bdfb6 | |||
| 9269e3b873 | |||
| 74b3e01556 | |||
| 7e21bde538 | |||
| 2ffd258f2f | |||
| f139ee8a00 |
+1
-1
@@ -92,7 +92,7 @@ external/*
|
||||
**/frontend/dist/
|
||||
|
||||
# Agent Tasks
|
||||
Provider/AgentTasks/
|
||||
# Provider/AgentTasks/
|
||||
|
||||
# OTA files
|
||||
*.bundle
|
||||
|
||||
Submodule Client/.tmp_seeed_gfx deleted from eefdf5f3fb
@@ -1,62 +1,71 @@
|
||||
# EPD Reference Driver — GDEY075T7 (UC8179)
|
||||
# EPD Implementation Reference — GDEY075T7 (UC8179)
|
||||
|
||||
## Source
|
||||
|
||||
**Repository:** [ekosboard/firmware](https://github.com/ekosboard/firmware)
|
||||
**File:** [`src/components/EPD/driver/epd_GDEY075T7.c`](https://github.com/ekosboard/firmware/blob/main/src/components/EPD/driver/epd_GDEY075T7.c)
|
||||
This document describes the current working state of the UC8179 E-Paper driver as implemented in `components/EPD/epd.cpp`.
|
||||
|
||||
## Panel Info
|
||||
|
||||
- **Controller:** UC8179
|
||||
- **Panel:** GDEY075T7 (Good Display 7.5" B/W, 800×480)
|
||||
- **Platform:** ESP-IDF (native SPI)
|
||||
- **Grayscale:** 4-level support present but marked as "NOT TESTED"
|
||||
- **Controller:** UC8179 (Good Display)
|
||||
- **Panel:** GDEY075T7 (7.5" B/W, 800×480)
|
||||
- **Platform:** ESP-IDF (Native SPI2 Host)
|
||||
- **Status:** **Fully Operational** (B/W and 4-Level Grayscale)
|
||||
|
||||
## Hardware Interface
|
||||
|
||||
### BUSY Pin Polarity
|
||||
- **Logic:** LOW = Busy, HIGH = Idle
|
||||
- **Wait loop:** `while (gpio_get_level(BUSY) == 0) { vTaskDelay(5); }`
|
||||
|
||||
### SPI Configuration
|
||||
- **Host:** `SPI2_HOST`
|
||||
- **Clock:** `SPI_FREQUENCY` (2MHz typically for EPD)
|
||||
- **Mode:** SPI Mode 0
|
||||
- **Transfer Size:** Chunked into 4096-byte buffers to avoid DMA limits and task watchdogs.
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
### BUSY Pin Polarity
|
||||
- **Reference driver:** HIGH = Busy, LOW = Idle
|
||||
- **⚠️ Our board is INVERTED:** LOW = Busy, HIGH = Idle
|
||||
- Wait loop polls `gpio_get_level(BUSY) == 0` to detect idle (reference)
|
||||
- Our code uses `== 0` to wait while busy (opposite meaning, same code pattern)
|
||||
### Data Polarity & VCOM (0x50)
|
||||
We use `0x29` for VCOM and Data Interval:
|
||||
- **VBD:** `00` (Floating/Black border?)
|
||||
- **N2OCP:** `1` (Auto-copy NEW data to OLD data after refresh)
|
||||
- **DDX:** `01` (Data Polarity: 0=Black, 1=White)
|
||||
- **Note:** In our 4-gray unpacking, we send `~output_byte`, effectively inverting the logic to match the panel's expectation for the LUTs used by the UC8179.
|
||||
|
||||
### Clear Screen (White)
|
||||
- Writes `0xFF` to **both** old (0x10) and new (0x13) data layers
|
||||
- `0xFF` = White, `0x00` = Black
|
||||
### Refresh Management
|
||||
The driver tracks refresh history to prevent ghosting or panel damage:
|
||||
- **Full Refresh:** Required every 5 fast refreshes or after 24 hours.
|
||||
- **Fast Refresh (B/W):** Optimized waveforms via `0xE5` -> `0x5A`.
|
||||
- **4-Gray Refresh:** Optimized waveforms via `0xE5` -> `0x5F`.
|
||||
|
||||
### Display Image
|
||||
- Writes `0x00` to old data layer (0x10) — assumes previous state was black
|
||||
- Writes actual image data to new data layer (0x13)
|
||||
## Grayscale Mapping (4-Level)
|
||||
|
||||
The input is a packed 2bpp bitmap (4 pixels per byte). We unpack this into two sequential data layers: **Old (0x10)** and **New (0x13)**.
|
||||
|
||||
| Gray Level | Input Bits (2bpp) | Old Layer (0x10) | New Layer (0x13) | Final Value (Inverted) |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **White** | `11` | 1 | 1 | `00` (Low) |
|
||||
| **Gray 1** | `10` | 0 | 1 | `10` |
|
||||
| **Gray 2** | `01` | 1 | 0 | `01` |
|
||||
| **Black** | `00` | 0 | 0 | `11` (High) |
|
||||
|
||||
*Note: The hardware logic for 4-gray on UC8179 drives the pixels based on the difference between the Old and New layers over multiple sub-frames.*
|
||||
|
||||
## Driver State Machine
|
||||
|
||||
### Initialization (Full)
|
||||
1. **Hardware Reset:** RST Low (10ms), RST High (10ms).
|
||||
2. **Power Setting (0x01):** `0x07, 0x07, 0x3f, 0x3f`.
|
||||
3. **Booster Soft Start (0x06):** `0x17, 0x17, 0x28, 0x17`.
|
||||
4. **Power On (0x04):** Wait 100ms + Busy Idle.
|
||||
5. **Panel Setting (0x00):** `0x1F` (800x480, KW Mode).
|
||||
6. **Resolution (0x61):** `800×480`.
|
||||
7. **VCOM (0x50):** `0x29, 0x07`.
|
||||
|
||||
### Fast Mode Adjustments
|
||||
- **Booster (0x06):** `0x27, 0x27, 0x18, 0x17` (Stronger drive for fast transitions).
|
||||
- **Fast Mode Enable (0xE0):** `0x02`.
|
||||
- **Timing (0xE5):** `0x5A` (B/W) or `0x5F` (4-Gray).
|
||||
|
||||
### Sleep Sequence
|
||||
1. `0x50` with `0xF7` — VCOM and data interval setting before sleep
|
||||
2. `0x02` — Power Off
|
||||
3. Wait for BUSY idle
|
||||
4. `0x07` + `0xA5` — Deep Sleep
|
||||
|
||||
### Init Sequence (Full Refresh)
|
||||
1. Hardware reset (RST low 10ms, high 10ms)
|
||||
2. `0x01` — Power Setting: VGH=20V, VGL=-20V, VDH=15V, VDL=-15V, VDHR=4.2V
|
||||
3. `0x06` — Booster Soft Start: 0x17, 0x17, 0x28, 0x17
|
||||
4. `0x04` — Power On + 100ms delay + wait busy
|
||||
5. `0x00` — Panel Setting: `0x1F` (KW B/W mode)
|
||||
6. `0x61` — Resolution: 800×480
|
||||
7. `0x15` — DUSPI disabled
|
||||
8. `0x50` — VCOM: 0x10, 0x07
|
||||
9. `0x60` — TCON: 0x22
|
||||
10. `0xE3` — PWS: 0x22
|
||||
|
||||
### Fast Refresh
|
||||
- `0xE0` → `0x02` (enable fast mode)
|
||||
- `0xE5` → `0x5A` (fast refresh timing)
|
||||
|
||||
### Partial Update
|
||||
- Uses `0x91` (partial in) / `0x92` (partial out) + `0x90` (resolution setting)
|
||||
- Writes `~data` (inverted) to old layer to force transitions
|
||||
- Sets `0x50` → `0x21` (N2OCP disabled) for partial mode
|
||||
|
||||
### 4-Level Grayscale
|
||||
- Requires different booster settings: `0x27, 0x27, 0x18, 0x17`
|
||||
- `0xE5` → `0x5F` for grayscale timing
|
||||
- Data encoding: 2 bits per pixel (0xC0=white, 0x00=black, 0x80=gray1, 0x40=gray2)
|
||||
- Old layer and new layer encode different bit planes, both inverted (`~temp3`)
|
||||
1. **VCOM pre-sleep (0x50):** `0xF7`.
|
||||
2. **Power Off (0x02):** Wait for Busy Idle.
|
||||
3. **Deep Sleep (0x07):** `0xA5` (Check code).
|
||||
|
||||
+55
-35
@@ -1,6 +1,8 @@
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "esp_event.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_system.h"
|
||||
@@ -8,13 +10,14 @@
|
||||
#include "freertos/task.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#include "esp_sleep.h"
|
||||
#include "epd.hpp"
|
||||
#include "led.hpp"
|
||||
#include "network.hpp"
|
||||
#include "provider.hpp"
|
||||
|
||||
#include "test_image.h"
|
||||
|
||||
static const char *TAG = "ClientMain";
|
||||
|
||||
extern "C" void app_main()
|
||||
@@ -34,55 +37,72 @@ extern "C" void app_main()
|
||||
|
||||
setup_led();
|
||||
|
||||
static uint8 display_buffer[96000];
|
||||
bool received_from_provider = false;
|
||||
|
||||
// Connect to WiFi
|
||||
if (false)
|
||||
ESP_LOGI(TAG, "Initializing WiFi connection");
|
||||
initialize_network();
|
||||
|
||||
esp_err_t err = connect_wifi(CONFIG_CALENDINK_WIFI_SSID,
|
||||
CONFIG_CALENDINK_WIFI_PASSWORD, false);
|
||||
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
ESP_LOGI(TAG, "Initializing WiFi connection");
|
||||
initialize_network();
|
||||
|
||||
esp_err_t err = connect_wifi(CONFIG_CALENDINK_WIFI_SSID,
|
||||
CONFIG_CALENDINK_WIFI_PASSWORD, false);
|
||||
|
||||
if (err == ESP_OK)
|
||||
uint8_t retries = 1;
|
||||
do
|
||||
{
|
||||
uint8_t retries = 1;
|
||||
do
|
||||
err = check_wifi_connection(retries);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
err = check_wifi_connection(retries);
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGW(TAG, "WiFi connection check timeout, retrying... (%d)",
|
||||
retries);
|
||||
led_blink_number(3, 255, 0, 0);
|
||||
}
|
||||
retries++;
|
||||
} while (err == ESP_ERR_TIMEOUT &&
|
||||
retries <= CONFIG_CALENDINK_WIFI_RETRIES);
|
||||
}
|
||||
ESP_LOGW(TAG, "WiFi connection check timeout, retrying... (%d)",
|
||||
retries);
|
||||
led_blink_number(3, 255, 0, 0);
|
||||
}
|
||||
retries++;
|
||||
} while (err == ESP_ERR_TIMEOUT &&
|
||||
retries <= CONFIG_CALENDINK_WIFI_RETRIES);
|
||||
}
|
||||
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
ESP_LOGI(TAG, "Successfully connected to WiFi!");
|
||||
test_provider_communication();
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to connect to WiFi.");
|
||||
}
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
ESP_LOGI(TAG, "Successfully connected to WiFi!");
|
||||
ESP_LOGI(TAG, "Fetching screen from Provider...");
|
||||
received_from_provider = test_provider_communication(display_buffer, sizeof(display_buffer));
|
||||
ESP_LOGI(TAG, "Provider result: %s", received_from_provider ? "SUCCESS" : "FAILED");
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to connect to WiFi.");
|
||||
}
|
||||
|
||||
turn_off_led();
|
||||
|
||||
ESP_LOGI(TAG, "Initializing EPD");
|
||||
epd_init();
|
||||
epd_init_display();
|
||||
epd_clear(epd_color_t::WHITE);
|
||||
epd_init_display(true);
|
||||
|
||||
if (received_from_provider) {
|
||||
ESP_LOGI(TAG, "Drawing image from Provider");
|
||||
epd_draw_bitmap_grayscale(epd_color::WHITE, display_buffer);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Drawing fallback test image");
|
||||
epd_draw_bitmap_grayscale(epd_color::WHITE, gImage_4G1);
|
||||
}
|
||||
|
||||
epd_refresh();
|
||||
epd_shutdown_display();
|
||||
|
||||
if (false)
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
disconnect_wifi();
|
||||
shutdown_network();
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Waiting 5 seconds before deep sleep...");
|
||||
vTaskDelay(pdMS_TO_TICKS(15000));
|
||||
|
||||
ESP_LOGI(TAG, "Entering Deep Sleep for 60 seconds...");
|
||||
esp_sleep_enable_timer_wakeup(30ULL * 1000000ULL);
|
||||
esp_deep_sleep_start();
|
||||
}
|
||||
|
||||
+58
-38
@@ -32,15 +32,19 @@ static bool resolve_provider_ip(char *out_ip, size_t out_ip_len)
|
||||
|
||||
{
|
||||
esp_ip4_addr_t addr = {};
|
||||
err = mdns_query_a(CONFIG_CALENDINK_PROVIDER_MDNS_HOSTNAME, 5000, &addr);
|
||||
if (err == ESP_OK)
|
||||
constexpr int kMaxRetries = 3;
|
||||
for (int attempt = 1; attempt <= kMaxRetries; attempt++)
|
||||
{
|
||||
snprintf(out_ip, out_ip_len, IPSTR, IP2STR(&addr));
|
||||
ESP_LOGI(TAG, "Provider resolved: %s", out_ip);
|
||||
return true;
|
||||
err = mdns_query_a(CONFIG_CALENDINK_PROVIDER_MDNS_HOSTNAME, 5000, &addr);
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
snprintf(out_ip, out_ip_len, IPSTR, IP2STR(&addr));
|
||||
ESP_LOGI(TAG, "Provider resolved: %s (attempt %d)", out_ip, attempt);
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "mDNS attempt %d/%d failed: %s", attempt, kMaxRetries,
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
|
||||
ESP_LOGW(TAG, "mDNS resolution failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
fallback:
|
||||
@@ -57,13 +61,15 @@ fallback:
|
||||
|
||||
// ── Provider Communication Test ─────────────────────────────────────────────
|
||||
|
||||
void test_provider_communication(void)
|
||||
bool test_provider_communication(uint8 *out_buffer, size_t buffer_size)
|
||||
{
|
||||
bool success = false;
|
||||
|
||||
// 1. Resolve Provider IP
|
||||
char provider_ip[16] = {};
|
||||
if (!resolve_provider_ip(provider_ip, sizeof(provider_ip)))
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t provider_port = CONFIG_CALENDINK_PROVIDER_PORT;
|
||||
@@ -74,7 +80,7 @@ void test_provider_communication(void)
|
||||
if (err != ESP_OK)
|
||||
{
|
||||
ESP_LOGE(TAG, "Failed to get WiFi MAC: %s", esp_err_to_name(err));
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
char mac_str[18] = {};
|
||||
@@ -84,59 +90,73 @@ void test_provider_communication(void)
|
||||
ESP_LOGI(TAG, "Client MAC: %s", mac_str);
|
||||
|
||||
// 3. Register with Provider: POST /api/devices/register
|
||||
// This may return "already_registered" — that's fine, we continue regardless.
|
||||
{
|
||||
char *url =
|
||||
http_build_url(provider_ip, provider_port, "/api/devices/register");
|
||||
if (url == nullptr)
|
||||
if (url != nullptr)
|
||||
{
|
||||
return;
|
||||
char json_body[64] = {};
|
||||
snprintf(json_body, sizeof(json_body), "{\"mac\":\"%s\"}", mac_str);
|
||||
|
||||
http_text_response_t resp = {};
|
||||
err = http_post_json(url, json_body, &resp);
|
||||
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
ESP_LOGI(TAG, "Register response (%d): %s", resp.status_code,
|
||||
resp.body ? resp.body : "(empty)");
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGW(TAG, "Register request failed: %s (continuing anyway)",
|
||||
esp_err_to_name(err));
|
||||
}
|
||||
|
||||
free(resp.body);
|
||||
free(url);
|
||||
}
|
||||
|
||||
char json_body[64] = {};
|
||||
snprintf(json_body, sizeof(json_body), "{\"mac\":\"%s\"}", mac_str);
|
||||
|
||||
http_text_response_t resp = {};
|
||||
err = http_post_json(url, json_body, &resp);
|
||||
|
||||
if (err == ESP_OK)
|
||||
{
|
||||
ESP_LOGI(TAG, "Register response (%d): %s", resp.status_code,
|
||||
resp.body ? resp.body : "(empty)");
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Register request failed: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
free(resp.body);
|
||||
free(url);
|
||||
}
|
||||
|
||||
// 4. Fetch screen image: GET /api/devices/screen.png?mac=XX:XX:XX:XX:XX:XX
|
||||
// 4. Fetch screen bitmap: GET /api/devices/screen.bin?mac=XX:XX:XX:XX:XX:XX
|
||||
{
|
||||
char path[80] = {};
|
||||
snprintf(path, sizeof(path), "/api/devices/screen.png?mac=%s", mac_str);
|
||||
snprintf(path, sizeof(path), "/api/devices/screen.bin?mac=%s", mac_str);
|
||||
|
||||
char *url = http_build_url(provider_ip, provider_port, path);
|
||||
if (url == nullptr)
|
||||
{
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
http_binary_response_t resp = {};
|
||||
err = http_get_binary(url, &resp);
|
||||
|
||||
if (err == ESP_OK)
|
||||
if (err == ESP_OK && resp.status_code == 200)
|
||||
{
|
||||
ESP_LOGI(TAG, "Screen image response (%d): %zu bytes", resp.status_code,
|
||||
resp.data_len);
|
||||
ESP_LOGI(TAG, "Screen bitmap response: %zu bytes", resp.data_len);
|
||||
if (resp.data != nullptr && resp.data_len > 0)
|
||||
{
|
||||
size_t copy_size = (resp.data_len < buffer_size) ? resp.data_len : buffer_size;
|
||||
memcpy(out_buffer, resp.data, copy_size);
|
||||
success = true;
|
||||
|
||||
// Debug: log first 10 bytes (should be 0xFF for white top-left pixels)
|
||||
ESP_LOGI(TAG, "First 10 bytes: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X",
|
||||
out_buffer[0], out_buffer[1], out_buffer[2], out_buffer[3],
|
||||
out_buffer[4], out_buffer[5], out_buffer[6], out_buffer[7],
|
||||
out_buffer[8], out_buffer[9]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(TAG, "Screen image request failed: %s", esp_err_to_name(err));
|
||||
ESP_LOGE(TAG, "Screen bitmap request failed: %s (status %d)",
|
||||
esp_err_to_name(err), resp.status_code);
|
||||
}
|
||||
|
||||
free(resp.data);
|
||||
free(url);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "types.hpp"
|
||||
#include <cstddef>
|
||||
|
||||
// Resolve the Provider's IP and run the device registration + screen fetch
|
||||
// test flow. Call once after WiFi is connected.
|
||||
void test_provider_communication(void);
|
||||
bool test_provider_communication(uint8 *out_buffer, size_t buffer_size);
|
||||
|
||||
+9014
-4001
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
# Epic 1: Client Power Strategy
|
||||
|
||||
## Goal
|
||||
Achieve a battery life measured in months for the ESP32-C6 Client.
|
||||
|
||||
## Context
|
||||
The ESP32-C6 serves as a "dumb" E-ink display client connected to the Calendink Provider. It needs to wake up, check for new layout data, download it, refresh the E-ink panel, and go back to sleep.
|
||||
The current standard method of deep sleep followed by full Wi-Fi re-association (DHCP negotiation) consumes ~100-300mA for multiple seconds, drastically limiting battery life for frequent updates.
|
||||
|
||||
## Scope & Technologies to Investigate
|
||||
1. **Wi-Fi 6 Target Wake Time (TWT):**
|
||||
- The ESP32-C6 supports 802.11ax TWT. TWT allows the device to negotiate specific wake-up schedules with the router, meaning the radio can sleep while the connection remains "active".
|
||||
- Packets sent to the device during sleep are buffered by the AP until the Target Wake Time.
|
||||
2. **ESP-PM (Power Management):**
|
||||
- Combine TWT with `esp-pm` dynamic frequency scaling and automatic Light Sleep.
|
||||
3. **Alternative - Hybrid ESP-NOW:**
|
||||
- If TWT requires unsupported features on the specific home router, evaluate a fallback where the Client sends a sub-millisecond ESP-NOW broadcast to ask "Is there an update?". Full Wi-Fi is only enabled if the Provider replies "Yes".
|
||||
|
||||
## Next Steps to Start
|
||||
1. Create a `tdd/client_power_strategy.md` in the Provider/Client workspace.
|
||||
2. Develop a minimal test firmware on the C6 to enable TWT via the `esp_wifi_twt_setup` API, monitoring power draw and wake times using a multimeter or profile.
|
||||
3. Document the final chosen power strategy pattern before modifying `main.cpp` or `epd.cpp`.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Epic 2: Provider Persistent Storage (SD Card)
|
||||
|
||||
## Goal
|
||||
Ensure Users, Tasks, and Settings survive device reboots on the ESP32-S3 Provider.
|
||||
|
||||
## Context
|
||||
Currently, the Provider's state (Todo tasks, Registered Devices, and User objects) resides in static BSS arrays like `g_Tasks[32]`. This means the state is lost on every reset.
|
||||
The ESP32-S3 board has an onboard 3GB SD Card reader. This Epic focuses on migrating the data layer to utilize this SD Card.
|
||||
|
||||
## Scope & Technologies to Investigate
|
||||
1. **Hardware Pinout:**
|
||||
- Determine the exact physical pins the SD Card reader is using on the specific ESP32-S3 board.
|
||||
- Investigate if it is wired for standard SPI (`sdspi`) or native SDMMC (1-bit or 4-bit mode).
|
||||
2. **ESP-IDF Storage Drivers:**
|
||||
- Mount a FATFS partition using the `esp_vfs_fat_sdmmc` / `esp_vfs_fat_sdspi` components.
|
||||
3. **Data Model:**
|
||||
- Decide between compiling SQLite for ESP-IDF (better querying, harder setup) or relying on flat `.json` files parsed via cJSON (easier setup, sufficient for MVP limits like 4 users and 32 tasks).
|
||||
|
||||
## Next Steps to Start
|
||||
1. Create a `tdd/sd_card_persistence.md`.
|
||||
2. Find the board schematic or test GPIO configurations to successfully mount the SD Card.
|
||||
3. Abstract the storage functionality into a generic `store.hpp/cpp` interface so the `manage.cpp` and API handlers don't need to be rewritten.
|
||||
4. Update `seed_users()` and `seed_tasks()` routines to populate initial `.json` files if the SD Card is empty.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Epic 3: Voice-to-Task AI (Gemini)
|
||||
|
||||
## Goal
|
||||
Allow users to dictate tasks naturally in French, directly creating JSON task structures on the Provider.
|
||||
|
||||
## Context
|
||||
Adding tasks manually using a keyboard via the web dashboard is friction. We want to utilize automated intelligence (Google Gemini API) to ingest natural language (specifically French) and figure out the JSON properties (task name, user, due date).
|
||||
Input can come from the Svelte frontend (browser recording) or an iOS Shortcut automation.
|
||||
|
||||
## Scope & Technologies to Investigate
|
||||
1. **Ingestion Endpoint:**
|
||||
- Create a `POST /api/tasks/audio` endpoint on the ESP32-S3 Provider.
|
||||
- Decide exactly what format this accepts. Is it raw audio binaries like `.wav` or `.m4a`? Or is it transcribed text sent from the device's native speech-to-text? (Note during implementation if ESP32 memory constraints force pre-transcription on iOS before sending).
|
||||
2. **Gemini API Integration:**
|
||||
- Implement an HTTP/HTTPS client on the ESP32-S3 to call Gemini API endpoints.
|
||||
- Construct a prompt template: *"You are an assistant. Extract the task properties from the following French audio/text and output strict JSON matching our schema: { "title": "...", "due_date": "...", "user_name": "..." }"*
|
||||
3. **API Key Management:**
|
||||
- Add a Svelte dashboard configuration page to allow the user to securely save their Gemini API key to the SD Card.
|
||||
|
||||
## Next Steps to Start
|
||||
1. Create a `tdd/voice_ai_integration.md`.
|
||||
2. Secure an HTTPS connection from ESP-IDF to the Google Gemini API (managing certs/mbedtls).
|
||||
3. Test a hardcoded prompt execution before wiring up the web UI.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Epic 4: Modular Grid Layout & Provider Integration
|
||||
|
||||
## Goal
|
||||
Replace the single full-screen XML structure with a flexible 6-pane grid that dynamically wraps external data.
|
||||
|
||||
## Context
|
||||
Currently, the Provider sends an entire E-ink screen generated from a single massive LVGL XML string. For the MVP, we want a flexible grid: 2 large top canvases (Main Task, Weather) and 4 small bottom canvases (One per Family Member).
|
||||
The user can assign distinct XML templates to any of these 6 panes.
|
||||
|
||||
## Scope & Technologies to Investigate
|
||||
1. **Widget Architecture (Grid):**
|
||||
- Rework the LVGL rendering logic in the Provider. It should boot an LVGL display, split it into 6 designated Canvas areas, and parse smaller, independent user XML strings into each respective area using `LV_USE_XML`.
|
||||
2. **Data Binding:**
|
||||
- Investigate how live data gets into the XML before parsing. E.g., if Canvas 2 is 'Weather', how does `{{TEMP}}` inside the user's XML become `22°C`? String replacement via standard C functions before passing to LVGL.
|
||||
3. **OpenWeatherMap Integration:**
|
||||
- Create a backend polling task or direct fetch mechanism on the Provider to query the OpenWeatherMap API.
|
||||
- Expose the OpenWeatherMap API Key configuration in the Svelte dashboard.
|
||||
4. **4-User Constraint:**
|
||||
- Enforce that the system tracks exactly 4 active users to match the 4 bottom E-ink panes.
|
||||
|
||||
## Next Steps to Start
|
||||
1. Create a `tdd/modular_grid_layout.md`.
|
||||
2. Prototype a basic HTTP GET to OpenWeatherMap using the `http_client` component and parse the resulting JSON.
|
||||
3. Rework the Svelte `DeviceManager.svelte` to show 6 individual XML editors per Device instead of just 1.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Calendink MVP Plan - Executive Summary
|
||||
|
||||
This document defines the macroscopic project scope for the Calendink Minimum Viable Product (MVP).
|
||||
|
||||
We will structure the development into **4 Major Epics**. Because features like Power Management and SD Card Integration require deep technical investigation, we will not prematurely guess the solutions here. Instead, each Epic will begin with a dedicated **Technical Design Document (TDD)** to map out the exact implementation before coding.
|
||||
|
||||
---
|
||||
|
||||
## The 4 Development Epics
|
||||
|
||||
### Epic 1: Client Power Strategy
|
||||
**Goal:** Achieve a battery life measured in months for the ESP32-C6 Client.
|
||||
**Scope:**
|
||||
- Research and design a formal power strategy.
|
||||
- Evaluate Target Wake Time (TWT), `esp-pm`, Light Sleep, Deep Sleep, and hybrid ESP-NOW routing.
|
||||
- The outcome will be a dedicated TDD followed by the firmware implementation.
|
||||
|
||||
### Epic 2: Provider Persistent Storage (SD Card)
|
||||
**Goal:** Ensure Users, Tasks, and Settings survive device reboots.
|
||||
**Scope:**
|
||||
- Investigate the physical SD Card pinout on the ESP32-S3.
|
||||
- Decide between SQLite or flat JSON files.
|
||||
- Implement the ESP-IDF SDMMC/SDSPI driver.
|
||||
- Migrate the current in-RAM `g_Tasks` and `g_Users` arrays to the new persistent backend.
|
||||
|
||||
### Epic 3: Voice-to-Task AI (Gemini)
|
||||
**Goal:** Allow users to dictate tasks naturally in French.
|
||||
**Scope:**
|
||||
- Implement an API endpoint on the Provider to accept raw audio/text.
|
||||
- Create an internal HTTPS client on the S3 to proxy the data to the Google Gemini API.
|
||||
- Parse the structured JSON response from Gemini to automatically save the new task.
|
||||
|
||||
### Epic 4: Modular Grid Layout & Provider Integration
|
||||
**Goal:** Replace the single-XML screen structure with a flexible 6-pane grid.
|
||||
**Scope:**
|
||||
- Define a fixed layout layout on the E-ink display: 2 large top canvases (Main Task, Weather) and 4 small bottom canvases (One per family member).
|
||||
- Limit the user system to exactly 4 Active Users.
|
||||
- Allow the dashboard to assign distinct XML templates to any of the 6 canvases, making it adaptable to future widgets.
|
||||
- Integrate an OpenWeatherMap API wrapper on the Provider.
|
||||
@@ -0,0 +1,235 @@
|
||||
// GET /api/devices/screen.bin?mac=XX — Render and return a raw 2bpp grayscale
|
||||
// bitmap for the device's current screen.
|
||||
// Uses LVGL to render the device's XML layout, quantizes to 4 grayscale levels,
|
||||
// and packs into 2 bits per pixel (96,000 bytes for 800×480).
|
||||
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_log.h"
|
||||
#include "lv_setup.hpp"
|
||||
#include "lvgl.h"
|
||||
#include <string.h>
|
||||
|
||||
#include "device.hpp"
|
||||
#include "types.hpp"
|
||||
|
||||
internal const char *kTagDeviceScreenBitmap = "API_DEV_SCREEN_BMP";
|
||||
|
||||
internal esp_err_t api_devices_screen_bitmap_handler(httpd_req_t *req)
|
||||
{
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
httpd_resp_set_hdr(req, "Cache-Control",
|
||||
"no-cache, no-store, must-revalidate");
|
||||
httpd_resp_set_type(req, "application/octet-stream");
|
||||
|
||||
// Extract mac query parameter
|
||||
char mac[18] = {};
|
||||
size_t buf_len = httpd_req_get_url_query_len(req) + 1;
|
||||
if (buf_len > 1)
|
||||
{
|
||||
char query[64] = {};
|
||||
if (httpd_req_get_url_query_str(req, query, sizeof(query)) == ESP_OK)
|
||||
{
|
||||
httpd_query_key_value(query, "mac", mac, sizeof(mac));
|
||||
}
|
||||
}
|
||||
|
||||
if (mac[0] == '\0')
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST,
|
||||
"Missing 'mac' query param");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
device_t *dev = find_device(mac);
|
||||
if (!dev)
|
||||
{
|
||||
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Device not registered");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// --- LVGL rendering (mutex-protected) ---
|
||||
if (xSemaphoreTake(g_LvglMutex, pdMS_TO_TICKS(5000)) != pdTRUE)
|
||||
{
|
||||
ESP_LOGE(kTagDeviceScreenBitmap, "Failed to get LVGL mutex");
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "LVGL busy");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
lv_obj_t *scr = lv_screen_active();
|
||||
|
||||
// Clear all children from the active screen
|
||||
lv_obj_clean(scr);
|
||||
|
||||
// White background for grayscale
|
||||
lv_obj_set_style_bg_color(scr, lv_color_white(), LV_PART_MAIN);
|
||||
|
||||
// Setup the MAC address subject so the XML can bind to it
|
||||
static lv_subject_t mac_subject;
|
||||
static char mac_buf[18];
|
||||
static char mac_prev_buf[18];
|
||||
|
||||
strncpy(mac_buf, mac, sizeof(mac_buf));
|
||||
strncpy(mac_prev_buf, mac, sizeof(mac_prev_buf));
|
||||
|
||||
lv_subject_init_string(&mac_subject, mac_buf, mac_prev_buf, sizeof(mac_buf),
|
||||
mac);
|
||||
|
||||
// Register the subject in the global XML scope under the name "device_mac"
|
||||
lv_xml_component_scope_t *global_scope =
|
||||
lv_xml_component_get_scope("globals");
|
||||
if (global_scope)
|
||||
{
|
||||
lv_xml_register_subject(global_scope, "device_mac", &mac_subject);
|
||||
ESP_LOGI(kTagDeviceScreenBitmap,
|
||||
"Registered subject 'device_mac' with value: %s", mac);
|
||||
}
|
||||
|
||||
bool render_success = false;
|
||||
|
||||
// 1. Prepare the XML payload
|
||||
const char *xml_to_register = NULL;
|
||||
|
||||
if (dev->xml_layout[0] == '\0')
|
||||
{
|
||||
ESP_LOGI(kTagDeviceScreenBitmap, "Device %s has no layout xml.", mac);
|
||||
xSemaphoreGive(g_LvglMutex);
|
||||
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "No layout configured");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (strstr(dev->xml_layout, "<screen") != NULL)
|
||||
{
|
||||
xml_to_register = dev->xml_layout;
|
||||
ESP_LOGI(kTagDeviceScreenBitmap,
|
||||
"XML already contains <screen>, passing directly to parser.");
|
||||
}
|
||||
|
||||
// 2. Register the XML payload as a component
|
||||
lv_result_t res =
|
||||
lv_xml_register_component_from_data("current_device", xml_to_register);
|
||||
|
||||
if (res == LV_RESULT_OK)
|
||||
{
|
||||
ESP_LOGI(kTagDeviceScreenBitmap,
|
||||
"Successfully registered XML for device %s", mac);
|
||||
|
||||
lv_obj_t *new_scr = lv_xml_create_screen("current_device");
|
||||
|
||||
if (new_scr)
|
||||
{
|
||||
lv_screen_load(new_scr);
|
||||
scr = new_scr;
|
||||
render_success = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(kTagDeviceScreenBitmap,
|
||||
"lv_xml_create_screen failed for device %s", mac);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ESP_LOGE(kTagDeviceScreenBitmap,
|
||||
"lv_xml_register_component_from_data failed for device %s", mac);
|
||||
}
|
||||
|
||||
// 3. Fallback if LVGL XML parsing or creation failed
|
||||
if (!render_success)
|
||||
{
|
||||
ESP_LOGW(kTagDeviceScreenBitmap,
|
||||
"XML render failed, falling back to raw text layout");
|
||||
lv_obj_t *label = lv_label_create(scr);
|
||||
lv_label_set_text(label, "XML Parsing Error\nSee serial log");
|
||||
lv_obj_set_style_text_color(label, lv_color_black(), LV_PART_MAIN);
|
||||
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
|
||||
}
|
||||
|
||||
// Force LVGL to fully render the screen
|
||||
lv_refr_now(g_LvglDisplay);
|
||||
|
||||
lv_draw_buf_t *draw_buf = lv_display_get_buf_active(g_LvglDisplay);
|
||||
if (!draw_buf)
|
||||
{
|
||||
xSemaphoreGive(g_LvglMutex);
|
||||
ESP_LOGE(kTagDeviceScreenBitmap, "No active draw buffer");
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Display uninitialized");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
uint32_t width = CONFIG_CALENDINK_DISPLAY_WIDTH;
|
||||
uint32_t height = CONFIG_CALENDINK_DISPLAY_HEIGHT;
|
||||
|
||||
// Output: 2 bits per pixel, 4 pixels per byte → width*height/4 bytes
|
||||
constexpr uint32_t kBitmapSize = 96000; // 800 * 480 / 4
|
||||
uint8_t *bitmap = (uint8_t *)heap_caps_malloc(kBitmapSize, MALLOC_CAP_SPIRAM);
|
||||
if (!bitmap)
|
||||
{
|
||||
bitmap = (uint8_t *)malloc(kBitmapSize);
|
||||
if (!bitmap)
|
||||
{
|
||||
xSemaphoreGive(g_LvglMutex);
|
||||
ESP_LOGE(kTagDeviceScreenBitmap, "Failed to allocate bitmap buffer");
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR,
|
||||
"Out of memory");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
// LVGL renders into RGB565 (2 bytes per pixel).
|
||||
// Quantize to 4 grayscale levels and pack 4 pixels per byte (2bpp).
|
||||
// Pixel encoding (MSB first):
|
||||
// 0b00 = BLACK (lum 0)
|
||||
// 0b01 = DARK_GRAY (lum 85)
|
||||
// 0b10 = LIGHT_GRAY (lum 170)
|
||||
// 0b11 = WHITE (lum 255)
|
||||
uint32_t bitmap_idx = 0;
|
||||
for (uint32_t y = 0; y < height; ++y)
|
||||
{
|
||||
const uint16_t *src_row =
|
||||
(const uint16_t *)((const uint8_t *)draw_buf->data +
|
||||
(y * draw_buf->header.stride));
|
||||
|
||||
for (uint32_t x = 0; x < width; x += 4)
|
||||
{
|
||||
uint8_t packed = 0;
|
||||
for (int p = 0; p < 4; ++p)
|
||||
{
|
||||
uint16_t c = src_row[x + p];
|
||||
// Expand 5/6/5 components
|
||||
uint8_t r_5 = (c >> 11) & 0x1F;
|
||||
uint8_t g_6 = (c >> 5) & 0x3F;
|
||||
uint8_t b_5 = c & 0x1F;
|
||||
|
||||
// Unpack to 8-bit
|
||||
uint8_t r = (r_5 << 3) | (r_5 >> 2);
|
||||
uint8_t g = (g_6 << 2) | (g_6 >> 4);
|
||||
uint8_t b = (b_5 << 3) | (b_5 >> 2);
|
||||
|
||||
// Luminance → 2-bit quantized level (0..3)
|
||||
uint8_t lum = (r * 77 + g * 150 + b * 29) >> 8;
|
||||
uint8_t level = lum >> 6; // 0,1,2,3
|
||||
|
||||
packed |= (level << (6 - p * 2));
|
||||
}
|
||||
bitmap[bitmap_idx++] = packed;
|
||||
}
|
||||
}
|
||||
|
||||
xSemaphoreGive(g_LvglMutex);
|
||||
|
||||
ESP_LOGI(kTagDeviceScreenBitmap, "Bitmap ready: %lu bytes. Sending...",
|
||||
(unsigned long)bitmap_idx);
|
||||
esp_err_t sendRes = httpd_resp_send(req, (const char *)bitmap, bitmap_idx);
|
||||
|
||||
free(bitmap);
|
||||
|
||||
return sendRes;
|
||||
}
|
||||
|
||||
internal const httpd_uri_t api_devices_screen_bitmap_uri = {
|
||||
.uri = "/api/devices/screen.bin",
|
||||
.method = HTTP_GET,
|
||||
.handler = api_devices_screen_bitmap_handler,
|
||||
.user_ctx = NULL};
|
||||
@@ -6,4 +6,5 @@
|
||||
#include "api/devices/layout.cpp"
|
||||
#include "api/devices/screen.cpp"
|
||||
#include "api/devices/screen_image.cpp"
|
||||
#include "api/devices/screen_bitmap.cpp"
|
||||
// clang-format on
|
||||
|
||||
@@ -308,6 +308,7 @@ internal httpd_handle_t start_webserver(void)
|
||||
httpd_register_uri_handler(server, &api_devices_layout_uri);
|
||||
httpd_register_uri_handler(server, &api_devices_screen_info_uri);
|
||||
httpd_register_uri_handler(server, &api_devices_screen_image_uri);
|
||||
httpd_register_uri_handler(server, &api_devices_screen_bitmap_uri);
|
||||
|
||||
// Populate dummy data for development (debug builds only)
|
||||
#ifndef NDEBUG
|
||||
|
||||
+260
-37
@@ -1,17 +1,29 @@
|
||||
#include "epd.hpp"
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "driver/spi_master.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <time.h>
|
||||
|
||||
#include "epd.hpp"
|
||||
|
||||
internal const char *kTagEPD = "EPD";
|
||||
|
||||
internal spi_device_handle_t g_spi_handle;
|
||||
internal bool g_is_asleep = true;
|
||||
internal constexpr size_t kTotal_bytes = (EPD_WIDTH * EPD_HEIGHT) / 8;
|
||||
internal uint8 g_scratch_buffer[4096];
|
||||
|
||||
internal struct epd_refresh_stats{
|
||||
time_t last_full_refresh_bw;
|
||||
int fast_count_bw;
|
||||
time_t last_full_refresh_4g;
|
||||
int fast_count_4g;
|
||||
} g_epd_stats = {0, 0, 0, 0};
|
||||
|
||||
internal void epd_spi_init(void)
|
||||
{
|
||||
@@ -78,6 +90,7 @@ internal void epd_write_buffer(const uint8 *data, size_t len)
|
||||
// Pull DC High for Data
|
||||
gpio_set_level((gpio_num_t)TFT_DC, GPIO_HIGH);
|
||||
|
||||
int chunk_count = 0;
|
||||
while (len > 0)
|
||||
{
|
||||
// max_transfer_sz is typically 4096 which is the limit for a single SPI
|
||||
@@ -90,6 +103,28 @@ internal void epd_write_buffer(const uint8 *data, size_t len)
|
||||
spi_device_transmit(g_spi_handle, &t);
|
||||
data += chunk;
|
||||
len -= chunk;
|
||||
|
||||
// Yield every 16 chunks (~64KB) to prevent task watchdog timeout
|
||||
if (++chunk_count % 16 == 0)
|
||||
{
|
||||
vTaskDelay(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void epd_fill_layer(uint8 cmd, uint8 color_byte)
|
||||
{
|
||||
epd_writecommand(cmd);
|
||||
|
||||
memset(g_scratch_buffer, color_byte, sizeof(g_scratch_buffer));
|
||||
|
||||
size_t remaining = kTotal_bytes;
|
||||
while (remaining > 0)
|
||||
{
|
||||
size_t to_write =
|
||||
(remaining > sizeof(g_scratch_buffer)) ? sizeof(g_scratch_buffer) : remaining;
|
||||
epd_write_buffer(g_scratch_buffer, to_write);
|
||||
remaining -= to_write;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,16 +156,9 @@ void epd_shutdown(void)
|
||||
spi_bus_free(SPI2_HOST);
|
||||
}
|
||||
|
||||
void epd_init_display()
|
||||
internal void epd_init_display_full(void)
|
||||
{
|
||||
g_is_asleep = false;
|
||||
|
||||
// Module reset
|
||||
gpio_set_level((gpio_num_t)TFT_RST, 0);
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
gpio_set_level((gpio_num_t)TFT_RST, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
|
||||
ESP_LOGI(kTagEPD, "Performing FULL initialization");
|
||||
epd_writecommand(0x01); // POWER SETTING
|
||||
epd_writedata(0x07);
|
||||
epd_writedata(0x07);
|
||||
@@ -160,13 +188,149 @@ void epd_init_display()
|
||||
epd_writedata(0x00);
|
||||
|
||||
epd_writecommand(0x50); // VCOM AND DATA INTERVAL SETTING
|
||||
epd_writedata(0x29); // BDV=10 (White Border), N2OCP=1 (Auto-copy NEW to OLD), DDX=01 (0=Black, 1=White)
|
||||
epd_writedata(0x29); // BDV=10 (White Border), N2OCP=1 (Auto-copy NEW to OLD),
|
||||
// DDX=01 (0=Black, 1=White)
|
||||
epd_writedata(0x07);
|
||||
|
||||
epd_writecommand(0x60); // TCON SETTING
|
||||
epd_writedata(0x22);
|
||||
}
|
||||
|
||||
internal void epd_init_display_fast(void)
|
||||
{
|
||||
ESP_LOGI(kTagEPD, "Performing FAST initialization");
|
||||
epd_writecommand(0X00); // PANEL SETTING
|
||||
epd_writedata(0x1F);
|
||||
|
||||
epd_writecommand(0x50); // VCOM AND DATA INTERVAL SETTING
|
||||
epd_writedata(0x29);
|
||||
epd_writedata(0x07);
|
||||
|
||||
epd_writecommand(0x04); // POWER ON
|
||||
vTaskDelay(pdMS_TO_TICKS(100));
|
||||
epd_wait_until_idle();
|
||||
|
||||
// Enhanced display drive (Booster values for fast mode)
|
||||
epd_writecommand(0x06); // Booster Soft Start
|
||||
epd_writedata(0x27);
|
||||
epd_writedata(0x27);
|
||||
epd_writedata(0x18);
|
||||
epd_writedata(0x17);
|
||||
|
||||
epd_writecommand(0xE0);
|
||||
epd_writedata(0x02);
|
||||
epd_writecommand(0xE5);
|
||||
epd_writedata(0x5A);
|
||||
}
|
||||
|
||||
internal void epd_init_display_4g_full(void)
|
||||
{
|
||||
ESP_LOGI(kTagEPD, "Performing FULL 4-GRAY initialization");
|
||||
epd_writecommand(0X00); // PANEL SETTING
|
||||
epd_writedata(0x1F);
|
||||
|
||||
epd_writecommand(0X50); // VCOM AND DATA INTERVAL SETTING
|
||||
epd_writedata(0x29);
|
||||
epd_writedata(0x07);
|
||||
|
||||
epd_writecommand(0x04); // POWER ON
|
||||
vTaskDelay(pdMS_TO_TICKS(100)); // Standard delay for full
|
||||
epd_wait_until_idle();
|
||||
|
||||
// Standard display drive (Full Booster values)
|
||||
epd_writecommand(0x06); // Booster Soft Start
|
||||
epd_writedata(0x17);
|
||||
epd_writedata(0x17);
|
||||
epd_writedata(0x28);
|
||||
epd_writedata(0x17);
|
||||
|
||||
epd_writecommand(0xE0);
|
||||
epd_writedata(0x02);
|
||||
epd_writecommand(0xE5);
|
||||
epd_writedata(0x5F); // 0x5F -- 4 Gray
|
||||
}
|
||||
|
||||
internal void epd_init_display_4g_fast(void)
|
||||
{
|
||||
ESP_LOGI(kTagEPD, "Performing FAST 4-GRAY initialization");
|
||||
epd_writecommand(0X00); // PANEL SETTING
|
||||
epd_writedata(0x1F);
|
||||
|
||||
epd_writecommand(0X50); // VCOM AND DATA INTERVAL SETTING
|
||||
epd_writedata(0x29);
|
||||
epd_writedata(0x07);
|
||||
|
||||
epd_writecommand(0x04); // POWER ON
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
epd_wait_until_idle();
|
||||
|
||||
// Enhanced display drive (Fast Booster values)
|
||||
epd_writecommand(0x06); // Booster Soft Start
|
||||
epd_writedata(0x27);
|
||||
epd_writedata(0x27);
|
||||
epd_writedata(0x18);
|
||||
epd_writedata(0x17);
|
||||
|
||||
epd_writecommand(0xE0);
|
||||
epd_writedata(0x02);
|
||||
epd_writecommand(0xE5);
|
||||
epd_writedata(0x5F); // 0x5F -- 4 Gray
|
||||
}
|
||||
|
||||
void epd_init_display(bool is_4gray)
|
||||
{
|
||||
g_is_asleep = false;
|
||||
|
||||
// Module reset
|
||||
gpio_set_level((gpio_num_t)TFT_RST, 0);
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
gpio_set_level((gpio_num_t)TFT_RST, 1);
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
|
||||
time_t now = time(NULL);
|
||||
|
||||
if (is_4gray)
|
||||
{
|
||||
double diff = difftime(now, g_epd_stats.last_full_refresh_4g);
|
||||
bool force_full = (g_epd_stats.last_full_refresh_4g == 0) || (diff > 86400.0) ||
|
||||
(g_epd_stats.fast_count_4g >= 5);
|
||||
|
||||
if (force_full)
|
||||
{
|
||||
epd_init_display_4g_full();
|
||||
g_epd_stats.last_full_refresh_4g = now;
|
||||
g_epd_stats.fast_count_4g = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
epd_init_display_4g_fast();
|
||||
g_epd_stats.fast_count_4g++;
|
||||
}
|
||||
ESP_LOGI(kTagEPD, "4G Refresh stats: (Fast count: %d/5, Age: %.1f hours)",
|
||||
g_epd_stats.fast_count_4g, diff / 3600.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
double diff = difftime(now, g_epd_stats.last_full_refresh_bw);
|
||||
bool force_full = (g_epd_stats.last_full_refresh_bw == 0) || (diff > 86400.0) ||
|
||||
(g_epd_stats.fast_count_bw >= 5);
|
||||
|
||||
if (force_full)
|
||||
{
|
||||
epd_init_display_full();
|
||||
g_epd_stats.last_full_refresh_bw = now;
|
||||
g_epd_stats.fast_count_bw = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
epd_init_display_fast();
|
||||
g_epd_stats.fast_count_bw++;
|
||||
}
|
||||
ESP_LOGI(kTagEPD, "BW Refresh stats: (Fast count: %d/5, Age: %.1f hours)",
|
||||
g_epd_stats.fast_count_bw, diff / 3600.0);
|
||||
}
|
||||
}
|
||||
|
||||
void epd_shutdown_display(void)
|
||||
{
|
||||
assert(!g_is_asleep);
|
||||
@@ -196,40 +360,99 @@ void epd_refresh(void)
|
||||
|
||||
bool epd_is_asleep(void) { return g_is_asleep; }
|
||||
|
||||
void epd_clear(epd_color_t level)
|
||||
void epd_clear(epd_color level)
|
||||
{
|
||||
assert(!g_is_asleep);
|
||||
|
||||
// Directly pass the color preference to hardware (hardware polarity is
|
||||
// configured in DDX)
|
||||
uint8 color_byte = static_cast<uint8>(level);
|
||||
ESP_LOGI(kTagEPD, "Clearing display (byte=0x%02X)", color_byte);
|
||||
|
||||
constexpr size_t total_bytes = (EPD_WIDTH * EPD_HEIGHT) / 8;
|
||||
epd_fill_layer(0x10,
|
||||
0xFF); // Old data layer (0xFF is mapped to White)
|
||||
epd_fill_layer(0x13, color_byte); // New data layer
|
||||
|
||||
auto write_layer = [&](uint8 cmd, uint8 fill_byte)
|
||||
ESP_LOGI(kTagEPD, "Data transmission complete (Refresh required)");
|
||||
}
|
||||
|
||||
void epd_draw_bitmap(epd_color clearColor, const uint8 *bitmap)
|
||||
{
|
||||
uint8 color_byte = static_cast<uint8>(clearColor);
|
||||
ESP_LOGI(kTagEPD, "Drawing bitmap (clearColor byte=0x%02X)", color_byte);
|
||||
|
||||
epd_fill_layer(0x10, color_byte); // Send clear color to "Old" layer
|
||||
|
||||
epd_writecommand(0x13); // "New" data layer
|
||||
epd_write_buffer(bitmap, kTotal_bytes);
|
||||
|
||||
ESP_LOGI(kTagEPD, "Data transmission complete (Refresh required)");
|
||||
}
|
||||
|
||||
void epd_draw_bitmap_grayscale(epd_color clearColor, const uint8 *bitmap)
|
||||
{
|
||||
ESP_LOGI(kTagEPD, "Drawing 4-GRAY bitmap (with unpacking)");
|
||||
|
||||
// THE LOGIC:
|
||||
// The input bitmap is 2-bits per pixel packed (4 pixels per byte).
|
||||
// Total pixels: 800 * 480 = 384,000.
|
||||
// Input size: 384,000 / 4 = 96,000 bytes.
|
||||
// Output: Two frames of 800 * 480 / 8 = 48,000 bytes each.
|
||||
|
||||
auto process_layer = [&](uint8 cmd, bool is_new_layer)
|
||||
{
|
||||
uint8 chunk[256];
|
||||
memset(chunk, fill_byte, sizeof(chunk));
|
||||
|
||||
epd_writecommand(cmd);
|
||||
size_t remaining = total_bytes;
|
||||
int chunk_count = 0;
|
||||
while (remaining > 0)
|
||||
size_t scratch_idx = 0;
|
||||
|
||||
// Process 48,000 output bytes (each covers 8 pixels)
|
||||
for (size_t i = 0; i < 48000; i++)
|
||||
{
|
||||
size_t to_write = (remaining > sizeof(chunk)) ? sizeof(chunk) : remaining;
|
||||
epd_write_buffer(chunk, to_write);
|
||||
remaining -= to_write;
|
||||
// Yield every 16 chunks (~4KB) to prevent task watchdog timeout
|
||||
if (++chunk_count % 16 == 0)
|
||||
vTaskDelay(1);
|
||||
uint8 output_byte = 0;
|
||||
// Each output byte comes from 2 input bytes (j=0, j=1)
|
||||
for (int j = 0; j < 2; j++)
|
||||
{
|
||||
uint8 input_byte = bitmap[i * 2 + j];
|
||||
// Extract 4 pixels from each input byte
|
||||
for (int k = 0; k < 4; k++)
|
||||
{
|
||||
uint8 pixel_bits = (input_byte >> (6 - k * 2)) & 0x03; // Correct bit order
|
||||
bool bit_val = false;
|
||||
|
||||
if (is_new_layer)
|
||||
{
|
||||
// NEW Layer (0x13) Mapping
|
||||
if (pixel_bits == 0x03) bit_val = true; // White (11)
|
||||
else if (pixel_bits == 0x00) bit_val = false; // Black (00)
|
||||
else if (pixel_bits == 0x02) bit_val = true; // Gray1 (10)
|
||||
else if (pixel_bits == 0x01) bit_val = false; // Gray2 (01)
|
||||
}
|
||||
else
|
||||
{
|
||||
// OLD Layer (0x10) Mapping
|
||||
if (pixel_bits == 0x03) bit_val = true; // White (11)
|
||||
else if (pixel_bits == 0x00) bit_val = false; // Black (00)
|
||||
else if (pixel_bits == 0x02) bit_val = false; // Gray1 (10)
|
||||
else if (pixel_bits == 0x01) bit_val = true; // Gray2 (01)
|
||||
}
|
||||
|
||||
if (bit_val) output_byte |= (1 << (7 - (j * 4 + k)));
|
||||
}
|
||||
}
|
||||
|
||||
g_scratch_buffer[scratch_idx++] = output_byte;
|
||||
|
||||
if (scratch_idx >= sizeof(g_scratch_buffer))
|
||||
{
|
||||
epd_write_buffer(g_scratch_buffer, scratch_idx);
|
||||
scratch_idx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (scratch_idx > 0)
|
||||
{
|
||||
epd_write_buffer(g_scratch_buffer, scratch_idx);
|
||||
}
|
||||
};
|
||||
|
||||
write_layer(
|
||||
0x10,
|
||||
0xFF); // Old data layer (0xFF is mapped to White under new polarity)
|
||||
write_layer(0x13, color_byte); // New data layer
|
||||
process_layer(0x10, false); // Old data
|
||||
process_layer(0x13, true); // New data
|
||||
|
||||
ESP_LOGI(kTagEPD, "Data transmission complete (Refresh required)");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,16 +19,20 @@
|
||||
#define EPD_WIDTH 800
|
||||
#define EPD_HEIGHT 480
|
||||
|
||||
enum class epd_color_t : uint8
|
||||
enum class epd_color : uint8
|
||||
{
|
||||
BLACK = 0x00,
|
||||
DARK_GRAY = 0x55,
|
||||
LIGHT_GRAY = 0xAA,
|
||||
WHITE = 0xFF
|
||||
};
|
||||
|
||||
void epd_init(void);
|
||||
void epd_shutdown(void);
|
||||
void epd_init_display();
|
||||
void epd_init_display(bool is_4gray);
|
||||
void epd_shutdown_display(void);
|
||||
void epd_refresh(void);
|
||||
void epd_clear(epd_color_t level);
|
||||
void epd_clear(epd_color level);
|
||||
void epd_draw_bitmap(epd_color clearColor, const uint8 *bitmap);
|
||||
void epd_draw_bitmap_grayscale(epd_color clearColor, const uint8 *bitmap);
|
||||
bool epd_is_asleep(void);
|
||||
Reference in New Issue
Block a user