velxio/frontend/src/simulation/displays/EPaperPanels.ts

184 lines
5.4 KiB
TypeScript
Raw Normal View History

/**
* EPaperPanels — per-panel geometry + controller assignments.
*
* Phase 1 ships the SSD168x mono family. Adding a panel = add an entry
* here; the Web Component, simulation hook, and ESP32 backend slave all
* key off the `id` field. See `test/test_epaper/autosearch/06_svg_layouts.md`
* for sources.
*/
feat(epaper): decode the UC8179/GD7965 7.5" panel + fix its BUSY polarity The 7.5" 800x480 dashboard (GxEPD2_750_T7) rendered blank: it is a UC8179 / GD7965 controller, but the panel config claimed controllerFamily 'ssd168x', so the SSD168x decoder (which only reads 0x24/0x26/0x44/0x45) ignored its 0x10/0x13 DTM stream. - Add a Uc8179 decoder (worker Uc8179EpaperSlave + browser Uc8179Decoder). UC8179 is the same UltraChip command family as the UC8159c (0x10/0x13 DTM, 0x12 refresh) but mono (1 bit/px). GxEPD2 writes the visible image to 0x13 (DTM2 "current"; 0x10 is the ignored "previous"), framed by 0x91/0x90 (partial window, pixel coords MSB-first)/0x13 data/0x92. Data lands at absolute pixel coords inside the window, so compose is just the RAM. The Frame reuses the SSD168x palette (0=black, 1=white) so paintFrame renders it. - EPaperPanels.ts: add the 'uc8179' family and point epaper-7in5-bw at it. EPaperPart.ts + esp32_worker.py dispatch 'uc8179' to the new decoder. - Fix the BUSY polarity: UC8179 (like the UC8159c) idles BUSY HIGH, not LOW. The worker seeded BUSY LOW for every non-uc8159c panel, so GxEPD2_750_T7's _PowerOn()/_InitDisplay() busy-wait timed out (~10 s, "Busy Timeout!") on every refresh. Now _PowerOn returns in ~129 us. - esp32_worker.py: the runtime sensor_attach epaper path still emitted the epaper_update payload nested under 'data' (the old double-wrap bug); emit it flat like the init path. The 5.65" ACeP UC8159c example already rendered (it has its own decoder and got the WS-plumbing fix); verified the 7 colour bars are correct.
2026-06-05 09:45:37 +07:00
export type EPaperControllerFamily = 'ssd168x' | 'uc8159c' | 'uc8179';
/**
* Visible palette for a panel.
* - 'bw' → black / white only (single 1-bit RAM plane)
* - 'bwr' → black / white / red (two 1-bit RAM planes; red wins on compose)
* - 'acep' → 7-colour ACeP (single 3-bit-per-pixel RAM, palette 0..6)
*/
export type EPaperPalette = 'bw' | 'bwr' | 'acep';
export interface EPaperPanelConfig {
/** Unique kebab-case identifier — also the metadataId used by the registry. */
id: string;
/** Human-readable label (gallery card). */
name: string;
/** Active area resolution, in panel-native pixels. */
width: number;
height: number;
/** Panel body width including bezel + FPC strip (CSS px). */
bodyW: number;
bodyH: number;
/** Bezel margin on the canvas (CSS px). */
bezelPx: number;
/** FPC tail height (CSS px). */
fpcStripPx: number;
/** Default refresh duration the emulator drives BUSY high for (ms). */
refreshMs: number;
/** Controller family — picks the decoder. */
controllerFamily: EPaperControllerFamily;
/** Concrete controller IC (informational, drives the Inspector tooltip). */
controllerIc: string;
/** Visible palette. Tri-colour panels (B/W/R) use the red RAM plane. */
palette: EPaperPalette;
}
export const PANEL_CONFIGS: Record<string, EPaperPanelConfig> = {
// ── 1.54" 200×200 — the canonical Phase-1 panel ─────────────────────
'epaper-1in54-bw': {
id: 'epaper-1in54-bw',
name: '1.54" ePaper (200×200, B/W)',
width: 200,
height: 200,
bodyW: 240,
bodyH: 280,
bezelPx: 14,
fpcStripPx: 36,
refreshMs: 50,
controllerFamily: 'ssd168x',
controllerIc: 'SSD1681',
palette: 'bw',
},
// ── 2.13" 250×122 — most popular community badge size ───────────────
'epaper-2in13-bw': {
id: 'epaper-2in13-bw',
name: '2.13" ePaper (250×122, B/W)',
width: 250,
height: 122,
bodyW: 290,
bodyH: 170,
bezelPx: 14,
fpcStripPx: 32,
refreshMs: 50,
controllerFamily: 'ssd168x',
controllerIc: 'SSD1675A / IL3897',
palette: 'bw',
},
// ── 2.13" 250×122 tri-colour B/W/R ───────────────────────────────────
'epaper-2in13-bwr': {
id: 'epaper-2in13-bwr',
name: '2.13" ePaper (250×122, B/W/Red)',
width: 250,
height: 122,
bodyW: 290,
bodyH: 170,
bezelPx: 14,
fpcStripPx: 32,
refreshMs: 80,
controllerFamily: 'ssd168x',
controllerIc: 'SSD1680 (3-colour)',
palette: 'bwr',
},
// ── 2.9" 296×128 — slightly bigger badge size ───────────────────────
'epaper-2in9-bw': {
id: 'epaper-2in9-bw',
name: '2.9" ePaper (296×128, B/W)',
width: 296,
height: 128,
bodyW: 340,
bodyH: 180,
bezelPx: 16,
fpcStripPx: 32,
refreshMs: 50,
controllerFamily: 'ssd168x',
controllerIc: 'SSD1680',
palette: 'bw',
},
// ── 2.9" 296×128 tri-colour B/W/R ────────────────────────────────────
'epaper-2in9-bwr': {
id: 'epaper-2in9-bwr',
name: '2.9" ePaper (296×128, B/W/Red)',
width: 296,
height: 128,
bodyW: 340,
bodyH: 180,
bezelPx: 16,
fpcStripPx: 32,
refreshMs: 80,
controllerFamily: 'ssd168x',
controllerIc: 'SSD1680 (3-colour)',
palette: 'bwr',
},
// ── 4.2" 400×300 — mid-size, popular for dashboards ─────────────────
'epaper-4in2-bw': {
id: 'epaper-4in2-bw',
name: '4.2" ePaper (400×300, B/W)',
width: 400,
height: 300,
bodyW: 440,
bodyH: 360,
bezelPx: 16,
fpcStripPx: 36,
refreshMs: 80,
controllerFamily: 'ssd168x',
controllerIc: 'SSD1683 / UC8176',
palette: 'bw',
},
// ── 7.5" 800×480 — biggest mono panel we ship in Phase 1 ────────────
'epaper-7in5-bw': {
id: 'epaper-7in5-bw',
name: '7.5" ePaper (800×480, B/W)',
width: 800,
height: 480,
bodyW: 860,
bodyH: 540,
bezelPx: 24,
fpcStripPx: 36,
refreshMs: 100,
feat(epaper): decode the UC8179/GD7965 7.5" panel + fix its BUSY polarity The 7.5" 800x480 dashboard (GxEPD2_750_T7) rendered blank: it is a UC8179 / GD7965 controller, but the panel config claimed controllerFamily 'ssd168x', so the SSD168x decoder (which only reads 0x24/0x26/0x44/0x45) ignored its 0x10/0x13 DTM stream. - Add a Uc8179 decoder (worker Uc8179EpaperSlave + browser Uc8179Decoder). UC8179 is the same UltraChip command family as the UC8159c (0x10/0x13 DTM, 0x12 refresh) but mono (1 bit/px). GxEPD2 writes the visible image to 0x13 (DTM2 "current"; 0x10 is the ignored "previous"), framed by 0x91/0x90 (partial window, pixel coords MSB-first)/0x13 data/0x92. Data lands at absolute pixel coords inside the window, so compose is just the RAM. The Frame reuses the SSD168x palette (0=black, 1=white) so paintFrame renders it. - EPaperPanels.ts: add the 'uc8179' family and point epaper-7in5-bw at it. EPaperPart.ts + esp32_worker.py dispatch 'uc8179' to the new decoder. - Fix the BUSY polarity: UC8179 (like the UC8159c) idles BUSY HIGH, not LOW. The worker seeded BUSY LOW for every non-uc8159c panel, so GxEPD2_750_T7's _PowerOn()/_InitDisplay() busy-wait timed out (~10 s, "Busy Timeout!") on every refresh. Now _PowerOn returns in ~129 us. - esp32_worker.py: the runtime sensor_attach epaper path still emitted the epaper_update payload nested under 'data' (the old double-wrap bug); emit it flat like the init path. The 5.65" ACeP UC8159c example already rendered (it has its own decoder and got the WS-plumbing fix); verified the 7 colour bars are correct.
2026-06-05 09:45:37 +07:00
controllerFamily: 'uc8179',
controllerIc: 'UC8179 / GD7965',
palette: 'bw',
},
// ── 5.65" 600×448 — ACeP 7-colour (UC8159c family) ──────────────────
// Real refresh time: ~12 s. We default to 150 ms emulator pulse.
'epaper-5in65-7c': {
id: 'epaper-5in65-7c',
name: '5.65" ePaper (600×448, ACeP 7-colour)',
width: 600,
height: 448,
bodyW: 660,
bodyH: 520,
bezelPx: 22,
fpcStripPx: 36,
refreshMs: 150,
controllerFamily: 'uc8159c',
controllerIc: 'UC8159c',
palette: 'acep',
},
};
export const PANEL_IDS = Object.keys(PANEL_CONFIGS);
/** Default if the Web Component is mounted without a panel-kind attribute. */
export const DEFAULT_PANEL_KIND = 'epaper-1in54-bw';
export function getPanelConfig(panelKind: string | null | undefined): EPaperPanelConfig {
return PANEL_CONFIGS[panelKind ?? DEFAULT_PANEL_KIND] ?? PANEL_CONFIGS[DEFAULT_PANEL_KIND];
}