133 lines
5.6 KiB
C++
133 lines
5.6 KiB
C++
#include <cassert>
|
|
#include <cstring>
|
|
#include <vector>
|
|
#include <helpers/ui/ST7735Display.h>
|
|
#include <helpers/ui/SmallMessageText.h>
|
|
#include <helpers/ui/CompanionHomeLayout.h>
|
|
|
|
ColorVal UIColor::window_bkg = 0, UIColor::title_bkg = 0, UIColor::title_txt = 1;
|
|
ColorVal UIColor::primary_txt = 1, UIColor::secondary_txt = 1, UIColor::warning_txt = 1;
|
|
ColorVal UIColor::popup_bkg = 0, UIColor::popup_txt = 1, UIColor::corp_blue = 1;
|
|
|
|
struct Canvas {
|
|
std::vector<int> pixels = std::vector<int>(160 * 80);
|
|
int x = 0, y = 0, size = 1, prints = 0;
|
|
void setCursor(int xx, int yy) { x = xx; y = yy; }
|
|
void setTextSize(int s) { size = s; }
|
|
void setTextColor(int) {}
|
|
void print(const char*) { ++prints; }
|
|
int textWidth(const char* s) { return std::strlen(s) * 6 * size; }
|
|
int fontHeight() { return 8 * size; }
|
|
void fillRect(int xx, int yy, int w, int h, int) {
|
|
assert(xx >= 0 && yy >= 0 && xx + w <= 160 && yy + h <= 80);
|
|
for (int row = yy; row < yy + h; ++row)
|
|
for (int col = xx; col < xx + w; ++col) ++pixels[row * 160 + col];
|
|
}
|
|
void drawRect(int xx, int yy, int w, int h, int c) { fillRect(xx, yy, w, h, c); }
|
|
void drawBitmap(int xx, int yy, const uint8_t*, int w, int h, int c) {
|
|
fillRect(xx, yy, w, h, c);
|
|
}
|
|
} canvas;
|
|
static Canvas* sprite = &canvas;
|
|
static int curr_color = 1;
|
|
static bool spriteReady() { return true; }
|
|
|
|
// Only hardware initialization and SPI transmission are doubled. The runner
|
|
// appends the real driver's coordinate/text drawing methods verbatim below.
|
|
bool ST7735Display::begin() { return true; }
|
|
void ST7735Display::turnOn() {}
|
|
void ST7735Display::turnOff() {}
|
|
void ST7735Display::clear() { canvas.pixels.assign(160 * 80, 0); }
|
|
void ST7735Display::startFrame(ColorVal) { clear(); }
|
|
void ST7735Display::endFrame() {}
|
|
|
|
int main() {
|
|
ST7735Display display;
|
|
assert(display.width() == 160 && display.height() == 80);
|
|
display.setCursor(159, 79);
|
|
assert(canvas.x == 159 && canvas.y == 79);
|
|
assert(display.getTextWidth("MMMM") == 24);
|
|
assert(display.textLineHeight() == 8);
|
|
display.setTextSize(2);
|
|
assert(display.textLineHeight() == 16);
|
|
display.setTextSize(1);
|
|
assert(!display.useSmallMessageFont());
|
|
// Every logical pixel covers its own exact physical cell, without gaps or
|
|
// overlap. In native mode each of these cells must be exactly one pixel.
|
|
display.clear();
|
|
for (int y = 0; y < display.height(); ++y)
|
|
for (int x = 0; x < display.width(); ++x) display.fillRect(x, y, 1, 1);
|
|
for (int count : canvas.pixels) assert(count == 1);
|
|
display.clear();
|
|
display.drawRect(0, 0, display.width(), display.height());
|
|
for (int count : canvas.pixels) assert(count == 1);
|
|
|
|
mesh::ui::SmallMessageText body(display);
|
|
assert(body.capitalHeight() == 6);
|
|
// The repeater's T096 status column uses the driver's normal 6x8 font.
|
|
const char* status_rows[] = {"GPS:OFF", "FEM:OFF", "RXB:OFF", "RXPS:OFF", "CAD:OFF"};
|
|
for (int i = 0; i < 5; ++i) {
|
|
assert(display.getTextWidth(status_rows[i]) <= 50);
|
|
display.clear();
|
|
display.drawTextRightAlign(158, 10 + (i < 4 ? i : 5) * 10, status_rows[i]);
|
|
assert(canvas.x >= 108 && canvas.x + display.getTextWidth(status_rows[i]) <= 158);
|
|
}
|
|
const char* noise_floor_rows[] = {"N1:-123.4", "N2:-123.4", "N1:4.2s", "N2:WAIT"};
|
|
for (const char* noise_floor : noise_floor_rows) {
|
|
// Decimal per-radio floors need 54px. The final T096 row has no wide
|
|
// left-column field, so it may use four pixels of its empty gutter.
|
|
assert(display.getTextWidth(noise_floor) <= 54);
|
|
display.clear();
|
|
display.drawTextRightAlign(158, 70, noise_floor);
|
|
assert(canvas.x >= 104 && canvas.x + display.getTextWidth(noise_floor) <= 158);
|
|
}
|
|
const char* radio_tags[] = {"R2:ON", "T2:ON"};
|
|
for (const char* tag : radio_tags) {
|
|
assert(display.getTextWidth(tag) <= 50);
|
|
display.clear();
|
|
display.drawTextRightAlign(158, 50, tag);
|
|
assert(canvas.x >= 108 && canvas.x + display.getTextWidth(tag) <= 158);
|
|
}
|
|
const char* dual_radio_rows[] = {"R1 F:2500.000 S:12", "T1 B:1625.00 C:8",
|
|
"R2 F:2500.000 S:12", "T2 B:1625.00 C:8"};
|
|
for (const char* row : dual_radio_rows) {
|
|
assert(display.getTextWidth(row) <= 108);
|
|
}
|
|
assert(display.getTextWidth("ID:ABCDEF") <= 108);
|
|
assert(display.getTextWidth("R2:RXTX") <= 108);
|
|
assert(display.getTextWidth("X:AUTO") <= 108);
|
|
// The name owns the full normal-font top row; radio/status columns start
|
|
// below it and cannot cause name wrapping.
|
|
canvas.prints = 0;
|
|
assert(display.getTextWidth("MercerWoodMesh T096 Node") <= 160);
|
|
display.drawTextEllipsized(0, 0, 160, "MercerWoodMesh T096 Node");
|
|
assert(canvas.prints == 1);
|
|
#if defined(HELTEC_T096)
|
|
assert(body.lineCount(12) == 8);
|
|
// Check all 95 real font glyphs against their native bitmap coordinates.
|
|
for (int c = 32; c <= 126; ++c) {
|
|
display.clear();
|
|
char text[] = {static_cast<char>(c), 0};
|
|
body.setCursor(0, 0);
|
|
body.print(text);
|
|
const auto& g = mesh::ui::squeezed6Glyphs[c - 32];
|
|
std::vector<int> expected(160 * 80);
|
|
for (int y = 0; y < g.height; ++y) {
|
|
for (int x = 0; x < g.width; ++x) {
|
|
int bit = y * g.width + x;
|
|
if (mesh::ui::squeezed6Bitmaps[g.bitmap_offset + bit / 8] & (0x80 >> (bit % 8)))
|
|
expected[(5 + g.y_offset + y) * 160 + g.x_offset + x] = 1;
|
|
}
|
|
}
|
|
assert(canvas.pixels == expected);
|
|
}
|
|
#endif
|
|
// A 160px message must use the panel's normal print path, not the compact
|
|
// wrapper's per-pixel 5/6px glyphs, even when the feature is compiled in.
|
|
display.clear();
|
|
canvas.prints = 0;
|
|
mesh::ui::drawSmallMessageBody(display, "Sender", "A normal-font message");
|
|
assert(canvas.prints >= 2);
|
|
for (int count : canvas.pixels) assert(count == 0);
|
|
}
|