InkHUD support for LilyGo T3S3 E-Paper (#6503)

* Purge an incomplete E-Ink driver

* Use the deep-sleep mode of SSD16XX E-Ink displays

* TwoButton doesn't need to store pin mode

* Fix false positive button presses after light sleep

* E-Ink driver for DEPG0213BNS800

* InkHUD support for LilyGo T3S3 E-paper

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
This commit is contained in:
todd-herbert 2025-04-10 03:41:51 +12:00 committed by GitHub
parent 5256ae90dc
commit 536b6d87c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 322 additions and 42 deletions

View File

@ -1 +0,0 @@
#include "./DEPG0154BNS800.h"

View File

@ -1,34 +0,0 @@
/*
E-Ink display driver
- DEPG0154BNS800
- Manufacturer: DKE
- Size: 1.54 inch
- Resolution: 152px x 152px
- Flex connector marking: FPC7525
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./SSD16XX.h"
namespace NicheGraphics::Drivers
{
class DEPG0154BNS800 : public SSD16XX
{
// Display properties
private:
static constexpr uint32_t width = 152;
static constexpr uint32_t height = 152;
static constexpr UpdateTypes supported = (UpdateTypes)(FULL);
public:
DEPG0154BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@ -0,0 +1,132 @@
#include "./DEPG0213BNS800.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
using namespace NicheGraphics::Drivers;
// Describes the operation performed when a "fast refresh" is performed
// Source: Modified from GxEPD2 (GxEPD2_213_BN)
static const uint8_t LUT_FAST[] = {
// 1 2 3
0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2B (Existing black pixels)
0x80, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // B2W (New white pixels)
0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2B (New black pixels)
0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // W2W (Existing white pixels)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // VCOM
0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // 1. Any pixels changing W2B or B2W. Two medium taps.
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 2. All pixels. One short tap.
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 3. Cooldown
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x00, 0x00, 0x00, //
};
// How strongly the pixels are pulled and pushed
void DEPG0213BNS800::configVoltages()
{
switch (updateType) {
case FAST:
// Reference: display datasheet, GxEPD1
sendCommand(0x03); // Gate voltage
sendData(0x17); // VGH: 20V
// Reference: display datasheet, GxEPD1
sendCommand(0x04); // Source voltage
sendData(0x41); // VSH1: 15V
sendData(0x00); // VSH2: NA
sendData(0x32); // VSL: -15V
// GxEPD1 sets this at -1.2V, but that seems to be drive the pixels very hard
sendCommand(0x2C); // VCOM voltage
sendData(0x08); // VCOM: -0.2V
break;
case FULL:
default:
// From OTP memory
break;
}
}
// Load settings about how the pixels are moved from old state to new state during a refresh
// - manually specified,
// - or with stored values from displays OTP memory
void DEPG0213BNS800::configWaveform()
{
switch (updateType) {
case FAST:
sendCommand(0x3C); // Border waveform:
sendData(0x80); // VSS
sendCommand(0x32); // Write LUT register from MCU:
sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh)
break;
case FULL:
default:
// From OTP memory
break;
}
}
// Describes the sequence of events performed by the displays controller IC during a refresh
// Includes "power up", "load settings from memory", "update the pixels", etc
void DEPG0213BNS800::configUpdateSequence()
{
switch (updateType) {
case FAST:
sendCommand(0x22); // Set "update sequence"
sendData(0xCF); // Differential, use manually loaded waveform
break;
case FULL:
default:
sendCommand(0x22); // Set "update sequence"
sendData(0xF7); // Non-differential, load waveform from OTP
break;
}
}
// Once the refresh operation has been started,
// begin periodically polling the display to check for completion, using the normal Meshtastic threading code
// Only used when refresh is "async"
void DEPG0213BNS800::detachFromUpdate()
{
switch (updateType) {
case FAST:
return beginPolling(50, 500); // At least 500ms, then poll every 50ms
case FULL:
default:
return beginPolling(100, 3500); // At least 3500ms, then poll every 100ms
}
}
// For this display, we do not need to re-write the new image.
// We're overriding SSD16XX::finalizeUpdate to make this small optimization.
// The display does also work just fine with the generic SSD16XX method, though.
void DEPG0213BNS800::finalizeUpdate()
{
// Put a copy of the image into the "old memory".
// Used with differential refreshes (e.g. FAST update), to determine which px need to move, and which can remain in place
// We need to keep the "old memory" up to date, because don't know whether next refresh will be FULL or FAST etc.
if (updateType != FULL) {
// writeNewImage(); // Not required for this display
writeOldImage();
sendCommand(0x7F); // Terminate image write without update
wait();
}
// Enter deep-sleep to save a few µA
// Waking from this requires that display's reset pin is broken out
if (pin_rst != 0xFF)
deepSleep();
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@ -0,0 +1,44 @@
/*
E-Ink display driver
- DEPG0213BNS800
- Manufacturer: DKE
- Size: 2.13 inch
- Resolution: 122px x 250px
- Flex connector marking: FPC-7528B
Note: this is from an older generation of DKE panels, which still used Solomon Systech controller ICs.
DKE's website suggests that the latest DEPG0213BN displays may use Fitipower controllers instead.
*/
#pragma once
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
#include "configuration.h"
#include "./SSD16XX.h"
namespace NicheGraphics::Drivers
{
class DEPG0213BNS800 : public SSD16XX
{
// Display properties
private:
static constexpr uint32_t width = 122;
static constexpr uint32_t height = 250;
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
public:
DEPG0213BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte
protected:
void configVoltages() override;
void configWaveform() override;
void configUpdateSequence() override;
void detachFromUpdate() override;
void finalizeUpdate() override; // Only overriden for a slight optimization
};
} // namespace NicheGraphics::Drivers
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@ -116,5 +116,10 @@ void DEPG0290BNS800::finalizeUpdate()
sendCommand(0x7F); // Terminate image write without update
wait();
}
// Enter deep-sleep to save a few µA
// Waking from this requires that display's reset pin is broken out
if (pin_rst != 0xFF)
deepSleep();
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@ -242,5 +242,18 @@ void SSD16XX::finalizeUpdate()
sendCommand(0x7F); // Terminate image write without update
wait();
}
// Enter deep-sleep to save a few µA
// Waking from this requires that display's reset pin is broken out
if (pin_rst != 0xFF)
deepSleep();
}
// Enter a lower-power state
// May only save a few µA..
void SSD16XX::deepSleep()
{
sendCommand(0x10); // Enter deep sleep
sendData(0x01); // Mode 1: preserve image RAM
}
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS

View File

@ -44,6 +44,7 @@ class SSD16XX : public EInk
virtual void detachFromUpdate();
virtual bool isUpdateDone() override;
virtual void finalizeUpdate() override;
virtual void deepSleep();
protected:
uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring?

View File

@ -98,9 +98,8 @@ void TwoButton::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup)
assert(whichButton < 2);
buttons[whichButton].pin = pin;
buttons[whichButton].activeLogic = LOW; // Unimplemented
buttons[whichButton].mode = internalPullup ? INPUT_PULLUP : INPUT;
pinMode(buttons[whichButton].pin, buttons[whichButton].mode);
pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT);
}
void TwoButton::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs)
@ -299,7 +298,9 @@ int TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause)
// Manually trigger the button-down ISR
// - during light sleep, our ISR is disabled
// - if light sleep ends by button press, pretend our own ISR caught it
if (cause == ESP_SLEEP_WAKEUP_GPIO)
// - need to manually confirm by reading pin ourselves, to avoid occasional false positives
// (false positive only when using internal pullup resistors?)
if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic)
isrPrimary();
return 0; // Indicates success

View File

@ -35,7 +35,7 @@ class TwoButton : protected concurrency::OSThread
static TwoButton *getInstance(); // Create or get the singleton instance
void start(); // Start handling button input
void stop(); // Stop handling button input (disconnect ISRs for sleep)
void setWiring(uint8_t whichButton, uint8_t pin, bool internalPulldown = false);
void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false);
void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs);
void setHandlerDown(uint8_t whichButton, Callback onDown);
void setHandlerUp(uint8_t whichButton, Callback onUp);
@ -65,7 +65,6 @@ class TwoButton : protected concurrency::OSThread
// Per-button config
uint8_t pin = 0xFF; // 0xFF: unset
bool activeLogic = LOW; // Active LOW by default. Currently unimplemented.
uint8_t mode = INPUT; // Whether to use internal pull up / pull down resistors
uint32_t debounceLength = 50; // Minimum length for shortpress, in ms
uint32_t longpressLength = 500; // How long after button down to fire longpress, in ms
volatile State state = State::REST; // Internal state

View File

@ -112,7 +112,7 @@ void setupNicheGraphics()
// Setup the capacitive touch button
// - short: momentary backlight
// - long: latch backlight on
buttons->setWiring(TOUCH_BUTTON, PIN_BUTTON_TOUCH, LOW);
buttons->setWiring(TOUCH_BUTTON, PIN_BUTTON_TOUCH);
buttons->setTiming(TOUCH_BUTTON, 50, 5000); // 5 seconds before latch - limited by T-Echo's capacitive touch IC
buttons->setHandlerDown(TOUCH_BUTTON, [backlight]() {
backlight->peek();

View File

@ -0,0 +1,102 @@
#pragma once
#include "configuration.h"
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
// InkHUD-specific components
// ---------------------------
#include "graphics/niche/InkHUD/InkHUD.h"
// Applets
#include "graphics/niche/InkHUD/Applets/User/AllMessage/AllMessageApplet.h"
#include "graphics/niche/InkHUD/Applets/User/DM/DMApplet.h"
#include "graphics/niche/InkHUD/Applets/User/Heard/HeardApplet.h"
#include "graphics/niche/InkHUD/Applets/User/Positions/PositionsApplet.h"
#include "graphics/niche/InkHUD/Applets/User/RecentsList/RecentsListApplet.h"
#include "graphics/niche/InkHUD/Applets/User/ThreadedMessage/ThreadedMessageApplet.h"
// #include "graphics/niche/InkHUD/Applets/Examples/BasicExample/BasicExampleApplet.h"
// #include "graphics/niche/InkHUD/Applets/Examples/NewMsgExample/NewMsgExampleApplet.h"
// Shared NicheGraphics components
// --------------------------------
#include "graphics/niche/Drivers/EInk/DEPG0213BNS800.h"
#include "graphics/niche/Inputs/TwoButton.h"
#include "graphics/niche/Fonts/FreeSans6pt7b.h"
#include "graphics/niche/Fonts/FreeSans6pt8bCyrillic.h"
#include <Fonts/FreeSans9pt7b.h>
void setupNicheGraphics()
{
using namespace NicheGraphics;
// SPI
// -----------------------------
// Display is connected to HSPI
SPIClass *hspi = new SPIClass(HSPI);
hspi->begin(PIN_EINK_SCLK, -1, PIN_EINK_MOSI, PIN_EINK_CS);
// E-Ink Driver
// -----------------------------
// Use E-Ink driver
Drivers::EInk *driver = new Drivers::DEPG0213BNS800;
driver->begin(hspi, PIN_EINK_DC, PIN_EINK_CS, PIN_EINK_BUSY, PIN_EINK_RES);
// InkHUD
// ----------------------------
InkHUD::InkHUD *inkhud = InkHUD::InkHUD::getInstance();
// Set the driver
inkhud->setDriver(driver);
// Set how many FAST updates per FULL update
// Set how unhealthy additional FAST updates beyond this number are
inkhud->setDisplayResilience(15, 1.5);
// Prepare fonts
InkHUD::Applet::fontLarge = InkHUD::AppletFont(FreeSans9pt7b);
InkHUD::Applet::fontSmall = InkHUD::AppletFont(FreeSans6pt7b);
/*
// Font localization demo: Cyrillic
InkHUD::Applet::fontSmall = InkHUD::AppletFont(FreeSans6pt8bCyrillic);
InkHUD::Applet::fontSmall.addSubstitutionsWin1251();
*/
// Customize default settings
inkhud->persistence->settings.userTiles.maxCount = 2; // How many tiles can the display handle?
inkhud->persistence->settings.rotation = 3; // 270 degrees clockwise
inkhud->persistence->settings.userTiles.count = 1; // One tile only by default, keep things simple for new users
// Pick applets
inkhud->addApplet("All Messages", new InkHUD::AllMessageApplet, true, true); // Activated, autoshown
inkhud->addApplet("DMs", new InkHUD::DMApplet); // Inactive
inkhud->addApplet("Channel 0", new InkHUD::ThreadedMessageApplet(0)); // Inactive
inkhud->addApplet("Channel 1", new InkHUD::ThreadedMessageApplet(1)); // Inactive
inkhud->addApplet("Positions", new InkHUD::PositionsApplet, true); // Activated
inkhud->addApplet("Recents List", new InkHUD::RecentsListApplet); // Inactive
inkhud->addApplet("Heard", new InkHUD::HeardApplet, true, false, 0); // Activated, not autoshown, default on tile 0
// inkhud->addApplet("Basic", new InkHUD::BasicExampleApplet);
// inkhud->addApplet("NewMsg", new InkHUD::NewMsgExampleApplet);
// Start running InkHUD
inkhud->begin();
// Buttons
// --------------------------
Inputs::TwoButton *buttons = Inputs::TwoButton::getInstance(); // Shared NicheGraphics component
// Setup the main user button
buttons->setWiring(0, Inputs::TwoButton::getUserButtonPin(), true);
buttons->setHandlerShortPress(0, []() { InkHUD::InkHUD::getInstance()->shortpress(); });
buttons->setHandlerLongPress(0, []() { InkHUD::InkHUD::getInstance()->longpress(); });
buttons->start();
}
#endif

View File

@ -7,6 +7,7 @@ upload_protocol = esptool
build_flags =
${esp32_base.build_flags} -D TLORA_T3S3_EPAPER -I variants/tlora_t3s3_epaper
-DGPS_POWER_TOGGLE
-DUSE_EINK
-DEINK_DISPLAY_MODEL=GxEPD2_213_BN
-DEINK_WIDTH=250
-DEINK_HEIGHT=122
@ -16,3 +17,21 @@ build_flags =
lib_deps =
${esp32s3_base.lib_deps}
https://github.com/meshtastic/GxEPD2/archive/b202ebfec6a4821e098cf7a625ba0f6f2400292d.zip
[env:tlora-t3s3-epaper-inkhud]
extends = esp32s3_base, inkhud
board = tlora-t3s3-v1
board_check = true
upload_protocol = esptool
build_src_filter =
${esp32_base.build_src_filter}
${inkhud.build_src_filter}
build_flags =
${esp32s3_base.build_flags}
${inkhud.build_flags}
-I variants/tlora_t3s3_epaper
-D TLORA_T3S3_EPAPER
-D MAX_THREADS=40 ; Required if used with WiFi
lib_deps =
${inkhud.lib_deps} ; InkHUD libs first, so we get GFXRoot instead of AdafruitGFX
${esp32s3_base.lib_deps}

View File

@ -2,7 +2,6 @@
#define SDCARD_USE_SPI1
// Display (E-Ink)
#define USE_EINK
#define PIN_EINK_CS 15
#define PIN_EINK_BUSY 48
#define PIN_EINK_DC 16