chore: pre-release cleanup pass (Lemon-era naming, bot sender parsing dedup)

Renamed the vestigial Lemon/default font-switch naming (setLemonFont/
isLemonFont/drawLemonChar/lemonXAdvance/_use_lemon -> setSingleFont/
isSingleFont/drawGlyph/glyphXAdvance/_single_font) across DisplayDriver.h
and both concrete drivers -- both have been permanently single-font for
several commits, so the old names invited a future reader to think a
real switch still existed. Pure identifier rename, no logic changed.

Also extracted the byte-identical "SenderName: " prefix-splitting in
MyMeshBot.h's tryBotReplyChannel()/tryBotChannelCommand() into a shared
botChannelSenderSplit(), mirroring the existing botRoomSenderName().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-07-17 21:02:33 +02:00
parent fe1e0d29ce
commit 273fbfde7a
8 changed files with 61 additions and 55 deletions

View File

@@ -328,6 +328,11 @@ private:
// Resolves a room post's signed author prefix to a display name for {name}; // Resolves a room post's signed author prefix to a display name for {name};
// falls back to a generic label when the poster isn't a known contact. // falls back to a generic label when the poster isn't a known contact.
void botRoomSenderName(const uint8_t* sender_prefix, char* out, int out_len); void botRoomSenderName(const uint8_t* sender_prefix, char* out, int out_len);
// Splits a channel message's leading "SenderName: " text convention off its
// body, for the {name} placeholder and to match triggers/commands against
// the body only. `*msg_out` points into `text` (no copy); `sender_name`
// defaults to "someone" if there's no ": " separator.
void botChannelSenderSplit(const char* text, char* sender_name, int sender_name_len, const char** msg_out);
void writeOKFrame(); void writeOKFrame();
void writeErrFrame(uint8_t err_code); void writeErrFrame(uint8_t err_code);

View File

@@ -54,6 +54,17 @@ void MyMesh::botRoomSenderName(const uint8_t* sender_prefix, char* out, int out_
else snprintf(out, out_len, "someone"); else snprintf(out, out_len, "someone");
} }
void MyMesh::botChannelSenderSplit(const char* text, char* sender_name, int sender_name_len, const char** msg_out) {
*msg_out = text;
snprintf(sender_name, sender_name_len, "someone");
const char* sep = strstr(text, ": ");
if (sep) {
*msg_out = sep + 2;
int n = (int)(sep - text);
if (n > 0 && n < sender_name_len) { memcpy(sender_name, text, n); sender_name[n] = '\0'; }
}
}
// Per-contact DM throttle: true if enough time has passed (or we've never // Per-contact DM throttle: true if enough time has passed (or we've never
// replied) to this contact. Only the first 4 key bytes are compared — ample to // replied) to this contact. Only the first 4 key bytes are compared — ample to
// tell local contacts apart. // tell local contacts apart.
@@ -142,14 +153,9 @@ void MyMesh::tryBotReplyChannel(uint8_t channel_idx, const char* text, uint8_t h
// message body only, and so the name is available for the {name} // message body only, and so the name is available for the {name}
// placeholder. Unsigned/unverified — channel messages carry no identity // placeholder. Unsigned/unverified — channel messages carry no identity
// beyond this text convention. // beyond this text convention.
const char* msg = text; const char* msg;
char sender_name[32] = "someone"; char sender_name[32];
const char* sep = strstr(text, ": "); botChannelSenderSplit(text, sender_name, sizeof(sender_name), &msg);
if (sep) {
msg = sep + 2;
int n = (int)(sep - text);
if (n > 0 && n < (int)sizeof(sender_name)) { memcpy(sender_name, text, n); sender_name[n] = '\0'; }
}
if (!botTriggerMatches(_prefs.bot_trigger_ch, msg, true)) return; // own trigger; "*" = any msg if (!botTriggerMatches(_prefs.bot_trigger_ch, msg, true)) return; // own trigger; "*" = any msg
@@ -328,14 +334,9 @@ bool MyMesh::tryBotChannelCommand(uint8_t channel_idx, const char* text, uint8_t
return false; return false;
// Skip "sender_name: " prefix so commands are read from the message body only. // Skip "sender_name: " prefix so commands are read from the message body only.
const char* msg = text; const char* msg;
char sender_name[32] = "someone"; char sender_name[32];
const char* sep = strstr(text, ": "); botChannelSenderSplit(text, sender_name, sizeof(sender_name), &msg);
if (sep) {
msg = sep + 2;
int n = (int)(sep - text);
if (n > 0 && n < (int)sizeof(sender_name)) { memcpy(sender_name, text, n); sender_name[n] = '\0'; }
}
uint32_t ts = getRTCClock()->getCurrentTime(); uint32_t ts = getRTCClock()->getCurrentTime();
char out[BOT_SCRATCH]; char out[BOT_SCRATCH];

View File

@@ -443,7 +443,7 @@ class HomeScreen : public UIScreen {
const int lh = display.getLineHeight(); const int lh = display.getLineHeight();
const int cw = display.getCharWidth(); const int cw = display.getCharWidth();
const int ind = cw + 2; // single-char indicator width const int ind = cw + 2; // single-char indicator width
const int ind_h = display.isLemonFont() ? lh - 2 : lh; const int ind_h = display.isSingleFont() ? lh - 2 : lh;
const int ind_gap = display.isLandscape() ? 3 : 1; // gap between indicator boxes const int ind_gap = display.isLandscape() ? 3 : 1; // gap between indicator boxes
int battLeftX; int battLeftX;
@@ -2942,7 +2942,7 @@ void UITask::applyBrightness() {
void UITask::applyFont() { void UITask::applyFont() {
if (_display != NULL && _node_prefs != NULL) { if (_display != NULL && _node_prefs != NULL) {
_display->setLemonFont(_node_prefs->use_lemon_font != 0); _display->setSingleFont(_node_prefs->use_lemon_font != 0);
_next_refresh = 0; _next_refresh = 0;
} }
} }

View File

@@ -33,8 +33,8 @@ public:
virtual uint16_t getTextWidth(const char* str) = 0; virtual uint16_t getTextWidth(const char* str) = 0;
virtual int getCharWidth() const { return 6; } // typical character advance width (px) virtual int getCharWidth() const { return 6; } // typical character advance width (px)
virtual int getLineHeight() const { return 8; } // pixel rows per text line virtual int getLineHeight() const { return 8; } // pixel rows per text line
virtual void setLemonFont(bool) { } // no-op; overridden by displays that support Lemon virtual void setSingleFont(bool) { } // no-op; both concrete drivers are permanently pinned to their one font
virtual bool isLemonFont() const { return false; } virtual bool isSingleFont() const { return false; }
// Layout helpers — derived from font metrics and screen size. // Layout helpers — derived from font metrics and screen size.
// Use these instead of hardcoded pixel values so layouts adapt to any display. // Use these instead of hardcoded pixel values so layouts adapt to any display.
int lineStep() const { return getLineHeight() + 2; } // row pitch: text + gap int lineStep() const { return getLineHeight() + 2; } // row pitch: text + gap

View File

@@ -17,16 +17,16 @@
// GFX fonts use the baseline as the cursor origin. UI code assumes top-of-cell // GFX fonts use the baseline as the cursor origin. UI code assumes top-of-cell
// coordinates (same convention as the OLED driver). Add the font ascender so // coordinates (same convention as the OLED driver). Add the font ascender so
// the two conventions match. // the two conventions match.
static int fontAscender(int sz, bool use_lemon, int scale) { static int fontAscender(int sz, bool single_font, int scale) {
if (sz == 3) return 26; // FreeSans18pt7b: proportional, baseline origin if (sz == 3) return 26; // FreeSans18pt7b: proportional, baseline origin
if (sz == 1 && use_lemon) return 7 * scale; // misc-fixed 6x9 GFX font: baseline origin, ascent 7px×scale if (sz == 1 && single_font) return 7 * scale; // misc-fixed 6x9 GFX font: baseline origin, ascent 7px×scale
return 0; // GFX built-in font: cursor is top-left of cell return 0; // GFX built-in font: cursor is top-left of cell
} }
// y is the GFX baseline (display.getCursorY()), which equals original_y + 8*sc. // y is the GFX baseline (display.getCursorY()), which equals original_y + 8*sc.
// Pixels are placed at y + yo*sc + row*sc — identical to how GFX would render // Pixels are placed at y + yo*sc + row*sc — identical to how GFX would render
// a scaled GFX font, but bypassing GFX so multi-byte UTF-8 is decoded correctly. // a scaled GFX font, but bypassing GFX so multi-byte UTF-8 is decoded correctly.
int16_t GxEPDDisplay::drawLemonChar(int16_t x, int16_t y, uint32_t cp, int sc) { int16_t GxEPDDisplay::drawGlyph(int16_t x, int16_t y, uint32_t cp, int sc) {
for (uint8_t i = 0; i < lemonIconCount; i++) { for (uint8_t i = 0; i < lemonIconCount; i++) {
if (pgm_read_dword(&lemonIconCPs[i]) == cp) { if (pgm_read_dword(&lemonIconCPs[i]) == cp) {
const GFXglyph* g = &lemonIconGlyphs[i]; const GFXglyph* g = &lemonIconGlyphs[i];
@@ -69,7 +69,7 @@ int16_t GxEPDDisplay::drawLemonChar(int16_t x, int16_t y, uint32_t cp, int sc) {
return x + xa * sc; return x + xa * sc;
} }
uint8_t GxEPDDisplay::lemonXAdvance(uint32_t cp, int sc) { uint8_t GxEPDDisplay::glyphXAdvance(uint32_t cp, int sc) {
uint8_t xa; uint8_t xa;
if (cp < MiscFixed.first || cp > MiscFixed.last) xa = 6; if (cp < MiscFixed.first || cp > MiscFixed.last) xa = 6;
else xa = pgm_read_byte(&MiscFixedGlyphs[cp - MiscFixed.first].xAdvance); else xa = pgm_read_byte(&MiscFixedGlyphs[cp - MiscFixed.first].xAdvance);
@@ -127,7 +127,7 @@ void GxEPDDisplay::startFrame(Color bkg) {
display.setTextColor(_curr_color = GxEPD_BLACK); display.setTextColor(_curr_color = GxEPD_BLACK);
_text_sz = 1; _text_sz = 1;
int sc = scale(); int sc = scale();
display.setFont(_use_lemon ? &MiscFixed : NULL); display.setFont(_single_font ? &MiscFixed : NULL);
display.setTextSize(sc); display.setTextSize(sc);
display_crc.reset(); display_crc.reset();
} }
@@ -156,7 +156,7 @@ void GxEPDDisplay::setTextSize(int sz) {
display.setTextSize(scale() * 2); display.setTextSize(scale() * 2);
break; break;
default: default:
display.setFont(_use_lemon ? &MiscFixed : NULL); display.setFont(_single_font ? &MiscFixed : NULL);
display.setTextSize(sc); display.setTextSize(sc);
break; break;
} }
@@ -178,13 +178,13 @@ void GxEPDDisplay::setCursor(int x, int y) {
// Offset y by the font ascender: callers pass top-of-cell y, GFX fonts // Offset y by the font ascender: callers pass top-of-cell y, GFX fonts
// expect baseline y. Without this, text would be clipped at the top. // expect baseline y. Without this, text would be clipped at the top.
int sc = scale(); int sc = scale();
display.setCursor(x, y + fontAscender(_text_sz, _use_lemon, sc)); display.setCursor(x, y + fontAscender(_text_sz, _single_font, sc));
} }
void GxEPDDisplay::print(const char* str) { void GxEPDDisplay::print(const char* str) {
display_crc.update<char>(str, strlen(str)); display_crc.update<char>(str, strlen(str));
// misc-fixed path only for sz=1 — setTextSize(2/3) switches GFX to other fonts. // misc-fixed path only for sz=1 — setTextSize(2/3) switches GFX to other fonts.
if (_use_lemon && _text_sz == 1) { if (_single_font && _text_sz == 1) {
int16_t cx = display.getCursorX(); int16_t cx = display.getCursorX();
int16_t cy = display.getCursorY(); int16_t cy = display.getCursorY();
const int sc = scale(); const int sc = scale();
@@ -192,7 +192,7 @@ void GxEPDDisplay::print(const char* str) {
while (*p) { while (*p) {
uint32_t cp = decodeCodepoint(p); uint32_t cp = decodeCodepoint(p);
if (cp == '\n') { cy += MiscFixed.yAdvance * sc; cx = 0; } if (cp == '\n') { cy += MiscFixed.yAdvance * sc; cx = 0; }
else { cx = drawLemonChar(cx, cy, cp, sc); } else { cx = drawGlyph(cx, cy, cp, sc); }
} }
display.setCursor(cx, cy); display.setCursor(cx, cy);
return; return;
@@ -250,11 +250,11 @@ void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
} }
uint16_t GxEPDDisplay::getTextWidth(const char* str) { uint16_t GxEPDDisplay::getTextWidth(const char* str) {
if (_use_lemon && _text_sz == 1) { if (_single_font && _text_sz == 1) {
uint16_t total = 0; uint16_t total = 0;
const int sc = scale(); const int sc = scale();
const uint8_t* p = (const uint8_t*)str; const uint8_t* p = (const uint8_t*)str;
while (*p) total += lemonXAdvance(decodeCodepoint(p), sc); while (*p) total += glyphXAdvance(decodeCodepoint(p), sc);
return total; return total;
} }
display.setTextWrap(false); display.setTextWrap(false);

View File

@@ -57,7 +57,7 @@ class GxEPDDisplay : public DisplayDriver {
#endif #endif
bool _init = false; bool _init = false;
bool _isOn = false; bool _isOn = false;
bool _use_lemon = true; // e-ink is single-font too now (misc-fixed 6x9), matching the OLED driver bool _single_font = true; // e-ink is single-font too now (misc-fixed 6x9), matching the OLED driver
uint16_t _curr_color; uint16_t _curr_color;
CRC32 display_crc; CRC32 display_crc;
int last_display_crc_value = 0; int last_display_crc_value = 0;
@@ -65,8 +65,8 @@ class GxEPDDisplay : public DisplayDriver {
uint8_t _full_refresh_interval = 0; uint8_t _full_refresh_interval = 0;
uint8_t _partial_count = 0; uint8_t _partial_count = 0;
int16_t drawLemonChar(int16_t x, int16_t y, uint32_t cp, int sc); int16_t drawGlyph(int16_t x, int16_t y, uint32_t cp, int sc);
uint8_t lemonXAdvance(uint32_t cp, int sc); uint8_t glyphXAdvance(uint32_t cp, int sc);
int scale() const { return (width() >= height()) ? 2 : 1; } int scale() const { return (width() >= height()) ? 2 : 1; }
public: public:
@@ -110,12 +110,12 @@ public:
if (_text_sz == 4) return 8 * BIG_TEXT_SCALE; if (_text_sz == 4) return 8 * BIG_TEXT_SCALE;
if (_text_sz == 3) return 28; if (_text_sz == 3) return 28;
if (_text_sz == 2) return 16 * sc; if (_text_sz == 2) return 16 * sc;
return (_use_lemon ? 9 : 8) * sc; // misc-fixed 6x9 box height return (_single_font ? 9 : 8) * sc; // misc-fixed 6x9 box height
} }
void setLemonFont(bool) override { } // single-font: ignore toggles, stay misc-fixed 6x9 void setSingleFont(bool) override { } // single-font: ignore toggles, stay misc-fixed 6x9
bool isLemonFont() const override { return _use_lemon; } bool isSingleFont() const override { return _single_font; }
void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override { void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override {
if (_use_lemon) { if (_single_font) {
strncpy(dest, src, dest_size - 1); strncpy(dest, src, dest_size - 1);
dest[dest_size - 1] = '\0'; dest[dest_size - 1] = '\0';
} else { } else {
@@ -139,7 +139,7 @@ public:
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
uint16_t getTextWidth(const char* str) override; uint16_t getTextWidth(const char* str) override;
uint16_t getCodepointWidth(uint32_t cp) override { uint16_t getCodepointWidth(uint32_t cp) override {
if (_use_lemon && _text_sz == 1) return lemonXAdvance(cp, scale()); if (_single_font && _text_sz == 1) return glyphXAdvance(cp, scale());
return DisplayDriver::getCodepointWidth(cp); return DisplayDriver::getCodepointWidth(cp);
} }
void setDisplayRotation(uint8_t rot) override; void setDisplayRotation(uint8_t rot) override;

View File

@@ -67,7 +67,7 @@ void SH1106Display::setCursor(int x, int y)
display.setCursor(x, y); display.setCursor(x, y);
} }
int16_t SH1106Display::drawLemonChar(int16_t x, int16_t y, uint32_t cp) { int16_t SH1106Display::drawGlyph(int16_t x, int16_t y, uint32_t cp) {
int sz = _text_sz; int sz = _text_sz;
for (uint8_t i = 0; i < lemonIconCount; i++) { for (uint8_t i = 0; i < lemonIconCount; i++) {
if (pgm_read_dword(&lemonIconCPs[i]) == cp) { if (pgm_read_dword(&lemonIconCPs[i]) == cp) {
@@ -95,7 +95,7 @@ int16_t SH1106Display::drawLemonChar(int16_t x, int16_t y, uint32_t cp) {
if (cp < MiscFixed.first || cp > MiscFixed.last) { if (cp < MiscFixed.first || cp > MiscFixed.last) {
// y here is the top of the current text row, i.e. already baseline minus // y here is the top of the current text row, i.e. already baseline minus
// the font's 7px ascent (see real-glyph rendering just below: y + 7 + yo + // the font's 7px ascent (see real-glyph rendering just below: y + 7 + yo +
// row) -- unlike GxEPDDisplay's drawLemonChar, where y IS the baseline and // row) -- unlike GxEPDDisplay's drawGlyph, where y IS the baseline and
// the box sits at y - 7*sc for the same reason. Drawing this box at plain // the box sits at y - 7*sc for the same reason. Drawing this box at plain
// `y` (not y - 7*sz) lands it in the same ascent-top position, within its // `y` (not y - 7*sz) lands it in the same ascent-top position, within its
// own row instead of bleeding into the row above. // own row instead of bleeding into the row above.
@@ -120,7 +120,7 @@ int16_t SH1106Display::drawLemonChar(int16_t x, int16_t y, uint32_t cp) {
return x + xa * sz; return x + xa * sz;
} }
uint8_t SH1106Display::lemonXAdvance(uint32_t cp) { uint8_t SH1106Display::glyphXAdvance(uint32_t cp) {
uint8_t xa; uint8_t xa;
if (cp < MiscFixed.first || cp > MiscFixed.last) xa = 6; if (cp < MiscFixed.first || cp > MiscFixed.last) xa = 6;
else xa = pgm_read_byte(&MiscFixedGlyphs[cp - MiscFixed.first].xAdvance); else xa = pgm_read_byte(&MiscFixedGlyphs[cp - MiscFixed.first].xAdvance);
@@ -128,7 +128,7 @@ uint8_t SH1106Display::lemonXAdvance(uint32_t cp) {
} }
void SH1106Display::translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) { void SH1106Display::translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) {
if (_use_lemon) { if (_single_font) {
size_t n = strlen(src); size_t n = strlen(src);
if (n >= dest_size) n = dest_size - 1; if (n >= dest_size) n = dest_size - 1;
memcpy(dest, src, n); memcpy(dest, src, n);
@@ -140,7 +140,7 @@ void SH1106Display::translateUTF8ToBlocks(char* dest, const char* src, size_t de
void SH1106Display::print(const char *str) void SH1106Display::print(const char *str)
{ {
if (!_use_lemon) { display.print(str); return; } if (!_single_font) { display.print(str); return; }
int16_t cx = display.getCursorX(); int16_t cx = display.getCursorX();
int16_t cy = display.getCursorY(); int16_t cy = display.getCursorY();
@@ -148,7 +148,7 @@ void SH1106Display::print(const char *str)
while (*p) { while (*p) {
uint32_t cp = decodeCodepoint(p); uint32_t cp = decodeCodepoint(p);
if (cp == '\n') { cy += MiscFixed.yAdvance; cx = 0; } if (cp == '\n') { cy += MiscFixed.yAdvance; cx = 0; }
else { cx = drawLemonChar(cx, cy, cp); } else { cx = drawGlyph(cx, cy, cp); }
} }
display.setCursor(cx, cy); display.setCursor(cx, cy);
} }
@@ -170,10 +170,10 @@ void SH1106Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h)
uint16_t SH1106Display::getTextWidth(const char *str) uint16_t SH1106Display::getTextWidth(const char *str)
{ {
if (_use_lemon) { if (_single_font) {
uint16_t width = 0; uint16_t width = 0;
const uint8_t* p = (const uint8_t*)str; const uint8_t* p = (const uint8_t*)str;
while (*p) width += lemonXAdvance(decodeCodepoint(p)); while (*p) width += glyphXAdvance(decodeCodepoint(p));
return width; return width;
} }
int16_t x1, y1; int16_t x1, y1;

View File

@@ -21,7 +21,7 @@ class SH1106Display : public DisplayDriver
uint8_t _color; uint8_t _color;
uint8_t _contrast; uint8_t _contrast;
uint8_t _precharge; uint8_t _precharge;
bool _use_lemon = true; // OLED is single-font (misc-fixed 6x9); the Lemon/default switch is retired here bool _single_font = true; // OLED is single-font (misc-fixed 6x9); the Lemon/default switch is retired here
int _text_sz; int _text_sz;
// Frame-skip: endFrame() hashes the GFX buffer (FNV-1a, no external dep — the // Frame-skip: endFrame() hashes the GFX buffer (FNV-1a, no external dep — the
// CRC32 lib is only wired into e-ink builds) and skips the I²C flush when it's // CRC32 lib is only wired into e-ink builds) and skips the I²C flush when it's
@@ -31,8 +31,8 @@ class SH1106Display : public DisplayDriver
bool _force_redraw = true; bool _force_redraw = true;
bool i2c_probe(TwoWire &wire, uint8_t addr); bool i2c_probe(TwoWire &wire, uint8_t addr);
int16_t drawLemonChar(int16_t x, int16_t y, uint32_t cp); int16_t drawGlyph(int16_t x, int16_t y, uint32_t cp);
uint8_t lemonXAdvance(uint32_t cp); uint8_t glyphXAdvance(uint32_t cp);
public: public:
SH1106Display() : DisplayDriver(128, 64), display(128, 64, &Wire, PIN_OLED_RESET) { SH1106Display() : DisplayDriver(128, 64), display(128, 64, &Wire, PIN_OLED_RESET) {
@@ -54,17 +54,17 @@ public:
void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override; void drawXbm(int x, int y, const uint8_t *bits, int w, int h) override;
uint16_t getTextWidth(const char *str) override; uint16_t getTextWidth(const char *str) override;
uint16_t getCodepointWidth(uint32_t cp) override { uint16_t getCodepointWidth(uint32_t cp) override {
if (_use_lemon) return lemonXAdvance(cp); if (_single_font) return glyphXAdvance(cp);
return 6 * _text_sz; // built-in 5x7 font: 6 px advance per glyph return 6 * _text_sz; // built-in 5x7 font: 6 px advance per glyph
} }
int getCharWidth() const override { return 6 * _text_sz; } // misc-fixed 6x9 is 6px wide int getCharWidth() const override { return 6 * _text_sz; } // misc-fixed 6x9 is 6px wide
int getLineHeight() const override { return (_use_lemon ? 9 : 8) * _text_sz; } // misc-fixed 6x9 box height int getLineHeight() const override { return (_single_font ? 9 : 8) * _text_sz; } // misc-fixed 6x9 box height
// Only the built-in classic font pads every measured string by one trailing // Only the built-in classic font pads every measured string by one trailing
// advance column (see DisplayDriver::textWidthTrailingGap()); the lemon // advance column (see DisplayDriver::textWidthTrailingGap()); the lemon
// font's width comes from its own glyph table (ink-tight, no padding). // font's width comes from its own glyph table (ink-tight, no padding).
int textWidthTrailingGap() const override { return _use_lemon ? 0 : 1; } int textWidthTrailingGap() const override { return _single_font ? 0 : 1; }
void setLemonFont(bool) override { } // single-font: ignore toggles, stay misc-fixed 6x9 void setSingleFont(bool) override { } // single-font: ignore toggles, stay misc-fixed 6x9
bool isLemonFont() const override { return _use_lemon; } bool isSingleFont() const override { return _single_font; }
void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override; void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override;
void setBrightness(uint8_t level) override; void setBrightness(uint8_t level) override;
void endFrame() override; void endFrame() override;