feat: add WT32-SC01 LCD support + dynamic display resolution

- Add WT32-SC01 V1 (ESP32-WROVER, 480x320 LCD) as second board option
- Split ESP32 code into epaper/ and lcd/ subdirectories
- LCD version: LovyanGFX, color display, touch-to-refresh, no deep sleep
- Add display_type, width, height to displays table (migration 003)
- Frontend: display type selector with predefined resolutions
- Preview and layout editor now use dynamic resolution from display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-07 21:12:08 +02:00
co-authored by Claude Opus 4.6
parent 52dddfd74f
commit 77c9a77645
12 changed files with 589 additions and 22 deletions
+30 -5
View File
@@ -5,6 +5,9 @@ export interface Display {
id: number;
name: string;
description: string | null;
display_type: string;
width: number;
height: number;
layout_id: number | null;
api_token: string | null;
last_seen: string | null;
@@ -12,8 +15,22 @@ export interface Display {
created_at: string;
}
// Predefined display types with resolutions
export const DISPLAY_TYPES: Record<string, { width: number; height: number; label: string }> = {
'epaper-7.5': { width: 800, height: 480, label: 'Waveshare 7.5" E-Paper (800x480)' },
'wt32-sc01': { width: 480, height: 320, label: 'WT32-SC01 LCD (480x320)' },
'epaper-4.2': { width: 400, height: 300, label: 'Waveshare 4.2" E-Paper (400x300)' },
'epaper-5.83': { width: 648, height: 480, label: 'Waveshare 5.83" E-Paper (648x480)' },
'custom': { width: 800, height: 480, label: 'Benutzerdefiniert' },
};
const router = Router();
// GET /api/displays/types — available display types
router.get('/types', (_req, res): void => {
res.json(DISPLAY_TYPES);
});
// GET /api/displays
router.get('/', (_req, res): void => {
try {
@@ -40,13 +57,18 @@ router.get('/:id', (req, res): void => {
router.post('/', (req, res): void => {
try {
const db = getDb();
const { name, description, layout_id, api_token, refresh_sec } = req.body;
const { name, description, display_type, width, height, layout_id, api_token, refresh_sec } = req.body;
if (!name) { res.status(400).json({ error: 'name is required' }); return; }
const dtype = display_type ?? 'epaper-7.5';
const defaults = DISPLAY_TYPES[dtype] ?? DISPLAY_TYPES['epaper-7.5'];
const w = width ?? defaults.width;
const h = height ?? defaults.height;
const result = db.prepare(
`INSERT INTO displays (name, description, layout_id, api_token, refresh_sec)
VALUES (?, ?, ?, ?, ?)`
).run(name, description ?? null, layout_id ?? null, api_token ?? null, refresh_sec ?? 300);
`INSERT INTO displays (name, description, display_type, width, height, layout_id, api_token, refresh_sec)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
).run(name, description ?? null, dtype, w, h, layout_id ?? null, api_token ?? null, refresh_sec ?? 300);
const id = result.lastInsertRowid as number;
res.status(201).json(db.prepare('SELECT * FROM displays WHERE id = ?').get(id));
} catch (err) {
@@ -62,9 +84,12 @@ router.put('/:id', (req, res): void => {
const fields: string[] = [];
const values: unknown[] = [];
const { name, description, layout_id, api_token, refresh_sec } = req.body;
const { name, description, display_type, width, height, layout_id, api_token, refresh_sec } = req.body;
if (name !== undefined) { fields.push('name = ?'); values.push(name); }
if (description !== undefined) { fields.push('description = ?'); values.push(description); }
if (display_type !== undefined) { fields.push('display_type = ?'); values.push(display_type); }
if (width !== undefined) { fields.push('width = ?'); values.push(width); }
if (height !== undefined) { fields.push('height = ?'); values.push(height); }
if (layout_id !== undefined) { fields.push('layout_id = ?'); values.push(layout_id); }
if (api_token !== undefined) { fields.push('api_token = ?'); values.push(api_token); }
if (refresh_sec !== undefined) { fields.push('refresh_sec = ?'); values.push(refresh_sec); }
+2
View File
@@ -1,6 +1,7 @@
import Database from 'better-sqlite3';
import { migrate001 } from './migrations/001-initial';
import { migrate002 } from './migrations/002-iobroker';
import { migrate003 } from './migrations/003-display-resolution';
let dbInstance: Database.Database | null = null;
@@ -13,6 +14,7 @@ export function initDb(path: string): Database.Database {
// Run migrations
migrate001(db);
migrate002(db);
migrate003(db);
dbInstance = db;
return db;
@@ -0,0 +1,13 @@
import Database from 'better-sqlite3';
export function migrate003(db: Database): void {
// Check if column already exists
const columns = db.pragma('table_info(displays)') as { name: string }[];
if (columns.some((c) => c.name === 'display_type')) return;
db.exec(`
ALTER TABLE displays ADD COLUMN display_type TEXT DEFAULT 'epaper-7.5' NOT NULL;
ALTER TABLE displays ADD COLUMN width INTEGER DEFAULT 800 NOT NULL;
ALTER TABLE displays ADD COLUMN height INTEGER DEFAULT 480 NOT NULL;
`);
}
@@ -43,6 +43,9 @@ interface RawDisplay {
id: number;
name: string;
description: string | null;
display_type: string;
width: number;
height: number;
layout_id: number | null;
api_token: string | null;
last_seen: string | null;
@@ -98,8 +101,8 @@ export function buildDisplayJson(displayId: number): DisplayJson | null {
display: {
id: display.id,
name: display.name,
width: 800,
height: 480,
width: display.width,
height: display.height,
refresh_sec: display.refresh_sec,
},
layout: { type: 'free' },
@@ -166,8 +169,8 @@ export function buildDisplayJson(displayId: number): DisplayJson | null {
display: {
id: display.id,
name: display.name,
width: layout.width,
height: layout.height,
width: display.width,
height: display.height,
refresh_sec: display.refresh_sec,
},
layout: layoutResult,