#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 }