mirror of
https://github.com/meshtastic/firmware.git
synced 2026-01-04 00:52:17 +00:00
Add InkHUD driver for WeAct Studio 4.2" display module (#6384)
* chore: todo.txt
* chore: InkHUD documentation
Word salad for maintainers
* refactor: don't init system applets using onActivate
System applets cannot be deactivated, so we will avoid using onActivate / onDeactivate methods entirely.
* chore: update the example applets
* fix: SSD16XX reset pulse
Allow time for controller IC to wake. Aligns with manufacturer's suggestions.
T-Echo button timing adjusted to prevent bouncing as a result(?) of slightly faster refreshes.
* fix: allow timeout if display update fails
Result is not graceful, but avoids total display lockup requiring power cycle.
Typical cause of failure is poor wiring / power supply.
* fix: improve display health on shutdown
Two extra full refreshes, masquerading as a "shutting down" screen. One is drawn white-on-black, to really shake the pixels up.
* feat: driver for display HINK_E042A87
As of Feb. 2025, these panels are used for "WeActStudio 4.2in B&W" display modules.
* fix: inkhud rotation should default to 0
* Revert "chore: todo.txt"
This reverts commit bea7df44a7.
* fix: more generous timeout for display updates
Previously this was tied to the expected duration of the update, but this didn't account for any delay if our polling thread got held up by an unrelated firmware task.
* fix: don't use the full shutdown screen during reboot
* fix: cooldown period during the display shutdown display sequence
Observed to prevent border pixels from being locked in place with some residual charge?
This commit is contained in:
@@ -6,7 +6,7 @@ using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Separate from EInk::begin method, as derived class constructors can probably supply these parameters as constants
|
||||
EInk::EInk(uint16_t width, uint16_t height, UpdateTypes supported)
|
||||
: concurrency::OSThread("E-Ink Driver"), width(width), height(height), supportedUpdateTypes(supported)
|
||||
: concurrency::OSThread("EInkDriver"), width(width), height(height), supportedUpdateTypes(supported)
|
||||
{
|
||||
OSThread::disable();
|
||||
}
|
||||
@@ -31,8 +31,8 @@ bool EInk::supports(UpdateTypes type)
|
||||
void EInk::beginPolling(uint32_t interval, uint32_t expectedDuration)
|
||||
{
|
||||
updateRunning = true;
|
||||
updateBegunAt = millis();
|
||||
pollingInterval = interval;
|
||||
pollingBegunAt = millis();
|
||||
|
||||
// To minimize load, we can choose to delay polling for a few seconds, if we know roughly how long the update will take
|
||||
// By default, expectedDuration is 0, and we'll start polling immediately
|
||||
@@ -45,10 +45,26 @@ void EInk::beginPolling(uint32_t interval, uint32_t expectedDuration)
|
||||
// This is what allows us to update the display asynchronously
|
||||
int32_t EInk::runOnce()
|
||||
{
|
||||
// Check for polling timeout
|
||||
// Manually set at 10 seconds, in case some big task holds up the firmware's cooperative multitasking
|
||||
if (millis() - pollingBegunAt > 10000)
|
||||
failed = true;
|
||||
|
||||
// Handle failure
|
||||
// - polling timeout
|
||||
// - other error (derived classes)
|
||||
if (failed) {
|
||||
LOG_WARN("Display update failed. Check wiring & power supply.");
|
||||
updateRunning = false;
|
||||
failed = false;
|
||||
return disable();
|
||||
}
|
||||
|
||||
// If update not yet done
|
||||
if (!isUpdateDone())
|
||||
return pollingInterval; // Poll again in a few ms
|
||||
|
||||
// If update done:
|
||||
// If update done
|
||||
finalizeUpdate(); // Any post-update code: power down panel hardware, hibernate, etc
|
||||
updateRunning = false; // Change what we report via EInk::busy()
|
||||
return disable(); // Stop polling
|
||||
|
||||
@@ -24,7 +24,7 @@ class EInk : private concurrency::OSThread
|
||||
enum UpdateTypes : uint8_t {
|
||||
UNSPECIFIED = 0,
|
||||
FULL = 1 << 0,
|
||||
FAST = 1 << 1,
|
||||
FAST = 1 << 1, // "Partial Refresh"
|
||||
};
|
||||
|
||||
EInk(uint16_t width, uint16_t height, UpdateTypes supported);
|
||||
@@ -41,14 +41,15 @@ class EInk : private concurrency::OSThread
|
||||
void beginPolling(uint32_t interval, uint32_t expectedDuration); // Begin checking repeatedly if update finished
|
||||
virtual bool isUpdateDone() = 0; // Check once if update finished
|
||||
virtual void finalizeUpdate() {} // Run any post-update code
|
||||
bool failed = false; // If an error occurred during update
|
||||
|
||||
private:
|
||||
int32_t runOnce() override; // Repeated checking if update finished
|
||||
|
||||
const UpdateTypes supportedUpdateTypes; // Capabilities of a derived display class
|
||||
bool updateRunning = false; // see EInk::busy()
|
||||
uint32_t updateBegunAt = 0; // For initial pause before polling for update completion
|
||||
uint32_t pollingInterval = 0; // How often to check if update complete (ms)
|
||||
uint32_t pollingBegunAt = 0; // To timeout during polling
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
58
src/graphics/niche/Drivers/EInk/HINK_E042A87.cpp
Normal file
58
src/graphics/niche/Drivers/EInk/HINK_E042A87.cpp
Normal file
@@ -0,0 +1,58 @@
|
||||
#include "./HINK_E042A87.h"
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// 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 HINK_E042A87::configWaveform()
|
||||
{
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x01); // Follow LUT for VSH1
|
||||
|
||||
sendCommand(0x18); // Temperature sensor:
|
||||
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
|
||||
}
|
||||
|
||||
// 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 HINK_E042A87::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x21); // Use both "old" and "new" image memory (differential)
|
||||
sendData(0x00);
|
||||
sendData(0x00);
|
||||
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Differential, load waveform from OTP
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x21); // Bypass "old" image memory (non-differential)
|
||||
sendData(0x40);
|
||||
sendData(0x00);
|
||||
|
||||
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 HINK_E042A87::detachFromUpdate()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 1000); // At least 1 second, then check every 50ms
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 3500); // At least 3.5 seconds, then check every 100ms
|
||||
}
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
43
src/graphics/niche/Drivers/EInk/HINK_E042A87.h
Normal file
43
src/graphics/niche/Drivers/EInk/HINK_E042A87.h
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
|
||||
E-Ink display driver
|
||||
- HINK-E042A87
|
||||
- Manufacturer: Holitech
|
||||
- Size: 4.2 inch
|
||||
- Resolution: 400px x 300px
|
||||
- Flex connector marking: HINK-E042A07-FPC-A1
|
||||
- Silver sticker with QR code, marked: HE042A87
|
||||
|
||||
Note: as of Feb. 2025, these panels are used for "WeActStudio 4.2in B&W" display modules
|
||||
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class HINK_E042A87 : public SSD16XX
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 400;
|
||||
static constexpr uint32_t height = 300;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
|
||||
public:
|
||||
HINK_E042A87() : SSD16XX(width, height, supported) {}
|
||||
|
||||
protected:
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -28,6 +28,17 @@ void setupNicheGraphics()
|
||||
}
|
||||
```
|
||||
|
||||
- [Methods](#methods)
|
||||
- [`update(uint8_t *imageData, UpdateTypes type)`](#updateuint8_t-imagedata-updatetypes-type)
|
||||
- [`await()`](#await)
|
||||
- [`supports(UpdateTypes type)`](#supportsupdatetypes-type)
|
||||
- [`busy()`](#busy)
|
||||
- [`width()`](#width)
|
||||
- [`height()`](#height)
|
||||
- [Supporting New Displays](#supporting-new-displays)
|
||||
- [Controller IC](#controller-ic)
|
||||
- [Finding Information](#finding-information)
|
||||
|
||||
## Methods
|
||||
|
||||
### `update(uint8_t *imageData, UpdateTypes type)`
|
||||
@@ -37,7 +48,7 @@ Update the image on the display
|
||||
- _`imageData`_ to draw to the display.
|
||||
- _`type`_ which type of update to perform.
|
||||
- `FULL`
|
||||
- `FAST`
|
||||
- `FAST` (partial refresh)
|
||||
- (Other custom types may be possible)
|
||||
|
||||
The imageData is a 1-bit image. X-Pixels are 8-per byte, with the MSB being the leftmost pixel. This was not an InkHUD design decision; it is the raw format accepted by the E-Ink display controllers ICs.
|
||||
@@ -83,3 +94,39 @@ Width of the display, in pixels. Note: most displays are portrait. Your UI will
|
||||
### `height()`
|
||||
|
||||
Height of the display, in pixels. Note: most displays are portrait. Your UI will need to implement rotation in software.
|
||||
|
||||
## Supporting New Displays
|
||||
|
||||
_This topic is not covered in depth, but these notes may be helpful._
|
||||
|
||||
The `InkHUD::Drivers::EInk` class contains only the mechanism for implementing an E-Ink driver on-top of Meshtastic's `OSThread`. A driver for a specific display needs to extend this class.
|
||||
|
||||
### Controller IC
|
||||
|
||||
If your display uses a controller IC from Solomon Systech, you can probably extend the existing `Drivers::SSD16XX` class, making only minor modifications.
|
||||
|
||||
At this stage, displays using controller ICS from other manufacturers (UltraChip, Fitipower, etc) need to manually implemented. See `Drivers::LCMEN2R13EFC1` for an example.
|
||||
|
||||
Generic base classes for manufacturers other than Solomon Systech might be added here in future.
|
||||
|
||||
### Finding Information
|
||||
|
||||
#### Flex-Connector Labels
|
||||
|
||||
The orange flex-connector attached to E-Ink displays is often printed with an identifying label. This is not a _totally_ unique identifier, but does give a very strong clue as to the true model of the display, which can be used to search out further information.
|
||||
|
||||
#### Datasheets
|
||||
|
||||
The manufacturer of a DIY display module may publish a datasheet. These are often incomplete, but might reveal the true model of the display, or the controller IC.
|
||||
|
||||
If you can determine the true model name of the display, you can likely find a more complete datasheet on the display manufacturer's website. This will often provide a "typical operating sequence"; a general overview of the code used to drive the display
|
||||
|
||||
#### Example Code
|
||||
|
||||
The manufacturer of a DIY module may publish example code. You may have more luck finding example code published by the display manufacturer themselves, if you can determine the true model of the panel. These examples are a very valuable reference.
|
||||
|
||||
#### Other E-Ink drivers
|
||||
|
||||
Libraries like ZinggJM's GxEPD2 can be valuable sources of information, although your panel may not be _specifically_ supported, and only _compatible_ with a driver there, so some caution is advised.
|
||||
|
||||
The display selection file in GxEPD2's Hello World example is also a useful resource for matching "flex connector labels" with display models, but the flex connector label is _not_ a unique identifier, so this is only another clue.
|
||||
|
||||
@@ -37,11 +37,26 @@ void SSD16XX::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_b
|
||||
reset();
|
||||
}
|
||||
|
||||
void SSD16XX::wait()
|
||||
// Poll the displays busy pin until an operation is complete
|
||||
// Timeout and set fail flag if something went wrong and the display got stuck
|
||||
void SSD16XX::wait(uint32_t timeout)
|
||||
{
|
||||
// Don't bother waiting if part of the update sequence failed
|
||||
// In that situation, we're now just failing-through the process, until we can try again with next update.
|
||||
if (failed)
|
||||
return;
|
||||
|
||||
uint32_t startMs = millis();
|
||||
|
||||
// Busy when HIGH
|
||||
while (digitalRead(pin_busy) == HIGH)
|
||||
while (digitalRead(pin_busy) == HIGH) {
|
||||
// Check for timeout
|
||||
if (millis() - startMs > timeout) {
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
void SSD16XX::reset()
|
||||
@@ -50,8 +65,9 @@ void SSD16XX::reset()
|
||||
if (pin_rst != 0xFF) {
|
||||
pinMode(pin_rst, OUTPUT);
|
||||
digitalWrite(pin_rst, LOW);
|
||||
delay(50);
|
||||
pinMode(pin_rst, INPUT_PULLUP);
|
||||
delay(10);
|
||||
digitalWrite(pin_rst, HIGH);
|
||||
delay(10);
|
||||
wait();
|
||||
}
|
||||
|
||||
@@ -61,6 +77,11 @@ void SSD16XX::reset()
|
||||
|
||||
void SSD16XX::sendCommand(const uint8_t command)
|
||||
{
|
||||
// Abort if part of the update sequence failed
|
||||
// This will unlock again once we have failed-through the entire process
|
||||
if (failed)
|
||||
return;
|
||||
|
||||
spi->beginTransaction(spiSettings);
|
||||
digitalWrite(pin_dc, LOW); // DC pin low indicates command
|
||||
digitalWrite(pin_cs, LOW);
|
||||
@@ -77,6 +98,11 @@ void SSD16XX::sendData(uint8_t data)
|
||||
|
||||
void SSD16XX::sendData(const uint8_t *data, uint32_t size)
|
||||
{
|
||||
// Abort if part of the update sequence failed
|
||||
// This will unlock again once we have failed-through the entire process
|
||||
if (failed)
|
||||
return;
|
||||
|
||||
spi->beginTransaction(spiSettings);
|
||||
digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
|
||||
digitalWrite(pin_cs, LOW);
|
||||
|
||||
@@ -27,7 +27,7 @@ class SSD16XX : public EInk
|
||||
virtual void update(uint8_t *imageData, UpdateTypes type) override;
|
||||
|
||||
protected:
|
||||
virtual void wait();
|
||||
virtual void wait(uint32_t timeout = 1000);
|
||||
virtual void reset();
|
||||
virtual void sendCommand(const uint8_t command);
|
||||
virtual void sendData(const uint8_t data);
|
||||
|
||||
Reference in New Issue
Block a user