Use readable 6px message font and keep 5px on tiny panels

This commit is contained in:
mikecarper committed 2026-09-08 14:23:39 -07:00
1 parent 5fe6f299f3
commit 272dc9aa4b
14 files changed
+1550 -93

No files matched your search

+3 -3
View File
@@ -22,9 +22,9 @@ It is not the generic XIAO profile for separately wired radio modules. Both
boards retain their parent Full Companion features, USB mOTA sender, WiFi
update slots, and PSRAM-backed offline queue. The V4 also retains direct MQTT.
The [small-screen Picopixel layout](v4_pixel5_font_trial.md) uses 5-pixel
capital letters, a compact channel/sender line and full message previews.
A named V4 trial profile is available for hardware qualification.
The [small-screen message layout](v4_pixel5_font_trial.md) uses 6-pixel capitals
on 128x64 panels and 5-pixel capitals on very tiny panels. It includes a compact
channel/sender line and full message previews. Font selection is automatic.
## Build
+41 -27
View File
@@ -1,23 +1,33 @@
# Small-screen Picopixel message font
# Small-screen message fonts
Small-screen Companion builds use Picopixel for received message text and the
channel/sender line, including `Ch 0 Public`. This covers SSD1306 and SH1106
OLEDs, ST7735 small TFTs, and the U8g2 T-Echo Card display. The shared renderer
uses the same font data on every driver. Larger displays keep their existing
font. Menus, Bluetooth PINs and WiFi setup QR codes keep their normal layout.
Small-screen Companion builds automatically select a compact font for received
message text and the channel/sender line, including `Ch 0 Public`. This covers
SSD1306 and SH1106 OLEDs, ST7735 small TFTs, and the U8g2 T-Echo Card display.
The shared renderer selects by logical panel dimensions, including rotation:
Capital letters are 5 logical pixels high; descenders can occupy a sixth
pixel, so lines advance by 7 pixels. Most characters advance by 4 pixels
horizontally. The normal OLED font uses 7-pixel letters, 8-pixel line spacing
and 6-pixel character advances. Small TFTs retain their existing logical to
physical scaling.
| Panel size | Font | Capital height | Line spacing |
| --- | --- | ---: | ---: |
| At least 128 x 64, or 64 x 128 rotated | Squeezed Regular 6 | 6 pixels | 8 pixels |
| Smaller panels, including 72 x 40, 128 x 32 and 64 x 48 | Picopixel | 5 pixels | 7 pixels |
On a 128 x 64 OLED, compacting the channel/sender line moves message text from
y=25 to y=21. Six complete small-font rows fit instead of five, roughly 30
additional characters depending on the letters. Text wraps at character
boundaries; the last line ends with `...` if it still cannot fit. Unsupported
characters appear as `?`. Long channel/sender names are ellipsized so they
cannot overlap the message.
Both fonts allow one pixel for descenders and one blank pixel row between
lines. Character widths vary: most letters advance by 4 pixels, with narrow
letters taking less space and `M` and `W` taking 6 pixels. Squeezed Regular 6
is a [public-domain font by Oliver Kraus](https://github.com/olikraus/u8g2/wiki/fntgrpu8g#squeezed_r6).
Its bitmap data is stored as constants without heap allocation.
On a 128 x 64 OLED, the compact channel/sender line starts at y=14 and message
text at y=22. Five complete 6px-font rows fit. The software comparison fits
three representative 160-character messages in those five rows. Capacity
depends on the characters; messages with many wide letters can still overflow.
Text wraps at character boundaries, with `...` on the last line if necessary.
Unsupported characters appear as `?`. Long channel/sender names are ellipsized
to stay on their own line. An enabled channel footer reserves its own space.
Larger display classes keep their existing font. Menus, Bluetooth PINs and WiFi
setup QR codes keep their normal layout. Small TFTs retain their existing
logical to physical scaling. The normal OLED font uses 7-pixel letters,
8-pixel line spacing and 6-pixel character advances.
The preview buffer holds a complete 160-byte MeshCore message plus its
terminator. Previously the main message UI allocated 78 bytes, leaving room
@@ -36,15 +46,17 @@ Enable `platformio.nimble.ini` in the ignored `platformio.local.ini` as shown
in the [NimBLE trial guide](nimble_companion_trial.md#build), then run:
```sh
OUTPUT_DIR=.releases/v4-pixel5 bash build.sh build-firmware \
heltec_v4_2_v4_3_companion_radio_full_femon_nimble_pixel5 \
--firmware-version v1.17.1.5-halo-keymind-cascade-pixel5-trial \
OUTPUT_DIR=.releases/v4-smallfont bash build.sh build-firmware \
heltec_v4_2_v4_3_companion_radio_full_femon_nimble \
--firmware-version v1.17.1.5-halo-keymind-cascade-squeezed6-trial \
--radio-preset usa-cascadia --profile cascade --standard --require-ota
```
The named trial keeps NimBLE, 350 contacts, 40 channels, the V4's 512-frame
This build keeps NimBLE, 350 contacts, 40 channels, the V4's 512-frame
PSRAM queue, USB mOTA sending and WiFi OTA support. The smaller font is also
the default in ordinary small-screen Companion builds from this source.
The older `_nimble_pixel5` environment name remains available, but also uses
the automatic 5px/6px selection.
Use the application `.bin` for WiFi OTA. A clean USB install uses the merged
image at address 0. When manually writing the application at `0x10000`, an
@@ -66,16 +78,18 @@ python3 -B test/test_firmware_ram.py
pio test -e native -f test_display_driver -f test_companion_message_history
```
The native tests exercise the shared renderer, five-pixel capitals, complete
rows at display edges, long sender lines, overflow markers and tiny/rotated
screen geometry. The additional Adafruit comparison checks all 95 printable
ASCII glyphs pixel for pixel against the original Picopixel font, under
address/undefined-behavior sanitizers. That comparison needs a cached
The native tests exercise both capital heights, automatic font selection,
160-character messages, complete rows at display edges, long sender lines,
reserved footers, overflow markers and tiny/rotated screen geometry. The
additional rendering comparison checks all 95 printable ASCII glyphs in each
font, under address/undefined-behavior sanitizers. Picopixel is compared pixel
for pixel with Adafruit GFX; Squeezed Regular 6 is compared with the upstream
BDF fixture independently of the converted C++ tables. This needs a cached
PlatformIO Adafruit GFX library; set `MESHCORE_GFX_LIBRARY` to its directory
if needed. It reports a skip when the library is absent. The native tests do
not require that dependency. Run only one PlatformIO command at a time.
## Hardware and build results, 2026-09-08
## Earlier 5px hardware and build results, 2026-09-08
Source revision `eeea15ef` passed seven representative firmware builds:
+2 -2
View File
@@ -4,7 +4,7 @@
#include <helpers/ui/CompanionHomeLayout.h>
#include <helpers/ui/CompanionMessageHistory.h>
#if UI_SMALL_MESSAGE_FONT == 1
#include <helpers/ui/Pixel5Text.h>
#include <helpers/ui/SmallMessageText.h>
#endif
#include <helpers/ui/CompanionTransportSelectorLayout.h>
#include "../MyMesh.h"
@@ -1444,7 +1444,7 @@ public:
#if UI_SMALL_MESSAGE_FONT == 1
mesh::ui::drawSmallMessageBody(display, p->origin, p->message,
layout.origin_y, layout.origin_y + 7,
layout.origin_y,
UI_MESSAGE_CHANNEL_FOOTER == 1
? display.height() - layout.filter_height : display.height());
#else
+2 -2
View File
@@ -1,6 +1,6 @@
#include "UITask.h"
#if UI_SMALL_MESSAGE_FONT == 1
#include <helpers/ui/Pixel5Text.h>
#include <helpers/ui/SmallMessageText.h>
#endif
#include <Arduino.h>
#include <helpers/TxtDataHelpers.h>
@@ -246,7 +246,7 @@ void UITask::renderCurrScreen() {
_display->print(_node_prefs->node_name);
#if UI_SMALL_MESSAGE_FONT == 1
mesh::ui::drawSmallMessageBody(*_display, _origin, _msg, 12, 19);
mesh::ui::drawSmallMessageBody(*_display, _origin, _msg, 12);
#else
_display->setCursor(0, 12);
_display->setColor(UIColor::secondary_txt);
+2 -2
View File
@@ -1,6 +1,6 @@
#include "UITask.h"
#if UI_SMALL_MESSAGE_FONT == 1
#include <helpers/ui/Pixel5Text.h>
#include <helpers/ui/SmallMessageText.h>
#endif
#include <helpers/TxtDataHelpers.h>
#include <helpers/ui/BluetoothPairingUiPolicy.h>
@@ -460,7 +460,7 @@ public:
}
int render(DisplayDriver& display) override {
// The 72x40 interface reserves its top 8px for the scrolling status bar.
mesh::ui::drawSmallMessageBody(display, _origin, _message, 10, 17);
mesh::ui::drawSmallMessageBody(display, _origin, _message, 10);
return 1000;
}
bool handleInput(char c) override {
+1
View File
@@ -19,6 +19,7 @@ lib_deps =
${companion_nimble_trial.lib_deps}
lib_ignore = ${companion_nimble_trial.lib_ignore}
; Legacy trial name; UI_SMALL_MESSAGE_FONT now selects 5px or 6px by panel size.
[env:heltec_v4_2_v4_3_companion_radio_full_femon_nimble_pixel5]
extends = env:heltec_v4_2_v4_3_companion_radio_full_femon_nimble
build_flags =
+2 -8
View File
@@ -30,17 +30,11 @@ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#include <stdint.h>
#include "SmallFontGlyph.h"
namespace mesh {
namespace ui {
struct Pixel5Glyph {
uint16_t bitmap_offset;
uint8_t width, height, advance;
int8_t x_offset, y_offset;
};
static const uint8_t pixel5Bitmaps[] = {
0xE8, 0xB4, 0x57, 0xD5, 0xF5, 0x00, 0x4E, 0x3E, 0x80, 0xA5, 0x4A, 0x4A,
0x5A, 0x50, 0xC0, 0x6A, 0x40, 0x95, 0x80, 0xAA, 0x80, 0x5D, 0x00, 0x60,
@@ -58,7 +52,7 @@ static const uint8_t pixel5Bitmaps[] = {
0x90, 0xE8, 0x71, 0xE0, 0xBA, 0x40, 0xB5, 0x80, 0xB5, 0x00, 0x8D, 0x54,
0xAA, 0x80, 0xAC, 0xE0, 0xE5, 0x70, 0x6A, 0x26, 0xFC, 0xC8, 0xAC, 0x5A};
static const Pixel5Glyph pixel5Glyphs[] = {{0, 0, 0, 2, 0, 1}, // 0x20 ' '
static const SmallFontGlyph pixel5Glyphs[] = {{0, 0, 0, 2, 0, 1}, // 0x20 ' '
{0, 1, 5, 2, 0, -4}, // 0x21 '!'
{1, 3, 2, 4, 0, -4}, // 0x22 '"'
{2, 5, 5, 6, 0, -4}, // 0x23 '#'
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <stdint.h>
namespace mesh {
namespace ui {
// Packed monochrome bitmap metrics, with GFX-style baseline offsets.
struct SmallFontGlyph {
uint16_t bitmap_offset;
uint8_t width, height, advance;
int8_t x_offset, y_offset;
};
} // namespace ui
} // namespace mesh
@@ -3,25 +3,40 @@
#include "DisplayDriver.h"
#include "DisplayTextLayout.h"
#include "Pixel5FontData.h"
#include "Squeezed6FontData.h"
namespace mesh {
namespace ui {
// Draw through the common pixel interface so OLED, TFT and U8g2 panels use
// identical glyphs. This stack wrapper leaves the driver's normal font alone.
class Pixel5Text : public DisplayDriver {
class SmallMessageText : public DisplayDriver {
DisplayDriver& _display;
const bool _six_pixel;
int _x = 0, _y = 0;
static const Pixel5Glyph& glyph(uint8_t c) {
return pixel5Glyphs[(c >= 32 && c <= 126 ? c : '?') - 32];
const SmallFontGlyph& glyph(uint8_t c) const {
const int index = (c >= 32 && c <= 126 ? c : '?') - 32;
return _six_pixel ? squeezed6Glyphs[index] : pixel5Glyphs[index];
}
public:
explicit Pixel5Text(DisplayDriver& display)
: DisplayDriver(display.width(), display.height()), _display(display) {}
static constexpr int line_height = 7;
static constexpr int glyph_height = 6; // 5px capitals plus descenders
explicit SmallMessageText(DisplayDriver& display)
: DisplayDriver(display.width(), display.height()), _display(display),
_six_pixel((width() >= 128 && height() >= 64)
|| (width() >= 64 && height() >= 128)) {}
// Preserve Picopixel on very tiny panels, including rotated 72x40 and
// 128x32 screens. This wrapper is used only by the compact message UI.
int capitalHeight() const { return _six_pixel ? 6 : 5; }
int glyphHeight() const { return capitalHeight() + 1; } // descenders
int lineHeight() const { return glyphHeight() + 1; } // blank row
int lineCount(int y, int bottom = -1) const {
if (bottom < 0 || bottom > height()) bottom = height();
const int remaining = bottom - y;
return remaining < glyphHeight() ? 0
: 1 + (remaining - glyphHeight()) / lineHeight();
}
bool isOn() override { return _display.isOn(); }
void turnOn() override { _display.turnOn(); }
@@ -48,13 +63,14 @@ public:
}
void print(const char* str) override {
while (*str) {
const Pixel5Glyph& g = glyph((uint8_t)*str++);
const SmallFontGlyph& g = glyph((uint8_t)*str++);
const uint8_t* bitmap = _six_pixel ? squeezed6Bitmaps : pixel5Bitmaps;
for (int row = 0; row < g.height; ++row) {
const int y = _y + 4 + g.y_offset + row;
const int y = _y + capitalHeight() - 1 + g.y_offset + row;
if (y < 0 || y >= height()) continue;
for (int col = 0; col < g.width; ++col) {
const int bit = row * g.width + col;
if (!(pixel5Bitmaps[g.bitmap_offset + bit / 8]
if (!(bitmap[g.bitmap_offset + bit / 8]
& (0x80 >> (bit % 8)))) continue;
const int x = _x + g.x_offset + col;
if (x >= 0 && x < width()) _display.fillRect(x, y, 1, 1);
@@ -65,32 +81,23 @@ public:
}
void printWordWrap(const char* str, int max_width) override {
if (max_width > width() - _x) max_width = width() - _x;
const int lines = (height() - _y + line_height - glyph_height) / line_height;
drawTextWrapped(*this, _x, _y, max_width, line_height, lines, str);
drawTextWrapped(*this, _x, _y, max_width, lineHeight(), lineCount(_y), str);
}
};
inline int smallMessageLineCount(int height, int message_y) {
const int remaining = height - message_y;
return remaining < Pixel5Text::glyph_height ? 0
: (remaining + Pixel5Text::line_height - Pixel5Text::glyph_height)
/ Pixel5Text::line_height;
}
inline void drawSmallMessageBody(DisplayDriver& display, const char* origin,
const char* message, int origin_y = 14,
int message_y = 21, int bottom = -1) {
Pixel5Text text(display);
int bottom = -1) {
SmallMessageText text(display);
const int message_y = origin_y + text.lineHeight();
char translated[161];
text.translateUTF8ToBlocks(translated, origin, sizeof(translated));
text.setColor(UIColor::secondary_txt);
text.drawTextEllipsized(0, origin_y, display.width(), translated);
text.translateUTF8ToBlocks(translated, message, sizeof(translated));
text.setColor(UIColor::primary_txt);
const int height = bottom < 0 || bottom > display.height()
? display.height() : bottom;
drawTextWrapped(text, 0, message_y, display.width(), Pixel5Text::line_height,
smallMessageLineCount(height, message_y), translated);
drawTextWrapped(text, 0, message_y, display.width(), text.lineHeight(),
text.lineCount(message_y, bottom), translated);
}
} // namespace ui
+134
View File
@@ -0,0 +1,134 @@
#pragma once
// Squeezed Regular 6 by Oliver Kraus. Public domain.
// Lossless conversion of the ASCII glyphs from the upstream BDF.
// Source: https://raw.githubusercontent.com/olikraus/u8g2/57d191014bebc263868526491e7f7c33ddcf6042/tools/font/bdf/u8g2_squeezed_regular_6.bdf
// BDF SHA-256: a50740bd62accaa104e52da98210b3db10e03d75c6fbac1a31010177fad92277
// Reference: test/fixtures/small_message_font/squeezed6.bdf
#include "SmallFontGlyph.h"
namespace mesh {
namespace ui {
static const uint8_t squeezed6Bitmaps[] = {
0xF4, 0xB4, 0x57, 0xD5, 0xF5, 0x00, 0x23, 0xE8, 0xE2, 0xF8, 0x80, 0xA5,
0x29, 0x40, 0x4A, 0x4A, 0xA5, 0xC0, 0x6A, 0x90, 0x95, 0x60, 0xAB, 0xAA,
0x5D, 0x00, 0xC0, 0xC0, 0x80, 0x25, 0x29, 0x00, 0x56, 0xDA, 0x80, 0x75,
0x50, 0x96, 0xB0, 0xD9, 0x60, 0xB7, 0x92, 0x40, 0xE9, 0x60, 0x53, 0x5A,
0x80, 0xE5, 0x24, 0x80, 0x55, 0x5A, 0x80, 0x55, 0x92, 0x80, 0xA0, 0x98,
0x64, 0xCC, 0x98, 0x9A, 0x20, 0x69, 0xBB, 0x87, 0x57, 0xDB, 0x40, 0xD7,
0x5B, 0x80, 0x6A, 0x90, 0xD6, 0xDB, 0x80, 0xEE, 0xB0, 0xEE, 0xA0, 0x72,
0xDA, 0xC0, 0xB7, 0xDB, 0x40, 0xFC, 0x55, 0x70, 0xB7, 0x5B, 0x40, 0xAA,
0xB0, 0x8E, 0xEB, 0x18, 0xC4, 0x9D, 0xB9, 0x99, 0x56, 0xDA, 0x80, 0xD7,
0x49, 0x00, 0x56, 0xDA, 0xC0, 0xD7, 0x5B, 0x40, 0x69, 0x60, 0xE9, 0x24,
0x80, 0xB6, 0xDB, 0xC0, 0xB6, 0xD4, 0x80, 0x8C, 0x63, 0x5A, 0xA8, 0xB5,
0x2B, 0x40, 0xB6, 0xA4, 0x80, 0xD6, 0xB0, 0xEA, 0xB0, 0x91, 0x22, 0x40,
0xD5, 0x70, 0x54, 0xC0, 0x90, 0x76, 0xB0, 0x93, 0x5B, 0x80, 0x69, 0x25,
0xDA, 0xC0, 0x57, 0x30, 0x6E, 0xA0, 0x75, 0x9C, 0x93, 0x5B, 0x40, 0xBC,
0x45, 0x58, 0x92, 0xEB, 0x40, 0xFC, 0xF5, 0x6B, 0x50, 0xD6, 0xD0, 0x56,
0xA0, 0xD6, 0xE8, 0x76, 0xB2, 0xBA, 0x40, 0x66, 0xAE, 0x90, 0xB6, 0xF0,
0xB6, 0xE0, 0x8D, 0x6A, 0xA0, 0xA9, 0x50, 0xB5, 0x94, 0xDB, 0x2B, 0x24,
0x40, 0xFC, 0x89, 0xA5, 0x00, 0x5A,
};
static const SmallFontGlyph squeezed6Glyphs[] = {
{0, 0, 0, 2, 0, 1}, // ASCII 32
{0, 1, 6, 2, 0, -5}, // ASCII 33
{1, 3, 2, 4, 0, -5}, // ASCII 34
{2, 5, 5, 6, 0, -4}, // ASCII 35
{6, 5, 7, 6, 0, -5}, // ASCII 36
{11, 3, 6, 4, 0, -5}, // ASCII 37
{14, 4, 6, 5, 0, -5}, // ASCII 38
{17, 1, 2, 2, 0, -5}, // ASCII 39
{18, 2, 6, 3, 0, -5}, // ASCII 40
{20, 2, 6, 3, 0, -5}, // ASCII 41
{22, 3, 5, 4, 0, -5}, // ASCII 42
{24, 3, 3, 4, 0, -4}, // ASCII 43
{26, 1, 2, 2, 0, 0}, // ASCII 44
{27, 2, 1, 3, 0, -3}, // ASCII 45
{28, 1, 1, 2, 0, 0}, // ASCII 46
{29, 3, 6, 4, 0, -5}, // ASCII 47
{32, 3, 6, 4, 0, -5}, // ASCII 48
{35, 2, 6, 3, 0, -5}, // ASCII 49
{37, 2, 6, 3, 0, -5}, // ASCII 50
{39, 2, 6, 3, 0, -5}, // ASCII 51
{41, 3, 6, 4, 0, -5}, // ASCII 52
{44, 2, 6, 3, 0, -5}, // ASCII 53
{46, 3, 6, 4, 0, -5}, // ASCII 54
{49, 3, 6, 4, 0, -5}, // ASCII 55
{52, 3, 6, 4, 0, -5}, // ASCII 56
{55, 3, 6, 4, 0, -5}, // ASCII 57
{58, 1, 3, 2, 0, -3}, // ASCII 58
{59, 1, 5, 2, 0, -3}, // ASCII 59
{60, 2, 3, 3, 0, -4}, // ASCII 60
{61, 2, 3, 3, 0, -4}, // ASCII 61
{62, 2, 3, 3, 0, -4}, // ASCII 62
{63, 2, 6, 3, 0, -5}, // ASCII 63
{65, 4, 6, 5, 0, -5}, // ASCII 64
{68, 3, 6, 4, 0, -5}, // ASCII 65
{71, 3, 6, 4, 0, -5}, // ASCII 66
{74, 2, 6, 3, 0, -5}, // ASCII 67
{76, 3, 6, 4, 0, -5}, // ASCII 68
{79, 2, 6, 3, 0, -5}, // ASCII 69
{81, 2, 6, 3, 0, -5}, // ASCII 70
{83, 3, 6, 4, 0, -5}, // ASCII 71
{86, 3, 6, 4, 0, -5}, // ASCII 72
{89, 1, 6, 2, 0, -5}, // ASCII 73
{90, 2, 6, 3, 0, -5}, // ASCII 74
{92, 3, 6, 4, 0, -5}, // ASCII 75
{95, 2, 6, 3, 0, -5}, // ASCII 76
{97, 5, 6, 6, 0, -5}, // ASCII 77
{101, 4, 6, 5, 0, -5}, // ASCII 78
{104, 3, 6, 4, 0, -5}, // ASCII 79
{107, 3, 6, 4, 0, -5}, // ASCII 80
{110, 3, 6, 4, 0, -5}, // ASCII 81
{113, 3, 6, 4, 0, -5}, // ASCII 82
{116, 2, 6, 3, 0, -5}, // ASCII 83
{118, 3, 6, 4, 0, -5}, // ASCII 84
{121, 3, 6, 4, 0, -5}, // ASCII 85
{124, 3, 6, 4, 0, -5}, // ASCII 86
{127, 5, 6, 6, 0, -5}, // ASCII 87
{131, 3, 6, 4, 0, -5}, // ASCII 88
{134, 3, 6, 4, 0, -5}, // ASCII 89
{137, 2, 6, 3, 0, -5}, // ASCII 90
{139, 2, 6, 3, 0, -5}, // ASCII 91
{141, 3, 6, 4, 0, -5}, // ASCII 92
{144, 2, 6, 3, 0, -5}, // ASCII 93
{146, 3, 2, 4, 0, -5}, // ASCII 94
{147, 2, 1, 3, 0, 0}, // ASCII 95
{148, 2, 2, 3, 0, -5}, // ASCII 96
{149, 3, 4, 4, 0, -3}, // ASCII 97
{151, 3, 6, 4, 0, -5}, // ASCII 98
{154, 2, 4, 3, 0, -3}, // ASCII 99
{155, 3, 6, 4, 0, -5}, // ASCII 100
{158, 3, 4, 4, 0, -3}, // ASCII 101
{160, 2, 6, 3, 0, -5}, // ASCII 102
{162, 3, 5, 4, 0, -3}, // ASCII 103
{164, 3, 6, 4, 0, -5}, // ASCII 104
{167, 1, 6, 2, 0, -5}, // ASCII 105
{168, 2, 7, 3, 0, -5}, // ASCII 106
{170, 3, 6, 4, 0, -5}, // ASCII 107
{173, 1, 6, 2, 0, -5}, // ASCII 108
{174, 5, 4, 6, 0, -3}, // ASCII 109
{177, 3, 4, 4, 0, -3}, // ASCII 110
{179, 3, 4, 4, 0, -3}, // ASCII 111
{181, 3, 5, 4, 0, -3}, // ASCII 112
{183, 3, 5, 4, 0, -3}, // ASCII 113
{185, 3, 4, 4, 0, -3}, // ASCII 114
{187, 2, 4, 3, 0, -3}, // ASCII 115
{188, 2, 6, 3, 0, -5}, // ASCII 116
{190, 3, 4, 4, 0, -3}, // ASCII 117
{192, 3, 4, 4, 0, -3}, // ASCII 118
{194, 5, 4, 6, 0, -3}, // ASCII 119
{197, 3, 4, 4, 0, -3}, // ASCII 120
{199, 3, 5, 4, 0, -3}, // ASCII 121
{201, 2, 4, 3, 0, -3}, // ASCII 122
{202, 3, 6, 4, 0, -5}, // ASCII 123
{205, 1, 6, 2, 0, -5}, // ASCII 124
{206, 3, 6, 4, 0, -5}, // ASCII 125
{209, 4, 2, 5, 0, -5}, // ASCII 126
};
} // namespace ui
} // namespace mesh
File diff suppressed because it is too large. Load diff
+23 -4
View File
@@ -1,5 +1,5 @@
#include <helpers/ui/SSD1306Display.h>
#include <helpers/ui/Pixel5Text.h>
#include <helpers/ui/SmallMessageText.h>
#include <Fonts/Picopixel.h>
#include <cassert>
#include <fstream>
@@ -12,8 +12,13 @@ void save(const char* file) {
f << "P1\n128 64\n";
for (int y=0;y<64;y++) { for (int x=0;x<128;x++) f << c.getPixel(x,y) << ' '; f << '\n'; }
}
class TestSSD1306Display : public SSD1306Display {
public:
void setCanvasSize(int w, int h) { setDimensions(w, h); }
};
int main(int argc, char** argv) {
SSD1306Display d;
TestSSD1306Display d;
assert(d.begin());
auto& c=*Adafruit_SSD1306::last;
const char* message="GIANT KILLER: Out on the Mercerwood mesh today, just the V4 and a small battery Checking the smaller font so the rest of this message is visible. 0123456789 END";
@@ -32,7 +37,21 @@ int main(int argc, char** argv) {
#if UI_SMALL_MESSAGE_FONT == 1
assert(c.outside==0);
assert(d.getTextWidth("ABC")==18); // regular driver font untouched
mesh::ui::Pixel5Text text(d);
mesh::ui::SmallMessageText six(d);
assert(six.capitalHeight()==6);
for (int ch=32;ch<=126;ch++) {
d.startFrame(); six.setCursor(0,0);
const std::string glyph(1,char(ch));
six.print(glyph.c_str());
// Compared with the independent upstream BDF in the Python harness.
std::cout << "six " << ch << ' ' << six.getTextWidth(glyph.c_str()) << ' ';
for (int y=0;y<8;y++) for (int x=0;x<8;x++)
std::cout << int(c.getPixel(x,y));
std::cout << '\n';
}
d.setCanvasSize(72,40);
mesh::ui::SmallMessageText text(d);
assert(text.capitalHeight()==5);
GFXcanvas1 reference(128,64);
reference.setFont(&Picopixel);
reference.setTextColor(1);
@@ -44,6 +63,6 @@ int main(int argc, char** argv) {
assert(c.getPixel(x,y)==reference.getPixel(x,y));
assert(text.getTextWidth(std::string(1,char(ch)).c_str())==PicopixelGlyphs[ch-32].xAdvance);
}
std::cout << "95 glyphs match Adafruit GFX Picopixel pixel for pixel; normal font preserved\n";
std::cout << "95 tiny-screen glyphs match Adafruit GFX Picopixel; normal font preserved\n";
#endif
}
+105 -17
View File
@@ -3,7 +3,7 @@
#include <helpers/ui/DisplayDriver.h>
#include <helpers/ui/CompanionHomeLayout.h>
#include <helpers/ui/DisplayTextLayout.h>
#include <helpers/ui/Pixel5Text.h>
#include <helpers/ui/SmallMessageText.h>
#include <helpers/ui/WiFiSetupQrDisplay.h>
#include <helpers/ui/WiFiSetupQrPayload.h>
@@ -134,9 +134,9 @@ TEST(DisplayDriver, EllipsizesOnlyAtUTF8CodepointBoundaries) {
EXPECT_TRUE(isValidUTF8(display.printed.c_str()));
}
TEST(Pixel5Text, CapitalsAreFivePixelsAndNormalFontIsUntouched) {
TestDisplay display(128, 64, 6);
mesh::ui::Pixel5Text text(display);
TEST(SmallMessageText, TinyPanelsKeepFivePixelCapitalsAndNormalFont) {
TestDisplay display(72, 40, 6);
mesh::ui::SmallMessageText text(display);
text.setCursor(0, 0);
text.print("A");
const char* rows[] = {"010", "101", "111", "101", "101"};
@@ -152,7 +152,41 @@ TEST(Pixel5Text, CapitalsAreFivePixelsAndNormalFontIsUntouched) {
EXPECT_EQ(18, display.getTextWidth("ABC"));
}
TEST(Pixel5Text, CompactOriginAllowsSixCompleteMessageRows) {
TEST(SmallMessageText, LargerPanelsUseSixPixelCapitalsAndNormalFont) {
TestDisplay display(128, 64, 6);
mesh::ui::SmallMessageText text(display);
text.setCursor(0, 0);
text.print("A");
const char* rows[] = {"010", "101", "111", "101", "101", "101"};
bool pixels[6][3] = {};
for (const auto& pixel : display.fills) {
ASSERT_GE(pixel.x, 0); ASSERT_LT(pixel.x, 3);
ASSERT_GE(pixel.y, 0); ASSERT_LT(pixel.y, 6);
pixels[pixel.y][pixel.x] = true;
}
for (int y = 0; y < 6; ++y) for (int x = 0; x < 3; ++x)
EXPECT_EQ(rows[y][x] == '1', pixels[y][x]);
EXPECT_EQ(11, text.getTextWidth("ABC"));
EXPECT_EQ(18, display.getTextWidth("ABC"));
}
TEST(SmallMessageText, FontChoiceAndLineSpacingFollowPanelGeometry) {
const int cases[][3] = {
{128, 64, 6}, {64, 128, 6}, {160, 80, 6},
{72, 40, 5}, {40, 72, 5}, {128, 32, 5}, {32, 128, 5},
{64, 48, 5}, {48, 64, 5}};
for (const auto& dimensions : cases) {
TestDisplay display(dimensions[0], dimensions[1]);
mesh::ui::SmallMessageText text(display);
EXPECT_EQ(dimensions[2], text.capitalHeight());
EXPECT_EQ(dimensions[2] + 1, text.glyphHeight());
EXPECT_EQ(dimensions[2] + 2, text.lineHeight());
EXPECT_EQ(0, text.lineCount(display.height() - text.glyphHeight() + 1));
EXPECT_EQ(1, text.lineCount(display.height() - text.glyphHeight()));
}
}
TEST(SmallMessageText, CompactOriginAllowsFiveCompleteSixPixelMessageRows) {
TestDisplay display(128, 64);
mesh::ui::drawSmallMessageBody(display, std::string(61, 'W').c_str(),
std::string(160, 'W').c_str());
@@ -162,33 +196,87 @@ TEST(Pixel5Text, CompactOriginAllowsSixCompleteMessageRows) {
EXPECT_GE(pixel.x, 0); EXPECT_LT(pixel.x, 128);
EXPECT_GE(pixel.y, 14); EXPECT_LT(pixel.y, 64);
// The origin stays in its own line; its long name cannot cover the body.
EXPECT_TRUE(pixel.y < 20 || pixel.y >= 21);
EXPECT_TRUE(pixel.y < 21 || pixel.y >= 22);
for (int i = 0; i < 3; ++i)
if (pixel.y == 60 && pixel.x == 120 + 2 * i) final_dots[i] = true;
if (pixel.y == 59 && pixel.x == 120 + 2 * i) final_dots[i] = true;
}
for (bool dot : final_dots) EXPECT_TRUE(dot);
EXPECT_EQ(5, mesh::ui::smallMessageLineCount(64, 25));
EXPECT_EQ(6, mesh::ui::smallMessageLineCount(64, 21));
EXPECT_EQ(3, mesh::ui::smallMessageLineCount(40, 17));
EXPECT_EQ(0, mesh::ui::smallMessageLineCount(64, 59));
mesh::ui::SmallMessageText text(display);
EXPECT_EQ(5, text.lineCount(22));
EXPECT_EQ(4, text.lineCount(22, 56));
EXPECT_EQ(5, text.lineCount(22, 80)); // never extend past the panel
EXPECT_EQ(0, text.lineCount(22, 20));
}
TEST(Pixel5Text, GlyphsStayInsideTinyAndRotatedScreens) {
for (auto dimensions : {std::pair<int, int>{72, 40}, {64, 128}, {128, 64}}) {
TEST(SmallMessageText, TinyInterfaceKeepsThreeCompleteFivePixelRows) {
TestDisplay display(72, 40);
mesh::ui::SmallMessageText text(display);
EXPECT_EQ(3, text.lineCount(17));
mesh::ui::drawSmallMessageBody(display, std::string(61, 'W').c_str(),
std::string(160, 'W').c_str(), 10);
bool final_dots[3] = {};
for (const auto& pixel : display.fills) {
EXPECT_GE(pixel.x, 0); EXPECT_LT(pixel.x, 72);
EXPECT_GE(pixel.y, 10); EXPECT_LT(pixel.y, 40);
EXPECT_TRUE(pixel.y < 16 || pixel.y >= 17);
for (int i = 0; i < 3; ++i)
if (pixel.y == 35 && pixel.x == 60 + 2 * i) final_dots[i] = true;
}
for (bool dot : final_dots) EXPECT_TRUE(dot);
}
TEST(SmallMessageText, MessageBodyRespectsReservedFooter) {
TestDisplay display(128, 64);
mesh::ui::drawSmallMessageBody(display, "Ch 0 Public [4h]:",
std::string(160, 'W').c_str(), 14, 56);
ASSERT_FALSE(display.fills.empty());
for (const auto& pixel : display.fills) EXPECT_LT(pixel.y, 56);
}
TEST(SmallMessageText, Full160CharacterSamplesFitInFiveRows) {
class RecordingText : public mesh::ui::SmallMessageText {
public:
std::string drawn;
explicit RecordingText(DisplayDriver& display) : SmallMessageText(display) {}
void print(const char* str) override {
drawn += str;
SmallMessageText::print(str);
}
};
const char* samples[] = {
"GIANT KILLER: Out on the Mercerwood mesh today, just the V4 and a small "
"battery Checking the smaller font so the rest of this message is visible. "
"0123456789 END",
"NimBLE-XIAO-Trial: Picopixel: 5 pixel letters and a compact channel line. "
"More of this message now fits on the small screen. "
"0123456789 0123456789 012345678 END"};
for (const char* sample : samples) {
ASSERT_EQ(160U, strlen(sample));
TestDisplay display(128, 64);
RecordingText text(display);
EXPECT_EQ(5, mesh::ui::drawTextWrapped(text, 0, 22, 128,
text.lineHeight(), text.lineCount(22), sample));
EXPECT_EQ(sample, text.drawn);
}
}
TEST(SmallMessageText, GlyphsStayInsideTinyAndRotatedScreens) {
for (auto dimensions : {std::pair<int, int>{72, 40}, {40, 72},
{128, 32}, {32, 128}, {64, 128}, {128, 64}}) {
TestDisplay display(dimensions.first, dimensions.second);
mesh::ui::Pixel5Text text(display);
mesh::ui::SmallMessageText text(display);
for (int c = 32; c <= 126; ++c) {
display.fills.clear();
text.setCursor(0, display.height() - 6);
text.setCursor(0, display.height() - text.glyphHeight());
text.printWordWrap(std::string(160, char(c)).c_str(), display.width());
for (const auto& pixel : display.fills) {
EXPECT_GE(pixel.x, 0); EXPECT_LT(pixel.x, display.width());
EXPECT_GE(pixel.y, display.height() - 6);
EXPECT_GE(pixel.y, display.height() - text.glyphHeight());
EXPECT_LT(pixel.y, display.height());
}
}
display.fills.clear();
text.setCursor(0, display.height() - 5);
text.setCursor(0, display.height() - text.glyphHeight() + 1);
text.printWordWrap("must not start a clipped final line", display.width());
EXPECT_TRUE(display.fills.empty());
}
+28 -3
View File
@@ -3,11 +3,13 @@
Build a V4 environment first to install Adafruit GFX, or point
MESHCORE_GFX_LIBRARY to that library's directory. Only the hardware panel,
Arduino strings and Print glue are replaced; the shared pixel renderer is compared with the original Adafruit font.
Arduino strings and Print glue are replaced. Picopixel is compared with the
original Adafruit font; Squeezed Regular 6 with its upstream BDF glyphs.
"""
import os
from pathlib import Path
import re
import subprocess
import tempfile
import unittest
@@ -16,7 +18,28 @@ ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "test/fixtures/ssd1306_picopixel"
class SSD1306PicopixelTest(unittest.TestCase):
class SSD1306SmallMessageFontTest(unittest.TestCase):
def assert_squeezed_glyphs_match_upstream(self, output):
rendered = {}
for line in output.splitlines():
if line.startswith("six "):
_, code, advance, pixels = line.split()
rendered[int(code)] = (int(advance), pixels)
self.assertEqual(set(rendered), set(range(32, 127)))
source = (ROOT / "test/fixtures/small_message_font/squeezed6.bdf").read_text()
for entry in source.split("STARTCHAR ")[1:]:
code = int(re.search(r"^ENCODING (\d+)", entry, re.M)[1])
advance = int(re.search(r"^DWIDTH (\d+)", entry, re.M)[1])
w, h, x, y = map(int, re.search(r"^BBX (.+)$", entry, re.M)[1].split())
rows = entry.split("BITMAP\n", 1)[1].split("ENDCHAR", 1)[0].splitlines()
expected = ["0"] * 64
for row, bits in enumerate(rows):
for col in range(w):
if int(bits, 16) & (1 << (len(bits) * 4 - 1 - col)):
expected[(6 - h - y + row) * 8 + x + col] = "1"
with self.subTest(glyph=chr(code)):
self.assertEqual(rendered[code], (advance, "".join(expected)))
def test_actual_font_bounds_wrapping_and_default_font_restoration(self):
configured = os.environ.get("MESHCORE_GFX_LIBRARY")
candidates = [Path(configured)] if configured else sorted(
@@ -29,7 +52,7 @@ class SSD1306PicopixelTest(unittest.TestCase):
with tempfile.TemporaryDirectory(prefix="meshcore-picopixel-") as temp:
for enabled in (0, 1):
with self.subTest(picopixel=enabled):
with self.subTest(small_message_font=enabled):
binary = Path(temp) / f"render-{enabled}"
result = subprocess.run([
"c++", "-std=c++17", "-g",
@@ -45,6 +68,8 @@ class SSD1306PicopixelTest(unittest.TestCase):
result = subprocess.run(
[str(binary)], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
if enabled:
self.assert_squeezed_glyphs_match_upstream(result.stdout)
if __name__ == "__main__":