#include #include #include #include #include #include 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 pixels = std::vector(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(c), 0}; body.setCursor(0, 0); body.print(text); const auto& g = mesh::ui::squeezed6Glyphs[c - 32]; std::vector 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); }