velxio/frontend/src/__tests__/uc8159c-decoder.test.ts

164 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* UC8159c (ACeP 7-colour) decoder tests.
*
* The 5.65" GoodDisplay GDEP0565D90 / Waveshare 5.65" panel uses this
* controller. Pixel packing: 2 px / byte, upper nibble = first pixel,
* each nibble's low 3 bits is a palette index 0..6.
*
* Latches on opcode 0x12 (DRF), not 0x20 like SSD168x. Power-on/off and
* deep sleep flags exposed for diagnostic introspection.
*/
import { describe, it, expect } from 'vitest';
import {
UC8159cDecoder,
UC_CMD_PANEL_SETTING,
UC_CMD_POWER_ON,
UC_CMD_POWER_OFF,
UC_CMD_DEEP_SLEEP,
UC_CMD_DTM1,
UC_CMD_DISPLAY_REFRESH,
UC_CMD_RESOLUTION_SETTING,
UC_CMD_VCOM_DATA_INTERVAL,
ACEP_PALETTE_RGB,
type UC8159cFrame,
} from '../simulation/displays/UC8159cDecoder';
const cmd = (c: number): Array<[number, boolean]> => [[c, false]];
const data = (...bs: number[]): Array<[number, boolean]> => bs.map((b) => [b, true] as [number, boolean]);
function feedAll(d: UC8159cDecoder, ...streams: Array<Array<[number, boolean]>>) {
for (const stream of streams) for (const [b, dc] of stream) d.feed(b, dc);
}
describe('UC8159cDecoder — init + power state', () => {
it('absorbs the GxEPD2 init sequence without unknown opcodes', () => {
const d = new UC8159cDecoder({ width: 600, height: 448 });
feedAll(
d,
cmd(UC_CMD_PANEL_SETTING), data(0xef, 0x08),
cmd(UC_CMD_RESOLUTION_SETTING), data(0x02, 0x58, 0x01, 0xc0),
cmd(UC_CMD_VCOM_DATA_INTERVAL), data(0x37),
);
expect(d.unknownCmds).toEqual([]);
});
it('POWER_ON sets the powered_on flag, POWER_OFF clears it', () => {
const d = new UC8159cDecoder({ width: 8, height: 1 });
expect(d.poweredOn).toBe(false);
feedAll(d, cmd(UC_CMD_POWER_ON));
expect(d.poweredOn).toBe(true);
feedAll(d, cmd(UC_CMD_POWER_OFF));
expect(d.poweredOn).toBe(false);
});
it('DEEP_SLEEP + 0xA5 sets the in_deep_sleep flag', () => {
const d = new UC8159cDecoder({ width: 8, height: 1 });
feedAll(d, cmd(UC_CMD_DEEP_SLEEP), data(0xa5));
expect(d.inDeepSleep).toBe(true);
});
it('unknown opcodes are logged not raised', () => {
const d = new UC8159cDecoder({ width: 8, height: 1 });
feedAll(d, cmd(0xab), data(0x01));
expect(d.unknownCmds).toContain(0xab);
});
});
describe('UC8159cDecoder — pixel packing', () => {
it('writes 2 pixels per byte, upper nibble first', () => {
// 4-px wide × 1-row panel = 2 bytes of pixel data.
const seen: UC8159cFrame[] = [];
const d = new UC8159cDecoder({
width: 4,
height: 1,
onFlush: (f) => seen.push(f),
});
feedAll(
d,
cmd(UC_CMD_DTM1),
// 0x40 → upper=0x4 (red), lower=0x0 (black)
// 0x52 → upper=0x5 (yellow), lower=0x2 (green)
data(0x40, 0x52),
cmd(UC_CMD_DISPLAY_REFRESH),
data(0x00),
);
expect(seen.length).toBe(1);
expect(Array.from(seen[0].pixels)).toEqual([4, 0, 5, 2]);
});
it('only stores low 3 bits as the palette index', () => {
// 0xFE → upper nibble=0xF (mask to 0x7 = 7=clean), lower=0xE (mask to 0x6 = orange)
const seen: UC8159cFrame[] = [];
const d = new UC8159cDecoder({
width: 2,
height: 1,
onFlush: (f) => seen.push(f),
});
feedAll(d, cmd(UC_CMD_DTM1), data(0xfe), cmd(UC_CMD_DISPLAY_REFRESH));
expect(Array.from(seen[0].pixels)).toEqual([7, 6]);
});
it('stops at width*height; extra bytes are ignored', () => {
const seen: UC8159cFrame[] = [];
const d = new UC8159cDecoder({
width: 2,
height: 1,
onFlush: (f) => seen.push(f),
});
// 3 bytes = 6 px attempted, but RAM is only 2 pixels.
feedAll(d, cmd(UC_CMD_DTM1), data(0x40, 0x52, 0x66), cmd(UC_CMD_DISPLAY_REFRESH));
expect(Array.from(seen[0].pixels)).toEqual([4, 0]);
});
it('DTM1 reset cursor each time so the same frame can be redrawn', () => {
const seen: UC8159cFrame[] = [];
const d = new UC8159cDecoder({
width: 2,
height: 1,
onFlush: (f) => seen.push(f),
});
feedAll(d, cmd(UC_CMD_DTM1), data(0x40), cmd(UC_CMD_DISPLAY_REFRESH));
feedAll(d, cmd(UC_CMD_DTM1), data(0x52), cmd(UC_CMD_DISPLAY_REFRESH));
expect(seen.length).toBe(2);
expect(Array.from(seen[0].pixels)).toEqual([4, 0]);
expect(Array.from(seen[1].pixels)).toEqual([5, 2]);
});
});
describe('UC8159cDecoder — refresh latching', () => {
it('DISPLAY_REFRESH (0x12) fires onFlush; refreshedCount increments', () => {
const seen: UC8159cFrame[] = [];
const d = new UC8159cDecoder({
width: 600,
height: 448,
onFlush: (f) => seen.push(f),
});
feedAll(d, cmd(UC_CMD_DISPLAY_REFRESH));
expect(d.refreshedCount).toBe(1);
expect(seen.length).toBe(1);
expect(seen[0].pixels.length).toBe(600 * 448);
});
it('default RAM is all index-1 (white)', () => {
const seen: UC8159cFrame[] = [];
const d = new UC8159cDecoder({
width: 100,
height: 1,
onFlush: (f) => seen.push(f),
});
feedAll(d, cmd(UC_CMD_DISPLAY_REFRESH));
expect(seen[0].pixels.every((p) => p === 1)).toBe(true);
});
});
describe('UC8159cDecoder — palette table sanity', () => {
it('all 7 ACeP indices map to distinct RGB triples', () => {
const seen = new Set<string>();
for (let i = 0; i < 7; i++) {
const [r, g, b] = ACEP_PALETTE_RGB[i];
seen.add(`${r},${g},${b}`);
}
expect(seen.size).toBe(7);
});
});