569bec322f
Adds a second panel target alongside the 7.3": - src/panels/waveshare13e6/v1/ — full epd.h impl with hardware SPI on FSPI, dual-CS dispatch (CS_M/CS_S split halves), PSRAM framebuffer for image/QR/setup-screen render paths - src/test_display_13e6.cpp + [env:test-display-13e6] — self-contained first-pixels color-bar smoke test, kept as a hardware diagnostic - [env:waveshare13e6-v1] — production env: ESP32-S3-WROOM-2 N32R16V with OPI flash + OPI PSRAM (the WROOM-2 is octal flash; QIO mode crashes at do_core_init startup.c:328) - scripts/gen_screens_13e6.py + data/waveshare13e6-v1/ — 1200x1600 portrait setup screens with QR overlay regions matching the driver - scripts/data_dir.py — extra_scripts shim that routes uploadfs to the right data/ tree based on $PIOENV (PlatformIO ignores per-env data_dir) - src/epd.h: epd_setup_pins() abstraction so each panel driver owns its own pinMode + SPI.begin; main/test_display/sim_border lose all panel-specific GPIO and call epd_setup_pins() once at boot - src/operation.h: report PANEL_ID via X-Panel-Id header on every poll so the server can auto-correct Device.model 7.3" production env stays byte-identical, all 43 native tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
201 lines
6.3 KiB
C++
201 lines
6.3 KiB
C++
#include "epd.h"
|
|
#include "config.h"
|
|
#include <LittleFS.h>
|
|
#include <qrcode.h>
|
|
#include <esp_task_wdt.h>
|
|
|
|
static uint8_t s_row[EPD_WIDTH / 2];
|
|
|
|
void epd_setup_pins() {
|
|
pinMode(PIN_CS, OUTPUT);
|
|
pinMode(PIN_DC, OUTPUT);
|
|
pinMode(PIN_RST, OUTPUT);
|
|
pinMode(PIN_BUSY, INPUT);
|
|
SPI.begin(PIN_SCK, -1, PIN_MOSI, PIN_CS);
|
|
SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
|
}
|
|
|
|
static void wait_busy() {
|
|
uint32_t start = millis();
|
|
while (digitalRead(PIN_BUSY) == LOW) {
|
|
if (millis() - start > 60000) return; // 6-color refresh takes ~20s
|
|
delay(5);
|
|
esp_task_wdt_reset(); // feed WDT — display refresh can take ~20 s
|
|
}
|
|
}
|
|
static void cmd(uint8_t c) {
|
|
digitalWrite(PIN_DC, LOW);
|
|
digitalWrite(PIN_CS, LOW);
|
|
SPI.transfer(c);
|
|
digitalWrite(PIN_CS, HIGH);
|
|
}
|
|
static void dat(uint8_t d) {
|
|
digitalWrite(PIN_DC, HIGH);
|
|
digitalWrite(PIN_CS, LOW);
|
|
SPI.transfer(d);
|
|
digitalWrite(PIN_CS, HIGH);
|
|
}
|
|
|
|
void epd_init() {
|
|
digitalWrite(PIN_RST, HIGH); delay(20);
|
|
digitalWrite(PIN_RST, LOW); delay(10);
|
|
digitalWrite(PIN_RST, HIGH); delay(20);
|
|
wait_busy(); delay(30);
|
|
|
|
cmd(0xAA);
|
|
dat(0x49); dat(0x55); dat(0x20);
|
|
dat(0x08); dat(0x09); dat(0x18);
|
|
cmd(0x01); dat(0x3F);
|
|
cmd(0x00); dat(0x5F); dat(0x69);
|
|
cmd(0x03); dat(0x00); dat(0x54); dat(0x00); dat(0x44);
|
|
cmd(0x05); dat(0x40); dat(0x1F); dat(0x1F); dat(0x2C);
|
|
cmd(0x06); dat(0x6F); dat(0x1F); dat(0x17); dat(0x49);
|
|
cmd(0x08); dat(0x6F); dat(0x1F); dat(0x1F); dat(0x22);
|
|
cmd(0x30); dat(0x03);
|
|
cmd(0x50); dat(0x3F);
|
|
cmd(0x60); dat(0x02); dat(0x00);
|
|
cmd(0x61); dat(0x03); dat(0x20); dat(0x01); dat(0xE0);
|
|
cmd(0x84); dat(0x01);
|
|
cmd(0xE3); dat(0x2F);
|
|
cmd(0x04); wait_busy();
|
|
}
|
|
|
|
void epd_sleep() {
|
|
cmd(0x02); dat(0x00); wait_busy();
|
|
cmd(0x07); dat(0xA5);
|
|
}
|
|
|
|
static void epd_refresh() {
|
|
cmd(0x04); wait_busy();
|
|
cmd(0x12); dat(0x00); wait_busy();
|
|
}
|
|
|
|
void epd_fill(uint8_t color) {
|
|
uint8_t byte = (color << 4) | color;
|
|
cmd(0x10);
|
|
digitalWrite(PIN_DC, HIGH);
|
|
digitalWrite(PIN_CS, LOW);
|
|
for (int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 2; i++) SPI.transfer(byte);
|
|
digitalWrite(PIN_CS, HIGH);
|
|
epd_refresh();
|
|
}
|
|
|
|
void epd_draw_image_from_file(fs::File& f) {
|
|
cmd(0x10);
|
|
uint8_t buf[512];
|
|
digitalWrite(PIN_DC, HIGH);
|
|
digitalWrite(PIN_CS, LOW);
|
|
while (f.available()) {
|
|
size_t n = f.read(buf, sizeof(buf));
|
|
SPI.writeBytes(buf, n);
|
|
}
|
|
digitalWrite(PIN_CS, HIGH);
|
|
epd_refresh();
|
|
}
|
|
|
|
void epd_draw_image_with_border(fs::File& f, uint8_t color, int thickness) {
|
|
const size_t expected = (size_t)EPD_WIDTH * EPD_HEIGHT / 2;
|
|
if (f.size() != expected) {
|
|
epd_fill(color);
|
|
return;
|
|
}
|
|
|
|
const uint8_t pair = (color << 4) | color;
|
|
|
|
cmd(0x10);
|
|
for (int y = 0; y < EPD_HEIGHT; y++) {
|
|
f.read(s_row, sizeof(s_row));
|
|
|
|
if (y < thickness || y >= EPD_HEIGHT - thickness) {
|
|
// Top/bottom band — solid color across the row.
|
|
for (size_t i = 0; i < sizeof(s_row); i++) s_row[i] = pair;
|
|
} else {
|
|
// Middle band — overlay border on left/right edges.
|
|
for (int x = 0; x < thickness; x++) {
|
|
if (x & 1) s_row[x/2] = (s_row[x/2] & 0xF0) | color;
|
|
else s_row[x/2] = (s_row[x/2] & 0x0F) | (color << 4);
|
|
}
|
|
for (int x = EPD_WIDTH - thickness; x < EPD_WIDTH; x++) {
|
|
if (x & 1) s_row[x/2] = (s_row[x/2] & 0xF0) | color;
|
|
else s_row[x/2] = (s_row[x/2] & 0x0F) | (color << 4);
|
|
}
|
|
}
|
|
|
|
digitalWrite(PIN_DC, HIGH); digitalWrite(PIN_CS, LOW);
|
|
SPI.writeBytes(s_row, sizeof(s_row));
|
|
digitalWrite(PIN_CS, HIGH);
|
|
}
|
|
epd_refresh();
|
|
}
|
|
|
|
void epd_draw_qr(QRCode* qr, uint8_t cellPx, uint8_t bg, uint8_t fg) {
|
|
int qrPx = qr->size * cellPx;
|
|
int offX = (EPD_WIDTH - qrPx) / 2;
|
|
int offY = (EPD_HEIGHT - qrPx) / 2;
|
|
|
|
cmd(0x10);
|
|
for (int y = 0; y < EPD_HEIGHT; y++) {
|
|
for (int x = 0; x < EPD_WIDTH; x += 2) {
|
|
auto nibble = [&](int px) -> uint8_t {
|
|
int qx = (px - offX) / cellPx, qy = (y - offY) / cellPx;
|
|
if (qx >= 0 && qx < qr->size && qy >= 0 && qy < qr->size)
|
|
return qrcode_getModule(qr, qx, qy) ? fg : bg;
|
|
return bg;
|
|
};
|
|
s_row[x/2] = (nibble(x) << 4) | nibble(x+1);
|
|
}
|
|
digitalWrite(PIN_DC, HIGH); digitalWrite(PIN_CS, LOW);
|
|
SPI.writeBytes(s_row, sizeof(s_row));
|
|
digitalWrite(PIN_CS, HIGH);
|
|
}
|
|
epd_refresh();
|
|
}
|
|
|
|
// Stream background from LittleFS, overlaying QR at (qr_x, qr_y) with given cell size.
|
|
// Falls back to a solid fill if the file is missing.
|
|
static void draw_from_lfs(const char* path, uint8_t fallback_color,
|
|
QRCode* qr, int qr_x, int qr_y, int qr_cell) {
|
|
File f = LittleFS.open(path, "r");
|
|
if (!f) { epd_fill(fallback_color); return; }
|
|
|
|
int qr_px = qr->size * qr_cell;
|
|
|
|
cmd(0x10);
|
|
for (int y = 0; y < EPD_HEIGHT; y++) {
|
|
f.read(s_row, sizeof(s_row));
|
|
|
|
if (y >= qr_y && y < qr_y + qr_px) {
|
|
int qy = (y - qr_y) / qr_cell;
|
|
int x0 = max(qr_x, 0), x1 = min(qr_x + qr_px, EPD_WIDTH);
|
|
for (int x = x0; x < x1; x++) {
|
|
uint8_t c = qrcode_getModule(qr, (x - qr_x) / qr_cell, qy)
|
|
? COLOR_BLACK : COLOR_WHITE;
|
|
if (x & 1) s_row[x/2] = (s_row[x/2] & 0xF0) | c;
|
|
else s_row[x/2] = (s_row[x/2] & 0x0F) | (c << 4);
|
|
}
|
|
}
|
|
|
|
digitalWrite(PIN_DC, HIGH); digitalWrite(PIN_CS, LOW);
|
|
SPI.writeBytes(s_row, sizeof(s_row));
|
|
digitalWrite(PIN_CS, HIGH);
|
|
}
|
|
f.close();
|
|
epd_refresh();
|
|
}
|
|
|
|
void epd_draw_ap_screen(QRCode* qr) {
|
|
// AP_QR_X=581, AP_QR_Y=100, AP_QR_CELL=4 (must match gen_screens.py)
|
|
draw_from_lfs("/ap_bg.bin", COLOR_YELLOW, qr, 581, 100, 4);
|
|
}
|
|
|
|
void epd_draw_ap_screen_retry(QRCode* qr) {
|
|
// Same QR coordinates — only the bg .bin differs (red accents,
|
|
// "Connection Failed — try again" label).
|
|
draw_from_lfs("/ap_bg_retry.bin", COLOR_RED, qr, 581, 100, 4);
|
|
}
|
|
|
|
void epd_draw_setup_screen(QRCode* qr) {
|
|
// SETUP_QR_X=553, SETUP_QR_Y=175, SETUP_QR_CELL=5 (must match gen_screens.py)
|
|
draw_from_lfs("/setup_bg.bin", COLOR_GREEN, qr, 553, 175, 5);
|
|
}
|