mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
feat(ui): unify OLED on misc-fixed 6x9 font; add Diagnostics tab carousel
Replace the Lemon/default font-switch with a single misc-fixed 6x9 font (full Latin/Greek/Cyrillic coverage), generated via a new tools/bdf2gfx.py BDF-to-GFX converter. Removes the keyboard's per-render font-switch workarounds now that one font fits its cell cleanly. DiagnosticsScreen becomes a circular tab carousel (Live / System / Font), adding a firmware+device+radio info tab and a per-alphabet rendering test card covering every keyboard language. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,30 +3,50 @@
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include <helpers/DeviceDiag.h>
|
||||
#include <Arduino.h>
|
||||
#include <stdarg.h>
|
||||
#include "icons.h"
|
||||
#include "PopupMenu.h"
|
||||
#include "TabBar.h"
|
||||
#include "../MyMesh.h"
|
||||
|
||||
extern MyMesh the_mesh;
|
||||
|
||||
// Single-screen device diagnostics: uptime, packet counts by category (RX/TX),
|
||||
// forwarded-packet count, heap and stack headroom, radio signal (noise floor,
|
||||
// RSSI/SNR), packet-pool/outbound-queue depth, and radio error flags. The packet
|
||||
// counts are built from Dispatcher's per-type counters (Mesh.h/Dispatcher.cpp) —
|
||||
// grouped into a handful of categories rather than all 16 raw PAYLOAD_TYPE_*
|
||||
// values, so they stay readable on a 128x64 OLED without the full type list.
|
||||
// Falls back to scrolling (same drawList-style indicator as every other screen)
|
||||
// on screens too small to show every row at once. Hold Enter resets the counters.
|
||||
// Device diagnostics, organised as a circular tab carousel (shared TabBar.h,
|
||||
// same idiom as BotScreen/NearbyScreen). LEFT/RIGHT switches tabs; UP/DOWN
|
||||
// scrolls within the active tab:
|
||||
// Live — live counters: uptime, packet counts by category (RX/TX),
|
||||
// forwarded count, heap/stack headroom, radio signal, pool/queue
|
||||
// depth, error flags. Hold Enter resets the counters.
|
||||
// System — static device identity: firmware version + build date, device
|
||||
// model, node name, and the active radio parameters.
|
||||
// Font — a rendering test card: one sample line per script the UI font
|
||||
// claims to cover (Latin, diacritics, Greek, Cyrillic, digits,
|
||||
// symbols), so the on-device font can be eyeballed for coverage.
|
||||
// The packet counts are built from Dispatcher's per-type counters — grouped
|
||||
// into a handful of categories rather than all 16 raw PAYLOAD_TYPE_* values,
|
||||
// so they stay readable on a 128x64 OLED. Every tab falls back to scrolling on
|
||||
// screens too small to show its rows at once.
|
||||
class DiagnosticsScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
int _scroll = 0;
|
||||
PopupMenu _reset_menu; // Hold Enter → 1-item "Reset counters" action menu (Back dismisses)
|
||||
uint8_t _tab = 0; // persists across visits (like BotScreen's _tab)
|
||||
PopupMenu _reset_menu; // Live tab, Hold Enter → 1-item "Reset counters" action menu (Back dismisses)
|
||||
|
||||
enum Tab : uint8_t { TAB_LIVE, TAB_SYSTEM, TAB_FONT, TAB_COUNT };
|
||||
static const char* const TAB_LABELS[TAB_COUNT];
|
||||
|
||||
struct Row { const char* label; char value[20]; };
|
||||
static const int MAX_ROWS = 14;
|
||||
Row _rows[MAX_ROWS];
|
||||
int _row_count = 0;
|
||||
|
||||
// Full-width text lines (System / Font tabs) — long values (device model,
|
||||
// node name) don't fit the Live tab's narrow right-aligned value column, so
|
||||
// these tabs draw one ellipsized line each instead of a label/value pair.
|
||||
static const int MAX_LINES = 15;
|
||||
char _lines[MAX_LINES][40];
|
||||
int _line_count = 0;
|
||||
|
||||
void addRow(const char* label, const char* value) {
|
||||
if (_row_count >= MAX_ROWS) return;
|
||||
_rows[_row_count].label = label;
|
||||
@@ -44,6 +64,14 @@ class DiagnosticsScreen : public UIScreen {
|
||||
addRow(label, buf);
|
||||
}
|
||||
|
||||
void addLine(const char* fmt, ...) {
|
||||
if (_line_count >= MAX_LINES) return;
|
||||
va_list ap; va_start(ap, fmt);
|
||||
vsnprintf(_lines[_line_count], sizeof(_lines[_line_count]), fmt, ap);
|
||||
va_end(ap);
|
||||
_line_count++;
|
||||
}
|
||||
|
||||
static uint32_t sumByTypes(bool recv, const uint8_t* types, int n) {
|
||||
uint32_t total = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
@@ -51,7 +79,7 @@ class DiagnosticsScreen : public UIScreen {
|
||||
return total;
|
||||
}
|
||||
|
||||
void buildRows() {
|
||||
void buildLiveRows() {
|
||||
_row_count = 0;
|
||||
|
||||
uint32_t up_secs = millis() / 1000;
|
||||
@@ -117,23 +145,63 @@ class DiagnosticsScreen : public UIScreen {
|
||||
addRow("Errors", buf);
|
||||
}
|
||||
|
||||
public:
|
||||
DiagnosticsScreen(UITask* task) : _task(task) {}
|
||||
void buildSystemLines() {
|
||||
_line_count = 0;
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
buildRows();
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawCenteredHeader("DIAGNOSTICS");
|
||||
// Firmware version, minus the commit-hash suffix build.sh appends as the
|
||||
// LAST dash-segment (v1.16-solo.0-abcdef -> v1.16-solo.0) — same strip the
|
||||
// boot screen does; the last dash, not the first, since a tag like
|
||||
// v1.21-rc1 has a dash of its own before the hash.
|
||||
const char* ver = FIRMWARE_VERSION;
|
||||
const char* dash = strrchr(ver, '-');
|
||||
int plen = dash ? (int)(dash - ver) : (int)strlen(ver);
|
||||
char sv[24];
|
||||
if (plen >= (int)sizeof(sv)) plen = sizeof(sv) - 1;
|
||||
memcpy(sv, ver, plen); sv[plen] = '\0';
|
||||
|
||||
addLine("FW %s", sv);
|
||||
addLine("Built %s", FIRMWARE_BUILD_DATE);
|
||||
addLine("Dev %s", board.getManufacturerName());
|
||||
addLine("Node %s", the_mesh.getNodeName());
|
||||
|
||||
NodePrefs* p = the_mesh.getNodePrefs();
|
||||
if (p) {
|
||||
addLine("Freq %.3f MHz", p->freq);
|
||||
addLine("SF%u BW%.0f CR%u", (unsigned)p->sf, p->bw, (unsigned)p->cr);
|
||||
addLine("TX %d dBm", (int)p->tx_power_dbm);
|
||||
}
|
||||
}
|
||||
|
||||
// One sample line per script/keyboard alphabet the UI font claims to cover
|
||||
// (all inside the U+0020-04FF glyph range the OLED misc-fixed / e-ink Lemon
|
||||
// fonts carry), so the font's coverage can be checked by eye on real
|
||||
// hardware. Per-language lines use the same 2-letter codes as
|
||||
// KeyboardWidget::altAlphabetHint and each shows that language's own
|
||||
// non-ASCII letters (see KeyboardWidget.h's KB_*_CHARS tables).
|
||||
void buildFontLines() {
|
||||
_line_count = 0;
|
||||
addLine("Latin ABCabc xyz");
|
||||
addLine("PL ąćęłńóśźż");
|
||||
addLine("CZ áčďéěíňóřš");
|
||||
addLine("SK áäčďíĺľňôŕ");
|
||||
addLine("DE äöüß");
|
||||
addLine("FR àâçéèêëîïœ");
|
||||
addLine("ES áéíñóúü");
|
||||
addLine("PT áàâãçéêíóôõú");
|
||||
addLine("ND åäæöø");
|
||||
addLine("Grk ΑΒΓ αβγξω"); // ΑΒΓ αβγξω
|
||||
addLine("Cyr АБВ абвжя"); // АБВ абвжя
|
||||
addLine("Num 0123456789");
|
||||
addLine("Sym @#&*()[]{}/\\+=");
|
||||
}
|
||||
|
||||
// Shared scrollable renderer for the label/value Live tab.
|
||||
void renderRows(DisplayDriver& display) {
|
||||
const int item_h = display.lineStep();
|
||||
const int start_y = display.listStart();
|
||||
int visible = display.listVisible(item_h);
|
||||
if (visible < 1) visible = 1;
|
||||
int max_scroll = _row_count - visible;
|
||||
if (max_scroll < 0) max_scroll = 0;
|
||||
if (_scroll > max_scroll) _scroll = max_scroll;
|
||||
if (_scroll < 0) _scroll = 0;
|
||||
clampScroll(_row_count, visible);
|
||||
|
||||
const int reserve = scrollIndicatorReserve(display, _row_count, visible);
|
||||
for (int i = 0; i < visible && (_scroll + i) < _row_count; i++) {
|
||||
@@ -144,9 +212,51 @@ public:
|
||||
display.drawTextRightAlign(display.width() - reserve - 2, y, r.value);
|
||||
}
|
||||
drawScrollIndicator(display, start_y, visible * item_h, _row_count, visible, _scroll);
|
||||
}
|
||||
|
||||
// Shared scrollable renderer for the full-width System / Font tabs.
|
||||
void renderLines(DisplayDriver& display) {
|
||||
const int item_h = display.lineStep();
|
||||
const int start_y = display.listStart();
|
||||
int visible = display.listVisible(item_h);
|
||||
if (visible < 1) visible = 1;
|
||||
clampScroll(_line_count, visible);
|
||||
|
||||
const int reserve = scrollIndicatorReserve(display, _line_count, visible);
|
||||
for (int i = 0; i < visible && (_scroll + i) < _line_count; i++) {
|
||||
int y = start_y + i * item_h;
|
||||
display.drawTextEllipsized(2, y, display.width() - reserve - 4, _lines[_scroll + i]);
|
||||
}
|
||||
drawScrollIndicator(display, start_y, visible * item_h, _line_count, visible, _scroll);
|
||||
}
|
||||
|
||||
void clampScroll(int total, int visible) {
|
||||
int max_scroll = total - visible;
|
||||
if (max_scroll < 0) max_scroll = 0;
|
||||
if (_scroll > max_scroll) _scroll = max_scroll;
|
||||
if (_scroll < 0) _scroll = 0;
|
||||
}
|
||||
|
||||
public:
|
||||
DiagnosticsScreen(UITask* task) : _task(task) {}
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
tabbar::draw(display, TAB_LABELS, TAB_COUNT, _tab);
|
||||
|
||||
switch (_tab) {
|
||||
case TAB_SYSTEM: buildSystemLines(); renderLines(display); break;
|
||||
case TAB_FONT: buildFontLines(); renderLines(display); break;
|
||||
default: buildLiveRows(); renderRows(display); break;
|
||||
}
|
||||
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
if (_reset_menu.active) _reset_menu.render(display);
|
||||
return _reset_menu.active ? 50 : 1000; // popup wants snappier redraw; stats otherwise refresh once a second
|
||||
// Live counters refresh once a second; the static System/Font cards don't
|
||||
// change, so they can idle. The reset popup wants a snappier redraw.
|
||||
if (_reset_menu.active) return 50;
|
||||
return _tab == TAB_LIVE ? 1000 : 2000;
|
||||
}
|
||||
|
||||
bool handleInput(char c) override {
|
||||
@@ -158,9 +268,11 @@ public:
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (keyIsPrev(c)) { _tab = (_tab + TAB_COUNT - 1) % TAB_COUNT; _scroll = 0; return true; }
|
||||
if (keyIsNext(c)) { _tab = (_tab + 1) % TAB_COUNT; _scroll = 0; return true; }
|
||||
if (c == KEY_UP) { if (_scroll > 0) _scroll--; return true; }
|
||||
if (c == KEY_DOWN) { _scroll++; return true; } // clamped to max_scroll in render()
|
||||
if (c == KEY_CONTEXT_MENU) { // Hold Enter — same action-menu gesture as the rest of the UI
|
||||
if (c == KEY_DOWN) { _scroll++; return true; } // clamped in render()
|
||||
if (c == KEY_CONTEXT_MENU && _tab == TAB_LIVE) { // Hold Enter — reset the live counters
|
||||
_reset_menu.begin("Diagnostics", 1);
|
||||
_reset_menu.addItem("Reset counters");
|
||||
return true;
|
||||
@@ -169,3 +281,5 @@ public:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const char* const DiagnosticsScreen::TAB_LABELS[DiagnosticsScreen::TAB_COUNT] = { "Live", "System", "Font" };
|
||||
|
||||
@@ -461,17 +461,8 @@ struct KeyboardWidget {
|
||||
// on the same cell from being treated as a continued cycle.
|
||||
if (t9_cell >= 0 && millis() - t9_last_ms > KB_T9_TIMEOUT_MS) t9_cell = -1;
|
||||
|
||||
// Lemon is the only font that can draw the alt-alphabet page (or any
|
||||
// already-typed non-ASCII bytes sitting in buf, e.g. Cyrillic composed
|
||||
// earlier, now viewed from the Latin/symbols page) — force it on for just
|
||||
// this render() call if the user's own font choice wouldn't otherwise show
|
||||
// it, then restore exactly the state found on entry. Self-contained: never
|
||||
// leaks a forced font into whatever renders next frame.
|
||||
bool want_lemon = pageIsAltAlphabet(page);
|
||||
if (!want_lemon) for (int i = 0; i < len; i++) if ((uint8_t)buf[i] >= 0x80) { want_lemon = true; break; }
|
||||
bool had_lemon = display.isLemonFont();
|
||||
if (want_lemon && !had_lemon) display.setLemonFont(true);
|
||||
|
||||
// Single UI font (misc-fixed 5x7) covers Latin/Greek/Cyrillic — the keyboard
|
||||
// renders in it directly, no per-render font switching or headroom padding.
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
|
||||
@@ -485,16 +476,7 @@ struct KeyboardWidget {
|
||||
const int preview_h = display.height() - kb_h - display.sepH();
|
||||
const int prev_lines = (preview_h / lh) > 1 ? (preview_h / lh) : 1;
|
||||
const int sep_y = prev_lines * lh;
|
||||
// Lemon's tallest glyphs (accented capitals like Ć/Š/Ž, needed once caps
|
||||
// is on) draw up to 3px above the nominal cursor y on SH1106 — its Lemon
|
||||
// baseline offset is tuned for the shorter plain-ASCII/Cyrillic/Greek
|
||||
// glyphs this keyboard used before the per-language Latin alphabets, and
|
||||
// wasn't tall enough for these. Padding the first char row down by 3px
|
||||
// (only while Lemon is actually in use) gives every accented glyph enough
|
||||
// headroom that it can't bleed into the separator bar above; harmless on
|
||||
// e-ink, which already has more headroom than it needs here.
|
||||
const int lemon_pad = want_lemon ? 3 : 0;
|
||||
const int chars_y = sep_y + display.sepH() + lemon_pad;
|
||||
const int chars_y = sep_y + display.sepH();
|
||||
const int cell_h = (display.height() - chars_y) / (rows + 1);
|
||||
const int spec_y = chars_y + rows * cell_h;
|
||||
const int spec_w = display.width() / KB_SPECIAL;
|
||||
@@ -608,8 +590,6 @@ struct KeyboardWidget {
|
||||
|
||||
// placeholder picker overlay (drawn on top of keyboard)
|
||||
if (_ph_menu.active) _ph_menu.render(display);
|
||||
|
||||
if (want_lemon && !had_lemon) display.setLemonFont(false); // restore exactly what we found
|
||||
return 50;
|
||||
}
|
||||
|
||||
|
||||
1537
src/helpers/ui/MiscFixedFont.h
Normal file
1537
src/helpers/ui/MiscFixedFont.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
#include "SH1106Display.h"
|
||||
#include <Adafruit_GrayOLED.h>
|
||||
#include "Adafruit_SH110X.h"
|
||||
#include "LemonFont.h"
|
||||
#include "MiscFixedFont.h"
|
||||
#include "LemonIcons.h"
|
||||
|
||||
bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr)
|
||||
@@ -90,11 +90,13 @@ int16_t SH1106Display::drawLemonChar(int16_t x, int16_t y, uint32_t cp) {
|
||||
}
|
||||
}
|
||||
|
||||
if (cp < Lemon.first || cp > Lemon.last) {
|
||||
// Font glyphs come from misc-fixed 6x9 (full Latin/Greek/Cyrillic, ascent 7) —
|
||||
// baseline +7. The custom UI icons above keep +6, so they sit 1px higher.
|
||||
if (cp < MiscFixed.first || cp > MiscFixed.last) {
|
||||
if (cp >= 0x20) display.fillRect(x + sz, y - sz, 4*sz, 6*sz, _color);
|
||||
return x + 6 * sz;
|
||||
}
|
||||
const GFXglyph* g = &lemonGlyphs[cp - Lemon.first];
|
||||
const GFXglyph* g = &MiscFixedGlyphs[cp - MiscFixed.first];
|
||||
uint8_t w = pgm_read_byte(&g->width), h = pgm_read_byte(&g->height);
|
||||
int8_t xo = (int8_t)pgm_read_byte(&g->xOffset), yo = (int8_t)pgm_read_byte(&g->yOffset);
|
||||
uint8_t xa = pgm_read_byte(&g->xAdvance);
|
||||
@@ -102,10 +104,10 @@ int16_t SH1106Display::drawLemonChar(int16_t x, int16_t y, uint32_t cp) {
|
||||
uint8_t bits = 0, bit = 0;
|
||||
for (uint8_t row = 0; row < h; row++)
|
||||
for (uint8_t col = 0; col < w; col++) {
|
||||
if (!bit) { bits = pgm_read_byte(&lemonBitmaps[bo++]); bit = 0x80; }
|
||||
if (!bit) { bits = pgm_read_byte(&MiscFixedBitmaps[bo++]); bit = 0x80; }
|
||||
if (bits & bit) {
|
||||
if (sz == 1) display.drawPixel(x + xo + col, y + 6 + yo + row, _color);
|
||||
else display.fillRect(x + xo*sz + col*sz, y + 6*sz + yo*sz + row*sz, sz, sz, _color);
|
||||
if (sz == 1) display.drawPixel(x + xo + col, y + 7 + yo + row, _color);
|
||||
else display.fillRect(x + xo*sz + col*sz, y + 7*sz + yo*sz + row*sz, sz, sz, _color);
|
||||
}
|
||||
bit >>= 1;
|
||||
}
|
||||
@@ -114,8 +116,8 @@ int16_t SH1106Display::drawLemonChar(int16_t x, int16_t y, uint32_t cp) {
|
||||
|
||||
uint8_t SH1106Display::lemonXAdvance(uint32_t cp) {
|
||||
uint8_t xa;
|
||||
if (cp < Lemon.first || cp > Lemon.last) xa = 6;
|
||||
else xa = pgm_read_byte(&lemonGlyphs[cp - Lemon.first].xAdvance);
|
||||
if (cp < MiscFixed.first || cp > MiscFixed.last) xa = 6;
|
||||
else xa = pgm_read_byte(&MiscFixedGlyphs[cp - MiscFixed.first].xAdvance);
|
||||
return xa * _text_sz;
|
||||
}
|
||||
|
||||
@@ -139,7 +141,7 @@ void SH1106Display::print(const char *str)
|
||||
const uint8_t* p = (const uint8_t*)str;
|
||||
while (*p) {
|
||||
uint32_t cp = decodeCodepoint(p);
|
||||
if (cp == '\n') { cy += Lemon.yAdvance; cx = 0; }
|
||||
if (cp == '\n') { cy += MiscFixed.yAdvance; cx = 0; }
|
||||
else { cx = drawLemonChar(cx, cy, cp); }
|
||||
}
|
||||
display.setCursor(cx, cy);
|
||||
|
||||
@@ -21,7 +21,7 @@ class SH1106Display : public DisplayDriver
|
||||
uint8_t _color;
|
||||
uint8_t _contrast;
|
||||
uint8_t _precharge;
|
||||
bool _use_lemon = false;
|
||||
bool _use_lemon = true; // OLED is single-font (misc-fixed 5x7); the Lemon/default switch is retired here
|
||||
int _text_sz;
|
||||
// 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
|
||||
@@ -57,13 +57,13 @@ public:
|
||||
if (_use_lemon) return lemonXAdvance(cp);
|
||||
return 6 * _text_sz; // built-in 5x7 font: 6 px advance per glyph
|
||||
}
|
||||
int getCharWidth() const override { return (_use_lemon ? 5 : 6) * _text_sz; }
|
||||
int getLineHeight() const override { return (_use_lemon ? 9 : 8) * _text_sz; }
|
||||
int getCharWidth() const override { return (_use_lemon ? 6 : 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
|
||||
// Only the built-in classic font pads every measured string by one trailing
|
||||
// advance column (see DisplayDriver::textWidthTrailingGap()); the lemon
|
||||
// font's width comes from its own glyph table (ink-tight, no padding).
|
||||
int textWidthTrailingGap() const override { return _use_lemon ? 0 : 1; }
|
||||
void setLemonFont(bool enabled) override { _use_lemon = enabled; _vw_dirty = true; }
|
||||
void setLemonFont(bool) override { } // single-font: ignore toggles, stay misc-fixed 5x7
|
||||
bool isLemonFont() const override { return _use_lemon; }
|
||||
void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override;
|
||||
void setBrightness(uint8_t level) override;
|
||||
|
||||
117
tools/bdf2gfx.py
Normal file
117
tools/bdf2gfx.py
Normal file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""bdf2gfx.py — convert a BDF bitmap font to an Adafruit-GFX header in the
|
||||
LemonFont.h layout: a *contiguous* glyph table over [FIRST, LAST], with empty
|
||||
placeholder glyphs for codepoints the BDF doesn't define, so the renderer can
|
||||
index by `cp - first`. Recreates the lost bdf2lemon.py.
|
||||
|
||||
Usage: bdf2gfx.py <font.bdf> <first_hex> <last_hex> <VarPrefix> > out.h
|
||||
"""
|
||||
import sys
|
||||
|
||||
def parse_bdf(path):
|
||||
glyphs = {} # codepoint -> dict(bbx=(w,h,xo,yo), dwidth=int, bitmap=[int rows])
|
||||
asc = desc = None
|
||||
with open(path, 'r', errors='replace') as f:
|
||||
cur = None
|
||||
reading_bits = False
|
||||
for line in f:
|
||||
p = line.split()
|
||||
if not p:
|
||||
continue
|
||||
k = p[0]
|
||||
if k == 'FONT_ASCENT': asc = int(p[1])
|
||||
elif k == 'FONT_DESCENT': desc = int(p[1])
|
||||
elif k == 'STARTCHAR':
|
||||
cur = {'cp': None, 'bbx': None, 'dwidth': None, 'bitmap': []}
|
||||
elif k == 'ENCODING':
|
||||
cur['cp'] = int(p[1])
|
||||
elif k == 'DWIDTH':
|
||||
cur['dwidth'] = int(p[1])
|
||||
elif k == 'BBX':
|
||||
cur['bbx'] = (int(p[1]), int(p[2]), int(p[3]), int(p[4]))
|
||||
elif k == 'BITMAP':
|
||||
reading_bits = True
|
||||
elif k == 'ENDCHAR':
|
||||
reading_bits = False
|
||||
if cur['cp'] is not None and cur['cp'] >= 0:
|
||||
glyphs[cur['cp']] = cur
|
||||
cur = None
|
||||
elif reading_bits and cur is not None:
|
||||
cur['bitmap'].append(int(k, 16))
|
||||
return glyphs, asc, desc
|
||||
|
||||
def pack_glyph_bits(g):
|
||||
"""Return a flat MSB-first, row-major bitstream of exactly w*h bits."""
|
||||
w, h, xo, yo = g['bbx']
|
||||
out = bytearray()
|
||||
acc = 0; nbits = 0
|
||||
for row in range(h):
|
||||
rowval = g['bitmap'][row] if row < len(g['bitmap']) else 0
|
||||
# BDF rows are left-justified, padded to a multiple of 8 bits.
|
||||
rowbits = ((len(bin(rowval)) - 2 + 7)//8*8) if rowval else 8
|
||||
rowbytes = max(1, (w + 7)//8)
|
||||
for col in range(w):
|
||||
byte_i = col // 8
|
||||
bit_i = 7 - (col % 8)
|
||||
shift = (rowbytes - 1 - byte_i) * 8 + bit_i
|
||||
bit = (rowval >> shift) & 1
|
||||
acc = (acc << 1) | bit
|
||||
nbits += 1
|
||||
if nbits == 8:
|
||||
out.append(acc); acc = 0; nbits = 0
|
||||
if nbits:
|
||||
out.append(acc << (8 - nbits))
|
||||
return out
|
||||
|
||||
def main():
|
||||
bdf, first_s, last_s, prefix = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]
|
||||
first = int(first_s, 16); last = int(last_s, 16)
|
||||
glyphs, asc, desc = parse_bdf(bdf)
|
||||
|
||||
bitmaps = bytearray()
|
||||
table = [] # (bitmapOffset, w, h, xadv, xo, yo, cp, present)
|
||||
present = 0
|
||||
for cp in range(first, last + 1):
|
||||
g = glyphs.get(cp)
|
||||
if g and g['bbx'] and g['bbx'][0] > 0 and g['bbx'][1] > 0:
|
||||
w, h, xo, yo = g['bbx']
|
||||
off = len(bitmaps)
|
||||
bitmaps += pack_glyph_bits(g)
|
||||
xadv = g['dwidth'] if g['dwidth'] is not None else w
|
||||
yoff_gfx = -(yo + h) # baseline->top, y-down (Adafruit convention)
|
||||
table.append((off, w, h, xadv, xo, yoff_gfx, cp, True))
|
||||
present += 1
|
||||
else:
|
||||
# placeholder: zero-size, advance = a space width
|
||||
table.append((len(bitmaps), 0, 0, (glyphs.get(0x20)['dwidth'] if 0x20 in glyphs else 4), 0, 0, cp, False))
|
||||
|
||||
yadv = (asc or 0) + (desc or 0)
|
||||
o = []
|
||||
o.append(f"// {prefix} font, converted from {bdf.split('/')[-1]} by bdf2gfx.py")
|
||||
o.append(f"// Range: U+{first:04X}-U+{last:04X} ({present} glyphs defined, rest placeholders)")
|
||||
o.append(f"// Metrics: ascent={asc} descent={desc} yAdvance={yadv}")
|
||||
o.append("#pragma once")
|
||||
o.append("#include <Adafruit_GFX.h>")
|
||||
o.append("")
|
||||
o.append(f"static const uint8_t {prefix}Bitmaps[] PROGMEM = {{")
|
||||
for i in range(0, len(bitmaps), 16):
|
||||
o.append(" " + ", ".join(f"0x{b:02X}" for b in bitmaps[i:i+16]) + ",")
|
||||
o.append("};")
|
||||
o.append("")
|
||||
o.append(f"static const GFXglyph {prefix}Glyphs[] PROGMEM = {{")
|
||||
for off, w, h, xadv, xo, yo, cp, pres in table:
|
||||
tag = "" if pres else " (placeholder)"
|
||||
o.append(f" {{ {off:5d}, {w:3d}, {h:3d}, {xadv:3d}, {xo:3d}, {yo:4d} }}, // U+{cp:04X}{tag}")
|
||||
o.append("};")
|
||||
o.append("")
|
||||
o.append(f"static const GFXfont {prefix} PROGMEM = {{")
|
||||
o.append(f" (uint8_t*){prefix}Bitmaps,")
|
||||
o.append(f" (GFXglyph*){prefix}Glyphs,")
|
||||
o.append(f" 0x{first:04X}, 0x{last:04X},")
|
||||
o.append(f" {yadv} // yAdvance")
|
||||
o.append("};")
|
||||
sys.stderr.write(f"defined={present} total={last-first+1} bitmap_bytes={len(bitmaps)} ascent={asc} descent={desc} yAdv={yadv}\n")
|
||||
print("\n".join(o))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user