Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build_firmware.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ jobs:
if [[ -f "$manifest" ]]; then
echo "Updating $manifest with $OTA_FILE (md5: $OTA_MD5, size: $OTA_SIZE)"
# Add OTA entry to files array if not already present
jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" --arg part "app1" \
'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes, "part_name": $part}] else . end' \
jq --arg name "$OTA_FILE" --arg md5 "$OTA_MD5" --argjson bytes "$OTA_SIZE" \
'if .files | map(select(.name == $name)) | length == 0 then .files += [{"name": $name, "md5": $md5, "bytes": $bytes}] else . end' \
"$manifest" > "${manifest}.tmp" && mv "${manifest}.tmp" "$manifest"
fi
done
Expand Down
11 changes: 0 additions & 11 deletions bin/platformio-custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,6 @@ def manifest_gather(source, target, env):
board_platform = env.BoardConfig().get("platform")
board_mcu = env.BoardConfig().get("build.mcu").lower()
needs_ota_suffix = board_platform == "nordicnrf52"

# Mapping of bin files to their target partition names
# Maps the filename pattern to the partition name where it should be flashed
partition_map = {
f"{progname}.bin": "app0", # primary application slot (app0 / OTA_0)
lfsbin: "spiffs", # filesystem image flashed to spiffs
}

check_paths = [
progname,
f"{progname}.elf",
Expand All @@ -93,9 +85,6 @@ def manifest_gather(source, target, env):
"md5": f.get_content_hash(), # Returns MD5 hash
"bytes": f.get_size() # Returns file size in bytes
}
# Add part_name if this file represents a partition that should be flashed
if p in partition_map:
d["part_name"] = partition_map[p]
out.append(d)
print(d)
manifest_write(out, env)
Expand Down
15 changes: 15 additions & 0 deletions src/graphics/EInkDisplay2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,21 @@ void EInkDisplay::setDetected(uint8_t detected)
(void)detected;
}

#ifdef USE_U8G2_EINK_TEXT
U8G2_FOR_ADAFRUIT_GFX *EInkDisplay::getU8g2()
{
if (!u8g2Ready) {
if (!u8g2Target) {
u8g2Target = new EInkBufferGFX(this);
}
u8g2Fonts.begin(*u8g2Target);
u8g2Ready = true;
}

return &u8g2Fonts;
}
#endif

// Connect to the display - variant specific
bool EInkDisplay::connect()
{
Expand Down
36 changes: 36 additions & 0 deletions src/graphics/EInkDisplay2.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
#include "GxEPD2_BW.h"
#include <OLEDDisplay.h>

#ifdef USE_U8G2_EINK_TEXT
#include <Adafruit_GFX.h>
#include <U8g2_for_Adafruit_GFX.h>
#endif

#ifdef GXEPD2_DRIVER_0 // If variant has multiple possible display models
#include "GxEPD2Multi.h"
#endif
Expand Down Expand Up @@ -66,6 +71,10 @@ class EInkDisplay : public OLEDDisplay
*/
void setDetected(uint8_t detected);

#ifdef USE_U8G2_EINK_TEXT
U8G2_FOR_ADAFRUIT_GFX *getU8g2();
#endif

protected:
// the header size of the buffer used, e.g. for the SPI command header
virtual int getBufferOffset(void) override { return 0; }
Expand All @@ -86,6 +95,33 @@ class EInkDisplay : public OLEDDisplay
GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT> *adafruitDisplay = NULL;
#endif

#ifdef USE_U8G2_EINK_TEXT
U8G2_FOR_ADAFRUIT_GFX u8g2Fonts;
bool u8g2Ready = false;
class EInkBufferGFX : public Adafruit_GFX
{
public:
explicit EInkBufferGFX(OLEDDisplay *display)
: Adafruit_GFX(display ? display->getWidth() : 0, display ? display->getHeight() : 0), display(display)
{
}

void drawPixel(int16_t x, int16_t y, uint16_t color) override
{
if (!display)
return;
if (x < 0 || y < 0 || x >= display->getWidth() || y >= display->getHeight())
return;
display->setColor(color ? WHITE : BLACK);
display->setPixel(x, y);
}

private:
OLEDDisplay *display;
};
EInkBufferGFX *u8g2Target = nullptr;
#endif

// If display uses HSPI
#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \
Expand Down
3 changes: 3 additions & 0 deletions src/graphics/Screen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1588,6 +1588,9 @@ int Screen::handleTextMessage(const meshtastic_MeshPacket *packet)
// Triggered by MeshModules
int Screen::handleUIFrameEvent(const UIFrameEvent *event)
{
if (event) {
LOG_DEBUG("[Screen] UIFrameEvent action=%d", static_cast<int>(event->action));
}
// Block UI frame events when virtual keyboard is active
if (NotificationRenderer::current_notification_type == notificationTypeEnum::text_input) {
return 0;
Expand Down
171 changes: 134 additions & 37 deletions src/graphics/draw/MessageRenderer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
#include "graphics/emotes.h"
#include "main.h"
#include "meshUtils.h"
#if defined(USE_U8G2_EINK_TEXT)
#if defined(USE_EINK)
#include "graphics/EInkDisplay2.h"
#else
#include <Adafruit_GFX.h>
#include <U8g2_for_Adafruit_GFX.h>
#endif
#endif
#include <string>
#include <vector>

Expand All @@ -36,6 +44,86 @@ static std::vector<std::string> cachedLines;
static std::vector<int> cachedHeights;
static bool manualScrolling = false;

#if defined(USE_U8G2_EINK_TEXT)
static U8G2_FOR_ADAFRUIT_GFX *getU8g2Fonts(OLEDDisplay *display)
{
#if defined(USE_EINK)
auto *u8g2 = static_cast<EInkDisplay *>(display)->getU8g2();
if (!u8g2)
return nullptr;
#else
if (!display)
return nullptr;
class BufferGFX : public Adafruit_GFX
{
public:
explicit BufferGFX(OLEDDisplay *display)
: Adafruit_GFX(display ? display->getWidth() : 0, display ? display->getHeight() : 0), display(display)
{
}

void drawPixel(int16_t x, int16_t y, uint16_t color) override
{
if (!display)
return;
if (x < 0 || y < 0 || x >= display->getWidth() || y >= display->getHeight())
return;
display->setColor(color ? WHITE : BLACK);
display->setPixel(x, y);
}

private:
OLEDDisplay *display;
};

static BufferGFX bufferTarget(display);
static U8G2_FOR_ADAFRUIT_GFX u8g2Fonts;
static bool u8g2Ready = false;
if (!u8g2Ready) {
u8g2Fonts.begin(bufferTarget);
u8g2Ready = true;
}
auto *u8g2 = &u8g2Fonts;
#endif

u8g2->setFont(u8g2_font_wqy16_t_gb2312);
u8g2->setFontMode(1);
u8g2->setForegroundColor(1);
u8g2->setBackgroundColor(0);
return u8g2;
}
#endif

static inline int getTextWidth(OLEDDisplay *display, const std::string &text)
{
#if defined(USE_U8G2_EINK_TEXT)
if (auto *u8g2 = getU8g2Fonts(display))
return u8g2->getUTF8Width(text.c_str());
#endif
#if defined(OLED_UA) || defined(OLED_RU)
return display->getStringWidth(text.c_str(), text.length(), true);
#else
return display->getStringWidth(text.c_str());
#endif
}

static inline int getTextWidth(OLEDDisplay *display, const char *text)
{
return getTextWidth(display, std::string(text));
}

static inline void drawText(OLEDDisplay *display, int x, int y, const std::string &text)
{
#if defined(USE_U8G2_EINK_TEXT)
if (auto *u8g2 = getU8g2Fonts(display)) {
const int baseline = y + u8g2->getFontAscent();
u8g2->drawUTF8(x, baseline, text.c_str());
return;
}
#endif
display->drawString(x, y, text.c_str());
}

// UTF-8 skip helper
static inline size_t utf8CharLen(uint8_t c)
{
Expand Down Expand Up @@ -189,14 +277,10 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
std::string textChunk = line.substr(i, nextControl - i);
if (inBold) {
// Faux bold: draw twice, offset by 1px
display->drawString(cursorX + 1, fontY, textChunk.c_str());
drawText(display, cursorX + 1, fontY, textChunk);
}
display->drawString(cursorX, fontY, textChunk.c_str());
#if defined(OLED_UA) || defined(OLED_RU)
cursorX += display->getStringWidth(textChunk.c_str(), textChunk.length(), true);
#else
cursorX += display->getStringWidth(textChunk.c_str());
#endif
drawText(display, cursorX, fontY, textChunk);
cursorX += getTextWidth(display, textChunk);
i = nextControl;
continue;
}
Expand All @@ -213,14 +297,10 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
// No more emotes — render the rest of the line
std::string remaining = line.substr(i);
if (inBold) {
display->drawString(cursorX + 1, fontY, remaining.c_str());
drawText(display, cursorX + 1, fontY, remaining);
}
display->drawString(cursorX, fontY, remaining.c_str());
#if defined(OLED_UA) || defined(OLED_RU)
cursorX += display->getStringWidth(remaining.c_str(), remaining.length(), true);
#else
cursorX += display->getStringWidth(remaining.c_str());
#endif
drawText(display, cursorX, fontY, remaining);
cursorX += getTextWidth(display, remaining);
break;
}
}
Expand Down Expand Up @@ -412,7 +492,13 @@ static inline int getRenderedLineWidth(OLEDDisplay *display, const std::string &
}
if (!matched) {
size_t charLen = utf8CharLen(static_cast<uint8_t>(normalized[i]));
#if defined(OLED_UA) || defined(OLED_RU)
#if defined(USE_U8G2_EINK_TEXT)
if (charLen > 1) {
totalWidth += getTextWidth(display, normalized.substr(i, charLen));
} else {
totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str());
}
#elif defined(OLED_UA) || defined(OLED_RU)
totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str(), charLen, true);
#else
totalWidth += display->getStringWidth(normalized.substr(i, charLen).c_str());
Expand Down Expand Up @@ -766,7 +852,6 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
int rightX = SCREEN_WIDTH - renderedWidth - SCROLLBAR_WIDTH - RIGHT_MARGIN;
if (rightX < LEFT_MARGIN)
rightX = LEFT_MARGIN;

drawStringWithEmotes(display, rightX, lineY, cachedLines[i], emotes, numEmotes);
} else {
drawStringWithEmotes(display, x, lineY, cachedLines[i], emotes, numEmotes);
Expand All @@ -793,38 +878,50 @@ std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerS
}

std::string line, word;
for (int i = 0; messageBuf[i]; ++i) {
char ch = messageBuf[i];
if ((unsigned char)messageBuf[i] == 0xE2 && (unsigned char)messageBuf[i + 1] == 0x80 &&
(unsigned char)messageBuf[i + 2] == 0x99) {
ch = '\''; // plain apostrophe
i += 2; // skip over the extra UTF-8 bytes
const size_t msgLen = strlen(messageBuf);
for (size_t i = 0; i < msgLen;) {
uint8_t c = static_cast<uint8_t>(messageBuf[i]);
size_t len = utf8CharLen(c);

// Normalize fancy apostrophe to ASCII
if (c == 0xE2 && i + 2 < msgLen && (uint8_t)messageBuf[i + 1] == 0x80 &&
(uint8_t)messageBuf[i + 2] == 0x99) {
c = '\'';
len = 1;
}
if (ch == '\n') {

if (len == 1 && c == '\n') {
if (!word.empty())
line += word;
if (!line.empty())
lines.push_back(line);
line.clear();
word.clear();
} else if (ch == ' ') {
i += 1;
continue;
}

if (len == 1 && c == ' ') {
line += word + ' ';
word.clear();
} else {
word += ch;
std::string test = line + word;
i += 1;
continue;
}

word.append(messageBuf + i, len);
std::string test = line + word;
#if defined(OLED_UA) || defined(OLED_RU)
uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true);
uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true);
#else
uint16_t strWidth = display->getStringWidth(test.c_str());
uint16_t strWidth = display->getStringWidth(test.c_str());
#endif
if (strWidth > textWidth) {
if (!line.empty())
lines.push_back(line);
line = word;
word.clear();
}
if (strWidth > textWidth) {
if (!line.empty())
lines.push_back(line);
line = word;
word.clear();
}
i += len;
}

if (!word.empty())
Expand Down Expand Up @@ -932,7 +1029,7 @@ void handleNewMessage(OLEDDisplay *display, const StoredMessage &sm, const mesht
availWidth = 0;

size_t origLen = strlen(longName);
while (longName[0] && display->getStringWidth(longName) > availWidth) {
while (longName[0] && getTextWidth(display, longName) > availWidth) {
longName[strlen(longName) - 1] = '\0';
}
if (strlen(longName) < origLen) {
Expand Down Expand Up @@ -1031,4 +1128,4 @@ void setThreadFor(const StoredMessage &sm, const meshtastic_MeshPacket &packet)

} // namespace MessageRenderer
} // namespace graphics
#endif
#endif
4 changes: 3 additions & 1 deletion src/input/InputBroker.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ enum input_broker_event {
#define INPUT_BROKER_MSG_BLUETOOTH_TOGGLE 0xAA
#define INPUT_BROKER_MSG_TAB 0x09
#define INPUT_BROKER_MSG_EMOTE_LIST 0x8F
#define INPUT_BROKER_MSG_IME_PAGE_PREV 0xf3
#define INPUT_BROKER_MSG_IME_PAGE_NEXT 0xf4

typedef struct _InputEvent {
const char *source;
Expand Down Expand Up @@ -84,4 +86,4 @@ class InputBroker : public Observable<const InputEvent *>
#endif
};

extern InputBroker *inputBroker;
extern InputBroker *inputBroker;
Loading