Merge origin/keymindCascade with companion button hints
This commit is contained in:
commit
d89d28f7ed
78 files changed
+6858
-601
No files matched your search
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
@@ -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,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. */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"; }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user