Merge origin/keymindCascade with companion button hints

This commit is contained in:
mikecarper committed 2026-09-08 17:24:27 -07:00
commit d89d28f7ed
78 files changed
+6858 -601

No files matched your search

+1 -1
View File
@@ -4675,7 +4675,7 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
#endif
} else if (configKeyEquals(config, "bootloader.ver")) {
#ifdef NRF52_PLATFORM
char ver[32];
char ver[128]; // preserve preview/fork/build suffixes without truncation
if (_board->getBootloaderVersion(ver, sizeof(ver))) {
sprintf(reply, "> %s", ver);
} else {
+53
View File
@@ -0,0 +1,53 @@
#include "CompanionJohn.h"
#if COMPANION_FEATURE_JOHN
#include <Arduino.h>
#include "bible/JohnLookup.h"
#include "bible/JohnData.generated.h"
namespace mesh {
bible::LookupResult readJohnVerse(bible::Reference ref, char* scratch,
size_t capacity, const char*& text) {
return bible::lookup(bible::generated::johnCorpus, ref, scratch, capacity, text);
}
namespace {
// Keep the decode buffer out of the parser's frame, including under LTO:
// unrelated terminal commands must not reserve another 2 KiB of stack.
__attribute__((noinline)) void printVerse(bible::Reference ref, Stream& output) {
char scratch[bible::kBlockSize];
const char* text = nullptr;
const bible::LookupResult result = readJohnVerse(ref, scratch, sizeof(scratch), text);
if (result == bible::LookupResult::Found) {
output.printf(" John %u:%u (%s)\r\n", static_cast<unsigned>(ref.chapter),
static_cast<unsigned>(ref.verse), bible::generated::johnCorpus.translation);
// Do not feed long text through Adafruit Print::printf's 256-byte scratch.
output.print(text);
output.print("\r\n");
output.print(bible::generated::johnCorpus.attribution);
output.print("\r\n");
} else if (result == bible::LookupResult::Missing) {
output.print(" This verse is not present in the supplied translation.\r\n");
} else {
output.print(" ERROR: invalid compressed John data\r\n");
}
}
} // namespace
bool handleJohnCommand(const char* command, Stream& output) {
bible::Reference ref = {};
const bible::ParseResult parsed = bible::parse(command, ref);
if (parsed == bible::ParseResult::NoMatch) return false;
if (parsed == bible::ParseResult::Invalid) {
output.print(" ERROR: use get John <chapter>:<verse> (example: get John 3:16)\r\n");
} else {
printVerse(ref, output);
}
return true;
}
} // namespace mesh
#endif
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include "CompanionJohnConfig.h"
#if COMPANION_FEATURE_JOHN
#include "bible/JohnLookup.h"
class Stream;
namespace mesh {
// Caller-owned scratch lets the terminal and screen share one flash corpus.
bible::LookupResult readJohnVerse(bible::Reference ref, char* scratch,
size_t capacity, const char*& text);
// Terminal-only: verses can exceed the framed/remote CLI's 160-byte reply.
bool handleJohnCommand(const char* command, Stream& output);
}
#endif
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#ifndef COMPANION_FEATURE_JOHN
#if defined(COMPANION_RADIO_FULL) && defined(ENABLE_USB_INTERFACE) \
&& (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM))
#define COMPANION_FEATURE_JOHN 1
#else
#define COMPANION_FEATURE_JOHN 0
#endif
#endif
#if COMPANION_FEATURE_JOHN \
&& !(defined(COMPANION_RADIO_FULL) && defined(ENABLE_USB_INTERFACE) \
&& (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)))
#error "John lookup requires an ESP32 or nRF52 Full Companion USB terminal"
#endif
+10 -16
View File
@@ -1,6 +1,7 @@
#if defined(NRF52_PLATFORM)
#include "NRF52Board.h"
#include "PowerManagementUtils.h"
#include "Nrf52BootloaderVersion.h"
#include <target.h>
#ifdef USER_GPIO_CONTROL
#include "UserGpioPinPolicy.h"
@@ -598,22 +599,15 @@ void NRF52Board::powerOff() {
}
bool NRF52Board::getBootloaderVersion(char* out, size_t max_len) {
static const char BOOTLOADER_MARKER[] = "UF2 Bootloader ";
const uint8_t* flash = (const uint8_t*)0x000FB000; // earliest known info.txt location is 0xFB90B, latest is 0xFCC4B
for (uint32_t i = 0; i < 0x3000 - (sizeof(BOOTLOADER_MARKER) - 1); i++) {
if (memcmp(&flash[i], BOOTLOADER_MARKER, sizeof(BOOTLOADER_MARKER) - 1) == 0) {
const char* ver = (const char*)&flash[i + sizeof(BOOTLOADER_MARKER) - 1];
size_t len = 0;
while (len < max_len - 1 && ver[len] != '\0' && ver[len] != ' ' && ver[len] != '\n' && ver[len] != '\r') {
out[len] = ver[len];
len++;
}
out[len] = '\0';
return len > 0; // bootloader string is non-empty
}
}
return false;
if (!out || max_len == 0u) return false;
out[0] = 0;
mesh::Nrf52BootloaderRegion region;
const uint32_t mbr_start = *(const volatile uint32_t*)(uintptr_t)0xFF8u;
if (!mesh::nrf52BootloaderRegion(NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE,
mbr_start, NRF_UICR->NRFFW[0], region)) return false;
return mesh::nrf52BootloaderVersion((const uint8_t*)(uintptr_t)region.start,
region.end - region.start, region.start,
bootloaderVersion, out, max_len);
}
bool NRF52Board::startOTAUpdate(const char *id, char reply[], bool force_ap) {
+139
View File
@@ -0,0 +1,139 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "ota/OtaBootloaderUpdate.h"
namespace mesh {
struct Nrf52BootloaderRegion {
uint32_t start = 0;
uint32_t end = 0;
};
// The MBR's flash word takes precedence over UICR, just as it does at boot.
// Exclude the MBR-parameter and settings pages. Never search the application,
// an OTA staging slot, external flash, or beyond the chip's physical flash.
inline bool nrf52BootloaderRegion(uint32_t page_size, uint32_t page_count,
uint32_t mbr_start, uint32_t uicr_start,
Nrf52BootloaderRegion& region) {
region = Nrf52BootloaderRegion();
if (page_size != 4096u || page_count < 16u || page_count > 256u) return false;
const uint32_t flash_size = page_size * page_count;
const uint32_t end = flash_size - 2u * page_size;
uint32_t start = mbr_start != UINT32_MAX ? mbr_start : uicr_start;
// Historical Adafruit/OTAFIX installations can have both words erased.
// Their 40 KiB layout is accepted only with valid vectors by the reader.
if (start == UINT32_MAX) start = end - 0xA000u;
if (start < 0x10000u || start >= end || start % page_size != 0u) return false;
region.start = start;
region.end = end;
return true;
}
inline bool nrf52BootloaderVectorsValid(const uint8_t* image, size_t size,
uint32_t start) {
if (!image || size < 8u || size > UINT32_MAX - start) return false;
const uint32_t sp = ota::ota_boot_rd32(image);
const uint32_t reset = ota::ota_boot_rd32(image + 4u);
return sp > 0x20000000u && sp <= 0x20040000u && (sp & 7u) == 0u &&
(reset & 1u) != 0u && (reset & ~1u) >= start &&
(reset & ~1u) < start + size;
}
inline bool nrf52BootVersionChar(uint8_t c) {
return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') || c == '.' || c == '-' || c == '_' || c == '+';
}
// Return the complete token, never a truncated (and possibly different) version.
// An embedded marker literal with no actual version is not an INFO_UF2 record.
inline size_t nrf52BootVersionTokenSize(const uint8_t* token, size_t available) {
size_t pos = available && token[0] == 'v' ? 1u : 0u;
for (unsigned component = 0; component < 3u; ++component) {
const size_t begin = pos;
while (pos < available && token[pos] >= '0' && token[pos] <= '9') ++pos;
if (pos == begin) return 0;
if (component != 2u && (pos >= available || token[pos++] != '.')) return 0;
}
size_t n = 0;
for (; n < available && n <= 127u; ++n) {
const uint8_t c = token[n];
if (c == 0 || c == ' ' || c == '\t' || c == '\r' || c == '\n')
return n;
if (!nrf52BootVersionChar(c)) return 0;
}
return 0; // unterminated or too long
}
inline bool nrf52BootloaderVersion(const uint8_t* image, size_t size,
uint32_t start, uint32_t cached_base_version,
char* out, size_t capacity) {
if (!out || capacity == 0u) return false;
out[0] = 0;
if (capacity < 2u || !nrf52BootloaderVectorsValid(image, size, start)) return false;
static const char marker[] = "UF2 Bootloader ";
const size_t marker_size = sizeof(marker) - 1u;
const uint8_t* selected = nullptr;
size_t selected_size = 0;
bool conflicting = false;
for (size_t offset = 0; offset + marker_size < size; ++offset) {
if (image[offset] != 'U' || memcmp(image + offset, marker, marker_size) != 0) continue;
const uint8_t* token = image + offset + marker_size;
const size_t length = nrf52BootVersionTokenSize(token, size - offset - marker_size);
if (length == 0u) continue;
if (selected && (length != selected_size || memcmp(selected, token, length) != 0))
conflicting = true;
selected = token;
selected_size = length;
}
if (selected && !conflicting) {
if (selected_size >= capacity) return false;
memcpy(out, selected, selected_size);
out[selected_size] = 0;
return true;
}
// Reuse the installer's whole-image CRC and unique BLMF/BLM2 validation.
// Metadata contains the OTAFIX version, not Adafruit's base release number;
// do not fabricate a 0.x.y prefix when INFO_UF2 is absent or ambiguous.
ota::OtaBootloaderIdentity identity;
if (start == ota::OTA_BOOT_IMAGE_START && size == ota::OTA_BOOT_IMAGE_SIZE &&
ota::ota_bootloader_identity_from_image(image, size, identity) &&
identity.continuity_present) {
const uint32_t version = identity.boot_version;
const unsigned preview = version & 0xffu;
int written;
if (preview == 0xffu) {
written = snprintf(out, capacity, "OTAFIX%u.%u.%u",
(unsigned)(version >> 24), (unsigned)((version >> 16) & 0xffu),
(unsigned)((version >> 8) & 0xffu));
} else {
written = snprintf(out, capacity, "OTAFIX%u.%u.%u-preview.%u",
(unsigned)(version >> 24), (unsigned)((version >> 16) & 0xffu),
(unsigned)((version >> 8) & 0xffu), preview);
}
if (written > 0 && (size_t)written < capacity) return true;
out[0] = 0;
return false;
}
if (conflicting) return false;
// Adafruit's core captures TIMER2->CC[0] at startup, before the timer can
// be reused. This is only a base version (no OTAFIX/fork suffix). Never read
// the live timer here or reinterpret Nordic settings with a different layout.
if (cached_base_version != 0u && cached_base_version <= 0x00ffffffu) {
const int written = snprintf(out, capacity, "%u.%u.%u (base)",
(unsigned)(cached_base_version >> 16),
(unsigned)((cached_base_version >> 8) & 0xffu),
(unsigned)(cached_base_version & 0xffu));
if (written > 0 && (size_t)written < capacity) return true;
out[0] = 0;
}
return false;
}
} // namespace mesh
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include "JohnReader.h"
namespace mesh { namespace bible {
// A verified replacement plus one previous record also works on SPIFFS,
// whose rename does not replace an existing destination. Never truncate the
// live bookmark. A reset between the two renames is recovered from .bak.
template <typename Files>
bool loadReaderBookmark(Files& files, Position& pos) {
uint8_t data[kBookmarkBytes];
pos = Position{};
if (files.read("/john.pos", data) && decodeBookmark(data, pos)) return true;
return files.read("/john.pos.bak", data) && decodeBookmark(data, pos);
}
template <typename Files>
bool saveReaderBookmark(Files& files, Position pos) {
if (!pos.valid()) return false;
if (pos.atStart()) {
// Remove the fallback before the live record so a completed clear cannot
// resurrect an earlier place on reboot. Do not create a start marker.
return files.remove("/john.pos.tmp") && files.remove("/john.pos.bak")
&& files.remove("/john.pos");
}
uint8_t data[kBookmarkBytes], verify[kBookmarkBytes];
encodeBookmark(pos, data);
if (!files.remove("/john.pos.tmp") || !files.write("/john.pos.tmp", data)) return false;
if (!files.read("/john.pos.tmp", verify) || memcmp(data, verify, sizeof(data)) != 0)
return false;
if (files.exists("/john.pos")) {
// Keep a valid fallback if the live record was interrupted/corrupt.
Position old;
// A failed read is not proof of corruption. Preserve the live record on
// transient I/O failure rather than deleting it before publication.
if (!files.read("/john.pos", verify)) return false;
if (decodeBookmark(verify, old)) {
if (!files.remove("/john.pos.bak") || !files.rename("/john.pos", "/john.pos.bak"))
return false;
} else if (!files.remove("/john.pos")) return false;
}
if (!files.rename("/john.pos.tmp", "/john.pos")) return false;
// Retain the last valid fallback until the next successful replacement.
return true;
}
}} // namespace mesh::bible
File diff suppressed because it is too large. Load diff
+124
View File
@@ -0,0 +1,124 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include "../ota/tinf/tinf.h"
namespace mesh { namespace bible {
static constexpr size_t kBlockSize = 2048;
static constexpr uint16_t kVerseCount = 879;
static constexpr uint8_t kChapterVerses[] = {
51, 25, 36, 54, 47, 71, 53, 59, 41, 42, 57,
50, 38, 31, 27, 33, 26, 40, 42, 31, 25
};
struct Reference { uint8_t chapter; uint8_t verse; };
enum class ParseResult { NoMatch, Invalid, Valid };
struct BlockIndex {
uint32_t offset;
uint16_t packed;
uint16_t plain;
uint16_t first_verse;
};
struct Corpus {
const char* translation;
const char* attribution;
const uint8_t* data;
uint32_t data_size;
const BlockIndex* blocks;
uint16_t block_count;
uint16_t verse_count;
};
enum class LookupResult { Found, Missing, Invalid, Corrupt };
inline bool space(char c) { return c == ' ' || c == '\t'; }
inline void skipSpace(const char*& p) { while (space(*p)) ++p; }
inline char lower(char c) { return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; }
inline bool token(const char*& p, const char* word) {
const char* q = p;
for (; *word; ++word, ++q) {
if (lower(*q) != *word) return false;
}
if (*q && !space(*q)) return false;
p = q;
return true;
}
inline bool number(const char*& p, uint8_t& result) {
unsigned value = 0;
const char* start = p;
while (*p >= '0' && *p <= '9') {
if (p - start >= 2) return false;
value = value * 10 + static_cast<unsigned>(*p++ - '0');
}
if (p == start || value == 0) return false;
result = static_cast<uint8_t>(value);
return true;
}
inline bool verseNumber(Reference ref, uint16_t& index) {
if (ref.chapter == 0 || ref.chapter > sizeof(kChapterVerses) ||
ref.verse == 0 || ref.verse > kChapterVerses[ref.chapter - 1]) return false;
index = ref.verse - 1;
for (unsigned c = 0; c + 1 < ref.chapter; ++c) index += kChapterVerses[c];
return true;
}
inline ParseResult parse(const char* command, Reference& ref) {
if (!command) return ParseResult::NoMatch;
skipSpace(command);
if (!token(command, "get")) return ParseResult::NoMatch;
skipSpace(command);
if (!token(command, "john")) return ParseResult::NoMatch;
skipSpace(command);
if (!number(command, ref.chapter) || *command != ':') return ParseResult::Invalid;
++command;
if (!number(command, ref.verse)) return ParseResult::Invalid;
skipSpace(command);
uint16_t unused;
return *command == 0 && verseNumber(ref, unused)
? ParseResult::Valid : ParseResult::Invalid;
}
// One independently compressed block is decoded on demand. Const tables/data
// stay in memory-mapped flash on both target families; there is no heap/cache.
inline LookupResult lookup(const Corpus& corpus, Reference ref,
char* scratch, size_t capacity, const char*& text) {
text = nullptr;
uint16_t index;
if (!verseNumber(ref, index)) return LookupResult::Invalid;
if (!scratch || capacity < kBlockSize || !corpus.data || !corpus.blocks ||
corpus.block_count == 0 || corpus.verse_count != kVerseCount ||
corpus.blocks[0].first_verse != 0) return LookupResult::Corrupt;
// Index only the start of each block. The few verse boundaries inside it
// are found by scanning NUL separators, saving a per-verse flash table.
unsigned block_index = 0;
while (block_index + 1 < corpus.block_count &&
corpus.blocks[block_index + 1].first_verse <= index) ++block_index;
const BlockIndex block = corpus.blocks[block_index];
if (block.plain == 0 || block.plain > kBlockSize || block.packed == 0 ||
block.offset > corpus.data_size || block.packed > corpus.data_size - block.offset ||
block.first_verse > index) return LookupResult::Corrupt;
unsigned int produced = block.plain;
if (tinf_uncompress_exact(scratch, &produced, corpus.data + block.offset,
block.packed) != TINF_OK || produced != block.plain) {
return LookupResult::Corrupt;
}
const char* current = scratch;
const char* end = scratch + produced;
for (unsigned v = block.first_verse; v <= index; ++v) {
const char* terminator = static_cast<const char*>(memchr(current, 0, end - current));
if (!terminator) return LookupResult::Corrupt;
if (v == index) {
if (terminator == current) return LookupResult::Missing;
text = current;
break;
}
current = terminator + 1;
}
return LookupResult::Found;
}
}} // namespace mesh::bible
+131
View File
@@ -0,0 +1,131 @@
#pragma once
#include "JohnLookup.h"
namespace mesh { namespace bible {
// Offset is into the display text, not a screen number, so rotation/font
// changes can resume at the page containing the same words.
struct Position {
uint16_t verse = 0;
uint16_t offset = 0;
bool atStart() const { return verse == 0 && offset == 0; }
bool valid() const { return verse < kVerseCount && offset < kBlockSize; }
bool operator==(const Position& other) const {
return verse == other.verse && offset == other.offset;
}
bool operator!=(const Position& other) const { return !(*this == other); }
};
inline Reference referenceAt(uint16_t index) {
Reference ref = {1, 1};
if (index >= kVerseCount) return ref;
while (index >= kChapterVerses[ref.chapter - 1]) {
index -= kChapterVerses[ref.chapter++ - 1];
}
ref.verse = static_cast<uint8_t>(index + 1);
return ref;
}
constexpr size_t kReaderLineBytes = 128;
// Return the next unread byte, preferring word boundaries. Never discard a
// word or cut a UTF-8 codepoint, including overlong words on narrow displays.
template <typename Measure>
uint16_t readerLine(const char* text, uint16_t start, int width,
Measure measure, char (&line)[kReaderLineBytes]) {
size_t count = 0, last_space = 0;
while (text[start + count]) {
size_t bytes = 1;
while (bytes < 4 && text[start + count + bytes]
&& (static_cast<uint8_t>(text[start + count + bytes]) & 0xc0) == 0x80) ++bytes;
if (count + bytes >= sizeof(line)) break;
memcpy(line + count, text + start + count, bytes);
line[count + bytes] = 0;
const int pixels = static_cast<int>(measure(line));
if (pixels > width && count != 0) break;
if (text[start + count] == ' ') last_space = count;
count += bytes;
if (pixels > width) break; // one too-wide glyph still makes progress
}
if (text[start + count] && last_space > 0) count = last_space;
memcpy(line, text + start, count);
line[count] = 0;
uint16_t next = static_cast<uint16_t>(start + count);
while (text[next] == ' ') ++next;
return next;
}
struct ReaderPage {
uint16_t start = 0, next = 0, previous = 0, part = 1, parts = 1;
};
template <typename Measure>
ReaderPage readerPage(const char* text, uint16_t offset, int width,
int rows, Measure measure) {
ReaderPage result;
if (rows < 1) rows = 1;
uint16_t start = 0, previous = 0, part = 0;
char line[kReaderLineBytes];
do {
uint16_t next = start;
for (int row = 0; row < rows && text[next]; ++row)
next = readerLine(text, next, width, measure, line);
++part;
if (start <= offset) {
result.start = start;
result.next = next;
result.previous = previous;
result.part = part;
}
previous = start;
start = next;
} while (text[start]);
result.parts = part;
return result;
}
// Fixed, versioned record independent of compiler padding and radio prefs.
constexpr size_t kBookmarkBytes = 12;
inline uint32_t bookmarkChecksum(const uint8_t* data) {
uint32_t hash = 2166136261UL;
for (unsigned i = 0; i < 8; ++i) hash = (hash ^ data[i]) * 16777619UL;
return hash;
}
inline void encodeBookmark(Position pos, uint8_t (&out)[kBookmarkBytes]) {
out[0] = 'J'; out[1] = 'N'; out[2] = 1; out[3] = 0;
out[4] = pos.verse; out[5] = pos.verse >> 8;
out[6] = pos.offset; out[7] = pos.offset >> 8;
const uint32_t hash = bookmarkChecksum(out);
for (unsigned i = 0; i < 4; ++i) out[8 + i] = hash >> (8 * i);
}
inline bool decodeBookmark(const uint8_t (&in)[kBookmarkBytes], Position& pos) {
pos = Position{};
if (in[0] != 'J' || in[1] != 'N' || in[2] != 1 || in[3] != 0) return false;
const uint32_t hash = bookmarkChecksum(in);
for (unsigned i = 0; i < 4; ++i)
if (in[8 + i] != static_cast<uint8_t>(hash >> (8 * i))) return false;
Position candidate;
candidate.verse = in[4] | (static_cast<uint16_t>(in[5]) << 8);
candidate.offset = in[6] | (static_cast<uint16_t>(in[7]) << 8);
if (!candidate.valid() || candidate.atStart()) return false;
pos = candidate;
return true;
}
// Debounced checkpointing: no writes just for opening John 1:1, no rewrite
// of unchanged positions, and a failed save remains pending for retry.
class ReaderBookmark {
Position _position, _saved;
uint32_t _changed_at = 0;
public:
Position position() const { return _position; }
void restore(Position pos) { _position = _saved = pos.valid() ? pos : Position{}; }
void move(Position pos, uint32_t now) {
if (pos.valid() && pos != _position) { _position = pos; _changed_at = now; }
}
bool dirty() const { return _position != _saved; }
bool due(uint32_t now) const { return dirty() && uint32_t(now - _changed_at) >= 2000; }
void saved() { _saved = _position; }
};
}} // namespace mesh::bible
+2 -1
View File
@@ -2,7 +2,8 @@
* Compile the vendored tinf core as C. Building the same source as C++ costs
* several extra kilobytes with the embedded toolchains used by MeshCore.
*/
#if defined(ENABLE_OTA) || defined(OTA_TRANSPORT_DEFLATE_TEST)
#include "../CompanionJohnConfig.h"
#if defined(ENABLE_OTA) || defined(OTA_TRANSPORT_DEFLATE_TEST) || COMPANION_FEATURE_JOHN
#if defined(__GNUC__) && !defined(__clang__)
/* nRF52's Arduino recipe defaults to -Ofast, which more than doubles this
* decoder. Keep the size policy local to tinf instead of changing MeshCore. */
+5 -4
View File
@@ -12,11 +12,12 @@ Vendored files:
MeshCore changes are marked in tinf.h and tinflate.c. They add
tinf_uncompress_exact(), which accepts legal padding bits in the byte containing
the final end-of-block code but rejects any trailing whole input byte. The C
implementation is included by ../OtaTinf.c only for ENABLE_OTA application
builds (and the native OTA test profile). ../OtaDeflate.cpp is the small C++
implementation is included by ../OtaTinf.c for ENABLE_OTA application builds,
COMPANION_FEATURE_JOHN offline lookup, and the native OTA test profile.
../OtaDeflate.cpp is the small C++
callback adapter. Keeping the decoder in a true C translation unit avoids the
several-kilobyte penalty produced by the embedded C++ toolchains; non-OTA
firmware links no decoder code or persistent RAM.
several-kilobyte penalty produced by the embedded C++ toolchains. Firmware
with neither feature links no decoder code or persistent RAM.
Only the raw RFC 1951 decoder is integrated. The zlib/gzip wrappers and checksum
sources are intentionally omitted. The original zlib license is preserved in
+1 -1
View File
@@ -27,7 +27,7 @@
* MeshCore modifications (2026):
* - expose tinf_uncompress_exact() to reject trailing whole input bytes;
* - compile this vendored implementation through OtaTinf.c only, keeping
* it out of builds which do not define ENABLE_OTA.
* it out of builds which need neither OTA nor offline John lookup.
* See README.meshcore.txt for the upstream revision and integration details.
*/
+12 -5
View File
@@ -27,21 +27,28 @@ struct CompanionMessageChromeLayout {
};
inline CompanionMessageChromeLayout makeCompanionMessageChromeLayout(
bool compact_text) {
bool compact_text, int native_line_height = 0) {
if (compact_text) {
// At the Indicator's 3x physical mapping these are a 27px header and a
// 42px footer around a 24px-tall fractionally scaled status font.
return {true, 8, 11, 22, 14, 3};
}
if (native_line_height > 11) {
const int bar_height = native_line_height + 8 > 24 ? native_line_height + 8 : 24;
return {false, native_line_height + 1, native_line_height + 4,
2 * native_line_height + 4, bar_height,
(bar_height - native_line_height) / 2};
}
return {false, 11, 14, 25, 24, 8};
}
inline CompanionMessageListLayout makeCompanionMessageListLayout(
int display_height) {
const int top = 20;
const int row_height = 26;
int display_height, int native_line_height = 10) {
if (native_line_height < 10) native_line_height = 10;
const int top = native_line_height + 4 > 20 ? native_line_height + 4 : 20;
const int row_height = native_line_height * 2 + 6;
const int usable_height = display_height > top ? display_height - top : 0;
return {top, row_height, 0, 11, 23, usable_height / row_height};
return {top, row_height, 0, native_line_height + 1, row_height - 3, usable_height / row_height};
}
inline void formatCompanionMessageAge(char* output, size_t output_size,
+3 -2
View File
@@ -19,8 +19,9 @@
// These drivers back physically compact panels where the message-channel
// selector consumes too much of the useful text area. Do not infer this from
// DisplayDriver::height(): larger scaled TFTs intentionally expose the same
// 128x64 logical canvas.
// DisplayDriver::height(): the 160x80 ST7735 is still physically compact.
// This footer policy is separate from the runtime small-font cutoff (<160px
// on both axes); retaining the flag also retains the full message buffer.
#if defined(DISPLAY_CLASS) \
&& MESHCORE_DISPLAY_TOKEN_JOIN(MESHCORE_SMALL_DISPLAY_CLASS_, DISPLAY_CLASS)
#define MESHCORE_HAS_SMALL_DISPLAY 1
+6
View File
@@ -31,6 +31,12 @@ public:
int width() const { return _w; }
int height() const { return _h; }
// Dense 5/6px body fonts are only for sub-160px panels, in either rotation.
bool useSmallMessageFont() const { return _w < 160 && _h < 160; }
virtual int textLineHeight() {
const int glyph = getTextWidth("M");
return glyph <= 6 ? 10 : glyph * 2;
}
virtual bool isOn() = 0;
virtual bool supportsRotation() const { return false; }
+15 -38
View File
@@ -38,6 +38,8 @@ bool GxEPDDisplay::begin() {
#endif
display.init(115200, true, 2, false);
display.setRotation(DISPLAY_ROTATION);
setDimensions(display.width(), display.height());
display.setTextWrap(false);
setTextSize(1); // Default to size 1
display.setFullWindow();
@@ -129,6 +131,12 @@ void GxEPDDisplay::setTextSize(int sz) {
display.setFont(&FreeSans9pt7b);
break;
}
// GFX uses a baseline; the UI supplies the top of the text line.
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds("Ag", 0, 0, &x1, &y1, &w, &h);
_text_ascent = -y1;
_line_height = h + 4;
}
void GxEPDDisplay::setColor(ColorVal c) {
@@ -139,7 +147,7 @@ void GxEPDDisplay::setColor(ColorVal c) {
void GxEPDDisplay::setCursor(int x, int y) {
display_crc.update<int>(x);
display_crc.update<int>(y);
display.setCursor((x+offset_x)*scale_x, (y+offset_y)*scale_y);
display.setCursor(x, y + _text_ascent);
}
void GxEPDDisplay::print(const char* str) {
@@ -152,7 +160,7 @@ void GxEPDDisplay::fillRect(int x, int y, int w, int h) {
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display.fillRect(x*scale_x, y*scale_y, w*scale_x, h*scale_y, _curr_color);
display.fillRect(x, y, w, h, _curr_color);
}
void GxEPDDisplay::drawRect(int x, int y, int w, int h) {
@@ -160,55 +168,24 @@ void GxEPDDisplay::drawRect(int x, int y, int w, int h) {
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display.drawRect(x*scale_x, y*scale_y, w*scale_x, h*scale_y, _curr_color);
display.drawRect(x, y, w, h, _curr_color);
}
void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
if (!bits || w <= 0 || h <= 0) return;
display_crc.update<int>(x);
display_crc.update<int>(y);
display_crc.update<int>(w);
display_crc.update<int>(h);
display_crc.update<uint8_t>(bits, w * h / 8);
// Calculate the base position in display coordinates
uint16_t startX = x * scale_x;
uint16_t startY = y * scale_y;
// Width in bytes for bitmap processing
uint16_t widthInBytes = (w + 7) / 8;
// Process the bitmap row by row
for (uint16_t by = 0; by < h; by++) {
// Calculate the target y-coordinates for this logical row
int y1 = startY + (int)(by * scale_y);
int y2 = startY + (int)((by + 1) * scale_y);
int block_h = y2 - y1;
// Scan across the row bit by bit
for (uint16_t bx = 0; bx < w; bx++) {
// Calculate the target x-coordinates for this logical column
int x1 = startX + (int)(bx * scale_x);
int x2 = startX + (int)((bx + 1) * scale_x);
int block_w = x2 - x1;
// Get the current bit
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
uint8_t bitMask = 0x80 >> (bx & 7);
bool bitSet = pgm_read_byte(bits + byteOffset) & bitMask;
// If the bit is set, draw a block of pixels
if (bitSet) {
// Draw the block as a filled rectangle
display.fillRect(x1, y1, block_w, block_h, _curr_color);
}
}
}
display_crc.update<uint8_t>(bits, ((w + 7) / 8) * h);
display.drawBitmap(x, y, bits, w, h, _curr_color);
}
uint16_t GxEPDDisplay::getTextWidth(const char* str) {
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return ceil((w + 1) / scale_x);
return w ? w + 1 : 0;
}
void GxEPDDisplay::endFrame() {
+16 -10
View File
@@ -22,21 +22,26 @@
#define EINK_MAX_PARTIAL_REFRESH -1
#endif
#ifndef DISPLAY_ROTATION
#define DISPLAY_ROTATION 3
#endif
class GxEPDDisplay : public DisplayDriver {
#if defined(EINK_DISPLAY_MODEL)
GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT> display;
const float scale_x = EINK_SCALE_X;
const float scale_y = EINK_SCALE_Y;
const float offset_x = EINK_X_OFFSET;
const float offset_y = EINK_Y_OFFSET;
#else
GxEPD2_BW<GxEPD2_150_BN, 200> display;
const float scale_x = 1.5625f;
const float scale_y = 1.5625f;
const float offset_x = 0;
const float offset_y = 10;
#endif
#if defined(EINK_DISPLAY_MODEL)
using Panel = EINK_DISPLAY_MODEL;
#else
using Panel = GxEPD2_150_BN;
#endif
static constexpr int kWidth = (DISPLAY_ROTATION & 1) ? Panel::HEIGHT : Panel::WIDTH;
static constexpr int kHeight = (DISPLAY_ROTATION & 1) ? Panel::WIDTH : Panel::HEIGHT;
int _text_ascent = 13;
int _line_height = 22;
bool _init = false;
bool _isOn = false;
uint16_t _curr_color;
@@ -47,9 +52,9 @@ class GxEPDDisplay : public DisplayDriver {
public:
#if defined(EINK_DISPLAY_MODEL)
GxEPDDisplay() : DisplayDriver(128, 128), display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {}
GxEPDDisplay() : DisplayDriver(kWidth, kHeight), display(EINK_DISPLAY_MODEL(PIN_DISPLAY_CS, PIN_DISPLAY_DC, PIN_DISPLAY_RST, PIN_DISPLAY_BUSY)) {}
#else
GxEPDDisplay() : DisplayDriver(128, 128), display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {}
GxEPDDisplay() : DisplayDriver(kWidth, kHeight), display(GxEPD2_150_BN(DISP_CS, DISP_DC, DISP_RST, DISP_BUSY)) {}
#endif
bool begin();
@@ -63,6 +68,7 @@ public:
void clear() override;
void startFrame(ColorVal bkg = UIColor::window_bkg) override;
void setTextSize(int sz) override;
int textLineHeight() override { return _line_height; }
void setColor(ColorVal c) override;
void setCursor(int x, int y) override;
void print(const char* str) override;
+12 -63
View File
@@ -51,22 +51,6 @@
#define DISPLAY_ROTATION 0
#endif
#ifndef NV3001B_SCREEN_WIDTH
#define NV3001B_SCREEN_WIDTH 220
#endif
#ifndef NV3001B_SCREEN_HEIGHT
#define NV3001B_SCREEN_HEIGHT 128
#endif
#ifndef DISPLAY_SCALE_X
#define DISPLAY_SCALE_X ((float)NV3001B_SCREEN_WIDTH / NV3001B_LOGICAL_WIDTH)
#endif
#ifndef DISPLAY_SCALE_Y
#define DISPLAY_SCALE_Y ((float)NV3001B_SCREEN_HEIGHT / NV3001B_LOGICAL_HEIGHT)
#endif
#define NV3001B_SWRESET 0x01
#define NV3001B_SLPOUT 0x11
#define NV3001B_DISPON 0x29
@@ -81,22 +65,6 @@
#define NV3001B_MADCTL_MV 0x20
#define NV3001B_MADCTL_RGB 0x00
#ifndef NV3001B_TEXT_SIZE1_SCALE_X
#define NV3001B_TEXT_SIZE1_SCALE_X 1
#endif
#ifndef NV3001B_TEXT_SIZE1_SCALE_Y
#define NV3001B_TEXT_SIZE1_SCALE_Y 2
#endif
#ifndef NV3001B_TEXT_SIZE2_SCALE_X
#define NV3001B_TEXT_SIZE2_SCALE_X 2
#endif
#ifndef NV3001B_TEXT_SIZE2_SCALE_Y
#define NV3001B_TEXT_SIZE2_SCALE_Y 3
#endif
// Color scheme
ColorVal UIColor::window_bkg = mesh::ui::color_theme::WINDOW_BACKGROUND;
ColorVal UIColor::title_bkg = mesh::ui::color_theme::TITLE_BACKGROUND;
@@ -108,26 +76,6 @@ ColorVal UIColor::popup_bkg = mesh::ui::color_theme::POPUP_BACKGROUND;
ColorVal UIColor::popup_txt = mesh::ui::color_theme::TEXT;
ColorVal UIColor::corp_blue = mesh::ui::color_theme::ACCENT;
static int scaleX(int x) {
return (int)(x * DISPLAY_SCALE_X);
}
static int scaleY(int y) {
return (int)(y * DISPLAY_SCALE_Y);
}
static int scaleWidth(int x, int w) {
if (w <= 0) return 0;
int scaled = scaleX(x + w) - scaleX(x);
return scaled > 0 ? scaled : 1;
}
static int scaleHeight(int y, int h) {
if (h <= 0) return 0;
int scaled = scaleY(y + h) - scaleY(y);
return scaled > 0 ? scaled : 1;
}
static uint8_t nv3001bMADCTL(uint8_t rotation) {
uint8_t madctl;
switch (rotation & 3) {
@@ -181,11 +129,11 @@ static const uint8_t font5x7[] PROGMEM = {
};
static int textPixelScaleX(uint8_t size) {
return size <= 1 ? NV3001B_TEXT_SIZE1_SCALE_X : NV3001B_TEXT_SIZE2_SCALE_X;
return size;
}
static int textPixelScaleY(uint8_t size) {
return size <= 1 ? NV3001B_TEXT_SIZE1_SCALE_Y : NV3001B_TEXT_SIZE2_SCALE_Y;
return size;
}
static void setupOptionalOutput(int pin, int level) {
@@ -487,8 +435,8 @@ void NV3001BDisplay::setColor(ColorVal c) {
}
void NV3001BDisplay::setCursor(int x, int y) {
cursor_x = scaleX(x);
cursor_y = scaleY(y);
cursor_x = x;
cursor_y = y;
}
void NV3001BDisplay::print(const char* str) {
@@ -511,14 +459,15 @@ void NV3001BDisplay::print(const char* str) {
}
void NV3001BDisplay::fillRect(int x, int y, int w, int h) {
fillPhysicalRect(scaleX(x), scaleY(y), scaleWidth(x, w), scaleHeight(y, h));
fillPhysicalRect(x, y, w, h);
}
void NV3001BDisplay::drawRect(int x, int y, int w, int h) {
int x1 = scaleX(x);
int y1 = scaleY(y);
int sw = scaleWidth(x, w);
int sh = scaleHeight(y, h);
if (w <= 0 || h <= 0) return;
int x1 = x;
int y1 = y;
int sw = w;
int sh = h;
fillPhysicalRect(x1, y1, sw, 1);
fillPhysicalRect(x1, y1 + sh - 1, sw, 1);
@@ -534,7 +483,7 @@ void NV3001BDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
for (int i = 0; i < w; i++) {
uint8_t byte = pgm_read_byte(bits + j * byte_width + i / 8);
if (byte & (0x80 >> (i & 7))) {
fillPhysicalRect(scaleX(x + i), scaleY(y + j), scaleWidth(x + i, 1), scaleHeight(y + j, 1));
fillPhysicalRect(x + i, y + j, 1, 1);
}
}
}
@@ -545,7 +494,7 @@ uint16_t NV3001BDisplay::getTextWidth(const char* str) {
uint16_t len = 0;
while (str[len] && str[len] != '\n' && str[len] != '\r') len++;
return (uint16_t)((len * 6 * textPixelScaleX(text_size)) / DISPLAY_SCALE_X);
return (uint16_t)((len * 6 * textPixelScaleX(text_size)));
}
void NV3001BDisplay::endFrame() {
+6 -6
View File
@@ -4,12 +4,12 @@
#include <SPI.h>
#include <helpers/RefCountedDigitalPin.h>
#ifndef NV3001B_LOGICAL_WIDTH
#define NV3001B_LOGICAL_WIDTH 128
// Landscape dimensions, matching the panel controller's configured orientation.
#ifndef NV3001B_SCREEN_WIDTH
#define NV3001B_SCREEN_WIDTH 220
#endif
#ifndef NV3001B_LOGICAL_HEIGHT
#define NV3001B_LOGICAL_HEIGHT 64
#ifndef NV3001B_SCREEN_HEIGHT
#define NV3001B_SCREEN_HEIGHT 128
#endif
#ifndef NV3001B_PANEL_WIDTH
@@ -44,7 +44,7 @@ class NV3001BDisplay : public DisplayDriver {
public:
NV3001BDisplay(RefCountedDigitalPin* power = nullptr) :
DisplayDriver(NV3001B_LOGICAL_WIDTH, NV3001B_LOGICAL_HEIGHT), spi(NV3001B_SPI_HOST), periph_power(power) { }
DisplayDriver(NV3001B_SCREEN_WIDTH, NV3001B_SCREEN_HEIGHT), spi(NV3001B_SPI_HOST), periph_power(power) { }
bool begin();
static const char* driverName() { return "NV3001B"; }
+31 -36
View File
@@ -54,10 +54,8 @@ inline void applyDarkPalette() {
}
// ----------------------------------------------------------------- layout ---
// Logical 128x64 coordinates, as the shared DisplayDriver API expects. The two
// orientations need separate row pitches: a text row is a fixed 16 physical
// pixels, which is 3.2 logical units in portrait (y scale 5) but 4.27 in
// landscape (y scale 3.75), so one shared grid would overlap in landscape.
// Native panel pixels. Each orientation has its own spacing and graph area;
// the driver neither stretches coordinates nor silently resizes text.
struct Layout {
int16_t margin_x;
@@ -74,41 +72,41 @@ struct Layout {
int16_t graph_h; // includes the one-unit baseline at the bottom
int16_t rf_y;
int16_t status_y;
int16_t text_h; // logical height of one size-1 text row
int16_t text_h; // native height reserved for one size-1 text row
int16_t max_chars; // size-1 characters that fit between the margins
int16_t max_chars_big; // size-2 characters that fit
};
// 240x320 panel: x scale 1.875, y scale 5, 12x16 glyphs (24x32 at size 2).
// 240x320 portrait, normal 6x8 glyph cells (12x16 at explicit size 2).
constexpr Layout portraitLayout() {
return Layout{
/*margin_x*/ 4, /*right_x*/ 124,
/*header_h*/ 9, /*header_text_y*/ 1, /*header_sub_y*/ 5,
/*radio_y*/ 11,
/*window_y*/ 16,
/*headline_y*/ 21, /*headline_h*/ 7,
/*rate_y*/ 29,
/*graph_y*/ 34, /*graph_h*/ 16,
/*rf_y*/ 52,
/*status_y*/ 57,
/*text_h*/ 4,
/*max_chars*/ 18, /*max_chars_big*/ 9};
/*margin_x*/ 8, /*right_x*/ 232,
/*header_h*/ 36, /*header_text_y*/ 6, /*header_sub_y*/ 20,
/*radio_y*/ 48,
/*window_y*/ 72,
/*headline_y*/ 96, /*headline_h*/ 20,
/*rate_y*/ 128,
/*graph_y*/ 156, /*graph_h*/ 92,
/*rf_y*/ 266,
/*status_y*/ 290,
/*text_h*/ 10,
/*max_chars*/ 37, /*max_chars_big*/ 18};
}
// 320x240 panel: x scale 2.5, y scale 3.75, 12x16 glyphs (30x40 at size 2).
// 320x240 landscape with the same normal font, without coordinate scaling.
constexpr Layout landscapeLayout() {
return Layout{
/*margin_x*/ 4, /*right_x*/ 124,
/*header_h*/ 7, /*header_text_y*/ 1, /*header_sub_y*/ -1,
/*radio_y*/ 9,
/*window_y*/ 14,
/*headline_y*/ 19, /*headline_h*/ 12,
/*rate_y*/ 31,
/*graph_y*/ 36, /*graph_h*/ 12,
/*rf_y*/ 50,
/*status_y*/ 56,
/*text_h*/ 5,
/*max_chars*/ 25, /*max_chars_big*/ 10};
/*margin_x*/ 10, /*right_x*/ 310,
/*header_h*/ 24, /*header_text_y*/ 8, /*header_sub_y*/ -1,
/*radio_y*/ 36,
/*window_y*/ 58,
/*headline_y*/ 80, /*headline_h*/ 20,
/*rate_y*/ 110,
/*graph_y*/ 136, /*graph_h*/ 50,
/*rf_y*/ 200,
/*status_y*/ 222,
/*text_h*/ 10,
/*max_chars*/ 50, /*max_chars_big*/ 25};
}
inline const Layout& activeLayout() {
@@ -212,11 +210,8 @@ inline void formatRadioStrip(char* out, size_t n, float freq, uint8_t sf, float
snprintf(out, n, "%.3f SF%u BW%s", (double)freq, (unsigned)sf, bw_str);
}
// Trims to a character budget rather than a measured width. The font is fixed
// width, so the budget is exact - and DisplayDriver::drawTextEllipsized() must
// not be used here: it trims against getTextWidth(), which reports an
// over-long string at the portrait driver's *fallback* scale and so stops
// trimming while the string is still too wide to draw at full size.
// The normal font is fixed-width, so native layouts have exact character
// budgets. Also show an ellipsis when the intermediate buffer limits a name.
inline void fitToChars(char* out, size_t n, const char* src, int max_chars) {
if (max_chars < 0) max_chars = 0;
if ((size_t)max_chars > n - 1) max_chars = (int)(n - 1);
@@ -461,13 +456,13 @@ inline void drawRow(DisplayDriver& d, const Layout& l, Row row, const Context& c
inline void drawHeader(DisplayDriver& d, const Layout& l, const Context& ctx) {
d.setColor(HEADER_BG);
d.fillRect(0, 0, 128, l.header_h);
d.fillRect(0, 0, d.width(), l.header_h);
d.setTextSize(1);
RowText t{};
t.left_color = TEXT;
t.right_color = HEADER_SUB;
snprintf(t.left, sizeof(t.left), "%s", ctx.node_name ? ctx.node_name : "");
fitToChars(t.left, sizeof(t.left), ctx.node_name, l.max_chars);
snprintf(t.right, sizeof(t.right), "%s", ctx.role_label ? ctx.role_label : "");
if (l.header_sub_y < 0) {
+6 -10
View File
@@ -409,8 +409,7 @@ static void setAddrWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h) {
writeCommand(ST77XX_RAMWR); // write to RAM
}
#define SCALE_X 1.25f // 160 / 128
#define SCALE_Y 1.25f // 80 / 64
// Compile-time 1:1 on T096; keep the legacy 1.25x coordinates on other boards.
static TFT_eSprite *sprite = NULL;
@@ -622,7 +621,7 @@ void ST7735Display::setColor(ColorVal c) {
void ST7735Display::setCursor(int x, int y) {
if (!spriteReady()) return;
sprite->setCursor(x*SCALE_X, y*SCALE_Y);
sprite->setCursor(x, y);
}
void ST7735Display::print(const char* str) {
@@ -632,25 +631,22 @@ void ST7735Display::print(const char* str) {
void ST7735Display::fillRect(int x, int y, int w, int h) {
if (!spriteReady()) return;
// 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);
sprite->fillRect(x, y, w, h, curr_color);
}
void ST7735Display::drawRect(int x, int y, int w, int h) {
if (!spriteReady()) return;
sprite->drawRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, curr_color);
sprite->drawRect(x, y, w, h, curr_color);
}
void ST7735Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
if (!spriteReady()) return;
sprite->drawBitmap(x*SCALE_X, y*SCALE_Y, bits, w, h, curr_color);
sprite->drawBitmap(x, y, bits, w, h, curr_color);
}
uint16_t ST7735Display::getTextWidth(const char* str) {
if (!spriteReady()) return 0;
return sprite->textWidth(str) / SCALE_X;
return sprite->textWidth(str);
}
void ST7735Display::endFrame() {
+4 -2
View File
@@ -6,20 +6,22 @@
#include <helpers/RefCountedDigitalPin.h>
class ST7735Display : public DisplayDriver {
// One UI coordinate is one pixel on every 160x80 ST7735 panel.
static constexpr int kLogicalWidth = 160, kLogicalHeight = 80;
bool _isOn;
RefCountedDigitalPin* _peripher_power;
bool i2c_probe(TwoWire& wire, uint8_t addr);
public:
#ifdef USE_PIN_TFT
ST7735Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
ST7735Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(kLogicalWidth, kLogicalHeight),
// display(PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL, PIN_TFT_RST),
_peripher_power(peripher_power)
{
_isOn = false;
}
#else
ST7735Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
ST7735Display(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(kLogicalWidth, kLogicalHeight),
// display(&SPI1, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST),
_peripher_power(peripher_power)
{
+18 -65
View File
@@ -2,29 +2,7 @@
#include "ST7789Display.h"
#ifndef X_OFFSET
#define X_OFFSET 0 // No offset needed for landscape
#endif
#ifndef Y_OFFSET
#define Y_OFFSET 1 // Vertical offset to prevent top row cutoff
#endif
#ifdef HELTEC_VISION_MASTER_T190
#define SCALE_X 2.5f // 320 / 128
#define SCALE_Y 2.65625f // 170 / 64
#else
#define SCALE_X 1.875f // 240 / 128
#define SCALE_Y 2.109375f // 135 / 64
#endif
#ifdef DISPLAY_SCALE_X
#define SCALE_X DISPLAY_SCALE_X
#endif
#ifdef DISPLAY_SCALE_Y
#define SCALE_Y DISPLAY_SCALE_Y
#endif
// Native coordinates; controller RAM offsets are handled by ST7789Spi.
// Color scheme
ColorVal UIColor::window_bkg = OLEDDISPLAY_COLOR::BLACK;
@@ -68,8 +46,9 @@ void ST7789Display::turnOn() {
digitalWrite(PIN_TFT_VDD_CTL, LOW);
digitalWrite(PIN_TFT_RST, HIGH);
// Re-initialize the display
// Re-initialize the same native landscape geometry after wake.
display.init();
display.landscapeScreen();
display.displayOn();
#ifdef DISPLAY_FLIP_VERTICALLY
display.flipScreenVertically();
@@ -104,10 +83,11 @@ void ST7789Display::clear() {
void ST7789Display::startFrame(ColorVal bkg) {
display.clear(); // TODO: use bkg
setColor(UIColor::primary_txt);
display.setFont(ArialMT_Plain_16);
setTextSize(1);
}
void ST7789Display::setTextSize(int sz) {
_line_height = sz == 2 ? 26 : 18;
switch(sz) {
case 1 :
display.setFont(ArialMT_Plain_16);
@@ -127,8 +107,8 @@ void ST7789Display::setColor(ColorVal c) {
}
void ST7789Display::setCursor(int x, int y) {
_x = x*SCALE_X + X_OFFSET;
_y = y*SCALE_Y + Y_OFFSET;
_x = x;
_y = y;
}
void ST7789Display::print(const char* str) {
@@ -136,59 +116,32 @@ void ST7789Display::print(const char* str) {
}
void ST7789Display::printWordWrap(const char* str, int max_width) {
display.drawStringMaxWidth(_x, _y, max_width*SCALE_X, str);
display.drawStringMaxWidth(_x, _y, max_width, str);
}
void ST7789Display::fillRect(int x, int y, int w, int h) {
display.fillRect(x*SCALE_X + X_OFFSET, y*SCALE_Y + Y_OFFSET, w*SCALE_X, h*SCALE_Y);
display.fillRect(x, y, w, h);
}
void ST7789Display::drawRect(int x, int y, int w, int h) {
display.drawRect(x*SCALE_X + X_OFFSET, y*SCALE_Y + Y_OFFSET, w*SCALE_X, h*SCALE_Y);
display.drawRect(x, y, w, h);
}
void ST7789Display::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
// Calculate the base position in display coordinates
uint16_t startX = x * SCALE_X + X_OFFSET;
uint16_t startY = y * SCALE_Y + Y_OFFSET;
// Width in bytes for bitmap processing
uint16_t widthInBytes = (w + 7) / 8;
// Process the bitmap row by row
for (uint16_t by = 0; by < h; by++) {
// Calculate the target y-coordinates for this logical row
int y1 = startY + (int)(by * SCALE_Y);
int y2 = startY + (int)((by + 1) * SCALE_Y);
int block_h = y2 - y1;
// Scan across the row bit by bit
for (uint16_t bx = 0; bx < w; bx++) {
// Calculate the target x-coordinates for this logical column
int x1 = startX + (int)(bx * SCALE_X);
int x2 = startX + (int)((bx + 1) * SCALE_X);
int block_w = x2 - x1;
// Get the current bit
uint16_t byteOffset = (by * widthInBytes) + (bx / 8);
uint8_t bitMask = 0x80 >> (bx & 7);
bool bitSet = pgm_read_byte(bits + byteOffset) & bitMask;
// If the bit is set, draw a block of pixels
if (bitSet) {
// Draw the block as a filled rectangle
display.fillRect(x1, y1, block_w, block_h);
}
}
}
if (!bits || w <= 0 || h <= 0) return;
const int stride = (w + 7) / 8;
for (int row = 0; row < h; ++row)
for (int col = 0; col < w; ++col)
if (pgm_read_byte(bits + row * stride + col / 8) & (0x80 >> (col & 7)))
display.fillRect(x + col, y + row, 1, 1);
}
uint16_t ST7789Display::getTextWidth(const char* str) {
return display.getStringWidth(str) / SCALE_X;
return display.getStringWidth(str);
}
void ST7789Display::endFrame() {
display.display();
}
#endif
#endif
+5 -3
View File
@@ -11,15 +11,16 @@ class ST7789Display : public DisplayDriver {
bool _isOn;
uint16_t _color;
int _x=0, _y=0;
int _line_height = 18;
bool i2c_probe(TwoWire& wire, uint8_t addr);
public:
#if defined(HELTEC_VISION_MASTER_T190)
ST7789Display() : DisplayDriver(128, 64), display(&SPI, PIN_TFT_RST, PIN_TFT_DC, PIN_TFT_CS, GEOMETRY_RAWMODE, 320, 170,PIN_TFT_SDA,-1,PIN_TFT_SCL) {_isOn = false;}
ST7789Display() : DisplayDriver(320, 170), display(&SPI, PIN_TFT_RST, PIN_TFT_DC, PIN_TFT_CS, GEOMETRY_RAWMODE, 320, 170,PIN_TFT_SDA,-1,PIN_TFT_SCL) {_isOn = false;}
#elif defined(THINKNODE_M9)
ST7789Display() : DisplayDriver(128, 64), display(&SPI, ST7789_RESET, ST7789_RS, ST7789_CS, GEOMETRY_RAWMODE, 320, 240, ST7789_SDA, ST7789_MISO, ST7789_SCK) {_isOn = false;}
ST7789Display() : DisplayDriver(320, 240), display(&SPI, ST7789_RESET, ST7789_RS, ST7789_CS, GEOMETRY_RAWMODE, 320, 240, ST7789_SDA, ST7789_MISO, ST7789_SCK) {_isOn = false;}
#else
ST7789Display() : DisplayDriver(128, 64), display(&SPI1, PIN_TFT_RST, PIN_TFT_DC, PIN_TFT_CS, GEOMETRY_RAWMODE, 240, 135) {_isOn = false;}
ST7789Display() : DisplayDriver(240, 135), display(&SPI1, PIN_TFT_RST, PIN_TFT_DC, PIN_TFT_CS, GEOMETRY_RAWMODE, 240, 135) {_isOn = false;}
#endif
bool begin();
@@ -29,6 +30,7 @@ public:
void clear() override;
void startFrame(ColorVal bkg = UIColor::window_bkg) override;
void setTextSize(int sz) override;
int textLineHeight() override { return _line_height; }
void setColor(ColorVal c) override;
void setCursor(int x, int y) override;
void print(const char* str) override;
+9 -164
View File
@@ -1,10 +1,6 @@
#include "ST7789LCDDisplay.h"
#include "ColorTheme.h"
#ifdef ST7789_PORTRAIT_PROFILE
#include "DisplayViewport.h"
#endif
#ifndef PIN_TFT_MISO
#define PIN_TFT_MISO -1
#endif
@@ -13,27 +9,9 @@
#define DISPLAY_ROTATION 3
#endif
#ifndef DISPLAY_SCALE_X
#define DISPLAY_SCALE_X 2.5f // 320 / 128
#endif
#ifndef DISPLAY_SCALE_Y
#define DISPLAY_SCALE_Y 3.75f // 240 / 64
#endif
#define DISPLAY_WIDTH 240
#define DISPLAY_HEIGHT 320
#ifdef ST7789_PORTRAIT_PROFILE
#ifndef ST7789_PORTRAIT_TEXT_SCALE
#define ST7789_PORTRAIT_TEXT_SCALE 2
#endif
static DisplayViewport::Geometry portraitViewport(int16_t physical_width, int16_t physical_height) {
return {128, 64, physical_width, physical_height};
}
#endif
// The compiled orientation, optionally turned 180 degrees by `display.flip`.
// Adding 2 keeps portrait portrait and landscape landscape, so the viewport
// geometry below never has to change with it.
@@ -103,18 +81,14 @@ bool ST7789LCDDisplay::begin() {
display.init(DISPLAY_WIDTH, DISPLAY_HEIGHT);
display.setRotation(effectiveRotation());
setDimensions(display.width(), display.height());
display.setSPISpeed(40e6);
display.fillScreen(ST77XX_BLACK);
display.setTextColor(ST77XX_WHITE);
#ifdef ST7789_PORTRAIT_PROFILE
_logical_text_size = 1;
display.setTextSize(ST7789_PORTRAIT_TEXT_SCALE);
display.setTextSize(1);
display.setTextWrap(false);
#else
display.setTextSize(2 * DISPLAY_SCALE_X);
#endif
display.cp437(true); // Use full 256 char 'Code Page 437' font
#ifdef HELTEC_V4_R8_TFT
@@ -169,22 +143,12 @@ void ST7789LCDDisplay::clear() {
void ST7789LCDDisplay::startFrame(ColorVal bkg) {
display.fillScreen(bkg);
display.setTextColor(_color = UIColor::primary_txt);
#ifdef ST7789_PORTRAIT_PROFILE
_logical_text_size = 1;
display.setTextSize(ST7789_PORTRAIT_TEXT_SCALE);
#else
display.setTextSize(1 * DISPLAY_SCALE_X); // This one affects size of Please wait... message
#endif
display.setTextSize(1);
display.cp437(true); // Use full 256 char 'Code Page 437' font
}
void ST7789LCDDisplay::setTextSize(int sz) {
#ifdef ST7789_PORTRAIT_PROFILE
_logical_text_size = sz > 0 ? static_cast<uint8_t>(sz) : 1;
display.setTextSize(_logical_text_size * ST7789_PORTRAIT_TEXT_SCALE);
#else
display.setTextSize(sz * DISPLAY_SCALE_X);
#endif
display.setTextSize(sz > 0 ? sz : 1);
}
void ST7789LCDDisplay::setColor(ColorVal c) {
@@ -192,152 +156,33 @@ void ST7789LCDDisplay::setColor(ColorVal c) {
}
void ST7789LCDDisplay::setCursor(int x, int y) {
#ifdef ST7789_PORTRAIT_PROFILE
DisplayViewport::Geometry viewport = portraitViewport(display.width(), display.height());
display.setCursor(viewport.mapX(x), viewport.mapY(y));
#else
display.setCursor(x * DISPLAY_SCALE_X, y * DISPLAY_SCALE_Y);
#endif
display.setCursor(x, y);
}
void ST7789LCDDisplay::print(const char* str) {
#ifdef ST7789_PORTRAIT_PROFILE
int16_t cursor_x = display.getCursorX();
if (cursor_x < 0) {
cursor_x = 0;
display.setCursor(cursor_x, display.getCursorY());
}
if (cursor_x >= display.width()) return;
printFitted(str, static_cast<uint16_t>(display.width() - cursor_x));
#else
display.print(str);
#endif
}
void ST7789LCDDisplay::fillRect(int x, int y, int w, int h) {
#ifdef ST7789_PORTRAIT_PROFILE
DisplayViewport::Geometry viewport = portraitViewport(display.width(), display.height());
display.fillRect(viewport.mapX(x), viewport.mapY(y), viewport.spanX(x, w), viewport.spanY(y, h), _color);
#else
display.fillRect(x * DISPLAY_SCALE_X, y * DISPLAY_SCALE_Y, w * DISPLAY_SCALE_X, h * DISPLAY_SCALE_Y, _color);
#endif
display.fillRect(x, y, w, h, _color);
}
void ST7789LCDDisplay::drawRect(int x, int y, int w, int h) {
#ifdef ST7789_PORTRAIT_PROFILE
DisplayViewport::Geometry viewport = portraitViewport(display.width(), display.height());
display.drawRect(viewport.mapX(x), viewport.mapY(y), viewport.spanX(x, w), viewport.spanY(y, h), _color);
#else
display.drawRect(x * DISPLAY_SCALE_X, y * DISPLAY_SCALE_Y, w * DISPLAY_SCALE_X, h * DISPLAY_SCALE_Y, _color);
#endif
display.drawRect(x, y, w, h, _color);
}
void ST7789LCDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) {
uint8_t byteWidth = (w + 7) / 8;
#ifdef ST7789_PORTRAIT_PROFILE
DisplayViewport::Geometry viewport = portraitViewport(display.width(), display.height());
int16_t physical_y = viewport.mapY(y);
for (int j = 0; j < h; j++) {
// Scale both bitmap axes from the logical X ratio so logo pixels stay square.
int16_t y0 = physical_y + viewport.mapX(j);
int16_t y1 = physical_y + viewport.mapX(j + 1);
for (int i = 0; i < w; i++) {
uint8_t byte = bits[j * byteWidth + i / 8];
bool pixelOn = byte & (0x80 >> (i & 7));
if (pixelOn) {
int16_t x0 = viewport.mapX(x + i);
int16_t x1 = viewport.mapX(x + i + 1);
display.fillRect(x0, y0, x1 - x0, y1 - y0, _color);
}
}
}
#else
for (int j = 0; j < h; j++) {
for (int i = 0; i < w; i++) {
uint8_t byte = bits[j * byteWidth + i / 8];
bool pixelOn = byte & (0x80 >> (i & 7));
if (pixelOn) {
for (int dy = 0; dy < DISPLAY_SCALE_X; dy++) {
for (int dx = 0; dx < DISPLAY_SCALE_X; dx++) {
display.drawPixel(x * DISPLAY_SCALE_X + i * DISPLAY_SCALE_X + dx, y * DISPLAY_SCALE_Y + j * DISPLAY_SCALE_X + dy, _color);
}
}
}
}
}
#endif
// The common UI bitmaps are MSB-first, as expected by drawBitmap.
display.drawBitmap(x, y, bits, w, h, _color);
}
uint16_t ST7789LCDDisplay::getTextWidth(const char* str) {
#ifdef ST7789_PORTRAIT_PROFILE
uint8_t physical_scale = selectTextScale(str, display.width());
uint16_t physical_width = measureTextWidth(str, physical_scale);
if (physical_width > display.width()) physical_width = display.width();
DisplayViewport::Geometry viewport = portraitViewport(display.width(), display.height());
return viewport.logicalWidthForPhysical(physical_width);
#else
int16_t x1, y1;
uint16_t w, h;
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return w / DISPLAY_SCALE_X;
#endif
}
#ifdef ST7789_PORTRAIT_PROFILE
uint16_t ST7789LCDDisplay::measureTextWidth(const char* str, uint8_t physical_scale) {
int16_t x1, y1;
uint16_t w, h;
display.setTextSize(physical_scale);
display.getTextBounds(str, 0, 0, &x1, &y1, &w, &h);
return w;
}
uint8_t ST7789LCDDisplay::selectTextScale(const char* str, uint16_t available_width) {
uint16_t width_at_scale_one = measureTextWidth(str, 1);
uint8_t preferred_scale = _logical_text_size * ST7789_PORTRAIT_TEXT_SCALE;
return DisplayViewport::selectTextScale(width_at_scale_one, preferred_scale, _logical_text_size,
available_width);
}
void ST7789LCDDisplay::printFitted(const char* str, uint16_t available_width) {
if (!str || available_width == 0) return;
uint8_t physical_scale = selectTextScale(str, available_width);
if (measureTextWidth(str, physical_scale) <= available_width) {
display.print(str);
return;
}
static const char* ellipsis = "...";
uint16_t ellipsis_width = measureTextWidth(ellipsis, physical_scale);
if (ellipsis_width > available_width) return;
char fitted[256];
size_t len = strlen(str);
if (len > sizeof(fitted) - 4) len = sizeof(fitted) - 4;
memcpy(fitted, str, len);
fitted[len] = 0;
while (len > 0 && measureTextWidth(fitted, physical_scale) + ellipsis_width > available_width) {
--len;
while (len > 0 && (static_cast<uint8_t>(fitted[len]) & 0xC0) == 0x80)
--len;
fitted[len] = 0;
}
memcpy(fitted + len, ellipsis, 4);
display.setTextSize(physical_scale);
display.print(fitted);
}
#endif
void ST7789LCDDisplay::endFrame() {
// display.display();
}
+9 -10
View File
@@ -7,6 +7,10 @@
#include <Adafruit_ST7789.h>
#include <helpers/RefCountedDigitalPin.h>
#ifndef DISPLAY_ROTATION
#define DISPLAY_ROTATION 3
#endif
class ST7789LCDDisplay : public DisplayDriver {
#if defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT)
SPIClass displaySPI;
@@ -18,26 +22,21 @@ class ST7789LCDDisplay : public DisplayDriver {
uint16_t _color;
RefCountedDigitalPin* _peripher_power;
#ifdef ST7789_PORTRAIT_PROFILE
uint8_t _logical_text_size = 1;
uint16_t measureTextWidth(const char* str, uint8_t physical_scale);
uint8_t selectTextScale(const char* str, uint16_t available_width);
void printFitted(const char* str, uint16_t available_width);
#endif
static constexpr int kWidth = (DISPLAY_ROTATION & 1) ? 320 : 240;
static constexpr int kHeight = (DISPLAY_ROTATION & 1) ? 240 : 320;
uint8_t effectiveRotation() const;
bool i2c_probe(TwoWire& wire, uint8_t addr);
public:
#ifdef USE_PIN_TFT
ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(kWidth, kHeight),
display(PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_SDA, PIN_TFT_SCL, PIN_TFT_RST),
_peripher_power(peripher_power)
{
_isOn = false;
}
#elif defined(LILYGO_TDECK) || defined(HELTEC_LORA_V4_TFT) || defined(HELTEC_V4_R8_TFT)
ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(kWidth, kHeight),
displaySPI(HSPI),
display(&displaySPI, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST),
_peripher_power(peripher_power)
@@ -45,7 +44,7 @@ public:
_isOn = false;
}
#else
ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(128, 64),
ST7789LCDDisplay(RefCountedDigitalPin* peripher_power=NULL) : DisplayDriver(kWidth, kHeight),
display(&SPI, PIN_TFT_CS, PIN_TFT_DC, PIN_TFT_RST),
_peripher_power(peripher_power)
{
+4 -4
View File
@@ -188,7 +188,7 @@ class ST7789Spi : public OLEDDisplay {
for (y = minBoundY; y <= maxBoundY; y++)
{
for(int temp = 0; temp<8;temp++)
for(int temp = 0; temp<8 && y*8+temp<displayHeight;temp++)
{
//setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1);
setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1);
@@ -213,14 +213,14 @@ class ST7789Spi : public OLEDDisplay {
#else
set_CS(LOW);
_spi->beginTransaction(_spiSettings);
uint8_t x, y;
uint16_t x, y;
for (y = 0; y < _buffheight; y++)
{
for(int temp = 0; temp<8;temp++)
for(int temp = 0; temp<8 && y*8+temp<displayHeight;temp++)
{
//setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1);
//setAddrWindow(minBoundX,y*8+temp,maxBoundX-minBoundX+1,1);
setAddrWindow(y*8+temp,0,1,displayWidth);
setAddrWindow(0,y*8+temp,displayWidth,1);
uint32_t const pixbufcount = displayWidth;
uint16_t *pixbuf = (uint16_t *)rtos_malloc(2 * pixbufcount);
for (x = 0; x < displayWidth; x++)
+9 -4
View File
@@ -88,16 +88,21 @@ public:
inline void drawSmallMessageBody(DisplayDriver& display, const char* origin,
const char* message, int origin_y = 14,
int bottom = -1) {
SmallMessageText text(display);
const int message_y = origin_y + text.lineHeight();
SmallMessageText compact(display);
const bool small = display.useSmallMessageFont();
DisplayDriver& text = small ? static_cast<DisplayDriver&>(compact) : display;
const int line_height = small ? compact.lineHeight() : display.textLineHeight();
const int message_y = origin_y + line_height;
if (bottom < 0 || bottom > display.height()) bottom = display.height();
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);
drawTextWrapped(text, 0, message_y, display.width(), text.lineHeight(),
text.lineCount(message_y, bottom), translated);
drawTextWrapped(text, 0, message_y, display.width(), line_height,
small ? compact.lineCount(message_y, bottom)
: (bottom - message_y) / line_height, translated);
}
} // namespace ui