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>
91 lines
3.1 KiB
C++
91 lines
3.1 KiB
C++
// PlasmaDisplay — the Babcock PD01D221 command parser + 128x32 framebuffer,
|
|
// ported line-for-line from vRIO's C# VPlasmaDevice (src/VPlasma.Core/Device/
|
|
// VPlasmaDevice.cs). Keep the two in sync: vPLASMA is the reference oracle.
|
|
//
|
|
// Feed received wire bytes to feed(); read the framebuffer with frame() and
|
|
// render it however the panel wants. Each pixel is a flag byte (PIX_LIT /
|
|
// PIX_HALF / PIX_FLASH), so the renderer can dim half-intensity dots and blink
|
|
// flashing ones. See PlasmaNew/FIRMWARE.md for the recovered command set.
|
|
#pragma once
|
|
#include <Arduino.h>
|
|
#include "plasma_fonts.h"
|
|
|
|
class PlasmaDisplay {
|
|
public:
|
|
static const int WIDTH = 128;
|
|
static const int HEIGHT = 32;
|
|
|
|
// Per-pixel flag bits in the framebuffer.
|
|
static const uint8_t PIX_LIT = 0x01;
|
|
static const uint8_t PIX_HALF = 0x02;
|
|
static const uint8_t PIX_FLASH = 0x04;
|
|
|
|
enum Orientation { HORIZONTAL, VERTICAL };
|
|
enum CursorMode { CURSOR_HIDDEN, CURSOR_STEADY, CURSOR_FLASHING };
|
|
|
|
PlasmaDisplay();
|
|
|
|
static const int TEST_PATTERN_COUNT = 5; // JP1 jumper-5 diagnostic patterns
|
|
|
|
void reset(); // power-on: dark glass, home cursor
|
|
void feed(uint8_t b); // one received wire byte
|
|
void feed(const uint8_t *buf, size_t n);
|
|
void showTestPattern() { showTestPattern(0); } // all dots lit
|
|
void showTestPattern(int index); // JP1 jumper 5: solid/grid/stripes/checker
|
|
void setOrientation(Orientation o); // JP1 jumper 4
|
|
|
|
const uint8_t *frame() const { return pixels_; }
|
|
bool takeDirty() { // true (and clears) if the frame changed
|
|
bool d = dirty_;
|
|
dirty_ = false;
|
|
return d;
|
|
}
|
|
|
|
CursorMode cursorMode() const { return cursorMode_; }
|
|
int cursorX() const { return cx_; }
|
|
int cursorY() const { return cy_; }
|
|
int fontWidth() const { return face_->width; }
|
|
int fontHeight() const { return face_->height; }
|
|
Orientation orientation() const { return orient_; }
|
|
|
|
private:
|
|
uint8_t pixels_[WIDTH * HEIGHT]; // always physical 128x32
|
|
|
|
// Text-mode state.
|
|
int font_;
|
|
const PlasmaFace *face_;
|
|
uint8_t attrs_; // low 4 bits: 1=half 2=underline 4=reverse 8=flash
|
|
int cx_, cy_; // cursor, logical pixels
|
|
CursorMode cursorMode_;
|
|
Orientation orient_;
|
|
|
|
// Parser state.
|
|
enum State { TEXT, ESCAPE, OPERAND, GHEADER, GDATA };
|
|
State state_;
|
|
uint8_t pendingCmd_;
|
|
uint8_t header_[5]; // screen, y, x, w, h
|
|
int headerFill_;
|
|
int dataIndex_, dataLength_;
|
|
|
|
bool dirty_;
|
|
|
|
int logicalW() const { return orient_ == HORIZONTAL ? WIDTH : HEIGHT; }
|
|
int logicalH() const { return orient_ == HORIZONTAL ? HEIGHT : WIDTH; }
|
|
|
|
void plot(int lx, int ly, uint8_t flags);
|
|
void stepText(uint8_t b);
|
|
void stepEscape(uint8_t b);
|
|
void applyOperand(uint8_t cmd, uint8_t op);
|
|
void beginGraphics();
|
|
void stepGraphics(uint8_t b);
|
|
void drawChar(uint8_t code);
|
|
void advanceCursor();
|
|
void nextLine();
|
|
|
|
bool faceHas(uint8_t code) const { return code >= face_->first && code <= face_->last; }
|
|
uint16_t faceRow(uint8_t code, int row) const {
|
|
if (!faceHas(code) || (unsigned)row >= (unsigned)face_->height) return 0;
|
|
return pgm_read_word(&face_->rows[(code - face_->first) * face_->height + row]);
|
|
}
|
|
};
|