Compare commits
7 Commits
13e6-v1.0.0
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 22c9edb09e | |||
| a31a39fdc4 | |||
| 55ee5aa95c | |||
| fc1367fc55 | |||
| 28b6a353aa | |||
| d900083398 | |||
| e2c9d8f1e4 |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 248 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 248 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 238 KiB |
+44
-24
@@ -281,6 +281,15 @@ def left_arrow(draw, cx, cy, half_h=18, w=34, color=BK):
|
|||||||
], fill=color)
|
], fill=color)
|
||||||
|
|
||||||
|
|
||||||
|
def right_arrow(draw, cx, cy, half_h=18, w=34, color=BK):
|
||||||
|
"""Solid filled triangle pointing right, centered on (cx, cy)."""
|
||||||
|
draw.polygon([
|
||||||
|
(cx + w // 2, cy),
|
||||||
|
(cx - w // 2, cy - half_h),
|
||||||
|
(cx - w // 2, cy + half_h),
|
||||||
|
], fill=color)
|
||||||
|
|
||||||
|
|
||||||
def paste_rotated_text(img, text, font, fill, anchor_xy, ccw_degrees=90):
|
def paste_rotated_text(img, text, font, fill, anchor_xy, ccw_degrees=90):
|
||||||
"""
|
"""
|
||||||
Render `text` horizontally onto a transparent overlay, rotate ccw, and
|
Render `text` horizontally onto a transparent overlay, rotate ccw, and
|
||||||
@@ -304,17 +313,17 @@ def orientation_diagrams(img, cx, top_y, label_color=None, compact=False):
|
|||||||
|
|
||||||
PORTRAIT = upright tall rect, ribbon along the bottom short edge,
|
PORTRAIT = upright tall rect, ribbon along the bottom short edge,
|
||||||
up-arrow inside.
|
up-arrow inside.
|
||||||
LANDSCAPE = pre-rotated 90° CCW from upright landscape. The frame
|
LANDSCAPE = pre-rotated 90° CW from upright landscape. The frame
|
||||||
rotation portrait→landscape is 90° CW (ribbon moves
|
rotation portrait→landscape is 90° CCW (ribbon moves
|
||||||
bottom→left as viewed by the user); the CCW pre-rotation
|
top→left as viewed by the user); the CW pre-rotation
|
||||||
cancels that, so when the user picks the frame up and
|
cancels that, so when the user picks the frame up and
|
||||||
rotates it 90° CW into landscape the diagram lands
|
rotates it 90° CCW into landscape the diagram lands
|
||||||
upright (wide rect, ribbon-left, up-arrow).
|
upright (wide rect, ribbon-left, up-arrow).
|
||||||
In the portrait rendering that means: tall rect, ribbon
|
In the portrait rendering that means: tall rect, ribbon
|
||||||
along bottom edge (was the LEFT edge upright), LEFT-
|
along TOP edge (was the LEFT edge upright), RIGHT-
|
||||||
pointing arrow (was UP upright), and the "LANDSCAPE"
|
pointing arrow (was UP upright), and the "LANDSCAPE"
|
||||||
label rotated 90° CCW so it runs up the long edge —
|
label rotated 90° CW so it runs DOWN the right long edge
|
||||||
reads horizontally once the frame is mounted landscape.
|
— reads horizontally once the frame is mounted landscape.
|
||||||
"""
|
"""
|
||||||
if label_color is None:
|
if label_color is None:
|
||||||
label_color = BK
|
label_color = BK
|
||||||
@@ -355,28 +364,29 @@ def orientation_diagrams(img, cx, top_y, label_color=None, compact=False):
|
|||||||
pt_y + (diag_h - ribbon_thick) // 2)
|
pt_y + (diag_h - ribbon_thick) // 2)
|
||||||
text_center(draw, pt_x + diag_w // 2, diag_bottom + 14, "PORTRAIT", F_TINY, BK)
|
text_center(draw, pt_x + diag_w // 2, diag_bottom + 14, "PORTRAIT", F_TINY, BK)
|
||||||
|
|
||||||
# LANDSCAPE — pre-rotated 90° CCW from upright.
|
# LANDSCAPE — pre-rotated 90° CW from upright (the user rotates 90° CCW
|
||||||
|
# into landscape; the CW pre-rotation cancels that so it lands upright).
|
||||||
# Upright landscape: wide rect, ribbon along LEFT long edge, UP arrow.
|
# Upright landscape: wide rect, ribbon along LEFT long edge, UP arrow.
|
||||||
# After 90° CCW (in portrait rendering): tall rect, ribbon along BOTTOM
|
# After 90° CW (in portrait rendering): tall rect, ribbon along TOP
|
||||||
# short edge, LEFT-pointing arrow. Label runs up the LEFT long edge,
|
# short edge, RIGHT-pointing arrow. Label runs DOWN the RIGHT long edge,
|
||||||
# rotated 90° CCW so it reads L→R once the frame is rotated to landscape.
|
# rotated 90° CW so it reads L→R once the frame is rotated to landscape.
|
||||||
draw.rectangle([ls_x, ls_y, ls_x + diag_w - 1, ls_y + diag_h - 1],
|
draw.rectangle([ls_x, ls_y, ls_x + diag_w - 1, ls_y + diag_h - 1],
|
||||||
outline=BK, width=3)
|
outline=BK, width=3)
|
||||||
draw.rectangle([ls_x, ls_y + diag_h - ribbon_thick,
|
draw.rectangle([ls_x, ls_y,
|
||||||
ls_x + diag_w - 1, ls_y + diag_h - 1], fill=BK)
|
ls_x + diag_w - 1, ls_y + ribbon_thick - 1], fill=BK)
|
||||||
left_arrow(draw, ls_x + diag_w // 2,
|
right_arrow(draw, ls_x + diag_w // 2,
|
||||||
ls_y + (diag_h - ribbon_thick) // 2)
|
ls_y + ribbon_thick + (diag_h - ribbon_thick) // 2)
|
||||||
# Rotated label, anchored just left of the diagram's left long edge.
|
# Rotated label, anchored just right of the diagram's right long edge.
|
||||||
label_text = "LANDSCAPE"
|
label_text = "LANDSCAPE"
|
||||||
bb = F_TINY.getbbox(label_text)
|
bb = F_TINY.getbbox(label_text)
|
||||||
label_w = bb[2] - bb[0]
|
label_w = bb[2] - bb[0]
|
||||||
label_h = bb[3] - bb[1]
|
label_h = bb[3] - bb[1]
|
||||||
# Rotated label is `label_w` tall, `label_h` wide. Centred vertically
|
# Rotated 90° CW: label is `label_w` tall, `label_h` wide. Centred
|
||||||
# against the rect, sitting just to its left.
|
# vertically against the rect, sitting just to its right.
|
||||||
rotated_x = ls_x - label_h - 16
|
rotated_x = ls_x + diag_w + 16
|
||||||
rotated_y = ls_y + (diag_h - label_w) // 2
|
rotated_y = ls_y + (diag_h - label_w) // 2
|
||||||
paste_rotated_text(img, label_text, F_TINY, BK, (rotated_x, rotated_y),
|
paste_rotated_text(img, label_text, F_TINY, BK, (rotated_x, rotated_y),
|
||||||
ccw_degrees=90)
|
ccw_degrees=-90)
|
||||||
|
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -564,6 +574,12 @@ def gen_setup():
|
|||||||
|
|
||||||
# ── Save ─────────────────────────────────────────────────────────────────────
|
# ── Save ─────────────────────────────────────────────────────────────────────
|
||||||
def save_bin(img, path, preview_path):
|
def save_bin(img, path, preview_path):
|
||||||
|
# Physical mount compensation: 13.3" panel ships ribbon-at-bottom of
|
||||||
|
# portrait, opposite the scan-zero corner. Rotate 180° before packing
|
||||||
|
# so the .bin's scan order maps to a right-side-up image on the panel.
|
||||||
|
# Server-side render does the same — see DeviceModel::physicalRotationDegrees().
|
||||||
|
img = img.rotate(180)
|
||||||
|
|
||||||
data = pack(img)
|
data = pack(img)
|
||||||
with open(path, "wb") as f:
|
with open(path, "wb") as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
@@ -593,7 +609,11 @@ if __name__ == "__main__":
|
|||||||
print("Generating setup screen…")
|
print("Generating setup screen…")
|
||||||
save_bin(gen_setup(), f"{out_dir}/setup_bg.bin", f"{out_dir}/setup_bg_preview.png")
|
save_bin(gen_setup(), f"{out_dir}/setup_bg.bin", f"{out_dir}/setup_bg_preview.png")
|
||||||
print()
|
print()
|
||||||
print("QR overlay constants — keep these in sync with epd_driver.cpp:")
|
# Post-180°-rotation coords for firmware (.bin is rotated in save_bin).
|
||||||
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}")
|
rot_ap_x = W - AP_QR_X - AP_QR_PX
|
||||||
print(f" SETUP_QR_X={SETUP_QR_X}, SETUP_QR_Y={SETUP_QR_Y}, "
|
rot_ap_y = H - AP_QR_Y - AP_QR_PX
|
||||||
f"SETUP_QR_CELL={SETUP_QR_CELL}, SETUP_QR_PX={SETUP_QR_PX}")
|
rot_setup_x = W - SETUP_QR_X - SETUP_QR_PX
|
||||||
|
rot_setup_y = H - SETUP_QR_Y - SETUP_QR_PX
|
||||||
|
print("QR overlay constants (POST-180°-rotation) — keep these in sync with epd_driver.cpp:")
|
||||||
|
print(f" AP qr_x={rot_ap_x}, qr_y={rot_ap_y}, cell={AP_QR_CELL}, px={AP_QR_PX}")
|
||||||
|
print(f" Setup qr_x={rot_setup_x}, qr_y={rot_setup_y}, cell={SETUP_QR_CELL}, px={SETUP_QR_PX}")
|
||||||
|
|||||||
@@ -9,9 +9,11 @@
|
|||||||
// The test build adds test/mocks to the include path via -iquote.
|
// The test build adds test/mocks to the include path via -iquote.
|
||||||
#include "epd_mock.h"
|
#include "epd_mock.h"
|
||||||
#include "esp_sleep.h"
|
#include "esp_sleep.h"
|
||||||
|
#include "driver/gpio.h"
|
||||||
#else
|
#else
|
||||||
#include "epd.h"
|
#include "epd.h"
|
||||||
#include <esp_sleep.h>
|
#include <esp_sleep.h>
|
||||||
|
#include <driver/gpio.h>
|
||||||
#include <mbedtls/sha256.h>
|
#include <mbedtls/sha256.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -354,6 +356,12 @@ void normal_operation_impl(const String& mac, HTTP& http, const String& url, Pre
|
|||||||
// false → normal_operation_impl runs → the next poll fetches a fresh
|
// false → normal_operation_impl runs → the next poll fetches a fresh
|
||||||
// image, which doubles as a "force refresh" gesture.
|
// image, which doubles as a "force refresh" gesture.
|
||||||
esp_sleep_enable_ext0_wakeup((gpio_num_t)PIN_BTN_RESET, 0);
|
esp_sleep_enable_ext0_wakeup((gpio_num_t)PIN_BTN_RESET, 0);
|
||||||
|
// Latch any gpio_hold_en pins through the deep sleep period.
|
||||||
|
// epd_sleep() cuts PIN_PWR LOW + holds it; without this call the
|
||||||
|
// hold releases on the RTC transition and the panel rail comes back
|
||||||
|
// up, losing the saving. Released per-pin in epd_setup_pins() on
|
||||||
|
// wake via gpio_hold_dis().
|
||||||
|
gpio_deep_sleep_hold_en();
|
||||||
esp_deep_sleep_start();
|
esp_deep_sleep_start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include <qrcode.h>
|
#include <qrcode.h>
|
||||||
#include <esp_task_wdt.h>
|
#include <esp_task_wdt.h>
|
||||||
#include <esp_heap_caps.h>
|
#include <esp_heap_caps.h>
|
||||||
|
#include <driver/gpio.h>
|
||||||
|
|
||||||
static constexpr uint16_t W = 1200;
|
static constexpr uint16_t W = 1200;
|
||||||
static constexpr uint16_t H = 1600;
|
static constexpr uint16_t H = 1600;
|
||||||
@@ -103,6 +104,12 @@ static void panel_reset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void epd_setup_pins() {
|
void epd_setup_pins() {
|
||||||
|
// Release the PIN_PWR hold latched from the previous deep sleep so we
|
||||||
|
// can drive it again. No-op on cold boot (nothing was held). Paired
|
||||||
|
// with gpio_hold_en() in epd_sleep() and gpio_deep_sleep_hold_en() in
|
||||||
|
// operation.h.
|
||||||
|
gpio_hold_dis((gpio_num_t)PIN_PWR);
|
||||||
|
|
||||||
pinMode(PIN_PWR, OUTPUT);
|
pinMode(PIN_PWR, OUTPUT);
|
||||||
pinMode(PIN_RST, OUTPUT);
|
pinMode(PIN_RST, OUTPUT);
|
||||||
pinMode(PIN_DC, OUTPUT);
|
pinMode(PIN_DC, OUTPUT);
|
||||||
@@ -191,6 +198,16 @@ void epd_sleep() {
|
|||||||
SPI.transfer(0xA5); // sentinel
|
SPI.transfer(0xA5); // sentinel
|
||||||
cs_both(HIGH);
|
cs_both(HIGH);
|
||||||
s_initialized = false;
|
s_initialized = false;
|
||||||
|
|
||||||
|
// Cut the panel power rail. The Waveshare board exposes PIN_PWR
|
||||||
|
// specifically for battery operation — the e-ink image persists
|
||||||
|
// without VDD (particles are bistable), and dropping the rail kills
|
||||||
|
// the boost converter's quiescent draw (~50–500 µA depending on the
|
||||||
|
// load on the rail). Latch LOW so it survives deep sleep; paired with
|
||||||
|
// gpio_deep_sleep_hold_en() in operation.h just before the chip
|
||||||
|
// enters sleep.
|
||||||
|
digitalWrite(PIN_PWR, LOW);
|
||||||
|
gpio_hold_en((gpio_num_t)PIN_PWR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Draw helpers ───────────────────────────────────────────────────────────────
|
// ── Draw helpers ───────────────────────────────────────────────────────────────
|
||||||
@@ -427,14 +444,29 @@ static void draw_from_lfs(const char* path, uint8_t fallback_color,
|
|||||||
// rectangle exactly the size of QR_MODS × QR_CELL at (X, Y), and the
|
// 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
|
// firmware paints the live QR into it. Mismatch = the QR draws over
|
||||||
// decorative borders or the QR placeholder shows through.
|
// decorative borders or the QR placeholder shows through.
|
||||||
|
//
|
||||||
|
// Coords are post-180°-rotation: the gen script rotates each .bin to
|
||||||
|
// compensate for the panel's ribbon-at-bottom physical mounting, so
|
||||||
|
// QR placeholders move to (W - old_x - QR_PX, H - old_y - QR_PX).
|
||||||
|
// AP (518 px QR): pre-rot 642,590 → post-rot 40,492
|
||||||
|
// Setup (574 px QR): pre-rot 313,750 → post-rot 313,276 (X centred)
|
||||||
|
// Setup screens (yellow AP / red retry / green setup) are mostly two-tone
|
||||||
|
// against a small palette. Transitioning from a full-color photo to one of
|
||||||
|
// these in a single DRF cycle leaves visible ghost of the previous image —
|
||||||
|
// Spectra-6's color particles need more discharge time than one refresh
|
||||||
|
// provides. Pre-clear to white first so the panel is fully reset, then draw
|
||||||
|
// the actual screen. Doubles the refresh time on setup events only.
|
||||||
void epd_draw_ap_screen(QRCode* qr) {
|
void epd_draw_ap_screen(QRCode* qr) {
|
||||||
draw_from_lfs("/ap_bg.bin", COLOR_YELLOW, qr, 642, 590, 14);
|
epd_fill(COLOR_WHITE);
|
||||||
|
draw_from_lfs("/ap_bg.bin", COLOR_YELLOW, qr, 40, 492, 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
void epd_draw_ap_screen_retry(QRCode* qr) {
|
void epd_draw_ap_screen_retry(QRCode* qr) {
|
||||||
draw_from_lfs("/ap_bg_retry.bin", COLOR_RED, qr, 642, 590, 14);
|
epd_fill(COLOR_WHITE);
|
||||||
|
draw_from_lfs("/ap_bg_retry.bin", COLOR_RED, qr, 40, 492, 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
void epd_draw_setup_screen(QRCode* qr) {
|
void epd_draw_setup_screen(QRCode* qr) {
|
||||||
draw_from_lfs("/setup_bg.bin", COLOR_GREEN, qr, 313, 750, 14);
|
epd_fill(COLOR_WHITE);
|
||||||
|
draw_from_lfs("/setup_bg.bin", COLOR_GREEN, qr, 313, 276, 14);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
// Panel-specific firmware version for the Waveshare 13.3" Spectra-6 driver.
|
// Panel-specific firmware version for the Waveshare 13.3" Spectra-6 driver.
|
||||||
// Bump on each driver change worth correlating with server-side reports.
|
// Bump on each driver change worth correlating with server-side reports.
|
||||||
// Independent of the shared firmware version (HTTP / NVS / sleep / etc.).
|
// Independent of the shared firmware version (HTTP / NVS / sleep / etc.).
|
||||||
#define PANEL_FW_VERSION "v1.0"
|
#define PANEL_FW_VERSION "v1.0.0"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
// Native-test stubs for gpio_hold_* — operation.h calls
|
||||||
|
// gpio_deep_sleep_hold_en() before esp_deep_sleep_start() to latch
|
||||||
|
// PIN_PWR LOW through deep sleep. epd_driver.cpp (not built natively)
|
||||||
|
// also uses gpio_hold_en / gpio_hold_dis. Stubs let tests link.
|
||||||
|
#include "esp_sleep.h" // for gpio_num_t
|
||||||
|
|
||||||
|
inline void gpio_hold_en(gpio_num_t) {}
|
||||||
|
inline void gpio_hold_dis(gpio_num_t) {}
|
||||||
|
inline void gpio_deep_sleep_hold_en() {}
|
||||||
|
inline void gpio_deep_sleep_hold_dis() {}
|
||||||
Reference in New Issue
Block a user