Compare commits

..
Author SHA1 Message Date
Thomas Göttgens fcd8490d15 Merge origin/eink-refactor; supersede bot formatting commit (ignored repo clang-format config) 2026-06-07 10:58:45 +02:00
Thomas Göttgens 316079d240 Merge origin/develop into eink-refactor; adopt unified MessageStore in InkHUD 2026-06-07 10:46:15 +02:00
AustinandGitHub 14e998e6c3 ESP32: Update pioarduino to v3.3.9 (#10637)
Arduino Release v3.3.9 based on ESP-IDF v5.5.4

May help with recent compile troubles?
2026-06-06 17:03:41 -04:00
AustinandGitHub 57f678240d Add 1.25 Meter '125cm' amateur radio region support (#10638) 2026-06-06 07:43:49 -05:00
AustinandGitHub 99df0a6fe9 Update protobufs (#10636) 2026-06-05 14:46:29 -05:00
ce7444c1a1 feat: add WIZnet W5500-EVB-Pico2 + E22P-868M30S variant (#10552)
* feat: add WIZnet W5500-EVB-Pico2 + E22P-868M30S variant

Adds a community variant for the WIZnet W5500-EVB-Pico2 development
board (https://docs.wiznet.io/Product/iEthernet/W5500/w5500-evb-pico2)
paired with an external EBYTE E22P-868M30S LoRa module.

This is the hardware twin of variants/rp2350/diy/pico2_w5500_e22 — same
LoRa pinout, same W5500 pin mapping — and reuses its variant.h verbatim
via `-I variants/rp2350/diy/pico2_w5500_e22`. No duplicated pinout file.

Two differences vs pico2_w5500_e22 justify the separate env:

1. Board target: `wiznet_5500_evb_pico2` (2 MB flash) instead of
   `rpipico2` (4 MB flash). The WIZnet PCB ships with a smaller Q-SPI
   chip than a stock Pi Pico 2. Selecting the correct board makes the
   linker fail fast on overflow instead of producing a UF2 that gets
   silently truncated when flashed to the device.
2. W5500 PHY is integrated on the PCB (hard-wired SPI0 GP16-20) rather
   than supplied as an external module the user wires manually.

The E22P-868M30S uses a single combined RFEN pin (LNA + PA enable)
instead of the older E22-900M30S's split RXEN/TXEN pins. RFEN is held
HIGH at all times via `SX126X_ANT_SW 3`; TX switching still uses the
on-module DIO2 → TXEN bridge through `SX126X_DIO2_AS_RF_SWITCH`.

Build verified: wiznet_5500_evb_pico2_e22p SUCCESS
(RAM 19.2%, Flash 65.6% of 1.5 MB partition / 2 MB chip).

* style(wiznet-evb-pico2-e22p): prettier-format README tables

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-06-05 09:08:43 -05:00
LittleatonandGitHub e3ae0a6ab5 missing module config (#10496)
13302 and 13300 had missing module config for slot 2, also the rf switch and voltage info was missing.

Without that the auto setting in the config.yaml will try to use the default  lora-hat-rak-6421-pi-hat.yaml and things do not work correctly.
2026-06-05 08:34:28 -05:00
pdxlocationsandGitHub 733430ed45 feat: Add Module configuration for RAK13300 and RAK13302 in Slot 2 (#10632) 2026-06-05 06:10:25 -05:00
cd2466692f fix: FEM Sleep/Wake Fix - Enable PA after sleep (#10617)
* fix: release Heltec v4 FEM sleep holds

* fix: increase FEM power settle delay

* Fix heltec_V4 documentation

Fixing the heltec_v4 documentation for enabling the PA after sleep.

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Add comment for the next guy about why 5ms was chosen.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-06-04 20:40:05 -05:00
AustinandGitHub 4b424f0234 Add support for T-Beam Supreme 144mhz variant (#10624)
Adds support for T-Beam-Supreme 144mhz version
https://github.com/Xinyuan-LilyGO/LilyGo-LoRa-Series/blob/master/docs/en/t_beam_supreme/t_beam_supreme_144_hw.md

Tested / working.
2026-06-04 16:03:39 -05:00
a212d2e84f Save Frame Visibility Actions (#10576)
Co-authored-by: HarukiToreda <116696711+HarukiToreda@users.noreply.github.com>
2026-06-04 10:05:51 -05:00
HarukiToredaandGitHub 5154e81d0b Unify InkHUD message storage with BaseUI's MessageStore (#10596)
* Unified Messagestore.

* DM fix

* Copilot fixes
2026-06-04 10:05:31 -05:00
copilot-swe-agent[bot]andGitHub 4a228a38c8 Run trunk-style formatting on touched e-ink files 2026-06-03 21:33:52 +00:00
Thomas Göttgens 490176b34b Merge origin/develop into eink-refactor; resolve rak4631_eth_gw flags conflict 2026-06-03 23:23:11 +02:00
Thomas Göttgens 62ce9ee1b1 Port t-deck-pro-v1_1 e-ink to NicheGraphics (GDEQ031T10), drop GxEPD2 2026-06-03 18:12:34 +02:00
Thomas Göttgens 194b5aef13 Merge origin/develop into eink-refactor; reconcile InkHUD/eink conflicts 2026-06-03 16:13:15 +02:00
Thomas Göttgens ac08ecad5e trunk 2026-04-19 15:24:44 +02:00
Thomas Göttgens 7c87b620a3 make trunk 'appy 2026-04-18 21:54:04 +02:00
Thomas Göttgens cbf9ff26e1 who in their right mind would name something in main.h 'drv'?
Shading locals...
2026-04-18 21:48:54 +02:00
Thomas Göttgens 817403f8be Merge branch 'eink-refactor' of https://github.com/meshtastic/firmware into eink-refactor 2026-04-18 20:53:03 +02:00
Thomas Göttgens 110933d73f This should explode in multiple ways. Let's see. 2026-04-18 20:40:14 +02:00
Thomas Göttgens 69794d495f This should explode in multiple ways. Let's see. 2026-04-18 20:14:16 +02:00
259 changed files with 3947 additions and 4768 deletions
+3 -3
View File
@@ -376,14 +376,14 @@ Multiple display driver families in `src/graphics/`:
- **OLED**: SSD1306, SH1106, ST7567
- **TFT**: TFTDisplay (LovyanGFX-based)
- **E-Ink**: EInkDisplay2, EInkDynamicDisplay, EInkParallelDisplay
- **E-Ink**: `src/graphics/BaseUIEInkDisplay.*` is the OLEDDisplay-compatible adapter (peer of `TFTDisplay`). The hardware layer it drives lives in `src/graphics/eink/` — chipset drivers in `Drivers/`, panel profiles in `Panels/`, optional `Backlight/`. Shared by both BaseUI-on-eink and InkHUD builds.
**InkHUD** (`src/graphics/niche/InkHUD/`) is an event-driven e-ink UI framework:
**InkHUD** (`src/graphics/niche/`) is an event-driven e-ink UI framework that sits on top of the `graphics/eink/` layer:
- Applet-based architecture — modular display tiles
- Read-only, static display optimized for minimal refreshes and low power
- Configured per-variant via `nicheGraphics.h`
- Separate PlatformIO config: `src/graphics/niche/InkHUD/PlatformioConfig.ini`
- Build helpers in top-level `platformio.ini``[niche]` pulls `graphics/eink/` only (BaseUI path), `[inkhud]` extends it with `graphics/niche/`
### Input System
+1 -1
View File
@@ -132,7 +132,7 @@ For e-ink display variants using the InkHUD framework, add `nicheGraphics.h`:
// Configure display, applets, and refresh behavior per device
```
InkHUD has its own PlatformIO config: `src/graphics/niche/InkHUD/PlatformioConfig.ini`
InkHUD and the shared E-Ink layer are wired up via the top-level `platformio.ini` `[niche]` (BaseUI + driver/panel layer in `src/graphics/eink/`) and `[inkhud]` (adds the InkHUD UI in `src/graphics/niche/`). Variants opt in with `extends = ..., niche` or `extends = ..., inkhud`.
## I2C Device Detection
+5 -1
View File
@@ -5,7 +5,9 @@ Meta:
- raspberry-pi
Lora:
### RAK13300 in Slot 2 pins
### RAK13300 in Slot 2
Module: sx1262
IRQ: 18 #IO6
Reset: 24 # IO4
Busy: 19 # IO5
@@ -13,5 +15,7 @@ Lora:
Enable_Pins:
- 26
- 23
DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true
spidev: spidev0.1
# CS: 7
+6 -2
View File
@@ -5,14 +5,18 @@ Meta:
- raspberry-pi
Lora:
### RAK13302 in Slot 2 pins
### RAK13302 in Slot 2
Module: sx1262
IRQ: 18 #IO6
Reset: 24 # IO4
Busy: 19 # IO5
# Ant_sw: 23 # IO3
# Ant_sw: 23 # IO3
Enable_Pins:
- 26
- 23
DIO3_TCXO_VOLTAGE: true
DIO2_AS_RF_SWITCH: true
spidev: spidev0.1
# CS: 7
TX_GAIN_LORA: [9, 9, 10, 11, 9, 8, 9, 10, 10, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 10, 9, 8]
-32
View File
@@ -1,32 +0,0 @@
{
"build": {
"core": "esp32",
"extra_flags": ["-DBOARD_HAS_PSRAM"],
"f_cpu": "360000000L",
"f_flash": "80000000L",
"f_psram": "200000000L",
"flash_mode": "qio",
"hwids": [["0x1A86", "0x7522"]],
"mcu": "esp32p4",
"chip_variant": "esp32p4_es",
"variant": "esp32p4"
},
"arduino": {
"partitions": "default_16MB.csv"
},
"connectivity": ["bluetooth", "openthread"],
"debug": {
"openocd_target": "esp32p4.cfg"
},
"frameworks": ["arduino", "espidf"],
"name": "CrowPanel Advanced ESP32-P4 HMI AI Display",
"upload": {
"flash_size": "16MB",
"maximum_ram_size": 512000,
"maximum_size": 16777216,
"require_upload_port": true,
"speed": 1500000
},
"url": "https://www.elecrow.com/crowpanel-advanced-5inch-esp32-p4-hmi-ai-display-800x480-ips-touch-screen-with-wifi-6.html",
"vendor": "Elecrow"
}
+22 -2
View File
@@ -8,10 +8,30 @@ extra_configs =
variants/*/*.ini
variants/*/*/platformio.ini
variants/*/diy/*/platformio.ini
src/graphics/niche/InkHUD/PlatformioConfig.ini
description = Meshtastic
; E-Ink / NicheGraphics build helpers.
[niche]
build_src_filter =
+<graphics/eink/>
build_flags =
-D MESHTASTIC_INCLUDE_NICHE_GRAPHICS
[inkhud]
build_src_filter =
${niche.build_src_filter}
+<graphics/niche/>
build_flags =
${niche.build_flags}
-D MESHTASTIC_INCLUDE_INKHUD ; Use InkHUD as the UI
-D MESHTASTIC_EXCLUDE_SCREEN ; Suppress default Screen class
-D MESHTASTIC_EXCLUDE_INPUTBROKER ; Suppress default input handling
-D HAS_BUTTON=0 ; Suppress default ButtonThread
lib_deps =
# renovate: datasource=github-tags depName=GFX_Root packageName=ZinggJM/GFX_Root
https://github.com/ZinggJM/GFX_Root/archive/3195764e352a0d2567c8d277ac408ca7293a99b0.zip ; Used by InkHUD as a "slimmer" version of AdafruitGFX
[env]
test_build_src = true
extra_scripts =
@@ -103,7 +123,7 @@ build_unflags =
-std=gnu++11
build_flags = ${env.build_flags} -Os
-std=gnu++17
build_src_filter = ${env.build_src_filter} -<platform/portduino/> -<graphics/niche/>
build_src_filter = ${env.build_src_filter} -<platform/portduino/> -<graphics/niche/> -<graphics/eink/>
; Common libs for communicating over TCP/IP networks such as MQTT
[networking_base]
+1 -25
View File
@@ -14,28 +14,4 @@ const uint8_t FROMNUM_UUID_16[16u] = {0x53, 0x44, 0xe3, 0x47, 0x75, 0xaa, 0x70,
const uint8_t LEGACY_LOGRADIO_UUID_16[16u] = {0xe2, 0xf2, 0x1e, 0xbe, 0xc5, 0x15, 0xcf, 0xaa,
0x6b, 0x43, 0xfa, 0x78, 0x38, 0xd2, 0x6f, 0x6c};
const uint8_t LOGRADIO_UUID_16[16u] = {0x47, 0x95, 0xDF, 0x8C, 0xDE, 0xE9, 0x44, 0x99,
0x23, 0x44, 0xE6, 0x06, 0x49, 0x6E, 0x3D, 0x5A};
void BluetoothApi::setup() {}
void BluetoothApi::shutdown() {}
void BluetoothApi::deinit() {}
void BluetoothApi::clearBonds() {}
bool BluetoothApi::isActive()
{
return false;
}
bool BluetoothApi::isConnected()
{
return false;
}
void BluetoothApi::sendLog(const uint8_t *logMessage, size_t length)
{
(void)logMessage;
(void)length;
}
void updateBatteryLevel(uint8_t level) __attribute__((weak));
void updateBatteryLevel(uint8_t level)
{
(void)level;
}
0x23, 0x44, 0xE6, 0x06, 0x49, 0x6E, 0x3D, 0x5A};
-5
View File
@@ -24,14 +24,9 @@ void updateBatteryLevel(uint8_t level);
class BluetoothApi
{
public:
virtual ~BluetoothApi() = default;
virtual void setup();
virtual void shutdown();
virtual void deinit();
virtual void clearBonds();
virtual bool isActive();
virtual bool isConnected();
virtual int getRssi() = 0;
virtual void sendLog(const uint8_t *logMessage, size_t length);
};
+1 -1
View File
@@ -1,5 +1,5 @@
#include "configuration.h"
#if HAS_SCREEN
#if HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
#include "FSCommon.h"
#include "MessageStore.h"
#include "NodeDB.h"
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once
#if HAS_SCREEN
#if HAS_SCREEN || defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
// Disable debug logging entirely on release builds of HELTEC_MESH_SOLAR for space constraints
#if defined(HELTEC_MESH_SOLAR)
+2 -2
View File
@@ -222,7 +222,7 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
if (config.security.debug_log_api_enabled && !pauseBluetoothLogging) {
bool isBleConnected = false;
#ifdef ARCH_ESP32
isBleConnected = bluetoothApi && bluetoothApi->isActive() && bluetoothApi->isConnected();
isBleConnected = nimbleBluetooth && nimbleBluetooth->isActive() && nimbleBluetooth->isConnected();
#elif defined(ARCH_NRF52)
isBleConnected = nrf52Bluetooth != nullptr && nrf52Bluetooth->isConnected();
#elif defined(ARCH_NRF54L15)
@@ -240,7 +240,7 @@ void RedirectablePrint::log_to_ble(const char *logLevel, const char *format, va_
auto buffer = std::unique_ptr<uint8_t[]>(new uint8_t[meshtastic_LogRecord_size]);
size_t size = pb_encode_to_bytes(buffer.get(), meshtastic_LogRecord_size, meshtastic_LogRecord_fields, &logRecord);
#ifdef ARCH_ESP32
bluetoothApi->sendLog(buffer.get(), size);
nimbleBluetooth->sendLog(buffer.get(), size);
#elif defined(ARCH_NRF52)
nrf52Bluetooth->sendLog(buffer.get(), size);
#elif defined(ARCH_NRF54L15)
-762
View File
@@ -1,762 +0,0 @@
#include "configuration.h"
#if defined(CONFIG_IDF_TARGET_ESP32P4) && defined(CONFIG_ESP_HOSTED_ENABLED) && !MESHTASTIC_EXCLUDE_BLUETOOTH
#include "BluetoothStatus.h"
#include "PowerFSM.h"
#include "bluetooth/HostedBluetooth.h"
#include "concurrency/OSThread.h"
#include "esp32-hal-hosted.h"
#include "esp_err.h"
#include "esp_event.h"
#include "esp_hosted.h"
#include "esp_hosted_event.h"
#include "main.h"
#include "mesh/PhoneAPI.h"
#include "mesh/mesh-pb-constants.h"
#include <BLEAdvertising.h>
#include <BLEDevice.h>
#include <BLESecurity.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <array>
#include <atomic>
#include <climits>
#include <cstring>
#include <driver/gpio.h>
#include <mutex>
#include "host/ble_gap.h"
#include "host/ble_store.h"
namespace
{
/*
* Maintainer note: HostedBluetooth intentionally stays close to NimbleBluetooth
* but is not a strict drop-in equivalent.
*
* Intentional differences from NimbleBluetooth include:
* - ESP-Hosted transport lifecycle handling (event callbacks + CP reset GPIO control).
* - Data-length update behavior (Hosted currently requests ble_gap_set_data_len on connect).
* - No Battery Service exposure here.
*
* If you modify common BLE flow in one class, review and likely mirror in both:
* - PhoneAPI queueing/synchronization between BLE callbacks and runOnce().
* - Security/pairing config and passkey UX/status updates.
* - Mesh GATT characteristics, permissions, and advertising setup.
* - Connection parameter tuning and reconnect/disconnect cleanup paths.
*/
constexpr uint16_t kPreferredBleMtu = 517;
constexpr uint16_t kPreferredBleTxOctets = 251;
constexpr uint16_t kPreferredBleTxTimeUs = (kPreferredBleTxOctets + 14) * 8;
constexpr size_t kBluetoothToPhoneQueueSize = 3;
constexpr size_t kBluetoothFromPhoneQueueSize = 3;
BLECharacteristic *fromNumCharacteristic = nullptr;
BLECharacteristic *logRadioCharacteristic = nullptr;
BLEServer *bleServer = nullptr;
static bool passkeyShowing = false;
std::atomic<uint16_t> hostedConnHandle{BLE_HS_CONN_HANDLE_NONE};
void clearPairingDisplay()
{
if (!passkeyShowing) {
return;
}
passkeyShowing = false;
#if HAS_SCREEN
if (screen) {
screen->endAlert();
}
#endif
}
class HostedBluetoothPhoneAPI : public PhoneAPI, public concurrency::OSThread
{
public:
HostedBluetoothPhoneAPI() : concurrency::OSThread("HostedBluetooth") { api_type = TYPE_BLE; }
std::mutex fromPhoneMutex;
std::atomic<size_t> fromPhoneQueueSize{0};
std::array<BLEValue, kBluetoothFromPhoneQueueSize> fromPhoneQueue{};
std::mutex toPhoneMutex;
std::atomic<size_t> toPhoneQueueSize{0};
std::array<std::array<uint8_t, meshtastic_FromRadio_size>, kBluetoothToPhoneQueueSize> toPhoneQueue{};
std::array<size_t, kBluetoothToPhoneQueueSize> toPhoneQueueByteSizes{};
std::atomic<bool> onReadCallbackIsWaitingForData{false};
protected:
int32_t runOnce() override
{
while (fromPhoneQueueSize > 0 || onReadCallbackIsWaitingForData) {
runOnceHandleFromPhoneQueue();
runOnceHandleToPhoneQueue();
}
return INT32_MAX;
}
void onNowHasData(uint32_t fromRadioNum) override
{
PhoneAPI::onNowHasData(fromRadioNum);
if (!fromNumCharacteristic || !bleServer || bleServer->getConnectedCount() == 0) {
return;
}
uint8_t val[4];
put_le32(val, fromRadioNum);
fromNumCharacteristic->setValue(val, sizeof(val));
fromNumCharacteristic->notify();
}
bool checkIsConnected() override { return bleServer && bleServer->getConnectedCount() > 0; }
private:
void runOnceHandleToPhoneQueue()
{
if (!onReadCallbackIsWaitingForData) {
return;
}
uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0};
size_t numBytes = getFromRadio(fromRadioBytes);
if (numBytes > 0 && toPhoneQueueSize < kBluetoothToPhoneQueueSize) {
std::lock_guard<std::mutex> guard(toPhoneMutex);
const size_t storeAtIndex = toPhoneQueueSize.load();
memcpy(toPhoneQueue[storeAtIndex].data(), fromRadioBytes, numBytes);
toPhoneQueueByteSizes[storeAtIndex] = numBytes;
toPhoneQueueSize++;
}
onReadCallbackIsWaitingForData = false;
}
void runOnceHandleFromPhoneQueue()
{
if (fromPhoneQueueSize == 0) {
return;
}
BLEValue val;
{
std::lock_guard<std::mutex> guard(fromPhoneMutex);
val = fromPhoneQueue[0];
for (size_t i = 1; i < fromPhoneQueueSize; ++i) {
fromPhoneQueue[i - 1] = fromPhoneQueue[i];
}
fromPhoneQueueSize--;
}
handleToRadio(val.getData(), val.getLength());
}
};
static HostedBluetoothPhoneAPI *bluetoothPhoneAPI = nullptr;
uint8_t lastToRadio[MAX_TO_FROM_RADIO_SIZE] = {0};
class HostedBluetoothToRadioCallback : public BLECharacteristicCallbacks
{
public:
void onWrite(BLECharacteristic *pCharacteristic) override
{
if (!bluetoothPhoneAPI) {
return;
}
BLEValue val;
val.setValue(pCharacteristic->getData(), pCharacteristic->getLength());
if (memcmp(lastToRadio, val.getData(), val.getLength()) == 0) {
return;
}
if (bluetoothPhoneAPI->fromPhoneQueueSize >= kBluetoothFromPhoneQueueSize) {
LOG_WARN("Hosted BLE onWrite drop: queue full (%u bytes)", val.getLength());
return;
}
memcpy(lastToRadio, val.getData(), val.getLength());
{
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->fromPhoneMutex);
bluetoothPhoneAPI->fromPhoneQueue.at(bluetoothPhoneAPI->fromPhoneQueueSize) = val;
bluetoothPhoneAPI->fromPhoneQueueSize++;
}
bluetoothPhoneAPI->setIntervalFromNow(0);
concurrency::mainDelay.interrupt();
}
};
class HostedBluetoothFromRadioCallback : public BLECharacteristicCallbacks
{
public:
void onRead(BLECharacteristic *pCharacteristic) override
{
if (!bluetoothPhoneAPI) {
return;
}
if (bluetoothPhoneAPI->toPhoneQueueSize == 0) {
bluetoothPhoneAPI->onReadCallbackIsWaitingForData = true;
bluetoothPhoneAPI->setIntervalFromNow(0);
concurrency::mainDelay.interrupt();
int tries = 0;
while (bluetoothPhoneAPI->onReadCallbackIsWaitingForData && tries < 4000) {
delay(tries < 20 ? 1 : 5);
tries++;
if (tries == 4000) {
LOG_WARN("BLE onRead: timeout waiting for data after %d tries, giving up and returning 0-size response",
tries);
}
}
}
uint8_t fromRadioBytes[meshtastic_FromRadio_size] = {0};
size_t numBytes = 0;
{
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->toPhoneMutex);
size_t pending = bluetoothPhoneAPI->toPhoneQueueSize.load();
if (pending > 0) {
numBytes = bluetoothPhoneAPI->toPhoneQueueByteSizes[0];
memcpy(fromRadioBytes, bluetoothPhoneAPI->toPhoneQueue[0].data(), numBytes);
for (size_t i = 1; i < pending; ++i) {
memcpy(bluetoothPhoneAPI->toPhoneQueue[i - 1].data(), bluetoothPhoneAPI->toPhoneQueue[i].data(),
bluetoothPhoneAPI->toPhoneQueueByteSizes[i]);
bluetoothPhoneAPI->toPhoneQueueByteSizes[i - 1] = bluetoothPhoneAPI->toPhoneQueueByteSizes[i];
}
bluetoothPhoneAPI->toPhoneQueueSize--;
}
}
pCharacteristic->setValue(fromRadioBytes, numBytes);
}
};
class HostedBluetoothServerCallback : public BLEServerCallbacks
{
public:
explicit HostedBluetoothServerCallback(HostedBluetooth *owner) : owner(owner) {}
private:
HostedBluetooth *owner;
void onConnect(BLEServer *pServer, struct ble_gap_conn_desc *desc) override
{
BLEAddress peerAddr(desc->peer_id_addr);
LOG_INFO("Hosted BLE incoming connection %s", peerAddr.toString().c_str());
owner->setConnected(true);
hostedConnHandle = desc->conn_handle;
const int dataLenResult = ble_gap_set_data_len(desc->conn_handle, kPreferredBleTxOctets, kPreferredBleTxTimeUs);
if (dataLenResult != 0) {
LOG_WARN("Hosted BLE failed to raise data length rc=%d", dataLenResult);
}
pServer->updateConnParams(desc->conn_handle, 6, 12, 0, 200);
}
void onDisconnect(BLEServer *pServer, struct ble_gap_conn_desc *desc) override
{
(void)pServer;
(void)desc;
LOG_INFO("Hosted BLE disconnected");
owner->setConnected(false);
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::DISCONNECTED);
bluetoothStatus->updateStatus(&newStatus);
clearPairingDisplay();
hostedConnHandle = BLE_HS_CONN_HANDLE_NONE;
memset(lastToRadio, 0, sizeof(lastToRadio));
if (bluetoothPhoneAPI) {
bluetoothPhoneAPI->close();
{
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->fromPhoneMutex);
bluetoothPhoneAPI->fromPhoneQueueSize = 0;
}
{
std::lock_guard<std::mutex> guard(bluetoothPhoneAPI->toPhoneMutex);
bluetoothPhoneAPI->toPhoneQueueSize = 0;
}
bluetoothPhoneAPI->onReadCallbackIsWaitingForData = false;
}
owner->startAdvertising();
}
};
class HostedBluetoothSecurityCallback : public BLESecurityCallbacks
{
public:
void onPassKeyNotify(uint32_t passkey) override
{
LOG_INFO("*** Enter passkey %06u on the peer side ***", passkey);
powerFSM.trigger(EVENT_BLUETOOTH_PAIR);
meshtastic::BluetoothStatus newStatus(std::to_string(passkey));
bluetoothStatus->updateStatus(&newStatus);
#if HAS_SCREEN
if (screen) {
screen->startAlert([passkey](OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) -> void {
char btPIN[16] = "888888";
snprintf(btPIN, sizeof(btPIN), "%06u", passkey);
int x_offset = display->width() / 2;
int y_offset = display->height() <= 80 ? 0 : 12;
display->setTextAlignment(TEXT_ALIGN_CENTER);
display->setFont(FONT_MEDIUM);
display->drawString(x_offset + x, y_offset + y, "Bluetooth");
#if !defined(M5STACK_UNITC6L)
display->setFont(FONT_SMALL);
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_MEDIUM - 4 : y_offset + FONT_HEIGHT_MEDIUM + 5;
display->drawString(x_offset + x, y_offset + y, "Enter this code");
#endif
display->setFont(FONT_LARGE);
char pin[8];
snprintf(pin, sizeof(pin), "%.3s %.3s", btPIN, btPIN + 3);
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_SMALL - 5 : y_offset + FONT_HEIGHT_SMALL + 5;
display->drawString(x_offset + x, y_offset + y, pin);
display->setFont(FONT_SMALL);
char deviceName[64];
snprintf(deviceName, sizeof(deviceName), "Name: %s", getDeviceName());
y_offset = display->height() == 64 ? y_offset + FONT_HEIGHT_LARGE - 6 : y_offset + FONT_HEIGHT_LARGE + 5;
display->drawString(x_offset + x, y_offset + y, deviceName);
});
}
#endif
passkeyShowing = true;
}
void onAuthenticationComplete(ble_gap_conn_desc *desc) override
{
(void)desc;
LOG_INFO("Hosted BLE authentication complete");
meshtastic::BluetoothStatus newStatus(meshtastic::BluetoothStatus::ConnectionState::CONNECTED);
bluetoothStatus->updateStatus(&newStatus);
clearPairingDisplay();
}
};
HostedBluetoothToRadioCallback *toRadioCallbacks = nullptr;
HostedBluetoothFromRadioCallback *fromRadioCallbacks = nullptr;
HostedBluetoothSecurityCallback *securityCallbacks = nullptr;
gpio_num_t getSlaveResetGpio()
{
#if defined(CONFIG_ESP_HOSTED_GPIO_SLAVE_RESET_SLAVE)
return static_cast<gpio_num_t>(CONFIG_ESP_HOSTED_GPIO_SLAVE_RESET_SLAVE);
#elif defined(CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE)
return static_cast<gpio_num_t>(CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE);
#else
return GPIO_NUM_NC;
#endif
}
void setSlaveResetLine(bool assertReset)
{
const gpio_num_t gpioNum = getSlaveResetGpio();
if (gpioNum == GPIO_NUM_NC || gpioNum < 0) {
return;
}
if (gpio_reset_pin(gpioNum) != ESP_OK) {
return;
}
if (gpio_set_direction(gpioNum, GPIO_MODE_OUTPUT) != ESP_OK) {
return;
}
const int activeLevel =
#if defined(CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH)
1;
#else
0;
#endif
const int inactiveLevel = activeLevel ? 0 : 1;
gpio_set_level(gpioNum, assertReset ? activeLevel : inactiveLevel);
LOG_DEBUG("[HostedBluetooth] setSlaveResetLine: GPIO[%d] -> %d (assertReset=%d, activeLevel=%d)", gpioNum,
assertReset ? activeLevel : inactiveLevel, assertReset, activeLevel);
}
void hostedEventHandler(void *arg, esp_event_base_t eventBase, int32_t eventId, void *eventData)
{
(void)eventData;
auto *self = static_cast<HostedBluetooth *>(arg);
if (!self || eventBase != ESP_HOSTED_EVENT) {
return;
}
switch (eventId) {
case ESP_HOSTED_EVENT_TRANSPORT_UP:
LOG_INFO("ESP-Hosted transport is up");
self->setConnected(true);
break;
case ESP_HOSTED_EVENT_TRANSPORT_DOWN:
LOG_WARN("ESP-Hosted transport is down");
[[fallthrough]];
case ESP_HOSTED_EVENT_TRANSPORT_FAILURE:
if (eventId == ESP_HOSTED_EVENT_TRANSPORT_FAILURE) {
LOG_ERROR("ESP-Hosted transport failure");
}
self->setConnected(false);
break;
case ESP_HOSTED_EVENT_CP_INIT:
case ESP_HOSTED_EVENT_CP_HEARTBEAT:
default:
break;
}
}
} // namespace
HostedBluetooth::HostedBluetooth() {}
HostedBluetooth::~HostedBluetooth()
{
deinit();
}
bool HostedBluetooth::registerCallbacks()
{
if (callbacksRegistered) {
return true;
}
// Defensive cleanup in case setup() is called again after an incomplete/early previous init.
// esp_event_handler_unregister can safely fail if the handler wasn't present.
esp_event_handler_unregister(ESP_HOSTED_EVENT, ESP_EVENT_ANY_ID, hostedEventHandler);
esp_err_t err = esp_event_handler_register(ESP_HOSTED_EVENT, ESP_EVENT_ANY_ID, hostedEventHandler, this);
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
LOG_ERROR("Failed to register hosted event handler: %s", esp_err_to_name(err));
return false;
}
if (err == ESP_ERR_INVALID_STATE) {
LOG_WARN("Hosted event handler already registered, continuing");
}
callbacksRegistered = true;
return true;
}
void HostedBluetooth::unregisterCallbacks()
{
if (!callbacksRegistered) {
return;
}
esp_event_handler_unregister(ESP_HOSTED_EVENT, ESP_EVENT_ANY_ID, hostedEventHandler);
callbacksRegistered = false;
}
void HostedBluetooth::setup()
{
if (active) {
return;
}
// Ensure co-processor is released from reset before bringing transport back up.
setSlaveResetLine(false);
LOG_DEBUG("[HostedBluetooth] setup(): Released co-processor from reset");
if (!registerCallbacks()) {
return;
}
active = setupGatt();
firstRssiLogged.store(false);
if (active) {
LOG_INFO("ESP-Hosted Bluetooth ready");
} else {
LOG_ERROR("Hosted BLE setup failed in hosted mode");
deinit();
}
}
void HostedBluetooth::shutdown()
{
deinit();
}
void HostedBluetooth::deinit()
{
if (!active && !callbacksRegistered) {
return;
}
shutdownGatt();
if (BLEDevice::getInitialized()) {
BLEDevice::deinit(false);
} else {
hostedDeinitBLE();
}
// Hold co-processor in reset when hosted transport is disabled to reduce idle draw.
setSlaveResetLine(true);
unregisterCallbacks();
connected.store(false);
active = false;
rssi.store(0);
firstRssiLogged.store(false);
}
void HostedBluetooth::clearBonds()
{
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_PEER_SEC, nullptr);
ble_store_util_delete_all(BLE_STORE_OBJ_TYPE_CCCD, nullptr);
}
bool HostedBluetooth::isActive()
{
return active;
}
bool HostedBluetooth::isConnected()
{
if (!connected.load()) {
return false;
}
return bleServer && bleServer->getConnectedCount() > 0;
}
int HostedBluetooth::getRssi()
{
if (!bleServer || !isConnected()) {
return 0;
}
uint16_t connHandle = hostedConnHandle.load();
if (connHandle == BLE_HS_CONN_HANDLE_NONE) {
const auto peers = bleServer->getPeerDevices(true);
if (!peers.empty()) {
connHandle = peers.begin()->first;
hostedConnHandle = connHandle;
}
}
if (connHandle == BLE_HS_CONN_HANDLE_NONE) {
return 0;
}
int8_t currentRssi = 0;
const int rc = ble_gap_conn_rssi(connHandle, &currentRssi);
if (rc == 0) {
setRssi(currentRssi);
return currentRssi;
}
return rssi.load();
}
void HostedBluetooth::sendLog(const uint8_t *logMessage, size_t length)
{
if (!logRadioCharacteristic || !isConnected() || length > 512) {
return;
}
logRadioCharacteristic->setValue(logMessage, length);
logRadioCharacteristic->notify();
}
void HostedBluetooth::setConnected(bool value)
{
connected.store(value);
}
void HostedBluetooth::setRssi(int value)
{
rssi.store(value);
maybeLogFirstRssi(value);
}
void HostedBluetooth::maybeLogFirstRssi(int value)
{
bool expected = false;
if (firstRssiLogged.compare_exchange_strong(expected, true)) {
LOG_INFO("ESP-Hosted first RSSI update: %d dBm", value);
}
}
bool HostedBluetooth::setupGatt()
{
memset(lastToRadio, 0, sizeof(lastToRadio));
hostedConnHandle = BLE_HS_CONN_HANDLE_NONE;
if (!BLEDevice::init(getDeviceName())) {
LOG_ERROR("Hosted BLE init failed");
return false;
}
#ifdef ESP_PWR_LVL_P9
BLEDevice::setPower(ESP_PWR_LVL_P9);
#endif
BLESecurity *security = new BLESecurity();
security->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
security->setRespEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
if (config.bluetooth.mode != meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
security->setCapability(ESP_IO_CAP_OUT);
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_RANDOM_PIN) {
LOG_INFO("Hosted BLE using random passkey");
security->setPassKey(false);
} else {
LOG_INFO("Hosted BLE using fixed passkey");
security->setPassKey(true, config.bluetooth.fixed_pin);
}
// Enable authorization requirements:
// - bonding: true (for persistent storage of the keys)
// - MITM: true (enables Man-In-The-Middle protection for password prompts)
// - secure connection: true (enables secure connection for encryption)
security->setAuthenticationMode(true, true, true);
} else {
security->setCapability(ESP_IO_CAP_NONE);
security->setAuthenticationMode(true, false, false);
}
if (!securityCallbacks) {
securityCallbacks = new HostedBluetoothSecurityCallback();
}
BLEDevice::setSecurityCallbacks(securityCallbacks);
const int mtuResult = BLEDevice::setMTU(kPreferredBleMtu);
if (mtuResult == 0) {
LOG_INFO("Hosted BLE MTU request set to %u", kPreferredBleMtu);
} else {
LOG_WARN("Hosted BLE unable to request MTU %u, rc=%d", kPreferredBleMtu, mtuResult);
}
bleServer = BLEDevice::createServer();
if (!bleServer) {
LOG_ERROR("Hosted BLE createServer failed");
return false;
}
const int nameRc = ble_svc_gap_device_name_set(BLEDevice::getDeviceName().c_str());
if (nameRc != 0) {
LOG_WARN("Hosted BLE device_name_set rc=%d %s", nameRc, BLEUtils::returnCodeToString(nameRc));
}
bleServer->setCallbacks(new HostedBluetoothServerCallback(this));
BLEService *meshService = bleServer->createService(MESH_SERVICE_UUID);
if (!meshService) {
LOG_ERROR("Hosted BLE mesh service creation failed");
return false;
}
BLECharacteristic *toRadioCharacteristic = nullptr;
BLECharacteristic *fromRadioCharacteristic = nullptr;
if (config.bluetooth.mode == meshtastic_Config_BluetoothConfig_PairingMode_NO_PIN) {
toRadioCharacteristic = meshService->createCharacteristic(TORADIO_UUID, BLECharacteristic::PROPERTY_WRITE);
fromRadioCharacteristic = meshService->createCharacteristic(FROMRADIO_UUID, BLECharacteristic::PROPERTY_READ);
fromNumCharacteristic = meshService->createCharacteristic(FROMNUM_UUID, BLECharacteristic::PROPERTY_NOTIFY |
BLECharacteristic::PROPERTY_READ);
logRadioCharacteristic = meshService->createCharacteristic(LOGRADIO_UUID, BLECharacteristic::PROPERTY_NOTIFY |
BLECharacteristic::PROPERTY_READ);
} else {
toRadioCharacteristic = meshService->createCharacteristic(TORADIO_UUID, BLECharacteristic::PROPERTY_WRITE |
BLECharacteristic::PROPERTY_WRITE_AUTHEN |
BLECharacteristic::PROPERTY_WRITE_ENC);
fromRadioCharacteristic = meshService->createCharacteristic(FROMRADIO_UUID, BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_READ_AUTHEN |
BLECharacteristic::PROPERTY_READ_ENC);
fromNumCharacteristic = meshService->createCharacteristic(
FROMNUM_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_READ_AUTHEN | BLECharacteristic::PROPERTY_READ_ENC);
logRadioCharacteristic = meshService->createCharacteristic(
LOGRADIO_UUID, BLECharacteristic::PROPERTY_NOTIFY | BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_READ_AUTHEN | BLECharacteristic::PROPERTY_READ_ENC);
}
if (!toRadioCharacteristic || !fromRadioCharacteristic || !fromNumCharacteristic || !logRadioCharacteristic) {
LOG_ERROR("Hosted BLE characteristic creation failed");
return false;
}
if (!bluetoothPhoneAPI) {
bluetoothPhoneAPI = new HostedBluetoothPhoneAPI();
}
if (!toRadioCallbacks) {
toRadioCallbacks = new HostedBluetoothToRadioCallback();
}
if (!fromRadioCallbacks) {
fromRadioCallbacks = new HostedBluetoothFromRadioCallback();
}
toRadioCharacteristic->setCallbacks(toRadioCallbacks);
fromRadioCharacteristic->setCallbacks(fromRadioCallbacks);
meshService->start();
startAdvertising();
return true;
}
void HostedBluetooth::shutdownGatt()
{
if (bluetoothPhoneAPI) {
bluetoothPhoneAPI->close();
delete bluetoothPhoneAPI;
bluetoothPhoneAPI = nullptr;
}
delete toRadioCallbacks;
toRadioCallbacks = nullptr;
delete fromRadioCallbacks;
fromRadioCallbacks = nullptr;
delete securityCallbacks;
securityCallbacks = nullptr;
if (bleServer) {
BLEAdvertising *advertising = BLEDevice::getAdvertising();
if (advertising) {
advertising->stop();
}
}
fromNumCharacteristic = nullptr;
logRadioCharacteristic = nullptr;
bleServer = nullptr;
hostedConnHandle = BLE_HS_CONN_HANDLE_NONE;
}
void HostedBluetooth::startAdvertising()
{
BLEAdvertising *advertising = BLEDevice::getAdvertising();
if (!advertising) {
LOG_ERROR("Hosted BLE getAdvertising failed");
return;
}
advertising->stop();
advertising->reset();
advertising->addServiceUUID(MESH_SERVICE_UUID);
BLEAdvertisementData scan;
scan.setName(getDeviceName());
advertising->setScanResponseData(scan);
advertising->setMinPreferred(0x06);
advertising->setMaxPreferred(0x12);
if (!advertising->start(0)) {
LOG_ERROR("Hosted BLE failed to start advertising");
} else {
LOG_INFO("Hosted BLE advertising started");
}
}
#endif
-43
View File
@@ -1,43 +0,0 @@
#pragma once
#include "BluetoothCommon.h"
#include <atomic>
/**
* Placeholder backend for ESP32-P4 + ESP-Hosted BLE transport.
*
* This intentionally mirrors the NimbleBluetooth surface used by the firmware,
* so the caller side can stay stable while we implement esp-hosted internals.
*/
class HostedBluetooth : public BluetoothApi
{
public:
HostedBluetooth();
~HostedBluetooth() override;
void setup() override;
void shutdown() override;
void deinit() override;
void clearBonds() override;
bool isActive() override;
bool isConnected() override;
int getRssi() override;
void sendLog(const uint8_t *logMessage, size_t length) override;
void startAdvertising();
void setConnected(bool value);
void setRssi(int value);
private:
bool setupGatt();
void shutdownGatt();
void maybeLogFirstRssi(int value);
bool registerCallbacks();
void unregisterCallbacks();
bool active = false;
std::atomic<bool> connected{false};
std::atomic<int> rssi{0};
std::atomic<bool> firstRssiLogged{false};
bool callbacksRegistered = false;
};
+211
View File
@@ -0,0 +1,211 @@
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./BaseUIEInkDisplay.h"
#include "configuration.h"
#include "main.h"
using namespace NicheGraphics;
BaseUIEInkDisplay::BaseUIEInkDisplay(Drivers::EInk *driver, uint8_t rotation) : driver(driver), rotation(rotation & 0x3)
{
this->geometry = GEOMETRY_RAWMODE;
// BaseUI draws in UI orientation. Physical panel dimensions are swapped for 90°/270°.
const bool swap = (this->rotation == 1) || (this->rotation == 3);
this->displayWidth = swap ? driver->height : driver->width;
this->displayHeight = swap ? driver->width : driver->height;
uint16_t shortSide = min(displayWidth, displayHeight);
uint16_t longSide = max(displayWidth, displayHeight);
if (shortSide % 8 != 0)
shortSide = (shortSide | 7) + 1;
this->displayBufferSize = longSide * (shortSide / 8);
// Panel-native row-major buffer
panelRowBytes = ((driver->width - 1) / 8) + 1;
panelBufferSize = panelRowBytes * driver->height;
panelBuffer = new uint8_t[panelBufferSize];
memset(panelBuffer, 0xFF, panelBufferSize); // All white
}
BaseUIEInkDisplay::~BaseUIEInkDisplay()
{
delete[] panelBuffer;
}
bool BaseUIEInkDisplay::connect()
{
LOG_INFO("Init BaseUI E-Ink (%u x %u, rot %u)", driver->width, driver->height, rotation);
return true;
}
void BaseUIEInkDisplay::addFrameFlag(frameFlagTypes flag)
{
frameFlags = (frameFlagTypes)(frameFlags | flag);
}
void BaseUIEInkDisplay::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier)
{
this->fastPerFull = (fastPerFull == 0) ? 1 : fastPerFull;
this->stressMultiplier = stressMultiplier;
}
void BaseUIEInkDisplay::joinAsyncRefresh()
{
if (driver->busy())
driver->await();
}
// OLEDDisplayUi tick path. Honours rate-limit unless flags demand otherwise.
void BaseUIEInkDisplay::display()
{
const bool demandFast = frameFlags & DEMAND_FAST;
const bool cosmetic = frameFlags & COSMETIC;
const bool unlimitedFast = frameFlags & UNLIMITED_FAST;
if (!demandFast && !cosmetic && !unlimitedFast) {
if (!forceDisplay(lastDrawMsec == 0 ? 0 : 1000))
return;
return;
}
forceDisplay(0);
}
// Keyframe path. Returns true if a frame was pushed (sets lastDrawMsec).
bool BaseUIEInkDisplay::forceDisplay(uint32_t msecLimit)
{
const uint32_t now = millis();
if (lastDrawMsec != 0 && (now - lastDrawMsec) < msecLimit)
return false;
const bool blocking = frameFlags & BLOCKING;
Drivers::EInk::UpdateTypes type = decide();
// Don't pile frames on top of a running update - wait it out.
if (driver->busy())
driver->await();
const bool pushed = commit(type, blocking);
if (pushed)
lastDrawMsec = now;
// Reset flags for next frame
frameFlags = BACKGROUND;
return pushed;
}
bool BaseUIEInkDisplay::commit(Drivers::EInk::UpdateTypes type, bool blocking)
{
uint32_t hash = repack();
// Skip if frame unchanged. Exception: caller explicitly wants a refresh (COSMETIC or FULL).
if (hash == lastHash && type != Drivers::EInk::UpdateTypes::FULL && lastDrawMsec != 0)
return false;
lastHash = hash;
// Fall back to FULL on panels that don't advertise FAST support.
if (type == Drivers::EInk::UpdateTypes::FAST && !driver->supports(Drivers::EInk::UpdateTypes::FAST))
type = Drivers::EInk::UpdateTypes::FULL;
driver->update(panelBuffer, type);
if (blocking)
driver->await();
return true;
}
Drivers::EInk::UpdateTypes BaseUIEInkDisplay::decide()
{
typedef Drivers::EInk::UpdateTypes UT;
const bool unlimitedFast = frameFlags & UNLIMITED_FAST;
// Explicit flag wins outright
if (frameFlags & COSMETIC) {
fullRefreshDebt = max(fullRefreshDebt - 1.0f, 0.0f);
return UT::FULL;
}
if (frameFlags & DEMAND_FAST) {
if (!unlimitedFast) {
fullRefreshDebt += (fullRefreshDebt < 1.0f) ? (1.0f / fastPerFull) : (stressMultiplier * (1.0f / fastPerFull));
}
return UT::FAST;
}
const bool explicitFast = frameFlags & RESPONSIVE;
if (explicitFast || unlimitedFast) {
if (!unlimitedFast) {
fullRefreshDebt += (fullRefreshDebt < 1.0f) ? (1.0f / fastPerFull) : (stressMultiplier * (1.0f / fastPerFull));
}
return UT::FAST;
}
// BACKGROUND / unspecified: let debt decide
if (fullRefreshDebt >= 1.0f) {
fullRefreshDebt = max(fullRefreshDebt - 1.0f, 0.0f);
return UT::FULL;
}
fullRefreshDebt += 1.0f / fastPerFull;
return UT::FAST;
}
uint32_t BaseUIEInkDisplay::repack()
{
memset(panelBuffer, 0xFF, panelBufferSize); // start all-white
const uint16_t pw = driver->width;
const uint16_t ph = driver->height;
// OLEDDisplay buffer: byte = buffer[x + (y/8) * displayWidth]; bit = 1 << (y & 7); 1 = black
// Niche buffer: byte = (y * panelRowBytes) + (x/8); bit = 1 << (7 - x%8); 1 = white
for (uint16_t oy = 0; oy < displayHeight; oy++) {
for (uint16_t ox = 0; ox < displayWidth; ox++) {
const uint8_t b = buffer[ox + (oy / 8) * displayWidth];
const bool isBlack = b & (1 << (oy & 7));
uint16_t px, py;
switch (rotation) {
case 1: // 90° CW: OLED (ox,oy) → panel (pw-1-oy, ox)
px = pw - 1 - oy;
py = ox;
break;
case 2: // 180°
px = pw - 1 - ox;
py = ph - 1 - oy;
break;
case 3: // 270° CW
px = oy;
py = ph - 1 - ox;
break;
case 0:
default:
px = ox;
py = oy;
break;
}
if (px >= pw || py >= ph)
continue;
const uint32_t byteNum = (py * panelRowBytes) + (px / 8);
const uint8_t bitNum = 7 - (px % 8);
if (isBlack)
panelBuffer[byteNum] &= ~(1 << bitNum);
else
panelBuffer[byteNum] |= (1 << bitNum);
}
}
// FNV-1a
uint32_t h = 2166136261u;
for (uint32_t i = 0; i < panelBufferSize; i++) {
h ^= panelBuffer[i];
h *= 16777619u;
}
return h;
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+98
View File
@@ -0,0 +1,98 @@
/*
OLEDDisplay adapter that routes BaseUI pixel output to a NicheGraphics::Drivers::EInk driver.
One adapter serves all E-Ink variants: the panel driver and orientation are injected at construction,
and FULL/FAST selection is made by the shared DisplayHealth model (same as InkHUD).
Replaces the per-board branching in EInkDisplay2 / EInkDynamicDisplay / EInkParallelDisplay.
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "graphics/eink/Drivers/EInk.h"
#include <OLEDDisplay.h>
namespace NicheGraphics
{
class BaseUIEInkDisplay : public OLEDDisplay
{
public:
// Flags Screen.cpp sets via EINK_ADD_FRAMEFLAG before triggering a draw.
// Bits are combined; decided at render time.
enum frameFlagTypes : uint8_t {
BACKGROUND = (1 << 0), // Regular OLEDDisplayUi tick - no urgency, UNSPECIFIED
RESPONSIVE = (1 << 1), // User-driven refresh - prefer FAST
COSMETIC = (1 << 2), // Clean splash / wake-from-sleep - force FULL
DEMAND_FAST = (1 << 3), // Menu interaction - force FAST
BLOCKING = (1 << 4), // Wait for update to finish before returning
UNLIMITED_FAST = (1 << 5), // Suppress health-driven FULL promotion (typing modes)
};
BaseUIEInkDisplay(Drivers::EInk *driver, uint8_t rotation);
~BaseUIEInkDisplay() override;
// OLEDDisplay overrides
bool connect() override;
void display() override;
void sendCommand(uint8_t com) override { (void)com; }
int getBufferOffset(void) override { return 0; }
// BaseUI public API (same shape as the old EInkDynamicDisplay)
bool forceDisplay(uint32_t msecLimit = 1000);
void addFrameFlag(frameFlagTypes flag);
void joinAsyncRefresh();
void enableUnlimitedFastMode() { addFrameFlag(UNLIMITED_FAST); }
void disableUnlimitedFastMode() { frameFlags = (frameFlagTypes)(frameFlags & ~UNLIMITED_FAST); }
// Tuning, called once per panel profile
void setDisplayResilience(uint8_t fastPerFull, float stressMultiplier = 2.0f);
// Exposed so Screen.cpp / variants can read the rotation passed in at construction
uint8_t getRotation() const { return rotation; }
private:
// Perform an update now, unconditionally. Returns true if a frame was pushed to the driver.
bool commit(Drivers::EInk::UpdateTypes type, bool blocking);
// Convert OLEDDisplay's column-major buffer into the panel's row-major MSB-left buffer.
// Applies rotation. Returns the hash of the panel buffer for frame-skip comparison.
uint32_t repack();
// Decide FULL vs FAST based on current frame flags + accumulated debt.
Drivers::EInk::UpdateTypes decide();
Drivers::EInk *driver = nullptr;
uint8_t rotation = 0; // 0=0°, 1=90°CW, 2=180°, 3=270°CW
uint8_t *panelBuffer = nullptr;
uint32_t panelBufferSize = 0;
uint16_t panelRowBytes = 0;
frameFlagTypes frameFlags = BACKGROUND;
uint32_t lastDrawMsec = 0;
uint32_t lastHash = 0;
// DisplayHealth-style debt tracking
float fullRefreshDebt = 0.0f;
uint8_t fastPerFull = 7;
float stressMultiplier = 2.0f;
};
} // namespace NicheGraphics
// Compat macros used throughout Screen.cpp - route straight to the adapter.
#define EINK_ADD_FRAMEFLAG(display, flag) \
static_cast<NicheGraphics::BaseUIEInkDisplay *>(display)->addFrameFlag(NicheGraphics::BaseUIEInkDisplay::flag)
#define EINK_JOIN_ASYNCREFRESH(display) static_cast<NicheGraphics::BaseUIEInkDisplay *>(display)->joinAsyncRefresh()
#else // !MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#define EINK_ADD_FRAMEFLAG(display, flag)
#define EINK_JOIN_ASYNCREFRESH(display)
#endif
-298
View File
@@ -1,298 +0,0 @@
#include "configuration.h"
#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY)
#include "EInkDisplay2.h"
#include "SPILock.h"
#include "main.h"
#include <SPI.h>
#ifdef GXEPD2_DRIVER_0
#include "einkDetect.h"
#endif
/*
The macros EINK_DISPLAY_MODEL, EINK_WIDTH, and EINK_HEIGHT are defined as build_flags in a variant's platformio.ini
Previously, these macros were defined at the top of this file.
For archival reasons, note that the following configurations had also been tested during this period:
* ifdef RAK4631
- 4.2 inch
EINK_DISPLAY_MODEL: GxEPD2_420_M01
EINK_WIDTH: 300
EINK_WIDTH: 400
- 2.9 inch
EINK_DISPLAY_MODEL: GxEPD2_290_T5D
EINK_WIDTH: 296
EINK_HEIGHT: 128
- 1.54 inch
EINK_DISPLAY_MODEL: GxEPD2_154_M09
EINK_WIDTH: 200
EINK_HEIGHT: 200
*/
// Constructor
EInkDisplay::EInkDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus)
{
// Set dimensions in OLEDDisplay base class
this->geometry = GEOMETRY_RAWMODE;
this->displayWidth = EINK_WIDTH;
this->displayHeight = EINK_HEIGHT;
// Round shortest side up to nearest byte, to prevent truncation causing an undersized buffer
uint16_t shortSide = min(EINK_WIDTH, EINK_HEIGHT);
uint16_t longSide = max(EINK_WIDTH, EINK_HEIGHT);
if (shortSide % 8 != 0)
shortSide = (shortSide | 7) + 1;
this->displayBufferSize = longSide * (shortSide / 8);
}
/**
* Force a display update if we haven't drawn within the specified msecLimit
*/
bool EInkDisplay::forceDisplay(uint32_t msecLimit)
{
// No need to grab this lock because we are on our own SPI bus
// concurrency::LockGuard g(spiLock);
uint32_t now = millis();
uint32_t sinceLast = now - lastDrawMsec;
if (adafruitDisplay && (sinceLast > msecLimit || lastDrawMsec == 0))
lastDrawMsec = now;
else
return false;
// FIXME - only draw bits have changed (use backbuf similar to the other displays)
const bool flipped = config.display.flip_screen;
// HACK for L1 EInk
#if defined(SEEED_WIO_TRACKER_L1_EINK)
// For SEEED_WIO_TRACKER_L1_EINK, setRotation(3) is correct but mirrored; flip both axes
for (uint32_t y = 0; y < displayHeight; y++) {
for (uint32_t x = 0; x < displayWidth; x++) {
auto b = buffer[x + (y / 8) * displayWidth];
auto isset = b & (1 << (y & 7));
adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE);
}
}
#else
for (uint32_t y = 0; y < displayHeight; y++) {
for (uint32_t x = 0; x < displayWidth; x++) {
auto b = buffer[x + (y / 8) * displayWidth];
auto isset = b & (1 << (y & 7));
if (flipped)
adafruitDisplay->drawPixel((displayWidth - 1) - x, (displayHeight - 1) - y, isset ? GxEPD_BLACK : GxEPD_WHITE);
else
adafruitDisplay->drawPixel(x, y, isset ? GxEPD_BLACK : GxEPD_WHITE);
}
}
#endif
// Trigger the refresh in GxEPD2
LOG_DEBUG("Update E-Paper");
adafruitDisplay->nextPage();
// End the update process
endUpdate();
LOG_DEBUG("done");
return true;
}
// End the update process - virtual method, overridden in derived class
void EInkDisplay::endUpdate()
{
#ifndef EINK_NOT_HIBERNATE
// By default, power off the E-Ink display hardware and enter hibernate().
// Boards/panels that define EINK_NOT_HIBERNATE intentionally skip this step.
// Skipping hibernate() can help avoid panel-specific wake/refresh or ghosting issues,
// but it typically trades lower power savings for that compatibility.
adafruitDisplay->hibernate();
#endif
}
// Write the buffer to the display memory
void EInkDisplay::display(void)
{
// We don't allow regular 'dumb' display() calls to draw on eink until we've shown
// at least one forceDisplay() keyframe. This prevents flashing when we should the critical
// bootscreen (that we want to look nice)
if (lastDrawMsec) {
forceDisplay(slowUpdateMsec); // Show the first screen a few seconds after boot, then slower
}
}
// Send a command to the display (low level function)
void EInkDisplay::sendCommand(uint8_t com)
{
(void)com;
// Drop all commands to device (we just update the buffer)
}
void EInkDisplay::setDetected(uint8_t detected)
{
(void)detected;
}
// Connect to the display - variant specific
bool EInkDisplay::connect()
{
LOG_INFO("Do EInk init");
#ifdef PIN_EINK_EN
// backlight power, HIGH is backlight on, LOW is off
pinMode(PIN_EINK_EN, OUTPUT);
#ifdef ELECROW_ThinkNode_M1
// ThinkNode M1 has a hardware dimmable backlight. Start enabled
digitalWrite(PIN_EINK_EN, HIGH);
#elif defined(MINI_EPAPER_S3)
// T-Mini Epaper S3 requires panel power rail enabled before SPI transfer.
digitalWrite(PIN_EINK_EN, HIGH);
delay(10);
#else
digitalWrite(PIN_EINK_EN, LOW);
#endif
#endif
#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE) || defined(TTGO_T_ECHO_PLUS)
{
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
adafruitDisplay->init();
#if defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE)
adafruitDisplay->setRotation(4);
#else
adafruitDisplay->setRotation(3);
#endif
adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight);
}
#elif defined(ELECROW_ThinkNode_M5)
{
// Start HSPI
hspi = new SPIClass(HSPI);
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
adafruitDisplay->init();
adafruitDisplay->setRotation(4);
adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight);
}
#elif defined(MESHLINK)
{
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
adafruitDisplay->init();
adafruitDisplay->setRotation(3);
adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight);
}
#elif defined(RAK4630) || defined(MAKERPYTHON)
{
if (eink_found) {
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
adafruitDisplay->init(115200, true, 10, false, SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
// RAK14000 2.13 inch b/w 250x122 does actually now support fast refresh
adafruitDisplay->setRotation(3);
// Fast refresh support for 1.54, 2.13 RAK14000 b/w , 2.9 and 4.2
// adafruitDisplay->setRotation(1);
adafruitDisplay->setPartialWindow(0, 0, displayWidth, displayHeight);
} else {
(void)adafruitDisplay;
}
}
#elif defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || \
defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || \
defined(MINI_EPAPER_S3)
{
// Start HSPI
hspi = new SPIClass(HSPI);
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS
// VExt already enabled in setup()
// RTC GPIO hold disabled in setup()
// Create GxEPD2 objects
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *hspi);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
// Init GxEPD2
adafruitDisplay->init();
#if defined(MINI_EPAPER_S3)
adafruitDisplay->setRotation(3);
#else
adafruitDisplay->setRotation(3);
#if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER)
adafruitDisplay->setRotation(0);
#endif
#endif
}
#elif defined(PCA10059) || defined(ME25LS01)
{
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
adafruitDisplay->init(115200, true, 40, false, SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
adafruitDisplay->setRotation(0);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
}
#elif defined(M5_COREINK) || defined(T_DECK_PRO)
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0));
adafruitDisplay->setRotation(0);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
#elif defined(my) || defined(ESP32_S3_PICO)
{
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
adafruitDisplay->init(115200, true, 40, false, SPI, SPISettings(4000000, MSBFIRST, SPI_MODE0));
adafruitDisplay->setRotation(1);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
}
#elif defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK)
{
spi1 = &SPI1;
spi1->begin();
// VExt already enabled in setup()
// RTC GPIO hold disabled in setup()
// Create GxEPD2 objects
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, *spi1);
adafruitDisplay = new GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT>(*lowLevel);
// Init GxEPD2
adafruitDisplay->init();
adafruitDisplay->setRotation(3);
adafruitDisplay->setPartialWindow(0, 0, EINK_WIDTH, EINK_HEIGHT);
}
#elif defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_VISION_MASTER_E213)
// Detect display model, before starting SPI
EInkDetectionResult displayModel = detectEInk();
// Start HSPI
hspi = new SPIClass(HSPI);
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS); // SCLK, MISO, MOSI, SS
// Create GxEPD2 object
adafruitDisplay = new GxEPD2_Multi<GXEPD2_DRIVER_0, GXEPD2_DRIVER_1>((uint8_t)displayModel, PIN_EINK_CS, PIN_EINK_DC,
PIN_EINK_RES, PIN_EINK_BUSY, *hspi);
// Init GxEPD2
adafruitDisplay->init();
adafruitDisplay->setRotation(3);
#endif
return true;
}
#endif
-106
View File
@@ -1,106 +0,0 @@
#pragma once
#if defined(USE_EINK) && !defined(USE_EINK_PARALLELDISPLAY)
#include "GxEPD2_BW.h"
#include <OLEDDisplay.h>
#ifdef GXEPD2_DRIVER_0 // If variant has multiple possible display models
#include "GxEPD2Multi.h"
#endif
// Limit how often we push a full E-Ink refresh. T-Deck Pro needs faster updates for typing.
#ifndef EINK_FORCE_DISPLAY_THROTTLE_MS
#if defined(T_DECK_PRO)
#define EINK_FORCE_DISPLAY_THROTTLE_MS 200
#else
#define EINK_FORCE_DISPLAY_THROTTLE_MS 1000
#endif
#endif
/**
* An adapter class that allows using the GxEPD2 library as if it was an OLEDDisplay implementation.
*
* Note: EInkDynamicDisplay derives from this class.
*
* Remaining TODO:
* optimize display() to only draw changed pixels (see other OLED subclasses for examples)
* implement displayOn/displayOff to turn off the TFT device (and backlight)
* Use the fast NRF52 SPI API rather than the slow standard arduino version
*
* turn radio back on - currently with both on spi bus is fucked? or are we leaving chip select asserted?
* Suggestion: perhaps similar to HELTEC_WIRELESS_PAPER issue, which resolved with rtc_gpio_hold_dis()
*/
class EInkDisplay : public OLEDDisplay
{
/// How often should we update the display
/// thereafter we do once per 5 minutes
uint32_t slowUpdateMsec = 5 * 60 * 1000;
public:
/* constructor
FIXME - the parameters are not used, just a temporary hack to keep working like the old displays
*/
EInkDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C);
// Write the buffer to the display memory (for eink we only do this occasionally)
virtual void display(void) override;
/**
* Force a display update if we haven't drawn within the specified msecLimit
*
* @return true if we did draw the screen
*/
virtual bool forceDisplay(uint32_t msecLimit = EINK_FORCE_DISPLAY_THROTTLE_MS);
/**
* Run any code needed to complete an update, after the physical refresh has completed.
* Split from forceDisplay(), to enable async refresh in derived EInkDynamicDisplay class.
*
*/
virtual void endUpdate();
/**
* shim to make the abstraction happy
*
*/
void setDetected(uint8_t detected);
protected:
// the header size of the buffer used, e.g. for the SPI command header
virtual int getBufferOffset(void) override { return 0; }
// Send a command to the display (low level function)
virtual void sendCommand(uint8_t com) override;
// Connect to the display
virtual bool connect() override;
#ifdef GXEPD2_DRIVER_0
// AdafruitGFX display object - wrapper for multiple drivers
// Allows runtime detection of multiple displays
// Avoid this situation if possible!
GxEPD2_Multi<GXEPD2_DRIVER_0, GXEPD2_DRIVER_1> *adafruitDisplay = NULL;
#else
// AdafruitGFX display object (for single display model) - instantiated in connect(), variant specific
GxEPD2_BW<EINK_DISPLAY_MODEL, EINK_DISPLAY_MODEL::HEIGHT> *adafruitDisplay = NULL;
#endif
// If display uses HSPI
#if defined(HELTEC_WIRELESS_PAPER) || defined(HELTEC_WIRELESS_PAPER_V1_0) || defined(HELTEC_VISION_MASTER_E213) || \
defined(HELTEC_VISION_MASTER_E290) || defined(TLORA_T3S3_EPAPER) || defined(CROWPANEL_ESP32S3_5_EPAPER) || \
defined(CROWPANEL_ESP32S3_4_EPAPER) || defined(CROWPANEL_ESP32S3_2_EPAPER) || defined(ELECROW_ThinkNode_M5) || \
defined(MINI_EPAPER_S3)
SPIClass *hspi = NULL;
#endif
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK)
SPIClass *spi1 = NULL;
#endif
private:
// FIXME quick hack to limit drawing to a very slow rate
uint32_t lastDrawMsec = 0;
};
#endif
-564
View File
@@ -1,564 +0,0 @@
#include "Throttle.h"
#include "configuration.h"
#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
#include "EInkDynamicDisplay.h"
// Constructor
EInkDynamicDisplay::EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus)
: EInkDisplay(address, sda, scl, geometry, i2cBus), NotifiedWorkerThread("EInkDynamicDisplay")
{
// If tracking ghost pixels, grab memory
#ifdef EINK_LIMIT_GHOSTING_PX
dirtyPixels = std::unique_ptr<uint8_t[]>(new uint8_t[EInkDisplay::displayBufferSize]()); // Init with zeros
#endif
}
// Destructor
EInkDynamicDisplay::~EInkDynamicDisplay()
{
// If we were tracking ghost pixels, free the memory
#ifdef EINK_LIMIT_GHOSTING_PX
dirtyPixels = nullptr;
#endif
}
// Screen requests a BACKGROUND frame
void EInkDynamicDisplay::display()
{
addFrameFlag(BACKGROUND);
update();
}
// Screen requests a RESPONSIVE frame
bool EInkDynamicDisplay::forceDisplay(uint32_t msecLimit)
{
addFrameFlag(RESPONSIVE);
return update(); // (Unutilized) Base class promises to return true if update ran
}
// Add flag for the next frame
void EInkDynamicDisplay::addFrameFlag(frameFlagTypes flag)
{
// OR the new flag into the existing flags
this->frameFlags = (frameFlagTypes)(this->frameFlags | flag);
}
// GxEPD2 code to set fast refresh
void EInkDynamicDisplay::configForFastRefresh()
{
// Variant-specific code can go here
#if defined(PRIVATE_HW)
#else
// Otherwise:
adafruitDisplay->setPartialWindow(0, 0, adafruitDisplay->width(), adafruitDisplay->height());
#endif
}
// GxEPD2 code to set full refresh
void EInkDynamicDisplay::configForFullRefresh()
{
// Variant-specific code can go here
#if defined(PRIVATE_HW)
#else
// Otherwise:
adafruitDisplay->setFullWindow();
#endif
}
// Run any relevant GxEPD2 code, so next update will use correct refresh type
void EInkDynamicDisplay::applyRefreshMode()
{
// Change from FULL to FAST
if (currentConfig == FULL && refresh == FAST) {
configForFastRefresh();
currentConfig = FAST;
}
// Change from FAST back to FULL
else if (currentConfig == FAST && refresh == FULL) {
configForFullRefresh();
currentConfig = FULL;
}
}
// Update fastRefreshCount
void EInkDynamicDisplay::adjustRefreshCounters()
{
if (refresh == FAST)
fastRefreshCount++;
else if (refresh == FULL)
fastRefreshCount = 0;
}
// Trigger the display update by calling base class
bool EInkDynamicDisplay::update()
{
// Determine the refresh mode to use, and start the update
bool refreshApproved = determineMode();
if (refreshApproved) {
EInkDisplay::forceDisplay(0); // Bypass base class' own rate-limiting system
storeAndReset(); // Store the result of this loop for next time. Note: call *before* endOrDetach()
endOrDetach(); // endUpdate() right now, or set the async refresh flag (if FULL and HAS_EINK_ASYNCFULL)
} else
storeAndReset(); // No update, no post-update code, just store the results
return refreshApproved; // (Unutilized) Base class promises to return true if update ran
}
// Figure out who runs the post-update code
void EInkDynamicDisplay::endOrDetach()
{
// If the GxEPD2 version reports that it has the async modifications
#ifdef HAS_EINK_ASYNCFULL
if (previousRefresh == FULL) {
asyncRefreshRunning = true; // Set the flag - checked in determineMode(); cleared by onNotify()
if (previousFrameFlags & BLOCKING)
awaitRefresh();
else {
// Async begins
LOG_DEBUG("Async full-refresh begins (drop frames)");
notifyLater(intervalPollAsyncRefresh, DUE_POLL_ASYNCREFRESH, true); // Hand-off to NotifiedWorkerThread
}
}
// Fast Refresh
else if (previousRefresh == FAST)
EInkDisplay::endUpdate(); // Still block while updating, but EInkDisplay needs us to call endUpdate() ourselves.
// Fallback - If using an unmodified version of GxEPD2 for some reason
#else
if (previousRefresh == FULL || previousRefresh == FAST) { // If refresh wasn't skipped (on unspecified..)
LOG_WARN(
"GxEPD2 version has not been modified to support async refresh; using fallback behavior. Please update lib_deps in "
"variant's platformio.ini file");
EInkDisplay::endUpdate();
}
#endif
}
// Assess situation, pick a refresh type
bool EInkDynamicDisplay::determineMode()
{
checkInitialized();
checkForPromotion();
#if defined(HAS_EINK_ASYNCFULL)
checkBusyAsyncRefresh();
#endif
checkRateLimiting();
// If too soon for a new frame, or display busy, abort early
if (refresh == SKIPPED)
return false; // No refresh
// -- New frame is due --
resetRateLimiting(); // Once determineMode() ends, will have to wait again
hashImage(); // Generate here, so we can still copy it to previousImageHash, even if we skip the comparison check
LOG_DEBUG("determineMode(): "); // Begin log entry
// Once mode determined, any remaining checks will bypass
checkCosmetic();
checkDemandingFast();
checkFrameMatchesPrevious();
checkConsecutiveFastRefreshes();
#ifdef EINK_LIMIT_GHOSTING_PX
checkExcessiveGhosting();
#endif
checkFastRequested();
if (refresh == UNSPECIFIED)
LOG_WARN("There was a flaw in the determineMode() logic");
// -- Decision has been reached --
applyRefreshMode();
adjustRefreshCounters();
#ifdef EINK_LIMIT_GHOSTING_PX
// Full refresh clears any ghosting
if (refresh == FULL)
resetGhostPixelTracking();
#endif
// Return - call a refresh or not?
if (refresh == SKIPPED)
return false; // Don't trigger a refresh
else
return true; // Do trigger a refresh
}
// Is this the very first frame?
void EInkDynamicDisplay::checkInitialized()
{
if (!initialized) {
// Undo GxEPD2_BW::partialWindow(), if set by developer in EInkDisplay::connect()
configForFullRefresh();
// Clear any existing image, so we can draw logo with fast-refresh, but also to set GxEPD2_EPD::_initial_write
adafruitDisplay->clearScreen();
LOG_DEBUG("initialized, ");
initialized = true;
// Use a fast-refresh for the next frame; no skipping or else blank screen when waking from deep sleep
addFrameFlag(DEMAND_FAST);
}
}
// Was a frame skipped (rate, display busy) that should have been a FAST refresh?
void EInkDynamicDisplay::checkForPromotion()
{
// If a frame was skipped (rate, display busy), then promote a BACKGROUND frame
// Because we DID want a RESPONSIVE/COSMETIC/DEMAND_FULL frame last time, we just didn't get it
switch (previousReason) {
case ASYNC_REFRESH_BLOCKED_DEMANDFAST:
addFrameFlag(DEMAND_FAST);
break;
case ASYNC_REFRESH_BLOCKED_COSMETIC:
addFrameFlag(COSMETIC);
break;
case ASYNC_REFRESH_BLOCKED_RESPONSIVE:
case EXCEEDED_RATELIMIT_FAST:
addFrameFlag(RESPONSIVE);
break;
default:
break;
}
}
// Is it too soon for another frame of this type?
void EInkDynamicDisplay::checkRateLimiting()
{
// Sanity check: millis() overflow - just let the update run..
if (previousRunMs > millis())
return;
// Skip update: too soon for BACKGROUND
if (frameFlags == BACKGROUND) {
if (Throttle::isWithinTimespanMs(previousRunMs, 30000)) {
refresh = SKIPPED;
reason = EXCEEDED_RATELIMIT_FULL;
return;
}
}
// No rate-limit for these special cases
if (frameFlags & COSMETIC || frameFlags & DEMAND_FAST)
return;
// Skip update: too soon for RESPONSIVE
if (frameFlags & RESPONSIVE) {
if (Throttle::isWithinTimespanMs(previousRunMs, 1000)) {
refresh = SKIPPED;
reason = EXCEEDED_RATELIMIT_FAST;
LOG_DEBUG("refresh=SKIPPED, reason=EXCEEDED_RATELIMIT_FAST, frameFlags=0x%x", frameFlags);
return;
}
}
}
// Is this frame COSMETIC (splash screens?)
void EInkDynamicDisplay::checkCosmetic()
{
// If a decision was already reached, don't run the check
if (refresh != UNSPECIFIED)
return;
// A full refresh is requested for cosmetic purposes: we have a decision
if (frameFlags & COSMETIC) {
refresh = FULL;
reason = FLAGGED_COSMETIC;
LOG_DEBUG("refresh=FULL, reason=FLAGGED_COSMETIC, frameFlags=0x%x", frameFlags);
}
}
// Is this a one-off special circumstance, where we REALLY want a fast refresh?
void EInkDynamicDisplay::checkDemandingFast()
{
// If a decision was already reached, don't run the check
if (refresh != UNSPECIFIED)
return;
// A fast refresh is demanded: we have a decision
if (frameFlags & DEMAND_FAST) {
refresh = FAST;
reason = FLAGGED_DEMAND_FAST;
LOG_DEBUG("refresh=FAST, reason=FLAGGED_DEMAND_FAST, frameFlags=0x%x", frameFlags);
}
}
// Does the new frame match the currently displayed image?
void EInkDynamicDisplay::checkFrameMatchesPrevious()
{
// If a decision was already reached, don't run the check
if (refresh != UNSPECIFIED)
return;
// If frame is *not* a duplicate, abort the check
if (imageHash != previousImageHash)
return;
#if !defined(EINK_BACKGROUND_USES_FAST)
// If BACKGROUND, and last update was FAST: redraw the same image in FULL (for display health + image quality)
if (frameFlags == BACKGROUND && fastRefreshCount > 0) {
refresh = FULL;
reason = REDRAW_WITH_FULL;
LOG_DEBUG("refresh=FULL, reason=REDRAW_WITH_FULL, frameFlags=0x%x", frameFlags);
return;
}
#endif
// Not redrawn, not COSMETIC, not DEMAND_FAST
refresh = SKIPPED;
reason = FRAME_MATCHED_PREVIOUS;
LOG_DEBUG("refresh=SKIPPED, reason=FRAME_MATCHED_PREVIOUS, frameFlags=0x%x", frameFlags);
}
// Have too many fast-refreshes occurred consecutively, since last full refresh?
void EInkDynamicDisplay::checkConsecutiveFastRefreshes()
{
// If a decision was already reached, don't run the check
if (refresh != UNSPECIFIED)
return;
// Bypass limit if UNLIMITED_FAST mode is active
if (frameFlags & UNLIMITED_FAST) {
refresh = FAST;
reason = NO_OBJECTIONS;
LOG_DEBUG("refresh=FAST, reason=UNLIMITED_FAST_MODE_ACTIVE, frameFlags=0x%x", frameFlags);
return;
}
// If too many FAST refreshes consecutively - force a FULL refresh
if (fastRefreshCount >= EINK_LIMIT_FASTREFRESH) {
refresh = FULL;
reason = EXCEEDED_LIMIT_FASTREFRESH;
LOG_DEBUG("refresh=FULL, reason=EXCEEDED_LIMIT_FASTREFRESH, frameFlags=0x%x", frameFlags);
}
}
// No objections, we can perform fast-refresh, if desired
void EInkDynamicDisplay::checkFastRequested()
{
if (refresh != UNSPECIFIED)
return;
if (frameFlags == BACKGROUND) {
#ifdef EINK_BACKGROUND_USES_FAST
// If we want BACKGROUND to use fast. (FULL only when a limit is hit)
refresh = FAST;
reason = BACKGROUND_USES_FAST;
LOG_DEBUG("refresh=FAST, reason=BACKGROUND_USES_FAST, fastRefreshCount=%lu, frameFlags=0x%x", fastRefreshCount,
frameFlags);
#else
// If we do want to use FULL for BACKGROUND updates
refresh = FULL;
reason = FLAGGED_BACKGROUND;
LOG_DEBUG("refresh=FULL, reason=FLAGGED_BACKGROUND");
#endif
}
// Sanity: confirm that we did ask for a RESPONSIVE frame.
if (frameFlags & RESPONSIVE) {
refresh = FAST;
reason = NO_OBJECTIONS;
LOG_DEBUG("refresh=FAST, reason=NO_OBJECTIONS, fastRefreshCount=%lu, frameFlags=0x%x", fastRefreshCount, frameFlags);
}
}
// Reset the timer used for rate-limiting
void EInkDynamicDisplay::resetRateLimiting()
{
previousRunMs = millis();
}
// Generate a hash of this frame, to compare against previous update
void EInkDynamicDisplay::hashImage()
{
imageHash = 0;
// Sum all bytes of the image buffer together
for (uint16_t b = 0; b < (displayWidth / 8) * displayHeight; b++) {
imageHash ^= buffer[b] << b;
}
}
// Store the results of determineMode() for future use, and reset for next call
void EInkDynamicDisplay::storeAndReset()
{
previousFrameFlags = frameFlags;
previousRefresh = refresh;
previousReason = reason;
// Only store image hash if the display will update
if (refresh != SKIPPED) {
previousImageHash = imageHash;
}
frameFlags = BACKGROUND;
refresh = UNSPECIFIED;
}
#ifdef EINK_LIMIT_GHOSTING_PX
// Count how many ghost pixels the new image will display
void EInkDynamicDisplay::countGhostPixels()
{
// If a decision was already reached, don't run the check
if (refresh != UNSPECIFIED)
return;
// Start a new count
ghostPixelCount = 0;
// Check new image, bit by bit, for any white pixels at locations marked "dirty"
for (uint16_t i = 0; i < displayBufferSize; i++) {
for (uint8_t bit = 0; bit < 7; bit++) {
const bool dirty = (dirtyPixels[i] >> bit) & 1; // Has pixel location been drawn to since full-refresh?
const bool shouldBeBlank = !((buffer[i] >> bit) & 1); // Is pixel location white in the new image?
// If pixel is (or has been) black since last full-refresh, and now is white: ghosting
if (dirty && shouldBeBlank)
ghostPixelCount++;
// Update the dirty status for this pixel - will this location become a ghost if set white in future?
if (!dirty && !shouldBeBlank)
dirtyPixels[i] |= (1 << bit);
}
}
LOG_DEBUG("ghostPixels=%hu, ", ghostPixelCount);
}
// Check if ghost pixel count exceeds the defined limit
void EInkDynamicDisplay::checkExcessiveGhosting()
{
// If a decision was already reached, don't run the check
if (refresh != UNSPECIFIED)
return;
countGhostPixels();
// If too many ghost pixels, select full refresh
if (ghostPixelCount > EINK_LIMIT_GHOSTING_PX) {
refresh = FULL;
reason = EXCEEDED_GHOSTINGLIMIT;
LOG_DEBUG("refresh=FULL, reason=EXCEEDED_GHOSTINGLIMIT, frameFlags=0x%x", frameFlags);
}
}
// Clear the dirty pixels array. Call when full-refresh cleans the display.
void EInkDynamicDisplay::resetGhostPixelTracking()
{
// Copy the current frame into dirtyPixels[] from the display buffer
memcpy(dirtyPixels.get(), EInkDisplay::buffer, EInkDisplay::displayBufferSize);
}
#endif // EINK_LIMIT_GHOSTING_PX
// Handle any asyc tasks
void EInkDynamicDisplay::onNotify(uint32_t notification)
{
// Which task
switch (notification) {
case DUE_POLL_ASYNCREFRESH:
pollAsyncRefresh();
break;
}
}
#ifdef HAS_EINK_ASYNCFULL
// Public: wait for an refresh already in progress, then run the post-update code. See Screen::setScreensaverFrames()
void EInkDynamicDisplay::joinAsyncRefresh()
{
// If no async refresh running, nothing to do
if (!asyncRefreshRunning)
return;
LOG_DEBUG("Join an async refresh in progress");
// Continually poll the BUSY pin
while (adafruitDisplay->epd2.isBusy())
yield();
// If asyncRefreshRunning flag is still set, but display's BUSY pin reports the refresh is done
adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code
EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override)
asyncRefreshRunning = false; // Unset the flag
LOG_DEBUG("Refresh complete");
// Note: this code only works because of a modification to meshtastic/GxEPD2.
// It is only equipped to intercept calls to nextPage()
}
// Called from NotifiedWorkerThread. Run the post-update code if the hardware is ready
void EInkDynamicDisplay::pollAsyncRefresh()
{
// In theory, this condition should never be met
if (!asyncRefreshRunning)
return;
// Still running, check back later
if (adafruitDisplay->epd2.isBusy()) {
// Schedule next call of pollAsyncRefresh()
NotifiedWorkerThread::notifyLater(intervalPollAsyncRefresh, DUE_POLL_ASYNCREFRESH, true);
return;
}
// If asyncRefreshRunning flag is still set, but display's BUSY pin reports the refresh is done
adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code
EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override)
asyncRefreshRunning = false; // Unset the flag
LOG_DEBUG("Async full-refresh complete");
// Note: this code only works because of a modification to meshtastic/GxEPD2.
// It is only equipped to intercept calls to nextPage()
}
// Check the status of "async full-refresh"; skip if running
void EInkDynamicDisplay::checkBusyAsyncRefresh()
{
// No refresh taking place, continue with determineMode()
if (!asyncRefreshRunning)
return;
// Full refresh still running
if (adafruitDisplay->epd2.isBusy()) {
// No refresh
refresh = SKIPPED;
// Set the reason, marking what type of frame we're skipping
if (frameFlags & DEMAND_FAST)
reason = ASYNC_REFRESH_BLOCKED_DEMANDFAST;
else if (frameFlags & COSMETIC)
reason = ASYNC_REFRESH_BLOCKED_COSMETIC;
else if (frameFlags & RESPONSIVE)
reason = ASYNC_REFRESH_BLOCKED_RESPONSIVE;
else
reason = ASYNC_REFRESH_BLOCKED_BACKGROUND;
return;
}
// Async refresh appears to have stopped, but wasn't caught by onNotify()
else
pollAsyncRefresh(); // Check (and terminate) the async refresh manually
}
// Hold control while an async refresh runs
void EInkDynamicDisplay::awaitRefresh()
{
// Continually poll the BUSY pin
while (adafruitDisplay->epd2.isBusy())
yield();
// End the full-refresh process
adafruitDisplay->endAsyncFull(); // Run the end of nextPage() code
EInkDisplay::endUpdate(); // Run base-class code to finish off update (NOT our derived class override)
asyncRefreshRunning = false; // Unset the flag
}
#endif // HAS_EINK_ASYNCFULL
#endif // USE_EINK_DYNAMICDISPLAY
-155
View File
@@ -1,155 +0,0 @@
#pragma once
#include "configuration.h"
#include <memory>
#if defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
#include "EInkDisplay2.h"
#include "GxEPD2_BW.h"
#include "concurrency/NotifiedWorkerThread.h"
/*
Derives from the EInkDisplay adapter class.
Accepts suggestions from Screen class about frame type.
Determines which refresh type is most suitable.
(Full, Fast, Skip)
*/
class EInkDynamicDisplay : public EInkDisplay, protected concurrency::NotifiedWorkerThread
{
public:
// Constructor
// ( Parameters unused, passed to EInkDisplay. Maintains compatibility OLEDDisplay class )
EInkDynamicDisplay(uint8_t address, int sda, int scl, OLEDDISPLAY_GEOMETRY geometry, HW_I2C i2cBus);
~EInkDynamicDisplay();
// Methods to enable or disable unlimited fast refresh mode
void enableUnlimitedFastMode() { addFrameFlag(UNLIMITED_FAST); }
void disableUnlimitedFastMode() { frameFlags = (frameFlagTypes)(frameFlags & ~UNLIMITED_FAST); }
// What kind of frame is this
enum frameFlagTypes : uint8_t {
BACKGROUND = (1 << 0), // For frames via display()
RESPONSIVE = (1 << 1), // For frames via forceDisplay()
COSMETIC = (1 << 2), // For splashes
DEMAND_FAST = (1 << 3), // Special case only
BLOCKING = (1 << 4), // Modifier - block while refresh runs
UNLIMITED_FAST = (1 << 5)
};
void addFrameFlag(frameFlagTypes flag);
// Set the correct frame flag, then call universal "update()" method
void display() override;
bool forceDisplay(uint32_t msecLimit) override; // Shadows base class. Parameter and return val unused.
protected:
enum refreshTypes : uint8_t { // Which refresh operation will be used
UNSPECIFIED,
FULL,
FAST,
SKIPPED,
};
enum reasonTypes : uint8_t { // How was the decision reached
NO_OBJECTIONS,
ASYNC_REFRESH_BLOCKED_DEMANDFAST,
ASYNC_REFRESH_BLOCKED_COSMETIC,
ASYNC_REFRESH_BLOCKED_RESPONSIVE,
ASYNC_REFRESH_BLOCKED_BACKGROUND,
EXCEEDED_RATELIMIT_FAST,
EXCEEDED_RATELIMIT_FULL,
FLAGGED_COSMETIC,
FLAGGED_DEMAND_FAST,
EXCEEDED_LIMIT_FASTREFRESH,
EXCEEDED_GHOSTINGLIMIT,
FRAME_MATCHED_PREVIOUS,
BACKGROUND_USES_FAST,
FLAGGED_BACKGROUND,
REDRAW_WITH_FULL,
};
enum notificationTypes : uint8_t { // What was onNotify() called for
NONE = 0, // This behavior (NONE=0) is fixed by NotifiedWorkerThread class
DUE_POLL_ASYNCREFRESH = 1,
};
const uint32_t intervalPollAsyncRefresh = 100;
void onNotify(uint32_t notification) override; // Handle any async tasks - overrides NotifiedWorkerThread
void configForFastRefresh(); // GxEPD2 code to set fast-refresh
void configForFullRefresh(); // GxEPD2 code to set full-refresh
bool determineMode(); // Assess situation, pick a refresh type
void applyRefreshMode(); // Run any relevant GxEPD2 code, so next update will use correct refresh type
void adjustRefreshCounters(); // Update fastRefreshCount
bool update(); // Trigger the display update - determine mode, then call base class
void endOrDetach(); // Run the post-update code, or delegate it off to checkBusyAsyncRefresh()
// Checks as part of determineMode()
void checkInitialized(); // Is this the very first frame?
void checkForPromotion(); // Was a frame skipped (rate, display busy) that should have been a FAST refresh?
void checkRateLimiting(); // Is this frame too soon?
void checkCosmetic(); // Was the COSMETIC flag set?
void checkDemandingFast(); // Was the DEMAND_FAST flag set?
void checkFrameMatchesPrevious(); // Does the new frame match the existing display image?
void checkConsecutiveFastRefreshes(); // Too many fast-refreshes consecutively?
void checkFastRequested(); // Was the flag set for RESPONSIVE, or only BACKGROUND?
void resetRateLimiting(); // Set previousRunMs - this now counts as an update, for rate-limiting
void hashImage(); // Generate a hashed version of this frame, to compare against previous update
void storeAndReset(); // Keep results of determineMode() for later, tidy-up for next call
// What we are determining for this frame
frameFlagTypes frameFlags = BACKGROUND; // Frame characteristics - determineMode() input
refreshTypes refresh = UNSPECIFIED; // Refresh type - determineMode() output
reasonTypes reason = NO_OBJECTIONS; // Reason - why was refresh type used
// What happened last time determineMode() ran
frameFlagTypes previousFrameFlags = BACKGROUND; // (Previous) Frame flags
refreshTypes previousRefresh = UNSPECIFIED; // (Previous) Outcome
reasonTypes previousReason = NO_OBJECTIONS; // (Previous) Reason
bool initialized = false; // Have we drawn at least one frame yet?
uint32_t previousRunMs = -1; // When did determineMode() last run (rather than rejecting for rate-limiting)
uint32_t imageHash = 0; // Hash of the current frame. Don't bother updating if nothing has changed!
uint32_t previousImageHash = 0; // Hash of the previous update's frame
uint32_t fastRefreshCount = 0; // How many fast-refreshes consecutively since last full refresh?
refreshTypes currentConfig = FULL; // Which refresh type is GxEPD2 currently configured for
// Optional - track ghosting, pixel by pixel
// May 2024: no longer used by any display. Kept for possible future use.
#ifdef EINK_LIMIT_GHOSTING_PX
void countGhostPixels(); // Count any pixels which have moved from black to white since last full-refresh
void checkExcessiveGhosting(); // Check if ghosting exceeds defined limit
void resetGhostPixelTracking(); // Clear the dirty pixels array. Call when full-refresh cleans the display.
std::unique_ptr<uint8_t[]> dirtyPixels; // Any pixels that have been black since last full-refresh (dynamically allocated mem)
uint32_t ghostPixelCount = 0; // Number of pixels with problematic ghosting. Retained here for LOG_DEBUG use
#endif
// Conditional - async full refresh - only with modified meshtastic/GxEPD2
#if defined(HAS_EINK_ASYNCFULL)
public:
void joinAsyncRefresh(); // Main thread joins an async refresh already in progress. Blocks, then runs post-update code
protected:
void pollAsyncRefresh(); // Run the post-update code if the hardware is ready
void checkBusyAsyncRefresh(); // Check if display is busy running an async full-refresh (rejecting new frames)
void awaitRefresh(); // Hold control while an async refresh runs
void endUpdate() override {} // Disable base-class behavior of running post-update immediately after forceDisplay()
bool asyncRefreshRunning = false; // Flag, checked by checkBusyAsyncRefresh()
#else
public:
void joinAsyncRefresh() {} // Dummy method
protected:
void pollAsyncRefresh() {} // Dummy method. In theory, not reachable
#endif
};
// Hide the ugly casts used in Screen.cpp
#define EINK_ADD_FRAMEFLAG(display, flag) static_cast<EInkDynamicDisplay *>(display)->addFrameFlag(EInkDynamicDisplay::flag)
#define EINK_JOIN_ASYNCREFRESH(display) static_cast<EInkDynamicDisplay *>(display)->joinAsyncRefresh()
#else // !USE_EINK_DYNAMICDISPLAY
// Dummy-macro, removes the need for include guards
#define EINK_ADD_FRAMEFLAG(display, flag)
#define EINK_JOIN_ASYNCREFRESH(display)
#endif
-427
View File
@@ -1,427 +0,0 @@
#include "EInkParallelDisplay.h"
#ifdef USE_EINK_PARALLELDISPLAY
#include "Wire.h"
#include "variant.h"
#include <Arduino.h>
#include <atomic>
#include <stdlib.h>
#include <string.h>
#include "FastEPD.h"
// Thresholds for choosing partial vs full update
#ifndef EPD_PARTIAL_THRESHOLD_ROWS
#define EPD_PARTIAL_THRESHOLD_ROWS 128 // if changed region <= this many rows, prefer partial
#endif
#ifndef EPD_FULLSLOW_PERIOD
#define EPD_FULLSLOW_PERIOD 100 // every N full updates do a slow (CLEAR_SLOW) full refresh
#endif
#ifndef EPD_RESPONSIVE_MIN_MS
#define EPD_RESPONSIVE_MIN_MS 1000 // simple rate-limit (ms) for responsive updates
#endif
EInkParallelDisplay::EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rot) : epaper(nullptr), rotation(rot)
{
LOG_INFO("init EInkParallelDisplay");
// Set dimensions in OLEDDisplay base class
this->geometry = GEOMETRY_RAWMODE;
this->displayWidth = width;
this->displayHeight = height;
// Round shortest side up to nearest byte, to prevent truncation causing an undersized buffer
uint16_t shortSide = min(width, height);
uint16_t longSide = max(width, height);
if (shortSide % 8 != 0)
shortSide = (shortSide | 7) + 1;
this->displayBufferSize = longSide * (shortSide / 8);
#ifdef EINK_LIMIT_GHOSTING_PX
// allocate dirty pixel buffer same size as epaper buffers (rowBytes * height)
size_t rowBytes = (this->displayWidth + 7) / 8;
dirtyPixelsSize = rowBytes * this->displayHeight;
dirtyPixels = (uint8_t *)calloc(dirtyPixelsSize, 1);
ghostPixelCount = 0;
#endif
}
EInkParallelDisplay::~EInkParallelDisplay()
{
#ifdef EINK_LIMIT_GHOSTING_PX
if (dirtyPixels) {
free(dirtyPixels);
dirtyPixels = nullptr;
}
#endif
// If an async full update is running, wait for it to finish
if (asyncFullRunning.load()) {
// wait a short while for task to finish
for (int i = 0; i < 50 && asyncFullRunning.load(); ++i) {
delay(50);
}
if (asyncTaskHandle) {
// Let it finish or delete it
vTaskDelete(asyncTaskHandle);
asyncTaskHandle = nullptr;
}
}
delete epaper;
}
/*
* Called by the OLEDDisplay::init() path.
*/
bool EInkParallelDisplay::connect()
{
LOG_INFO("Do EPD init");
if (!epaper) {
epaper = new FASTEPD;
#if defined(T5_S3_EPAPER_PRO_V1)
epaper->initPanel(BB_PANEL_LILYGO_T5PRO, 28000000);
#elif defined(T5_S3_EPAPER_PRO_V2)
epaper->initPanel(BB_PANEL_LILYGO_T5PRO_V2, 28000000);
// initialize all port 0 pins (0-7) as outputs / HIGH
for (int i = 0; i < 8; i++) {
epaper->ioPinMode(i, OUTPUT);
epaper->ioWrite(i, HIGH);
}
#else
#error "unsupported EPD device!"
#endif
}
// epaper->setRotation(rotation); // does not work, messes up width/height
epaper->setMode(BB_MODE_1BPP);
epaper->clearWhite();
epaper->fullUpdate(true);
#ifdef EINK_LIMIT_GHOSTING_PX
// After a full/clear the dirty tracking should be reset
resetGhostPixelTracking();
#endif
return true;
}
/*
* sendCommand - simple passthrough (not required for epd_driver-based path)
*/
void EInkParallelDisplay::sendCommand(uint8_t com)
{
LOG_DEBUG("EInkParallelDisplay::sendCommand %d", (int)com);
}
/*
* Start a background task that will perform a blocking fullUpdate(). This lets
* display() return quickly while the heavy refresh runs in the background.
*/
void EInkParallelDisplay::startAsyncFullUpdate(int clearMode)
{
if (asyncFullRunning.load())
return; // already running
asyncFullRunning.store(true);
// pass 'this' as parameter
BaseType_t rc = xTaskCreatePinnedToCore(EInkParallelDisplay::asyncFullUpdateTask, "epd_full", 4096 / sizeof(StackType_t),
this, 2, &asyncTaskHandle,
#if CONFIG_FREERTOS_UNICORE
0
#else
1
#endif
);
if (rc != pdPASS) {
LOG_WARN("Failed to create async full-update task, falling back to blocking update");
epaper->fullUpdate(clearMode, false);
epaper->backupPlane();
asyncFullRunning.store(false);
asyncTaskHandle = nullptr;
}
}
/*
* FreeRTOS task entry: runs the full update and then backs up plane.
*/
void EInkParallelDisplay::asyncFullUpdateTask(void *pvParameters)
{
EInkParallelDisplay *self = static_cast<EInkParallelDisplay *>(pvParameters);
if (!self) {
vTaskDelete(nullptr);
return;
}
// choose CLEAR_SLOW occasionally
int clearMode = CLEAR_FAST;
if (self->fastRefreshCount >= EPD_FULLSLOW_PERIOD) {
clearMode = CLEAR_SLOW;
self->fastRefreshCount = 0;
} else {
// when running async full, treat it as a full so reset fast count
self->fastRefreshCount = 0;
}
self->epaper->fullUpdate(clearMode, false);
self->epaper->backupPlane();
#ifdef EINK_LIMIT_GHOSTING_PX
// A full refresh clears ghosting state
self->resetGhostPixelTracking();
#endif
self->asyncFullRunning.store(false);
self->asyncTaskHandle = nullptr;
// delete this task
vTaskDelete(nullptr);
}
/*
* Convert the OLEDDisplay buffer (vertical byte layout) into the 1bpp horizontal-bytes
* buffer used by the FASTEPD library. For performance we write directly into FASTEPD's
* currentBuffer() while comparing against previousBuffer() to detect changed rows.
* After conversion we call FASTEPD::partialUpdate() or FASTEPD::fullUpdate() according
* to a heuristic so only the minimal region is refreshed.
*/
void EInkParallelDisplay::display(void)
{
const uint16_t w = this->displayWidth;
const uint16_t h = this->displayHeight;
// Simple rate limiting: avoid very-frequent responsive updates
uint32_t nowMs = millis();
if (lastUpdateMs != 0 && (nowMs - lastUpdateMs) < EPD_RESPONSIVE_MIN_MS) {
LOG_DEBUG("rate-limited, skipping update");
return;
}
// bytes per row in epd format (one byte = 8 horizontal pixels)
const uint32_t rowBytes = (w + 7) / 8;
// Get pointers to internal buffers
uint8_t *cur = epaper->currentBuffer();
const uint8_t *prev = epaper->previousBuffer(); // may be NULL on first init
// Track changed row range while converting
int newTop = h; // min changed row (initialized to out-of-range)
int newBottom = -1; // max changed row
#ifdef FAST_EPD_PARTIAL_UPDATE_BUG
// Track changed byte column range (for clipped fullUpdate fallback)
int newLeftByte = (int)rowBytes;
int newRightByte = -1;
#endif
// Compute a quick hash of the incoming OLED buffer (so we can skip identical frames)
uint32_t imageHash = 0;
uint32_t bufBytes = (w / 8) * h; // vertical-byte layout size
for (uint32_t bi = 0; bi < bufBytes; ++bi) {
imageHash ^= ((uint32_t)buffer[bi]) << (bi & 31);
}
if (imageHash == previousImageHash) {
// LOG_DEBUG("image identical to previous, skipping update");
return;
}
#ifdef EINK_LIMIT_GHOSTING_PX
// reset ghost count for this conversion pass; we'll mark bits that change
ghostPixelCount = 0;
#endif
// Convert: OLED buffer layout -> FASTEPD 1bpp horizontal-bytes layout into cur,
// comparing against prev when available to detect changes.
for (uint32_t y = 0; y < h; ++y) {
const uint32_t base = (y >> 3) * w; // (y/8) * width
const uint8_t bitMask = (uint8_t)(1u << (y & 7)); // mask for this row in vertical-byte layout
const uint32_t rowBase = y * rowBytes;
// process full 8-pixel bytes
for (uint32_t xb = 0; xb < rowBytes; ++xb) {
uint32_t x0 = xb * 8;
// read up to 8 source bytes (vertical-byte per column)
uint8_t b0 = (x0 + 0 < w) ? buffer[base + x0 + 0] : 0;
uint8_t b1 = (x0 + 1 < w) ? buffer[base + x0 + 1] : 0;
uint8_t b2 = (x0 + 2 < w) ? buffer[base + x0 + 2] : 0;
uint8_t b3 = (x0 + 3 < w) ? buffer[base + x0 + 3] : 0;
uint8_t b4 = (x0 + 4 < w) ? buffer[base + x0 + 4] : 0;
uint8_t b5 = (x0 + 5 < w) ? buffer[base + x0 + 5] : 0;
uint8_t b6 = (x0 + 6 < w) ? buffer[base + x0 + 6] : 0;
uint8_t b7 = (x0 + 7 < w) ? buffer[base + x0 + 7] : 0;
// build output byte: MSB = leftmost pixel
uint8_t out = 0;
out |= (uint8_t)((b0 & bitMask) ? 0x80 : 0x00);
out |= (uint8_t)((b1 & bitMask) ? 0x40 : 0x00);
out |= (uint8_t)((b2 & bitMask) ? 0x20 : 0x00);
out |= (uint8_t)((b3 & bitMask) ? 0x10 : 0x00);
out |= (uint8_t)((b4 & bitMask) ? 0x08 : 0x00);
out |= (uint8_t)((b5 & bitMask) ? 0x04 : 0x00);
out |= (uint8_t)((b6 & bitMask) ? 0x02 : 0x00);
out |= (uint8_t)((b7 & bitMask) ? 0x01 : 0x00);
// handle partial byte at end of row by masking off invalid bits
uint8_t mask = 0xFF;
uint32_t bitsRemain = (w > x0) ? (w - x0) : 0;
if (bitsRemain > 0 && bitsRemain < 8) {
mask = (uint8_t)(0xFF << (8 - bitsRemain));
out &= mask;
}
// invert to FASTEPD polarity
out = (~out) & mask;
uint32_t pos = rowBase + xb;
uint8_t prevVal = prev ? (prev[pos] & mask) : 0x00;
// Consider this byte changed if previous buffer differs (or prev is null)
bool changed = (prev == nullptr) || (prevVal != out);
#ifdef EINK_LIMIT_GHOSTING_PX
if (changed && prev)
markDirtyBits(prev, pos, mask, out);
#endif
// mark row changed only if the previous buffer differs
if (changed) {
if (y < (uint32_t)newTop)
newTop = y;
if ((int)y > newBottom)
newBottom = y;
#ifdef FAST_EPD_PARTIAL_UPDATE_BUG
// record changed column bytes
if ((int)xb < newLeftByte)
newLeftByte = (int)xb;
if ((int)xb > newRightByte)
newRightByte = (int)xb;
#endif
}
// Always write the computed value into the current buffer (avoid leaving stale bytes)
cur[pos] = (cur[pos] & ~mask) | out;
}
}
// If nothing changed, avoid any panel update
if (newBottom < 0) {
LOG_DEBUG("no pixel changes detected, skipping update (conv)");
previousImageHash = imageHash; // still remember that frame
return;
}
// Choose partial vs full update using heuristic
// Decide if we should force a full update after many fast updates
bool forceFull = (fastRefreshCount >= EPD_FULLSLOW_PERIOD);
#ifdef EINK_LIMIT_GHOSTING_PX
// If ghost pixels exceed limit, force a full update to clear ghosting
if (ghostPixelCount > ghostPixelLimit) {
LOG_WARN("ghost pixels %u > limit %u, forcing full refresh", ghostPixelCount, ghostPixelLimit);
forceFull = true;
}
#endif
// Compute pixel bounds from newTop/newBottom
int startRow = (newTop / 8) * 8;
int endRow = (newBottom / 8) * 8 + 7;
LOG_DEBUG("EPD update rows=%d..%d alignedRows=%d..%d rowBytes=%u", newTop, newBottom, startRow, endRow, rowBytes);
if (epaper->getMode() == BB_MODE_1BPP && !forceFull && (newBottom - newTop) <= EPD_PARTIAL_THRESHOLD_ROWS) {
// Prefer partial update path if driver is reliable; otherwise use clipped fullUpdate fallback.
#ifdef FAST_EPD_PARTIAL_UPDATE_BUG
// Workaround for FastEPD partial update bug: use clipped fullUpdate instead
// Build a pixel rectangle for a clipped fullUpdate using the changed columns
int startCol = (newLeftByte <= newRightByte) ? (newLeftByte * 8) : 0;
int endCol = (newLeftByte <= newRightByte) ? ((newRightByte + 1) * 8 - 1) : (w - 1);
BB_RECT rect{startCol, startRow, endCol - startCol + 1, endRow - startRow + 1};
// LOG_DEBUG("Using clipped fullUpdate rect x=%d y=%d w=%d h=%d", rect.x, rect.y, rect.w, rect.h);
epaper->fullUpdate(CLEAR_FAST, false, &rect);
#else
// Use rows for partial update
LOG_DEBUG("calling partialUpdate startRow=%d endRow=%d", startRow, endRow);
epaper->partialUpdate(true, startRow, endRow);
#endif
epaper->backupPlane();
fastRefreshCount++;
} else {
// Full update: run async if possible (startAsyncFullUpdate will fall back to blocking)
startAsyncFullUpdate(forceFull ? CLEAR_SLOW : CLEAR_FAST);
}
lastUpdateMs = millis();
previousImageHash = imageHash;
// Keep same behavior as before
lastDrawMsec = millis();
}
#ifdef EINK_LIMIT_GHOSTING_PX
// markDirtyBits: mark per-bit dirty flags and update ghostPixelCount
void EInkParallelDisplay::markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out)
{
// defensive: need dirtyPixels allocated and prevBuf valid
if (!dirtyPixels || !prevBuf)
return;
// 'out' is in FASTEPD polarity (1 = black, 0 = white)
uint8_t newBlack = out & mask; // bits that will be black now
uint8_t newWhite = (~out) & mask; // bits that will be white now
// previously recorded dirty bits for this byte
uint8_t before = dirtyPixels[pos];
// Ghost bits: bits that were previously marked dirty and are now being driven white
uint8_t ghostBits = before & newWhite;
if (ghostBits) {
ghostPixelCount += __builtin_popcount((unsigned)ghostBits);
}
// Only mark bits dirty when they turn black now (accumulate until a full refresh)
uint8_t newlyDirty = newBlack & (~before);
if (newlyDirty) {
dirtyPixels[pos] |= newlyDirty;
}
}
// reset ghost tracking (call after a full refresh)
void EInkParallelDisplay::resetGhostPixelTracking()
{
if (!dirtyPixels)
return;
memset(dirtyPixels, 0, dirtyPixelsSize);
ghostPixelCount = 0;
}
#endif
/*
* forceDisplay: use lastDrawMsec
*/
bool EInkParallelDisplay::forceDisplay(uint32_t msecLimit)
{
uint32_t now = millis();
if (lastDrawMsec == 0 || (now - lastDrawMsec) > msecLimit) {
display();
return true;
}
return false;
}
void EInkParallelDisplay::endUpdate()
{
{
// ensure any async full update is started/completed
if (asyncFullRunning.load()) {
// nothing to do; background task will run and call backupPlane when done
} else {
epaper->fullUpdate(CLEAR_FAST, false);
epaper->backupPlane();
#ifdef EINK_LIMIT_GHOSTING_PX
resetGhostPixelTracking();
#endif
}
}
}
#endif
-69
View File
@@ -1,69 +0,0 @@
#pragma once
#include "configuration.h"
#ifdef USE_EINK_PARALLELDISPLAY
#include <OLEDDisplay.h>
#include <atomic>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
class FASTEPD;
/**
* Adapter for E-Ink 8-bit parallel displays (EPD), specifically devices supported by FastEPD library
*/
class EInkParallelDisplay : public OLEDDisplay
{
public:
enum EpdRotation {
EPD_ROT_LANDSCAPE = 0,
EPD_ROT_PORTRAIT = 90,
EPD_ROT_INVERTED_LANDSCAPE = 180,
EPD_ROT_INVERTED_PORTRAIT = 270,
};
EInkParallelDisplay(uint16_t width, uint16_t height, EpdRotation rotation);
virtual ~EInkParallelDisplay();
// OLEDDisplay virtuals
bool connect() override;
void sendCommand(uint8_t com) override;
int getBufferOffset(void) override { return 0; }
void display(void) override;
bool forceDisplay(uint32_t msecLimit = 1000);
void endUpdate();
protected:
uint32_t lastDrawMsec = 0;
FASTEPD *epaper;
private:
// Async full-refresh support
std::atomic<bool> asyncFullRunning{false};
TaskHandle_t asyncTaskHandle = nullptr;
void startAsyncFullUpdate(int clearMode);
static void asyncFullUpdateTask(void *pvParameters);
#ifdef EINK_LIMIT_GHOSTING_PX
// helpers
void resetGhostPixelTracking();
void markDirtyBits(const uint8_t *prevBuf, uint32_t pos, uint8_t mask, uint8_t out);
void countGhostPixelsAndMaybePromote(int &newTop, int &newBottom, bool &forceFull);
// per-bit dirty buffer (same format as epaper buffers): one bit == one pixel
uint8_t *dirtyPixels = nullptr;
size_t dirtyPixelsSize = 0;
uint32_t ghostPixelCount = 0;
uint32_t ghostPixelLimit = EINK_LIMIT_GHOSTING_PX;
#endif
EpdRotation rotation;
uint32_t previousImageHash = 0;
uint32_t lastUpdateMs = 0;
int fastRefreshCount = 0;
};
#endif
-135
View File
@@ -1,135 +0,0 @@
// Wrapper class for GxEPD2_BW
// Generic signature at build-time, so that we can detect display model at run-time
// Workaround for issue of GxEPD2_BW objects not having a shared base class
// Only exposes methods which we are actually using
template <typename Driver0, typename Driver1> class GxEPD2_Multi
{
public:
void drawPixel(int16_t x, int16_t y, uint16_t color)
{
if (which == 0)
driver0->drawPixel(x, y, color);
else
driver1->drawPixel(x, y, color);
}
bool nextPage()
{
if (which == 0)
return driver0->nextPage();
else
return driver1->nextPage();
}
void hibernate()
{
if (which == 0)
driver0->hibernate();
else
driver1->hibernate();
}
void init(uint32_t serial_diag_bitrate = 0)
{
if (which == 0)
driver0->init(serial_diag_bitrate);
else
driver1->init(serial_diag_bitrate);
}
void init(uint32_t serial_diag_bitrate, bool initial, uint16_t reset_duration = 20, bool pulldown_rst_mode = false)
{
if (which == 0)
driver0->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode);
else
driver1->init(serial_diag_bitrate, initial, reset_duration, pulldown_rst_mode);
}
void setRotation(uint8_t x)
{
if (which == 0)
driver0->setRotation(x);
else
driver1->setRotation(x);
}
void setPartialWindow(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
if (which == 0)
driver0->setPartialWindow(x, y, w, h);
else
driver1->setPartialWindow(x, y, w, h);
}
void setFullWindow()
{
if (which == 0)
driver0->setFullWindow();
else
driver1->setFullWindow();
}
int16_t width()
{
if (which == 0)
return driver0->width();
else
return driver1->width();
}
int16_t height()
{
if (which == 0)
return driver0->height();
else
return driver1->height();
}
void clearScreen(uint8_t value = 0xFF)
{
if (which == 0)
driver0->clearScreen();
else
driver1->clearScreen();
}
void endAsyncFull()
{
if (which == 0)
driver0->endAsyncFull();
else
driver1->endAsyncFull();
}
// Exposes methods of the GxEPD2_EPD object which is usually available as GxEPD2_BW::epd
class Epd2Wrapper
{
public:
bool isBusy() { return m_epd2->isBusy(); }
GxEPD2_EPD *m_epd2;
} epd2;
// Constructor
// Select driver by passing whichDriver as 0 or 1
GxEPD2_Multi(uint8_t whichDriver, int16_t cs, int16_t dc, int16_t rst, int16_t busy, SPIClass &spi)
{
assert(whichDriver == 0 || whichDriver == 1);
which = whichDriver;
LOG_DEBUG("GxEPD2_Multi driver: %d", which);
if (which == 0) {
driver0 = new GxEPD2_BW<Driver0, Driver0::HEIGHT>(Driver0(cs, dc, rst, busy, spi));
epd2.m_epd2 = &(driver0->epd2);
} else if (which == 1) {
driver1 = new GxEPD2_BW<Driver1, Driver1::HEIGHT>(Driver1(cs, dc, rst, busy, spi));
epd2.m_epd2 = &(driver1->epd2);
}
}
private:
uint8_t which;
GxEPD2_BW<Driver0, Driver0::HEIGHT> *driver0;
GxEPD2_BW<Driver1, Driver1::HEIGHT> *driver1;
};
+187 -34
View File
@@ -27,9 +27,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "configuration.h"
#include "meshUtils.h"
#if HAS_SCREEN
#include "EInkParallelDisplay.h"
#include <OLEDDisplay.h>
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "graphics/BaseUIEInkDisplay.h"
// Provided by each niche-enabled variant's nicheGraphics.h (defined once, in main.cpp TU).
extern NicheGraphics::BaseUIEInkDisplay *setupNicheGraphicsBaseUI();
#endif
#include "DisplayFormatters.h"
#include "TimeFormatters.h"
#include "draw/ClockRenderer.h"
@@ -50,6 +55,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "MeshService.h"
#include "MessageStore.h"
#include "RadioLibInterface.h"
#include "SPILock.h"
#include "error.h"
#include "gps/GeoCoord.h"
#include "gps/RTC.h"
@@ -442,14 +448,9 @@ Screen::Screen(ScanI2C::DeviceAddress address, meshtastic_Config_DisplayConfig_O
defined(RAK14014) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR)
dispdev = new TFTDisplay(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY) && !defined(USE_EINK_PARALLELDISPLAY)
dispdev = new EInkDisplay(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif defined(USE_EINK) && defined(USE_EINK_DYNAMICDISPLAY)
dispdev = new EInkDynamicDisplay(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
#elif defined(USE_EINK_PARALLELDISPLAY)
dispdev = new EInkParallelDisplay(EPD_WIDTH, EPD_HEIGHT, EInkParallelDisplay::EPD_ROT_PORTRAIT);
#elif defined(USE_EINK) && defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
// NicheGraphics-backed BaseUI E-Ink path. Variant provides setupNicheGraphicsBaseUI() in its nicheGraphics.h.
dispdev = setupNicheGraphicsBaseUI();
#elif defined(USE_ST7567)
dispdev = new ST7567Wire(address.address, -1, -1, geometry,
(address.port == ScanI2C::I2CPort::WIRE1) ? HW_I2C::I2C_TWO : HW_I2C::I2C_ONE);
@@ -655,6 +656,10 @@ void Screen::setup()
brightness = uiconfig.screen_brightness;
}
// Restore which frames the user has hidden (persisted across reboots).
// Must happen before the first setFrames().
loadFrameVisibility();
// Detect OLED subtype (if supported by board variant)
#ifdef AutoOLEDWire_h
if (isAUTOOled)
@@ -843,10 +848,8 @@ void Screen::forceDisplay(bool forceUiUpdate)
}
// Tell EInk class to update the display
#if defined(USE_EINK_PARALLELDISPLAY)
static_cast<EInkParallelDisplay *>(dispdev)->forceDisplay();
#elif defined(USE_EINK)
static_cast<EInkDisplay *>(dispdev)->forceDisplay();
#if defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
static_cast<NicheGraphics::BaseUIEInkDisplay *>(dispdev)->forceDisplay();
#endif
#else
// No delay between UI frame rendering
@@ -1033,12 +1036,7 @@ int32_t Screen::runOnce()
NotificationRenderer::current_notification_type != notificationTypeEnum::text_input &&
!Throttle::isWithinTimespanMs(lastScreenTransition, config.display.auto_screen_carousel_secs * 1000)) {
// If an E-Ink display struggles with fast refresh, force carousel to use full refresh instead
// Carousel is potentially a major source of E-Ink display wear
#if !defined(EINK_BACKGROUND_USES_FAST)
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC);
#endif
// Carousel rotations let BaseUIEInkDisplay's DisplayHealth debt model decide FAST vs FULL.
LOG_DEBUG("LastScreenTransition exceeded %ums transition to next frame", (millis() - lastScreenTransition));
handleOnPress();
}
@@ -1072,11 +1070,8 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
static FrameCallback screensaverFrame;
static OverlayCallback screensaverOverlay;
#if defined(HAS_EINK_ASYNCFULL) && defined(USE_EINK_DYNAMICDISPLAY)
// Join (await) a currently running async refresh, then run the post-update code.
// Avoid skipping of screensaver frame. Would otherwise be handled by NotifiedWorkerThread.
// Join (await) any currently running async refresh before drawing the screensaver frame.
EINK_JOIN_ASYNCREFRESH(dispdev);
#endif
// If: one-off screensaver frame passed as argument. Handles doDeepSleep()
if (einkScreensaver != NULL) {
@@ -1099,23 +1094,17 @@ void Screen::setScreensaverFrames(FrameCallback einkScreensaver)
updateUiFrame(ui);
} while (ui->getUiState()->lastUpdate < startUpdate);
#if defined(USE_EINK_PARALLELDISPLAY)
static_cast<EInkParallelDisplay *>(dispdev)->forceDisplay(0);
#elif defined(USE_EINK) && !defined(USE_EINK_DYNAMICDISPLAY)
// Old EInkDisplay class
static_cast<EInkDisplay *>(dispdev)->forceDisplay(0); // Screen::forceDisplay(), but override rate-limit
#if defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS)
static_cast<NicheGraphics::BaseUIEInkDisplay *>(dispdev)->forceDisplay(0);
#endif
// Prepare now for next frame, shown when display wakes
ui->setOverlays(NULL, 0); // Clear overlay
setFrames(FOCUS_PRESERVE); // Return to normal display updates, showing same frame as before screensaver, ideally
// Pick a refresh method, for when display wakes
#ifdef EINK_HASQUIRK_GHOSTING
EINK_ADD_FRAMEFLAG(dispdev, COSMETIC); // Really ugly to see ghosting from "screen paused"
#else
EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE); // Really nice to wake screen with a fast-refresh
#endif
// Pick a refresh method for when the display wakes. RESPONSIVE = FAST; DisplayHealth
// will promote to FULL on its own schedule if FAST debt has built up.
EINK_ADD_FRAMEFLAG(dispdev, RESPONSIVE);
}
#endif
@@ -1416,6 +1405,9 @@ void Screen::toggleFrameVisibility(const std::string &frameName)
if (frameName == "chirpy") {
hiddenFrames.chirpy = !hiddenFrames.chirpy;
}
// Save the new visibility state so it survives a reboot.
saveFrameVisibility();
}
bool Screen::isFrameHidden(const std::string &frameName) const
@@ -1454,6 +1446,167 @@ bool Screen::isFrameHidden(const std::string &frameName) const
return false;
}
// ---------------------------------------------------------------------------
// Frame visibility persistence
//
// The set of hideable frames varies by build (USE_EINK, HAS_GPS, ...), so we
// serialize to a fixed bitmask where each frame name owns a permanent bit
// position. Bits for frames that don't exist in the current build are simply
// left untouched, which keeps the saved file portable across firmware variants.
// ---------------------------------------------------------------------------
namespace
{
static const char *frameVisibilityFileName = "/prefs/framevis";
constexpr uint32_t FRAMEVIS_MAGIC = 0x53495646; // "FVIS" little-endian
constexpr uint8_t FRAMEVIS_VERSION = 1;
// Permanent bit assignments. Never renumber these; only append new ones.
enum FrameVisBit : uint8_t {
FVBIT_TEXT_MESSAGE = 0,
FVBIT_WAYPOINT = 1,
FVBIT_WIFI = 2,
FVBIT_SYSTEM = 3,
FVBIT_HOME = 4,
FVBIT_CLOCK = 5,
FVBIT_NODELIST_NODES = 6,
FVBIT_NODELIST_LOCATION = 7,
FVBIT_NODELIST_LASTHEARD = 8,
FVBIT_NODELIST_HOPSIGNAL = 9,
FVBIT_NODELIST_DISTANCE = 10,
FVBIT_NODELIST_BEARINGS = 11,
FVBIT_GPS = 12,
FVBIT_LORA = 13,
FVBIT_SHOW_FAVORITES = 14,
FVBIT_CHIRPY = 15,
};
struct __attribute__((packed)) FrameVisFile {
uint32_t magic;
uint8_t version;
uint32_t mask;
};
inline void setBit(uint32_t &mask, uint8_t bit, bool value)
{
if (value)
mask |= (1UL << bit);
else
mask &= ~(1UL << bit);
}
inline bool getBit(uint32_t mask, uint8_t bit)
{
return (mask & (1UL << bit)) != 0;
}
} // namespace
uint32_t Screen::packHiddenFrames() const
{
uint32_t mask = 0;
setBit(mask, FVBIT_TEXT_MESSAGE, hiddenFrames.textMessage);
setBit(mask, FVBIT_WAYPOINT, hiddenFrames.waypoint);
setBit(mask, FVBIT_WIFI, hiddenFrames.wifi);
setBit(mask, FVBIT_SYSTEM, hiddenFrames.system);
setBit(mask, FVBIT_HOME, hiddenFrames.home);
setBit(mask, FVBIT_CLOCK, hiddenFrames.clock);
#ifndef USE_EINK
setBit(mask, FVBIT_NODELIST_NODES, hiddenFrames.nodelist_nodes);
setBit(mask, FVBIT_NODELIST_LOCATION, hiddenFrames.nodelist_location);
#endif
#ifdef USE_EINK
setBit(mask, FVBIT_NODELIST_LASTHEARD, hiddenFrames.nodelist_lastheard);
setBit(mask, FVBIT_NODELIST_HOPSIGNAL, hiddenFrames.nodelist_hopsignal);
setBit(mask, FVBIT_NODELIST_DISTANCE, hiddenFrames.nodelist_distance);
#endif
#if HAS_GPS
#ifdef USE_EINK
setBit(mask, FVBIT_NODELIST_BEARINGS, hiddenFrames.nodelist_bearings);
#endif
setBit(mask, FVBIT_GPS, hiddenFrames.gps);
#endif
setBit(mask, FVBIT_LORA, hiddenFrames.lora);
setBit(mask, FVBIT_SHOW_FAVORITES, hiddenFrames.show_favorites);
setBit(mask, FVBIT_CHIRPY, hiddenFrames.chirpy);
return mask;
}
void Screen::applyHiddenFramesMask(uint32_t mask)
{
hiddenFrames.textMessage = getBit(mask, FVBIT_TEXT_MESSAGE);
hiddenFrames.waypoint = getBit(mask, FVBIT_WAYPOINT);
hiddenFrames.wifi = getBit(mask, FVBIT_WIFI);
hiddenFrames.system = getBit(mask, FVBIT_SYSTEM);
hiddenFrames.home = getBit(mask, FVBIT_HOME);
hiddenFrames.clock = getBit(mask, FVBIT_CLOCK);
#ifndef USE_EINK
hiddenFrames.nodelist_nodes = getBit(mask, FVBIT_NODELIST_NODES);
hiddenFrames.nodelist_location = getBit(mask, FVBIT_NODELIST_LOCATION);
#endif
#ifdef USE_EINK
hiddenFrames.nodelist_lastheard = getBit(mask, FVBIT_NODELIST_LASTHEARD);
hiddenFrames.nodelist_hopsignal = getBit(mask, FVBIT_NODELIST_HOPSIGNAL);
hiddenFrames.nodelist_distance = getBit(mask, FVBIT_NODELIST_DISTANCE);
#endif
#if HAS_GPS
#ifdef USE_EINK
hiddenFrames.nodelist_bearings = getBit(mask, FVBIT_NODELIST_BEARINGS);
#endif
hiddenFrames.gps = getBit(mask, FVBIT_GPS);
#endif
hiddenFrames.lora = getBit(mask, FVBIT_LORA);
hiddenFrames.show_favorites = getBit(mask, FVBIT_SHOW_FAVORITES);
hiddenFrames.chirpy = getBit(mask, FVBIT_CHIRPY);
}
void Screen::loadFrameVisibility()
{
#ifdef FSCom
spiLock->lock();
auto file = FSCom.open(frameVisibilityFileName, FILE_O_READ);
if (file) {
FrameVisFile data{};
bool ok = file.read((uint8_t *)&data, sizeof(data)) == sizeof(data) && data.magic == FRAMEVIS_MAGIC &&
data.version == FRAMEVIS_VERSION;
file.close();
spiLock->unlock();
if (ok) {
applyHiddenFramesMask(data.mask);
LOG_INFO("Loaded frame visibility (mask 0x%08x)", data.mask);
} else {
LOG_WARN("Frame visibility file invalid, keeping defaults");
}
return;
}
spiLock->unlock();
LOG_DEBUG("No saved frame visibility, using defaults");
#endif
}
void Screen::saveFrameVisibility()
{
#ifdef FSCom
spiLock->lock();
FSCom.mkdir("/prefs");
if (FSCom.exists(frameVisibilityFileName))
FSCom.remove(frameVisibilityFileName);
auto file = FSCom.open(frameVisibilityFileName, FILE_O_WRITE);
if (file) {
FrameVisFile data{};
data.magic = FRAMEVIS_MAGIC;
data.version = FRAMEVIS_VERSION;
data.mask = packHiddenFrames();
file.write((uint8_t *)&data, sizeof(data));
file.flush();
file.close();
LOG_INFO("Saved frame visibility (mask 0x%08x)", data.mask);
} else {
LOG_WARN("Failed to open %s for writing", frameVisibilityFileName);
}
spiLock->unlock();
#endif
}
void Screen::handleStartFirmwareUpdateScreen()
{
LOG_DEBUG("Show firmware screen");
+11 -2
View File
@@ -87,8 +87,7 @@ class Screen
#include <AutoOLEDWire.h>
#endif
#include "EInkDisplay2.h"
#include "EInkDynamicDisplay.h"
#include "BaseUIEInkDisplay.h"
#include "PointStruct.h"
#include "TFTDisplay.h"
#include "TypedQueue.h"
@@ -623,6 +622,11 @@ class Screen : public concurrency::OSThread
void toggleFrameVisibility(const std::string &frameName);
bool isFrameHidden(const std::string &frameName) const;
// Persist / restore which frames are hidden, across reboots.
// Stored as a single uint32 bitmask in /prefs (see Screen.cpp for the format).
void loadFrameVisibility();
void saveFrameVisibility();
#ifdef USE_EINK
/// Draw an image to remain on E-Ink display after screen off
void setScreensaverFrames(FrameCallback einkScreensaver = NULL);
@@ -738,6 +742,11 @@ class Screen : public concurrency::OSThread
bool chirpy = true;
} hiddenFrames;
// Convert hiddenFrames to a uint32 bitmask. Bit positions are fixed per
// frame name (see Screen.cpp).
uint32_t packHiddenFrames() const;
void applyHiddenFramesMask(uint32_t mask);
/// Try to start drawing ASAP
void setFastFramerate();
+1
View File
@@ -209,6 +209,7 @@ void menuHandler::LoraRegionPicker(uint32_t duration)
{"ITU1_2M (144-146)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU1_2M},
{"ITU2_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU2_2M},
{"ITU3_2M (144-148)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU3_2M},
{"ITU2_125CM (220-225)", OptionsAction::Select, meshtastic_Config_LoRaConfig_RegionCode_ITU2_125CM},
};
+132
View File
@@ -0,0 +1,132 @@
#include "./EInkParallel.h"
#if defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) && defined(ARCH_ESP32) && defined(NICHE_HAS_FASTEPD)
#include "FastEPD.h"
using namespace NicheGraphics::Drivers;
EInkParallel::EInkParallel(uint16_t width, uint16_t height, uint32_t panelType, uint32_t panelClock)
: EInk(width, height, (UpdateTypes)(FULL | FAST)), panelType(panelType), panelClock(panelClock)
{
}
EInkParallel::~EInkParallel()
{
if (asyncRunning.load()) {
for (int i = 0; i < 50 && asyncRunning.load(); ++i)
delay(50);
if (asyncTaskHandle) {
vTaskDelete(asyncTaskHandle);
asyncTaskHandle = nullptr;
}
}
delete epaper;
}
void EInkParallel::begin(SPIClass *, uint8_t, uint8_t, uint8_t, uint8_t)
{
// Parallel panels don't use the SPI args; FastEPD owns the bus.
if (!epaper) {
epaper = new FASTEPD;
epaper->initPanel((int)panelType, panelClock);
postPanelInit();
epaper->setMode(BB_MODE_1BPP);
epaper->clearWhite();
epaper->fullUpdate(true);
}
}
void EInkParallel::update(uint8_t *imageData, UpdateTypes type)
{
if (!epaper)
return;
pendingType = type;
copyImageInverted(imageData);
if (type == FULL) {
// Pick CLEAR_SLOW periodically to clear ghosting.
const int clearMode = (fastRefreshCount >= FULL_SLOW_PERIOD) ? CLEAR_SLOW : CLEAR_FAST;
fastRefreshCount = 0;
if (!asyncRunning.load()) {
asyncRunning.store(true);
BaseType_t rc =
xTaskCreatePinnedToCore(asyncFullTask, "epd_full", 4096 / sizeof(StackType_t), this, 2, &asyncTaskHandle,
#if CONFIG_FREERTOS_UNICORE
0
#else
1
#endif
);
if (rc != pdPASS) {
LOG_WARN("Async full failed; running blocking");
epaper->fullUpdate(clearMode, false);
epaper->backupPlane();
asyncRunning.store(false);
asyncTaskHandle = nullptr;
return; // synchronous: nothing to poll
}
// Begin polling for completion.
beginPolling(100, 1500);
}
} else {
// FAST: synchronous partial / clipped fullUpdate. Block briefly here.
epaper->fullUpdate(CLEAR_FAST, false);
epaper->backupPlane();
fastRefreshCount++;
// No polling needed; isUpdateDone() will report done immediately.
beginPolling(10, 0);
}
}
void EInkParallel::asyncFullTask(void *param)
{
auto *self = static_cast<EInkParallel *>(param);
if (!self) {
vTaskDelete(nullptr);
return;
}
self->epaper->fullUpdate(CLEAR_FAST, false);
self->epaper->backupPlane();
self->asyncRunning.store(false);
self->asyncTaskHandle = nullptr;
vTaskDelete(nullptr);
}
bool EInkParallel::isUpdateDone()
{
return !asyncRunning.load();
}
void EInkParallel::finalizeUpdate()
{
pendingType = UpdateTypes::UNSPECIFIED;
}
// Convert a niche-format buffer (row-major, MSB-left, 1=WHITE) into FastEPD's currentBuffer
// (row-major, MSB-left, 1=BLACK). Polarity inversion only.
void EInkParallel::copyImageInverted(const uint8_t *src)
{
uint8_t *dst = epaper->currentBuffer();
if (!dst || !src)
return;
const uint16_t rowBytes = ((width - 1) / 8) + 1;
const uint32_t total = rowBytes * height;
// Mask off bits beyond the panel width in the trailing byte of each row.
const uint8_t trailingMask = (uint8_t)(0xFFu << ((rowBytes * 8) - width));
for (uint16_t y = 0; y < height; y++) {
const uint32_t base = y * rowBytes;
for (uint16_t b = 0; b < rowBytes - 1; b++) {
dst[base + b] = ~src[base + b];
}
dst[base + rowBytes - 1] = (~src[base + rowBytes - 1]) & trailingMask;
}
(void)total;
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS && ARCH_ESP32 && NICHE_HAS_FASTEPD
+69
View File
@@ -0,0 +1,69 @@
/*
Parallel-EPD niche driver, backed by FastEPD.
Used for boards with an 8-bit parallel EPD interface (e.g. LILYGO T5 S3 ePaper).
The base class signature passes SPI parameters; this driver ignores them and uses FastEPD
to drive the parallel bus directly.
Gated on NICHE_HAS_FASTEPD because FastEPD is a heavy dependency that only parallel-EPD
variants want pulled in. Variants opt in by defining NICHE_HAS_FASTEPD in their platformio.ini
and adding the FastEPD library to lib_deps.
*/
#pragma once
#include "configuration.h"
#if defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) && defined(ARCH_ESP32) && defined(NICHE_HAS_FASTEPD)
#include "./EInk.h"
#include <atomic>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
class FASTEPD;
namespace NicheGraphics::Drivers
{
class EInkParallel : public EInk
{
public:
EInkParallel(uint16_t width, uint16_t height, uint32_t panelType, uint32_t panelClock = 28000000);
~EInkParallel();
// SPI parameters are unused for parallel panels.
void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) override;
void update(uint8_t *imageData, UpdateTypes type) override;
FASTEPD *fastEpd() { return epaper; }
protected:
bool isUpdateDone() override;
void finalizeUpdate() override;
// Hook for boards that need to bring up GPIO expanders / power pins after FastEPD::initPanel.
virtual void postPanelInit() {}
private:
void copyImageInverted(const uint8_t *src);
static void asyncFullTask(void *param);
FASTEPD *epaper = nullptr;
uint32_t panelType;
uint32_t panelClock;
UpdateTypes pendingType = UpdateTypes::UNSPECIFIED;
std::atomic<bool> asyncRunning{false};
TaskHandle_t asyncTaskHandle = nullptr;
uint8_t fastRefreshCount = 0;
static constexpr uint8_t FULL_SLOW_PERIOD = 100;
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS && ARCH_ESP32 && NICHE_HAS_FASTEPD
+49
View File
@@ -0,0 +1,49 @@
#include "./GDEH0122T61.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
using namespace NicheGraphics::Drivers;
void GDEH0122T61::configScanning()
{
sendCommand(0x01);
sendData(0xAF); // Scan until gate 175 (176px vertical resolution, low byte)
sendData(0x00); // high byte
sendData(0x00);
}
void GDEH0122T61::configWaveform()
{
sendCommand(0x3C);
sendData(0x05);
sendCommand(0x18);
sendData(0x80);
}
void GDEH0122T61::configUpdateSequence()
{
switch (updateType) {
case FAST:
sendCommand(0x22);
sendData(0xFF);
break;
case FULL:
default:
sendCommand(0x22);
sendData(0xF7);
break;
}
}
void GDEH0122T61::detachFromUpdate()
{
switch (updateType) {
case FAST:
return beginPolling(50, 250);
case FULL:
default:
return beginPolling(100, 1500);
}
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+43
View File
@@ -0,0 +1,43 @@
/*
E-Ink display driver
- GDEH0122T61
- Manufacturer: Good Display
- Size: 1.22 inch
- Resolution: 192px x 176px
- Controller IC: SSD1681 (operating in a sub-200x200 window)
Used by: t-echo-lite.
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./SSD16XX.h"
namespace NicheGraphics::Drivers
{
class GDEH0122T61 : public SSD16XX
{
private:
static constexpr uint32_t width = 192;
static constexpr uint32_t height = 176;
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
public:
GDEH0122T61() : SSD16XX(width, height, supported) {}
protected:
void configScanning() override;
void configWaveform() override;
void configUpdateSequence() override;
void detachFromUpdate() override;
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+57
View File
@@ -0,0 +1,57 @@
#include "./GDEQ031T10.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
using namespace NicheGraphics::Drivers;
void GDEQ031T10::configScanning()
{
sendCommand(0x01);
sendData(0x3F); // 319, low byte
sendData(0x01); // 319, high byte
sendData(0x00);
}
void GDEQ031T10::configWaveform()
{
sendCommand(0x3C);
sendData(0x01);
sendCommand(0x18);
sendData(0x80);
}
void GDEQ031T10::configUpdateSequence()
{
switch (updateType) {
case FAST:
sendCommand(0x21);
sendData(0x00);
sendData(0x00);
sendCommand(0x22);
sendData(0xFF);
break;
case FULL:
default:
sendCommand(0x21);
sendData(0x40);
sendData(0x00);
sendCommand(0x22);
sendData(0xF7);
break;
}
}
void GDEQ031T10::detachFromUpdate()
{
switch (updateType) {
case FAST:
return beginPolling(50, 400);
case FULL:
default:
return beginPolling(100, 2500);
}
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+43
View File
@@ -0,0 +1,43 @@
/*
E-Ink display driver
- GDEQ031T10
- Manufacturer: Good Display
- Size: 3.1 inch
- Resolution: 240px x 320px
- Controller IC: SSD1677 (SSD16XX-family, larger memory range)
Used by: t-deck-pro.
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./SSD16XX.h"
namespace NicheGraphics::Drivers
{
class GDEQ031T10 : public SSD16XX
{
private:
static constexpr uint32_t width = 240;
static constexpr uint32_t height = 320;
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
public:
GDEQ031T10() : SSD16XX(width, height, supported) {}
protected:
void configScanning() override;
void configWaveform() override;
void configUpdateSequence() override;
void detachFromUpdate() override;
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+49
View File
@@ -0,0 +1,49 @@
#include "./GDEY029T94.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
using namespace NicheGraphics::Drivers;
void GDEY029T94::configScanning()
{
sendCommand(0x01);
sendData(0x27); // 295, low byte
sendData(0x01); // 295, high byte
sendData(0x00);
}
void GDEY029T94::configWaveform()
{
sendCommand(0x3C);
sendData(0x05);
sendCommand(0x18);
sendData(0x80);
}
void GDEY029T94::configUpdateSequence()
{
switch (updateType) {
case FAST:
sendCommand(0x22);
sendData(0xFF);
break;
case FULL:
default:
sendCommand(0x22);
sendData(0xF7);
break;
}
}
void GDEY029T94::detachFromUpdate()
{
switch (updateType) {
case FAST:
return beginPolling(50, 300);
case FULL:
default:
return beginPolling(100, 2000);
}
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+43
View File
@@ -0,0 +1,43 @@
/*
E-Ink display driver
- GDEY029T94 (also sold as GDEY029T94-V2)
- Manufacturer: Good Display
- Size: 2.9 inch
- Resolution: 128px x 296px
- Controller IC: SSD1680
Used by: esp32-s3-pico, crowpanel-esp32s3-2-epaper.
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./SSD16XX.h"
namespace NicheGraphics::Drivers
{
class GDEY029T94 : public SSD16XX
{
private:
static constexpr uint32_t width = 128;
static constexpr uint32_t height = 296;
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
public:
GDEY029T94() : SSD16XX(width, height, supported) {}
protected:
void configScanning() override;
void configWaveform() override;
void configUpdateSequence() override;
void detachFromUpdate() override;
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+49
View File
@@ -0,0 +1,49 @@
#include "./GDEY042T81.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
using namespace NicheGraphics::Drivers;
void GDEY042T81::configWaveform()
{
sendCommand(0x3C);
sendData(0x01);
sendCommand(0x18);
sendData(0x80);
}
void GDEY042T81::configUpdateSequence()
{
switch (updateType) {
case FAST:
sendCommand(0x21);
sendData(0x00);
sendData(0x00);
sendCommand(0x22);
sendData(0xFF);
break;
case FULL:
default:
sendCommand(0x21);
sendData(0x40);
sendData(0x00);
sendCommand(0x22);
sendData(0xF7);
break;
}
}
void GDEY042T81::detachFromUpdate()
{
switch (updateType) {
case FAST:
return beginPolling(50, 1000);
case FULL:
default:
return beginPolling(100, 3500);
}
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+42
View File
@@ -0,0 +1,42 @@
/*
E-Ink display driver
- GDEY042T81
- Manufacturer: Good Display
- Size: 4.2 inch
- Resolution: 400px x 300px
- Controller IC: SSD1683
Used by: ME25LS01-4Y10TD_e-ink.
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./SSD16XX.h"
namespace NicheGraphics::Drivers
{
class GDEY042T81 : public SSD16XX
{
private:
static constexpr uint32_t width = 400;
static constexpr uint32_t height = 300;
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
public:
GDEY042T81() : SSD16XX(width, height, supported) {}
protected:
void configWaveform() override;
void configUpdateSequence() override;
void detachFromUpdate() override;
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+57
View File
@@ -0,0 +1,57 @@
#include "./GDEY0579T93.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
using namespace NicheGraphics::Drivers;
void GDEY0579T93::configScanning()
{
sendCommand(0x01);
sendData(0x0F); // 271, low byte
sendData(0x01); // 271, high byte
sendData(0x00);
}
void GDEY0579T93::configWaveform()
{
sendCommand(0x3C);
sendData(0x01);
sendCommand(0x18);
sendData(0x80);
}
void GDEY0579T93::configUpdateSequence()
{
switch (updateType) {
case FAST:
sendCommand(0x21);
sendData(0x00);
sendData(0x00);
sendCommand(0x22);
sendData(0xFF);
break;
case FULL:
default:
sendCommand(0x21);
sendData(0x40);
sendData(0x00);
sendCommand(0x22);
sendData(0xF7);
break;
}
}
void GDEY0579T93::detachFromUpdate()
{
switch (updateType) {
case FAST:
return beginPolling(100, 2000);
case FULL:
default:
return beginPolling(150, 5000);
}
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+43
View File
@@ -0,0 +1,43 @@
/*
E-Ink display driver
- GDEY0579T93
- Manufacturer: Good Display
- Size: 5.79 inch
- Resolution: 792px x 272px
- Controller IC: SSD1683 (extended memory range)
Used by: crowpanel-esp32s3-5-epaper.
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./SSD16XX.h"
namespace NicheGraphics::Drivers
{
class GDEY0579T93 : public SSD16XX
{
private:
static constexpr uint32_t width = 792;
static constexpr uint32_t height = 272;
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
public:
GDEY0579T93() : SSD16XX(width, height, supported) {}
protected:
void configScanning() override;
void configWaveform() override;
void configUpdateSequence() override;
void detachFromUpdate() override;
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/DEPG0213BNS800.h"
namespace NicheGraphics::Panels
{
class DEPG0213BNS800 : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::DEPG0213BNS800();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
uint8_t rotation() const override { return 3; }
};
} // namespace NicheGraphics::Panels
#endif
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/DEPG0290BNS800.h"
namespace NicheGraphics::Panels
{
class DEPG0290BNS800 : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::DEPG0290BNS800();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
uint8_t rotation() const override { return 1; }
};
} // namespace NicheGraphics::Panels
#endif
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/E0213A367.h"
namespace NicheGraphics::Panels
{
class E0213A367 : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::E0213A367();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
uint8_t rotation() const override { return 3; }
};
} // namespace NicheGraphics::Panels
#endif
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/GDEH0122T61.h"
namespace NicheGraphics::Panels
{
class GDEH0122T61 : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::GDEH0122T61();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
};
} // namespace NicheGraphics::Panels
#endif
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/GDEQ031T10.h"
namespace NicheGraphics::Panels
{
class GDEQ031T10 : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::GDEQ031T10();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
};
} // namespace NicheGraphics::Panels
#endif
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/GDEW0102T4.h"
namespace NicheGraphics::Panels
{
class GDEW0102T4 : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::GDEW0102T4();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
uint8_t rotation() const override { return 3; }
};
} // namespace NicheGraphics::Panels
#endif
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/GDEY0154D67.h"
namespace NicheGraphics::Panels
{
class GDEY0154D67 : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::GDEY0154D67();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
};
} // namespace NicheGraphics::Panels
#endif
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/GDEY0213B74.h"
namespace NicheGraphics::Panels
{
class GDEY0213B74 : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::GDEY0213B74();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
uint8_t rotation() const override { return 3; }
};
} // namespace NicheGraphics::Panels
#endif
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/GDEY029T94.h"
namespace NicheGraphics::Panels
{
class GDEY029T94 : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::GDEY029T94();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
uint8_t rotation() const override { return 1; }
};
} // namespace NicheGraphics::Panels
#endif
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/GDEY042T81.h"
namespace NicheGraphics::Panels
{
class GDEY042T81 : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::GDEY042T81();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
};
} // namespace NicheGraphics::Panels
#endif
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/GDEY0579T93.h"
namespace NicheGraphics::Panels
{
class GDEY0579T93 : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::GDEY0579T93();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
};
} // namespace NicheGraphics::Panels
#endif
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/HINK_E042A87.h"
namespace NicheGraphics::Panels
{
class HINK_E042A87 : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::HINK_E042A87();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
};
} // namespace NicheGraphics::Panels
#endif
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/LCMEN2R13EFC1.h"
namespace NicheGraphics::Panels
{
class LCMEN213EFC1 : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::LCMEN213EFC1();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
uint8_t rotation() const override { return 3; }
};
} // namespace NicheGraphics::Panels
#endif
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/LCMEN2R13ECC1.h"
namespace NicheGraphics::Panels
{
class LCMEN2R13ECC1 : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::LCMEN2R13ECC1();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
uint8_t rotation() const override { return 3; }
};
} // namespace NicheGraphics::Panels
#endif
+70
View File
@@ -0,0 +1,70 @@
#include "./PanelProfile.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
using namespace NicheGraphics::Panels;
SPIClass *PanelProfile::beginSpi()
{
#if defined(ARCH_ESP32)
auto *spi = new SPIClass(HSPI);
#if defined(PIN_EINK_SCLK) && defined(PIN_EINK_MOSI) && defined(PIN_EINK_CS)
spi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS);
#else
spi->begin();
#endif
return spi;
#elif defined(ARCH_NRF52)
SPI1.begin();
return &SPI1;
#else
return &SPI;
#endif
}
int8_t PanelProfile::backlightPin() const
{
#ifdef PIN_EINK_EN
return PIN_EINK_EN;
#else
return -1;
#endif
}
uint8_t PanelProfile::pinDC() const
{
#ifdef PIN_EINK_DC
return PIN_EINK_DC;
#else
return 0xFF;
#endif
}
uint8_t PanelProfile::pinCS() const
{
#ifdef PIN_EINK_CS
return PIN_EINK_CS;
#else
return 0xFF;
#endif
}
uint8_t PanelProfile::pinBusy() const
{
#ifdef PIN_EINK_BUSY
return PIN_EINK_BUSY;
#else
return 0xFF;
#endif
}
int8_t PanelProfile::pinReset() const
{
#ifdef PIN_EINK_RES
return PIN_EINK_RES;
#else
return -1;
#endif
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+52
View File
@@ -0,0 +1,52 @@
/*
Panel profile: single source of truth for how a specific E-Ink panel is wired and brought up.
Variants subclass a per-panel profile only to override differences (SPI bus, pins, rotation, backlight pin,
power-up quirks). The profile's create() constructs and begins the underlying
NicheGraphics::Drivers::EInk subclass exactly once.
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "graphics/eink/Drivers/EInk.h"
#include <SPI.h>
namespace NicheGraphics::Panels
{
class PanelProfile
{
public:
virtual ~PanelProfile() = default;
// Produce and begin() the underlying E-Ink driver. Called once per boot.
virtual NicheGraphics::Drivers::EInk *create() = 0;
// Public, variant-overridable metadata
virtual uint8_t rotation() const { return 0; }
virtual int8_t backlightPin() const;
protected:
// Default SPI bring-up. ESP32 uses HSPI with PIN_EINK_SCLK/MOSI; nRF52 uses SPI1 (pins from variant.h).
// Variants override when using a non-default bus or pin set.
virtual SPIClass *beginSpi();
// Pin defaults read the variant's PIN_EINK_* macros. Variants override if mapping differs.
virtual uint8_t pinDC() const;
virtual uint8_t pinCS() const;
virtual uint8_t pinBusy() const;
virtual int8_t pinReset() const;
// Hook for variants that need to raise a power rail / observe settle time before SPI traffic.
virtual void prePowerOn() {}
};
} // namespace NicheGraphics::Panels
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
+38
View File
@@ -0,0 +1,38 @@
/*
Panel profile base for the LILYGO T5 ePaper Pro family (ED047TC1, 960x540, 8-bit parallel via FastEPD).
V1 and V2 use different FastEPD panel IDs and V2 also needs GPIO-expander pins raised.
Variants subclass to provide a Drivers::EInkParallel subclass that implements
postPanelInit() if needed.
*/
#pragma once
#include "configuration.h"
#if defined(MESHTASTIC_INCLUDE_NICHE_GRAPHICS) && defined(ARCH_ESP32) && defined(NICHE_HAS_FASTEPD)
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/EInkParallel.h"
namespace NicheGraphics::Panels
{
class T5EpaperPanel : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
auto *drv = makeDriver();
drv->begin(nullptr, 0, 0, 0); // SPI args ignored
return drv;
}
protected:
// Variant returns a Drivers::EInkParallel subclass configured for its specific panel/init.
virtual NicheGraphics::Drivers::EInkParallel *makeDriver() = 0;
};
} // namespace NicheGraphics::Panels
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS && ARCH_ESP32 && NICHE_HAS_FASTEPD
@@ -0,0 +1,25 @@
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "./PanelProfile.h"
#include "graphics/eink/Drivers/ZJY122250_0213BAAMFGN.h"
namespace NicheGraphics::Panels
{
class ZJY122250_0213BAAMFGN : public PanelProfile
{
public:
NicheGraphics::Drivers::EInk *create() override
{
prePowerOn();
SPIClass *spi = beginSpi();
auto *drv = new NicheGraphics::Drivers::ZJY122250_0213BAAMFGN();
drv->begin(spi, pinDC(), pinCS(), pinBusy(), pinReset());
return drv;
}
uint8_t rotation() const override { return 1; }
};
} // namespace NicheGraphics::Panels
#endif
@@ -1,4 +1,4 @@
#include "graphics/niche/InkHUD/Tile.h"
#include "graphics/niche/Tile.h"
#include <cstdint>
#ifdef MESHTASTIC_INCLUDE_INKHUD
@@ -23,7 +23,7 @@
#include "./InkHUD.h"
#include "./Persistence.h"
#include "./Tile.h"
#include "graphics/niche/Drivers/EInk/EInk.h"
#include "graphics/eink/Drivers/EInk.h"
namespace NicheGraphics::InkHUD
{

Some files were not shown because too many files have changed in this diff Show More