I only use the 74LS245 after finishing my code, mainly to reduce flickering on the panels. Before that, I wired the ESP8266 directly to the panel. I used the 74LS245 only because I couldn't find the recommended 74HCT245.
From my testing, that chip didn't affect the color mapping or fix the color problems.
One more detail about the mapping: whenever I tried changing the wiring or mapping between the two panels, the result would change. Sometimes the panels would show different colors, sometimes one panel would light up while the other stayed blank, and sometimes the color behavior would change again. So I'm getting different color and display issues depending on how the panels are connected. That's why I tried many combinations, but I still couldn't determine the correct mapping for these panels.
#include "Display.h"
#include <string.h>
#include <math.h>
#ifdef ESP8266
#include <Ticker.h>
static Ticker display_ticker;
#endif
// =====================================================================
// Panel object + refresh ISR (unchanged in spirit from the single-panel
// version - the panel is refreshed on a timer independent of everything
// below).
// =====================================================================
PxMATRIX display(MATRIX_WIDTH, MATRIX_HEIGHT, P_LAT, P_OE, P_A, P_B, P_C);
static uint8_t display_draw_time = 40;
uint16_t COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_WHITE;
uint16_t COLOR_YELLOW, COLOR_CYAN, COLOR_MAGENTA, COLOR_BLACK;
#ifdef ESP8266
static void display_updater()
{
display.display(display_draw_time);
}
#endif
static void display_update_enable(bool enable)
{
#ifdef ESP8266
if (enable)
display_ticker.attach_ms(2, display_updater);
else
display_ticker.detach();
#endif
}
// =====================================================================
// Virtual-canvas / rotation / two-panel-split machinery. Everything
// below this point draws onto vCanvas (and vCanvasColon where a second
// color is needed) in normal left-to-right/top-to-bottom terms, then one
// of the push*() functions maps that onto the two physical panels,
// compensating for the panel unit being physically turned 90 degrees.
// Ported as-is from the working single-file scoreboard sketch.
// =====================================================================
#define panel_width 32
#define vcanvas_w 32
#define vcanvas_h 32
static GFXcanvas1 vCanvas(vcanvas_w, vcanvas_h);
static GFXcanvas1 vCanvasColon(vcanvas_w, vcanvas_h);
const bool ROTATE_CW = true;
// Maps a physical-buffer position (vx across a panel, vy down the two
// stacked halves) back to the upright canvas pixel it displays.
static void rotatedSourceXY(int16_t vx, int16_t vy, int16_t &srcX, int16_t &srcY)
{
if (ROTATE_CW)
{
srcX = vy;
srcY = (vcanvas_w - 1) - vx;
}
else
{
srcX = (vcanvas_h - 1) - vy;
srcY = vx;
}
}
static bool getRotatedPixelFrom(GFXcanvas1 &canvas, int16_t vx, int16_t vy)
{
int16_t srcX, srcY;
rotatedSourceXY(vx, vy, srcX, srcY);
return canvas.getPixel(srcX, srcY);
}
static void pushVirtualCanvasToDisplay(uint16_t color)
{
display.clearDisplay();
for (int16_t vy = 0; vy < vcanvas_h; vy++)
{
for (int16_t vx = 0; vx < vcanvas_w; vx++)
{
if (getRotatedPixelFrom(vCanvas, vx, vy))
{
int16_t physX = (vy < 16 ? 0 : panel_width) + vx;
int16_t physY = (vy < 16 ? vy : vy - 16);
display.drawPixel(physX, physY, color);
}
}
}
}
// Clock push: the 4 big digits sit in a 2x2 grid on the upright canvas
// (minutes on the top row, seconds on the bottom row, tens on the left,
// ones on the right), so each digit's color is chosen by which quadrant
// the lit pixel is in. The colon dots (vCanvasColon) get their own color.
static void pushClockToDisplay(uint16_t minTensColor, uint16_t minOnesColor,
uint16_t secTensColor, uint16_t secOnesColor,
uint16_t colonColor)
{
display.clearDisplay();
for (int16_t vy = 0; vy < vcanvas_h; vy++)
{
for (int16_t vx = 0; vx < vcanvas_w; vx++)
{
int16_t physX = (vy < 16 ? 0 : panel_width) + vx;
int16_t physY = (vy < 16 ? vy : vy - 16);
int16_t srcX, srcY;
rotatedSourceXY(vx, vy, srcX, srcY);
if (vCanvas.getPixel(srcX, srcY))
{
bool bottomRow = srcY >= 16;
bool rightCol = srcX >= 16;
uint16_t color = bottomRow ? (rightCol ? secOnesColor : secTensColor)
: (rightCol ? minOnesColor : minTensColor);
display.drawPixel(physX, physY, color);
}
else if (vCanvasColon.getPixel(srcX, srcY))
{
display.drawPixel(physX, physY, colonColor);
}
}
}
}
// Color-pixel plotting (for the multi-color scoreboard - flags/poles/
// digits/star all differently colored in the same frame). Same
// coordinate convention as vCanvas (normal x=column, y=row), rotated and
// split the same way.
static void plotColorPixel(int16_t srcX, int16_t srcY, uint16_t color)
{
int16_t vx, vy;
if (ROTATE_CW)
{
vy = srcX;
vx = (vcanvas_w - 1) - srcY;
}
else
{
vy = (vcanvas_h - 1) - srcX;
vx = srcY;
}
if (vx < 0 || vx >= vcanvas_w || vy < 0 || vy >= vcanvas_h) return;
int16_t physX = (vy < 16 ? 0 : panel_width) + vx;
int16_t physY = (vy < 16 ? vy : vy - 16);
display.drawPixel(physX, physY, color);
}
static void plotColorRect(int16_t srcX, int16_t srcY, int16_t w, int16_t h, uint16_t color)
{
for (int16_t j = 0; j < h; j++)
for (int16_t i = 0; i < w; i++)
plotColorPixel(srcX + i, srcY + j, color);
}
void displayInit()
{
display.begin(4);
display.setScanPattern(ZAGZIG);
COLOR_RED = display.color565(255, 0, 0);
COLOR_GREEN = display.color565(0, 255, 0);
COLOR_BLUE = display.color565(0, 0, 255);
COLOR_WHITE = display.color565(255, 255, 255);
COLOR_YELLOW = display.color565(255, 255, 0);
COLOR_CYAN = display.color565(0, 255, 255);
COLOR_MAGENTA = display.color565(255, 0, 255);
COLOR_BLACK = display.color565(0, 0, 0);
display.clearDisplay();
display.setTextWrap(false);
display.setRotation(0); // leave library rotation off - we rotate content ourselves above
display_update_enable(true);
}
// =====================================================================
// Non-blocking countdown timer - single big number, centered.
// =====================================================================
static bool cd_running = false;
static uint8_t cd_current = 0;
static unsigned long cd_lastTick = 0;
static void countdown_drawFrame()
{
char buf[4];
snprintf(buf, sizeof(buf), "%d", cd_current);
int16_t x1, y1;
uint16_t w, h;
vCanvas.setTextSize(3);
vCanvas.getTextBounds(buf, 0, 0, &x1, &y1, &w, &h);
vCanvas.fillScreen(0);
vCanvas.setCursor((vcanvas_w - (int)w) / 2, (vcanvas_h - (int)h) / 2);
vCanvas.setTextColor(1);
vCanvas.print(buf);
pushVirtualCanvasToDisplay(COLOR_MAGENTA);
}
void countdownBegin(uint8_t seconds)
{
cd_current = seconds;
cd_lastTick = millis();
cd_running = (seconds > 0);
if (cd_running) countdown_drawFrame();
}
bool countdownIsRunning() { return cd_running; }
bool countdownUpdate()
{
if (!cd_running) return false;
if (millis() - cd_lastTick >= 1000)
{
cd_lastTick += 1000;
cd_current--;
if (cd_current == 0)
{
cd_running = false;
return true;
}
countdown_drawFrame();
}
return false;
}
// =====================================================================
// Non-blocking scrolling text, used for the welcome / kickoff /
// end-of-match messages: right-to-left, always green, drawn at text
// size 2 (the standard 5x7 font doubled = 2-LED strokes, letters 14 rows
// tall), vertically centered, with a 1-LED red border around the whole
// panel while it is showing. Written as a Begin/Update/IsRunning state
// machine (one step per call, no delay()) so it doesn't block the web
// server / audio / serial handling in the main loop.
// yPos/frameDelayMs/r/g/b are accepted for API compatibility with the
// caller but intentionally ignored.
//
// Spacing: the built-in font is fixed-width, which leaves big gaps around
// narrow letters (i, l, comma...) and a 12-LED-wide space. Instead each
// glyph's real width is measured and the letters are packed with exactly
// LETTER_GAP_PX LEDs between them; a space adds SPACE_ADVANCE_PX on top of
// the letter gap, so words end up WORD_GAP_PX (= 6) LEDs apart.
//
// Speed: one step every SCROLL_FRAME_MS (90 ms), each step moves 2 LEDs
// (one "big pixel" of the doubled font). Lower the number to speed the
// text up, raise it to slow it down.
//
// Chevrons: while a message is showing, rows of yellow 2-LED-thick
// chevrons stream out of the center along the top and bottom edges -
// "<" going left on the left half, ">" going right on the right half -
// one LED per step, appearing from the center seam and sliding off the
// outer edges.
// =====================================================================
static bool sc_running = false;
static int sc_x = 0;
static int sc_endX = 0;
static int sc_y = 0;
static unsigned long sc_lastFrame = 0;
static String sc_text;
static unsigned int sc_phase = 0; // chevron animation step counter
static const unsigned long SCROLL_FRAME_MS = 90; // one step per 90 ms (was 40, then 55, then 70 - slowed for easy reading)...
static const int SCROLL_STEP_PX = 2; // ...of 2 LEDs (text is drawn at 2x)
static const int SCROLL_TEXT_Y = 8; // capitals rows 8-21, descenders to row 23 -> whole text block rows 8-23, exactly centered
static const int LETTER_GAP_PX = 1; // LEDs between letters
static const int SPACE_ADVANCE_PX = 5; // + LETTER_GAP_PX = 6 LEDs between words
// Real (unspaced) pixel columns each character uses in the built-in 5x7
// font, measured once per character by drawing it into a scratch canvas.
static GFXcanvas1 glyphProbe(8, 8);
static int8_t glyphLeft[128]; // first used column (0-4)
static int8_t glyphCols[128]; // number of columns from first to last used
static bool glyphMeasured[128];
static void measureGlyph(unsigned char ch)
{
if (ch >= 128 || glyphMeasured[ch]) return;
glyphProbe.fillScreen(0);
glyphProbe.drawChar(0, 0, ch, 1, 1, 1); // bg == color -> transparent
int8_t left = -1, right = -1;
for (int8_t col = 0; col < 6; col++)
for (int8_t row = 0; row < 8; row++)
if (glyphProbe.getPixel(col, row))
{
if (left < 0) left = col;
right = col;
}
glyphLeft[ch] = (left < 0) ? 0 : left;
glyphCols[ch] = (left < 0) ? 0 : (right - left + 1);
glyphMeasured[ch] = true;
}
// Anything outside plain printable ASCII (custom messages can contain
// whatever was typed) is drawn as '?' so the glyph tables are never
// indexed out of range.
static unsigned char printableChar(unsigned char ch)
{
return (ch >= 32 && ch <= 126) ? ch : '?';
}
static int scrollTextWidth(const String &t)
{
int w = 0;
for (unsigned i = 0; i < t.length(); i++)
{
unsigned char ch = printableChar((unsigned char)t[i]);
if (ch == ' ') { w += SPACE_ADVANCE_PX; continue; }
measureGlyph(ch);
w += glyphCols[ch] * 2 + LETTER_GAP_PX;
}
return w;
}
// 1-LED red frame around the whole 32x32 panel (drawn last so it sits on
// top of the text as it slides in and out at the edges).
static void drawRedBorder()
{
plotColorRect(0, 0, 32, 1, COLOR_RED);
plotColorRect(0, 31, 32, 1, COLOR_RED);
plotColorRect(0, 1, 1, 30, COLOR_RED);
plotColorRect(31, 1, 1, 30, COLOR_RED);
}
// Yellow chevron streams along the top (rows 1-6) and bottom (rows 25-30)
// edges. Each chevron is 4 wide x 6 tall with 2-LED strokes; CHEVRON_PITCH
// LEDs apart. Left chevrons are clipped to cols <= 15 and right ones to
// cols >= 16, so they seem to come out of the center line.
static const int CHEVRON_PITCH = 6;
static const char *const chevronLeft[6] = { "..##", ".##.", "##..", "##..", ".##.", "..##" };
static void drawChevronStream(int topY)
{
int offset = sc_phase % CHEVRON_PITCH;
for (int o = offset; o < 16 + 4; o += CHEVRON_PITCH)
{
int xLeft = 16 - o; // glyph's left column; starts hidden right of the seam, slides left
for (int r = 0; r < 6; r++)
for (int col = 0; col < 4; col++)
{
if (chevronLeft[r][col] != '#') continue;
int xl = xLeft + col; // left-going "<"
if (xl >= 0 && xl <= 15)
plotColorPixel(xl, topY + r, COLOR_YELLOW);
int xr = 31 - xl; // mirror image: right-going ">"
if (xr >= 16 && xr <= 31)
plotColorPixel(xr, topY + r, COLOR_YELLOW);
}
}
}
static void scroll_drawFrame()
{
vCanvas.fillScreen(0);
int x = sc_x;
for (unsigned i = 0; i < sc_text.length() && x < vcanvas_w; i++)
{
unsigned char ch = printableChar((unsigned char)sc_text[i]);
if (ch == ' ') { x += SPACE_ADVANCE_PX; continue; }
measureGlyph(ch);
int gw = glyphCols[ch] * 2;
if (x + gw > 0) // skip letters that are entirely off the left edge
vCanvas.drawChar(x - glyphLeft[ch] * 2, sc_y, ch, 1, 1, 2); // bg == color -> transparent
x += gw + LETTER_GAP_PX;
}
pushVirtualCanvasToDisplay(COLOR_GREEN);
drawChevronStream(1);
drawChevronStream(25);
drawRedBorder();
}
void scrollTextBegin(uint8_t yPos, unsigned long frameDelayMs, const String &text,
uint8_t r, uint8_t g, uint8_t b)
{
(void)yPos; (void)frameDelayMs; (void)r; (void)g; (void)b; // see note above - all ignored on purpose
sc_text = text;
sc_y = SCROLL_TEXT_Y;
sc_x = vcanvas_w;
sc_endX = -scrollTextWidth(text);
sc_lastFrame = millis();
sc_phase = 0;
sc_running = true;
scroll_drawFrame();
}
bool scrollTextIsRunning() { return sc_running; }
bool scrollTextUpdate()
{
if (!sc_running) return false;
unsigned long now = millis();
if (now - sc_lastFrame >= SCROLL_FRAME_MS)
{
sc_lastFrame = now;
sc_x -= SCROLL_STEP_PX;
sc_phase++;
if (sc_x <= sc_endX)
{
sc_running = false;
return true;
}
scroll_drawFrame();
}
return false;
}
// =====================================================================
// Small color-aware 7-segment digit (7x16), for the scoreboard's score
// readout - ported from the flag-scoreboard sketch, unchanged.
// =====================================================================
static const uint8_t sevenSegPatterns[10] = {
0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F
};
static void drawSevenSegDigitColor(int16_t x0, int16_t y0, uint8_t digit, uint16_t color)
{
if (digit > 9) return;
// The "1" uses the same shape as the big clock's 1 (little flag at the
// top-left, straight stem, foot at the bottom) drawn with 2-LED strokes,
// instead of the plain two-bar seven-segment 1.
if (digit == 1)
{
static const char *const one[16] = {
"...##..",
"..###..",
".####..",
"...##..", "...##..", "...##..", "...##..", "...##..", "...##..",
"...##..", "...##..", "...##..", "...##..", "...##..",
".#####.",
".#####."
};
for (uint8_t r = 0; r < 16; r++)
for (uint8_t col = 0; col < 7; col++)
if (one[r][col] == '#') plotColorPixel(x0 + col, y0 + r, color);
return;
}
uint8_t seg = sevenSegPatterns[digit];
bool hasA = seg & 0x01, hasB = seg & 0x02, hasC = seg & 0x04, hasD = seg & 0x08;
bool hasE = seg & 0x10, hasF = seg & 0x20, hasG = seg & 0x40;
int upperY = hasA ? (y0 + 2) : y0;
int upperH = hasA ? 5 : 7;
int lowerY = y0 + 9;
int lowerH = hasD ? 5 : 7;
if (hasA) plotColorRect(x0, y0, 7, 2, color);
if (hasF) plotColorRect(x0, upperY, 2, upperH, color);
if (hasB) plotColorRect(x0 + 5, upperY, 2, upperH, color);
if (hasG) plotColorRect(x0, y0 + 7, 7, 2, color);
if (hasE) plotColorRect(x0, lowerY, 2, lowerH, color);
if (hasC) plotColorRect(x0 + 5, lowerY, 2, lowerH, color);
if (hasD) plotColorRect(x0, y0 + 14, 7, 2, color);
if (!hasG)
{
if (hasB && hasC) plotColorRect(x0 + 5, y0 + 7, 2, 2, color);
if (hasF && hasE) plotColorRect(x0, y0 + 7, 2, 2, color);
}
// Chamfer: shave 1 LED off each of the 4 outer corners for a clean,
// non-blocky look - same treatment used on the other digit fonts.
plotColorPixel(x0, y0, COLOR_BLACK);
plotColorPixel(x0 + 6, y0, COLOR_BLACK);
plotColorPixel(x0, y0 + 15, COLOR_BLACK);
plotColorPixel(x0 + 6, y0 + 15, COLOR_BLACK);
}
// One or two digits (score 0-99), centered horizontally inside the free
// columns [areaLeftX .. areaRightX] of one panel half, and sitting at the
// bottom of the canvas with 1 blank LED row underneath (digit rows 15-30).
static void drawScoreDigits(int areaLeftX, int areaRightX, uint8_t score, uint16_t color)
{
const int digitY = 15; // 16-row digit -> rows 15..30, row 31 left blank
if (score > 99) score = 99;
int areaW = areaRightX - areaLeftX + 1;
if (score < 10)
{
int x0 = areaLeftX + (areaW - 7) / 2;
drawSevenSegDigitColor(x0, digitY, score, color);
}
else
{
int x0 = areaLeftX + (areaW - 14) / 2;
drawSevenSegDigitColor(x0, digitY, (score / 10) % 10, color);
drawSevenSegDigitColor(x0 + 7, digitY, score % 10, color);
}
}
// =====================================================================
// Scoreboard: flags/poles/star exactly as our existing design/colors,
// now driven by real scores. Self-throttled to redraw every ~150ms so
// it's safe to call every loop() iteration, same pattern the original
// single-panel drawScoreboard() used.
// =====================================================================
static void drawWavyFlag(int x0, int y0, int flagW, int flagH, int attach_col, int star_dx, int star_dy,
float phase, uint16_t bodyColor, uint16_t starColor)
{
for (int col = 0; col < flagW; col++)
{
int shift = (col == attach_col) ? 0 : (int)round(sin((col * 0.9) + phase));
plotColorRect(x0 + col, y0 + shift, 1, flagH, bodyColor);
}
int cx = x0 + star_dx, cy = y0 + star_dy;
plotColorPixel(cx, cy - 1, starColor);
plotColorPixel(cx - 1, cy, starColor);
plotColorPixel(cx, cy, starColor);
plotColorPixel(cx + 1, cy, starColor);
plotColorPixel(cx, cy + 1, starColor);
}
void drawScoreboard(uint8_t redScore, uint8_t greenScore, uint16_t scoreColor)
{
(void)scoreColor; // score digits are always red now, regardless of what's passed in - see below
static unsigned long lastFrameMs = 0;
static float wavePhase = 0;
unsigned long now = millis();
if (now - lastFrameMs < 150) return;
lastFrameMs = now;
wavePhase += 0.6;
// BUG FIX: this was never clearing before redrawing, so as the flag's
// wave animation shifted pixels frame to frame, the previous frame's
// pixels never turned off - they just kept accumulating, which is
// what was causing the messy/flickery buildup.
display.clearDisplay();
const int poleTop = 1;
const int poleBottom = 31;
const int poleHeight = poleBottom - poleTop + 1;
const int flagW = 11, flagH = 9;
plotColorRect(0, poleTop, 1, poleHeight, COLOR_GREEN);
drawWavyFlag(1, poleTop, flagW, flagH, /*attach_col=*/0, /*star_dx=*/5, /*star_dy=*/3, wavePhase, COLOR_RED, COLOR_GREEN);
drawWavyFlag(20, poleTop, flagW, flagH, /*attach_col=*/flagW - 1, /*star_dx=*/5, /*star_dy=*/3, wavePhase, COLOR_RED, COLOR_GREEN);
plotColorRect(31, poleTop, 1, poleHeight, COLOR_GREEN);
// Red team's score on the left panel, green team's on the right -
// centered in each half, along the bottom of the board.
// Always red, per request - the scoreColor argument is intentionally
// ignored now (kept in the signature for API compatibility).
drawScoreDigits(1, 15, redScore, COLOR_RED); // left half: between pole (col 0) and centre marks (col 15)
drawScoreDigits(17, 30, greenScore, COLOR_RED); // right half: between centre marks (col 16) and pole (col 31)
plotColorRect(15, 20, 2, 2, COLOR_GREEN);
plotColorRect(15, 26, 2, 2, COLOR_GREEN);
}
// =====================================================================
// Match timer / full-time flash - the bigger clock-style font (13x14),
// minutes on top, seconds on bottom, colon between, ported from the
// same clock-digit work already validated on this rig.
// =====================================================================
static const uint8_t clockSevenSegPatterns[10] = {
0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F
};
static const char* const clockDigitBitmap1[14] = {
"......###....", ".....####....", "....#####....", "......###....",
"......###....", "......###....", "......###....", "......###....",
"......###....", "......###....", "......###....", "......###....",
"....######...", "....######..."
};
static void drawClockDigitBitmap1(GFXcanvas1 &canvas, int x0, int y0)
{
for (int row = 0; row < 14; row++)
for (int col = 0; col < 13; col++)
if (clockDigitBitmap1[row][col] == '#') canvas.drawPixel(x0 + col, y0 + row, 1);
}
static const char* const clockDigitBitmap7[14] = {
".############", ".############", "..........###", ".........###.",
"........###..", ".......###...", "......###....", ".....###.....",
"....###......", "....###......", "....###......", "....###......",
"....###......", "....###......"
};
static void drawClockDigitBitmap7(GFXcanvas1 &canvas, int x0, int y0)
{
for (int row = 0; row < 14; row++)
for (int col = 0; col < 13; col++)
if (clockDigitBitmap7[row][col] == '#') canvas.drawPixel(x0 + col, y0 + row, 1);
}
static void drawClockSevenSegDigit(GFXcanvas1 &canvas, int x0, int y0, uint8_t digit)
{
if (digit > 9) return;
uint8_t seg = clockSevenSegPatterns[digit];
bool hasA = seg & 0x01, hasB = seg & 0x02, hasC = seg & 0x04, hasD = seg & 0x08;
bool hasE = seg & 0x10, hasF = seg & 0x20, hasG = seg & 0x40;
int upperY = hasA ? (y0 + 2) : y0;
int upperH = hasA ? 4 : 6;
int lowerY = y0 + 8;
int lowerH = hasD ? 4 : 6;
if (hasA) canvas.fillRect(x0, y0, 13, 2, 1);
if (hasF) canvas.fillRect(x0, upperY, 3, upperH, 1);
if (hasB) canvas.fillRect(x0 + 10, upperY, 3, upperH, 1);
if (hasG) canvas.fillRect(x0, y0 + 6, 13, 2, 1);
if (hasE) canvas.fillRect(x0, lowerY, 3, lowerH, 1);
if (hasC) canvas.fillRect(x0 + 10, lowerY, 3, lowerH, 1);
if (hasD) canvas.fillRect(x0, y0 + 12, 13, 2, 1);
if (!hasG)
{
if (hasB && hasC) canvas.fillRect(x0 + 10, y0 + 6, 3, 2, 1);
if (hasF && hasE) canvas.fillRect(x0, y0 + 6, 3, 2, 1);
}
if (hasA && hasF) canvas.drawPixel(x0, y0, 0);
if (hasA && hasB) canvas.drawPixel(x0 + 12, y0, 0);
if (hasD && hasE) canvas.drawPixel(x0, y0 + 13, 0);
if (hasD && hasC) canvas.drawPixel(x0 + 12, y0 + 13, 0);
if (hasG && !hasF) canvas.drawPixel(x0, y0 + 6, 0);
if (hasG && !hasB) canvas.drawPixel(x0 + 12, y0 + 6, 0);
if (hasG && !hasE) canvas.drawPixel(x0, y0 + 7, 0);
if (hasG && !hasC) canvas.drawPixel(x0 + 12, y0 + 7, 0);
}
static void drawClockDigit(GFXcanvas1 &canvas, int x0, int y0, uint8_t digit)
{
if (digit == 1) drawClockDigitBitmap1(canvas, x0, y0);
else if (digit == 7) drawClockDigitBitmap7(canvas, x0, y0);
else drawClockSevenSegDigit(canvas, x0, y0, digit);
}
static void showClockBigNumber(int minutes, int seconds)
{
minutes = minutes % 100;
seconds = seconds % 100;
uint8_t d[4] = {
(uint8_t)((minutes / 10) % 10), (uint8_t)(minutes % 10),
(uint8_t)((seconds / 10) % 10), (uint8_t)(seconds % 10)
};
const int digit_h = 14;
int row1_y = (16 - digit_h) / 2;
int row2_y = 16 + (16 - digit_h) / 2;
vCanvas.fillScreen(0);
vCanvasColon.fillScreen(0);
drawClockDigit(vCanvas, 0, row1_y, d[0]);
drawClockDigit(vCanvas, 19, row1_y, d[1]);
drawClockDigit(vCanvas, 0, row2_y, d[2]);
drawClockDigit(vCanvas, 19, row2_y, d[3]);
vCanvasColon.fillRect(13, 15, 2, 2, 1);
vCanvasColon.fillRect(17, 15, 2, 2, 1);
// Digit colors by position: minutes-tens red, minutes-ones yellow,
// seconds-tens yellow, seconds-ones red; colon dots green.
pushClockToDisplay(COLOR_RED, COLOR_YELLOW, COLOR_YELLOW, COLOR_RED, COLOR_GREEN);
}
void drawMatchTimer(uint16_t elapsedSeconds)
{
showClockBigNumber(elapsedSeconds / 60, elapsedSeconds % 60);
}
void drawFullTimeFlash(uint16_t finalElapsedSeconds)
{
showClockBigNumber(finalElapsedSeconds / 60, finalElapsedSeconds % 60);
}
// =====================================================================
// Referee screen - "REF" in green, sized to fill the panel exactly:
// each pixel of the built-in 5x7 font becomes 2 LEDs wide x 3 LEDs tall,
// so every letter is 10 x 21 LEDs and 3 letters + 2 one-LED gaps = 32
// columns. The glyph shapes are read from the built-in font through the
// scratch canvas used by the scroller (glyphProbe).
// =====================================================================
void drawReferee()
{
vCanvas.fillScreen(0);
const char *word = "REF";
const int cellW = 2, cellH = 3; // LEDs per font pixel
const int letterW = 5 * cellW; // 10
const int gap = 1;
const int total = 3 * letterW + 2 * gap; // 32
const int x0 = (vcanvas_w - total) / 2;
const int y0 = (vcanvas_h - 7 * cellH) / 2; // rows 5..25
for (int i = 0; i < 3; i++)
{
glyphProbe.fillScreen(0);
glyphProbe.drawChar(0, 0, (unsigned char)word[i], 1, 1, 1);
for (int col = 0; col < 5; col++)
for (int row = 0; row < 7; row++)
if (glyphProbe.getPixel(col, row))
vCanvas.fillRect(x0 + i * (letterW + gap) + col * cellW, y0 + row * cellH, cellW, cellH, 1);
}
pushVirtualCanvasToDisplay(COLOR_GREEN);
}
// =====================================================================
// "WAITING FOR START" - static, three centered lines with 1-LED-thick
// strokes (letters 6 rows tall), one color per line - WAITING red, FOR
// green, START yellow - inside a 1-LED red border around the whole panel.
// Hand-drawn glyphs plotted through plotColorPixel() so they use the same
// rotation / two-panel mapping as everything else.
// =====================================================================
struct ThinGlyph
{
char ch;
uint8_t w;
const char *rows[6];
};
static const ThinGlyph thinGlyphs[] = {
{ 'W', 5, { "#...#", "#...#", "#...#", "#.#.#", "#.#.#", ".#.#." } },
{ 'A', 3, { ".#.", "#.#", "#.#", "###", "#.#", "#.#" } },
{ 'I', 1, { "#", "#", "#", "#", "#", "#" } },
{ 'T', 3, { "###", ".#.", ".#.", ".#.", ".#.", ".#." } },
{ 'N', 4, { "#..#", "##.#", "##.#", "#.##", "#.##", "#..#" } },
{ 'G', 3, { ".##", "#..", "#..", "#.#", "#.#", ".##" } },
{ 'F', 3, { "###", "#..", "##.", "#..", "#..", "#.." } },
{ 'O', 3, { ".#.", "#.#", "#.#", "#.#", "#.#", ".#." } },
{ 'R', 3, { "##.", "#.#", "#.#", "##.", "#.#", "#.#" } },
{ 'S', 3, { ".##", "#..", "#..", ".#.", "..#", "##." } },
};
static const ThinGlyph *findThinGlyph(char ch)
{
for (uint8_t i = 0; i < sizeof(thinGlyphs) / sizeof(thinGlyphs[0]); i++)
if (thinGlyphs[i].ch == ch) return &thinGlyphs[i];
return nullptr;
}
// One word centered horizontally with its top row at `top`, 1 LED between letters.
static void drawThinLine(const char *word, int16_t top, uint16_t color)
{
uint8_t n = strlen(word);
int16_t total = 0;
for (uint8_t i = 0; i < n; i++)
{
const ThinGlyph *g = findThinGlyph(word[i]);
if (g) total += g->w + (i < n - 1 ? 1 : 0);
}
int16_t x = (vcanvas_w - total) / 2;
for (uint8_t i = 0; i < n; i++)
{
const ThinGlyph *g = findThinGlyph(word[i]);
if (!g) continue;
for (uint8_t r = 0; r < 6; r++)
for (uint8_t col = 0; col < g->w; col++)
if (g->rows[r][col] == '#') plotColorPixel(x + col, top + r, color);
x += g->w + 1;
}
}
void drawWaitingForStart()
{
display.clearDisplay();
// Inside the 1-LED border (rows 1-30): three 6-row lines with 3 blank
// rows above, between and below them -> tops at rows 4 / 13 / 22.
drawThinLine("WAITING", 4, COLOR_RED);
drawThinLine("FOR", 13, COLOR_GREEN);
drawThinLine("START", 22, COLOR_YELLOW);
drawRedBorder();
}
// =====================================================================
// Goal celebration - static two-half layout, refreshed ~7x/second only so
// the flag keeps waving.
//
// GREEN team scores: left half = flag (pole on the left edge) + the
// new score; right half = GOAL / FOR / GREEN / TEAM
// RED team scores: mirror image - text on the left half, flag (pole
// on the right edge) + new score on the right half
//
// Text is yellow, 1-LED strokes (3x6 letters, 4 wide for M/N). The flag
// half is drawn exactly like the scoreboard's: same flag (red, green
// star), same green pole, same position next to the pole, red score.
// =====================================================================
struct SmallGlyph
{
char ch;
uint8_t w;
const char *rows[6];
};
static const SmallGlyph goalGlyphs[] = {
{ 'G', 3, { ".##", "#..", "#..", "#.#", "#.#", ".##" } },
{ 'O', 3, { ".#.", "#.#", "#.#", "#.#", "#.#", ".#." } },
{ 'A', 3, { ".#.", "#.#", "#.#", "###", "#.#", "#.#" } },
{ 'L', 3, { "#..", "#..", "#..", "#..", "#..", "###" } },
{ 'F', 3, { "###", "#..", "##.", "#..", "#..", "#.." } },
{ 'R', 3, { "##.", "#.#", "#.#", "##.", "#.#", "#.#" } },
{ 'E', 3, { "###", "#..", "##.", "#..", "#..", "###" } },
// 'e' = 2-wide E, used only inside "GREEN" to keep that word as narrow as possible.
{ 'e', 2, { "##", "#.", "##", "#.", "#.", "##" } },
{ 'D', 3, { "##.", "#.#", "#.#", "#.#", "#.#", "##." } },
{ 'T', 3, { "###", ".#.", ".#.", ".#.", ".#.", ".#." } },
{ 'N', 4, { "#..#", "##.#", "##.#", "#.##", "#.##", "#..#" } },
{ 'M', 4, { "#..#", "####", "####", "#..#", "#..#", "#..#" } },
};
static const SmallGlyph *findGoalGlyph(char ch)
{
for (uint8_t i = 0; i < sizeof(goalGlyphs) / sizeof(goalGlyphs[0]); i++)
if (goalGlyphs[i].ch == ch) return &goalGlyphs[i];
return nullptr;
}
// One word, centered inside the 16-column half starting at halfLeftX,
// 1 LED between letters. "GREEN" is 18 wide (even with the 2-wide 'e's),
// so a word wider than 16 is pushed back to end at the half's far edge:
// it then starts at col 14, which is empty on the green screen (the flag
// and one-digit scores stay within cols 4-11). Only "GREEN" does this,
// and it is only ever drawn on the right half.
static void drawGoalWord(const char *word, int16_t halfLeftX, int16_t top, uint16_t color)
{
uint8_t n = strlen(word);
int16_t total = 0;
for (uint8_t i = 0; i < n; i++)
{
const SmallGlyph *g = findGoalGlyph(word[i]);
if (g) total += g->w + (i < n - 1 ? 1 : 0);
}
int16_t x = (total > 16) ? (halfLeftX + 16 - total)
: (halfLeftX + (16 - total) / 2);
for (uint8_t i = 0; i < n; i++)
{
const SmallGlyph *g = findGoalGlyph(word[i]);
if (!g) continue;
for (uint8_t r = 0; r < 6; r++)
for (uint8_t col = 0; col < g->w; col++)
if (g->rows[r][col] == '#') plotColorPixel(x + col, top + r, color);
x += g->w + 1;
}
}
static uint8_t gc_team = 0; // 0 = red, 1 = green
static uint8_t gc_score = 0;
static float gc_phase = 0;
static unsigned long gc_lastFrame = 0;
static void goalCeleb_drawFrame()
{
const bool green = (gc_team == 1);
display.clearDisplay();
// ---- Text half (opposite side from the flag) ----
const int16_t textHalfX = green ? 16 : 0;
drawGoalWord("GOAL", textHalfX, 1, COLOR_YELLOW);
drawGoalWord("FOR", textHalfX, 9, COLOR_YELLOW);
drawGoalWord(green ? "GReeN" : "RED", textHalfX, 17, COLOR_YELLOW);
drawGoalWord("TEAM", textHalfX, 25, COLOR_YELLOW);
// ---- Flag half: same calls as drawScoreboard() uses for its left
// flag (green team) / right flag (red team) ----
const int flagW = 11, flagH = 9;
if (green)
{
plotColorRect(0, 1, 1, 31, COLOR_GREEN);
drawWavyFlag(1, 1, flagW, flagH, /*attach_col=*/0, /*star_dx=*/5, /*star_dy=*/3,
gc_phase, COLOR_RED, COLOR_GREEN);
drawScoreDigits(1, 15, gc_score, COLOR_RED);
}
else
{
drawWavyFlag(20, 1, flagW, flagH, /*attach_col=*/flagW - 1, /*star_dx=*/5, /*star_dy=*/3,
gc_phase, COLOR_RED, COLOR_GREEN);
plotColorRect(31, 1, 1, 31, COLOR_GREEN);
drawScoreDigits(17, 30, gc_score, COLOR_RED);
}
}
void goalCelebBegin(uint8_t team, uint8_t score)
{
gc_team = team;
gc_score = score;
gc_phase = 0;
gc_lastFrame = millis();
goalCeleb_drawFrame();
}
// Pass the team's current score every call so an edit made during the
// celebration shows up; the panel is only redrawn every ~150 ms.
void goalCelebUpdate(uint8_t score)
{
gc_score = score;
unsigned long now = millis();
if (now - gc_lastFrame < 150) return;
gc_lastFrame = now;
gc_phase += 0.6;
goalCeleb_drawFrame();
}
#ifndef DISPLAY_H
#define DISPLAY_H
#include <Arduino.h>
// Must be defined BEFORE PxMatrix.h is included anywhere in the build -
// two chained panels shift out more data per refresh than one, and
// without slowing the SPI clock down the panel shows exactly the kind
// of scrambled/flickering corruption seen in testing. This was in the
// original single-file two-panel sketch and got dropped when the
// project was split into these separate files - restoring it here.
#define PxMATRIX_SPI_FREQUENCY 10000000
#include <PxMatrix.h>
// =====================================================================
// PANEL CONFIGURATION - TWO-PANEL VERSION
// Pins/dimensions match the two-panel rig (64x16 physical, chained as
// one 32x16-per-panel strip, remapped through a 32x32 virtual canvas so
// content can be taller than either panel alone). See Display.cpp for
// the rotation/split mechanics - nothing here or in GameState/WebServer/
// AudioPlayer/the main .ino needs to know about that; this header's
// public API is UNCHANGED from the single-panel version on purpose.
// =====================================================================
#ifdef ESP8266
#define P_LAT 16 // D0
#define P_A 5 // D1
#define P_B 4 // D2
#define P_C 15 // D8 - used now (1/4 scan on a 32x16 panel, two chained)
#define P_OE 2 // D4
// Data/Clock (D7/D5) are driven by hardware SPI inside PxMatrix - not
// named here, same as before.
#endif
#define MATRIX_WIDTH 64 // physical, both panels chained
#define MATRIX_HEIGHT 16 // physical
extern PxMATRIX display;
// Standard colors (same palette as before)
extern uint16_t COLOR_RED;
extern uint16_t COLOR_GREEN;
extern uint16_t COLOR_BLUE;
extern uint16_t COLOR_WHITE;
extern uint16_t COLOR_YELLOW;
extern uint16_t COLOR_CYAN;
extern uint16_t COLOR_MAGENTA;
extern uint16_t COLOR_BLACK;
// ---------------------------------------------------------------------
// Call once from setup()
// ---------------------------------------------------------------------
void displayInit();
// ---------------------------------------------------------------------
// Each animation below follows the same non-blocking pattern as before:
// xxxBegin(...) -> starts (or restarts) the animation
// xxxUpdate() -> call every loop() iteration; advances one frame
// if enough time has passed. Returns true on the
// exact frame the animation finishes, false otherwise.
// xxxIsRunning() -> true while the animation is still in progress
//
// None of these ever call delay() - required since main.ino also has to
// keep servicing the web server and audio every iteration.
// ---------------------------------------------------------------------
// ---- Non-blocking countdown timer (counts down N, N-1, ... 1) ----
void countdownBegin(uint8_t seconds);
bool countdownIsRunning();
bool countdownUpdate();
// NOTE: flagsBegin()/flagsUpdate()/flagsIsRunning() from the single-panel
// version are gone - the main .ino never called them (dead code), and
// drawScoreboard() below already has its own flag animation built in.
// ---- Non-blocking scrolling text ----
void scrollTextBegin(uint8_t yPos, unsigned long frameDelayMs, const String &text,
uint8_t r, uint8_t g, uint8_t b);
bool scrollTextIsRunning();
bool scrollTextUpdate();
// mm:ss timer readout
void drawMatchTimer(uint16_t elapsedSeconds);
// Scoreboard: our flag/pole design (unchanged colors/look), now driven by
// real scores - red team's score on the left digit(s), green team's on
// the right. scoreColor tints the SCORE DIGITS only (matching the
// waiting-loop blue vs. other states, per the original spec); the flags/
// poles/star keep their own fixed colors regardless of scoreColor.
void drawScoreboard(uint8_t redScore, uint8_t greenScore, uint16_t scoreColor);
// "REF" in green, sized to fill the panel.
void drawReferee();
// "WAITING / FOR / START" on three centered lines, drawn all at once
// (no scrolling), 1-LED-thick letters: WAITING red, FOR green, START
// yellow, inside a 1-LED red border. One-shot draw - the panel refresh ISR
// keeps it lit until something else draws.
void drawWaitingForStart();
// ---- Goal celebration (static layout, flag keeps waving) ----
// team: 0 = red, 1 = green (matches GameState.h's Team enum numbering).
// Green team: flag + new score on the left half, "GOAL FOR GREEN TEAM"
// (yellow, stacked) on the right. Red team: mirrored.
// Pass the scoring team's CURRENT score to both calls.
void goalCelebBegin(uint8_t team, uint8_t score);
void goalCelebUpdate(uint8_t score);
// Flashing final time - caller decides on/off by calling this only during
// the "on" half of the flash cycle, and calling display.clearDisplay()
// directly during the "off" half (unchanged from before).
void drawFullTimeFlash(uint16_t finalElapsedSeconds);
#endif