mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-16 13:32:35 +00:00
Use compact message text and sender lines on all small displays
This commit is contained in:
@@ -22,8 +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.
|
||||
|
||||
An additional [V4 Picopixel trial](v4_pixel5_font_trial.md) uses 5-pixel capital
|
||||
letters and a longer on-device message preview.
|
||||
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.
|
||||
|
||||
## Build
|
||||
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
# V4 Picopixel message font trial
|
||||
# Small-screen Picopixel message font
|
||||
|
||||
This optional V4.2/V4.3 OLED Full Companion build uses Adafruit's Picopixel
|
||||
font for received message text. Capital letters are 5 pixels high; descenders
|
||||
can occupy a sixth pixel, so lines advance by 7 pixels. Most characters
|
||||
advance by 4 pixels horizontally, compared with the normal font's 6 pixels.
|
||||
The normal font has 7-pixel letters and an 8-pixel line height.
|
||||
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.
|
||||
|
||||
The title, channel/sender, menus, Bluetooth PIN and WiFi QR page retain their
|
||||
normal font. The message preview buffer increases from 78 bytes (77 text
|
||||
bytes plus a terminator) to 161 bytes, enough for a complete 160-byte MeshCore
|
||||
message. The screen retains 32 previews; this is separate from the V4's
|
||||
512-frame offline queue and its mOTA policy.
|
||||
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.
|
||||
|
||||
Five complete Picopixel message lines fit below the title and sender on the
|
||||
128 x 64 OLED. A typical message can fit in full; wide letters can still
|
||||
exceed the available space. The last line then ends with `...`. Text wraps
|
||||
at character boundaries. Unsupported characters appear as `?`.
|
||||
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.
|
||||
|
||||
## Build and install
|
||||
The preview buffer holds a complete 160-byte MeshCore message plus its
|
||||
terminator. Previously the main message UI allocated 78 bytes, leaving room
|
||||
for only 77 bytes of text. It still retains 32 previews; larger records add
|
||||
about 2.8 KB of RAM. The firmware RAM guard includes that increase. This
|
||||
history is separate from the offline queue and its mOTA policy.
|
||||
|
||||
The tiny 72 x 40 T-Echo Card interface now previews the latest received
|
||||
message below its status bar, with three small-font message rows. A button
|
||||
press dismisses it. It stores one full message, and shows `...` when the
|
||||
screen fills. Incoming text does not replace an active Bluetooth pairing PIN.
|
||||
|
||||
## V4 hardware trial
|
||||
|
||||
Enable `platformio.nimble.ini` in the ignored `platformio.local.ini` as shown
|
||||
in the [NimBLE trial guide](nimble_companion_trial.md#build), then run:
|
||||
@@ -29,26 +42,32 @@ OUTPUT_DIR=.releases/v4-pixel5 bash build.sh build-firmware \
|
||||
--radio-preset usa-cascadia --profile cascade --standard --require-ota
|
||||
```
|
||||
|
||||
The named trial 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.
|
||||
|
||||
For a V4 already using the matching 16 MB Full Companion partition layout,
|
||||
flash the application `.bin` at `0x10000`, or use it for WiFi OTA. A clean
|
||||
USB install uses the merged image at address 0. The trial preserves NimBLE,
|
||||
350 contacts, 40 channels, USB mOTA sending and WiFi OTA support.
|
||||
USB install uses the merged image at address 0.
|
||||
|
||||
To restore the normal message font, install
|
||||
`heltec_v4_2_v4_3_companion_radio_full_femon_nimble`. Both builds use the same
|
||||
partition layout; erasing settings is unnecessary.
|
||||
A custom build can set `-D UI_SMALL_MESSAGE_FONT=0` to restore the old font
|
||||
and spacing. Remove any explicit `UI_MSG_PREVIEW_SIZE` flag too if the old
|
||||
preview capacity is desired. These are compile-time options, not CLI commands.
|
||||
No settings erase is necessary when changing between matching V4 layouts.
|
||||
|
||||
## Verification
|
||||
|
||||
```sh
|
||||
python3 -B test/test_ssd1306_picopixel.py
|
||||
python3 -B test/test_firmware_ram.py
|
||||
pio test -e native -f test_display_driver -f test_companion_message_history
|
||||
```
|
||||
|
||||
The font test uses the real SSD1306 driver, Adafruit GFX rendering and
|
||||
Picopixel bitmaps with a host panel double. It checks a full 160-character
|
||||
sample, all printable ASCII characters, descenders, rotations, bottom-edge
|
||||
clipping, overflow markers, unsupported-character placeholders and restoring
|
||||
the normal font. It runs with address/undefined-behavior sanitizers. It needs
|
||||
the Adafruit GFX dependency from an existing PlatformIO build; without that
|
||||
dependency it reports a skip. Run only one PlatformIO command at a time.
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include <helpers/ui/BluetoothPairingUiPolicy.h>
|
||||
#include <helpers/ui/CompanionHomeLayout.h>
|
||||
#include <helpers/ui/CompanionMessageHistory.h>
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
#include <helpers/ui/Pixel5Text.h>
|
||||
#endif
|
||||
#include <helpers/ui/CompanionTransportSelectorLayout.h>
|
||||
#include "../MyMesh.h"
|
||||
#include "../CompanionWiFi.h"
|
||||
@@ -1171,7 +1174,11 @@ public:
|
||||
};
|
||||
|
||||
#ifndef UI_MSG_PREVIEW_SIZE
|
||||
#define UI_MSG_PREVIEW_SIZE 78
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
#define UI_MSG_PREVIEW_SIZE 161
|
||||
#else
|
||||
#define UI_MSG_PREVIEW_SIZE 78
|
||||
#endif
|
||||
#endif
|
||||
#ifndef UI_COMPACT_MESSAGE_STATUS
|
||||
#define UI_COMPACT_MESSAGE_STATUS 0
|
||||
@@ -1407,7 +1414,7 @@ public:
|
||||
display.setCursor(0, 0);
|
||||
display.setTextSize(1);
|
||||
display.setColor(UIColor::corp_blue);
|
||||
snprintf(tmp, sizeof(tmp), "Message %d/%d",
|
||||
snprintf(tmp, sizeof(tmp), display.width() < 100 ? "%d/%d" : "Message %d/%d",
|
||||
filtered_count == 0 ? 0 : view_offset + 1, filtered_count);
|
||||
display.print(tmp);
|
||||
|
||||
@@ -1425,12 +1432,22 @@ public:
|
||||
|
||||
mesh::ui::formatCompanionMessageAge(
|
||||
tmp, sizeof(tmp), companionMessageElapsedMillis(p->heard_millis));
|
||||
if (display.width() < 100) {
|
||||
char* suffix = strchr(tmp, ' ');
|
||||
if (suffix != nullptr) *suffix = 0;
|
||||
}
|
||||
display.setCursor(display.width() - display.getTextWidth(tmp) - 2, 0);
|
||||
display.print(tmp);
|
||||
|
||||
display.drawRect(0, layout.header_divider_y, display.width(), 1);
|
||||
display.setCompactText(false);
|
||||
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
mesh::ui::drawSmallMessageBody(display, p->origin, p->message,
|
||||
layout.origin_y, layout.origin_y + 7,
|
||||
UI_MESSAGE_CHANNEL_FOOTER == 1
|
||||
? display.height() - layout.filter_height : display.height());
|
||||
#else
|
||||
display.setCursor(0, layout.origin_y);
|
||||
display.setColor(UIColor::secondary_txt);
|
||||
char filtered_origin[sizeof(p->origin)];
|
||||
@@ -1442,6 +1459,7 @@ public:
|
||||
char filtered_msg[sizeof(p->message)];
|
||||
display.translateUTF8ToBlocks(filtered_msg, p->message, sizeof(filtered_msg));
|
||||
display.printWordWrap(filtered_msg, display.width());
|
||||
#endif
|
||||
|
||||
renderChannelFilter(display);
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include "UITask.h"
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
#include <helpers/ui/Pixel5Text.h>
|
||||
#endif
|
||||
#include <Arduino.h>
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include <helpers/ui/BluetoothPairingUiPolicy.h>
|
||||
@@ -242,6 +245,9 @@ void UITask::renderCurrScreen() {
|
||||
_display->setColor(UIColor::primary_txt);
|
||||
_display->print(_node_prefs->node_name);
|
||||
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
mesh::ui::drawSmallMessageBody(*_display, _origin, _msg, 12, 19);
|
||||
#else
|
||||
_display->setCursor(0, 12);
|
||||
_display->setColor(UIColor::secondary_txt);
|
||||
_display->print(_origin);
|
||||
@@ -253,6 +259,7 @@ void UITask::renderCurrScreen() {
|
||||
_display->setColor(UIColor::primary_txt);
|
||||
sprintf(tmp, "%d", _msgcount);
|
||||
_display->print(tmp);
|
||||
#endif
|
||||
_display->setColor(UIColor::secondary_txt); // last color will be kept on T114
|
||||
} else if (!pairing_screen_active && (millis() - ui_started_at) < BOOT_SCREEN_MILLIS) { // boot screen
|
||||
// meshcore logo
|
||||
|
||||
@@ -31,7 +31,11 @@ class UITask : public AbstractUITask {
|
||||
CompanionNodePrefs* _node_prefs;
|
||||
char _version_info[32];
|
||||
char _origin[62];
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
char _msg[161];
|
||||
#else
|
||||
char _msg[80];
|
||||
#endif
|
||||
char _alert[80];
|
||||
int _msgcount;
|
||||
bool _need_refresh = true;
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include "UITask.h"
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
#include <helpers/ui/Pixel5Text.h>
|
||||
#endif
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include <helpers/ui/BluetoothPairingUiPolicy.h>
|
||||
#include "../MyMesh.h"
|
||||
@@ -439,6 +442,38 @@ public:
|
||||
};
|
||||
|
||||
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
class TinyMessageScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
char _origin[62] = {};
|
||||
char _message[161] = {};
|
||||
public:
|
||||
explicit TinyMessageScreen(UITask* task) : _task(task) {}
|
||||
void setMessage(uint8_t path_len, const char* from, const char* message) {
|
||||
if (path_len == 0xFF) {
|
||||
snprintf(_origin, sizeof(_origin), "%s [direct]:", from);
|
||||
} else {
|
||||
snprintf(_origin, sizeof(_origin), "%s [%uh]:", from,
|
||||
(unsigned int)path_len);
|
||||
}
|
||||
StrHelper::strncpy(_message, message, sizeof(_message));
|
||||
}
|
||||
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);
|
||||
return 1000;
|
||||
}
|
||||
bool handleInput(char c) override {
|
||||
if (c == KEY_NEXT || c == KEY_PREV || c == KEY_LEFT || c == KEY_RIGHT
|
||||
|| c == KEY_ENTER) {
|
||||
_task->gotoHomeScreen();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNodePrefs* node_prefs) {
|
||||
_display = display;
|
||||
_sensors = sensors;
|
||||
@@ -478,6 +513,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, CompanionNode
|
||||
|
||||
splash = new SplashScreen(this);
|
||||
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs);
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
msg_preview = new TinyMessageScreen(this);
|
||||
#endif
|
||||
setCurrScreen(splash);
|
||||
}
|
||||
|
||||
@@ -519,6 +557,11 @@ switch(t){
|
||||
void UITask::msgRead(int msgcount) {
|
||||
_msgcount = msgcount;
|
||||
if (msgcount == 0) {
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
if (curr == msg_preview
|
||||
&& static_cast<int32_t>(millis() - _msg_preview_until) < 0) return;
|
||||
_deferred_msg_preview = false;
|
||||
#endif
|
||||
gotoHomeScreen();
|
||||
}
|
||||
}
|
||||
@@ -529,6 +572,12 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text,
|
||||
(void)channel_idx;
|
||||
(void)channel_name;
|
||||
_msgcount = msgcount;
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
static_cast<TinyMessageScreen*>(msg_preview)->setMessage(path_len, from_name, text);
|
||||
if (isPairingScreenActive()) _deferred_msg_preview = true;
|
||||
else setCurrScreen(msg_preview);
|
||||
_msg_preview_until = millis() + 15000UL;
|
||||
#endif
|
||||
|
||||
if (_display != NULL) {
|
||||
if (!_display->isOn() && shouldWakeDisplayForMessage()) {
|
||||
@@ -591,6 +640,12 @@ void UITask::showPairingPin() {
|
||||
void UITask::finishPairingScreen(bool timed_out) {
|
||||
_pairing_screen_until = 0;
|
||||
_next_refresh = 0;
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
if (_deferred_msg_preview) {
|
||||
_deferred_msg_preview = false;
|
||||
setCurrScreen(msg_preview);
|
||||
}
|
||||
#endif
|
||||
if (_display == NULL) return;
|
||||
|
||||
if (timed_out) {
|
||||
|
||||
@@ -56,7 +56,11 @@ class UITask : public AbstractUITask {
|
||||
|
||||
UIScreen* splash;
|
||||
UIScreen* home;
|
||||
// UIScreen* msg_preview;
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
UIScreen* msg_preview = nullptr;
|
||||
bool _deferred_msg_preview = false;
|
||||
unsigned long _msg_preview_until = 0;
|
||||
#endif
|
||||
UIScreen* curr;
|
||||
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ lib_ignore = ${companion_nimble_trial.lib_ignore}
|
||||
extends = env:heltec_v4_2_v4_3_companion_radio_full_femon_nimble
|
||||
build_flags =
|
||||
${env:heltec_v4_2_v4_3_companion_radio_full_femon_nimble.build_flags}
|
||||
-D UI_SSD1306_PICOPIXEL_MESSAGES=1
|
||||
-D UI_SMALL_MESSAGE_FONT=1
|
||||
-D UI_MSG_PREVIEW_SIZE=161
|
||||
|
||||
[env:Xiao_S3_WIO_companion_radio_full_nimble]
|
||||
|
||||
@@ -88,7 +88,12 @@ def requirements(platform, defines, target):
|
||||
# ui-new retains 32 previews in one heap allocation. The baseline
|
||||
# covers 78 bytes per message; budget larger buffers explicitly,
|
||||
# including worst-case 8-byte Entry alignment.
|
||||
extra = max(0, integer(defines, "UI_MSG_PREVIEW_SIZE", 78) - 78)
|
||||
small_display = display in {
|
||||
"SSD1306Display", "SH1106Display", "ST7735Display", "U8g2Display",
|
||||
}
|
||||
small_font = integer(defines, "UI_SMALL_MESSAGE_FONT", int(small_display))
|
||||
default_preview = 161 if small_font else 78
|
||||
extra = max(0, integer(defines, "UI_MSG_PREVIEW_SIZE", default_preview) - 78)
|
||||
if extra:
|
||||
parts["expanded_message_previews"] = 32 * ((extra + 7) // 8) * 8
|
||||
parts["allocation_and_transient_margin"] = 16384 if platform == "ESP32_PLATFORM" else 4096
|
||||
|
||||
@@ -26,6 +26,14 @@
|
||||
#define MESHCORE_HAS_SMALL_DISPLAY 1
|
||||
#endif
|
||||
|
||||
#ifndef UI_SMALL_MESSAGE_FONT
|
||||
#if defined(MESHCORE_HAS_SMALL_DISPLAY)
|
||||
#define UI_SMALL_MESSAGE_FONT 1
|
||||
#else
|
||||
#define UI_SMALL_MESSAGE_FONT 0
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#undef MESHCORE_DISPLAY_TOKEN_JOIN
|
||||
#undef MESHCORE_DISPLAY_TOKEN_JOIN_INNER
|
||||
#undef MESHCORE_SMALL_DISPLAY_CLASS_U8g2Display
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
// Picopixel by Sebastian Weber, from Adafruit GFX Library 1.12.6.
|
||||
// Data is unchanged; the type names are independent of a display library.
|
||||
// Source: https://github.com/adafruit/Adafruit-GFX-Library/blob/1.12.6/Fonts/Picopixel.h
|
||||
/*
|
||||
Software License Agreement (BSD License)
|
||||
|
||||
Copyright (c) 2012 Adafruit Industries. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
- Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include <stdint.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,
|
||||
0xE0, 0x80, 0x25, 0x48, 0x56, 0xD4, 0x75, 0x40, 0xC5, 0x4E, 0xC5, 0x1C,
|
||||
0x97, 0x92, 0xF3, 0x1C, 0x53, 0x54, 0xE5, 0x48, 0x55, 0x54, 0x55, 0x94,
|
||||
0xA0, 0x46, 0x64, 0xE3, 0x80, 0x98, 0xC5, 0x04, 0x56, 0xC6, 0x57, 0xDA,
|
||||
0xD7, 0x5C, 0x72, 0x46, 0xD6, 0xDC, 0xF3, 0xCE, 0xF3, 0x48, 0x72, 0xD4,
|
||||
0xB7, 0xDA, 0xF8, 0x24, 0xD4, 0xBB, 0x5A, 0x92, 0x4E, 0x8E, 0xEB, 0x58,
|
||||
0x80, 0x9D, 0xB9, 0x90, 0x56, 0xD4, 0xD7, 0x48, 0x56, 0xD4, 0x40, 0xD7,
|
||||
0x5A, 0x71, 0x1C, 0xE9, 0x24, 0xB6, 0xD4, 0xB6, 0xA4, 0x8C, 0x6B, 0x55,
|
||||
0x00, 0xB5, 0x5A, 0xB5, 0x24, 0xE5, 0x4E, 0xEA, 0xC0, 0x91, 0x12, 0xD5,
|
||||
0xC0, 0x54, 0xF0, 0x90, 0xC7, 0xF0, 0x93, 0x5E, 0x71, 0x80, 0x25, 0xDE,
|
||||
0x5E, 0x30, 0x6E, 0x80, 0x77, 0x9C, 0x93, 0x5A, 0xB8, 0x45, 0x60, 0x92,
|
||||
0xEA, 0xAA, 0x40, 0xD5, 0x6A, 0xD6, 0x80, 0x55, 0x00, 0xD7, 0x40, 0x75,
|
||||
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 ' '
|
||||
{0, 1, 5, 2, 0, -4}, // 0x21 '!'
|
||||
{1, 3, 2, 4, 0, -4}, // 0x22 '"'
|
||||
{2, 5, 5, 6, 0, -4}, // 0x23 '#'
|
||||
{6, 3, 6, 4, 0, -4}, // 0x24 '$'
|
||||
{9, 3, 5, 4, 0, -4}, // 0x25 '%'
|
||||
{11, 4, 5, 5, 0, -4}, // 0x26 '&'
|
||||
{14, 1, 2, 2, 0, -4}, // 0x27 '''
|
||||
{15, 2, 5, 3, 0, -4}, // 0x28 '('
|
||||
{17, 2, 5, 3, 0, -4}, // 0x29 ')'
|
||||
{19, 3, 3, 4, 0, -3}, // 0x2A '*'
|
||||
{21, 3, 3, 4, 0, -3}, // 0x2B '+'
|
||||
{23, 2, 2, 3, 0, 0}, // 0x2C ','
|
||||
{24, 3, 1, 4, 0, -2}, // 0x2D '-'
|
||||
{25, 1, 1, 2, 0, 0}, // 0x2E '.'
|
||||
{26, 3, 5, 4, 0, -4}, // 0x2F '/'
|
||||
{28, 3, 5, 4, 0, -4}, // 0x30 '0'
|
||||
{30, 2, 5, 3, 0, -4}, // 0x31 '1'
|
||||
{32, 3, 5, 4, 0, -4}, // 0x32 '2'
|
||||
{34, 3, 5, 4, 0, -4}, // 0x33 '3'
|
||||
{36, 3, 5, 4, 0, -4}, // 0x34 '4'
|
||||
{38, 3, 5, 4, 0, -4}, // 0x35 '5'
|
||||
{40, 3, 5, 4, 0, -4}, // 0x36 '6'
|
||||
{42, 3, 5, 4, 0, -4}, // 0x37 '7'
|
||||
{44, 3, 5, 4, 0, -4}, // 0x38 '8'
|
||||
{46, 3, 5, 4, 0, -4}, // 0x39 '9'
|
||||
{48, 1, 3, 2, 0, -3}, // 0x3A ':'
|
||||
{49, 2, 4, 3, 0, -3}, // 0x3B ';'
|
||||
{50, 2, 3, 3, 0, -3}, // 0x3C '<'
|
||||
{51, 3, 3, 4, 0, -3}, // 0x3D '='
|
||||
{53, 2, 3, 3, 0, -3}, // 0x3E '>'
|
||||
{54, 3, 5, 4, 0, -4}, // 0x3F '?'
|
||||
{56, 3, 5, 4, 0, -4}, // 0x40 '@'
|
||||
{58, 3, 5, 4, 0, -4}, // 0x41 'A'
|
||||
{60, 3, 5, 4, 0, -4}, // 0x42 'B'
|
||||
{62, 3, 5, 4, 0, -4}, // 0x43 'C'
|
||||
{64, 3, 5, 4, 0, -4}, // 0x44 'D'
|
||||
{66, 3, 5, 4, 0, -4}, // 0x45 'E'
|
||||
{68, 3, 5, 4, 0, -4}, // 0x46 'F'
|
||||
{70, 3, 5, 4, 0, -4}, // 0x47 'G'
|
||||
{72, 3, 5, 4, 0, -4}, // 0x48 'H'
|
||||
{74, 1, 5, 2, 0, -4}, // 0x49 'I'
|
||||
{75, 3, 5, 4, 0, -4}, // 0x4A 'J'
|
||||
{77, 3, 5, 4, 0, -4}, // 0x4B 'K'
|
||||
{79, 3, 5, 4, 0, -4}, // 0x4C 'L'
|
||||
{81, 5, 5, 6, 0, -4}, // 0x4D 'M'
|
||||
{85, 4, 5, 5, 0, -4}, // 0x4E 'N'
|
||||
{88, 3, 5, 4, 0, -4}, // 0x4F 'O'
|
||||
{90, 3, 5, 4, 0, -4}, // 0x50 'P'
|
||||
{92, 3, 6, 4, 0, -4}, // 0x51 'Q'
|
||||
{95, 3, 5, 4, 0, -4}, // 0x52 'R'
|
||||
{97, 3, 5, 4, 0, -4}, // 0x53 'S'
|
||||
{99, 3, 5, 4, 0, -4}, // 0x54 'T'
|
||||
{101, 3, 5, 4, 0, -4}, // 0x55 'U'
|
||||
{103, 3, 5, 4, 0, -4}, // 0x56 'V'
|
||||
{105, 5, 5, 6, 0, -4}, // 0x57 'W'
|
||||
{109, 3, 5, 4, 0, -4}, // 0x58 'X'
|
||||
{111, 3, 5, 4, 0, -4}, // 0x59 'Y'
|
||||
{113, 3, 5, 4, 0, -4}, // 0x5A 'Z'
|
||||
{115, 2, 5, 3, 0, -4}, // 0x5B '['
|
||||
{117, 3, 5, 4, 0, -4}, // 0x5C '\'
|
||||
{119, 2, 5, 3, 0, -4}, // 0x5D ']'
|
||||
{121, 3, 2, 4, 0, -4}, // 0x5E '^'
|
||||
{122, 4, 1, 4, 0, 1}, // 0x5F '_'
|
||||
{123, 2, 2, 3, 0, -4}, // 0x60 '`'
|
||||
{124, 3, 4, 4, 0, -3}, // 0x61 'a'
|
||||
{126, 3, 5, 4, 0, -4}, // 0x62 'b'
|
||||
{128, 3, 3, 4, 0, -2}, // 0x63 'c'
|
||||
{130, 3, 5, 4, 0, -4}, // 0x64 'd'
|
||||
{132, 3, 4, 4, 0, -3}, // 0x65 'e'
|
||||
{134, 2, 5, 3, 0, -4}, // 0x66 'f'
|
||||
{136, 3, 5, 4, 0, -3}, // 0x67 'g'
|
||||
{138, 3, 5, 4, 0, -4}, // 0x68 'h'
|
||||
{140, 1, 5, 2, 0, -4}, // 0x69 'i'
|
||||
{141, 2, 6, 3, 0, -4}, // 0x6A 'j'
|
||||
{143, 3, 5, 4, 0, -4}, // 0x6B 'k'
|
||||
{145, 2, 5, 3, 0, -4}, // 0x6C 'l'
|
||||
{147, 5, 3, 6, 0, -2}, // 0x6D 'm'
|
||||
{149, 3, 3, 4, 0, -2}, // 0x6E 'n'
|
||||
{151, 3, 3, 4, 0, -2}, // 0x6F 'o'
|
||||
{153, 3, 4, 4, 0, -2}, // 0x70 'p'
|
||||
{155, 3, 4, 4, 0, -2}, // 0x71 'q'
|
||||
{157, 2, 3, 3, 0, -2}, // 0x72 'r'
|
||||
{158, 3, 4, 4, 0, -3}, // 0x73 's'
|
||||
{160, 2, 5, 3, 0, -4}, // 0x74 't'
|
||||
{162, 3, 3, 4, 0, -2}, // 0x75 'u'
|
||||
{164, 3, 3, 4, 0, -2}, // 0x76 'v'
|
||||
{166, 5, 3, 6, 0, -2}, // 0x77 'w'
|
||||
{168, 3, 3, 4, 0, -2}, // 0x78 'x'
|
||||
{170, 3, 4, 4, 0, -2}, // 0x79 'y'
|
||||
{172, 3, 4, 4, 0, -3}, // 0x7A 'z'
|
||||
{174, 3, 5, 4, 0, -4}, // 0x7B '{'
|
||||
{176, 1, 6, 2, 0, -4}, // 0x7C '|'
|
||||
{177, 3, 5, 4, 0, -4}, // 0x7D '}'
|
||||
{179, 4, 2, 5, 0, -3}}; // 0x7E '~'
|
||||
|
||||
|
||||
} // namespace ui
|
||||
} // namespace mesh
|
||||
@@ -0,0 +1,97 @@
|
||||
#pragma once
|
||||
|
||||
#include "DisplayDriver.h"
|
||||
#include "DisplayTextLayout.h"
|
||||
#include "Pixel5FontData.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 {
|
||||
DisplayDriver& _display;
|
||||
int _x = 0, _y = 0;
|
||||
|
||||
static const Pixel5Glyph& glyph(uint8_t c) {
|
||||
return pixel5Glyphs[(c >= 32 && c <= 126 ? c : '?') - 32];
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
bool isOn() override { return _display.isOn(); }
|
||||
void turnOn() override { _display.turnOn(); }
|
||||
void turnOff() override { _display.turnOff(); }
|
||||
void clear() override { _display.clear(); }
|
||||
void startFrame(ColorVal bkg) override { _display.startFrame(bkg); }
|
||||
void endFrame() override { _display.endFrame(); }
|
||||
void setTextSize(int) override {} // This font deliberately has one size.
|
||||
void setColor(ColorVal color) override { _display.setColor(color); }
|
||||
void setCursor(int x, int y) override { _x = x; _y = y; }
|
||||
void fillRect(int x, int y, int w, int h) override {
|
||||
_display.fillRect(x, y, w, h);
|
||||
}
|
||||
void drawRect(int x, int y, int w, int h) override {
|
||||
_display.drawRect(x, y, w, h);
|
||||
}
|
||||
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override {
|
||||
_display.drawXbm(x, y, bits, w, h);
|
||||
}
|
||||
uint16_t getTextWidth(const char* str) override {
|
||||
uint16_t result = 0;
|
||||
while (*str) result += glyph((uint8_t)*str++).advance;
|
||||
return result;
|
||||
}
|
||||
void print(const char* str) override {
|
||||
while (*str) {
|
||||
const Pixel5Glyph& g = glyph((uint8_t)*str++);
|
||||
for (int row = 0; row < g.height; ++row) {
|
||||
const int y = _y + 4 + 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]
|
||||
& (0x80 >> (bit % 8)))) continue;
|
||||
const int x = _x + g.x_offset + col;
|
||||
if (x >= 0 && x < width()) _display.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
_x += g.advance;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace mesh
|
||||
@@ -1,16 +1,5 @@
|
||||
#include "SSD1306Display.h"
|
||||
|
||||
#if defined(UI_SSD1306_PICOPIXEL_MESSAGES) && UI_SSD1306_PICOPIXEL_MESSAGES == 1
|
||||
#include <Fonts/Picopixel.h>
|
||||
#include "DisplayTextLayout.h"
|
||||
|
||||
// The message UI has already replaced unsupported UTF-8 with CP437 blocks.
|
||||
// Picopixel is ASCII-only; retain a visible placeholder for those characters.
|
||||
static uint8_t picopixelCharacter(uint8_t c) {
|
||||
return c >= 0x20 && c <= 0x7E ? c : '?';
|
||||
}
|
||||
#endif
|
||||
|
||||
bool SSD1306Display::i2c_probe(TwoWire& wire, uint8_t addr) {
|
||||
wire.beginTransmission(addr);
|
||||
uint8_t error = wire.endTransmission();
|
||||
@@ -123,43 +112,13 @@ void SSD1306Display::setColor(ColorVal c) {
|
||||
}
|
||||
|
||||
void SSD1306Display::setCursor(int x, int y) {
|
||||
#if defined(UI_SSD1306_PICOPIXEL_MESSAGES) && UI_SSD1306_PICOPIXEL_MESSAGES == 1
|
||||
// GFX custom fonts take a baseline; the shared UI takes a top coordinate.
|
||||
if (_picopixel_message) y += 4;
|
||||
#endif
|
||||
display.setCursor(x, y);
|
||||
}
|
||||
|
||||
void SSD1306Display::print(const char* str) {
|
||||
#if defined(UI_SSD1306_PICOPIXEL_MESSAGES) && UI_SSD1306_PICOPIXEL_MESSAGES == 1
|
||||
if (_picopixel_message) {
|
||||
while (*str) display.write(picopixelCharacter((uint8_t)*str++));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
display.print(str);
|
||||
}
|
||||
|
||||
#if defined(UI_SSD1306_PICOPIXEL_MESSAGES) && UI_SSD1306_PICOPIXEL_MESSAGES == 1
|
||||
void SSD1306Display::printWordWrap(const char* str, int max_width) {
|
||||
const int x = display.getCursorX();
|
||||
const int y = display.getCursorY();
|
||||
if (max_width > width() - x) max_width = width() - x;
|
||||
// Capitals are 5px high; descenders can use a sixth pixel. Leave one blank
|
||||
// row between lines and never start a partially visible final line.
|
||||
const int max_lines = (height() - y + 1) / 7;
|
||||
display.setFont(&Picopixel);
|
||||
display.setTextSize(1);
|
||||
display.setTextWrap(false);
|
||||
_picopixel_message = true;
|
||||
mesh::ui::drawTextWrapped(*this, x, y, max_width, 7, max_lines, str);
|
||||
_picopixel_message = false;
|
||||
display.setFont(nullptr);
|
||||
display.setTextWrap(true);
|
||||
display.setCursor(x, y);
|
||||
}
|
||||
#endif
|
||||
|
||||
void SSD1306Display::fillRect(int x, int y, int w, int h) {
|
||||
display.fillRect(x, y, w, h, _color);
|
||||
}
|
||||
@@ -173,17 +132,6 @@ void SSD1306Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
|
||||
}
|
||||
|
||||
uint16_t SSD1306Display::getTextWidth(const char* str) {
|
||||
#if defined(UI_SSD1306_PICOPIXEL_MESSAGES) && UI_SSD1306_PICOPIXEL_MESSAGES == 1
|
||||
if (_picopixel_message) {
|
||||
// Include advances for spaces; GFX's ink bounds omit trailing whitespace.
|
||||
uint16_t width = 0;
|
||||
while (*str) {
|
||||
const uint8_t c = picopixelCharacter((uint8_t)*str++);
|
||||
width += pgm_read_byte(&PicopixelGlyphs[c - 0x20].xAdvance);
|
||||
}
|
||||
return width;
|
||||
}
|
||||
#endif
|
||||
int16_t x1, y1;
|
||||
uint16_t w, h;
|
||||
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
|
||||
|
||||
@@ -27,9 +27,6 @@ class SSD1306Display : public DisplayDriver {
|
||||
#endif
|
||||
uint8_t _color;
|
||||
RefCountedDigitalPin* _peripher_power;
|
||||
#if defined(UI_SSD1306_PICOPIXEL_MESSAGES) && UI_SSD1306_PICOPIXEL_MESSAGES == 1
|
||||
bool _picopixel_message = false;
|
||||
#endif
|
||||
|
||||
bool i2c_probe(TwoWire& wire, uint8_t addr);
|
||||
void applyRotation();
|
||||
@@ -54,9 +51,6 @@ public:
|
||||
void setColor(ColorVal c) override;
|
||||
void setCursor(int x, int y) override;
|
||||
void print(const char* str) override;
|
||||
#if defined(UI_SSD1306_PICOPIXEL_MESSAGES) && UI_SSD1306_PICOPIXEL_MESSAGES == 1
|
||||
void printWordWrap(const char* str, int max_width) override;
|
||||
#endif
|
||||
void fillRect(int x, int y, int w, int h) override;
|
||||
void drawRect(int x, int y, int w, int h) override;
|
||||
void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override;
|
||||
|
||||
@@ -632,7 +632,10 @@ void ST7735Display::print(const char* str) {
|
||||
|
||||
void ST7735Display::fillRect(int x, int y, int w, int h) {
|
||||
if (!spriteReady()) return;
|
||||
sprite->fillRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, curr_color);
|
||||
// Scale the edges, so adjacent 1px font cells meet at fractional scaling.
|
||||
const int left = x*SCALE_X, top = y*SCALE_Y;
|
||||
sprite->fillRect(left, top, int((x+w)*SCALE_X)-left,
|
||||
int((y+h)*SCALE_Y)-top, curr_color);
|
||||
}
|
||||
|
||||
void ST7735Display::drawRect(int x, int y, int w, int h) {
|
||||
|
||||
+21
-42
@@ -1,9 +1,11 @@
|
||||
#include <helpers/ui/SSD1306Display.h>
|
||||
#include <helpers/ui/Pixel5Text.h>
|
||||
#include <Fonts/Picopixel.h>
|
||||
#include <cassert>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
void save(const char* file) {
|
||||
auto& c=*Adafruit_SSD1306::last;
|
||||
std::ofstream f(file);
|
||||
@@ -15,56 +17,33 @@ int main(int argc, char** argv) {
|
||||
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";
|
||||
assert(std::string(message).size()==160);
|
||||
d.startFrame();
|
||||
d.setCursor(0,0); d.print("Message 3/29");
|
||||
d.setCursor(86,0); d.print("1m ago");
|
||||
d.drawRect(0,11,128,1);
|
||||
d.setCursor(0,14); d.print("Ch 0 Public [4h]:");
|
||||
d.setCursor(0,25);
|
||||
#if UI_SSD1306_PICOPIXEL_MESSAGES == 1
|
||||
d.printWordWrap(message,128);
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
mesh::ui::drawSmallMessageBody(d,"Ch 0 Public [4h]:",message);
|
||||
#else
|
||||
d.print(std::string(message,77).c_str());
|
||||
d.setCursor(0,14); d.print("Ch 0 Public [4h]:");
|
||||
d.setCursor(0,25); d.print(std::string(message,77).c_str());
|
||||
#endif
|
||||
if (argc > 1) save(argv[1]);
|
||||
#if UI_SSD1306_PICOPIXEL_MESSAGES == 1
|
||||
std::string shown;
|
||||
for (const auto& g:c.glyphs) {
|
||||
shown += g.c;
|
||||
if (g.w && g.h) assert(g.x>=0 && g.x+g.w<=128 && g.y>=25 && g.y+g.h<=64);
|
||||
}
|
||||
assert(shown==message);
|
||||
#if UI_SMALL_MESSAGE_FONT == 1
|
||||
assert(c.outside==0);
|
||||
assert(d.getTextWidth("ABC")==18); // regular font restored
|
||||
d.setCursor(0,0); d.print("Regular font");
|
||||
assert(d.getTextWidth("ABC")==18);
|
||||
int checks=1;
|
||||
for (int rotation:{0,90,180,270}) for (int top:{0,25,55,58,59,63,64}) {
|
||||
d.setRotationDegrees(rotation);
|
||||
d.startFrame(); d.setCursor(0,top);
|
||||
d.printWordWrap(std::string(160,'W').c_str(),d.width());
|
||||
for (const auto& g:c.glyphs) {
|
||||
if (g.w && g.h) assert(g.x>=0 && g.x+g.w<=d.width() && g.y>=top && g.y+g.h<=d.height());
|
||||
}
|
||||
if (!c.glyphs.empty()) {
|
||||
std::string row;
|
||||
for (const auto& g:c.glyphs) row += g.c;
|
||||
while (!row.empty() && row.back()==' ') row.pop_back();
|
||||
assert(row==std::string(160, 'W') || (row.size()>=3 && row.substr(row.size()-3)=="..."));
|
||||
}
|
||||
assert(c.outside==0);
|
||||
++checks;
|
||||
}
|
||||
d.setRotationDegrees(0);
|
||||
assert(d.getTextWidth("ABC")==18); // regular driver font untouched
|
||||
mesh::ui::Pixel5Text text(d);
|
||||
GFXcanvas1 reference(128,64);
|
||||
reference.setFont(&Picopixel);
|
||||
reference.setTextColor(1);
|
||||
for (int ch=32;ch<=126;ch++) {
|
||||
d.startFrame(); d.setCursor(0,25);
|
||||
d.printWordWrap(std::string(160,char(ch)).c_str(),128);
|
||||
assert(c.outside==0);
|
||||
++checks;
|
||||
d.startFrame(); text.setCursor(3,25);
|
||||
text.print(std::string(1,char(ch)).c_str());
|
||||
reference.fillScreen(0); reference.setCursor(3,29); reference.write(ch);
|
||||
for (int y=0;y<64;y++) for (int x=0;x<128;x++)
|
||||
assert(c.getPixel(x,y)==reference.getPixel(x,y));
|
||||
assert(text.getTextWidth(std::string(1,char(ch)).c_str())==PicopixelGlyphs[ch-32].xAdvance);
|
||||
}
|
||||
d.startFrame(); d.setCursor(0,25);
|
||||
d.printWordWrap("A\xDB" "B",128);
|
||||
assert(c.glyphs.size()==3 && c.glyphs[1].c=='?');
|
||||
std::cout << "Checks: " << checks+1 << "; sample characters: " << shown.size() << "; sample: " << shown << '\n';
|
||||
std::cout << "95 glyphs match Adafruit GFX Picopixel pixel for pixel; normal font preserved\n";
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -3,6 +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/WiFiSetupQrDisplay.h>
|
||||
#include <helpers/ui/WiFiSetupQrPayload.h>
|
||||
|
||||
@@ -133,6 +134,66 @@ TEST(DisplayDriver, EllipsizesOnlyAtUTF8CodepointBoundaries) {
|
||||
EXPECT_TRUE(isValidUTF8(display.printed.c_str()));
|
||||
}
|
||||
|
||||
TEST(Pixel5Text, CapitalsAreFivePixelsAndNormalFontIsUntouched) {
|
||||
TestDisplay display(128, 64, 6);
|
||||
mesh::ui::Pixel5Text text(display);
|
||||
text.setCursor(0, 0);
|
||||
text.print("A");
|
||||
const char* rows[] = {"010", "101", "111", "101", "101"};
|
||||
bool pixels[5][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, 5);
|
||||
pixels[pixel.y][pixel.x] = true;
|
||||
}
|
||||
for (int y = 0; y < 5; ++y) for (int x = 0; x < 3; ++x)
|
||||
EXPECT_EQ(rows[y][x] == '1', pixels[y][x]);
|
||||
EXPECT_EQ(12, text.getTextWidth("ABC"));
|
||||
EXPECT_EQ(18, display.getTextWidth("ABC"));
|
||||
}
|
||||
|
||||
TEST(Pixel5Text, CompactOriginAllowsSixCompleteMessageRows) {
|
||||
TestDisplay display(128, 64);
|
||||
mesh::ui::drawSmallMessageBody(display, std::string(61, 'W').c_str(),
|
||||
std::string(160, 'W').c_str());
|
||||
ASSERT_FALSE(display.fills.empty());
|
||||
bool final_dots[3] = {};
|
||||
for (const auto& pixel : display.fills) {
|
||||
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);
|
||||
for (int i = 0; i < 3; ++i)
|
||||
if (pixel.y == 60 && 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));
|
||||
}
|
||||
|
||||
TEST(Pixel5Text, GlyphsStayInsideTinyAndRotatedScreens) {
|
||||
for (auto dimensions : {std::pair<int, int>{72, 40}, {64, 128}, {128, 64}}) {
|
||||
TestDisplay display(dimensions.first, dimensions.second);
|
||||
mesh::ui::Pixel5Text text(display);
|
||||
for (int c = 32; c <= 126; ++c) {
|
||||
display.fills.clear();
|
||||
text.setCursor(0, display.height() - 6);
|
||||
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_LT(pixel.y, display.height());
|
||||
}
|
||||
}
|
||||
display.fills.clear();
|
||||
text.setCursor(0, display.height() - 5);
|
||||
text.printWordWrap("must not start a clipped final line", display.width());
|
||||
EXPECT_TRUE(display.fills.empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DisplayDriver, QrCodeIsOptionalByDefault) {
|
||||
TestDisplay display;
|
||||
EXPECT_FALSE(display.drawQrCode("WIFI:T:nopass;S:MC-Set-90DF;;",
|
||||
|
||||
@@ -67,7 +67,7 @@ def esp_fixture(path, modern=False, fragmented=False):
|
||||
|
||||
class FirmwareRamTest(unittest.TestCase):
|
||||
def test_longer_display_previews_reserve_heap_and_contiguous_history(self):
|
||||
defines = {"DISPLAY_CLASS": "SSD1306Display"}
|
||||
defines = {"DISPLAY_CLASS": "SSD1306Display", "UI_SMALL_MESSAGE_FONT": 0}
|
||||
base = ram.requirements("ESP32_PLATFORM", defines, "v4_companion")
|
||||
expanded = ram.requirements("ESP32_PLATFORM", {
|
||||
**defines, "UI_MSG_PREVIEW_SIZE": 161,
|
||||
@@ -80,6 +80,10 @@ class FirmwareRamTest(unittest.TestCase):
|
||||
**defines, "UI_MSG_PREVIEW_SIZE": 32,
|
||||
}, "v4_companion")
|
||||
self.assertEqual(smaller, base)
|
||||
default = ram.requirements("ESP32_PLATFORM", {
|
||||
"DISPLAY_CLASS": "SSD1306Display",
|
||||
}, "v4_companion")
|
||||
self.assertEqual(default, expanded)
|
||||
|
||||
def test_published_image_tables_match_elf_and_use_its_own_reservations(self):
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
@@ -166,7 +170,8 @@ int main() {
|
||||
subprocess.run([str(binary)], check=True)
|
||||
|
||||
def test_t096_release_fails_and_exact_boundary_passes(self):
|
||||
flags = {"COMPANION_RADIO_FULL": 1, "DISPLAY_CLASS": "ST7735Display", "BLE_PIN_CODE": 123456}
|
||||
flags = {"COMPANION_RADIO_FULL": 1, "DISPLAY_CLASS": "ST7735Display",
|
||||
"BLE_PIN_CODE": 123456, "UI_SMALL_MESSAGE_FONT": 0}
|
||||
with tempfile.TemporaryDirectory() as temp, contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
||||
path = Path(temp) / "firmware.elf"
|
||||
for available, accepted in ((54724, False), (73727, False), (73728, True), (74060, True)):
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
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; font metrics and drawing come
|
||||
from the library used by the firmware.
|
||||
Arduino strings and Print glue are replaced; the shared pixel renderer is compared with the original Adafruit font.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -35,7 +34,7 @@ class SSD1306PicopixelTest(unittest.TestCase):
|
||||
result = subprocess.run([
|
||||
"c++", "-std=c++17", "-g",
|
||||
"-fsanitize=address,undefined", "-DARDUINO=10819",
|
||||
f"-DUI_SSD1306_PICOPIXEL_MESSAGES={enabled}",
|
||||
f"-DUI_SMALL_MESSAGE_FONT={enabled}",
|
||||
"-I", str(FIXTURE / "mocks"),
|
||||
"-I", str(ROOT / "src"), "-I", str(library),
|
||||
str(ROOT / "src/helpers/ui/SSD1306Display.cpp"),
|
||||
|
||||
Reference in New Issue
Block a user