diff --git a/release-notes.md b/release-notes.md index 714be1d7..e1524014 100644 --- a/release-notes.md +++ b/release-notes.md @@ -8,6 +8,7 @@ - **Bot Trigger fields accept multiple phrases.** Pack several trigger words into one Trigger field, comma-separated (`hi,hello there,yo`) — matching any one of them fires the reply, same as before for a single phrase. - **Remote Bot Actions gain `!gpio1`..`!gpio4`** (Wio Tracker L1 only — 4 otherwise-unused pins). Each pin is independently set to Off/Input/Output (GPIO1/GPIO2 also offer Analog) from a new **Tools › GPIO** screen; `!gpio1 on`/`!gpio1 off` drives an Output pin remotely, a bare `!gpio1` reports the current mode and reading (including a millivolt value in Analog mode). Gated by the same per-target Actions toggle as `!buzz`/`!gps`/`!advert`. - **Optional CardKB support (Wio Tracker L1, Grove I2C) — full keyboard-only navigation.** Plug an M5Stack CardKB into the Grove connector and it's auto-detected at boot — no setting to flip. Typing goes straight into the message/name field instead of navigating the on-screen keyboard grid, always as plain Latin text regardless of the Settings › Keyboard language/type — arrows and Esc work as expected everywhere else too (including inside the placeholder and accent popups). Enter acts like the physical centre button (advances the on-screen grid selection) rather than submitting, so **Fn+Enter** sends the message/confirms the field from anywhere, and **Fn+``** opens that letter's accent popup directly (no arrow-hunting needed — handy for Polish/Czech/etc. diacritics — and works no matter what language/keyboard type is configured, since CardKB always types Latin either way). **Tab** is the Hold-Enter equivalent everywhere, including message reply/navigate and the Bot/Admin/Repeater menus — same single shortcut whether or not the on-screen keyboard is showing. +- **GAT562 30S solo builds now use the same OLED font as the Wio Tracker L1.** The misc-fixed 6x9 font (full Latin, Greek and Cyrillic) had only ever reached the SH1106 and e-ink drivers, so on this board every keyboard alphabet beyond ASCII, and every accented contact name, was still being transliterated (`Łódź` → `Lodz`) or drawn as a filled block. Solo builds only — the font costs ~14 KB of flash and the repeater/companion builds have no keyboard to type those alphabets on; they keep the built-in font and are byte-for-byte unchanged. - **Settings › Keyboard gets an "Ext. KB" row** (boards that support CardKB, above). Switching it to **Compact** hides the on-screen letter grid, special-row icons, and status line entirely — a CardKB typist never looks at any of it, and none of it (script/page, T9-vs-ABC, caps) is actually actionable from CardKB anyway — replacing them with just a reminder of the Tab/Fn+letter shortcuts; the accent and placeholder popups still show normally. With the grid hidden, arrows and Tab take on more useful direct meanings instead of driving an invisible grid selection: **arrows** move the text cursor immediately (no more entering cursor mode first), **plain Enter** submits the message/field (same as Fn+Enter — there's no grid cell to commit), and **plain Tab** opens the placeholder picker directly. Compact is designed to need no joystick/physical button at all, and reclaims the freed space for extra message-preview lines. Off (**Full**) by default. ### Fixes diff --git a/src/helpers/ui/MiscFixedRenderer.h b/src/helpers/ui/MiscFixedRenderer.h new file mode 100644 index 00000000..798aed7f --- /dev/null +++ b/src/helpers/ui/MiscFixedRenderer.h @@ -0,0 +1,103 @@ +#pragma once + +#include +#include "DisplayDriver.h" +#include "MiscFixedFont.h" +#include "LemonIcons.h" + +// Shared misc-fixed 6x9 text renderer for the monochrome OLED drivers. +// +// Both Adafruit_SH110X and Adafruit_SSD1306 derive from Adafruit_GFX and the +// font path is pure pixel plotting, so SH1106Display and SSD1306Display render +// off this one implementation instead of two copies of it. Everything here +// bypasses GFX's own print(): GFX walks bytes and would treat each byte of a +// multi-byte UTF-8 sequence as its own CP437 glyph, and it can't reach the +// custom UI icons either. +// +// Include only from a .cpp — the font tables are static const arrays, so a +// header pulling this in would land a copy of them in every translation unit. +// +// `y` is the TOP of the text row in every function below, matching the UI's +// coordinate convention (GFX fonts would use the baseline). + +// Pixel advance of one codepoint at text size sz. Unmapped codepoints get the +// font's own 6px cell, same as the substitution box drawn for them. +static inline uint8_t miscFixedXAdvance(uint32_t cp, int sz) { + uint8_t xa; + if (cp < MiscFixed.first || cp > MiscFixed.last) xa = 6; + else xa = pgm_read_byte(&MiscFixedGlyphs[cp - MiscFixed.first].xAdvance); + return xa * sz; +} + +// Draw one codepoint at (x, y); returns the x to continue from. +static inline int16_t miscFixedDrawGlyph(Adafruit_GFX& gfx, int16_t x, int16_t y, + uint32_t cp, int sz, uint16_t color) { + for (uint8_t i = 0; i < lemonIconCount; i++) { + if (pgm_read_dword(&lemonIconCPs[i]) == cp) { + const GFXglyph* g = &lemonIconGlyphs[i]; + 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); + uint16_t bo = pgm_read_word(&g->bitmapOffset); + 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(&lemonIconBitmaps[bo++]); bit = 0x80; } + if (bits & bit) { + // The UI icons keep a +6 baseline; the font glyphs below use +7 + // (misc-fixed's ascent), so the icons sit 1px higher in their cell. + if (sz == 1) gfx.drawPixel(x + xo + col, y + 6 + yo + row, color); + else gfx.fillRect(x + xo*sz + col*sz, y + 6*sz + yo*sz + row*sz, sz, sz, color); + } + bit >>= 1; + } + return x + xa * sz; + } + } + + if (cp < MiscFixed.first || cp > MiscFixed.last) { + // Substitution box for anything the font doesn't cover. Drawn at plain `y` + // (not y - 7*sz): `y` is already the ascent top, so the box stays inside + // its own row instead of bleeding into the one above. + if (cp >= 0x20) gfx.fillRect(x + sz, y, 4*sz, 6*sz, color); + return x + 6 * sz; + } + + 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); + uint16_t bo = pgm_read_word(&g->bitmapOffset); + 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(&MiscFixedBitmaps[bo++]); bit = 0x80; } + if (bits & bit) { + if (sz == 1) gfx.drawPixel(x + xo + col, y + 7 + yo + row, color); + else gfx.fillRect(x + xo*sz + col*sz, y + 7*sz + yo*sz + row*sz, sz, sz, color); + } + bit >>= 1; + } + return x + xa * sz; +} + +// Draw a UTF-8 string from the driver's current cursor, honouring '\n', and +// leave the cursor where the text ended (same contract as GFX's print()). +static inline void miscFixedPrint(Adafruit_GFX& gfx, const char* str, int sz, uint16_t color) { + int16_t cx = gfx.getCursorX(); + int16_t cy = gfx.getCursorY(); + const uint8_t* p = (const uint8_t*)str; + while (*p) { + uint32_t cp = DisplayDriver::decodeCodepoint(p); + if (cp == '\n') { cy += MiscFixed.yAdvance * sz; cx = 0; } + else { cx = miscFixedDrawGlyph(gfx, cx, cy, cp, sz, color); } + } + gfx.setCursor(cx, cy); +} + +static inline uint16_t miscFixedTextWidth(const char* str, int sz) { + uint16_t width = 0; + const uint8_t* p = (const uint8_t*)str; + while (*p) width += miscFixedXAdvance(DisplayDriver::decodeCodepoint(p), sz); + return width; +} diff --git a/src/helpers/ui/SH1106Display.cpp b/src/helpers/ui/SH1106Display.cpp index 91c115f8..37caf4e7 100644 --- a/src/helpers/ui/SH1106Display.cpp +++ b/src/helpers/ui/SH1106Display.cpp @@ -1,8 +1,7 @@ #include "SH1106Display.h" #include #include "Adafruit_SH110X.h" -#include "MiscFixedFont.h" -#include "LemonIcons.h" +#include "MiscFixedRenderer.h" bool SH1106Display::i2c_probe(TwoWire &wire, uint8_t addr) { @@ -67,64 +66,8 @@ void SH1106Display::setCursor(int x, int y) display.setCursor(x, y); } -int16_t SH1106Display::drawGlyph(int16_t x, int16_t y, uint32_t cp) { - int sz = _text_sz; - for (uint8_t i = 0; i < lemonIconCount; i++) { - if (pgm_read_dword(&lemonIconCPs[i]) == cp) { - const GFXglyph* g = &lemonIconGlyphs[i]; - 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); - uint16_t bo = pgm_read_word(&g->bitmapOffset); - 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(&lemonIconBitmaps[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); - } - bit >>= 1; - } - return x + xa * sz; - } - } - - // 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) { - // 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 + - // 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 - // `y` (not y - 7*sz) lands it in the same ascent-top position, within its - // own row instead of bleeding into the row above. - if (cp >= 0x20) display.fillRect(x + sz, y, 4*sz, 6*sz, _color); - return x + 6 * sz; - } - 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); - uint16_t bo = pgm_read_word(&g->bitmapOffset); - 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(&MiscFixedBitmaps[bo++]); bit = 0x80; } - if (bits & bit) { - 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; - } - return x + xa * sz; -} - uint8_t SH1106Display::glyphXAdvance(uint32_t cp) { - uint8_t xa; - if (cp < MiscFixed.first || cp > MiscFixed.last) xa = 6; - else xa = pgm_read_byte(&MiscFixedGlyphs[cp - MiscFixed.first].xAdvance); - return xa * _text_sz; + return miscFixedXAdvance(cp, _text_sz); } void SH1106Display::translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) { @@ -141,16 +84,7 @@ void SH1106Display::translateUTF8ToBlocks(char* dest, const char* src, size_t de void SH1106Display::print(const char *str) { if (!_single_font) { display.print(str); return; } - - int16_t cx = display.getCursorX(); - int16_t cy = display.getCursorY(); - const uint8_t* p = (const uint8_t*)str; - while (*p) { - uint32_t cp = decodeCodepoint(p); - if (cp == '\n') { cy += MiscFixed.yAdvance; cx = 0; } - else { cx = drawGlyph(cx, cy, cp); } - } - display.setCursor(cx, cy); + miscFixedPrint(display, str, _text_sz, _color); } void SH1106Display::fillRect(int x, int y, int w, int h) @@ -170,12 +104,7 @@ void SH1106Display::drawXbm(int x, int y, const uint8_t *bits, int w, int h) uint16_t SH1106Display::getTextWidth(const char *str) { - if (_single_font) { - uint16_t width = 0; - const uint8_t* p = (const uint8_t*)str; - while (*p) width += glyphXAdvance(decodeCodepoint(p)); - return width; - } + if (_single_font) return miscFixedTextWidth(str, _text_sz); int16_t x1, y1; uint16_t w, h; display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); diff --git a/src/helpers/ui/SH1106Display.h b/src/helpers/ui/SH1106Display.h index 1f868d25..e2ab5a1e 100644 --- a/src/helpers/ui/SH1106Display.h +++ b/src/helpers/ui/SH1106Display.h @@ -31,7 +31,8 @@ class SH1106Display : public DisplayDriver bool _force_redraw = true; bool i2c_probe(TwoWire &wire, uint8_t addr); - int16_t drawGlyph(int16_t x, int16_t y, uint32_t cp); + // Thin wrapper over the shared misc-fixed renderer (MiscFixedRenderer.h), kept + // out of this header so the font tables land in one translation unit only. uint8_t glyphXAdvance(uint32_t cp); public: diff --git a/src/helpers/ui/SSD1306Display.cpp b/src/helpers/ui/SSD1306Display.cpp index 6d00d32d..0bd9fe2a 100644 --- a/src/helpers/ui/SSD1306Display.cpp +++ b/src/helpers/ui/SSD1306Display.cpp @@ -1,4 +1,7 @@ #include "SSD1306Display.h" +#ifdef OLED_MISC_FIXED_FONT + #include "MiscFixedRenderer.h" +#endif bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) { wire.beginTransmission(addr); @@ -54,6 +57,7 @@ void SSD1306Display::startFrame(Color bkg) { void SSD1306Display::setTextSize(int sz) { _text_sz = sz; + _vw_dirty = true; display.setTextSize(sz); } @@ -66,8 +70,18 @@ void SSD1306Display::setCursor(int x, int y) { display.setCursor(x, y); } +#ifdef OLED_MISC_FIXED_FONT +uint8_t SSD1306Display::glyphXAdvance(uint32_t cp) { + return miscFixedXAdvance(cp, _text_sz); +} +#endif + void SSD1306Display::print(const char* str) { +#ifdef OLED_MISC_FIXED_FONT + miscFixedPrint(display, str, _text_sz, _color); +#else display.print(str); +#endif } void SSD1306Display::fillRect(int x, int y, int w, int h) { @@ -83,10 +97,14 @@ void SSD1306Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { } uint16_t SSD1306Display::getTextWidth(const char* str) { +#ifdef OLED_MISC_FIXED_FONT + return miscFixedTextWidth(str, _text_sz); +#else int16_t x1, y1; uint16_t w, h; display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h); return w; +#endif } void SSD1306Display::endFrame() { diff --git a/src/helpers/ui/SSD1306Display.h b/src/helpers/ui/SSD1306Display.h index 5aa555a4..324c76c0 100644 --- a/src/helpers/ui/SSD1306Display.h +++ b/src/helpers/ui/SSD1306Display.h @@ -23,6 +23,11 @@ class SSD1306Display : public DisplayDriver { RefCountedDigitalPin* _peripher_power; bool i2c_probe(TwoWire& wire, uint8_t addr); +#ifdef OLED_MISC_FIXED_FONT + // Thin wrapper over the shared misc-fixed renderer (MiscFixedRenderer.h), kept + // out of this header so the font tables land in one translation unit only. + uint8_t glyphXAdvance(uint32_t cp); +#endif public: SSD1306Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64), display(128, 64, &Wire, PIN_OLED_RESET), @@ -33,10 +38,29 @@ public: bool begin(); int getCharWidth() const override { return 6 * _text_sz; } +#ifdef OLED_MISC_FIXED_FONT + // Same misc-fixed 6x9 font the SH1106 and e-ink drivers use: full + // Latin/Greek/Cyrillic instead of the built-in font's ASCII-with-blocks, so + // the keyboard's alphabets and accented names render as themselves. Opt-in + // per variant (the font costs ~14 KB of flash) — see the boards that set + // OLED_MISC_FIXED_FONT in their platformio.ini. + int getLineHeight() const override { return 9 * _text_sz; } // 6x9 box height + // The font's own glyph table is ink-tight, unlike the built-in font (below). + int textWidthTrailingGap() const override { return 0; } + bool isSingleFont() const override { return true; } + void setSingleFont(bool) override { } // single-font: ignore toggles + void translateUTF8ToBlocks(char* dest, const char* src, size_t dest_size) override { + // No transliteration needed — print() renders UTF-8 directly. + strncpy(dest, src, dest_size - 1); + dest[dest_size - 1] = '\0'; + } + uint16_t getCodepointWidth(uint32_t cp) override { return glyphXAdvance(cp); } +#else int getLineHeight() const override { return 8 * _text_sz; } // Classic Adafruit_GFX built-in font pads every measured string by one // trailing advance column (see DisplayDriver::textWidthTrailingGap()). int textWidthTrailingGap() const override { return 1; } +#endif bool isOn() override { return _isOn; } void turnOn() override; diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index d299155a..f86cbd03 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -135,6 +135,10 @@ build_flags = -D UI_HAS_JOYSTICK_UPDOWN=1 -D ADVERT_NAME='"@@MAC"' -D ENV_INCLUDE_GPS=1 + ; misc-fixed 6x9 OLED font (full Latin/Greek/Cyrillic), same as the Wio L1 + ; builds. Solo only: it costs ~14 KB of flash and the repeater/companion + ; builds have no keyboard to type those alphabets on. + -D OLED_MISC_FIXED_FONT=1 -Os ; QSPIFLASH disabled: QSPI IO0/IO2 pins (P0.30/P0.28) conflict with joystick LEFT/UP ; -D MESH_PACKET_LOGGING=1 @@ -173,6 +177,10 @@ build_flags = -D UI_HAS_JOYSTICK_UPDOWN=1 -D ADVERT_NAME='"@@MAC"' -D ENV_INCLUDE_GPS=1 + ; misc-fixed 6x9 OLED font (full Latin/Greek/Cyrillic), same as the Wio L1 + ; builds. Solo only: it costs ~14 KB of flash and the repeater/companion + ; builds have no keyboard to type those alphabets on. + -D OLED_MISC_FIXED_FONT=1 -D DUAL_SERIAL=1 -D ENABLE_SCREENSHOT -Os