Use readable 6px message font and keep 5px on tiny panels
This commit is contained in:
1 parent
03e42830b0
commit
469b47d190
14 files changed
+1550
-93
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 '#'
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+1159
File diff suppressed because it is too large.
Load diff
+23
-4
@@ -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
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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__":
|
||||
|
||||
Reference in new issue
Block a user