mirror of
https://github.com/pelgraine/Meck.git
synced 2026-03-28 17:42:44 +01:00
Compare commits
6 Commits
duty-cycle
...
notes-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dd5c4f59f | ||
|
|
ee2a27258b | ||
|
|
5b868d51ca | ||
|
|
220006c229 | ||
|
|
a60f4146d5 | ||
|
|
017b170e81 |
@@ -8,11 +8,11 @@
|
||||
#define FIRMWARE_VER_CODE 8
|
||||
|
||||
#ifndef FIRMWARE_BUILD_DATE
|
||||
#define FIRMWARE_BUILD_DATE "11 Feb 2026"
|
||||
#define FIRMWARE_BUILD_DATE "12 Feb 2026"
|
||||
#endif
|
||||
|
||||
#ifndef FIRMWARE_VERSION
|
||||
#define FIRMWARE_VERSION "Meck v0.8.4"
|
||||
#define FIRMWARE_VERSION "Meck v0.8.5"
|
||||
#endif
|
||||
|
||||
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "TCA8418Keyboard.h"
|
||||
#include <SD.h>
|
||||
#include "TextReaderScreen.h"
|
||||
#include "NotesScreen.h"
|
||||
#include "ContactsScreen.h"
|
||||
#include "ChannelScreen.h"
|
||||
#include "SettingsScreen.h"
|
||||
@@ -25,7 +26,7 @@
|
||||
static uint8_t composeChannelIdx = 0;
|
||||
static unsigned long lastComposeRefresh = 0;
|
||||
static bool composeNeedsRefresh = false;
|
||||
#define COMPOSE_REFRESH_INTERVAL 600 // ms between e-ink refreshes while typing (refresh takes ~650ms)
|
||||
#define COMPOSE_REFRESH_INTERVAL 100 // ms before starting e-ink refresh after keypress (refresh itself takes ~644ms)
|
||||
|
||||
// DM compose mode (direct message to a specific contact)
|
||||
static bool composeDM = false;
|
||||
@@ -43,6 +44,9 @@
|
||||
// Text reader mode state
|
||||
static bool readerMode = false;
|
||||
|
||||
// Notes mode state
|
||||
static bool notesMode = false;
|
||||
|
||||
// Power management
|
||||
#if HAS_GPS
|
||||
GPSDutyCycle gpsDuty;
|
||||
@@ -400,7 +404,7 @@ void setup() {
|
||||
MESH_DEBUG_PRINTLN("setup() - SPIFFS.begin() done");
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Early SD card init — needed BEFORE the_mesh.begin() so we can restore
|
||||
// Early SD card init  needed BEFORE the_mesh.begin() so we can restore
|
||||
// settings from a previous firmware flash. The display SPI bus is already
|
||||
// up (display.begin() ran earlier), so SD can share it now.
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -508,6 +512,12 @@ void setup() {
|
||||
}
|
||||
}
|
||||
|
||||
// Tell notes screen that SD is ready
|
||||
NotesScreen* notesScr = (NotesScreen*)ui_task.getNotesScreen();
|
||||
if (notesScr) {
|
||||
notesScr->setSDReady(true);
|
||||
}
|
||||
|
||||
// Do an initial settings backup to SD (captures any first-boot defaults)
|
||||
backupSettingsToSD();
|
||||
}
|
||||
@@ -530,7 +540,7 @@ void setup() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// GPS duty cycle — honour saved pref, default to enabled on first boot
|
||||
// GPS duty cycle — honour saved pref, default to enabled on first boot
|
||||
#if HAS_GPS
|
||||
{
|
||||
bool gps_wanted = the_mesh.getNodePrefs()->gps_enabled;
|
||||
@@ -545,7 +555,7 @@ void setup() {
|
||||
}
|
||||
#endif
|
||||
|
||||
// CPU frequency scaling — drop to 80 MHz for idle mesh listening
|
||||
// CPU frequency scaling — drop to 80 MHz for idle mesh listening
|
||||
cpuPower.begin();
|
||||
|
||||
// T-Deck Pro: BLE starts disabled for standalone-first operation
|
||||
@@ -561,7 +571,7 @@ void setup() {
|
||||
void loop() {
|
||||
the_mesh.loop();
|
||||
|
||||
// GPS duty cycle — check for fix and manage power state
|
||||
// GPS duty cycle — check for fix and manage power state
|
||||
#if HAS_GPS
|
||||
{
|
||||
bool gps_hw_on = gpsDuty.loop();
|
||||
@@ -581,22 +591,33 @@ void loop() {
|
||||
#ifdef DISPLAY_CLASS
|
||||
// Skip UITask rendering when in compose mode to prevent flickering
|
||||
#if defined(LilyGo_TDeck_Pro)
|
||||
if (!composeMode) {
|
||||
// Also suppress during notes editing (same debounce pattern as compose)
|
||||
bool notesEditing = notesMode && ((NotesScreen*)ui_task.getNotesScreen())->isEditing();
|
||||
bool notesRenaming = notesMode && ((NotesScreen*)ui_task.getNotesScreen())->isRenaming();
|
||||
bool notesSuppressLoop = notesEditing || notesRenaming;
|
||||
if (!composeMode && !notesSuppressLoop) {
|
||||
ui_task.loop();
|
||||
} else {
|
||||
// Handle debounced compose/emoji picker screen refresh
|
||||
// Handle debounced screen refresh (compose, emoji picker, or notes editor)
|
||||
if (composeNeedsRefresh && (millis() - lastComposeRefresh) >= COMPOSE_REFRESH_INTERVAL) {
|
||||
if (emojiPickerMode) {
|
||||
drawEmojiPicker();
|
||||
} else {
|
||||
drawComposeScreen();
|
||||
if (composeMode) {
|
||||
if (emojiPickerMode) {
|
||||
drawEmojiPicker();
|
||||
} else {
|
||||
drawComposeScreen();
|
||||
}
|
||||
} else if (notesSuppressLoop) {
|
||||
// Notes editor/rename renders through UITask - force a refresh cycle
|
||||
ui_task.forceRefresh();
|
||||
ui_task.loop();
|
||||
}
|
||||
lastComposeRefresh = millis();
|
||||
composeNeedsRefresh = false;
|
||||
}
|
||||
}
|
||||
// Track reader mode state for key routing
|
||||
// Track reader/notes mode state for key routing
|
||||
readerMode = ui_task.isOnTextReader();
|
||||
notesMode = ui_task.isOnNotesScreen();
|
||||
#else
|
||||
ui_task.loop();
|
||||
#endif
|
||||
@@ -810,6 +831,141 @@ void handleKeyboardInput() {
|
||||
return;
|
||||
}
|
||||
|
||||
// *** NOTES MODE ***
|
||||
if (notesMode) {
|
||||
NotesScreen* notes = (NotesScreen*)ui_task.getNotesScreen();
|
||||
|
||||
// ---- EDITING MODE ----
|
||||
if (notes->isEditing()) {
|
||||
// Shift+Backspace = save and exit
|
||||
if (key == '\b') {
|
||||
if (keyboard.wasShiftConsumed()) {
|
||||
Serial.println("Notes: Shift+Backspace, saving...");
|
||||
notes->saveAndExit();
|
||||
ui_task.forceRefresh();
|
||||
return;
|
||||
}
|
||||
// Regular backspace - delete before cursor
|
||||
ui_task.injectKey(key);
|
||||
composeNeedsRefresh = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Cursor navigation via Shift+WASD (produces uppercase)
|
||||
if (key == 'W') { notes->moveCursorUp(); composeNeedsRefresh = true; return; }
|
||||
if (key == 'A') { notes->moveCursorLeft(); composeNeedsRefresh = true; return; }
|
||||
if (key == 'S') { notes->moveCursorDown(); composeNeedsRefresh = true; return; }
|
||||
if (key == 'D') { notes->moveCursorRight(); composeNeedsRefresh = true; return; }
|
||||
|
||||
// Q when buffer is empty or unchanged = exit (nothing to lose)
|
||||
if (key == 'q' && (notes->isEmpty() || !notes->isDirty())) {
|
||||
Serial.println("Notes: Q exit (nothing to save)");
|
||||
notes->discardAndExit();
|
||||
ui_task.forceRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter = newline (pass through with debounce)
|
||||
if (key == '\r') {
|
||||
ui_task.injectKey(key);
|
||||
composeNeedsRefresh = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// All other printable chars (lowercase only - uppercase consumed by cursor nav)
|
||||
if (key >= 32 && key < 127) {
|
||||
ui_task.injectKey(key);
|
||||
composeNeedsRefresh = true;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- RENAMING MODE ----
|
||||
if (notes->isRenaming()) {
|
||||
// All input goes to rename handler (debounced like editing)
|
||||
ui_task.injectKey(key);
|
||||
composeNeedsRefresh = true;
|
||||
if (!notes->isRenaming()) {
|
||||
// Exited rename mode (confirmed or cancelled)
|
||||
ui_task.forceRefresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- DELETE CONFIRMATION MODE ----
|
||||
if (notes->isConfirmingDelete()) {
|
||||
ui_task.injectKey(key);
|
||||
if (!notes->isConfirmingDelete()) {
|
||||
// Exited confirm mode
|
||||
ui_task.forceRefresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- FILE LIST MODE ----
|
||||
if (notes->isInFileList()) {
|
||||
if (key == 'q') {
|
||||
notes->exitNotes();
|
||||
Serial.println("Exiting notes");
|
||||
ui_task.gotoHomeScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
// Shift+Backspace on a file = delete with confirmation
|
||||
if (key == '\b' && keyboard.wasShiftConsumed()) {
|
||||
if (notes->startDeleteFromList()) {
|
||||
ui_task.forceRefresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// R on a file = rename
|
||||
if (key == 'r') {
|
||||
if (notes->startRename()) {
|
||||
composeNeedsRefresh = true;
|
||||
lastComposeRefresh = millis() - COMPOSE_REFRESH_INTERVAL; // Trigger on next loop iteration
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal keys pass through
|
||||
ui_task.injectKey(key);
|
||||
// Check if we just entered editing mode (new note via Enter)
|
||||
if (notes->isEditing()) {
|
||||
composeNeedsRefresh = true;
|
||||
lastComposeRefresh = millis(); // Draw after debounce interval, not immediately
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- READING MODE ----
|
||||
if (notes->isReading()) {
|
||||
if (key == 'q') {
|
||||
ui_task.injectKey('q');
|
||||
return;
|
||||
}
|
||||
|
||||
// Shift+Backspace = delete note
|
||||
if (key == '\b' && keyboard.wasShiftConsumed()) {
|
||||
Serial.println("Notes: Deleting current note");
|
||||
notes->deleteCurrentNote();
|
||||
ui_task.forceRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// All other keys (Enter for edit, W/S for page nav)
|
||||
ui_task.injectKey(key);
|
||||
if (notes->isEditing()) {
|
||||
composeNeedsRefresh = true;
|
||||
lastComposeRefresh = millis(); // Draw after debounce interval, not immediately
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// *** SETTINGS MODE ***
|
||||
if (ui_task.isOnSettingsScreen()) {
|
||||
SettingsScreen* settings = (SettingsScreen*)ui_task.getSettingsScreen();
|
||||
@@ -826,7 +982,7 @@ void handleKeyboardInput() {
|
||||
return;
|
||||
}
|
||||
|
||||
// All other keys → settings screen via injectKey
|
||||
// All other keys → settings screen via injectKey
|
||||
ui_task.injectKey(key);
|
||||
return;
|
||||
}
|
||||
@@ -851,6 +1007,20 @@ void handleKeyboardInput() {
|
||||
ui_task.gotoTextReader();
|
||||
break;
|
||||
|
||||
case 'n':
|
||||
// Open notes
|
||||
Serial.println("Opening notes");
|
||||
{
|
||||
NotesScreen* notesScr2 = (NotesScreen*)ui_task.getNotesScreen();
|
||||
if (notesScr2) {
|
||||
uint32_t ts = rtc_clock.getCurrentTime();
|
||||
int8_t utcOff = the_mesh.getNodePrefs()->utc_offset_hours;
|
||||
notesScr2->setTimestamp(ts, utcOff);
|
||||
}
|
||||
}
|
||||
ui_task.gotoNotesScreen();
|
||||
break;
|
||||
|
||||
case 's':
|
||||
// Open settings (from home), or navigate down on channel/contacts
|
||||
if (ui_task.isOnChannelScreen() || ui_task.isOnContactsScreen()) {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// =============================================================================
|
||||
// EpubProcessor.h - Convert EPUB files to plain text for TextReaderScreen
|
||||
//
|
||||
// Pipeline: EPUB (ZIP) → container.xml → OPF spine → extract chapters →
|
||||
// strip XHTML tags → concatenated plain text → cached .txt on SD
|
||||
// Pipeline: EPUB (ZIP) → container.xml → OPF spine → extract chapters →
|
||||
// strip XHTML tags → concatenated plain text → cached .txt on SD
|
||||
//
|
||||
// The resulting .txt file is placed in /books/ and picked up automatically
|
||||
// by TextReaderScreen's existing pagination, indexing, and bookmarking.
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <SD.h>
|
||||
#include <FS.h>
|
||||
#include "EpubZipReader.h"
|
||||
#include "Utf8CP437.h"
|
||||
|
||||
// Maximum chapters in spine (most novels have 20-80)
|
||||
#define EPUB_MAX_CHAPTERS 200
|
||||
@@ -426,7 +427,7 @@ private:
|
||||
//
|
||||
// Handles:
|
||||
// - Tag removal (everything between < and >)
|
||||
// - <p>, <br>, <div>, <h1>-<h6> → newlines
|
||||
// - <p>, <br>, <div>, <h1>-<h6> → newlines
|
||||
// - HTML entity decoding (& < > " ' &#NNN; &#xHH;)
|
||||
// - Collapse multiple whitespace/newlines
|
||||
// - Skip <head>, <style>, <script> content entirely
|
||||
@@ -547,9 +548,9 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
// Handle UTF-8 multi-byte sequences (smart quotes, em dashes, etc.)
|
||||
// These appear as raw bytes in XHTML and must be mapped to ASCII
|
||||
// since the e-ink font only supports ASCII characters.
|
||||
// Handle UTF-8 multi-byte sequences (smart quotes, em dashes, accented chars, etc.)
|
||||
// These appear as raw bytes in XHTML. Typographic chars are mapped to ASCII;
|
||||
// accented Latin chars are preserved as UTF-8 for CP437 rendering on e-ink.
|
||||
if ((uint8_t)c >= 0xC0) {
|
||||
uint32_t codepoint = 0;
|
||||
int extraBytes = 0;
|
||||
@@ -579,7 +580,8 @@ private:
|
||||
if (valid && extraBytes > 0) {
|
||||
p += extraBytes; // Skip continuation bytes (loop increments past lead byte)
|
||||
|
||||
// Map Unicode codepoints to ASCII equivalents
|
||||
// Map Unicode codepoints to displayable equivalents
|
||||
// Typographic chars → ASCII, accented chars → preserved as UTF-8
|
||||
char mapped = 0;
|
||||
switch (codepoint) {
|
||||
case 0x2018: case 0x2019: mapped = '\''; break; // Smart single quotes
|
||||
@@ -598,6 +600,21 @@ private:
|
||||
default:
|
||||
if (codepoint >= 0x20 && codepoint < 0x7F) {
|
||||
mapped = (char)codepoint; // Basic ASCII range
|
||||
} else if (unicodeToCP437(codepoint)) {
|
||||
// Accented character that the e-ink font can render via CP437.
|
||||
// Preserve as UTF-8 in the output; the text reader will decode
|
||||
// and map to CP437 at render time.
|
||||
if (codepoint <= 0x7FF) {
|
||||
output[outPos++] = 0xC0 | (codepoint >> 6);
|
||||
output[outPos++] = 0x80 | (codepoint & 0x3F);
|
||||
} else if (codepoint <= 0xFFFF) {
|
||||
output[outPos++] = 0xE0 | (codepoint >> 12);
|
||||
output[outPos++] = 0x80 | ((codepoint >> 6) & 0x3F);
|
||||
output[outPos++] = 0x80 | (codepoint & 0x3F);
|
||||
}
|
||||
lastWasNewline = false;
|
||||
lastWasSpace = false;
|
||||
continue; // Already wrote to output
|
||||
} else {
|
||||
continue; // Skip unmappable characters
|
||||
}
|
||||
@@ -608,7 +625,7 @@ private:
|
||||
continue; // Skip malformed UTF-8
|
||||
}
|
||||
} else if ((uint8_t)c >= 0x80) {
|
||||
// Stray continuation byte (0x80-0xBF) — skip
|
||||
// Stray continuation byte (0x80-0xBF) — skip
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -683,6 +700,37 @@ private:
|
||||
if (entityLen == 5 && strncmp(entity, "ldquo", 5) == 0) return '"';
|
||||
if (entityLen == 5 && strncmp(entity, "rdquo", 5) == 0) return '"';
|
||||
|
||||
// Common accented character entities → CP437 bytes for built-in font
|
||||
if (entityLen == 6 && strncmp(entity, "eacute", 6) == 0) return (char)0x82; // é
|
||||
if (entityLen == 6 && strncmp(entity, "egrave", 6) == 0) return (char)0x8A; // è
|
||||
if (entityLen == 5 && strncmp(entity, "ecirc", 5) == 0) return (char)0x88; // ê
|
||||
if (entityLen == 4 && strncmp(entity, "euml", 4) == 0) return (char)0x89; // ë
|
||||
if (entityLen == 6 && strncmp(entity, "agrave", 6) == 0) return (char)0x85; // à
|
||||
if (entityLen == 6 && strncmp(entity, "aacute", 6) == 0) return (char)0xA0; // á
|
||||
if (entityLen == 5 && strncmp(entity, "acirc", 5) == 0) return (char)0x83; // â
|
||||
if (entityLen == 4 && strncmp(entity, "auml", 4) == 0) return (char)0x84; // ä
|
||||
if (entityLen == 6 && strncmp(entity, "ccedil", 6) == 0) return (char)0x87; // ç
|
||||
if (entityLen == 6 && strncmp(entity, "iacute", 6) == 0) return (char)0xA1; // í
|
||||
if (entityLen == 5 && strncmp(entity, "icirc", 5) == 0) return (char)0x8C; // î
|
||||
if (entityLen == 4 && strncmp(entity, "iuml", 4) == 0) return (char)0x8B; // ï
|
||||
if (entityLen == 6 && strncmp(entity, "igrave", 6) == 0) return (char)0x8D; // ì
|
||||
if (entityLen == 6 && strncmp(entity, "oacute", 6) == 0) return (char)0xA2; // ó
|
||||
if (entityLen == 5 && strncmp(entity, "ocirc", 5) == 0) return (char)0x93; // ô
|
||||
if (entityLen == 4 && strncmp(entity, "ouml", 4) == 0) return (char)0x94; // ö
|
||||
if (entityLen == 6 && strncmp(entity, "ograve", 6) == 0) return (char)0x95; // ò
|
||||
if (entityLen == 6 && strncmp(entity, "uacute", 6) == 0) return (char)0xA3; // ú
|
||||
if (entityLen == 5 && strncmp(entity, "ucirc", 5) == 0) return (char)0x96; // û
|
||||
if (entityLen == 4 && strncmp(entity, "uuml", 4) == 0) return (char)0x81; // ü
|
||||
if (entityLen == 6 && strncmp(entity, "ugrave", 6) == 0) return (char)0x97; // ù
|
||||
if (entityLen == 6 && strncmp(entity, "ntilde", 6) == 0) return (char)0xA4; // ñ
|
||||
if (entityLen == 6 && strncmp(entity, "Eacute", 6) == 0) return (char)0x90; // É
|
||||
if (entityLen == 6 && strncmp(entity, "Ccedil", 6) == 0) return (char)0x80; // Ç
|
||||
if (entityLen == 6 && strncmp(entity, "Ntilde", 6) == 0) return (char)0xA5; // Ñ
|
||||
if (entityLen == 4 && strncmp(entity, "Auml", 4) == 0) return (char)0x8E; // Ä
|
||||
if (entityLen == 4 && strncmp(entity, "Ouml", 4) == 0) return (char)0x99; // Ö
|
||||
if (entityLen == 4 && strncmp(entity, "Uuml", 4) == 0) return (char)0x9A; // Ü
|
||||
if (entityLen == 5 && strncmp(entity, "szlig", 5) == 0) return (char)0xE1; // ß
|
||||
|
||||
// Numeric entities: &#NNN; or &#xHH;
|
||||
if (entityLen >= 2 && entity[0] == '#') {
|
||||
int codepoint = 0;
|
||||
@@ -701,14 +749,13 @@ private:
|
||||
if (ch >= '0' && ch <= '9') codepoint = codepoint * 10 + (ch - '0');
|
||||
}
|
||||
}
|
||||
// Map to ASCII (best effort - e-ink font is ASCII only)
|
||||
// Map to displayable character (best effort)
|
||||
if (codepoint >= 32 && codepoint < 127) return (char)codepoint;
|
||||
if (codepoint == 160) return ' '; // non-breaking space
|
||||
if (codepoint == 8211 || codepoint == 8212) return '-'; // en/em dash
|
||||
if (codepoint == 8216 || codepoint == 8217) return '\''; // smart quotes
|
||||
if (codepoint == 8220 || codepoint == 8221) return '"'; // smart quotes
|
||||
if (codepoint == 8230) return '.'; // ellipsis
|
||||
if (codepoint == 8226) return '*'; // bullet
|
||||
// Try CP437 mapping for accented characters.
|
||||
// The byte value will be passed through to the built-in font.
|
||||
uint8_t cp437 = unicodeToCP437(codepoint);
|
||||
if (cp437) return (char)cp437;
|
||||
// Unknown codepoint > 127: skip it
|
||||
return ' ';
|
||||
}
|
||||
1299
examples/companion_radio/ui-new/Notesscreen.h
Normal file
1299
examples/companion_radio/ui-new/Notesscreen.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <SD.h>
|
||||
#include <vector>
|
||||
#include "Utf8CP437.h"
|
||||
#include "EpubProcessor.h"
|
||||
|
||||
// Forward declarations
|
||||
@@ -14,7 +15,7 @@ class UITask;
|
||||
// ============================================================================
|
||||
#define BOOKS_FOLDER "/books"
|
||||
#define INDEX_FOLDER "/.indexes"
|
||||
#define INDEX_VERSION 4
|
||||
#define INDEX_VERSION 5 // v5: UTF-8 aware word wrap (accented char support)
|
||||
#define PREINDEX_PAGES 100
|
||||
#define READER_MAX_FILES 50
|
||||
#define READER_BUF_SIZE 4096
|
||||
@@ -57,6 +58,10 @@ inline WrapResult findLineBreak(const char* buffer, int bufLen, int lineStart, i
|
||||
}
|
||||
|
||||
if (c >= 32) {
|
||||
// Skip UTF-8 continuation bytes (0x80-0xBF) - the lead byte already
|
||||
// counted as one display character, so don't double-count these.
|
||||
if ((uint8_t)c >= 0x80 && (uint8_t)c < 0xC0) continue;
|
||||
|
||||
charCount++;
|
||||
if (c == ' ' || c == '\t') {
|
||||
if (inWord) {
|
||||
@@ -855,12 +860,40 @@ private:
|
||||
if (wrap.nextStart <= oldPos && wrap.lineEnd >= _pageBufLen) break;
|
||||
|
||||
display.setCursor(0, y);
|
||||
// Print line character by character (only printable)
|
||||
// Print line with UTF-8 decoding: multi-byte sequences are decoded
|
||||
// to Unicode codepoints, then mapped to CP437 for the built-in font.
|
||||
char charStr[2] = {0, 0};
|
||||
for (int j = pos; j < wrap.lineEnd && j < _pageBufLen; j++) {
|
||||
if (_pageBuf[j] >= 32) {
|
||||
charStr[0] = _pageBuf[j];
|
||||
int j = pos;
|
||||
while (j < wrap.lineEnd && j < _pageBufLen) {
|
||||
uint8_t b = (uint8_t)_pageBuf[j];
|
||||
|
||||
if (b < 32) {
|
||||
// Control character — skip
|
||||
j++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (b < 0x80) {
|
||||
// Plain ASCII — print directly
|
||||
charStr[0] = (char)b;
|
||||
display.print(charStr);
|
||||
j++;
|
||||
} else if (b >= 0xC0) {
|
||||
// UTF-8 lead byte — decode full sequence and map to CP437
|
||||
int savedJ = j;
|
||||
uint32_t cp = decodeUtf8Char(_pageBuf, wrap.lineEnd, &j);
|
||||
uint8_t glyph = unicodeToCP437(cp);
|
||||
if (glyph) {
|
||||
charStr[0] = (char)glyph;
|
||||
display.print(charStr);
|
||||
}
|
||||
// If unmappable (glyph==0), just skip the character
|
||||
} else {
|
||||
// Standalone byte 0x80-0xBF: not a valid UTF-8 lead byte.
|
||||
// Treat as CP437 pass-through (e.g. from EPUB numeric entity decoding).
|
||||
charStr[0] = (char)b;
|
||||
display.print(charStr);
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -878,8 +911,9 @@ private:
|
||||
display.drawRect(0, footerY - 2, display.width(), 1);
|
||||
display.setColor(DisplayDriver::YELLOW);
|
||||
|
||||
char status[20];
|
||||
sprintf(status, "%d/%d", _currentPage + 1, _totalPages);
|
||||
char status[30];
|
||||
int pct = _totalPages > 1 ? (_currentPage * 100) / (_totalPages - 1) : 100;
|
||||
sprintf(status, "%d/%d %d%%", _currentPage + 1, _totalPages, pct);
|
||||
display.setCursor(0, footerY);
|
||||
display.print(status);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "UITask.h"
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include "../MyMesh.h"
|
||||
#include "NotesScreen.h"
|
||||
#include "target.h"
|
||||
#include "GPSDutyCycle.h"
|
||||
#ifdef WIFI_SSID
|
||||
@@ -373,7 +374,7 @@ public:
|
||||
display.drawTextRightAlign(display.width()-1, y, buf);
|
||||
y = y + 12;
|
||||
|
||||
// NMEA sentence counter — confirms baud rate and data flow
|
||||
// NMEA sentence counter  confirms baud rate and data flow
|
||||
display.drawTextLeftAlign(0, y, "sentences");
|
||||
if (gpsDuty.isHardwareOn()) {
|
||||
uint16_t sps = gpsStream.getSentencesPerSec();
|
||||
@@ -747,6 +748,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
channel_screen = new ChannelScreen(this, &rtc_clock);
|
||||
contacts_screen = new ContactsScreen(this, &rtc_clock);
|
||||
text_reader = new TextReaderScreen(this);
|
||||
notes_screen = new NotesScreen(this);
|
||||
settings_screen = new SettingsScreen(this, &rtc_clock, node_prefs);
|
||||
setCurrScreen(splash);
|
||||
}
|
||||
@@ -1080,13 +1082,13 @@ void UITask::toggleGPS() {
|
||||
|
||||
if (_sensors != NULL) {
|
||||
if (_node_prefs->gps_enabled) {
|
||||
// Disable GPS — cut hardware power
|
||||
// Disable GPS  cut hardware power
|
||||
_sensors->setSettingValue("gps", "0");
|
||||
_node_prefs->gps_enabled = 0;
|
||||
gpsDuty.disable();
|
||||
notify(UIEventType::ack);
|
||||
} else {
|
||||
// Enable GPS — start duty cycle
|
||||
// Enable GPS  start duty cycle
|
||||
_sensors->setSettingValue("gps", "1");
|
||||
_node_prefs->gps_enabled = 1;
|
||||
gpsDuty.enable();
|
||||
@@ -1184,6 +1186,19 @@ void UITask::gotoTextReader() {
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoNotesScreen() {
|
||||
NotesScreen* notes = (NotesScreen*)notes_screen;
|
||||
if (_display != NULL) {
|
||||
notes->enter(*_display);
|
||||
}
|
||||
setCurrScreen(notes_screen);
|
||||
if (_display != NULL && !_display->isOn()) {
|
||||
_display->turnOn();
|
||||
}
|
||||
_auto_off = millis() + AUTO_OFF_MILLIS;
|
||||
_next_refresh = 100;
|
||||
}
|
||||
|
||||
void UITask::gotoSettingsScreen() {
|
||||
((SettingsScreen *) settings_screen)->enter();
|
||||
setCurrScreen(settings_screen);
|
||||
|
||||
@@ -54,6 +54,7 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* channel_screen; // Channel message history screen
|
||||
UIScreen* contacts_screen; // Contacts list screen
|
||||
UIScreen* text_reader; // *** NEW: Text reader screen ***
|
||||
UIScreen* notes_screen; // Notes editor screen
|
||||
UIScreen* settings_screen; // Settings/onboarding screen
|
||||
UIScreen* curr;
|
||||
|
||||
@@ -80,6 +81,7 @@ public:
|
||||
void gotoChannelScreen(); // Navigate to channel message screen
|
||||
void gotoContactsScreen(); // Navigate to contacts list
|
||||
void gotoTextReader(); // *** NEW: Navigate to text reader ***
|
||||
void gotoNotesScreen(); // Navigate to notes editor
|
||||
void gotoSettingsScreen(); // Navigate to settings
|
||||
void gotoOnboarding(); // Navigate to settings in onboarding mode
|
||||
void showAlert(const char* text, int duration_millis) override;
|
||||
@@ -90,6 +92,7 @@ public:
|
||||
bool isOnChannelScreen() const { return curr == channel_screen; }
|
||||
bool isOnContactsScreen() const { return curr == contacts_screen; }
|
||||
bool isOnTextReader() const { return curr == text_reader; } // *** NEW ***
|
||||
bool isOnNotesScreen() const { return curr == notes_screen; }
|
||||
bool isOnSettingsScreen() const { return curr == settings_screen; }
|
||||
uint8_t getChannelScreenViewIdx() const;
|
||||
|
||||
@@ -110,6 +113,7 @@ public:
|
||||
UIScreen* getCurrentScreen() const { return curr; }
|
||||
UIScreen* getMsgPreviewScreen() const { return msg_preview; }
|
||||
UIScreen* getTextReaderScreen() const { return text_reader; } // *** NEW ***
|
||||
UIScreen* getNotesScreen() const { return notes_screen; }
|
||||
UIScreen* getContactsScreen() const { return contacts_screen; }
|
||||
UIScreen* getChannelScreen() const { return channel_screen; }
|
||||
UIScreen* getSettingsScreen() const { return settings_screen; }
|
||||
|
||||
152
examples/companion_radio/ui-new/Utf8cp437.h
Normal file
152
examples/companion_radio/ui-new/Utf8cp437.h
Normal file
@@ -0,0 +1,152 @@
|
||||
#pragma once
|
||||
// =============================================================================
|
||||
// Utf8CP437.h - UTF-8 decoding and Unicode-to-CP437 mapping
|
||||
//
|
||||
// The Adafruit GFX built-in 6x8 font uses the CP437 character set for codes
|
||||
// 128-255. This header provides utilities to:
|
||||
// 1. Decode UTF-8 multi-byte sequences into Unicode codepoints
|
||||
// 2. Map Unicode codepoints to CP437 byte values for display
|
||||
//
|
||||
// Used by both EpubProcessor (at XHTML→text conversion time) and
|
||||
// TextReaderScreen (at render time for plain .txt files).
|
||||
// =============================================================================
|
||||
|
||||
// Map a Unicode codepoint to its CP437 equivalent byte.
|
||||
// Returns the CP437 byte (0x80-0xFF) for supported accented characters,
|
||||
// the codepoint itself for ASCII (0x20-0x7E), or 0 if unmappable.
|
||||
inline uint8_t unicodeToCP437(uint32_t cp) {
|
||||
// ASCII passthrough
|
||||
if (cp >= 0x20 && cp < 0x7F) return (uint8_t)cp;
|
||||
|
||||
switch (cp) {
|
||||
// Uppercase accented
|
||||
case 0x00C7: return 0x80; // Ç
|
||||
case 0x00C9: return 0x90; // É
|
||||
case 0x00C4: return 0x8E; // Ä
|
||||
case 0x00C5: return 0x8F; // Å
|
||||
case 0x00C6: return 0x92; // Æ
|
||||
case 0x00D6: return 0x99; // Ö
|
||||
case 0x00DC: return 0x9A; // Ü
|
||||
case 0x00D1: return 0xA5; // Ñ
|
||||
|
||||
// Lowercase accented
|
||||
case 0x00E9: return 0x82; // é
|
||||
case 0x00E2: return 0x83; // â
|
||||
case 0x00E4: return 0x84; // ä
|
||||
case 0x00E0: return 0x85; // à
|
||||
case 0x00E5: return 0x86; // å
|
||||
case 0x00E7: return 0x87; // ç
|
||||
case 0x00EA: return 0x88; // ê
|
||||
case 0x00EB: return 0x89; // ë
|
||||
case 0x00E8: return 0x8A; // è
|
||||
case 0x00EF: return 0x8B; // ï
|
||||
case 0x00EE: return 0x8C; // î
|
||||
case 0x00EC: return 0x8D; // ì
|
||||
case 0x00E6: return 0x91; // æ
|
||||
case 0x00F4: return 0x93; // ô
|
||||
case 0x00F6: return 0x94; // ö
|
||||
case 0x00F2: return 0x95; // ò
|
||||
case 0x00FB: return 0x96; // û
|
||||
case 0x00F9: return 0x97; // ù
|
||||
case 0x00FF: return 0x98; // ÿ
|
||||
case 0x00FC: return 0x81; // ü
|
||||
case 0x00E1: return 0xA0; // á
|
||||
case 0x00ED: return 0xA1; // í
|
||||
case 0x00F3: return 0xA2; // ó
|
||||
case 0x00FA: return 0xA3; // ú
|
||||
case 0x00F1: return 0xA4; // ñ
|
||||
|
||||
// Currency / symbols
|
||||
case 0x00A2: return 0x9B; // ¢
|
||||
case 0x00A3: return 0x9C; // £
|
||||
case 0x00A5: return 0x9D; // ¥
|
||||
case 0x00BF: return 0xA8; // ¿
|
||||
case 0x00A1: return 0xAD; // ¡
|
||||
case 0x00AB: return 0xAE; // «
|
||||
case 0x00BB: return 0xAF; // »
|
||||
case 0x00B0: return 0xF8; // °
|
||||
case 0x00B1: return 0xF1; // ±
|
||||
case 0x00B5: return 0xE6; // µ
|
||||
case 0x00DF: return 0xE1; // ß
|
||||
|
||||
// Typographic (smart quotes, dashes, etc.)
|
||||
case 0x2018: case 0x2019: return '\''; // Smart single quotes
|
||||
case 0x201C: case 0x201D: return '"'; // Smart double quotes
|
||||
case 0x2013: case 0x2014: return '-'; // En/em dash
|
||||
case 0x2010: case 0x2011: case 0x2012: case 0x2015: return '-'; // Hyphens/bars
|
||||
case 0x2026: return 0xFD; // Ellipsis (CP437 has no …, use ²? no, skip)
|
||||
case 0x2022: return 0x07; // Bullet → CP437 bullet
|
||||
case 0x00A0: return ' '; // Non-breaking space
|
||||
case 0x2039: case 0x203A: return '\''; // Single guillemets
|
||||
case 0x2032: return '\''; // Prime
|
||||
case 0x2033: return '"'; // Double prime
|
||||
|
||||
default: return 0; // Unmappable
|
||||
}
|
||||
}
|
||||
|
||||
// Decode a single UTF-8 character from a byte buffer.
|
||||
// Returns the Unicode codepoint and advances *pos past the full sequence.
|
||||
// If the sequence is invalid, returns 0xFFFD (replacement char) and advances by 1.
|
||||
//
|
||||
// buf: input buffer
|
||||
// bufLen: total buffer length
|
||||
// pos: pointer to current position (updated on return)
|
||||
inline uint32_t decodeUtf8Char(const char* buf, int bufLen, int* pos) {
|
||||
int i = *pos;
|
||||
if (i >= bufLen) return 0;
|
||||
|
||||
uint8_t c = (uint8_t)buf[i];
|
||||
|
||||
// ASCII (single byte)
|
||||
if (c < 0x80) {
|
||||
*pos = i + 1;
|
||||
return c;
|
||||
}
|
||||
|
||||
// Continuation byte without lead byte — skip
|
||||
if (c < 0xC0) {
|
||||
*pos = i + 1;
|
||||
return 0xFFFD;
|
||||
}
|
||||
|
||||
uint32_t codepoint;
|
||||
int extraBytes;
|
||||
|
||||
if ((c & 0xE0) == 0xC0) {
|
||||
codepoint = c & 0x1F;
|
||||
extraBytes = 1;
|
||||
} else if ((c & 0xF0) == 0xE0) {
|
||||
codepoint = c & 0x0F;
|
||||
extraBytes = 2;
|
||||
} else if ((c & 0xF8) == 0xF0) {
|
||||
codepoint = c & 0x07;
|
||||
extraBytes = 3;
|
||||
} else {
|
||||
*pos = i + 1;
|
||||
return 0xFFFD;
|
||||
}
|
||||
|
||||
// Verify we have enough bytes and they're valid continuation bytes
|
||||
if (i + extraBytes >= bufLen) {
|
||||
*pos = i + 1;
|
||||
return 0xFFFD;
|
||||
}
|
||||
|
||||
for (int b = 1; b <= extraBytes; b++) {
|
||||
uint8_t cb = (uint8_t)buf[i + b];
|
||||
if ((cb & 0xC0) != 0x80) {
|
||||
*pos = i + 1;
|
||||
return 0xFFFD;
|
||||
}
|
||||
codepoint = (codepoint << 6) | (cb & 0x3F);
|
||||
}
|
||||
|
||||
*pos = i + 1 + extraBytes;
|
||||
return codepoint;
|
||||
}
|
||||
|
||||
// Check if a byte is a UTF-8 continuation byte (10xxxxxx)
|
||||
inline bool isUtf8Continuation(uint8_t c) {
|
||||
return (c & 0xC0) == 0x80;
|
||||
}
|
||||
@@ -28,7 +28,10 @@ private:
|
||||
uint8_t _addr;
|
||||
TwoWire* _wire;
|
||||
bool _initialized;
|
||||
bool _shiftActive; // Sticky shift (one-shot)
|
||||
bool _shiftActive; // Sticky shift (one-shot or held)
|
||||
bool _shiftConsumed; // Was shift active for the last returned key
|
||||
bool _shiftHeld; // Shift key physically held down
|
||||
bool _shiftUsedWhileHeld; // Was shift consumed by any key while held
|
||||
bool _altActive; // Sticky alt (one-shot)
|
||||
bool _symActive; // Sticky sym (one-shot)
|
||||
unsigned long _lastShiftTime; // For Shift+key combos
|
||||
@@ -148,7 +151,7 @@ private:
|
||||
public:
|
||||
TCA8418Keyboard(uint8_t addr = 0x34, TwoWire* wire = &Wire)
|
||||
: _addr(addr), _wire(wire), _initialized(false),
|
||||
_shiftActive(false), _altActive(false), _symActive(false), _lastShiftTime(0) {}
|
||||
_shiftActive(false), _shiftConsumed(false), _shiftHeld(false), _shiftUsedWhileHeld(false), _altActive(false), _symActive(false), _lastShiftTime(0) {}
|
||||
|
||||
bool begin() {
|
||||
// Check if device responds
|
||||
@@ -203,6 +206,19 @@ public:
|
||||
Serial.printf("KB raw: event=0x%02X code=%d pressed=%d count=%d\n",
|
||||
keyEvent, keyCode, pressed, keyCount);
|
||||
|
||||
// Track shift release (before the general release-ignore)
|
||||
if (!pressed && (keyCode == 35 || keyCode == 31)) {
|
||||
_shiftHeld = false;
|
||||
// If shift was used while held (e.g. cursor nav), clear it completely
|
||||
// so the next bare keypress isn't treated as shifted.
|
||||
// If shift was NOT used (tap-then-release), keep _shiftActive for one-shot.
|
||||
if (_shiftUsedWhileHeld) {
|
||||
_shiftActive = false;
|
||||
}
|
||||
_shiftUsedWhileHeld = false;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Only act on key press, not release
|
||||
if (!pressed || keyCode == 0) {
|
||||
return 0;
|
||||
@@ -211,6 +227,8 @@ public:
|
||||
// Handle modifier keys - set sticky state and return 0
|
||||
if (keyCode == 35 || keyCode == 31) { // Shift keys
|
||||
_shiftActive = true;
|
||||
_shiftHeld = true;
|
||||
_shiftUsedWhileHeld = false;
|
||||
_lastShiftTime = millis();
|
||||
Serial.println("KB: Shift activated");
|
||||
return 0;
|
||||
@@ -276,7 +294,17 @@ public:
|
||||
if (c >= 'a' && c <= 'z') {
|
||||
c = c - 'a' + 'A';
|
||||
}
|
||||
_shiftActive = false; // Reset sticky shift
|
||||
// Track that shift was used while physically held
|
||||
if (_shiftHeld) {
|
||||
_shiftUsedWhileHeld = true;
|
||||
}
|
||||
// Only clear shift if it's one-shot (tap), not held down
|
||||
if (!_shiftHeld) {
|
||||
_shiftActive = false;
|
||||
}
|
||||
_shiftConsumed = true; // Record that shift was active for this key
|
||||
} else {
|
||||
_shiftConsumed = false;
|
||||
}
|
||||
|
||||
if (c != 0) {
|
||||
@@ -294,4 +322,10 @@ public:
|
||||
bool wasShiftRecentlyPressed(unsigned long withinMs = 500) const {
|
||||
return (millis() - _lastShiftTime) < withinMs;
|
||||
}
|
||||
|
||||
// Check if shift was active when the most recent key was produced
|
||||
// (immune to e-ink refresh timing unlike wasShiftRecentlyPressed)
|
||||
bool wasShiftConsumed() const {
|
||||
return _shiftConsumed;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user