feat: add Chinese 12x12 bitmap font (21075 glyphs) and fix boot gate
- Add ChineseFont12x12.h: U+4E00-U+9FFF CJK coverage, 535KB flash - Add gen_chinese_font.mjs: @napi-rs/canvas based font generator tool - Enable CJK rendering in MessageRenderer and CannedMessageModule - Remove boot confirmation gate (required 2s button hold, caused shutdown loop) - Update partition table: app 2.75MB, OTA 192KB, spiffs 1MB - Update CHANGELOG
This commit is contained in:
@@ -30,6 +30,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#include "configuration.h"
|
||||
#include "gps/RTC.h"
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/fonts/ChineseFont12x12.h"
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include "graphics/emotes.h"
|
||||
#include "main.h"
|
||||
@@ -61,6 +62,46 @@ static size_t cachedKey = 0;
|
||||
static std::vector<std::string> cachedLines;
|
||||
static std::vector<int> cachedHeights;
|
||||
|
||||
// Helper: measure mixed ASCII+CJK string width
|
||||
static int measureMixedWidth(OLEDDisplay *display, const std::string &s)
|
||||
{
|
||||
int w = 0;
|
||||
for (size_t i = 0; i < s.length();) {
|
||||
int utf8len = 1;
|
||||
uint16_t cp = cfont12_utf8(s.c_str() + i, &utf8len);
|
||||
if (cp >= 0x80 && cfont12_find(cp)) {
|
||||
w += CFONT_W;
|
||||
} else {
|
||||
std::string oneChar = s.substr(i, utf8len);
|
||||
w += display->getStringWidth(oneChar.c_str());
|
||||
}
|
||||
i += utf8len;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
// Helper: render a mixed ASCII+CJK string at (x, y), return total width
|
||||
static int drawMixedString(OLEDDisplay *display, int x, int y, const std::string &s, bool inBold)
|
||||
{
|
||||
int cursorX = x;
|
||||
for (size_t i = 0; i < s.length();) {
|
||||
int utf8len = 1;
|
||||
uint16_t cp = cfont12_utf8(s.c_str() + i, &utf8len);
|
||||
if (cp >= 0x80 && cfont12_find(cp)) {
|
||||
cursorX += cfont12_draw(display, cursorX, y, cp);
|
||||
} else {
|
||||
std::string oneChar = s.substr(i, utf8len);
|
||||
if (inBold) {
|
||||
display->drawString(cursorX + 1, y, oneChar.c_str());
|
||||
}
|
||||
display->drawString(cursorX, y, oneChar.c_str());
|
||||
cursorX += display->getStringWidth(oneChar.c_str());
|
||||
}
|
||||
i += utf8len;
|
||||
}
|
||||
return cursorX - x;
|
||||
}
|
||||
|
||||
void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string &line, const Emote *emotes, int emoteCount)
|
||||
{
|
||||
int cursorX = x;
|
||||
@@ -132,16 +173,7 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
|
||||
|
||||
if (nextControl > i) {
|
||||
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());
|
||||
}
|
||||
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
|
||||
cursorX += drawMixedString(display, cursorX, fontY, textChunk, inBold);
|
||||
i = nextControl;
|
||||
continue;
|
||||
}
|
||||
@@ -155,16 +187,7 @@ void drawStringWithEmotes(OLEDDisplay *display, int x, int y, const std::string
|
||||
} else {
|
||||
// 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());
|
||||
}
|
||||
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
|
||||
|
||||
cursorX += drawMixedString(display, cursorX, fontY, remaining, inBold);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -433,41 +456,69 @@ std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerS
|
||||
lines.push_back(std::string(headerStr)); // Header line is always first
|
||||
|
||||
std::string line, word;
|
||||
for (int i = 0; messageBuf[i]; ++i) {
|
||||
char ch = messageBuf[i];
|
||||
int i = 0;
|
||||
while (messageBuf[i]) {
|
||||
// Check for smart quote U+2019 (E2 80 99)
|
||||
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
|
||||
}
|
||||
if (ch == '\n') {
|
||||
word += '\'';
|
||||
i += 3;
|
||||
} else if ((unsigned char)messageBuf[i] == '\n') {
|
||||
if (!word.empty())
|
||||
line += word;
|
||||
if (!line.empty())
|
||||
lines.push_back(line);
|
||||
line.clear();
|
||||
word.clear();
|
||||
} else if (ch == ' ') {
|
||||
i++;
|
||||
} else if ((unsigned char)messageBuf[i] == ' ') {
|
||||
line += word + ' ';
|
||||
word.clear();
|
||||
i++;
|
||||
} else {
|
||||
word += ch;
|
||||
std::string test = line + word;
|
||||
// Keep these lines for diagnostics
|
||||
// LOG_INFO("Char: '%c' (0x%02X)", ch, (unsigned char)ch);
|
||||
// LOG_INFO("Current String: %s", test.c_str());
|
||||
// Note: there are boolean comparison uint16 (getStringWidth) with int (textWidth), hope textWidth is always positive :)
|
||||
#if defined(OLED_UA) || defined(OLED_RU)
|
||||
uint16_t strWidth = display->getStringWidth(test.c_str(), test.length(), true);
|
||||
#else
|
||||
uint16_t strWidth = display->getStringWidth(test.c_str());
|
||||
#endif
|
||||
if (strWidth > textWidth) {
|
||||
if (!line.empty())
|
||||
// Decode UTF-8 character
|
||||
int utf8len = 1;
|
||||
uint16_t cp = cfont12_utf8(messageBuf + i, &utf8len);
|
||||
std::string chStr(messageBuf + i, utf8len);
|
||||
i += utf8len;
|
||||
|
||||
bool isCJK = (cp >= 0x80 && cfont12_find(cp));
|
||||
|
||||
// CJK characters are treated as individual "words" (break before/after them)
|
||||
if (isCJK && !word.empty()) {
|
||||
// Flush current ASCII word first
|
||||
std::string test = line + word;
|
||||
if (measureMixedWidth(display, test) > textWidth && !line.empty()) {
|
||||
lines.push_back(line);
|
||||
line = word;
|
||||
line = word;
|
||||
} else {
|
||||
line = test;
|
||||
}
|
||||
word.clear();
|
||||
}
|
||||
|
||||
word += chStr;
|
||||
|
||||
if (isCJK) {
|
||||
// CJK char is its own word — immediately check if it fits
|
||||
std::string test = line + word;
|
||||
if (measureMixedWidth(display, test) > textWidth && !line.empty()) {
|
||||
lines.push_back(line);
|
||||
line = word;
|
||||
} else {
|
||||
line = test;
|
||||
}
|
||||
word.clear();
|
||||
} else {
|
||||
// ASCII char — accumulate in word, check width
|
||||
std::string test = line + word;
|
||||
if (measureMixedWidth(display, test) > textWidth) {
|
||||
if (!line.empty())
|
||||
lines.push_back(line);
|
||||
line = word;
|
||||
word.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,7 +537,9 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
|
||||
for (const auto &_line : lines) {
|
||||
int lineHeight = FONT_HEIGHT_SMALL;
|
||||
bool hasEmote = false;
|
||||
bool hasCJK = false;
|
||||
|
||||
// Check for emotes
|
||||
for (int i = 0; i < numEmotes; ++i) {
|
||||
const Emote &e = emotes[i];
|
||||
if (_line.find(e.label) != std::string::npos) {
|
||||
@@ -495,8 +548,20 @@ std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, con
|
||||
}
|
||||
}
|
||||
|
||||
// Apply tighter spacing if no emotes on this line
|
||||
if (!hasEmote) {
|
||||
// Check for CJK characters
|
||||
for (size_t ci = 0; ci < _line.length();) {
|
||||
int utf8len = 1;
|
||||
uint16_t cp = cfont12_utf8(_line.c_str() + ci, &utf8len);
|
||||
if (cp >= 0x80 && cfont12_find(cp)) {
|
||||
hasCJK = true;
|
||||
lineHeight = std::max(lineHeight, CFONT_H);
|
||||
break;
|
||||
}
|
||||
ci += utf8len;
|
||||
}
|
||||
|
||||
// Apply tighter spacing if no emotes/CJK on this line
|
||||
if (!hasEmote && !hasCJK) {
|
||||
lineHeight -= 2; // reduce by 2px for tighter spacing
|
||||
if (lineHeight < 8)
|
||||
lineHeight = 8; // minimum safety
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -589,40 +589,6 @@ void setup()
|
||||
// MOS 断电,系统在 setup() 中途就会掉电。
|
||||
tca9535PowerEn(true);
|
||||
LOG_INFO("TCA9535: POWER_EN latched HIGH (early boot)");
|
||||
|
||||
// 开机确认窗口:检测 P1.3 是否持续按住 2 秒,防止意外通电卡死
|
||||
// 最多等待 3 秒,3 秒内未连续按满 2 秒则断电关机
|
||||
{
|
||||
bool bootConfirmed = false;
|
||||
uint32_t pressStart = 0;
|
||||
uint32_t deadline = millis() + 3000;
|
||||
LOG_INFO("TCA9535: Waiting for 2s button hold to confirm boot (timeout 3s)...");
|
||||
|
||||
while (millis() < deadline) {
|
||||
bool pressed = tca9535ReadPowerBoot();
|
||||
if (pressed && pressStart == 0) {
|
||||
pressStart = millis();
|
||||
} else if (pressed && pressStart != 0) {
|
||||
if (millis() - pressStart >= 2000) {
|
||||
bootConfirmed = true;
|
||||
LOG_INFO("TCA9535: Boot confirmed (button held 2s)");
|
||||
break;
|
||||
}
|
||||
} else if (!pressed && pressStart != 0) {
|
||||
// 松手重置计时
|
||||
pressStart = 0;
|
||||
}
|
||||
delay(50); // 50ms 轮询
|
||||
}
|
||||
|
||||
if (!bootConfirmed) {
|
||||
LOG_WARN("TCA9535: Boot not confirmed, shutting down");
|
||||
tca9535PowerEn(false);
|
||||
// 等待 MOS 断开
|
||||
delay(500);
|
||||
doDeepSleep(0, false, false);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#elif defined(ARCH_PORTDUINO)
|
||||
if (portduino_config.i2cdev != "") {
|
||||
|
||||
@@ -54,6 +54,7 @@ const char *const CannedMessageModule::t9LetterMap[][5] = {
|
||||
#endif
|
||||
|
||||
#include "graphics/ScreenFonts.h"
|
||||
#include "graphics/fonts/ChineseFont12x12.h"
|
||||
#include <Throttle.h>
|
||||
|
||||
// Remove Canned message screen if no action is taken for some milliseconds
|
||||
@@ -2061,27 +2062,47 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
int spacePos = text.indexOf(' ', pos);
|
||||
int endPos = (spacePos == -1) ? text.length() : spacePos + 1; // Include space
|
||||
String word = text.substring(pos, endPos);
|
||||
int wordWidth = display->getStringWidth(word);
|
||||
|
||||
// Calculate word width: ASCII chars use getStringWidth, CJK use CFONT_W
|
||||
int wordWidth = 0;
|
||||
for (int ci = 0; ci < word.length();) {
|
||||
int utf8len = 1;
|
||||
uint16_t cp = cfont12_utf8(word.c_str() + ci, &utf8len);
|
||||
if (cp >= 0x80 && cfont12_find(cp)) {
|
||||
wordWidth += CFONT_W;
|
||||
} else {
|
||||
String oneChar = word.substring(ci, ci + utf8len);
|
||||
wordWidth += display->getStringWidth(oneChar);
|
||||
}
|
||||
ci += utf8len;
|
||||
}
|
||||
|
||||
if (lineWidth + wordWidth > maxWidth && lineWidth > 0) {
|
||||
lines.push_back(currentLine);
|
||||
currentLine.clear();
|
||||
lineWidth = 0;
|
||||
}
|
||||
// If word itself too big, split by character
|
||||
// If word itself too big, split by character (CJK-aware)
|
||||
if (wordWidth > maxWidth) {
|
||||
uint16_t charPos = 0;
|
||||
int charPos = 0;
|
||||
while (charPos < word.length()) {
|
||||
String oneChar = word.substring(charPos, charPos + 1);
|
||||
int charWidth = display->getStringWidth(oneChar);
|
||||
int utf8len = 1;
|
||||
uint16_t cp = cfont12_utf8(word.c_str() + charPos, &utf8len);
|
||||
int charWidth;
|
||||
if (cp >= 0x80 && cfont12_find(cp)) {
|
||||
charWidth = CFONT_W;
|
||||
} else {
|
||||
String oneChar = word.substring(charPos, charPos + utf8len);
|
||||
charWidth = display->getStringWidth(oneChar);
|
||||
}
|
||||
if (lineWidth + charWidth > maxWidth && lineWidth > 0) {
|
||||
lines.push_back(currentLine);
|
||||
currentLine.clear();
|
||||
lineWidth = 0;
|
||||
}
|
||||
currentLine.push_back({false, oneChar});
|
||||
currentLine.push_back({false, word.substring(charPos, charPos + utf8len)});
|
||||
lineWidth += charWidth;
|
||||
charPos++;
|
||||
charPos += utf8len;
|
||||
}
|
||||
} else {
|
||||
currentLine.push_back({false, word});
|
||||
@@ -2114,8 +2135,23 @@ void CannedMessageModule::drawFrame(OLEDDisplay *display, OLEDDisplayUiState *st
|
||||
nextX += emote->width + 2;
|
||||
}
|
||||
} else {
|
||||
display->drawString(nextX, yLine, token.second);
|
||||
nextX += display->getStringWidth(token.second);
|
||||
// Mixed ASCII + Chinese rendering
|
||||
const char *str = token.second.c_str();
|
||||
const char *p = str;
|
||||
while (*p) {
|
||||
int utf8len = 1;
|
||||
uint16_t cp = cfont12_utf8(p, &utf8len);
|
||||
if (cp >= 0x80 && cfont12_find(cp)) {
|
||||
// CJK character: draw using Chinese bitmap font
|
||||
nextX += cfont12_draw(display, nextX, yLine, cp);
|
||||
} else {
|
||||
// ASCII / non-CJK: render single char via drawString
|
||||
String oneChar = String(p).substring(0, utf8len);
|
||||
display->drawString(nextX, yLine, oneChar);
|
||||
nextX += display->getStringWidth(oneChar);
|
||||
}
|
||||
p += utf8len;
|
||||
}
|
||||
}
|
||||
}
|
||||
yLine += rowHeight;
|
||||
|
||||
Reference in New Issue
Block a user