Files
VRIO/PlasmaNew/replica/MatrixPortalPlasma/PlasmaDisplay.cpp
T
CydandClaude Opus 4.8 ab36cdd826 PlasmaNew/replica: Matrix Portal S3 firmware for the hardware replica
The hardware replacement for the failing Babcock PD01D221: an Adafruit Matrix
Portal S3 (ESP32-S3) reads the game's serial command stream over its native
USB-CDC port and renders it to two chained Adafruit 64x32 HUB75 RGB panels
(= 128x32) in neon-orange, via Adafruit Protomatter.

- PlasmaDisplay.h/.cpp — the PD01D221 command parser + 128x32 framebuffer,
  ported line-for-line from vPLASMA's VPlasmaDevice (the reference oracle):
  the full ESC command set, pixel-addressed cursor, attributes, orientation,
  and the rotate-onto-glass plot() mapping.
- plasma_fonts.h — the 8 real ROM fonts (PROGMEM), generated from
  tms27pc512.BIN.
- demo_screens.h — the 10 firmware demo screens (PROGMEM).
- MatrixPortalPlasma.ino — Protomatter init (128x32), USB serial input,
  the orange/dim/blink render loop, and onboard buttons (UP = firmware demo,
  DOWN = panel test) for no-host bring-up.
- README.md — BOM, wiring, build/flash, DOSBox-X serial config.

Parser verified on the host with g++ (font-0 'H', ESC Q/R positioning,
demo replay, test pattern) against a minimal Arduino shim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 20:22:06 -05:00

209 lines
5.8 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;
}
void PlasmaDisplay::showTestPattern() {
for (int i = 0; i < WIDTH * HEIGHT; ++i) pixels_[i] = PIX_LIT;
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
}