chore: restructure firmware into subdirectory, add DDEV config
Move firmware files from repo root src/ into firmware/ to avoid collision with Symfony's src/ PHP class directory. Add DDEV config targeting PHP 8.4 / PostgreSQL 16 / nginx-fpm with Imagick extension via docker-compose override. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
name: pictureframe
|
||||
type: symfony
|
||||
docroot: public
|
||||
php_version: "8.4"
|
||||
webserver_type: nginx-fpm
|
||||
database:
|
||||
type: postgres
|
||||
version: "16"
|
||||
composer_version: "2"
|
||||
hooks:
|
||||
post-start:
|
||||
- exec: composer install --no-interaction 2>/dev/null || true
|
||||
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile_inline: |
|
||||
ARG BASE_IMAGE
|
||||
FROM $BASE_IMAGE
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libmagickwand-dev \
|
||||
&& pecl install imagick \
|
||||
&& docker-php-ext-enable imagick \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -1,119 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Download a photo, letterbox to 800×480, Floyd-Steinberg dither to the
|
||||
Waveshare 7.3" 6-colour palette, and write src/image.h as a PROGMEM array."""
|
||||
|
||||
import io
|
||||
import math
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
W, H = 480, 800 # portrait canvas; firmware rotates 90° CW onto landscape display
|
||||
OUT = Path(__file__).parent / "src" / "image.h"
|
||||
|
||||
URL = (
|
||||
"https://d3eguztg5751m.cloudfront.net/as/assets-mem-com/cmi/3/0/0/7/11557003"
|
||||
"/20231128_115457370_0_orig.jpg/-/kenneth-edholm-fort-wayne-in-obituary.jpg"
|
||||
"?maxheight=650"
|
||||
)
|
||||
|
||||
# Waveshare 6-colour palette: (display code, match RGB)
|
||||
# RGB values tuned for photographic content — skin tones sit between
|
||||
# white and yellow/red, so those two anchors matter most.
|
||||
PALETTE = [
|
||||
(0x0, ( 0, 0, 0)), # Black
|
||||
(0x1, (255, 255, 255)), # White
|
||||
(0x2, (255, 220, 0)), # Yellow
|
||||
(0x3, (220, 40, 40)), # Red
|
||||
(0x5, ( 20, 80, 200)), # Blue
|
||||
(0x6, ( 50, 160, 50)), # Green
|
||||
]
|
||||
CODES = np.array([c for c, _ in PALETTE], dtype=np.uint8)
|
||||
PAL_RGB = np.array([rgb for _, rgb in PALETTE], dtype=np.float32) # (6,3)
|
||||
|
||||
|
||||
def floyd_steinberg(img_rgb: np.ndarray) -> np.ndarray:
|
||||
"""In-place F-S dither; returns (H,W) array of display colour codes."""
|
||||
arr = img_rgb.astype(np.float32)
|
||||
out = np.zeros((H, W), dtype=np.uint8)
|
||||
|
||||
for y in range(H):
|
||||
for x in range(W):
|
||||
px = np.clip(arr[y, x], 0, 255)
|
||||
diffs = PAL_RGB - px
|
||||
idx = int(np.argmin((diffs ** 2).sum(axis=1)))
|
||||
out[y, x] = CODES[idx]
|
||||
|
||||
err = px - PAL_RGB[idx]
|
||||
if x + 1 < W:
|
||||
arr[y, x + 1] += err * (7 / 16)
|
||||
if y + 1 < H:
|
||||
if x > 0:
|
||||
arr[y + 1, x - 1] += err * (3 / 16)
|
||||
arr[y + 1, x] += err * (5 / 16)
|
||||
if x + 1 < W:
|
||||
arr[y + 1, x + 1] += err * (1 / 16)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print(f"Downloading {URL} ...")
|
||||
req = urllib.request.Request(URL, headers={"User-Agent": "pictureFrame/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
img_bytes = r.read()
|
||||
img = Image.open(io.BytesIO(img_bytes)).convert("RGB")
|
||||
print(f"Downloaded: {img.size[0]}×{img.size[1]}")
|
||||
|
||||
# Letterbox: fit inside 800×480, centre on white background
|
||||
scale = min(W / img.size[0], H / img.size[1])
|
||||
new_w = int(img.size[0] * scale)
|
||||
new_h = int(img.size[1] * scale)
|
||||
resized = img.resize((new_w, new_h), Image.LANCZOS)
|
||||
canvas = Image.new("RGB", (W, H), (255, 255, 255))
|
||||
canvas.paste(resized, ((W - new_w) // 2, (H - new_h) // 2))
|
||||
canvas.save("/tmp/picture_frame_preview.png")
|
||||
print("Preview → /tmp/picture_frame_preview.png")
|
||||
|
||||
print(f"Dithering {W}×{H} to 6-colour palette (this takes ~60s) ...")
|
||||
arr = np.array(canvas, dtype=np.float32)
|
||||
codes = floyd_steinberg(arr)
|
||||
|
||||
# Colour distribution
|
||||
names = ["Black", "White", "Yellow", "Red", "Blue", "Green"]
|
||||
for i, (code, name) in enumerate(zip(CODES, names)):
|
||||
cnt = int((codes == code).sum())
|
||||
print(f" {name:7s} ({code:#04x}): {cnt:7d} px ({100*cnt/(W*H):.1f}%)")
|
||||
|
||||
# Pack 4bpp: high nibble = even column, low nibble = odd column
|
||||
high = codes[:, 0::2].astype(np.uint8)
|
||||
low = codes[:, 1::2].astype(np.uint8)
|
||||
packed = ((high << 4) | low)
|
||||
packed_bytes = packed.tobytes()
|
||||
assert len(packed_bytes) == H * (W // 2)
|
||||
|
||||
OUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
print(f"Writing {OUT} ...")
|
||||
ROW = W // 2
|
||||
with OUT.open("w") as f:
|
||||
f.write("#pragma once\n")
|
||||
f.write("#include <pgmspace.h>\n\n")
|
||||
f.write(f"// {W}×{H} photo, 4bpp Waveshare 7.3\" 6-colour\n")
|
||||
f.write("// Packing: byte[x/2] = (code[x]<<4)|code[x+1], even col = high nibble\n")
|
||||
f.write(f"#define IMAGE_ROW {ROW} // bytes per display row\n\n")
|
||||
f.write("const uint8_t IMAGE_DATA[] PROGMEM = {\n")
|
||||
for y in range(H):
|
||||
row = packed_bytes[y * ROW:(y + 1) * ROW]
|
||||
for off in range(0, ROW, 16):
|
||||
chunk = row[off:off + 16]
|
||||
f.write(" " + ",".join(f"0x{b:02X}" for b in chunk) + ",\n")
|
||||
f.write("};\n")
|
||||
|
||||
kb = len(packed_bytes) / 1024
|
||||
print(f"Done — {len(packed_bytes):,} bytes ({kb:.1f} KB) → {OUT}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,7 +0,0 @@
|
||||
[env:esp32dev]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
upload_port = /dev/ttyUSB0
|
||||
monitor_port = /dev/ttyUSB0
|
||||
-12009
File diff suppressed because it is too large
Load Diff
-119
@@ -1,119 +0,0 @@
|
||||
#include <Arduino.h>
|
||||
#include <SPI.h>
|
||||
#include "image.h"
|
||||
|
||||
#define EPD_WIDTH 800
|
||||
#define EPD_HEIGHT 480
|
||||
#define IMG_W 480 // portrait image dimensions
|
||||
#define IMG_H 800
|
||||
#define PIN_SCK 18
|
||||
#define PIN_MOSI 23
|
||||
#define PIN_CS 5
|
||||
#define PIN_DC 17
|
||||
#define PIN_RST 16
|
||||
#define PIN_BUSY 4
|
||||
|
||||
static uint8_t row_buf[EPD_WIDTH / 2];
|
||||
|
||||
void wait_busy() { while (digitalRead(PIN_BUSY) == LOW) delay(5); }
|
||||
|
||||
void send_command(uint8_t cmd) {
|
||||
digitalWrite(PIN_DC, LOW);
|
||||
digitalWrite(PIN_CS, LOW);
|
||||
SPI.transfer(cmd);
|
||||
digitalWrite(PIN_CS, HIGH);
|
||||
}
|
||||
|
||||
void send_data(uint8_t data) {
|
||||
digitalWrite(PIN_DC, HIGH);
|
||||
digitalWrite(PIN_CS, LOW);
|
||||
SPI.transfer(data);
|
||||
digitalWrite(PIN_CS, HIGH);
|
||||
}
|
||||
|
||||
void epd_reset() {
|
||||
digitalWrite(PIN_RST, HIGH); delay(20);
|
||||
digitalWrite(PIN_RST, LOW); delay(2);
|
||||
digitalWrite(PIN_RST, HIGH); delay(20);
|
||||
}
|
||||
|
||||
void epd_init() {
|
||||
epd_reset();
|
||||
wait_busy();
|
||||
delay(30);
|
||||
send_command(0xAA);
|
||||
send_data(0x49); send_data(0x55); send_data(0x20);
|
||||
send_data(0x08); send_data(0x09); send_data(0x18);
|
||||
send_command(0x01); send_data(0x3F);
|
||||
send_command(0x00); send_data(0x5F); send_data(0x69);
|
||||
send_command(0x03);
|
||||
send_data(0x00); send_data(0x54); send_data(0x00); send_data(0x44);
|
||||
send_command(0x05);
|
||||
send_data(0x40); send_data(0x1F); send_data(0x1F); send_data(0x2C);
|
||||
send_command(0x06);
|
||||
send_data(0x6F); send_data(0x1F); send_data(0x17); send_data(0x49);
|
||||
send_command(0x08);
|
||||
send_data(0x6F); send_data(0x1F); send_data(0x1F); send_data(0x22);
|
||||
send_command(0x30); send_data(0x03);
|
||||
send_command(0x50); send_data(0x3F);
|
||||
send_command(0x60); send_data(0x02); send_data(0x00);
|
||||
send_command(0x61);
|
||||
send_data(0x03); send_data(0x20); send_data(0x01); send_data(0xE0);
|
||||
send_command(0x84); send_data(0x01);
|
||||
send_command(0xE3); send_data(0x2F);
|
||||
send_command(0x04);
|
||||
wait_busy();
|
||||
}
|
||||
|
||||
void epd_sleep() {
|
||||
send_command(0x02); send_data(0x00); wait_busy();
|
||||
send_command(0x07); send_data(0xA5);
|
||||
}
|
||||
|
||||
void show_image() {
|
||||
send_command(0x10);
|
||||
digitalWrite(PIN_DC, HIGH);
|
||||
digitalWrite(PIN_CS, LOW);
|
||||
|
||||
// 90° CW rotation: display(x,y) → portrait(IMG_W-1-y, x)
|
||||
// Portrait image is IMG_W×IMG_H, IMAGE_ROW = IMG_W/2 bytes per portrait row.
|
||||
for (int y = 0; y < EPD_HEIGHT; y++) {
|
||||
int pcol = (IMG_W - 1) - y;
|
||||
for (int x = 0; x < EPD_WIDTH; x++) {
|
||||
uint32_t bidx = (uint32_t)x * IMAGE_ROW + pcol / 2;
|
||||
uint8_t b = pgm_read_byte(&IMAGE_DATA[bidx]);
|
||||
uint8_t code = (pcol & 1) ? (b & 0x0F) : (b >> 4);
|
||||
if (x & 1)
|
||||
row_buf[x / 2] = (row_buf[x / 2] & 0xF0) | code;
|
||||
else
|
||||
row_buf[x / 2] = (code << 4) | (row_buf[x / 2] & 0x0F);
|
||||
}
|
||||
SPI.writeBytes(row_buf, sizeof(row_buf));
|
||||
}
|
||||
digitalWrite(PIN_CS, HIGH);
|
||||
|
||||
send_command(0x04); wait_busy();
|
||||
send_command(0x12); send_data(0x00); wait_busy();
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Serial.println("pictureFrame");
|
||||
|
||||
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));
|
||||
|
||||
epd_init();
|
||||
show_image();
|
||||
epd_sleep();
|
||||
Serial.println("Done.");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
delay(60000);
|
||||
}
|
||||
Reference in New Issue
Block a user