feat(13e6): bring up Waveshare 13.3" Spectra-6 end-to-end
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>
This commit is contained in:
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
+64
-6
@@ -10,12 +10,13 @@
|
|||||||
; Old envs preserved as historical snapshots — re-flashing units in the
|
; Old envs preserved as historical snapshots — re-flashing units in the
|
||||||
; field stays a one-line `pio run -e <env> --target upload` command.
|
; field stays a one-line `pio run -e <env> --target upload` command.
|
||||||
|
|
||||||
; data_dir lives at project level (PlatformIO ignores it inside [env:…]
|
; data_dir is single project-wide in [platformio]; PlatformIO silently
|
||||||
; blocks — the silent ignore once led to a LittleFS upload that put every
|
; ignores it inside [env:…] blocks. To route uploadfs to the right
|
||||||
; file under /waveshare73-v1/ instead of /, blanking the panel to the
|
; LittleFS payload per panel (waveshare73-v1 vs waveshare13e6-v1), each
|
||||||
; epd_fill yellow fallback). All current envs target the V1 panel; when
|
; env wires in scripts/data_dir.py via extra_scripts, which overrides
|
||||||
; a second panel ships, swap this via an extra_scripts shim that picks
|
; PROJECT_DATA_DIR from $PIOENV before SCons evaluates uploadfs.
|
||||||
; the dir from $PIOENV before uploadfs runs.
|
; The project-level default below keeps tooling that runs outside a
|
||||||
|
; specific env (e.g. raw `pio run`) pointed at the 7.3" payload.
|
||||||
[platformio]
|
[platformio]
|
||||||
data_dir = data/waveshare73-v1
|
data_dir = data/waveshare73-v1
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ upload_port = /dev/ttyUSB0
|
|||||||
monitor_port = /dev/ttyUSB0
|
monitor_port = /dev/ttyUSB0
|
||||||
monitor_speed = 115200
|
monitor_speed = 115200
|
||||||
board_build.filesystem = littlefs
|
board_build.filesystem = littlefs
|
||||||
|
extra_scripts = pre:scripts/data_dir.py
|
||||||
build_src_filter =
|
build_src_filter =
|
||||||
+<main.cpp>
|
+<main.cpp>
|
||||||
+<panels/waveshare73/v1/>
|
+<panels/waveshare73/v1/>
|
||||||
@@ -85,6 +87,62 @@ build_flags =
|
|||||||
-DSIM_BORDER
|
-DSIM_BORDER
|
||||||
-DSIM_BORDER_COLOR=COLOR_RED
|
-DSIM_BORDER_COLOR=COLOR_RED
|
||||||
|
|
||||||
|
; ── Production firmware: Waveshare 13.3" Spectra-6 + ESP32-S3-ePaper-13.3E6 ──
|
||||||
|
; All-in-one board: ESP32-S3-WROOM-2-N32R16V (32MB OPI flash, 16MB OPI PSRAM),
|
||||||
|
; CH343 USB-serial on USB-C. PIN_CS aliases CS_M only to satisfy config.h's
|
||||||
|
; required-pin check — the driver uses CS_M / CS_S explicitly, never PIN_CS.
|
||||||
|
[env:waveshare13e6-v1]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-s3-devkitc-1
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
board_build.flash_size = 32MB
|
||||||
|
board_build.flash_mode = opi
|
||||||
|
board_upload.flash_size = 32MB
|
||||||
|
board_build.arduino.memory_type = opi_opi
|
||||||
|
board_build.filesystem = littlefs
|
||||||
|
extra_scripts = pre:scripts/data_dir.py
|
||||||
|
build_src_filter =
|
||||||
|
+<main.cpp>
|
||||||
|
+<panels/waveshare13e6/v1/>
|
||||||
|
build_flags =
|
||||||
|
-DEPD_WIDTH=1200
|
||||||
|
-DEPD_HEIGHT=1600
|
||||||
|
-DMAX_PANEL_WIDTH=1200
|
||||||
|
-DPIN_SCK=9
|
||||||
|
-DPIN_MOSI=46
|
||||||
|
-DPIN_CS=10
|
||||||
|
-DPIN_CS_M=10
|
||||||
|
-DPIN_CS_S=3
|
||||||
|
-DPIN_DC=11
|
||||||
|
-DPIN_RST=2
|
||||||
|
-DPIN_BUSY=12
|
||||||
|
-DPIN_PWR=1
|
||||||
|
-DPANEL_ID=\"waveshare-13.3-spectra6\"
|
||||||
|
-DBOARD_HAS_PSRAM
|
||||||
|
lib_deps =
|
||||||
|
ricmoo/QRCode@^0.0.1
|
||||||
|
|
||||||
|
; ── First-pixels smoke test for ESP32-S3-ePaper-13.3E6 ──
|
||||||
|
; Self-contained color-bar bringup, no LittleFS / no shared driver. Kept
|
||||||
|
; alongside the production env as a fallback for hardware diagnostics:
|
||||||
|
; if waveshare13e6-v1 misbehaves, this verifies the panel + SPI bus alone.
|
||||||
|
; pio run -e test-display-13e6 --target upload
|
||||||
|
[env:test-display-13e6]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-s3-devkitc-1
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
board_build.flash_size = 32MB
|
||||||
|
board_build.flash_mode = opi
|
||||||
|
board_upload.flash_size = 32MB
|
||||||
|
board_build.arduino.memory_type = opi_opi
|
||||||
|
build_src_filter =
|
||||||
|
+<test_display_13e6.cpp>
|
||||||
|
build_flags =
|
||||||
|
-DENV_TEST_DISPLAY_13E6
|
||||||
|
-DBOARD_HAS_PSRAM
|
||||||
|
|
||||||
; ── Native unit tests — no hardware, uses test/mocks/ ──
|
; ── Native unit tests — no hardware, uses test/mocks/ ──
|
||||||
[env:native-test]
|
[env:native-test]
|
||||||
platform = native
|
platform = native
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""
|
||||||
|
Per-env data_dir multiplexer.
|
||||||
|
|
||||||
|
PlatformIO's [platformio].data_dir is a single project-level setting — it
|
||||||
|
ignores `data_dir` inside [env:...] blocks. With more than one panel in the
|
||||||
|
tree (waveshare73-v1, waveshare13e6-v1), we need to route uploadfs to the
|
||||||
|
right LittleFS payload based on the active env.
|
||||||
|
|
||||||
|
Wired into envs via:
|
||||||
|
extra_scripts = pre:scripts/data_dir.py
|
||||||
|
|
||||||
|
The script runs before SCons evaluates uploadfs and overrides
|
||||||
|
PROJECT_DATA_DIR for the envs in `ENV_TO_DATA`. Envs not listed fall
|
||||||
|
through to the project default (set in [platformio]).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
Import("env") # noqa: F821 — provided by PlatformIO's SCons context
|
||||||
|
|
||||||
|
ENV_TO_DATA = {
|
||||||
|
"waveshare73-v1": "waveshare73-v1",
|
||||||
|
"test-display": "waveshare73-v1",
|
||||||
|
"sim-yellow": "waveshare73-v1",
|
||||||
|
"sim-red": "waveshare73-v1",
|
||||||
|
"waveshare13e6-v1": "waveshare13e6-v1",
|
||||||
|
}
|
||||||
|
|
||||||
|
pioenv = env["PIOENV"] # noqa: F821
|
||||||
|
if pioenv in ENV_TO_DATA:
|
||||||
|
data_dir = os.path.join(env["PROJECT_DIR"], "data", ENV_TO_DATA[pioenv]) # noqa: F821
|
||||||
|
env.Replace(PROJECT_DATA_DIR=data_dir) # noqa: F821
|
||||||
|
print(f"[data_dir] {pioenv} -> {data_dir}")
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Generate 1200×1600 portrait setup-screen backgrounds for the Waveshare
|
||||||
|
13.3" Spectra-6 panel.
|
||||||
|
|
||||||
|
Layout is a single-column vertical stack — much simpler than the 800×480
|
||||||
|
three-panel design in gen_screens.py — chosen for the end-to-end port
|
||||||
|
MVP. Polish the visual treatment later once the provisioning flow is
|
||||||
|
proven on real hardware.
|
||||||
|
|
||||||
|
Run from the firmware/ directory:
|
||||||
|
python3 scripts/gen_screens_13e6.py
|
||||||
|
|
||||||
|
Constants used by src/panels/waveshare13e6/v1/epd_driver.cpp:
|
||||||
|
AP_QR_X, AP_QR_Y, AP_QR_CELL
|
||||||
|
SETUP_QR_X, SETUP_QR_Y, SETUP_QR_CELL
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
import qrcode
|
||||||
|
import os, sys
|
||||||
|
|
||||||
|
MANUAL_URL = "https://pictureframe.edholm.me/help"
|
||||||
|
|
||||||
|
W, H = 1200, 1600
|
||||||
|
|
||||||
|
# ── Spectra-6 palette ─────────────────────────────────────────────────────────
|
||||||
|
BLACK = 0x0; BK = (26, 26, 26 )
|
||||||
|
WHITE = 0x1; WH = (245, 245, 240)
|
||||||
|
YELLOW = 0x2; YL = (240, 208, 0 )
|
||||||
|
RED = 0x3; RD = (192, 48, 32 )
|
||||||
|
BLUE = 0x5; BL = (24, 64, 192)
|
||||||
|
GREEN = 0x6; GR = (16, 160, 64 )
|
||||||
|
|
||||||
|
PALETTE_RGB = {BLACK: BK, WHITE: WH, YELLOW: YL, RED: RD, BLUE: BL, GREEN: GR}
|
||||||
|
|
||||||
|
|
||||||
|
def nearest(r, g, b):
|
||||||
|
best, best_d = WHITE, float("inf")
|
||||||
|
for n, (pr, pg, pb) in PALETTE_RGB.items():
|
||||||
|
d = (r - pr) ** 2 + (g - pg) ** 2 + (b - pb) ** 2
|
||||||
|
if d < best_d:
|
||||||
|
best, best_d = n, d
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
def pack(img):
|
||||||
|
"""RGB PIL → 4bpp packed bytearray, row-major panel-native order."""
|
||||||
|
px = img.load()
|
||||||
|
out = bytearray()
|
||||||
|
for y in range(H):
|
||||||
|
for x in range(0, W, 2):
|
||||||
|
hi = nearest(*px[x, y])
|
||||||
|
lo = nearest(*px[x + 1, y])
|
||||||
|
out.append((hi << 4) | lo)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# ── Fonts ────────────────────────────────────────────────────────────────────
|
||||||
|
FONT_DIR = "/usr/share/fonts/truetype/dejavu"
|
||||||
|
def ttf(name, size):
|
||||||
|
try:
|
||||||
|
return ImageFont.truetype(os.path.join(FONT_DIR, name), size)
|
||||||
|
except Exception:
|
||||||
|
return ImageFont.load_default()
|
||||||
|
|
||||||
|
|
||||||
|
F_HEADER = ttf("DejaVuSans-Bold.ttf", 44)
|
||||||
|
F_TITLE = ttf("DejaVuSans-Bold.ttf", 60)
|
||||||
|
F_LABEL = ttf("DejaVuSans-Bold.ttf", 32)
|
||||||
|
F_STEP = ttf("DejaVuSans.ttf", 32)
|
||||||
|
F_STEP_B = ttf("DejaVuSans-Bold.ttf", 32)
|
||||||
|
F_FOOT = ttf("DejaVuSans.ttf", 28)
|
||||||
|
F_NUM = ttf("DejaVuSans-Bold.ttf", 30)
|
||||||
|
F_TINY = ttf("DejaVuSans-Bold.ttf", 20)
|
||||||
|
|
||||||
|
|
||||||
|
# ── QR overlay regions — must match the panel driver ──────────────────────────
|
||||||
|
# Cell sizes are chosen so each QR fits comfortably in its vertical band with
|
||||||
|
# room for label + caption. Centered horizontally on a 1200-wide canvas.
|
||||||
|
AP_QR_MODS = 37 # version 5, ECC_LOW
|
||||||
|
AP_QR_CELL = 16 # 37 × 16 = 592 px square
|
||||||
|
AP_QR_PX = AP_QR_MODS * AP_QR_CELL
|
||||||
|
AP_QR_X = (W - AP_QR_PX) // 2 # 304
|
||||||
|
AP_QR_Y = 220
|
||||||
|
|
||||||
|
SETUP_QR_MODS = 41 # version 6, ECC_LOW
|
||||||
|
SETUP_QR_CELL = 14 # 41 × 14 = 574 px square
|
||||||
|
SETUP_QR_PX = SETUP_QR_MODS * SETUP_QR_CELL
|
||||||
|
SETUP_QR_X = (W - SETUP_QR_PX) // 2 # 313
|
||||||
|
SETUP_QR_Y = 450
|
||||||
|
|
||||||
|
HEADER_H = 120
|
||||||
|
|
||||||
|
|
||||||
|
def text_center(draw, cx, y, text, font, fill):
|
||||||
|
bb = draw.textbbox((0, 0), text, font=font)
|
||||||
|
tw = bb[2] - bb[0]
|
||||||
|
draw.text((cx - tw // 2, y), text, font=font, fill=fill)
|
||||||
|
|
||||||
|
|
||||||
|
def leave_qr_white(draw, qr_x, qr_y, qr_px):
|
||||||
|
draw.rectangle([qr_x, qr_y, qr_x + qr_px - 1, qr_y + qr_px - 1], fill=WH)
|
||||||
|
|
||||||
|
|
||||||
|
def draw_qr_frame(draw, qx, qy, qp, accent):
|
||||||
|
"""Two-layer decorative border around a QR placeholder."""
|
||||||
|
draw.rectangle([qx - 12, qy - 12, qx + qp + 11, qy + qp + 11], outline=accent, width=6)
|
||||||
|
draw.rectangle([qx - 4, qy - 4, qx + qp + 3, qy + qp + 3], outline=BK, width=4)
|
||||||
|
|
||||||
|
|
||||||
|
# ── AP SCREEN (Step 1/2 — WiFi join) ──────────────────────────────────────────
|
||||||
|
def gen_ap(accent=YL, header_text="SETUP MODE — STEP 1 OF 2",
|
||||||
|
qr_caption="Scan to join PictureFrame"):
|
||||||
|
img = Image.new("RGB", (W, H), WH)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
# Status header band
|
||||||
|
draw.rectangle([0, 0, W - 1, HEADER_H - 1], fill=accent)
|
||||||
|
text_center(draw, W // 2, 38, header_text, F_HEADER, BK)
|
||||||
|
|
||||||
|
# WiFi QR step
|
||||||
|
text_center(draw, W // 2, AP_QR_Y - 60, "STEP 1 — JOIN WIFI", F_LABEL, BK)
|
||||||
|
draw_qr_frame(draw, AP_QR_X, AP_QR_Y, AP_QR_PX, accent)
|
||||||
|
leave_qr_white(draw, AP_QR_X, AP_QR_Y, AP_QR_PX)
|
||||||
|
text_center(draw, W // 2, AP_QR_Y + AP_QR_PX + 16, qr_caption, F_FOOT, BK)
|
||||||
|
|
||||||
|
# URL QR step — static, baked into the bg. Scanning it opens Safari,
|
||||||
|
# which forces iOS to render the captive portal.
|
||||||
|
url_qr = qrcode.QRCode(
|
||||||
|
version=None,
|
||||||
|
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||||
|
box_size=10,
|
||||||
|
border=2,
|
||||||
|
)
|
||||||
|
url_qr.add_data("http://192.168.4.1/")
|
||||||
|
url_qr.make(fit=True)
|
||||||
|
url_img = url_qr.make_image(fill_color=BK, back_color=WH).convert("RGB")
|
||||||
|
url_w, url_h = url_img.size
|
||||||
|
url_y = 1180
|
||||||
|
url_x = (W - url_w) // 2
|
||||||
|
|
||||||
|
text_center(draw, W // 2, url_y - 60, "STEP 2 — OPEN PAGE", F_LABEL, BK)
|
||||||
|
draw.rectangle([url_x - 12, url_y - 12, url_x + url_w + 11, url_y + url_h + 11],
|
||||||
|
outline=accent, width=6)
|
||||||
|
draw.rectangle([url_x - 4, url_y - 4, url_x + url_w + 3, url_y + url_h + 3],
|
||||||
|
outline=BK, width=4)
|
||||||
|
img.paste(url_img, (url_x, url_y))
|
||||||
|
text_center(draw, W // 2, url_y + url_h + 16, "http://192.168.4.1/", F_FOOT, BK)
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
def gen_ap_retry():
|
||||||
|
"""Step 1/2 with red accent + retry messaging."""
|
||||||
|
return gen_ap(
|
||||||
|
accent=RD,
|
||||||
|
header_text="CONNECTION FAILED — TRY AGAIN",
|
||||||
|
qr_caption="Connection failed — try again",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ── SETUP SCREEN (post-WiFi, scan-to-claim) ───────────────────────────────────
|
||||||
|
def gen_setup():
|
||||||
|
img = Image.new("RGB", (W, H), WH)
|
||||||
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
|
draw.rectangle([0, 0, W - 1, HEADER_H - 1], fill=GR)
|
||||||
|
text_center(draw, W // 2, 38, "WIFI CONNECTED — STEP 2 OF 2", F_HEADER, WH)
|
||||||
|
|
||||||
|
text_center(draw, W // 2, 200, "Almost ready", F_TITLE, BK)
|
||||||
|
text_center(draw, W // 2, 290, "Scan the QR to link this frame", F_STEP, BK)
|
||||||
|
text_center(draw, W // 2, 332, "to your pictureframe.edholm.me account.", F_STEP, BK)
|
||||||
|
|
||||||
|
text_center(draw, W // 2, SETUP_QR_Y - 60, "SCAN TO FINISH", F_LABEL, BK)
|
||||||
|
draw_qr_frame(draw, SETUP_QR_X, SETUP_QR_Y, SETUP_QR_PX, GR)
|
||||||
|
leave_qr_white(draw, SETUP_QR_X, SETUP_QR_Y, SETUP_QR_PX)
|
||||||
|
|
||||||
|
# MAC chip below QR — populated at runtime by the firmware would be nicer,
|
||||||
|
# but the firmware doesn't write text yet on this panel; the link-target
|
||||||
|
# URL is what the user actually scans, so the chip stays a static "your
|
||||||
|
# frame ID will appear in the QR" placeholder for now.
|
||||||
|
text_center(draw, W // 2,
|
||||||
|
SETUP_QR_Y + SETUP_QR_PX + 16,
|
||||||
|
"Your frame ID is encoded in the QR above.",
|
||||||
|
F_FOOT, BK)
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
|
||||||
|
# ── Save ──────────────────────────────────────────────────────────────────────
|
||||||
|
def save_bin(img, path, preview_path):
|
||||||
|
data = pack(img)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
print(f"Written {len(data):,} bytes → {os.path.abspath(path)}")
|
||||||
|
|
||||||
|
prev = Image.new("RGB", (W, H))
|
||||||
|
px = prev.load()
|
||||||
|
for y in range(H):
|
||||||
|
for x in range(0, W, 2):
|
||||||
|
byte = data[y * (W // 2) + x // 2]
|
||||||
|
px[x, y] = PALETTE_RGB.get(byte >> 4, (128, 128, 128))
|
||||||
|
px[x + 1, y] = PALETTE_RGB.get(byte & 0xF, (128, 128, 128))
|
||||||
|
prev.save(preview_path)
|
||||||
|
print(f"Preview → {os.path.abspath(preview_path)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
out_dir = os.path.join(os.path.dirname(__file__), "../data/waveshare13e6-v1")
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
print("Generating AP screen…")
|
||||||
|
save_bin(gen_ap(), f"{out_dir}/ap_bg.bin", f"{out_dir}/ap_bg_preview.png")
|
||||||
|
print()
|
||||||
|
print("Generating AP retry screen…")
|
||||||
|
save_bin(gen_ap_retry(), f"{out_dir}/ap_bg_retry.bin", f"{out_dir}/ap_bg_retry_preview.png")
|
||||||
|
print()
|
||||||
|
print("Generating setup screen…")
|
||||||
|
save_bin(gen_setup(), f"{out_dir}/setup_bg.bin", f"{out_dir}/setup_bg_preview.png")
|
||||||
|
print()
|
||||||
|
print("QR overlay constants — keep these in sync with epd_driver.cpp:")
|
||||||
|
print(f" AP_QR_X={AP_QR_X}, AP_QR_Y={AP_QR_Y}, AP_QR_CELL={AP_QR_CELL}, AP_QR_PX={AP_QR_PX}")
|
||||||
|
print(f" SETUP_QR_X={SETUP_QR_X}, SETUP_QR_Y={SETUP_QR_Y}, "
|
||||||
|
f"SETUP_QR_CELL={SETUP_QR_CELL}, SETUP_QR_PX={SETUP_QR_PX}")
|
||||||
@@ -3,6 +3,13 @@
|
|||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
#include <FS.h>
|
#include <FS.h>
|
||||||
|
|
||||||
|
// One-shot panel hardware init — pinModes for the panel's GPIO contract and
|
||||||
|
// SPI.begin() with the right bus/CS layout. Each panel driver implements
|
||||||
|
// this for its own pinout: single-CS for the 7.3", dual-CS + power-enable
|
||||||
|
// for the 13.3". Callers hit this once at boot, then use epd_init() per
|
||||||
|
// redraw cycle.
|
||||||
|
void epd_setup_pins();
|
||||||
|
|
||||||
void epd_init();
|
void epd_init();
|
||||||
void epd_sleep();
|
void epd_sleep();
|
||||||
void epd_fill(uint8_t color);
|
void epd_fill(uint8_t color);
|
||||||
|
|||||||
+3
-8
@@ -300,15 +300,10 @@ void setup() {
|
|||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
Serial.println("pictureFrame boot");
|
Serial.println("pictureFrame boot");
|
||||||
|
|
||||||
// Init GPIO
|
// BOOT button is panel-agnostic; everything else (CS/DC/RST/BUSY + SPI)
|
||||||
pinMode(PIN_CS, OUTPUT);
|
// is panel-private and lives in the active driver's epd_setup_pins().
|
||||||
pinMode(PIN_DC, OUTPUT);
|
|
||||||
pinMode(PIN_RST, OUTPUT);
|
|
||||||
pinMode(PIN_BUSY, INPUT);
|
|
||||||
pinMode(PIN_BTN_RESET, INPUT_PULLUP);
|
pinMode(PIN_BTN_RESET, INPUT_PULLUP);
|
||||||
|
epd_setup_pins();
|
||||||
SPI.begin(PIN_SCK, -1, PIN_MOSI, PIN_CS);
|
|
||||||
SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
|
||||||
|
|
||||||
LittleFS.begin(true); // format on first use
|
LittleFS.begin(true); // format on first use
|
||||||
|
|
||||||
|
|||||||
@@ -134,6 +134,13 @@ void normal_operation_impl(const String& mac, HTTP& http, const String& url, Pre
|
|||||||
http.addHeader("X-Current-Image-Id", String(currentImgId));
|
http.addHeader("X-Current-Image-Id", String(currentImgId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Report the panel hardware so the server can route image-rendering
|
||||||
|
// dimensions to the right DeviceModel. Comes from -DPANEL_ID in the
|
||||||
|
// active env's build_flags (see config.h). Sent on every poll so the
|
||||||
|
// server can correct a mis-set Device.model lazily without needing
|
||||||
|
// a separate registration handshake.
|
||||||
|
http.addHeader("X-Panel-Id", PANEL_ID);
|
||||||
|
|
||||||
// Tell the server how we got here. The server uses this to honor a
|
// Tell the server how we got here. The server uses this to honor a
|
||||||
// power-cycle as a deliberate "force resync" — a poll that arrives with
|
// power-cycle as a deliberate "force resync" — a poll that arrives with
|
||||||
// X-Boot-Reason: cold gets a fresh rotation even outside configured wake
|
// X-Boot-Reason: cold gets a fresh rotation even outside configured wake
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
// Waveshare 13.3" Spectra-6 (E6) panel driver — implements epd.h for the
|
||||||
|
// ESP32-S3-ePaper-13.3E6 board.
|
||||||
|
//
|
||||||
|
// Hardware: 1200 × 1600 pixels, 6 colors, 4 bits-per-pixel packed two
|
||||||
|
// pixels per byte (full framebuffer = 960 KB). Panel is internally split:
|
||||||
|
// the left 600 columns are driven via CS_M (master), the right 600 via
|
||||||
|
// CS_S (slave), both sharing SCK / MOSI / DC / BUSY. Init commands go to
|
||||||
|
// both halves with both CS lines asserted; framebuffer pushes go to one
|
||||||
|
// half at a time.
|
||||||
|
//
|
||||||
|
// .bin layout (server-rendered, panel-native): row-major, 600 bytes/row
|
||||||
|
// × 1600 rows. Within each row, bytes [0..300) are the left half and
|
||||||
|
// [300..600) are the right half. Server writes in this exact order; the
|
||||||
|
// driver streams the file into a PSRAM buffer then pushes each half's
|
||||||
|
// columns to its respective CS line.
|
||||||
|
//
|
||||||
|
// Init sequence + command set verified against:
|
||||||
|
// github.com/teatall/13.3inch_e-Paper_E-Frame (community photo-frame
|
||||||
|
// project on the same board) and Waveshare's stock 13in3e demo.
|
||||||
|
|
||||||
|
#include "epd.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include <LittleFS.h>
|
||||||
|
#include <qrcode.h>
|
||||||
|
#include <esp_task_wdt.h>
|
||||||
|
#include <esp_heap_caps.h>
|
||||||
|
|
||||||
|
static constexpr uint16_t W = 1200;
|
||||||
|
static constexpr uint16_t H = 1600;
|
||||||
|
static constexpr uint16_t BYTES_PER_ROW = W / 2; // 600
|
||||||
|
static constexpr uint16_t HALF_BYTES_ROW = W / 4; // 300 per half
|
||||||
|
static constexpr size_t FB_BYTES = (size_t)BYTES_PER_ROW * H; // 960000
|
||||||
|
|
||||||
|
// Driver-level "is the panel awake?" flag. Useful for asserts; not load-bearing.
|
||||||
|
static bool s_initialized = false;
|
||||||
|
|
||||||
|
// PSRAM framebuffer for image render paths. Allocated lazily on first use
|
||||||
|
// so a fill-only or QR-only path doesn't pay for it. Caller frees via
|
||||||
|
// release_fb() once the half-pushes are done.
|
||||||
|
static uint8_t* fb_alloc() {
|
||||||
|
return (uint8_t*)heap_caps_malloc(FB_BYTES, MALLOC_CAP_SPIRAM);
|
||||||
|
}
|
||||||
|
static void fb_release(uint8_t* fb) { if (fb) heap_caps_free(fb); }
|
||||||
|
|
||||||
|
// ── BUSY ───────────────────────────────────────────────────────────────────────
|
||||||
|
// Active-LOW: panel pulls BUSY low while working, releases high when idle.
|
||||||
|
// Full refresh on Spectra-6 takes ~25 s; bound the wait at 60 s so a
|
||||||
|
// hung panel doesn't strand the boot cycle.
|
||||||
|
static void wait_busy() {
|
||||||
|
uint32_t start = millis();
|
||||||
|
while (digitalRead(PIN_BUSY) == LOW) {
|
||||||
|
if (millis() - start > 60000) return;
|
||||||
|
delay(5);
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── CS / cmd / data helpers ────────────────────────────────────────────────────
|
||||||
|
// Pattern: assert one or both CS lines, send cmd byte with DC=LOW, then
|
||||||
|
// stream data with DC=HIGH, then deassert. Matches Waveshare's reference.
|
||||||
|
|
||||||
|
static inline void cs(int pin, int v) { digitalWrite(pin, v); }
|
||||||
|
static inline void cs_both(int v) { cs(PIN_CS_M, v); cs(PIN_CS_S, v); }
|
||||||
|
|
||||||
|
static void begin_cmd_both(uint8_t c) {
|
||||||
|
cs_both(LOW);
|
||||||
|
digitalWrite(PIN_DC, LOW);
|
||||||
|
SPI.transfer(c);
|
||||||
|
digitalWrite(PIN_DC, HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void begin_cmd(int cs_pin, uint8_t c) {
|
||||||
|
cs(cs_pin, LOW);
|
||||||
|
digitalWrite(PIN_DC, LOW);
|
||||||
|
SPI.transfer(c);
|
||||||
|
digitalWrite(PIN_DC, HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_data_n(const uint8_t* buf, size_t n) {
|
||||||
|
SPI.writeBytes(buf, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_cmd_with_data_both(uint8_t c, const uint8_t* buf, size_t n) {
|
||||||
|
begin_cmd_both(c);
|
||||||
|
if (buf && n) send_data_n(buf, n);
|
||||||
|
cs_both(HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_cmd_with_data_master(uint8_t c, const uint8_t* buf, size_t n) {
|
||||||
|
begin_cmd(PIN_CS_M, c);
|
||||||
|
if (buf && n) send_data_n(buf, n);
|
||||||
|
cs_both(HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Panel reset + init sequence ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
static void panel_reset() {
|
||||||
|
digitalWrite(PIN_RST, HIGH); delay(30);
|
||||||
|
digitalWrite(PIN_RST, LOW); delay(30);
|
||||||
|
digitalWrite(PIN_RST, HIGH); delay(30);
|
||||||
|
digitalWrite(PIN_RST, LOW); delay(30);
|
||||||
|
digitalWrite(PIN_RST, HIGH); delay(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
void epd_setup_pins() {
|
||||||
|
pinMode(PIN_PWR, OUTPUT);
|
||||||
|
pinMode(PIN_RST, OUTPUT);
|
||||||
|
pinMode(PIN_DC, OUTPUT);
|
||||||
|
pinMode(PIN_BUSY, INPUT);
|
||||||
|
pinMode(PIN_CS_M, OUTPUT);
|
||||||
|
pinMode(PIN_CS_S, OUTPUT);
|
||||||
|
|
||||||
|
digitalWrite(PIN_PWR, HIGH);
|
||||||
|
digitalWrite(PIN_CS_M, HIGH);
|
||||||
|
digitalWrite(PIN_CS_S, HIGH);
|
||||||
|
digitalWrite(PIN_RST, HIGH);
|
||||||
|
|
||||||
|
// FSPI on S3, manual CS. 10 MHz is well under the panel's documented
|
||||||
|
// 20 MHz ceiling and gives plenty of margin on the long traces between
|
||||||
|
// the module and the connector.
|
||||||
|
SPI.begin(PIN_SCK, -1, PIN_MOSI, -1);
|
||||||
|
SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void epd_init() {
|
||||||
|
panel_reset();
|
||||||
|
|
||||||
|
static const uint8_t AN_TM_V[] = {0xC0,0x1C,0x1C,0xCC,0xCC,0xCC,0x15,0x15,0x55};
|
||||||
|
static const uint8_t CMD66_V[] = {0x49,0x55,0x13,0x5D,0x05,0x10};
|
||||||
|
static const uint8_t PSR_V[] = {0xDF,0x69};
|
||||||
|
static const uint8_t CDI_V[] = {0xF7};
|
||||||
|
static const uint8_t TCON_V[] = {0x03,0x03};
|
||||||
|
static const uint8_t AGID_V[] = {0x10};
|
||||||
|
static const uint8_t PWS_V[] = {0x22};
|
||||||
|
static const uint8_t CCSET_V[] = {0x01};
|
||||||
|
static const uint8_t TRES_V[] = {0x04,0xB0,0x03,0x20};
|
||||||
|
static const uint8_t PWR_V[] = {0x0F,0x00,0x28,0x2C,0x28,0x38};
|
||||||
|
static const uint8_t EN_BUF_V[] = {0x07};
|
||||||
|
static const uint8_t BTST_P_V[] = {0xE8,0x28};
|
||||||
|
static const uint8_t BOOST_VDDP_EN_V[] = {0x01};
|
||||||
|
static const uint8_t BTST_N_V[] = {0xE8,0x28};
|
||||||
|
static const uint8_t BUCK_VDDN_V[] = {0x01};
|
||||||
|
static const uint8_t TFT_VCOM_V[] = {0x02};
|
||||||
|
|
||||||
|
// AN_TM goes to master only first (per reference; not obvious why but
|
||||||
|
// matching exactly de-risks bringup), then a batch of commands to
|
||||||
|
// both halves, then a tail of master-only tuning commands.
|
||||||
|
send_cmd_with_data_master(0x74, AN_TM_V, sizeof(AN_TM_V));
|
||||||
|
send_cmd_with_data_both (0xF0, CMD66_V, sizeof(CMD66_V));
|
||||||
|
send_cmd_with_data_both (0x00, PSR_V, sizeof(PSR_V));
|
||||||
|
send_cmd_with_data_both (0x50, CDI_V, sizeof(CDI_V));
|
||||||
|
send_cmd_with_data_both (0x60, TCON_V, sizeof(TCON_V));
|
||||||
|
send_cmd_with_data_both (0x86, AGID_V, sizeof(AGID_V));
|
||||||
|
send_cmd_with_data_both (0xE3, PWS_V, sizeof(PWS_V));
|
||||||
|
send_cmd_with_data_both (0xE0, CCSET_V, sizeof(CCSET_V));
|
||||||
|
send_cmd_with_data_both (0x61, TRES_V, sizeof(TRES_V));
|
||||||
|
send_cmd_with_data_master(0x01, PWR_V, sizeof(PWR_V));
|
||||||
|
send_cmd_with_data_master(0xB6, EN_BUF_V, sizeof(EN_BUF_V));
|
||||||
|
send_cmd_with_data_master(0x06, BTST_P_V, sizeof(BTST_P_V));
|
||||||
|
send_cmd_with_data_master(0xB7, BOOST_VDDP_EN_V, sizeof(BOOST_VDDP_EN_V));
|
||||||
|
send_cmd_with_data_master(0x05, BTST_N_V, sizeof(BTST_N_V));
|
||||||
|
send_cmd_with_data_master(0xB0, BUCK_VDDN_V, sizeof(BUCK_VDDN_V));
|
||||||
|
send_cmd_with_data_master(0xB1, TFT_VCOM_V, sizeof(TFT_VCOM_V));
|
||||||
|
|
||||||
|
s_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Refresh / sleep ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
static void epd_refresh() {
|
||||||
|
begin_cmd_both(0x04); // POWER_ON
|
||||||
|
cs_both(HIGH);
|
||||||
|
wait_busy();
|
||||||
|
|
||||||
|
delay(50);
|
||||||
|
begin_cmd_both(0x12); SPI.transfer(0x00); // DRF (full refresh)
|
||||||
|
cs_both(HIGH);
|
||||||
|
wait_busy();
|
||||||
|
|
||||||
|
begin_cmd_both(0x02); SPI.transfer(0x00); // POWER_OFF
|
||||||
|
cs_both(HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
void epd_sleep() {
|
||||||
|
cs_both(LOW);
|
||||||
|
digitalWrite(PIN_DC, LOW);
|
||||||
|
SPI.transfer(0x07); // DEEP_SLEEP
|
||||||
|
digitalWrite(PIN_DC, HIGH);
|
||||||
|
SPI.transfer(0xA5); // sentinel
|
||||||
|
cs_both(HIGH);
|
||||||
|
s_initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Draw helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// Push one half's framebuffer slice to its CS line. The slice is the
|
||||||
|
// HALF_BYTES_ROW × H bytes for that half, laid out row-major-contiguous.
|
||||||
|
static void push_half(int cs_pin, const uint8_t* half_fb) {
|
||||||
|
begin_cmd(cs_pin, 0x10);
|
||||||
|
send_data_n(half_fb, (size_t)HALF_BYTES_ROW * H);
|
||||||
|
cs(cs_pin, HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take a full-frame row-major framebuffer (BYTES_PER_ROW × H) and push
|
||||||
|
// left-then-right halves. Needs a scratch buffer to deinterleave halves
|
||||||
|
// from the row-major layout — the SPI bus needs contiguous bytes per CS.
|
||||||
|
static void push_full_frame(const uint8_t* fb) {
|
||||||
|
// Allocate a half-slice scratch buffer in PSRAM. 300 × 1600 = 480 KB.
|
||||||
|
constexpr size_t HALF_BYTES = (size_t)HALF_BYTES_ROW * H;
|
||||||
|
uint8_t* slice = (uint8_t*)heap_caps_malloc(HALF_BYTES, MALLOC_CAP_SPIRAM);
|
||||||
|
if (!slice) return;
|
||||||
|
|
||||||
|
for (uint16_t y = 0; y < H; y++) {
|
||||||
|
memcpy(slice + (size_t)y * HALF_BYTES_ROW,
|
||||||
|
fb + (size_t)y * BYTES_PER_ROW,
|
||||||
|
HALF_BYTES_ROW);
|
||||||
|
}
|
||||||
|
push_half(PIN_CS_M, slice);
|
||||||
|
|
||||||
|
for (uint16_t y = 0; y < H; y++) {
|
||||||
|
memcpy(slice + (size_t)y * HALF_BYTES_ROW,
|
||||||
|
fb + (size_t)y * BYTES_PER_ROW + HALF_BYTES_ROW,
|
||||||
|
HALF_BYTES_ROW);
|
||||||
|
}
|
||||||
|
push_half(PIN_CS_S, slice);
|
||||||
|
|
||||||
|
heap_caps_free(slice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── epd.h surface ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void epd_fill(uint8_t color) {
|
||||||
|
const uint8_t byte = (color << 4) | color;
|
||||||
|
|
||||||
|
// Solid fill needs no framebuffer — stream the byte directly per half.
|
||||||
|
for (int half = 0; half < 2; half++) {
|
||||||
|
const int p = (half == 0) ? PIN_CS_M : PIN_CS_S;
|
||||||
|
begin_cmd(p, 0x10);
|
||||||
|
for (size_t i = 0; i < (size_t)HALF_BYTES_ROW * H; i++) {
|
||||||
|
SPI.transfer(byte);
|
||||||
|
}
|
||||||
|
cs(p, HIGH);
|
||||||
|
}
|
||||||
|
epd_refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void epd_draw_image_from_file(fs::File& f) {
|
||||||
|
uint8_t* fb = fb_alloc();
|
||||||
|
if (!fb) { epd_fill(COLOR_WHITE); return; }
|
||||||
|
|
||||||
|
size_t n = f.read(fb, FB_BYTES);
|
||||||
|
if (n != FB_BYTES) {
|
||||||
|
fb_release(fb);
|
||||||
|
epd_fill(COLOR_WHITE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
push_full_frame(fb);
|
||||||
|
fb_release(fb);
|
||||||
|
epd_refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void epd_draw_image_with_border(fs::File& f, uint8_t color, int thickness) {
|
||||||
|
if (f.size() != FB_BYTES) {
|
||||||
|
epd_fill(color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint8_t* fb = fb_alloc();
|
||||||
|
if (!fb) { epd_fill(color); return; }
|
||||||
|
if (f.read(fb, FB_BYTES) != FB_BYTES) { fb_release(fb); epd_fill(color); return; }
|
||||||
|
|
||||||
|
const uint8_t pair = (color << 4) | color;
|
||||||
|
|
||||||
|
// Overlay border in-place. Same x/y orientation as the 7.3" driver:
|
||||||
|
// top/bottom solid stripes, plus left/right edges on the middle band.
|
||||||
|
for (int y = 0; y < H; y++) {
|
||||||
|
uint8_t* row = fb + (size_t)y * BYTES_PER_ROW;
|
||||||
|
if (y < thickness || y >= H - thickness) {
|
||||||
|
memset(row, pair, BYTES_PER_ROW);
|
||||||
|
} else {
|
||||||
|
for (int x = 0; x < thickness; x++) {
|
||||||
|
if (x & 1) row[x/2] = (row[x/2] & 0xF0) | color;
|
||||||
|
else row[x/2] = (row[x/2] & 0x0F) | (color << 4);
|
||||||
|
}
|
||||||
|
for (int x = W - thickness; x < W; x++) {
|
||||||
|
if (x & 1) row[x/2] = (row[x/2] & 0xF0) | color;
|
||||||
|
else row[x/2] = (row[x/2] & 0x0F) | (color << 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
push_full_frame(fb);
|
||||||
|
fb_release(fb);
|
||||||
|
epd_refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void epd_draw_qr(QRCode* qr, uint8_t cellPx, uint8_t bg, uint8_t fg) {
|
||||||
|
uint8_t* fb = fb_alloc();
|
||||||
|
if (!fb) { epd_fill(bg); return; }
|
||||||
|
|
||||||
|
const uint8_t bg_pair = (bg << 4) | bg;
|
||||||
|
memset(fb, bg_pair, FB_BYTES);
|
||||||
|
|
||||||
|
const int qrPx = qr->size * cellPx;
|
||||||
|
const int offX = (W - qrPx) / 2;
|
||||||
|
const int offY = (H - qrPx) / 2;
|
||||||
|
|
||||||
|
for (int y = offY; y < offY + qrPx; y++) {
|
||||||
|
if (y < 0 || y >= H) continue;
|
||||||
|
uint8_t* row = fb + (size_t)y * BYTES_PER_ROW;
|
||||||
|
const int qy = (y - offY) / cellPx;
|
||||||
|
for (int x = offX; x < offX + qrPx; x++) {
|
||||||
|
if (x < 0 || x >= W) continue;
|
||||||
|
const int qx = (x - offX) / cellPx;
|
||||||
|
const uint8_t c = qrcode_getModule(qr, qx, qy) ? fg : bg;
|
||||||
|
if (x & 1) row[x/2] = (row[x/2] & 0xF0) | c;
|
||||||
|
else row[x/2] = (row[x/2] & 0x0F) | (c << 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
push_full_frame(fb);
|
||||||
|
fb_release(fb);
|
||||||
|
epd_refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream background from LittleFS into the PSRAM framebuffer, overlay the QR
|
||||||
|
// at (qr_x, qr_y) with the given cell size, then push the full frame in two
|
||||||
|
// halves. Falls back to a solid fill if the file is missing or wrong size.
|
||||||
|
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 || f.size() != FB_BYTES) {
|
||||||
|
if (f) f.close();
|
||||||
|
epd_fill(fallback_color);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* fb = fb_alloc();
|
||||||
|
if (!fb) { f.close(); epd_fill(fallback_color); return; }
|
||||||
|
|
||||||
|
if (f.read(fb, FB_BYTES) != FB_BYTES) {
|
||||||
|
fb_release(fb); f.close(); epd_fill(fallback_color); return;
|
||||||
|
}
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
const int qr_px = qr->size * qr_cell;
|
||||||
|
const int y0 = max(qr_y, 0);
|
||||||
|
const int y1 = min(qr_y + qr_px, (int)H);
|
||||||
|
const int x0 = max(qr_x, 0);
|
||||||
|
const int x1 = min(qr_x + qr_px, (int)W);
|
||||||
|
for (int y = y0; y < y1; y++) {
|
||||||
|
uint8_t* row = fb + (size_t)y * BYTES_PER_ROW;
|
||||||
|
const int qy = (y - qr_y) / qr_cell;
|
||||||
|
for (int x = x0; x < x1; x++) {
|
||||||
|
const int qx = (x - qr_x) / qr_cell;
|
||||||
|
const uint8_t c = qrcode_getModule(qr, qx, qy) ? COLOR_BLACK : COLOR_WHITE;
|
||||||
|
if (x & 1) row[x/2] = (row[x/2] & 0xF0) | c;
|
||||||
|
else row[x/2] = (row[x/2] & 0x0F) | (c << 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
push_full_frame(fb);
|
||||||
|
fb_release(fb);
|
||||||
|
epd_refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// QR overlay coordinates for the 13.3" portrait setup screens. Must stay
|
||||||
|
// in sync with scripts/gen_screens_13e6.py — the bg .bin leaves a white
|
||||||
|
// rectangle exactly the size of QR_MODS × QR_CELL at (X, Y), and the
|
||||||
|
// firmware paints the live QR into it. Mismatch = the QR draws over
|
||||||
|
// decorative borders or the QR placeholder shows through.
|
||||||
|
void epd_draw_ap_screen(QRCode* qr) {
|
||||||
|
draw_from_lfs("/ap_bg.bin", COLOR_YELLOW, qr, 304, 220, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
void epd_draw_ap_screen_retry(QRCode* qr) {
|
||||||
|
draw_from_lfs("/ap_bg_retry.bin", COLOR_RED, qr, 304, 220, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
void epd_draw_setup_screen(QRCode* qr) {
|
||||||
|
draw_from_lfs("/setup_bg.bin", COLOR_GREEN, qr, 313, 450, 14);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// Panel-specific firmware version for the Waveshare 13.3" Spectra-6 driver.
|
||||||
|
// Bump on each driver change worth correlating with server-side reports.
|
||||||
|
// Independent of the shared firmware version (HTTP / NVS / sleep / etc.).
|
||||||
|
#define PANEL_FW_VERSION "v1.0"
|
||||||
@@ -6,6 +6,15 @@
|
|||||||
|
|
||||||
static uint8_t s_row[EPD_WIDTH / 2];
|
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() {
|
static void wait_busy() {
|
||||||
uint32_t start = millis();
|
uint32_t start = millis();
|
||||||
while (digitalRead(PIN_BUSY) == LOW) {
|
while (digitalRead(PIN_BUSY) == LOW) {
|
||||||
|
|||||||
+1
-7
@@ -28,14 +28,8 @@ void setup() {
|
|||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
Serial.println("[sim_border] boot");
|
Serial.println("[sim_border] boot");
|
||||||
|
|
||||||
pinMode(PIN_CS, OUTPUT);
|
|
||||||
pinMode(PIN_DC, OUTPUT);
|
|
||||||
pinMode(PIN_RST, OUTPUT);
|
|
||||||
pinMode(PIN_BUSY, INPUT);
|
|
||||||
pinMode(PIN_BTN_RESET, INPUT_PULLUP);
|
pinMode(PIN_BTN_RESET, INPUT_PULLUP);
|
||||||
|
epd_setup_pins();
|
||||||
SPI.begin(PIN_SCK, -1, PIN_MOSI, PIN_CS);
|
|
||||||
SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));
|
|
||||||
LittleFS.begin(true);
|
LittleFS.begin(true);
|
||||||
|
|
||||||
epd_init();
|
epd_init();
|
||||||
|
|||||||
@@ -10,12 +10,7 @@ void setup() {
|
|||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
Serial.println("display test boot");
|
Serial.println("display test boot");
|
||||||
|
|
||||||
pinMode(PIN_CS, OUTPUT);
|
epd_setup_pins();
|
||||||
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));
|
|
||||||
|
|
||||||
LittleFS.begin(true);
|
LittleFS.begin(true);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
// First-pixels bringup test for the Waveshare ESP32-S3-ePaper-13.3E6.
|
||||||
|
//
|
||||||
|
// Renders the panel's six color bars (BLACK/BLUE/GREEN/RED/YELLOW/WHITE)
|
||||||
|
// — same pattern as Waveshare's stock demo, ported to Arduino-on-S3.
|
||||||
|
// Self-contained on purpose: no LittleFS, no shared epd.h, no PSRAM
|
||||||
|
// framebuffer, no panels/ driver. Once pixels render, refactor into the
|
||||||
|
// existing panels/{vendor}/v{N}/ structure.
|
||||||
|
//
|
||||||
|
// Pin map verified against the ESP32-S3-ePaper-13.3E6 (all-in-one) board,
|
||||||
|
// not the separate ESP32 driver board (which uses different GPIOs).
|
||||||
|
//
|
||||||
|
// Source of init sequence + display protocol:
|
||||||
|
// github.com/teatall/13.3inch_e-Paper_E-Frame (community project for
|
||||||
|
// the same Waveshare board) and Waveshare's official 13in3e demo.
|
||||||
|
|
||||||
|
#ifdef ENV_TEST_DISPLAY_13E6
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
static constexpr int PIN_SCK = 9;
|
||||||
|
static constexpr int PIN_MOSI = 46;
|
||||||
|
static constexpr int PIN_CS_M = 10;
|
||||||
|
static constexpr int PIN_CS_S = 3;
|
||||||
|
static constexpr int PIN_RST = 2;
|
||||||
|
static constexpr int PIN_DC = 11;
|
||||||
|
static constexpr int PIN_BUSY = 12;
|
||||||
|
static constexpr int PIN_PWR = 1;
|
||||||
|
|
||||||
|
static constexpr uint16_t EPD_W = 1200;
|
||||||
|
static constexpr uint16_t EPD_H = 1600;
|
||||||
|
|
||||||
|
static constexpr uint8_t C_BLACK = 0x0;
|
||||||
|
static constexpr uint8_t C_WHITE = 0x1;
|
||||||
|
static constexpr uint8_t C_YELLOW = 0x2;
|
||||||
|
static constexpr uint8_t C_RED = 0x3;
|
||||||
|
static constexpr uint8_t C_BLUE = 0x5;
|
||||||
|
static constexpr uint8_t C_GREEN = 0x6;
|
||||||
|
|
||||||
|
static inline void cs_both(int v) {
|
||||||
|
digitalWrite(PIN_CS_M, v);
|
||||||
|
digitalWrite(PIN_CS_S, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bit-banged MSB-first SPI matching Waveshare's reference timing exactly.
|
||||||
|
// DC and CS are handled by the caller. Cmd byte goes out while DC is low;
|
||||||
|
// data while DC is high.
|
||||||
|
static void spi_write_byte(uint8_t b) {
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
digitalWrite(PIN_MOSI, (b & 0x80) ? HIGH : LOW);
|
||||||
|
b <<= 1;
|
||||||
|
digitalWrite(PIN_SCK, HIGH);
|
||||||
|
digitalWrite(PIN_SCK, LOW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_cmd(uint8_t cmd) {
|
||||||
|
digitalWrite(PIN_DC, LOW);
|
||||||
|
spi_write_byte(cmd);
|
||||||
|
digitalWrite(PIN_DC, HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_data(uint8_t d) {
|
||||||
|
spi_write_byte(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_cmd_with_data(uint8_t cmd, const uint8_t* buf, size_t len) {
|
||||||
|
send_cmd(cmd);
|
||||||
|
for (size_t i = 0; i < len; i++) spi_write_byte(buf[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// BUSY is active-LOW: panel pulls it low while working, releases high when idle.
|
||||||
|
static void wait_busy_idle() {
|
||||||
|
Serial.println("busy: wait");
|
||||||
|
while (digitalRead(PIN_BUSY) == LOW) {
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
delay(20);
|
||||||
|
Serial.println("busy: idle");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void panel_reset() {
|
||||||
|
digitalWrite(PIN_RST, HIGH); delay(30);
|
||||||
|
digitalWrite(PIN_RST, LOW); delay(30);
|
||||||
|
digitalWrite(PIN_RST, HIGH); delay(30);
|
||||||
|
digitalWrite(PIN_RST, LOW); delay(30);
|
||||||
|
digitalWrite(PIN_RST, HIGH); delay(30);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void panel_init() {
|
||||||
|
panel_reset();
|
||||||
|
|
||||||
|
static const uint8_t AN_TM_V[] = {0xC0,0x1C,0x1C,0xCC,0xCC,0xCC,0x15,0x15,0x55};
|
||||||
|
static const uint8_t CMD66_V[] = {0x49,0x55,0x13,0x5D,0x05,0x10};
|
||||||
|
static const uint8_t PSR_V[] = {0xDF,0x69};
|
||||||
|
static const uint8_t CDI_V[] = {0xF7};
|
||||||
|
static const uint8_t TCON_V[] = {0x03,0x03};
|
||||||
|
static const uint8_t AGID_V[] = {0x10};
|
||||||
|
static const uint8_t PWS_V[] = {0x22};
|
||||||
|
static const uint8_t CCSET_V[] = {0x01};
|
||||||
|
static const uint8_t TRES_V[] = {0x04,0xB0,0x03,0x20};
|
||||||
|
static const uint8_t PWR_V[] = {0x0F,0x00,0x28,0x2C,0x28,0x38};
|
||||||
|
static const uint8_t EN_BUF_V[] = {0x07};
|
||||||
|
static const uint8_t BTST_P_V[] = {0xE8,0x28};
|
||||||
|
static const uint8_t BOOST_VDDP_EN_V[] = {0x01};
|
||||||
|
static const uint8_t BTST_N_V[] = {0xE8,0x28};
|
||||||
|
static const uint8_t BUCK_VDDN_V[] = {0x01};
|
||||||
|
static const uint8_t TFT_VCOM_V[] = {0x02};
|
||||||
|
|
||||||
|
// The first few commands go to BOTH halves of the panel (CS_M then CS_S
|
||||||
|
// released together). After that, several "tuning" commands are master-
|
||||||
|
// only — exactly as in the reference, even though it's not obvious why.
|
||||||
|
digitalWrite(PIN_CS_M, LOW);
|
||||||
|
send_cmd_with_data(0x74, AN_TM_V, sizeof(AN_TM_V));
|
||||||
|
cs_both(HIGH);
|
||||||
|
|
||||||
|
cs_both(LOW); send_cmd_with_data(0xF0, CMD66_V, sizeof(CMD66_V)); cs_both(HIGH);
|
||||||
|
cs_both(LOW); send_cmd_with_data(0x00, PSR_V, sizeof(PSR_V)); cs_both(HIGH);
|
||||||
|
cs_both(LOW); send_cmd_with_data(0x50, CDI_V, sizeof(CDI_V)); cs_both(HIGH);
|
||||||
|
cs_both(LOW); send_cmd_with_data(0x60, TCON_V, sizeof(TCON_V)); cs_both(HIGH);
|
||||||
|
cs_both(LOW); send_cmd_with_data(0x86, AGID_V, sizeof(AGID_V)); cs_both(HIGH);
|
||||||
|
cs_both(LOW); send_cmd_with_data(0xE3, PWS_V, sizeof(PWS_V)); cs_both(HIGH);
|
||||||
|
cs_both(LOW); send_cmd_with_data(0xE0, CCSET_V, sizeof(CCSET_V)); cs_both(HIGH);
|
||||||
|
cs_both(LOW); send_cmd_with_data(0x61, TRES_V, sizeof(TRES_V)); cs_both(HIGH);
|
||||||
|
|
||||||
|
digitalWrite(PIN_CS_M, LOW); send_cmd_with_data(0x01, PWR_V, sizeof(PWR_V)); cs_both(HIGH);
|
||||||
|
digitalWrite(PIN_CS_M, LOW); send_cmd_with_data(0xB6, EN_BUF_V, sizeof(EN_BUF_V)); cs_both(HIGH);
|
||||||
|
digitalWrite(PIN_CS_M, LOW); send_cmd_with_data(0x06, BTST_P_V, sizeof(BTST_P_V)); cs_both(HIGH);
|
||||||
|
digitalWrite(PIN_CS_M, LOW); send_cmd_with_data(0xB7, BOOST_VDDP_EN_V, sizeof(BOOST_VDDP_EN_V)); cs_both(HIGH);
|
||||||
|
digitalWrite(PIN_CS_M, LOW); send_cmd_with_data(0x05, BTST_N_V, sizeof(BTST_N_V)); cs_both(HIGH);
|
||||||
|
digitalWrite(PIN_CS_M, LOW); send_cmd_with_data(0xB0, BUCK_VDDN_V, sizeof(BUCK_VDDN_V)); cs_both(HIGH);
|
||||||
|
digitalWrite(PIN_CS_M, LOW); send_cmd_with_data(0xB1, TFT_VCOM_V, sizeof(TFT_VCOM_V)); cs_both(HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void panel_turn_on() {
|
||||||
|
cs_both(LOW); send_cmd(0x04); cs_both(HIGH); // POWER_ON
|
||||||
|
wait_busy_idle();
|
||||||
|
|
||||||
|
delay(50);
|
||||||
|
cs_both(LOW); send_cmd(0x12); send_data(0x00); cs_both(HIGH); // DRF (refresh)
|
||||||
|
wait_busy_idle();
|
||||||
|
|
||||||
|
cs_both(LOW); send_cmd(0x02); send_data(0x00); cs_both(HIGH); // POWER_OFF
|
||||||
|
}
|
||||||
|
|
||||||
|
static void panel_sleep() {
|
||||||
|
cs_both(LOW);
|
||||||
|
send_cmd(0x07);
|
||||||
|
send_data(0xA5);
|
||||||
|
cs_both(HIGH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Six horizontal bars: BLACK, BLUE, GREEN, RED, YELLOW, WHITE — top to bottom.
|
||||||
|
// 4 bits/pixel packed 2-per-byte; each half-panel is 600 wide → 300 bytes/row.
|
||||||
|
static void draw_color_bars() {
|
||||||
|
static const uint8_t bars[6] = { C_BLACK, C_BLUE, C_GREEN, C_RED, C_YELLOW, C_WHITE };
|
||||||
|
constexpr uint16_t bytes_per_row = EPD_W / 4; // 300 bytes per half
|
||||||
|
constexpr uint16_t rows_per_bar = EPD_H / 6; // ~266 rows per bar
|
||||||
|
|
||||||
|
for (int half = 0; half < 2; half++) {
|
||||||
|
digitalWrite(half == 0 ? PIN_CS_M : PIN_CS_S, LOW);
|
||||||
|
send_cmd(0x10);
|
||||||
|
for (int k = 0; k < 6; k++) {
|
||||||
|
uint8_t byte = (bars[k] << 4) | bars[k];
|
||||||
|
for (uint16_t row = 0; row < rows_per_bar; row++) {
|
||||||
|
for (uint16_t col = 0; col < bytes_per_row; col++) {
|
||||||
|
send_data(byte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
cs_both(HIGH);
|
||||||
|
Serial.printf("half %d: pushed\n", half);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(2000);
|
||||||
|
Serial.println("\n13.3E6 first-pixels test");
|
||||||
|
Serial.printf("Free heap: %u, PSRAM: %u\n",
|
||||||
|
ESP.getFreeHeap(),
|
||||||
|
(unsigned)ESP.getPsramSize());
|
||||||
|
|
||||||
|
pinMode(PIN_PWR, OUTPUT);
|
||||||
|
pinMode(PIN_RST, OUTPUT);
|
||||||
|
pinMode(PIN_DC, OUTPUT);
|
||||||
|
pinMode(PIN_BUSY, INPUT);
|
||||||
|
pinMode(PIN_SCK, OUTPUT);
|
||||||
|
pinMode(PIN_MOSI, OUTPUT);
|
||||||
|
pinMode(PIN_CS_M, OUTPUT);
|
||||||
|
pinMode(PIN_CS_S, OUTPUT);
|
||||||
|
|
||||||
|
digitalWrite(PIN_PWR, HIGH);
|
||||||
|
digitalWrite(PIN_CS_M, HIGH);
|
||||||
|
digitalWrite(PIN_CS_S, HIGH);
|
||||||
|
digitalWrite(PIN_SCK, LOW);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
Serial.println("init");
|
||||||
|
panel_init();
|
||||||
|
Serial.println("draw color bars");
|
||||||
|
draw_color_bars();
|
||||||
|
Serial.println("refresh");
|
||||||
|
panel_turn_on();
|
||||||
|
Serial.println("sleep");
|
||||||
|
panel_sleep();
|
||||||
|
|
||||||
|
Serial.println("done");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
delay(60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -16,6 +16,7 @@ extern int g_epd_draw_border_count;
|
|||||||
extern int g_epd_draw_border_last_color;
|
extern int g_epd_draw_border_last_color;
|
||||||
extern int g_epd_draw_border_last_thickness;
|
extern int g_epd_draw_border_last_thickness;
|
||||||
|
|
||||||
|
inline void epd_setup_pins() {}
|
||||||
inline void epd_init() { g_epd_init_count++; }
|
inline void epd_init() { g_epd_init_count++; }
|
||||||
inline void epd_sleep() { g_epd_sleep_count++; }
|
inline void epd_sleep() { g_epd_sleep_count++; }
|
||||||
inline void epd_draw_image_from_file(File& f) {
|
inline void epd_draw_image_from_file(File& f) {
|
||||||
|
|||||||
Reference in New Issue
Block a user