The real display's power-on test (JP1 jumper 5, firmware $B888) runs through several drawing sequences — a solid fill, a border+grid, more — each held briefly, to expose dead dots and addressing faults; it isn't just full on/off. Both vPLASMA and the replica firmware now cycle 5 representative patterns: solid, border+grid, horizontal stripes, vertical stripes, checkerboard. - VPlasmaDevice.ShowTestPattern(int) + TestPatternCount; the app cycles them on a 1.2s timer while jumper 5 is installed. - PlasmaDisplay::showTestPattern(int) + TEST_PATTERN_COUNT (shared logic); the Matrix Portal sketch's DOWN button toggles the cycling test. (The firmware's exact byte patterns live in the display's native scan-buffer layout, which we haven't mapped, so these stand in for that sequence rather than reproducing it byte-for-byte.) Verified: 29 C# tests pass; the C++ patterns render distinctly on the host. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
226 lines
6.7 KiB
C++
226 lines
6.7 KiB
C++
#include "PlasmaDisplay.h"
|
|
|
|
// Command bytes (see PlasmaProtocol.cs / FIRMWARE.md).
|
|
static const uint8_t ESC = 0x1B;
|
|
static const uint8_t BS = 0x08, HT = 0x09, LF = 0x0A, VT = 0x0B, CR = 0x0D;
|
|
static const uint8_t CMD_CLEAR = '@', CMD_CURSOR = 'G', CMD_ATTR = 'H';
|
|
static const uint8_t CMD_FONT = 'K', CMD_HOME = 'L', CMD_GRAPHICS = 'P';
|
|
static const uint8_t CMD_SETROW = 'Q', CMD_SETCOL = 'R';
|
|
static const uint8_t CMD_DRAWPAGE = 'I', CMD_DISPPAGE = 'i';
|
|
|
|
PlasmaDisplay::PlasmaDisplay() { reset(); }
|
|
|
|
void PlasmaDisplay::reset() {
|
|
memset(pixels_, 0, sizeof(pixels_));
|
|
cx_ = cy_ = 0;
|
|
font_ = 0;
|
|
face_ = &plasmaFonts[0];
|
|
attrs_ = 0;
|
|
cursorMode_ = CURSOR_STEADY; // power-on default; the game hides it
|
|
orient_ = HORIZONTAL;
|
|
state_ = TEXT;
|
|
headerFill_ = 0;
|
|
dataIndex_ = dataLength_ = 0;
|
|
dirty_ = true;
|
|
}
|
|
|
|
void PlasmaDisplay::setOrientation(Orientation o) {
|
|
if (orient_ == o) return;
|
|
orient_ = o;
|
|
memset(pixels_, 0, sizeof(pixels_));
|
|
cx_ = cy_ = 0;
|
|
dirty_ = true;
|
|
}
|
|
|
|
// The real display's power-on test (JP1 jumper 5, firmware $B888) runs through
|
|
// several drawing sequences — solid, border+grid, and more — to expose dead
|
|
// dots and addressing faults. These stand in for that; the sketch cycles them.
|
|
static bool testDot(int index, int x, int y) {
|
|
switch (index) {
|
|
case 0: return true; // all on
|
|
case 1: return x == 0 || x == PlasmaDisplay::WIDTH - 1 || // frame
|
|
y == 0 || y == PlasmaDisplay::HEIGHT - 1 ||
|
|
(x % 16) == 0 || (y % 8) == 0; // + grid
|
|
case 2: return ((y / 2) % 2) == 0; // horizontal stripes
|
|
case 3: return ((x / 4) % 2) == 0; // vertical stripes
|
|
default: return (((x / 8) + (y / 8)) % 2) == 0; // checkerboard
|
|
}
|
|
}
|
|
|
|
void PlasmaDisplay::showTestPattern(int index) {
|
|
for (int y = 0; y < HEIGHT; ++y)
|
|
for (int x = 0; x < WIDTH; ++x)
|
|
pixels_[y * WIDTH + x] = testDot(index, x, y) ? PIX_LIT : 0;
|
|
dirty_ = true;
|
|
}
|
|
|
|
// One logical dot → the physical 128x32 buffer, rotated per orientation.
|
|
void PlasmaDisplay::plot(int lx, int ly, uint8_t flags) {
|
|
if ((unsigned)lx >= (unsigned)logicalW() || (unsigned)ly >= (unsigned)logicalH()) return;
|
|
int px, py;
|
|
if (orient_ == HORIZONTAL) {
|
|
px = lx;
|
|
py = ly;
|
|
} else {
|
|
px = ly;
|
|
py = HEIGHT - 1 - lx; // 90° rotation onto landscape glass
|
|
}
|
|
pixels_[py * WIDTH + px] = flags;
|
|
}
|
|
|
|
void PlasmaDisplay::feed(const uint8_t *buf, size_t n) {
|
|
for (size_t i = 0; i < n; ++i) feed(buf[i]);
|
|
}
|
|
|
|
void PlasmaDisplay::feed(uint8_t b) {
|
|
switch (state_) {
|
|
case TEXT: stepText(b); break;
|
|
case ESCAPE: stepEscape(b); break;
|
|
case OPERAND:
|
|
state_ = TEXT;
|
|
applyOperand(pendingCmd_, b);
|
|
break;
|
|
case GHEADER:
|
|
header_[headerFill_++] = b;
|
|
if (headerFill_ == 5) beginGraphics();
|
|
break;
|
|
case GDATA: stepGraphics(b); break;
|
|
}
|
|
}
|
|
|
|
void PlasmaDisplay::stepText(uint8_t b) {
|
|
switch (b) {
|
|
case ESC: state_ = ESCAPE; return;
|
|
case BS: cx_ = max(0, cx_ - face_->width); dirty_ = true; return;
|
|
case HT: advanceCursor(); dirty_ = true; return;
|
|
case LF: nextLine(); dirty_ = true; return;
|
|
case VT:
|
|
cy_ -= face_->height;
|
|
if (cy_ < 0) cy_ = max(0, logicalH() - face_->height);
|
|
dirty_ = true;
|
|
return;
|
|
case CR: cx_ = 0; dirty_ = true; return;
|
|
}
|
|
if (b < 0x20) return; // any other control byte: swallow (matches firmware)
|
|
drawChar(b);
|
|
}
|
|
|
|
void PlasmaDisplay::stepEscape(uint8_t b) {
|
|
state_ = TEXT;
|
|
switch (b) {
|
|
case CMD_CLEAR:
|
|
memset(pixels_, 0, sizeof(pixels_));
|
|
cx_ = cy_ = 0;
|
|
font_ = 0;
|
|
face_ = &plasmaFonts[0];
|
|
attrs_ = 0;
|
|
dirty_ = true;
|
|
break;
|
|
case CMD_HOME:
|
|
cx_ = cy_ = 0;
|
|
dirty_ = true;
|
|
break;
|
|
case CMD_CURSOR:
|
|
case CMD_FONT:
|
|
case CMD_ATTR:
|
|
case CMD_SETROW:
|
|
case CMD_SETCOL:
|
|
case CMD_DRAWPAGE:
|
|
case CMD_DISPPAGE:
|
|
pendingCmd_ = b;
|
|
state_ = OPERAND;
|
|
break;
|
|
case CMD_GRAPHICS:
|
|
headerFill_ = 0;
|
|
state_ = GHEADER;
|
|
break;
|
|
default: break; // unknown command: ignored (does not consume an operand)
|
|
}
|
|
}
|
|
|
|
void PlasmaDisplay::applyOperand(uint8_t cmd, uint8_t op) {
|
|
switch (cmd) {
|
|
case CMD_CURSOR:
|
|
cursorMode_ = (op == 0x00 || op == 0xFF) ? CURSOR_HIDDEN
|
|
: (op & 0x02) ? CURSOR_FLASHING
|
|
: CURSOR_STEADY;
|
|
dirty_ = true;
|
|
break;
|
|
case CMD_FONT:
|
|
if (op < 8) { // 8 real fonts; firmware ignores larger operands
|
|
font_ = op;
|
|
face_ = &plasmaFonts[op];
|
|
if (cx_ > logicalW() - 1) cx_ = logicalW() - 1;
|
|
if (cy_ > logicalH() - 1) cy_ = logicalH() - 1;
|
|
dirty_ = true;
|
|
}
|
|
break;
|
|
case CMD_ATTR:
|
|
attrs_ = op & 0x0F; // low 4 bits: half/underline/reverse/flash
|
|
break;
|
|
case CMD_SETROW:
|
|
if (op < logicalH()) { cy_ = op; dirty_ = true; }
|
|
break;
|
|
case CMD_SETCOL:
|
|
if (op < logicalW()) { cx_ = op; dirty_ = true; }
|
|
break;
|
|
case CMD_DRAWPAGE:
|
|
case CMD_DISPPAGE:
|
|
break; // page select: consumed but single-page (see FIRMWARE.md)
|
|
}
|
|
}
|
|
|
|
void PlasmaDisplay::beginGraphics() {
|
|
int w = header_[3], h = header_[4];
|
|
dataLength_ = w * h;
|
|
dataIndex_ = 0;
|
|
state_ = dataLength_ > 0 ? GDATA : TEXT;
|
|
}
|
|
|
|
void PlasmaDisplay::stepGraphics(uint8_t b) {
|
|
int w = header_[3];
|
|
int rowOfBlock = dataIndex_ / w;
|
|
int byteOfRow = dataIndex_ % w;
|
|
int y = header_[1] + rowOfBlock;
|
|
int baseX = (header_[2] + byteOfRow) * 8;
|
|
// MSB = leftmost pixel (L4PLASMA.CPP packs 0x80 first).
|
|
for (int bit = 0; bit < 8; ++bit)
|
|
plot(baseX + bit, y, (b & (0x80 >> bit)) ? PIX_LIT : 0);
|
|
dirty_ = true;
|
|
if (++dataIndex_ >= dataLength_) state_ = TEXT;
|
|
}
|
|
|
|
void PlasmaDisplay::drawChar(uint8_t code) {
|
|
if (!faceHas(code)) return; // firmware ignores out-of-range chars
|
|
|
|
int w = face_->width, h = face_->height;
|
|
bool reverse = attrs_ & 0x04;
|
|
bool underline = attrs_ & 0x02;
|
|
uint8_t litFlags = PIX_LIT;
|
|
if (attrs_ & 0x01) litFlags |= PIX_HALF;
|
|
if (attrs_ & 0x08) litFlags |= PIX_FLASH;
|
|
|
|
for (int row = 0; row < h; ++row) {
|
|
uint16_t bits = faceRow(code, row); // bit15 = leftmost pixel
|
|
for (int col = 0; col < w; ++col) {
|
|
bool on = bits & (0x8000 >> col);
|
|
if (underline && row == h - 1) on = true;
|
|
if (reverse) on = !on;
|
|
plot(cx_ + col, cy_ + row, on ? litFlags : 0);
|
|
}
|
|
}
|
|
dirty_ = true;
|
|
advanceCursor();
|
|
}
|
|
|
|
void PlasmaDisplay::advanceCursor() {
|
|
cx_ += face_->width;
|
|
if (cx_ > logicalW() - face_->width) nextLine();
|
|
}
|
|
|
|
void PlasmaDisplay::nextLine() {
|
|
cx_ = 0;
|
|
cy_ += face_->height;
|
|
if (cy_ > logicalH() - face_->height) cy_ = 0; // wrap to top; no scroll
|
|
}
|