add a .clang-format file (#9154)
This commit is contained in:
+184
-193
@@ -33,252 +33,243 @@
|
||||
*/
|
||||
|
||||
// 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;
|
||||
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;
|
||||
// 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);
|
||||
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);
|
||||
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;
|
||||
uint32_t now = millis();
|
||||
uint32_t sinceLast = now - lastDrawMsec;
|
||||
|
||||
if (adafruitDisplay && (sinceLast > msecLimit || lastDrawMsec == 0))
|
||||
lastDrawMsec = now;
|
||||
else
|
||||
return false;
|
||||
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
|
||||
// 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);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
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();
|
||||
// Trigger the refresh in GxEPD2
|
||||
LOG_DEBUG("Update E-Paper");
|
||||
adafruitDisplay->nextPage();
|
||||
|
||||
// End the update process
|
||||
endUpdate();
|
||||
// End the update process
|
||||
endUpdate();
|
||||
|
||||
LOG_DEBUG("done");
|
||||
return true;
|
||||
LOG_DEBUG("done");
|
||||
return true;
|
||||
}
|
||||
|
||||
// End the update process - virtual method, overriden in derived class
|
||||
void EInkDisplay::endUpdate()
|
||||
{
|
||||
// Power off display hardware, then deep-sleep (Except Wireless Paper V1.1, no deep-sleep)
|
||||
adafruitDisplay->hibernate();
|
||||
void EInkDisplay::endUpdate() {
|
||||
// Power off display hardware, then deep-sleep (Except Wireless Paper V1.1, no deep-sleep)
|
||||
adafruitDisplay->hibernate();
|
||||
}
|
||||
|
||||
// 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)
|
||||
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
|
||||
}
|
||||
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::sendCommand(uint8_t com) {
|
||||
(void)com;
|
||||
// Drop all commands to device (we just update the buffer)
|
||||
}
|
||||
|
||||
void EInkDisplay::setDetected(uint8_t detected)
|
||||
{
|
||||
(void)detected;
|
||||
}
|
||||
void EInkDisplay::setDetected(uint8_t detected) { (void)detected; }
|
||||
|
||||
// Connect to the display - variant specific
|
||||
bool EInkDisplay::connect()
|
||||
{
|
||||
LOG_INFO("Do EInk init");
|
||||
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);
|
||||
// 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);
|
||||
// ThinkNode M1 has a hardware dimmable backlight. Start enabled
|
||||
digitalWrite(PIN_EINK_EN, HIGH);
|
||||
#else
|
||||
digitalWrite(PIN_EINK_EN, LOW);
|
||||
digitalWrite(PIN_EINK_EN, LOW);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(TTGO_T_ECHO) || defined(ELECROW_ThinkNode_M1) || defined(T_ECHO_LITE)
|
||||
{
|
||||
auto lowLevel = new EINK_DISPLAY_MODEL(PIN_EINK_CS, PIN_EINK_DC, PIN_EINK_RES, PIN_EINK_BUSY, SPI1);
|
||||
{
|
||||
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)
|
||||
{
|
||||
// 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();
|
||||
adafruitDisplay->setRotation(3);
|
||||
#if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER)
|
||||
adafruitDisplay->setRotation(0);
|
||||
#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();
|
||||
|
||||
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
|
||||
|
||||
// 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);
|
||||
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)
|
||||
{
|
||||
// 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();
|
||||
adafruitDisplay->setRotation(3);
|
||||
#if defined(CROWPANEL_ESP32S3_5_EPAPER) || defined(CROWPANEL_ESP32S3_4_EPAPER)
|
||||
adafruitDisplay->setRotation(0);
|
||||
#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;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
+49
-50
@@ -22,75 +22,74 @@
|
||||
* 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;
|
||||
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);
|
||||
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;
|
||||
// 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 = 1000);
|
||||
/**
|
||||
* 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 = 1000);
|
||||
|
||||
/**
|
||||
* 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();
|
||||
/**
|
||||
* 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);
|
||||
/**
|
||||
* 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; }
|
||||
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;
|
||||
// Send a command to the display (low level function)
|
||||
virtual void sendCommand(uint8_t com) override;
|
||||
|
||||
// Connect to the display
|
||||
virtual bool connect() 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;
|
||||
// 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;
|
||||
// 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) || \
|
||||
// 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)
|
||||
SPIClass *hspi = NULL;
|
||||
SPIClass *hspi = NULL;
|
||||
#endif
|
||||
|
||||
#if defined(HELTEC_MESH_POCKET) || defined(SEEED_WIO_TRACKER_L1_EINK) || defined(HELTEC_MESH_SOLAR_EINK)
|
||||
SPIClass *spi1 = NULL;
|
||||
SPIClass *spi1 = NULL;
|
||||
#endif
|
||||
|
||||
private:
|
||||
// FIXME quick hack to limit drawing to a very slow rate
|
||||
uint32_t lastDrawMsec = 0;
|
||||
private:
|
||||
// FIXME quick hack to limit drawing to a very slow rate
|
||||
uint32_t lastDrawMsec = 0;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
+346
-380
@@ -6,558 +6,524 @@
|
||||
|
||||
// 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
|
||||
: EInkDisplay(address, sda, scl, geometry, i2cBus), NotifiedWorkerThread("EInkDynamicDisplay") {
|
||||
// If tracking ghost pixels, grab memory
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros
|
||||
dirtyPixels = new uint8_t[EInkDisplay::displayBufferSize](); // Init with zeros
|
||||
#endif
|
||||
}
|
||||
|
||||
// Destructor
|
||||
EInkDynamicDisplay::~EInkDynamicDisplay()
|
||||
{
|
||||
// If we were tracking ghost pixels, free the memory
|
||||
EInkDynamicDisplay::~EInkDynamicDisplay() {
|
||||
// If we were tracking ghost pixels, free the memory
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
delete[] dirtyPixels;
|
||||
delete[] dirtyPixels;
|
||||
#endif
|
||||
}
|
||||
|
||||
// Screen requests a BACKGROUND frame
|
||||
void EInkDynamicDisplay::display()
|
||||
{
|
||||
addFrameFlag(BACKGROUND);
|
||||
update();
|
||||
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
|
||||
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);
|
||||
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
|
||||
void EInkDynamicDisplay::configForFastRefresh() {
|
||||
// Variant-specific code can go here
|
||||
#if defined(PRIVATE_HW)
|
||||
#else
|
||||
// Otherwise:
|
||||
adafruitDisplay->setPartialWindow(0, 0, adafruitDisplay->width(), adafruitDisplay->height());
|
||||
// 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
|
||||
void EInkDynamicDisplay::configForFullRefresh() {
|
||||
// Variant-specific code can go here
|
||||
#if defined(PRIVATE_HW)
|
||||
#else
|
||||
// Otherwise:
|
||||
adafruitDisplay->setFullWindow();
|
||||
// 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
// Change from FAST back to FULL
|
||||
else if (currentConfig == FAST && refresh == FULL) {
|
||||
configForFullRefresh();
|
||||
currentConfig = FULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Update fastRefreshCount
|
||||
void EInkDynamicDisplay::adjustRefreshCounters()
|
||||
{
|
||||
if (refresh == FAST)
|
||||
fastRefreshCount++;
|
||||
void EInkDynamicDisplay::adjustRefreshCounters() {
|
||||
if (refresh == FAST)
|
||||
fastRefreshCount++;
|
||||
|
||||
else if (refresh == FULL)
|
||||
fastRefreshCount = 0;
|
||||
else if (refresh == FULL)
|
||||
fastRefreshCount = 0;
|
||||
}
|
||||
|
||||
// Trigger the display update by calling base class
|
||||
bool EInkDynamicDisplay::update()
|
||||
{
|
||||
// Detemine 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
|
||||
bool EInkDynamicDisplay::update() {
|
||||
// Detemine 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
|
||||
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
|
||||
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 (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
|
||||
}
|
||||
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.
|
||||
// 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
|
||||
// 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();
|
||||
}
|
||||
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();
|
||||
bool EInkDynamicDisplay::determineMode() {
|
||||
checkInitialized();
|
||||
checkForPromotion();
|
||||
#if defined(HAS_EINK_ASYNCFULL)
|
||||
checkBusyAsyncRefresh();
|
||||
checkBusyAsyncRefresh();
|
||||
#endif
|
||||
checkRateLimiting();
|
||||
checkRateLimiting();
|
||||
|
||||
// If too soon for a new frame, or display busy, abort early
|
||||
if (refresh == SKIPPED)
|
||||
return false; // No refresh
|
||||
// If too soon for a new frame, or display busy, abort early
|
||||
if (refresh == SKIPPED)
|
||||
return false; // No refresh
|
||||
|
||||
// -- New frame is due --
|
||||
// -- 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
|
||||
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();
|
||||
// Once mode determined, any remaining checks will bypass
|
||||
checkCosmetic();
|
||||
checkDemandingFast();
|
||||
checkFrameMatchesPrevious();
|
||||
checkConsecutiveFastRefreshes();
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
checkExcessiveGhosting();
|
||||
checkExcessiveGhosting();
|
||||
#endif
|
||||
checkFastRequested();
|
||||
checkFastRequested();
|
||||
|
||||
if (refresh == UNSPECIFIED)
|
||||
LOG_WARN("There was a flaw in the determineMode() logic");
|
||||
if (refresh == UNSPECIFIED)
|
||||
LOG_WARN("There was a flaw in the determineMode() logic");
|
||||
|
||||
// -- Decision has been reached --
|
||||
applyRefreshMode();
|
||||
adjustRefreshCounters();
|
||||
// -- Decision has been reached --
|
||||
applyRefreshMode();
|
||||
adjustRefreshCounters();
|
||||
|
||||
#ifdef EINK_LIMIT_GHOSTING_PX
|
||||
// Full refresh clears any ghosting
|
||||
if (refresh == FULL)
|
||||
resetGhostPixelTracking();
|
||||
// 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
|
||||
// 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();
|
||||
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();
|
||||
// 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;
|
||||
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);
|
||||
}
|
||||
// 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
|
||||
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;
|
||||
}
|
||||
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;
|
||||
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;
|
||||
}
|
||||
// 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;
|
||||
// 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;
|
||||
}
|
||||
// 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;
|
||||
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);
|
||||
}
|
||||
// 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;
|
||||
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);
|
||||
}
|
||||
// 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;
|
||||
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 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;
|
||||
}
|
||||
// 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);
|
||||
// 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 occured consecutively, since last full refresh?
|
||||
void EInkDynamicDisplay::checkConsecutiveFastRefreshes()
|
||||
{
|
||||
// If a decision was already reached, don't run the check
|
||||
if (refresh != UNSPECIFIED)
|
||||
return;
|
||||
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;
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
// 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;
|
||||
void EInkDynamicDisplay::checkFastRequested() {
|
||||
if (refresh != UNSPECIFIED)
|
||||
return;
|
||||
|
||||
if (frameFlags == BACKGROUND) {
|
||||
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);
|
||||
// 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");
|
||||
// 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);
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
void EInkDynamicDisplay::resetRateLimiting() { previousRunMs = millis(); }
|
||||
|
||||
// Generate a hash of this frame, to compare against previous update
|
||||
void EInkDynamicDisplay::hashImage()
|
||||
{
|
||||
imageHash = 0;
|
||||
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;
|
||||
}
|
||||
// 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;
|
||||
void EInkDynamicDisplay::storeAndReset() {
|
||||
previousFrameFlags = frameFlags;
|
||||
previousRefresh = refresh;
|
||||
previousReason = reason;
|
||||
|
||||
// Only store image hash if the display will update
|
||||
if (refresh != SKIPPED) {
|
||||
previousImageHash = imageHash;
|
||||
}
|
||||
// Only store image hash if the display will update
|
||||
if (refresh != SKIPPED) {
|
||||
previousImageHash = imageHash;
|
||||
}
|
||||
|
||||
frameFlags = BACKGROUND;
|
||||
refresh = UNSPECIFIED;
|
||||
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;
|
||||
void EInkDynamicDisplay::countGhostPixels() {
|
||||
// If a decision was already reached, don't run the check
|
||||
if (refresh != UNSPECIFIED)
|
||||
return;
|
||||
|
||||
// Start a new count
|
||||
ghostPixelCount = 0;
|
||||
// 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++) {
|
||||
// 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?
|
||||
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++;
|
||||
// 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);
|
||||
}
|
||||
// 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);
|
||||
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;
|
||||
void EInkDynamicDisplay::checkExcessiveGhosting() {
|
||||
// If a decision was already reached, don't run the check
|
||||
if (refresh != UNSPECIFIED)
|
||||
return;
|
||||
|
||||
countGhostPixels();
|
||||
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);
|
||||
}
|
||||
// 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, EInkDisplay::buffer, EInkDisplay::displayBufferSize);
|
||||
void EInkDynamicDisplay::resetGhostPixelTracking() {
|
||||
// Copy the current frame into dirtyPixels[] from the display buffer
|
||||
memcpy(dirtyPixels, 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;
|
||||
}
|
||||
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;
|
||||
void EInkDynamicDisplay::joinAsyncRefresh() {
|
||||
// If no async refresh running, nothing to do
|
||||
if (!asyncRefreshRunning)
|
||||
return;
|
||||
|
||||
LOG_DEBUG("Join an async refresh in progress");
|
||||
LOG_DEBUG("Join an async refresh in progress");
|
||||
|
||||
// Continually poll the BUSY pin
|
||||
while (adafruitDisplay->epd2.isBusy())
|
||||
yield();
|
||||
// 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");
|
||||
// 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()
|
||||
// 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;
|
||||
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;
|
||||
}
|
||||
// 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");
|
||||
// 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()
|
||||
// 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;
|
||||
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;
|
||||
// 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()
|
||||
// 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
|
||||
pollAsyncRefresh(); // Check (and terminate) the async refresh manually
|
||||
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();
|
||||
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
|
||||
// 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
|
||||
|
||||
|
||||
+105
-106
@@ -15,131 +15,130 @@
|
||||
(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();
|
||||
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); }
|
||||
// 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);
|
||||
// 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.
|
||||
// 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,
|
||||
};
|
||||
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;
|
||||
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()
|
||||
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?
|
||||
// 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
|
||||
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 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
|
||||
// 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
|
||||
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.
|
||||
// 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.
|
||||
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
|
||||
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.
|
||||
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
|
||||
// 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
|
||||
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()
|
||||
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
|
||||
public:
|
||||
void joinAsyncRefresh() {} // Dummy method
|
||||
|
||||
protected:
|
||||
void pollAsyncRefresh() {} // Dummy method. In theory, not reachable
|
||||
protected:
|
||||
void pollAsyncRefresh() {} // Dummy method. In theory, not reachable
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
+110
-125
@@ -4,132 +4,117 @@
|
||||
// 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
|
||||
{
|
||||
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:
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
private:
|
||||
uint8_t which;
|
||||
GxEPD2_BW<Driver0, Driver0::HEIGHT> *driver0;
|
||||
GxEPD2_BW<Driver1, Driver1::HEIGHT> *driver1;
|
||||
};
|
||||
+521
-567
File diff suppressed because it is too large
Load Diff
+92
-95
@@ -36,129 +36,126 @@ Porting for SDL:
|
||||
#include "lgfx/v1/panel/Panel_FrameBufferBase.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
namespace lgfx
|
||||
{
|
||||
inline namespace v1
|
||||
{
|
||||
namespace lgfx {
|
||||
inline namespace v1 {
|
||||
|
||||
struct Panel_sdl;
|
||||
struct monitor_t {
|
||||
SDL_Window *window = nullptr;
|
||||
SDL_Renderer *renderer = nullptr;
|
||||
SDL_Texture *texture = nullptr;
|
||||
SDL_Texture *texture_frameimage = nullptr;
|
||||
Panel_sdl *panel = nullptr;
|
||||
SDL_Window *window = nullptr;
|
||||
SDL_Renderer *renderer = nullptr;
|
||||
SDL_Texture *texture = nullptr;
|
||||
SDL_Texture *texture_frameimage = nullptr;
|
||||
Panel_sdl *panel = nullptr;
|
||||
|
||||
// 外枠
|
||||
const void *frame_image = 0;
|
||||
uint_fast16_t frame_width = 0;
|
||||
uint_fast16_t frame_height = 0;
|
||||
uint_fast16_t frame_inner_x = 0;
|
||||
uint_fast16_t frame_inner_y = 0;
|
||||
int_fast16_t frame_rotation = 0;
|
||||
int_fast16_t frame_angle = 0;
|
||||
// 外枠
|
||||
const void *frame_image = 0;
|
||||
uint_fast16_t frame_width = 0;
|
||||
uint_fast16_t frame_height = 0;
|
||||
uint_fast16_t frame_inner_x = 0;
|
||||
uint_fast16_t frame_inner_y = 0;
|
||||
int_fast16_t frame_rotation = 0;
|
||||
int_fast16_t frame_angle = 0;
|
||||
|
||||
float scaling_x = 1;
|
||||
float scaling_y = 1;
|
||||
int_fast16_t touch_x, touch_y;
|
||||
bool touched = false;
|
||||
bool closing = false;
|
||||
float scaling_x = 1;
|
||||
float scaling_y = 1;
|
||||
int_fast16_t touch_x, touch_y;
|
||||
bool touched = false;
|
||||
bool closing = false;
|
||||
};
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
struct Touch_sdl : public ITouch {
|
||||
bool init(void) override { return true; }
|
||||
void wakeup(void) override {}
|
||||
void sleep(void) override {}
|
||||
bool isEnable(void) override { return true; };
|
||||
uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override { return 0; }
|
||||
bool init(void) override { return true; }
|
||||
void wakeup(void) override {}
|
||||
void sleep(void) override {}
|
||||
bool isEnable(void) override { return true; };
|
||||
uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override { return 0; }
|
||||
};
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
struct Panel_sdl : public Panel_FrameBufferBase {
|
||||
static constexpr size_t EMULATED_GPIO_MAX = 128;
|
||||
static volatile uint8_t _gpio_dummy_values[EMULATED_GPIO_MAX];
|
||||
static constexpr size_t EMULATED_GPIO_MAX = 128;
|
||||
static volatile uint8_t _gpio_dummy_values[EMULATED_GPIO_MAX];
|
||||
|
||||
public:
|
||||
Panel_sdl(void);
|
||||
virtual ~Panel_sdl(void);
|
||||
public:
|
||||
Panel_sdl(void);
|
||||
virtual ~Panel_sdl(void);
|
||||
|
||||
bool init(bool use_reset) override;
|
||||
bool init(bool use_reset) override;
|
||||
|
||||
color_depth_t setColorDepth(color_depth_t depth) override;
|
||||
color_depth_t setColorDepth(color_depth_t depth) override;
|
||||
|
||||
void display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) override;
|
||||
void display(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h) override;
|
||||
|
||||
// void setInvert(bool invert) override {}
|
||||
void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) override;
|
||||
void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) override;
|
||||
void writeBlock(uint32_t rawcolor, uint32_t length) override;
|
||||
void writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param,
|
||||
bool use_dma) override;
|
||||
void writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param) override;
|
||||
void writePixels(pixelcopy_t *param, uint32_t len, bool use_dma) override;
|
||||
// void setInvert(bool invert) override {}
|
||||
void drawPixelPreclipped(uint_fast16_t x, uint_fast16_t y, uint32_t rawcolor) override;
|
||||
void writeFillRectPreclipped(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, uint32_t rawcolor) override;
|
||||
void writeBlock(uint32_t rawcolor, uint32_t length) override;
|
||||
void writeImage(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param, bool use_dma) override;
|
||||
void writeImageARGB(uint_fast16_t x, uint_fast16_t y, uint_fast16_t w, uint_fast16_t h, pixelcopy_t *param) override;
|
||||
void writePixels(pixelcopy_t *param, uint32_t len, bool use_dma) override;
|
||||
|
||||
uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override;
|
||||
uint_fast8_t getTouchRaw(touch_point_t *tp, uint_fast8_t count) override;
|
||||
|
||||
void setWindowTitle(const char *title);
|
||||
void setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y);
|
||||
void setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y);
|
||||
void setFrameRotation(uint_fast16_t frame_rotaion);
|
||||
void setBrightness(uint8_t brightness) override{};
|
||||
void setWindowTitle(const char *title);
|
||||
void setScaling(uint_fast8_t scaling_x, uint_fast8_t scaling_y);
|
||||
void setFrameImage(const void *frame_image, int frame_width, int frame_height, int inner_x, int inner_y);
|
||||
void setFrameRotation(uint_fast16_t frame_rotaion);
|
||||
void setBrightness(uint8_t brightness) override{};
|
||||
|
||||
static volatile void gpio_hi(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 1; }
|
||||
static volatile void gpio_lo(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 0; }
|
||||
static volatile bool gpio_in(uint32_t pin) { return _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)]; }
|
||||
static volatile void gpio_hi(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 1; }
|
||||
static volatile void gpio_lo(uint32_t pin) { _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)] = 0; }
|
||||
static volatile bool gpio_in(uint32_t pin) { return _gpio_dummy_values[pin & (EMULATED_GPIO_MAX - 1)]; }
|
||||
|
||||
static int setup(void);
|
||||
static int loop(void);
|
||||
static int close(void);
|
||||
static int setup(void);
|
||||
static int loop(void);
|
||||
static int close(void);
|
||||
|
||||
static int main(int (*fn)(bool *), uint32_t msec_step_exec = 512);
|
||||
static int main(int (*fn)(bool *), uint32_t msec_step_exec = 512);
|
||||
|
||||
static void setShortcutKeymod(SDL_Keymod keymod) { _keymod = keymod; }
|
||||
static void setShortcutKeymod(SDL_Keymod keymod) { _keymod = keymod; }
|
||||
|
||||
struct KeyCodeMapping_t {
|
||||
SDL_KeyCode keycode = SDLK_UNKNOWN;
|
||||
uint8_t gpio = 0;
|
||||
};
|
||||
static void addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio);
|
||||
static int getKeyCodeMapping(SDL_KeyCode keyCode);
|
||||
struct KeyCodeMapping_t {
|
||||
SDL_KeyCode keycode = SDLK_UNKNOWN;
|
||||
uint8_t gpio = 0;
|
||||
};
|
||||
static void addKeyCodeMapping(SDL_KeyCode keyCode, uint8_t gpio);
|
||||
static int getKeyCodeMapping(SDL_KeyCode keyCode);
|
||||
|
||||
protected:
|
||||
const char *_window_title = "LGFX Simulator";
|
||||
SDL_mutex *_sdl_mutex = nullptr;
|
||||
|
||||
void sdl_create(monitor_t *m);
|
||||
void sdl_update(void);
|
||||
|
||||
touch_point_t _touch_point;
|
||||
monitor_t monitor;
|
||||
|
||||
rgb888_t *_texturebuf = nullptr;
|
||||
uint_fast16_t _modified_counter;
|
||||
uint_fast16_t _texupdate_counter;
|
||||
uint_fast16_t _display_counter;
|
||||
bool _invalidated;
|
||||
|
||||
static void _event_proc(void);
|
||||
static void _update_proc(void);
|
||||
static void _update_scaling(monitor_t *m, float sx, float sy);
|
||||
void sdl_invalidate(void) { _invalidated = true; }
|
||||
void render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle);
|
||||
bool initFrameBuffer(size_t width, size_t height);
|
||||
void deinitFrameBuffer(void);
|
||||
|
||||
static SDL_Keymod _keymod;
|
||||
|
||||
struct lock_t {
|
||||
lock_t(Panel_sdl *parent);
|
||||
~lock_t();
|
||||
|
||||
protected:
|
||||
const char *_window_title = "LGFX Simulator";
|
||||
SDL_mutex *_sdl_mutex = nullptr;
|
||||
|
||||
void sdl_create(monitor_t *m);
|
||||
void sdl_update(void);
|
||||
|
||||
touch_point_t _touch_point;
|
||||
monitor_t monitor;
|
||||
|
||||
rgb888_t *_texturebuf = nullptr;
|
||||
uint_fast16_t _modified_counter;
|
||||
uint_fast16_t _texupdate_counter;
|
||||
uint_fast16_t _display_counter;
|
||||
bool _invalidated;
|
||||
|
||||
static void _event_proc(void);
|
||||
static void _update_proc(void);
|
||||
static void _update_scaling(monitor_t *m, float sx, float sy);
|
||||
void sdl_invalidate(void) { _invalidated = true; }
|
||||
void render_texture(SDL_Texture *texture, int tx, int ty, int tw, int th, float angle);
|
||||
bool initFrameBuffer(size_t width, size_t height);
|
||||
void deinitFrameBuffer(void);
|
||||
|
||||
static SDL_Keymod _keymod;
|
||||
|
||||
struct lock_t {
|
||||
lock_t(Panel_sdl *parent);
|
||||
~lock_t();
|
||||
|
||||
protected:
|
||||
Panel_sdl *_parent;
|
||||
};
|
||||
Panel_sdl *_parent;
|
||||
};
|
||||
};
|
||||
//----------------------------------------------------------------------------
|
||||
} // namespace v1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
struct PointStruct {
|
||||
int x;
|
||||
int y;
|
||||
int x;
|
||||
int y;
|
||||
};
|
||||
+1211
-1272
File diff suppressed because it is too large
Load Diff
+513
-535
File diff suppressed because it is too large
Load Diff
@@ -72,9 +72,9 @@
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||
defined(HACKADAY_COMMUNICATOR) || defined(USE_ST7796)) && \
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || \
|
||||
defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(HACKADAY_COMMUNICATOR) || \
|
||||
defined(USE_ST7796)) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
// The screen is bigger so use bigger fonts
|
||||
#define FONT_SMALL FONT_MEDIUM_LOCAL // Height: 19
|
||||
|
||||
+416
-428
@@ -13,46 +13,43 @@
|
||||
#include <OLEDDisplay.h>
|
||||
#include <graphics/images.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace graphics {
|
||||
|
||||
ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth)
|
||||
{
|
||||
ScreenResolution determineScreenResolution(int16_t screenheight, int16_t screenwidth) {
|
||||
|
||||
#ifdef FORCE_LOW_RES
|
||||
return ScreenResolution::Low;
|
||||
return ScreenResolution::Low;
|
||||
#else
|
||||
// Unit C6L and other ultra low res screens
|
||||
if (screenwidth <= 64 || screenheight <= 48) {
|
||||
return ScreenResolution::UltraLow;
|
||||
}
|
||||
// Unit C6L and other ultra low res screens
|
||||
if (screenwidth <= 64 || screenheight <= 48) {
|
||||
return ScreenResolution::UltraLow;
|
||||
}
|
||||
|
||||
// Standard OLED screens
|
||||
if (screenwidth > 128 && screenheight <= 64) {
|
||||
return ScreenResolution::Low;
|
||||
}
|
||||
|
||||
// High Resolutions screens like T114, TDeck, TLora Pager, etc
|
||||
if (screenwidth > 128) {
|
||||
return ScreenResolution::High;
|
||||
}
|
||||
|
||||
// Default to low resolution
|
||||
// Standard OLED screens
|
||||
if (screenwidth > 128 && screenheight <= 64) {
|
||||
return ScreenResolution::Low;
|
||||
}
|
||||
|
||||
// High Resolutions screens like T114, TDeck, TLora Pager, etc
|
||||
if (screenwidth > 128) {
|
||||
return ScreenResolution::High;
|
||||
}
|
||||
|
||||
// Default to low resolution
|
||||
return ScreenResolution::Low;
|
||||
#endif
|
||||
}
|
||||
|
||||
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second)
|
||||
{
|
||||
hour = 0;
|
||||
minute = 0;
|
||||
second = 0;
|
||||
if (rtc_sec == 0)
|
||||
return;
|
||||
uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
hour = hms / SEC_PER_HOUR;
|
||||
minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
second = hms % SEC_PER_MIN;
|
||||
void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second) {
|
||||
hour = 0;
|
||||
minute = 0;
|
||||
second = 0;
|
||||
if (rtc_sec == 0)
|
||||
return;
|
||||
uint32_t hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
hour = hms / SEC_PER_HOUR;
|
||||
minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
second = hms % SEC_PER_MIN;
|
||||
}
|
||||
|
||||
// === Shared External State ===
|
||||
@@ -68,457 +65,448 @@ uint32_t lastMailBlink = 0;
|
||||
// *********************************
|
||||
// * Rounded Header when inverted *
|
||||
// *********************************
|
||||
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r)
|
||||
{
|
||||
// Draw the center and side rectangles
|
||||
display->fillRect(x + r, y, w - 2 * r, h); // center bar
|
||||
display->fillRect(x, y + r, r, h - 2 * r); // left edge
|
||||
display->fillRect(x + w - r, y + r, r, h - 2 * r); // right edge
|
||||
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r) {
|
||||
// Draw the center and side rectangles
|
||||
display->fillRect(x + r, y, w - 2 * r, h); // center bar
|
||||
display->fillRect(x, y + r, r, h - 2 * r); // left edge
|
||||
display->fillRect(x + w - r, y + r, r, h - 2 * r); // right edge
|
||||
|
||||
// Draw the rounded corners using filled circles
|
||||
display->fillCircle(x + r + 1, y + r, r); // top-left
|
||||
display->fillCircle(x + w - r - 1, y + r, r); // top-right
|
||||
display->fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left
|
||||
display->fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right
|
||||
// Draw the rounded corners using filled circles
|
||||
display->fillCircle(x + r + 1, y + r, r); // top-left
|
||||
display->fillCircle(x + w - r - 1, y + r, r); // top-right
|
||||
display->fillCircle(x + r + 1, y + h - r - 1, r); // bottom-left
|
||||
display->fillCircle(x + w - r - 1, y + h - r - 1, r); // bottom-right
|
||||
}
|
||||
|
||||
// *************************
|
||||
// * Common Header Drawing *
|
||||
// *************************
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date)
|
||||
{
|
||||
constexpr int HEADER_OFFSET_Y = 1;
|
||||
y += HEADER_OFFSET_Y;
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr, bool force_no_invert, bool show_date) {
|
||||
constexpr int HEADER_OFFSET_Y = 1;
|
||||
y += HEADER_OFFSET_Y;
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
const int xOffset = 4;
|
||||
const int highlightHeight = FONT_HEIGHT_SMALL - 1;
|
||||
const bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
||||
const bool isBold = config.display.heading_bold;
|
||||
const int xOffset = 4;
|
||||
const int highlightHeight = FONT_HEIGHT_SMALL - 1;
|
||||
const bool isInverted = (config.display.displaymode != meshtastic_Config_DisplayConfig_DisplayMode_INVERTED);
|
||||
const bool isBold = config.display.heading_bold;
|
||||
|
||||
const int screenW = display->getWidth();
|
||||
const int screenH = display->getHeight();
|
||||
const int screenW = display->getWidth();
|
||||
const int screenH = display->getHeight();
|
||||
|
||||
if (!force_no_invert) {
|
||||
// === Inverted Header Background ===
|
||||
if (isInverted) {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||
display->setColor(WHITE);
|
||||
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(0, 20, screenW, 20);
|
||||
} else {
|
||||
display->drawLine(0, 14, screenW, 14);
|
||||
}
|
||||
}
|
||||
|
||||
// === Screen Title ===
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(SCREEN_WIDTH / 2, y, titleStr);
|
||||
if (config.display.heading_bold) {
|
||||
display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr);
|
||||
}
|
||||
}
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
// === Battery State ===
|
||||
int chargePercent = powerStatus->getBatteryChargePercent();
|
||||
bool isCharging = powerStatus->getIsCharging();
|
||||
bool usbPowered = powerStatus->getHasUSB();
|
||||
|
||||
if (chargePercent >= 100) {
|
||||
isCharging = false;
|
||||
}
|
||||
if (chargePercent == 101) {
|
||||
usbPowered = true; // Forcing this flag on for the express purpose that some devices have no concept of having a USB cable
|
||||
// plugged in
|
||||
if (!force_no_invert) {
|
||||
// === Inverted Header Background ===
|
||||
if (isInverted) {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||
display->setColor(WHITE);
|
||||
drawRoundedHighlight(display, x, y, screenW, highlightHeight, 2);
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, 0, screenW, highlightHeight + 2);
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawLine(0, 20, screenW, 20);
|
||||
} else {
|
||||
display->drawLine(0, 14, screenW, 14);
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t now = millis();
|
||||
// === Screen Title ===
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->drawString(SCREEN_WIDTH / 2, y, titleStr);
|
||||
if (config.display.heading_bold) {
|
||||
display->drawString((SCREEN_WIDTH / 2) + 1, y, titleStr);
|
||||
}
|
||||
}
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
// === Battery State ===
|
||||
int chargePercent = powerStatus->getBatteryChargePercent();
|
||||
bool isCharging = powerStatus->getIsCharging();
|
||||
bool usbPowered = powerStatus->getHasUSB();
|
||||
|
||||
if (chargePercent >= 100) {
|
||||
isCharging = false;
|
||||
}
|
||||
if (chargePercent == 101) {
|
||||
usbPowered = true; // Forcing this flag on for the express purpose that some devices have no concept of having a USB
|
||||
// cable plugged in
|
||||
}
|
||||
|
||||
uint32_t now = millis();
|
||||
|
||||
#ifndef USE_EINK
|
||||
if (isCharging && now - lastBlinkShared > 500) {
|
||||
isBoltVisibleShared = !isBoltVisibleShared;
|
||||
lastBlinkShared = now;
|
||||
}
|
||||
if (isCharging && now - lastBlinkShared > 500) {
|
||||
isBoltVisibleShared = !isBoltVisibleShared;
|
||||
lastBlinkShared = now;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH);
|
||||
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
||||
bool useHorizontalBattery = (currentResolution == ScreenResolution::High && screenW >= screenH);
|
||||
const int textY = y + (highlightHeight - FONT_HEIGHT_SMALL) / 2;
|
||||
|
||||
int batteryX = 1;
|
||||
int batteryY = HEADER_OFFSET_Y + 1;
|
||||
int batteryX = 1;
|
||||
int batteryY = HEADER_OFFSET_Y + 1;
|
||||
#if !defined(M5STACK_UNITC6L)
|
||||
// === Battery Icons ===
|
||||
if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging
|
||||
batteryX += 1;
|
||||
batteryY += 2;
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution);
|
||||
batteryX += 20; // Icon + 1 pixel
|
||||
} else {
|
||||
display->drawXbm(batteryX, batteryY, 10, 8, imgUSB);
|
||||
batteryX += 11; // Icon + 1 pixel
|
||||
}
|
||||
// === Battery Icons ===
|
||||
if (usbPowered && !isCharging) { // This is a basic check to determine USB Powered is flagged but not charging
|
||||
batteryX += 1;
|
||||
batteryY += 2;
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->drawXbm(batteryX, batteryY, 19, 12, imgUSB_HighResolution);
|
||||
batteryX += 20; // Icon + 1 pixel
|
||||
} else {
|
||||
display->drawXbm(batteryX, batteryY, 10, 8, imgUSB);
|
||||
batteryX += 11; // Icon + 1 pixel
|
||||
}
|
||||
} else {
|
||||
if (useHorizontalBattery) {
|
||||
batteryX += 1;
|
||||
batteryY += 2;
|
||||
display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom);
|
||||
display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top);
|
||||
if (isCharging && isBoltVisibleShared)
|
||||
display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h);
|
||||
else {
|
||||
display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY);
|
||||
display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12);
|
||||
int fillWidth = 14 * chargePercent / 100;
|
||||
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11);
|
||||
}
|
||||
batteryX += 18; // Icon + 2 pixels
|
||||
} else {
|
||||
if (useHorizontalBattery) {
|
||||
batteryX += 1;
|
||||
batteryY += 2;
|
||||
display->drawXbm(batteryX, batteryY, 9, 13, batteryBitmap_h_bottom);
|
||||
display->drawXbm(batteryX + 9, batteryY, 9, 13, batteryBitmap_h_top);
|
||||
if (isCharging && isBoltVisibleShared)
|
||||
display->drawXbm(batteryX + 4, batteryY, 9, 13, lightning_bolt_h);
|
||||
else {
|
||||
display->drawLine(batteryX + 5, batteryY, batteryX + 10, batteryY);
|
||||
display->drawLine(batteryX + 5, batteryY + 12, batteryX + 10, batteryY + 12);
|
||||
int fillWidth = 14 * chargePercent / 100;
|
||||
display->fillRect(batteryX + 1, batteryY + 1, fillWidth, 11);
|
||||
}
|
||||
batteryX += 18; // Icon + 2 pixels
|
||||
} else {
|
||||
#ifdef USE_EINK
|
||||
batteryY += 2;
|
||||
batteryY += 2;
|
||||
#endif
|
||||
display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v);
|
||||
if (isCharging && isBoltVisibleShared)
|
||||
display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v);
|
||||
else {
|
||||
display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v);
|
||||
int fillHeight = 8 * chargePercent / 100;
|
||||
int fillY = batteryY - fillHeight;
|
||||
display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight);
|
||||
}
|
||||
batteryX += 9; // Icon + 2 pixels
|
||||
}
|
||||
display->drawXbm(batteryX, batteryY, 7, 11, batteryBitmap_v);
|
||||
if (isCharging && isBoltVisibleShared)
|
||||
display->drawXbm(batteryX + 1, batteryY + 3, 5, 5, lightning_bolt_v);
|
||||
else {
|
||||
display->drawXbm(batteryX - 1, batteryY + 4, 8, 3, batteryBitmap_sidegaps_v);
|
||||
int fillHeight = 8 * chargePercent / 100;
|
||||
int fillY = batteryY - fillHeight;
|
||||
display->fillRect(batteryX + 1, fillY + 10, 5, fillHeight);
|
||||
}
|
||||
batteryX += 9; // Icon + 2 pixels
|
||||
}
|
||||
}
|
||||
|
||||
if (chargePercent != 101) {
|
||||
// === Battery % Display ===
|
||||
char chargeStr[4];
|
||||
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
|
||||
int chargeNumWidth = display->getStringWidth(chargeStr);
|
||||
display->drawString(batteryX, textY, chargeStr);
|
||||
display->drawString(batteryX + chargeNumWidth - 1, textY, "%");
|
||||
if (isBold) {
|
||||
display->drawString(batteryX + 1, textY, chargeStr);
|
||||
display->drawString(batteryX + chargeNumWidth, textY, "%");
|
||||
}
|
||||
if (chargePercent != 101) {
|
||||
// === Battery % Display ===
|
||||
char chargeStr[4];
|
||||
snprintf(chargeStr, sizeof(chargeStr), "%d", chargePercent);
|
||||
int chargeNumWidth = display->getStringWidth(chargeStr);
|
||||
display->drawString(batteryX, textY, chargeStr);
|
||||
display->drawString(batteryX + chargeNumWidth - 1, textY, "%");
|
||||
if (isBold) {
|
||||
display->drawString(batteryX + 1, textY, chargeStr);
|
||||
display->drawString(batteryX + chargeNumWidth, textY, "%");
|
||||
}
|
||||
}
|
||||
|
||||
// === Time and Right-aligned Icons ===
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
char timeStr[10] = "--:--"; // Fallback display
|
||||
int timeStrWidth = display->getStringWidth("12:34"); // Default alignment
|
||||
int timeX = screenW - xOffset - timeStrWidth + 4;
|
||||
// === Time and Right-aligned Icons ===
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true);
|
||||
char timeStr[10] = "--:--"; // Fallback display
|
||||
int timeStrWidth = display->getStringWidth("12:34"); // Default alignment
|
||||
int timeX = screenW - xOffset - timeStrWidth + 4;
|
||||
|
||||
if (rtc_sec > 0) {
|
||||
// === Build Time String ===
|
||||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
int hour, minute, second;
|
||||
graphics::decomposeTime(rtc_sec, hour, minute, second);
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
||||
if (rtc_sec > 0) {
|
||||
// === Build Time String ===
|
||||
long hms = (rtc_sec % SEC_PER_DAY + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
int hour, minute, second;
|
||||
graphics::decomposeTime(rtc_sec, hour, minute, second);
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d", hour, minute);
|
||||
|
||||
// === Build Date String ===
|
||||
char datetimeStr[25];
|
||||
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false);
|
||||
char dateLine[40];
|
||||
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr);
|
||||
} else {
|
||||
if (hasUnreadMessage) {
|
||||
snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[5]);
|
||||
} else {
|
||||
snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[2]);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.display.use_12h_clock) {
|
||||
bool isPM = hour >= 12;
|
||||
hour %= 12;
|
||||
if (hour == 0)
|
||||
hour = 12;
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a");
|
||||
}
|
||||
|
||||
if (show_date) {
|
||||
timeStrWidth = display->getStringWidth(dateLine);
|
||||
} else {
|
||||
timeStrWidth = display->getStringWidth(timeStr);
|
||||
}
|
||||
timeX = screenW - xOffset - timeStrWidth + 3;
|
||||
|
||||
// === Show Mail or Mute Icon to the Left of Time ===
|
||||
int iconRightEdge = timeX - 2;
|
||||
|
||||
bool showMail = false;
|
||||
|
||||
#ifndef USE_EINK
|
||||
if (hasUnreadMessage) {
|
||||
if (now - lastMailBlink > 500) {
|
||||
isMailIconVisible = !isMailIconVisible;
|
||||
lastMailBlink = now;
|
||||
}
|
||||
showMail = isMailIconVisible;
|
||||
}
|
||||
#else
|
||||
if (hasUnreadMessage) {
|
||||
showMail = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (showMail) {
|
||||
if (useHorizontalBattery) {
|
||||
int iconW = 16, iconH = 12;
|
||||
int iconX = iconRightEdge - iconW;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||||
if (isInverted && !force_no_invert) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
display->drawRect(iconX, iconY, iconW + 1, iconH);
|
||||
display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||||
display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||||
} else {
|
||||
int iconX = iconRightEdge - (mail_width - 2);
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
if (isInverted && !force_no_invert) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||||
}
|
||||
} else if (externalNotificationModule->getMute()) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||
|
||||
if (isInverted && !force_no_invert) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
|
||||
} else {
|
||||
int iconX = iconRightEdge - mute_symbol_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
|
||||
if (isInverted && !force_no_invert) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol);
|
||||
}
|
||||
}
|
||||
|
||||
if (show_date) {
|
||||
// === Draw Date ===
|
||||
display->drawString(timeX, textY, dateLine);
|
||||
if (isBold)
|
||||
display->drawString(timeX - 1, textY, dateLine);
|
||||
} else {
|
||||
// === Draw Time ===
|
||||
display->drawString(timeX, textY, timeStr);
|
||||
if (isBold)
|
||||
display->drawString(timeX - 1, textY, timeStr);
|
||||
}
|
||||
|
||||
} else {
|
||||
// === No Time Available: Mail/Mute Icon Moves to Far Right ===
|
||||
int iconRightEdge = screenW - xOffset;
|
||||
|
||||
bool showMail = false;
|
||||
|
||||
#ifndef USE_EINK
|
||||
if (hasUnreadMessage) {
|
||||
if (now - lastMailBlink > 500) {
|
||||
isMailIconVisible = !isMailIconVisible;
|
||||
lastMailBlink = now;
|
||||
}
|
||||
showMail = isMailIconVisible;
|
||||
}
|
||||
#else
|
||||
if (hasUnreadMessage) {
|
||||
showMail = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (showMail) {
|
||||
if (useHorizontalBattery) {
|
||||
int iconW = 16, iconH = 12;
|
||||
int iconX = iconRightEdge - iconW;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||||
display->drawRect(iconX, iconY, iconW + 1, iconH);
|
||||
display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||||
display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||||
} else {
|
||||
int iconX = iconRightEdge - mail_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||||
}
|
||||
} else if (externalNotificationModule->getMute()) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
|
||||
} else {
|
||||
int iconX = iconRightEdge - mute_symbol_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
display->setColor(WHITE); // Reset for other UI
|
||||
}
|
||||
|
||||
const int *getTextPositions(OLEDDisplay *display)
|
||||
{
|
||||
static int textPositions[7]; // Static array that persists beyond function scope
|
||||
// === Build Date String ===
|
||||
char datetimeStr[25];
|
||||
UIRenderer::formatDateTime(datetimeStr, sizeof(datetimeStr), rtc_sec, display, false);
|
||||
char dateLine[40];
|
||||
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
textPositions[0] = textZeroLine;
|
||||
textPositions[1] = textFirstLine_medium;
|
||||
textPositions[2] = textSecondLine_medium;
|
||||
textPositions[3] = textThirdLine_medium;
|
||||
textPositions[4] = textFourthLine_medium;
|
||||
textPositions[5] = textFifthLine_medium;
|
||||
textPositions[6] = textSixthLine_medium;
|
||||
snprintf(dateLine, sizeof(dateLine), "%s", datetimeStr);
|
||||
} else {
|
||||
textPositions[0] = textZeroLine;
|
||||
textPositions[1] = textFirstLine;
|
||||
textPositions[2] = textSecondLine;
|
||||
textPositions[3] = textThirdLine;
|
||||
textPositions[4] = textFourthLine;
|
||||
textPositions[5] = textFifthLine;
|
||||
textPositions[6] = textSixthLine;
|
||||
if (hasUnreadMessage) {
|
||||
snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[5]);
|
||||
} else {
|
||||
snprintf(dateLine, sizeof(dateLine), "%s", &datetimeStr[2]);
|
||||
}
|
||||
}
|
||||
return textPositions;
|
||||
|
||||
if (config.display.use_12h_clock) {
|
||||
bool isPM = hour >= 12;
|
||||
hour %= 12;
|
||||
if (hour == 0)
|
||||
hour = 12;
|
||||
snprintf(timeStr, sizeof(timeStr), "%d:%02d%s", hour, minute, isPM ? "p" : "a");
|
||||
}
|
||||
|
||||
if (show_date) {
|
||||
timeStrWidth = display->getStringWidth(dateLine);
|
||||
} else {
|
||||
timeStrWidth = display->getStringWidth(timeStr);
|
||||
}
|
||||
timeX = screenW - xOffset - timeStrWidth + 3;
|
||||
|
||||
// === Show Mail or Mute Icon to the Left of Time ===
|
||||
int iconRightEdge = timeX - 2;
|
||||
|
||||
bool showMail = false;
|
||||
|
||||
#ifndef USE_EINK
|
||||
if (hasUnreadMessage) {
|
||||
if (now - lastMailBlink > 500) {
|
||||
isMailIconVisible = !isMailIconVisible;
|
||||
lastMailBlink = now;
|
||||
}
|
||||
showMail = isMailIconVisible;
|
||||
}
|
||||
#else
|
||||
if (hasUnreadMessage) {
|
||||
showMail = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (showMail) {
|
||||
if (useHorizontalBattery) {
|
||||
int iconW = 16, iconH = 12;
|
||||
int iconX = iconRightEdge - iconW;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||||
if (isInverted && !force_no_invert) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(iconX - 1, iconY - 1, iconW + 3, iconH + 2);
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
display->drawRect(iconX, iconY, iconW + 1, iconH);
|
||||
display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||||
display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||||
} else {
|
||||
int iconX = iconRightEdge - (mail_width - 2);
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
if (isInverted && !force_no_invert) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(iconX - 1, iconY - 1, mail_width + 2, mail_height + 2);
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||||
}
|
||||
} else if (externalNotificationModule->getMute()) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||
|
||||
if (isInverted && !force_no_invert) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_big_width + 2, mute_symbol_big_height + 2);
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
|
||||
} else {
|
||||
int iconX = iconRightEdge - mute_symbol_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
|
||||
if (isInverted && !force_no_invert) {
|
||||
display->setColor(WHITE);
|
||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
|
||||
display->setColor(BLACK);
|
||||
} else {
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(iconX - 1, iconY - 1, mute_symbol_width + 2, mute_symbol_height + 2);
|
||||
display->setColor(WHITE);
|
||||
}
|
||||
display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol);
|
||||
}
|
||||
}
|
||||
|
||||
if (show_date) {
|
||||
// === Draw Date ===
|
||||
display->drawString(timeX, textY, dateLine);
|
||||
if (isBold)
|
||||
display->drawString(timeX - 1, textY, dateLine);
|
||||
} else {
|
||||
// === Draw Time ===
|
||||
display->drawString(timeX, textY, timeStr);
|
||||
if (isBold)
|
||||
display->drawString(timeX - 1, textY, timeStr);
|
||||
}
|
||||
|
||||
} else {
|
||||
// === No Time Available: Mail/Mute Icon Moves to Far Right ===
|
||||
int iconRightEdge = screenW - xOffset;
|
||||
|
||||
bool showMail = false;
|
||||
|
||||
#ifndef USE_EINK
|
||||
if (hasUnreadMessage) {
|
||||
if (now - lastMailBlink > 500) {
|
||||
isMailIconVisible = !isMailIconVisible;
|
||||
lastMailBlink = now;
|
||||
}
|
||||
showMail = isMailIconVisible;
|
||||
}
|
||||
#else
|
||||
if (hasUnreadMessage) {
|
||||
showMail = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (showMail) {
|
||||
if (useHorizontalBattery) {
|
||||
int iconW = 16, iconH = 12;
|
||||
int iconX = iconRightEdge - iconW;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - iconH) / 2 - 1;
|
||||
display->drawRect(iconX, iconY, iconW + 1, iconH);
|
||||
display->drawLine(iconX, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||||
display->drawLine(iconX + iconW, iconY, iconX + iconW / 2, iconY + iconH - 4);
|
||||
} else {
|
||||
int iconX = iconRightEdge - mail_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
display->drawXbm(iconX, iconY, mail_width, mail_height, mail);
|
||||
}
|
||||
} else if (externalNotificationModule->getMute()) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
int iconX = iconRightEdge - mute_symbol_big_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mute_symbol_big_height) / 2;
|
||||
display->drawXbm(iconX, iconY, mute_symbol_big_width, mute_symbol_big_height, mute_symbol_big);
|
||||
} else {
|
||||
int iconX = iconRightEdge - mute_symbol_width;
|
||||
int iconY = textY + (FONT_HEIGHT_SMALL - mail_height) / 2;
|
||||
display->drawXbm(iconX, iconY, mute_symbol_width, mute_symbol_height, mute_symbol);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
display->setColor(WHITE); // Reset for other UI
|
||||
}
|
||||
|
||||
const int *getTextPositions(OLEDDisplay *display) {
|
||||
static int textPositions[7]; // Static array that persists beyond function scope
|
||||
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
textPositions[0] = textZeroLine;
|
||||
textPositions[1] = textFirstLine_medium;
|
||||
textPositions[2] = textSecondLine_medium;
|
||||
textPositions[3] = textThirdLine_medium;
|
||||
textPositions[4] = textFourthLine_medium;
|
||||
textPositions[5] = textFifthLine_medium;
|
||||
textPositions[6] = textSixthLine_medium;
|
||||
} else {
|
||||
textPositions[0] = textZeroLine;
|
||||
textPositions[1] = textFirstLine;
|
||||
textPositions[2] = textSecondLine;
|
||||
textPositions[3] = textThirdLine;
|
||||
textPositions[4] = textFourthLine;
|
||||
textPositions[5] = textFifthLine;
|
||||
textPositions[6] = textSixthLine;
|
||||
}
|
||||
return textPositions;
|
||||
}
|
||||
|
||||
// *************************
|
||||
// * Common Footer Drawing *
|
||||
// *************************
|
||||
void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y)
|
||||
{
|
||||
bool drawConnectionState = false;
|
||||
if (service->api_state == service->STATE_BLE || service->api_state == service->STATE_WIFI ||
|
||||
service->api_state == service->STATE_SERIAL || service->api_state == service->STATE_PACKET ||
|
||||
service->api_state == service->STATE_HTTP || service->api_state == service->STATE_ETH) {
|
||||
drawConnectionState = true;
|
||||
}
|
||||
void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y) {
|
||||
bool drawConnectionState = false;
|
||||
if (service->api_state == service->STATE_BLE || service->api_state == service->STATE_WIFI || service->api_state == service->STATE_SERIAL ||
|
||||
service->api_state == service->STATE_PACKET || service->api_state == service->STATE_HTTP || service->api_state == service->STATE_ETH) {
|
||||
drawConnectionState = true;
|
||||
}
|
||||
|
||||
if (drawConnectionState) {
|
||||
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
||||
(connection_icon_height * scale) + (2 * scale));
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||
int iconX = 0;
|
||||
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
||||
if (drawConnectionState) {
|
||||
const int scale = (currentResolution == ScreenResolution::High) ? 2 : 1;
|
||||
display->setColor(BLACK);
|
||||
display->fillRect(0, SCREEN_HEIGHT - (1 * scale) - (connection_icon_height * scale), (connection_icon_width * scale),
|
||||
(connection_icon_height * scale) + (2 * scale));
|
||||
display->setColor(WHITE);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
const int bytesPerRow = (connection_icon_width + 7) / 8;
|
||||
int iconX = 0;
|
||||
int iconY = SCREEN_HEIGHT - (connection_icon_height * 2);
|
||||
|
||||
for (int yy = 0; yy < connection_icon_height; ++yy) {
|
||||
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
|
||||
for (int xx = 0; xx < connection_icon_width; ++xx) {
|
||||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
||||
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
|
||||
if (byteVal & bitMask) {
|
||||
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height,
|
||||
connection_icon);
|
||||
for (int yy = 0; yy < connection_icon_height; ++yy) {
|
||||
const uint8_t *rowPtr = connection_icon + yy * bytesPerRow;
|
||||
for (int xx = 0; xx < connection_icon_width; ++xx) {
|
||||
const uint8_t byteVal = pgm_read_byte(rowPtr + (xx >> 3));
|
||||
const uint8_t bitMask = 1U << (xx & 7); // XBM is LSB-first
|
||||
if (byteVal & bitMask) {
|
||||
display->fillRect(iconX + xx * scale, iconY + yy * scale, scale, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
display->drawXbm(0, SCREEN_HEIGHT - connection_icon_height, connection_icon_width, connection_icon_height, connection_icon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool isAllowedPunctuation(char c)
|
||||
{
|
||||
const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ ";
|
||||
return allowed.find(c) != std::string::npos;
|
||||
bool isAllowedPunctuation(char c) {
|
||||
const std::string allowed = ".,!?;:-_()[]{}'\"@#$/\\&+=%~^ ";
|
||||
return allowed.find(c) != std::string::npos;
|
||||
}
|
||||
|
||||
static void replaceAll(std::string &s, const std::string &from, const std::string &to)
|
||||
{
|
||||
if (from.empty())
|
||||
return;
|
||||
size_t pos = 0;
|
||||
while ((pos = s.find(from, pos)) != std::string::npos) {
|
||||
s.replace(pos, from.size(), to);
|
||||
pos += to.size();
|
||||
}
|
||||
static void replaceAll(std::string &s, const std::string &from, const std::string &to) {
|
||||
if (from.empty())
|
||||
return;
|
||||
size_t pos = 0;
|
||||
while ((pos = s.find(from, pos)) != std::string::npos) {
|
||||
s.replace(pos, from.size(), to);
|
||||
pos += to.size();
|
||||
}
|
||||
}
|
||||
|
||||
std::string sanitizeString(const std::string &input)
|
||||
{
|
||||
std::string output;
|
||||
bool inReplacement = false;
|
||||
std::string sanitizeString(const std::string &input) {
|
||||
std::string output;
|
||||
bool inReplacement = false;
|
||||
|
||||
// Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first.
|
||||
std::string s = input;
|
||||
// Make a mutable copy so we can normalize UTF-8 “smart punctuation” into ASCII first.
|
||||
std::string s = input;
|
||||
|
||||
// Curly single quotes: ‘ ’
|
||||
replaceAll(s, "\xE2\x80\x98", "'"); // U+2018
|
||||
replaceAll(s, "\xE2\x80\x99", "'"); // U+2019
|
||||
// Curly single quotes: ‘ ’
|
||||
replaceAll(s, "\xE2\x80\x98", "'"); // U+2018
|
||||
replaceAll(s, "\xE2\x80\x99", "'"); // U+2019
|
||||
|
||||
// Curly double quotes: “ ”
|
||||
replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C
|
||||
replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D
|
||||
// Curly double quotes: “ ”
|
||||
replaceAll(s, "\xE2\x80\x9C", "\""); // U+201C
|
||||
replaceAll(s, "\xE2\x80\x9D", "\""); // U+201D
|
||||
|
||||
// En dash / Em dash: – —
|
||||
replaceAll(s, "\xE2\x80\x93", "-"); // U+2013
|
||||
replaceAll(s, "\xE2\x80\x94", "-"); // U+2014
|
||||
// En dash / Em dash: – —
|
||||
replaceAll(s, "\xE2\x80\x93", "-"); // U+2013
|
||||
replaceAll(s, "\xE2\x80\x94", "-"); // U+2014
|
||||
|
||||
// Non-breaking space
|
||||
replaceAll(s, "\xC2\xA0", " "); // U+00A0
|
||||
// Non-breaking space
|
||||
replaceAll(s, "\xC2\xA0", " "); // U+00A0
|
||||
|
||||
// Now do your original sanitize pass over the normalized string.
|
||||
for (unsigned char uc : s) {
|
||||
char c = static_cast<char>(uc);
|
||||
if (std::isalnum(uc) || isAllowedPunctuation(c)) {
|
||||
output += c;
|
||||
inReplacement = false;
|
||||
} else {
|
||||
if (!inReplacement) {
|
||||
output += static_cast<char>(0xBF); // ISO-8859-1 for inverted question mark
|
||||
inReplacement = true;
|
||||
}
|
||||
}
|
||||
// Now do your original sanitize pass over the normalized string.
|
||||
for (unsigned char uc : s) {
|
||||
char c = static_cast<char>(uc);
|
||||
if (std::isalnum(uc) || isAllowedPunctuation(c)) {
|
||||
output += c;
|
||||
inReplacement = false;
|
||||
} else {
|
||||
if (!inReplacement) {
|
||||
output += static_cast<char>(0xBF); // ISO-8859-1 for inverted question mark
|
||||
inReplacement = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
#include <OLEDDisplay.h>
|
||||
#include <string>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace graphics {
|
||||
|
||||
// =======================
|
||||
// Shared UI Helpers
|
||||
@@ -51,8 +50,7 @@ void decomposeTime(uint32_t rtc_sec, int &hour, int &minute, int &second);
|
||||
void drawRoundedHighlight(OLEDDisplay *display, int16_t x, int16_t y, int16_t w, int16_t h, int16_t r);
|
||||
|
||||
// Shared battery/time/mail header
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false,
|
||||
bool show_date = false);
|
||||
void drawCommonHeader(OLEDDisplay *display, int16_t x, int16_t y, const char *titleStr = "", bool force_no_invert = false, bool show_date = false);
|
||||
|
||||
// Shared battery/time/mail header
|
||||
void drawCommonFooter(OLEDDisplay *display, int16_t x, int16_t y);
|
||||
|
||||
+1109
-1160
File diff suppressed because it is too large
Load Diff
+39
-40
@@ -12,55 +12,54 @@
|
||||
*
|
||||
* turn radio back on - currently with both on spi bus is fucked? or are we leaving chip select asserted?
|
||||
*/
|
||||
class TFTDisplay : public OLEDDisplay
|
||||
{
|
||||
public:
|
||||
/* constructor
|
||||
FIXME - the parameters are not used, just a temporary hack to keep working like the old displays
|
||||
*/
|
||||
TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C);
|
||||
class TFTDisplay : public OLEDDisplay {
|
||||
public:
|
||||
/* constructor
|
||||
FIXME - the parameters are not used, just a temporary hack to keep working like the old displays
|
||||
*/
|
||||
TFTDisplay(uint8_t, int, int, OLEDDISPLAY_GEOMETRY, HW_I2C);
|
||||
|
||||
// Destructor to clean up allocated memory
|
||||
~TFTDisplay();
|
||||
// Destructor to clean up allocated memory
|
||||
~TFTDisplay();
|
||||
|
||||
// Write the buffer to the display memory
|
||||
virtual void display() override { display(false); };
|
||||
virtual void display(bool fromBlank);
|
||||
void sdlLoop();
|
||||
// Write the buffer to the display memory
|
||||
virtual void display() override { display(false); };
|
||||
virtual void display(bool fromBlank);
|
||||
void sdlLoop();
|
||||
|
||||
// Turn the display upside down
|
||||
virtual void flipScreenVertically();
|
||||
// Turn the display upside down
|
||||
virtual void flipScreenVertically();
|
||||
|
||||
// Touch screen (static handlers)
|
||||
static bool hasTouch(void);
|
||||
static bool getTouch(int16_t *x, int16_t *y);
|
||||
// Touch screen (static handlers)
|
||||
static bool hasTouch(void);
|
||||
static bool getTouch(int16_t *x, int16_t *y);
|
||||
|
||||
// Functions for changing display brightness
|
||||
void setDisplayBrightness(uint8_t);
|
||||
// Functions for changing display brightness
|
||||
void setDisplayBrightness(uint8_t);
|
||||
|
||||
/**
|
||||
* shim to make the abstraction happy
|
||||
*
|
||||
*/
|
||||
void setDetected(uint8_t detected);
|
||||
/**
|
||||
* shim to make the abstraction happy
|
||||
*
|
||||
*/
|
||||
void setDetected(uint8_t detected);
|
||||
|
||||
/**
|
||||
* This is normally managed entirely by TFTDisplay, but some rare applications (heltec tracker) might need to replace the
|
||||
* default GPIO behavior with something a bit more complex.
|
||||
*
|
||||
* We (cruftily) make it static so that variant.cpp can access it without needing a ptr to the TFTDisplay instance.
|
||||
*/
|
||||
static GpioPin *backlightEnable;
|
||||
/**
|
||||
* This is normally managed entirely by TFTDisplay, but some rare applications (heltec tracker) might need to replace
|
||||
* the default GPIO behavior with something a bit more complex.
|
||||
*
|
||||
* We (cruftily) make it static so that variant.cpp can access it without needing a ptr to the TFTDisplay instance.
|
||||
*/
|
||||
static GpioPin *backlightEnable;
|
||||
|
||||
protected:
|
||||
// the header size of the buffer used, e.g. for the SPI command header
|
||||
virtual int getBufferOffset(void) override { return 0; }
|
||||
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;
|
||||
// Send a command to the display (low level function)
|
||||
virtual void sendCommand(uint8_t com) override;
|
||||
|
||||
// Connect to the display
|
||||
virtual bool connect() override;
|
||||
// Connect to the display
|
||||
virtual bool connect() override;
|
||||
|
||||
uint16_t *linePixelBuffer = nullptr;
|
||||
uint16_t *linePixelBuffer = nullptr;
|
||||
};
|
||||
+107
-110
@@ -4,120 +4,117 @@
|
||||
#include "mesh/NodeDB.h"
|
||||
#include <cstring>
|
||||
|
||||
bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo)
|
||||
{
|
||||
// Cache the result - avoid frequent recalculation
|
||||
static uint8_t hoursCached = 0, minutesCached = 0;
|
||||
static uint32_t daysAgoCached = 0;
|
||||
static uint32_t secondsAgoCached = 0;
|
||||
static bool validCached = false;
|
||||
bool deltaToTimestamp(uint32_t secondsAgo, uint8_t *hours, uint8_t *minutes, int32_t *daysAgo) {
|
||||
// Cache the result - avoid frequent recalculation
|
||||
static uint8_t hoursCached = 0, minutesCached = 0;
|
||||
static uint32_t daysAgoCached = 0;
|
||||
static uint32_t secondsAgoCached = 0;
|
||||
static bool validCached = false;
|
||||
|
||||
// Abort: if timezone not set
|
||||
if (strlen(config.device.tzdef) == 0) {
|
||||
validCached = false;
|
||||
return validCached;
|
||||
}
|
||||
|
||||
// Abort: if invalid pointers passed
|
||||
if (hours == nullptr || minutes == nullptr || daysAgo == nullptr) {
|
||||
validCached = false;
|
||||
return validCached;
|
||||
}
|
||||
|
||||
// Abort: if time seems invalid.. (> 6 months ago, probably seen before RTC set)
|
||||
if (secondsAgo > SEC_PER_DAY * 30UL * 6) {
|
||||
validCached = false;
|
||||
return validCached;
|
||||
}
|
||||
|
||||
// If repeated request, don't bother recalculating
|
||||
if (secondsAgo - secondsAgoCached < 60 && secondsAgoCached != 0) {
|
||||
if (validCached) {
|
||||
*hours = hoursCached;
|
||||
*minutes = minutesCached;
|
||||
*daysAgo = daysAgoCached;
|
||||
}
|
||||
return validCached;
|
||||
}
|
||||
|
||||
// Get local time
|
||||
uint32_t secondsRTC = getValidTime(RTCQuality::RTCQualityDevice, true); // Get local time
|
||||
|
||||
// Abort: if RTC not set
|
||||
if (!secondsRTC) {
|
||||
validCached = false;
|
||||
return validCached;
|
||||
}
|
||||
|
||||
// Get absolute time when last seen
|
||||
uint32_t secondsSeenAt = secondsRTC - secondsAgo;
|
||||
|
||||
// Calculate daysAgo
|
||||
*daysAgo = (secondsRTC / SEC_PER_DAY) - (secondsSeenAt / SEC_PER_DAY); // How many "midnights" have passed
|
||||
|
||||
// Get seconds since midnight
|
||||
uint32_t hms = (secondsRTC - secondsAgo) % SEC_PER_DAY;
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
|
||||
// Tear apart hms into hours and minutes
|
||||
*hours = hms / SEC_PER_HOUR;
|
||||
*minutes = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
|
||||
// Cache the result
|
||||
daysAgoCached = *daysAgo;
|
||||
hoursCached = *hours;
|
||||
minutesCached = *minutes;
|
||||
secondsAgoCached = secondsAgo;
|
||||
|
||||
validCached = true;
|
||||
// Abort: if timezone not set
|
||||
if (strlen(config.device.tzdef) == 0) {
|
||||
validCached = false;
|
||||
return validCached;
|
||||
}
|
||||
}
|
||||
|
||||
void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength)
|
||||
{
|
||||
// Use an absolute timestamp in some cases.
|
||||
// Particularly useful with E-Ink displays. Static UI, fewer refreshes.
|
||||
uint8_t timestampHours, timestampMinutes;
|
||||
int32_t daysAgo;
|
||||
bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo);
|
||||
// Abort: if invalid pointers passed
|
||||
if (hours == nullptr || minutes == nullptr || daysAgo == nullptr) {
|
||||
validCached = false;
|
||||
return validCached;
|
||||
}
|
||||
|
||||
if (agoSecs < 120) // last 2 mins?
|
||||
snprintf(timeStr, maxLength, "%u seconds ago", agoSecs);
|
||||
// -- if suitable for timestamp --
|
||||
else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes
|
||||
snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / SECONDS_IN_MINUTE);
|
||||
else if (useTimestamp && daysAgo == 0) // Today
|
||||
snprintf(timeStr, maxLength, "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes);
|
||||
else if (useTimestamp && daysAgo == 1) // Yesterday
|
||||
snprintf(timeStr, maxLength, "Seen yesterday");
|
||||
else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method)
|
||||
snprintf(timeStr, maxLength, "%li days ago", (long)daysAgo);
|
||||
// -- if using time delta instead --
|
||||
else if (agoSecs < 120 * 60) // last 2 hrs
|
||||
snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60);
|
||||
// Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data.
|
||||
else if ((agoSecs / 60 / 60) < (730 * 6))
|
||||
snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60);
|
||||
else
|
||||
snprintf(timeStr, maxLength, "unknown age");
|
||||
}
|
||||
// Abort: if time seems invalid.. (> 6 months ago, probably seen before RTC set)
|
||||
if (secondsAgo > SEC_PER_DAY * 30UL * 6) {
|
||||
validCached = false;
|
||||
return validCached;
|
||||
}
|
||||
|
||||
void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, uint8_t maxLength, bool includeSecs)
|
||||
{
|
||||
uint32_t days = uptimeMillis / 86400000;
|
||||
uint32_t hours = (uptimeMillis % 86400000) / 3600000;
|
||||
uint32_t mins = (uptimeMillis % 3600000) / 60000;
|
||||
uint32_t secs = (uptimeMillis % 60000) / 1000;
|
||||
|
||||
if (days) {
|
||||
snprintf(uptimeStr, maxLength, "%s: %ud %uh", prefix, days, hours);
|
||||
} else if (hours) {
|
||||
snprintf(uptimeStr, maxLength, "%s: %uh %um", prefix, hours, mins);
|
||||
} else if (!includeSecs) {
|
||||
snprintf(uptimeStr, maxLength, "%s: %um", prefix, mins);
|
||||
} else if (mins) {
|
||||
snprintf(uptimeStr, maxLength, "%s: %um %us", prefix, mins, secs);
|
||||
} else {
|
||||
snprintf(uptimeStr, maxLength, "%s: %us", prefix, secs);
|
||||
// If repeated request, don't bother recalculating
|
||||
if (secondsAgo - secondsAgoCached < 60 && secondsAgoCached != 0) {
|
||||
if (validCached) {
|
||||
*hours = hoursCached;
|
||||
*minutes = minutesCached;
|
||||
*daysAgo = daysAgoCached;
|
||||
}
|
||||
return validCached;
|
||||
}
|
||||
|
||||
// Get local time
|
||||
uint32_t secondsRTC = getValidTime(RTCQuality::RTCQualityDevice, true); // Get local time
|
||||
|
||||
// Abort: if RTC not set
|
||||
if (!secondsRTC) {
|
||||
validCached = false;
|
||||
return validCached;
|
||||
}
|
||||
|
||||
// Get absolute time when last seen
|
||||
uint32_t secondsSeenAt = secondsRTC - secondsAgo;
|
||||
|
||||
// Calculate daysAgo
|
||||
*daysAgo = (secondsRTC / SEC_PER_DAY) - (secondsSeenAt / SEC_PER_DAY); // How many "midnights" have passed
|
||||
|
||||
// Get seconds since midnight
|
||||
uint32_t hms = (secondsRTC - secondsAgo) % SEC_PER_DAY;
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
|
||||
// Tear apart hms into hours and minutes
|
||||
*hours = hms / SEC_PER_HOUR;
|
||||
*minutes = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
|
||||
// Cache the result
|
||||
daysAgoCached = *daysAgo;
|
||||
hoursCached = *hours;
|
||||
minutesCached = *minutes;
|
||||
secondsAgoCached = secondsAgo;
|
||||
|
||||
validCached = true;
|
||||
return validCached;
|
||||
}
|
||||
|
||||
void getTimeAgoStr(uint32_t agoSecs, char *timeStr, uint8_t maxLength) {
|
||||
// Use an absolute timestamp in some cases.
|
||||
// Particularly useful with E-Ink displays. Static UI, fewer refreshes.
|
||||
uint8_t timestampHours, timestampMinutes;
|
||||
int32_t daysAgo;
|
||||
bool useTimestamp = deltaToTimestamp(agoSecs, ×tampHours, ×tampMinutes, &daysAgo);
|
||||
|
||||
if (agoSecs < 120) // last 2 mins?
|
||||
snprintf(timeStr, maxLength, "%u seconds ago", agoSecs);
|
||||
// -- if suitable for timestamp --
|
||||
else if (useTimestamp && agoSecs < 15 * SECONDS_IN_MINUTE) // Last 15 minutes
|
||||
snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / SECONDS_IN_MINUTE);
|
||||
else if (useTimestamp && daysAgo == 0) // Today
|
||||
snprintf(timeStr, maxLength, "Last seen: %02u:%02u", (unsigned int)timestampHours, (unsigned int)timestampMinutes);
|
||||
else if (useTimestamp && daysAgo == 1) // Yesterday
|
||||
snprintf(timeStr, maxLength, "Seen yesterday");
|
||||
else if (useTimestamp && daysAgo > 1) // Last six months (capped by deltaToTimestamp method)
|
||||
snprintf(timeStr, maxLength, "%li days ago", (long)daysAgo);
|
||||
// -- if using time delta instead --
|
||||
else if (agoSecs < 120 * 60) // last 2 hrs
|
||||
snprintf(timeStr, maxLength, "%u minutes ago", agoSecs / 60);
|
||||
// Only show hours ago if it's been less than 6 months. Otherwise, we may have bad data.
|
||||
else if ((agoSecs / 60 / 60) < (730 * 6))
|
||||
snprintf(timeStr, maxLength, "%u hours ago", agoSecs / 60 / 60);
|
||||
else
|
||||
snprintf(timeStr, maxLength, "unknown age");
|
||||
}
|
||||
|
||||
void getUptimeStr(uint32_t uptimeMillis, const char *prefix, char *uptimeStr, uint8_t maxLength, bool includeSecs) {
|
||||
uint32_t days = uptimeMillis / 86400000;
|
||||
uint32_t hours = (uptimeMillis % 86400000) / 3600000;
|
||||
uint32_t mins = (uptimeMillis % 3600000) / 60000;
|
||||
uint32_t secs = (uptimeMillis % 60000) / 1000;
|
||||
|
||||
if (days) {
|
||||
snprintf(uptimeStr, maxLength, "%s: %ud %uh", prefix, days, hours);
|
||||
} else if (hours) {
|
||||
snprintf(uptimeStr, maxLength, "%s: %uh %um", prefix, hours, mins);
|
||||
} else if (!includeSecs) {
|
||||
snprintf(uptimeStr, maxLength, "%s: %um", prefix, mins);
|
||||
} else if (mins) {
|
||||
snprintf(uptimeStr, maxLength, "%s: %um %us", prefix, mins, secs);
|
||||
} else {
|
||||
snprintf(uptimeStr, maxLength, "%s: %us", prefix, secs);
|
||||
}
|
||||
}
|
||||
|
||||
+612
-651
File diff suppressed because it is too large
Load Diff
@@ -5,76 +5,73 @@
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace graphics {
|
||||
|
||||
enum VirtualKeyType { VK_CHAR, VK_BACKSPACE, VK_ENTER, VK_SHIFT, VK_ESC, VK_SPACE };
|
||||
|
||||
struct VirtualKey {
|
||||
char character;
|
||||
VirtualKeyType type;
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
char character;
|
||||
VirtualKeyType type;
|
||||
uint8_t x;
|
||||
uint8_t y;
|
||||
uint8_t width;
|
||||
uint8_t height;
|
||||
};
|
||||
|
||||
class VirtualKeyboard
|
||||
{
|
||||
public:
|
||||
VirtualKeyboard();
|
||||
~VirtualKeyboard();
|
||||
class VirtualKeyboard {
|
||||
public:
|
||||
VirtualKeyboard();
|
||||
~VirtualKeyboard();
|
||||
|
||||
void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY);
|
||||
void setInputText(const std::string &text);
|
||||
std::string getInputText() const;
|
||||
void setHeader(const std::string &header);
|
||||
void setCallback(std::function<void(const std::string &)> callback);
|
||||
void draw(OLEDDisplay *display, int16_t offsetX, int16_t offsetY);
|
||||
void setInputText(const std::string &text);
|
||||
std::string getInputText() const;
|
||||
void setHeader(const std::string &header);
|
||||
void setCallback(std::function<void(const std::string &)> callback);
|
||||
|
||||
// Navigation methods for encoder input
|
||||
void moveCursorUp();
|
||||
void moveCursorDown();
|
||||
void moveCursorLeft();
|
||||
void moveCursorRight();
|
||||
void handlePress();
|
||||
void handleLongPress();
|
||||
// Navigation methods for encoder input
|
||||
void moveCursorUp();
|
||||
void moveCursorDown();
|
||||
void moveCursorLeft();
|
||||
void moveCursorRight();
|
||||
void handlePress();
|
||||
void handleLongPress();
|
||||
|
||||
// Timeout management
|
||||
void resetTimeout();
|
||||
bool isTimedOut() const;
|
||||
// Timeout management
|
||||
void resetTimeout();
|
||||
bool isTimedOut() const;
|
||||
|
||||
private:
|
||||
static const uint8_t KEYBOARD_ROWS = 4;
|
||||
static const uint8_t KEYBOARD_COLS = 11;
|
||||
static const uint8_t KEY_WIDTH = 9;
|
||||
static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays
|
||||
static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom
|
||||
private:
|
||||
static const uint8_t KEYBOARD_ROWS = 4;
|
||||
static const uint8_t KEYBOARD_COLS = 11;
|
||||
static const uint8_t KEY_WIDTH = 9;
|
||||
static const uint8_t KEY_HEIGHT = 9; // Compressed to fit 4 rows on 64px displays
|
||||
static const uint8_t KEYBOARD_START_Y = 26; // Start just below input box bottom
|
||||
|
||||
VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS];
|
||||
VirtualKey keyboard[KEYBOARD_ROWS][KEYBOARD_COLS];
|
||||
|
||||
std::string inputText;
|
||||
std::string headerText;
|
||||
std::function<void(const std::string &)> onTextEntered;
|
||||
std::string inputText;
|
||||
std::string headerText;
|
||||
std::function<void(const std::string &)> onTextEntered;
|
||||
|
||||
uint8_t cursorRow;
|
||||
uint8_t cursorCol;
|
||||
uint8_t cursorRow;
|
||||
uint8_t cursorCol;
|
||||
|
||||
// Timeout management for auto-exit
|
||||
uint32_t lastActivityTime;
|
||||
static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout
|
||||
// Timeout management for auto-exit
|
||||
uint32_t lastActivityTime;
|
||||
static const uint32_t TIMEOUT_MS = 60000; // 1 minute timeout
|
||||
|
||||
void initializeKeyboard();
|
||||
void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h,
|
||||
bool isLastCol);
|
||||
void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY);
|
||||
void initializeKeyboard();
|
||||
void drawKey(OLEDDisplay *display, const VirtualKey &key, bool selected, int16_t x, int16_t y, uint8_t w, uint8_t h, bool isLastCol);
|
||||
void drawInputArea(OLEDDisplay *display, int16_t offsetX, int16_t offsetY, int16_t keyboardStartY);
|
||||
|
||||
// Unified cursor movement helper
|
||||
void moveCursorDelta(int dRow, int dCol);
|
||||
// Unified cursor movement helper
|
||||
void moveCursorDelta(int dRow, int dCol);
|
||||
|
||||
char getCharForKey(const VirtualKey &key, bool isLongPress = false);
|
||||
void insertCharacter(char c);
|
||||
void deleteCharacter();
|
||||
void submitText();
|
||||
char getCharForKey(const VirtualKey &key, bool isLongPress = false);
|
||||
void insertCharacter(char c);
|
||||
void deleteCharacter();
|
||||
void submitText();
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
+369
-381
@@ -12,11 +12,9 @@
|
||||
#include "nimble/NimbleBluetooth.h"
|
||||
#endif
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace graphics {
|
||||
|
||||
namespace ClockRenderer
|
||||
{
|
||||
namespace ClockRenderer {
|
||||
|
||||
// Segment bitmaps for numerals 0-9 stored in flash to save RAM.
|
||||
// Each row is a digit, each column is a segment state (1 = on, 0 = off).
|
||||
@@ -43,436 +41,426 @@ static const uint8_t PROGMEM digitSegments[10][7] = {
|
||||
{1, 1, 1, 1, 0, 1, 1} // 9
|
||||
};
|
||||
|
||||
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale)
|
||||
{
|
||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
void drawSegmentedDisplayColon(OLEDDisplay *display, int x, int y, float scale) {
|
||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
|
||||
uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8;
|
||||
uint16_t cellHeight = (segmentWidth * 2) + (segmentHeight * 3) + 8;
|
||||
|
||||
uint16_t topAndBottomX = x + static_cast<uint16_t>(4 * scale);
|
||||
uint16_t topAndBottomX = x + static_cast<uint16_t>(4 * scale);
|
||||
|
||||
uint16_t quarterCellHeight = cellHeight / 4;
|
||||
uint16_t quarterCellHeight = cellHeight / 4;
|
||||
|
||||
uint16_t topY = y + quarterCellHeight;
|
||||
uint16_t bottomY = y + (quarterCellHeight * 3);
|
||||
uint16_t topY = y + quarterCellHeight;
|
||||
uint16_t bottomY = y + (quarterCellHeight * 3);
|
||||
|
||||
display->fillRect(topAndBottomX, topY, segmentHeight, segmentHeight);
|
||||
display->fillRect(topAndBottomX, bottomY, segmentHeight, segmentHeight);
|
||||
display->fillRect(topAndBottomX, topY, segmentHeight, segmentHeight);
|
||||
display->fillRect(topAndBottomX, bottomY, segmentHeight, segmentHeight);
|
||||
}
|
||||
|
||||
void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale)
|
||||
{
|
||||
// Read 7-segment pattern for the digit from flash
|
||||
uint8_t seg[7];
|
||||
for (uint8_t i = 0; i < 7; i++) {
|
||||
seg[i] = pgm_read_byte(&digitSegments[number][i]);
|
||||
void drawSegmentedDisplayCharacter(OLEDDisplay *display, int x, int y, uint8_t number, float scale) {
|
||||
// Read 7-segment pattern for the digit from flash
|
||||
uint8_t seg[7];
|
||||
for (uint8_t i = 0; i < 7; i++) {
|
||||
seg[i] = pgm_read_byte(&digitSegments[number][i]);
|
||||
}
|
||||
|
||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
|
||||
// Precompute segment positions
|
||||
uint16_t segmentOneX = x + segmentHeight + 2;
|
||||
uint16_t segmentOneY = y;
|
||||
|
||||
uint16_t segmentTwoX = segmentOneX + segmentWidth + 2;
|
||||
uint16_t segmentTwoY = segmentOneY + segmentHeight + 2;
|
||||
|
||||
uint16_t segmentThreeX = segmentTwoX;
|
||||
uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2 + segmentHeight + 2;
|
||||
|
||||
uint16_t segmentFourX = segmentOneX;
|
||||
uint16_t segmentFourY = segmentThreeY + segmentWidth + 2;
|
||||
|
||||
uint16_t segmentFiveX = x;
|
||||
uint16_t segmentFiveY = segmentThreeY;
|
||||
|
||||
uint16_t segmentSixX = x;
|
||||
uint16_t segmentSixY = segmentTwoY;
|
||||
|
||||
uint16_t segmentSevenX = segmentOneX;
|
||||
uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2;
|
||||
|
||||
// Draw only the active segments
|
||||
if (seg[0])
|
||||
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||||
if (seg[1])
|
||||
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||||
if (seg[2])
|
||||
drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||||
if (seg[3])
|
||||
drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||
if (seg[4])
|
||||
drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
|
||||
if (seg[5])
|
||||
drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
|
||||
if (seg[6])
|
||||
drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height) {
|
||||
int halfHeight = height / 2;
|
||||
|
||||
// draw central rectangle
|
||||
display->fillRect(x, y, width, height);
|
||||
|
||||
// draw end triangles
|
||||
display->fillTriangle(x, y, x, y + height - 1, x - halfHeight, y + halfHeight);
|
||||
|
||||
display->fillTriangle(x + width, y, x + width + halfHeight, y + halfHeight, x + width, y + height - 1);
|
||||
}
|
||||
|
||||
void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height) {
|
||||
int halfHeight = height / 2;
|
||||
|
||||
// draw central rectangle
|
||||
display->fillRect(x, y, height, width);
|
||||
|
||||
// draw end triangles
|
||||
display->fillTriangle(x + halfHeight, y - halfHeight, x + height - 1, y, x, y);
|
||||
|
||||
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
|
||||
}
|
||||
|
||||
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
|
||||
display->clear();
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
// === Set Title, Blank for Clock
|
||||
const char *titleStr = "";
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||
char timeString[16];
|
||||
int hour = 0;
|
||||
int minute = 0;
|
||||
int second = 0;
|
||||
|
||||
if (rtc_sec > 0) {
|
||||
long hms = rtc_sec % SEC_PER_DAY;
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
|
||||
hour = hms / SEC_PER_HOUR;
|
||||
minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||
}
|
||||
|
||||
bool isPM = hour >= 12;
|
||||
if (config.display.use_12h_clock) {
|
||||
hour %= 12;
|
||||
if (hour == 0) {
|
||||
hour = 12;
|
||||
}
|
||||
snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute);
|
||||
} else {
|
||||
snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute);
|
||||
}
|
||||
|
||||
// Format seconds string
|
||||
char secondString[8];
|
||||
snprintf(secondString, sizeof(secondString), "%02d", second);
|
||||
|
||||
static bool scaleInitialized = false;
|
||||
static float scale = 0.75f;
|
||||
static float segmentWidth = SEGMENT_WIDTH * 0.75f;
|
||||
static float segmentHeight = SEGMENT_HEIGHT * 0.75f;
|
||||
|
||||
if (!scaleInitialized) {
|
||||
float screenwidth_target_ratio = 0.80f; // Target 80% of display width (adjustable)
|
||||
float max_scale = 3.5f; // Safety limit to avoid runaway scaling
|
||||
float step = 0.05f; // Step increment per iteration
|
||||
|
||||
float target_width = display->getWidth() * screenwidth_target_ratio;
|
||||
float target_height =
|
||||
display->getHeight() - ((currentResolution == ScreenResolution::High)
|
||||
? 46
|
||||
: 33); // Be careful adjusting this number, we have to account for header and the text under the time
|
||||
|
||||
float calculated_width_size = 0.0f;
|
||||
float calculated_height_size = 0.0f;
|
||||
|
||||
while (true) {
|
||||
segmentWidth = SEGMENT_WIDTH * scale;
|
||||
segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
|
||||
calculated_width_size = segmentHeight + ((segmentWidth + (segmentHeight * 2) + 4) * 4);
|
||||
calculated_height_size = segmentHeight + ((segmentHeight + (segmentHeight * 2) + 4) * 2);
|
||||
|
||||
if (calculated_width_size >= target_width || calculated_height_size >= target_height || scale >= max_scale) {
|
||||
break;
|
||||
}
|
||||
|
||||
scale += step;
|
||||
}
|
||||
|
||||
uint16_t segmentWidth = SEGMENT_WIDTH * scale;
|
||||
uint16_t segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
|
||||
// Precompute segment positions
|
||||
uint16_t segmentOneX = x + segmentHeight + 2;
|
||||
uint16_t segmentOneY = y;
|
||||
|
||||
uint16_t segmentTwoX = segmentOneX + segmentWidth + 2;
|
||||
uint16_t segmentTwoY = segmentOneY + segmentHeight + 2;
|
||||
|
||||
uint16_t segmentThreeX = segmentTwoX;
|
||||
uint16_t segmentThreeY = segmentTwoY + segmentWidth + 2 + segmentHeight + 2;
|
||||
|
||||
uint16_t segmentFourX = segmentOneX;
|
||||
uint16_t segmentFourY = segmentThreeY + segmentWidth + 2;
|
||||
|
||||
uint16_t segmentFiveX = x;
|
||||
uint16_t segmentFiveY = segmentThreeY;
|
||||
|
||||
uint16_t segmentSixX = x;
|
||||
uint16_t segmentSixY = segmentTwoY;
|
||||
|
||||
uint16_t segmentSevenX = segmentOneX;
|
||||
uint16_t segmentSevenY = segmentTwoY + segmentWidth + 2;
|
||||
|
||||
// Draw only the active segments
|
||||
if (seg[0])
|
||||
drawHorizontalSegment(display, segmentOneX, segmentOneY, segmentWidth, segmentHeight);
|
||||
if (seg[1])
|
||||
drawVerticalSegment(display, segmentTwoX, segmentTwoY, segmentWidth, segmentHeight);
|
||||
if (seg[2])
|
||||
drawVerticalSegment(display, segmentThreeX, segmentThreeY, segmentWidth, segmentHeight);
|
||||
if (seg[3])
|
||||
drawHorizontalSegment(display, segmentFourX, segmentFourY, segmentWidth, segmentHeight);
|
||||
if (seg[4])
|
||||
drawVerticalSegment(display, segmentFiveX, segmentFiveY, segmentWidth, segmentHeight);
|
||||
if (seg[5])
|
||||
drawVerticalSegment(display, segmentSixX, segmentSixY, segmentWidth, segmentHeight);
|
||||
if (seg[6])
|
||||
drawHorizontalSegment(display, segmentSevenX, segmentSevenY, segmentWidth, segmentHeight);
|
||||
}
|
||||
|
||||
void drawHorizontalSegment(OLEDDisplay *display, int x, int y, int width, int height)
|
||||
{
|
||||
int halfHeight = height / 2;
|
||||
|
||||
// draw central rectangle
|
||||
display->fillRect(x, y, width, height);
|
||||
|
||||
// draw end triangles
|
||||
display->fillTriangle(x, y, x, y + height - 1, x - halfHeight, y + halfHeight);
|
||||
|
||||
display->fillTriangle(x + width, y, x + width + halfHeight, y + halfHeight, x + width, y + height - 1);
|
||||
}
|
||||
|
||||
void drawVerticalSegment(OLEDDisplay *display, int x, int y, int width, int height)
|
||||
{
|
||||
int halfHeight = height / 2;
|
||||
|
||||
// draw central rectangle
|
||||
display->fillRect(x, y, height, width);
|
||||
|
||||
// draw end triangles
|
||||
display->fillTriangle(x + halfHeight, y - halfHeight, x + height - 1, y, x, y);
|
||||
|
||||
display->fillTriangle(x, y + width, x + height - 1, y + width, x + halfHeight, y + width + halfHeight);
|
||||
}
|
||||
|
||||
void drawDigitalClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->clear();
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
|
||||
// === Set Title, Blank for Clock
|
||||
const char *titleStr = "";
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||
char timeString[16];
|
||||
int hour = 0;
|
||||
int minute = 0;
|
||||
int second = 0;
|
||||
|
||||
if (rtc_sec > 0) {
|
||||
long hms = rtc_sec % SEC_PER_DAY;
|
||||
hms = (hms + SEC_PER_DAY) % SEC_PER_DAY;
|
||||
|
||||
hour = hms / SEC_PER_HOUR;
|
||||
minute = (hms % SEC_PER_HOUR) / SEC_PER_MIN;
|
||||
second = (hms % SEC_PER_HOUR) % SEC_PER_MIN; // or hms % SEC_PER_MIN
|
||||
// If we overshot width, back off one step and recompute segment sizes
|
||||
if (calculated_width_size > target_width || calculated_height_size > target_height) {
|
||||
scale -= step;
|
||||
segmentWidth = SEGMENT_WIDTH * scale;
|
||||
segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
}
|
||||
|
||||
bool isPM = hour >= 12;
|
||||
if (config.display.use_12h_clock) {
|
||||
hour %= 12;
|
||||
if (hour == 0) {
|
||||
hour = 12;
|
||||
}
|
||||
snprintf(timeString, sizeof(timeString), "%d:%02d", hour, minute);
|
||||
scaleInitialized = true;
|
||||
}
|
||||
|
||||
// calculate hours:minutes string width
|
||||
size_t len = strlen(timeString);
|
||||
uint16_t timeStringWidth = len * 5;
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char character = timeString[i];
|
||||
|
||||
if (character == ':') {
|
||||
timeStringWidth += segmentHeight;
|
||||
} else {
|
||||
snprintf(timeString, sizeof(timeString), "%02d:%02d", hour, minute);
|
||||
timeStringWidth += segmentWidth + (segmentHeight * 2) + 4;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t hourMinuteTextX = (display->getWidth() / 2) - (timeStringWidth / 2);
|
||||
uint16_t startingHourMinuteTextX = hourMinuteTextX;
|
||||
|
||||
uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2) + 2;
|
||||
|
||||
// iterate over characters in hours:minutes string and draw segmented characters
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char character = timeString[i];
|
||||
|
||||
if (character == ':') {
|
||||
drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale);
|
||||
|
||||
hourMinuteTextX += segmentHeight + 6;
|
||||
if (scale >= 2.0f) {
|
||||
hourMinuteTextX += (uint16_t)(4.5f * scale);
|
||||
}
|
||||
} else {
|
||||
drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale);
|
||||
|
||||
hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4;
|
||||
}
|
||||
|
||||
// Format seconds string
|
||||
char secondString[8];
|
||||
snprintf(secondString, sizeof(secondString), "%02d", second);
|
||||
hourMinuteTextX += 5;
|
||||
}
|
||||
|
||||
static bool scaleInitialized = false;
|
||||
static float scale = 0.75f;
|
||||
static float segmentWidth = SEGMENT_WIDTH * 0.75f;
|
||||
static float segmentHeight = SEGMENT_HEIGHT * 0.75f;
|
||||
|
||||
if (!scaleInitialized) {
|
||||
float screenwidth_target_ratio = 0.80f; // Target 80% of display width (adjustable)
|
||||
float max_scale = 3.5f; // Safety limit to avoid runaway scaling
|
||||
float step = 0.05f; // Step increment per iteration
|
||||
|
||||
float target_width = display->getWidth() * screenwidth_target_ratio;
|
||||
float target_height =
|
||||
display->getHeight() -
|
||||
((currentResolution == ScreenResolution::High)
|
||||
? 46
|
||||
: 33); // Be careful adjusting this number, we have to account for header and the text under the time
|
||||
|
||||
float calculated_width_size = 0.0f;
|
||||
float calculated_height_size = 0.0f;
|
||||
|
||||
while (true) {
|
||||
segmentWidth = SEGMENT_WIDTH * scale;
|
||||
segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
|
||||
calculated_width_size = segmentHeight + ((segmentWidth + (segmentHeight * 2) + 4) * 4);
|
||||
calculated_height_size = segmentHeight + ((segmentHeight + (segmentHeight * 2) + 4) * 2);
|
||||
|
||||
if (calculated_width_size >= target_width || calculated_height_size >= target_height || scale >= max_scale) {
|
||||
break;
|
||||
}
|
||||
|
||||
scale += step;
|
||||
}
|
||||
|
||||
// If we overshot width, back off one step and recompute segment sizes
|
||||
if (calculated_width_size > target_width || calculated_height_size > target_height) {
|
||||
scale -= step;
|
||||
segmentWidth = SEGMENT_WIDTH * scale;
|
||||
segmentHeight = SEGMENT_HEIGHT * scale;
|
||||
}
|
||||
|
||||
scaleInitialized = true;
|
||||
}
|
||||
|
||||
// calculate hours:minutes string width
|
||||
size_t len = strlen(timeString);
|
||||
uint16_t timeStringWidth = len * 5;
|
||||
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char character = timeString[i];
|
||||
|
||||
if (character == ':') {
|
||||
timeStringWidth += segmentHeight;
|
||||
} else {
|
||||
timeStringWidth += segmentWidth + (segmentHeight * 2) + 4;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t hourMinuteTextX = (display->getWidth() / 2) - (timeStringWidth / 2);
|
||||
uint16_t startingHourMinuteTextX = hourMinuteTextX;
|
||||
|
||||
uint16_t hourMinuteTextY = (display->getHeight() / 2) - (((segmentWidth * 2) + (segmentHeight * 3) + 8) / 2) + 2;
|
||||
|
||||
// iterate over characters in hours:minutes string and draw segmented characters
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
char character = timeString[i];
|
||||
|
||||
if (character == ':') {
|
||||
drawSegmentedDisplayColon(display, hourMinuteTextX, hourMinuteTextY, scale);
|
||||
|
||||
hourMinuteTextX += segmentHeight + 6;
|
||||
if (scale >= 2.0f) {
|
||||
hourMinuteTextX += (uint16_t)(4.5f * scale);
|
||||
}
|
||||
} else {
|
||||
drawSegmentedDisplayCharacter(display, hourMinuteTextX, hourMinuteTextY, character - '0', scale);
|
||||
|
||||
hourMinuteTextX += segmentWidth + (segmentHeight * 2) + 4;
|
||||
}
|
||||
|
||||
hourMinuteTextX += 5;
|
||||
}
|
||||
|
||||
// draw seconds string + AM/PM
|
||||
display->setFont(FONT_SMALL);
|
||||
int xOffset = -1;
|
||||
// draw seconds string + AM/PM
|
||||
display->setFont(FONT_SMALL);
|
||||
int xOffset = -1;
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
xOffset = 0;
|
||||
}
|
||||
if (hour >= 10) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
xOffset = 0;
|
||||
}
|
||||
if (hour >= 10) {
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
xOffset += 32;
|
||||
} else {
|
||||
xOffset += 18;
|
||||
}
|
||||
xOffset += 32;
|
||||
} else {
|
||||
xOffset += 18;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.display.use_12h_clock) {
|
||||
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - 1, isPM ? "pm" : "am");
|
||||
}
|
||||
if (config.display.use_12h_clock) {
|
||||
display->drawString(startingHourMinuteTextX + xOffset, (display->getHeight() - hourMinuteTextY) - 1, isPM ? "pm" : "am");
|
||||
}
|
||||
|
||||
#ifndef USE_EINK
|
||||
xOffset = (currentResolution == ScreenResolution::High) ? 18 : 10;
|
||||
if (scale >= 2.0f) {
|
||||
xOffset -= (int)(4.5f * scale);
|
||||
}
|
||||
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - 1,
|
||||
secondString);
|
||||
xOffset = (currentResolution == ScreenResolution::High) ? 18 : 10;
|
||||
if (scale >= 2.0f) {
|
||||
xOffset -= (int)(4.5f * scale);
|
||||
}
|
||||
display->drawString(startingHourMinuteTextX + timeStringWidth - xOffset, (display->getHeight() - hourMinuteTextY) - 1, secondString);
|
||||
#endif
|
||||
|
||||
graphics::drawCommonFooter(display, x, y);
|
||||
graphics::drawCommonFooter(display, x, y);
|
||||
}
|
||||
|
||||
// Draw an analog clock
|
||||
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y)
|
||||
{
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
// === Set Title, Blank for Clock
|
||||
const char *titleStr = "";
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y) {
|
||||
display->setTextAlignment(TEXT_ALIGN_LEFT);
|
||||
// === Set Title, Blank for Clock
|
||||
const char *titleStr = "";
|
||||
// === Header ===
|
||||
graphics::drawCommonHeader(display, x, y, titleStr, true, true);
|
||||
|
||||
// clock face center coordinates
|
||||
int16_t centerX = display->getWidth() / 2;
|
||||
int16_t centerY = display->getHeight() / 2;
|
||||
// clock face center coordinates
|
||||
int16_t centerX = display->getWidth() / 2;
|
||||
int16_t centerY = display->getHeight() / 2;
|
||||
|
||||
// clock face radius
|
||||
int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9;
|
||||
// clock face radius
|
||||
int16_t radius = (std::min(display->getWidth(), display->getHeight()) / 2) * 0.9;
|
||||
#ifdef T_WATCH_S3
|
||||
radius = (display->getWidth() / 2) * 0.8;
|
||||
radius = (display->getWidth() / 2) * 0.8;
|
||||
#endif
|
||||
|
||||
// noon (0 deg) coordinates (outermost circle)
|
||||
int16_t noonX = centerX;
|
||||
int16_t noonY = centerY - radius;
|
||||
// noon (0 deg) coordinates (outermost circle)
|
||||
int16_t noonX = centerX;
|
||||
int16_t noonY = centerY - radius;
|
||||
|
||||
// second hand radius and y coordinate (outermost circle)
|
||||
int16_t secondHandNoonY = noonY + 1;
|
||||
// second hand radius and y coordinate (outermost circle)
|
||||
int16_t secondHandNoonY = noonY + 1;
|
||||
|
||||
// tick mark outer y coordinate; (first nested circle)
|
||||
int16_t tickMarkOuterNoonY = secondHandNoonY;
|
||||
// tick mark outer y coordinate; (first nested circle)
|
||||
int16_t tickMarkOuterNoonY = secondHandNoonY;
|
||||
|
||||
double secondsTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 8 : 4);
|
||||
double hoursTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 16 : 6);
|
||||
double secondsTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 8 : 4);
|
||||
double hoursTickMarkInnerNoonY = noonY + ((currentResolution == ScreenResolution::High) ? 16 : 6);
|
||||
|
||||
// minute hand y coordinate
|
||||
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
|
||||
// minute hand y coordinate
|
||||
int16_t minuteHandNoonY = secondsTickMarkInnerNoonY + 4;
|
||||
|
||||
// hour string y coordinate
|
||||
int16_t hourStringNoonY = minuteHandNoonY + 18;
|
||||
// hour string y coordinate
|
||||
int16_t hourStringNoonY = minuteHandNoonY + 18;
|
||||
|
||||
// hour hand radius and y coordinate
|
||||
int16_t hourHandRadius = radius * 0.35;
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
hourHandRadius = radius * 0.55;
|
||||
// hour hand radius and y coordinate
|
||||
int16_t hourHandRadius = radius * 0.35;
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
hourHandRadius = radius * 0.55;
|
||||
}
|
||||
int16_t hourHandNoonY = centerY - hourHandRadius;
|
||||
|
||||
display->setColor(OLEDDISPLAY_COLOR::WHITE);
|
||||
display->drawCircle(centerX, centerY, radius);
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||
if (rtc_sec > 0) {
|
||||
int hour, minute, second;
|
||||
decomposeTime(rtc_sec, hour, minute, second);
|
||||
|
||||
if (config.display.use_12h_clock) {
|
||||
bool isPM = hour >= 12;
|
||||
display->setFont(FONT_SMALL);
|
||||
int yOffset = (currentResolution == ScreenResolution::High) ? 1 : 0;
|
||||
#ifdef USE_EINK
|
||||
yOffset += 3;
|
||||
#endif
|
||||
display->drawString(centerX - (display->getStringWidth(isPM ? "pm" : "am") / 2), centerY + yOffset, isPM ? "pm" : "am");
|
||||
}
|
||||
int16_t hourHandNoonY = centerY - hourHandRadius;
|
||||
hour %= 12;
|
||||
if (hour == 0)
|
||||
hour = 12;
|
||||
|
||||
display->setColor(OLEDDISPLAY_COLOR::WHITE);
|
||||
display->drawCircle(centerX, centerY, radius);
|
||||
int16_t degreesPerHour = 30;
|
||||
int16_t degreesPerMinuteOrSecond = 6;
|
||||
|
||||
uint32_t rtc_sec = getValidTime(RTCQuality::RTCQualityDevice, true); // Display local timezone
|
||||
if (rtc_sec > 0) {
|
||||
int hour, minute, second;
|
||||
decomposeTime(rtc_sec, hour, minute, second);
|
||||
double hourBaseAngle = hour * degreesPerHour;
|
||||
double hourAngleOffset = ((double)minute / 60) * degreesPerHour;
|
||||
double hourAngle = radians(hourBaseAngle + hourAngleOffset);
|
||||
|
||||
if (config.display.use_12h_clock) {
|
||||
bool isPM = hour >= 12;
|
||||
display->setFont(FONT_SMALL);
|
||||
int yOffset = (currentResolution == ScreenResolution::High) ? 1 : 0;
|
||||
#ifdef USE_EINK
|
||||
yOffset += 3;
|
||||
#endif
|
||||
display->drawString(centerX - (display->getStringWidth(isPM ? "pm" : "am") / 2), centerY + yOffset,
|
||||
isPM ? "pm" : "am");
|
||||
double minuteBaseAngle = minute * degreesPerMinuteOrSecond;
|
||||
double minuteAngleOffset = ((double)second / 60) * degreesPerMinuteOrSecond;
|
||||
double minuteAngle = radians(minuteBaseAngle + minuteAngleOffset);
|
||||
|
||||
double secondAngle = radians(second * degreesPerMinuteOrSecond);
|
||||
|
||||
double hourX = sin(-hourAngle) * (hourHandNoonY - centerY) + noonX;
|
||||
double hourY = cos(-hourAngle) * (hourHandNoonY - centerY) + centerY;
|
||||
|
||||
double minuteX = sin(-minuteAngle) * (minuteHandNoonY - centerY) + noonX;
|
||||
double minuteY = cos(-minuteAngle) * (minuteHandNoonY - centerY) + centerY;
|
||||
|
||||
double secondX = sin(-secondAngle) * (secondHandNoonY - centerY) + noonX;
|
||||
double secondY = cos(-secondAngle) * (secondHandNoonY - centerY) + centerY;
|
||||
|
||||
display->setFont(FONT_MEDIUM);
|
||||
|
||||
// draw minute and hour tick marks and hour numbers
|
||||
for (uint16_t angle = 0; angle < 360; angle += 6) {
|
||||
double angleInRadians = radians(angle);
|
||||
|
||||
double sineAngleInRadians = sin(-angleInRadians);
|
||||
double cosineAngleInRadians = cos(-angleInRadians);
|
||||
|
||||
double endX = sineAngleInRadians * (tickMarkOuterNoonY - centerY) + noonX;
|
||||
double endY = cosineAngleInRadians * (tickMarkOuterNoonY - centerY) + centerY;
|
||||
|
||||
if (angle % degreesPerHour == 0) {
|
||||
double startX = sineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + noonX;
|
||||
double startY = cosineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + centerY;
|
||||
|
||||
// draw hour tick mark
|
||||
display->drawLine(startX, startY, endX, endY);
|
||||
|
||||
static char buffer[2];
|
||||
|
||||
uint8_t hourInt = (angle / 30);
|
||||
|
||||
if (hourInt == 0) {
|
||||
hourInt = 12;
|
||||
}
|
||||
hour %= 12;
|
||||
if (hour == 0)
|
||||
hour = 12;
|
||||
|
||||
int16_t degreesPerHour = 30;
|
||||
int16_t degreesPerMinuteOrSecond = 6;
|
||||
// hour number x offset needs to be adjusted for some cases
|
||||
int8_t hourStringXOffset;
|
||||
int8_t hourStringYOffset = 13;
|
||||
|
||||
double hourBaseAngle = hour * degreesPerHour;
|
||||
double hourAngleOffset = ((double)minute / 60) * degreesPerHour;
|
||||
double hourAngle = radians(hourBaseAngle + hourAngleOffset);
|
||||
switch (hourInt) {
|
||||
case 3:
|
||||
hourStringXOffset = 5;
|
||||
break;
|
||||
case 9:
|
||||
hourStringXOffset = 7;
|
||||
break;
|
||||
case 10:
|
||||
case 11:
|
||||
hourStringXOffset = 8;
|
||||
break;
|
||||
case 12:
|
||||
hourStringXOffset = 13;
|
||||
break;
|
||||
default:
|
||||
hourStringXOffset = 6;
|
||||
break;
|
||||
}
|
||||
|
||||
double minuteBaseAngle = minute * degreesPerMinuteOrSecond;
|
||||
double minuteAngleOffset = ((double)second / 60) * degreesPerMinuteOrSecond;
|
||||
double minuteAngle = radians(minuteBaseAngle + minuteAngleOffset);
|
||||
|
||||
double secondAngle = radians(second * degreesPerMinuteOrSecond);
|
||||
|
||||
double hourX = sin(-hourAngle) * (hourHandNoonY - centerY) + noonX;
|
||||
double hourY = cos(-hourAngle) * (hourHandNoonY - centerY) + centerY;
|
||||
|
||||
double minuteX = sin(-minuteAngle) * (minuteHandNoonY - centerY) + noonX;
|
||||
double minuteY = cos(-minuteAngle) * (minuteHandNoonY - centerY) + centerY;
|
||||
|
||||
double secondX = sin(-secondAngle) * (secondHandNoonY - centerY) + noonX;
|
||||
double secondY = cos(-secondAngle) * (secondHandNoonY - centerY) + centerY;
|
||||
|
||||
display->setFont(FONT_MEDIUM);
|
||||
|
||||
// draw minute and hour tick marks and hour numbers
|
||||
for (uint16_t angle = 0; angle < 360; angle += 6) {
|
||||
double angleInRadians = radians(angle);
|
||||
|
||||
double sineAngleInRadians = sin(-angleInRadians);
|
||||
double cosineAngleInRadians = cos(-angleInRadians);
|
||||
|
||||
double endX = sineAngleInRadians * (tickMarkOuterNoonY - centerY) + noonX;
|
||||
double endY = cosineAngleInRadians * (tickMarkOuterNoonY - centerY) + centerY;
|
||||
|
||||
if (angle % degreesPerHour == 0) {
|
||||
double startX = sineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + noonX;
|
||||
double startY = cosineAngleInRadians * (hoursTickMarkInnerNoonY - centerY) + centerY;
|
||||
|
||||
// draw hour tick mark
|
||||
display->drawLine(startX, startY, endX, endY);
|
||||
|
||||
static char buffer[2];
|
||||
|
||||
uint8_t hourInt = (angle / 30);
|
||||
|
||||
if (hourInt == 0) {
|
||||
hourInt = 12;
|
||||
}
|
||||
|
||||
// hour number x offset needs to be adjusted for some cases
|
||||
int8_t hourStringXOffset;
|
||||
int8_t hourStringYOffset = 13;
|
||||
|
||||
switch (hourInt) {
|
||||
case 3:
|
||||
hourStringXOffset = 5;
|
||||
break;
|
||||
case 9:
|
||||
hourStringXOffset = 7;
|
||||
break;
|
||||
case 10:
|
||||
case 11:
|
||||
hourStringXOffset = 8;
|
||||
break;
|
||||
case 12:
|
||||
hourStringXOffset = 13;
|
||||
break;
|
||||
default:
|
||||
hourStringXOffset = 6;
|
||||
break;
|
||||
}
|
||||
|
||||
double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset;
|
||||
double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset;
|
||||
double hourStringX = (sineAngleInRadians * (hourStringNoonY - centerY) + noonX) - hourStringXOffset;
|
||||
double hourStringY = (cosineAngleInRadians * (hourStringNoonY - centerY) + centerY) - hourStringYOffset;
|
||||
|
||||
#ifdef T_WATCH_S3
|
||||
// draw hour number
|
||||
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||
// draw hour number
|
||||
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||
#else
|
||||
#ifdef USE_EINK
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
// draw hour number
|
||||
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||
}
|
||||
#else
|
||||
if (currentResolution == ScreenResolution::High &&
|
||||
(hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) {
|
||||
// draw hour number
|
||||
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
if (angle % degreesPerMinuteOrSecond == 0) {
|
||||
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
|
||||
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
|
||||
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
// draw minute tick mark
|
||||
display->drawLine(startX, startY, endX, endY);
|
||||
}
|
||||
}
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
// draw hour number
|
||||
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||
}
|
||||
#else
|
||||
if (currentResolution == ScreenResolution::High && (hourInt == 3 || hourInt == 6 || hourInt == 9 || hourInt == 12)) {
|
||||
// draw hour number
|
||||
display->drawStringf(hourStringX, hourStringY, buffer, "%d", hourInt);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
// draw hour hand
|
||||
display->drawLine(centerX, centerY, hourX, hourY);
|
||||
if (angle % degreesPerMinuteOrSecond == 0) {
|
||||
double startX = sineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + noonX;
|
||||
double startY = cosineAngleInRadians * (secondsTickMarkInnerNoonY - centerY) + centerY;
|
||||
|
||||
// draw minute hand
|
||||
display->drawLine(centerX, centerY, minuteX, minuteY);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
// draw minute tick mark
|
||||
display->drawLine(startX, startY, endX, endY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// draw hour hand
|
||||
display->drawLine(centerX, centerY, hourX, hourY);
|
||||
|
||||
// draw minute hand
|
||||
display->drawLine(centerX, centerY, minuteX, minuteY);
|
||||
#ifndef USE_EINK
|
||||
// draw second hand
|
||||
display->drawLine(centerX, centerY, secondX, secondY);
|
||||
// draw second hand
|
||||
display->drawLine(centerX, centerY, secondX, secondY);
|
||||
#endif
|
||||
}
|
||||
graphics::drawCommonFooter(display, x, y);
|
||||
}
|
||||
graphics::drawCommonFooter(display, x, y);
|
||||
}
|
||||
|
||||
} // namespace ClockRenderer
|
||||
|
||||
@@ -3,14 +3,12 @@
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace graphics {
|
||||
|
||||
/// Forward declarations
|
||||
class Screen;
|
||||
|
||||
namespace ClockRenderer
|
||||
{
|
||||
namespace ClockRenderer {
|
||||
|
||||
// Clock frame functions
|
||||
void drawAnalogClockFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
@@ -9,130 +9,120 @@
|
||||
#include "graphics/SharedUIDisplay.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace CompassRenderer
|
||||
{
|
||||
namespace graphics {
|
||||
namespace CompassRenderer {
|
||||
|
||||
// Point helper class for compass calculations
|
||||
struct Point {
|
||||
float x, y;
|
||||
Point(float x, float y) : x(x), y(y) {}
|
||||
float x, y;
|
||||
Point(float x, float y) : x(x), y(y) {}
|
||||
|
||||
void rotate(float angle)
|
||||
{
|
||||
float cos_a = cos(angle);
|
||||
float sin_a = sin(angle);
|
||||
float new_x = x * cos_a - y * sin_a;
|
||||
float new_y = x * sin_a + y * cos_a;
|
||||
x = new_x;
|
||||
y = new_y;
|
||||
}
|
||||
void rotate(float angle) {
|
||||
float cos_a = cos(angle);
|
||||
float sin_a = sin(angle);
|
||||
float new_x = x * cos_a - y * sin_a;
|
||||
float new_y = x * sin_a + y * cos_a;
|
||||
x = new_x;
|
||||
y = new_y;
|
||||
}
|
||||
|
||||
void scale(float factor)
|
||||
{
|
||||
x *= factor;
|
||||
y *= factor;
|
||||
}
|
||||
void scale(float factor) {
|
||||
x *= factor;
|
||||
y *= factor;
|
||||
}
|
||||
|
||||
void translate(float dx, float dy)
|
||||
{
|
||||
x += dx;
|
||||
y += dy;
|
||||
}
|
||||
void translate(float dx, float dy) {
|
||||
x += dx;
|
||||
y += dy;
|
||||
}
|
||||
};
|
||||
|
||||
void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius)
|
||||
{
|
||||
// Show the compass heading (not implemented in original)
|
||||
// This could draw a "N" indicator or north arrow
|
||||
// For now, we'll draw a simple north indicator
|
||||
// const float radius = 17.0f;
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
radius += 4;
|
||||
}
|
||||
Point north(0, -radius);
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
north.rotate(-myHeading);
|
||||
north.translate(compassX, compassY);
|
||||
void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius) {
|
||||
// Show the compass heading (not implemented in original)
|
||||
// This could draw a "N" indicator or north arrow
|
||||
// For now, we'll draw a simple north indicator
|
||||
// const float radius = 17.0f;
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
radius += 4;
|
||||
}
|
||||
Point north(0, -radius);
|
||||
if (uiconfig.compass_mode != meshtastic_CompassMode_FIXED_RING)
|
||||
north.rotate(-myHeading);
|
||||
north.translate(compassX, compassY);
|
||||
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setColor(BLACK);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
||||
} else {
|
||||
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
||||
}
|
||||
display->setColor(WHITE);
|
||||
display->drawString(north.x, north.y - 3, "N");
|
||||
display->setFont(FONT_SMALL);
|
||||
display->setTextAlignment(TEXT_ALIGN_CENTER);
|
||||
display->setColor(BLACK);
|
||||
if (currentResolution == ScreenResolution::High) {
|
||||
display->fillRect(north.x - 8, north.y - 1, display->getStringWidth("N") + 3, FONT_HEIGHT_SMALL - 6);
|
||||
} else {
|
||||
display->fillRect(north.x - 4, north.y - 1, display->getStringWidth("N") + 2, FONT_HEIGHT_SMALL - 6);
|
||||
}
|
||||
display->setColor(WHITE);
|
||||
display->drawString(north.x, north.y - 3, "N");
|
||||
}
|
||||
|
||||
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian)
|
||||
{
|
||||
Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially
|
||||
float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f;
|
||||
Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY);
|
||||
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian) {
|
||||
Point tip(0.0f, -0.5f), tail(0.0f, 0.35f); // pointing up initially
|
||||
float arrowOffsetX = 0.14f, arrowOffsetY = 0.9f;
|
||||
Point leftArrow(tip.x - arrowOffsetX, tip.y + arrowOffsetY), rightArrow(tip.x + arrowOffsetX, tip.y + arrowOffsetY);
|
||||
|
||||
Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow};
|
||||
Point *arrowPoints[] = {&tip, &tail, &leftArrow, &rightArrow};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
arrowPoints[i]->rotate(headingRadian);
|
||||
arrowPoints[i]->scale(compassDiam * 0.6);
|
||||
arrowPoints[i]->translate(compassX, compassY);
|
||||
}
|
||||
for (int i = 0; i < 4; i++) {
|
||||
arrowPoints[i]->rotate(headingRadian);
|
||||
arrowPoints[i]->scale(compassDiam * 0.6);
|
||||
arrowPoints[i]->translate(compassX, compassY);
|
||||
}
|
||||
|
||||
#ifdef USE_EINK
|
||||
display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
|
||||
display->drawTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
|
||||
#else
|
||||
display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
|
||||
display->fillTriangle(tip.x, tip.y, rightArrow.x, rightArrow.y, tail.x, tail.y);
|
||||
#endif
|
||||
display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y);
|
||||
display->drawTriangle(tip.x, tip.y, leftArrow.x, leftArrow.y, tail.x, tail.y);
|
||||
}
|
||||
|
||||
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing)
|
||||
{
|
||||
float radians = bearing * DEG_TO_RAD;
|
||||
void drawArrowToNode(OLEDDisplay *display, int16_t x, int16_t y, int16_t size, float bearing) {
|
||||
float radians = bearing * DEG_TO_RAD;
|
||||
|
||||
Point tip(0, -size / 2);
|
||||
Point left(-size / 6, size / 4);
|
||||
Point right(size / 6, size / 4);
|
||||
Point tail(0, size / 4.5);
|
||||
Point tip(0, -size / 2);
|
||||
Point left(-size / 6, size / 4);
|
||||
Point right(size / 6, size / 4);
|
||||
Point tail(0, size / 4.5);
|
||||
|
||||
tip.rotate(radians);
|
||||
left.rotate(radians);
|
||||
right.rotate(radians);
|
||||
tail.rotate(radians);
|
||||
tip.rotate(radians);
|
||||
left.rotate(radians);
|
||||
right.rotate(radians);
|
||||
tail.rotate(radians);
|
||||
|
||||
tip.translate(x, y);
|
||||
left.translate(x, y);
|
||||
right.translate(x, y);
|
||||
tail.translate(x, y);
|
||||
tip.translate(x, y);
|
||||
left.translate(x, y);
|
||||
right.translate(x, y);
|
||||
tail.translate(x, y);
|
||||
|
||||
display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y);
|
||||
display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y);
|
||||
display->fillTriangle(tip.x, tip.y, left.x, left.y, tail.x, tail.y);
|
||||
display->fillTriangle(tip.x, tip.y, right.x, right.y, tail.x, tail.y);
|
||||
}
|
||||
|
||||
float estimatedHeading(double lat, double lon)
|
||||
{
|
||||
// Simple magnetic declination estimation
|
||||
// This is a very basic implementation - the original might be more sophisticated
|
||||
return 0.0f; // Return 0 for now, indicating no heading available
|
||||
float estimatedHeading(double lat, double lon) {
|
||||
// Simple magnetic declination estimation
|
||||
// This is a very basic implementation - the original might be more sophisticated
|
||||
return 0.0f; // Return 0 for now, indicating no heading available
|
||||
}
|
||||
|
||||
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight)
|
||||
{
|
||||
// Calculate appropriate compass diameter based on display size
|
||||
uint16_t minDimension = (displayWidth < displayHeight) ? displayWidth : displayHeight;
|
||||
uint16_t maxDiam = minDimension / 3; // Use 1/3 of the smaller dimension
|
||||
uint16_t getCompassDiam(uint32_t displayWidth, uint32_t displayHeight) {
|
||||
// Calculate appropriate compass diameter based on display size
|
||||
uint16_t minDimension = (displayWidth < displayHeight) ? displayWidth : displayHeight;
|
||||
uint16_t maxDiam = minDimension / 3; // Use 1/3 of the smaller dimension
|
||||
|
||||
// Ensure minimum and maximum bounds
|
||||
if (maxDiam < 16)
|
||||
maxDiam = 16;
|
||||
if (maxDiam > 64)
|
||||
maxDiam = 64;
|
||||
// Ensure minimum and maximum bounds
|
||||
if (maxDiam < 16)
|
||||
maxDiam = 16;
|
||||
if (maxDiam > 64)
|
||||
maxDiam = 64;
|
||||
|
||||
return maxDiam;
|
||||
return maxDiam;
|
||||
}
|
||||
|
||||
} // namespace CompassRenderer
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace graphics {
|
||||
|
||||
/// Forward declarations
|
||||
class Screen;
|
||||
@@ -17,8 +16,7 @@ class Screen;
|
||||
* Contains all functions related to drawing compass elements, headings,
|
||||
* navigation arrows, and location-based UI components.
|
||||
*/
|
||||
namespace CompassRenderer
|
||||
{
|
||||
namespace CompassRenderer {
|
||||
// Compass drawing functions
|
||||
void drawCompassNorth(OLEDDisplay *display, int16_t compassX, int16_t compassY, float myHeading, int16_t radius);
|
||||
void drawNodeHeading(OLEDDisplay *display, int16_t compassX, int16_t compassY, uint16_t compassDiam, float headingRadian);
|
||||
|
||||
+525
-554
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,7 @@
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace graphics {
|
||||
|
||||
/// Forward declarations
|
||||
class Screen;
|
||||
@@ -16,8 +15,7 @@ class DebugInfo;
|
||||
* Contains all functions related to drawing debug information,
|
||||
* WiFi status, settings screens, and diagnostic data.
|
||||
*/
|
||||
namespace DebugRenderer
|
||||
{
|
||||
namespace DebugRenderer {
|
||||
// Debug frame functions
|
||||
void drawFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
void drawFrameSettings(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
#include "graphics/draw/NodeListRenderer.h"
|
||||
#include "graphics/draw/UIRenderer.h"
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace graphics {
|
||||
|
||||
/**
|
||||
* @brief Collection of all draw renderers
|
||||
@@ -22,8 +21,7 @@ namespace graphics
|
||||
* This namespace provides access to all the specialized rendering
|
||||
* functions organized by category.
|
||||
*/
|
||||
namespace DrawRenderers
|
||||
{
|
||||
namespace DrawRenderers {
|
||||
// Re-export all renderer namespaces for convenience
|
||||
using namespace ClockRenderer;
|
||||
using namespace CompassRenderer;
|
||||
|
||||
+2229
-2314
File diff suppressed because it is too large
Load Diff
+114
-122
@@ -1,143 +1,135 @@
|
||||
#pragma once
|
||||
#if HAS_SCREEN
|
||||
#include "configuration.h"
|
||||
namespace graphics
|
||||
{
|
||||
namespace graphics {
|
||||
|
||||
class menuHandler
|
||||
{
|
||||
public:
|
||||
enum screenMenus {
|
||||
menu_none,
|
||||
lora_Menu,
|
||||
lora_picker,
|
||||
device_role_picker,
|
||||
radio_preset_picker,
|
||||
no_timeout_lora_picker,
|
||||
TZ_picker,
|
||||
twelve_hour_picker,
|
||||
clock_face_picker,
|
||||
clock_menu,
|
||||
position_base_menu,
|
||||
node_base_menu,
|
||||
gps_toggle_menu,
|
||||
gps_format_menu,
|
||||
gps_smart_position_menu,
|
||||
gps_update_interval_menu,
|
||||
gps_position_broadcast_menu,
|
||||
compass_point_north_menu,
|
||||
reset_node_db_menu,
|
||||
buzzermodemenupicker,
|
||||
mui_picker,
|
||||
tftcolormenupicker,
|
||||
brightness_picker,
|
||||
reboot_menu,
|
||||
shutdown_menu,
|
||||
add_favorite,
|
||||
remove_favorite,
|
||||
test_menu,
|
||||
number_test,
|
||||
wifi_toggle_menu,
|
||||
bluetooth_toggle_menu,
|
||||
screen_options_menu,
|
||||
power_menu,
|
||||
system_base_menu,
|
||||
key_verification_init,
|
||||
key_verification_final_prompt,
|
||||
trace_route_menu,
|
||||
throttle_message,
|
||||
message_response_menu,
|
||||
message_viewmode_menu,
|
||||
reply_menu,
|
||||
delete_messages_menu,
|
||||
node_name_length_menu,
|
||||
FrameToggles,
|
||||
DisplayUnits
|
||||
};
|
||||
static screenMenus menuQueue;
|
||||
class menuHandler {
|
||||
public:
|
||||
enum screenMenus {
|
||||
menu_none,
|
||||
lora_Menu,
|
||||
lora_picker,
|
||||
device_role_picker,
|
||||
radio_preset_picker,
|
||||
no_timeout_lora_picker,
|
||||
TZ_picker,
|
||||
twelve_hour_picker,
|
||||
clock_face_picker,
|
||||
clock_menu,
|
||||
position_base_menu,
|
||||
node_base_menu,
|
||||
gps_toggle_menu,
|
||||
gps_format_menu,
|
||||
gps_smart_position_menu,
|
||||
gps_update_interval_menu,
|
||||
gps_position_broadcast_menu,
|
||||
compass_point_north_menu,
|
||||
reset_node_db_menu,
|
||||
buzzermodemenupicker,
|
||||
mui_picker,
|
||||
tftcolormenupicker,
|
||||
brightness_picker,
|
||||
reboot_menu,
|
||||
shutdown_menu,
|
||||
add_favorite,
|
||||
remove_favorite,
|
||||
test_menu,
|
||||
number_test,
|
||||
wifi_toggle_menu,
|
||||
bluetooth_toggle_menu,
|
||||
screen_options_menu,
|
||||
power_menu,
|
||||
system_base_menu,
|
||||
key_verification_init,
|
||||
key_verification_final_prompt,
|
||||
trace_route_menu,
|
||||
throttle_message,
|
||||
message_response_menu,
|
||||
message_viewmode_menu,
|
||||
reply_menu,
|
||||
delete_messages_menu,
|
||||
node_name_length_menu,
|
||||
FrameToggles,
|
||||
DisplayUnits
|
||||
};
|
||||
static screenMenus menuQueue;
|
||||
|
||||
static void OnboardMessage();
|
||||
static void LoraRegionPicker(uint32_t duration = 30000);
|
||||
static void loraMenu();
|
||||
static void DeviceRolePicker();
|
||||
static void RadioPresetPicker();
|
||||
static void handleMenuSwitch(OLEDDisplay *display);
|
||||
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
|
||||
static void clockMenu();
|
||||
static void TZPicker();
|
||||
static void TwelveHourPicker();
|
||||
static void ClockFacePicker();
|
||||
static void messageResponseMenu();
|
||||
static void messageViewModeMenu();
|
||||
static void replyMenu();
|
||||
static void deleteMessagesMenu();
|
||||
static void homeBaseMenu();
|
||||
static void textMessageBaseMenu();
|
||||
static void systemBaseMenu();
|
||||
static void favoriteBaseMenu();
|
||||
static void positionBaseMenu();
|
||||
static void compassNorthMenu();
|
||||
static void GPSToggleMenu();
|
||||
static void GPSFormatMenu();
|
||||
static void GPSSmartPositionMenu();
|
||||
static void GPSUpdateIntervalMenu();
|
||||
static void GPSPositionBroadcastMenu();
|
||||
static void BuzzerModeMenu();
|
||||
static void switchToMUIMenu();
|
||||
static void TFTColorPickerMenu(OLEDDisplay *display);
|
||||
static void nodeListMenu();
|
||||
static void resetNodeDBMenu();
|
||||
static void BrightnessPickerMenu();
|
||||
static void rebootMenu();
|
||||
static void shutdownMenu();
|
||||
static void addFavoriteMenu();
|
||||
static void removeFavoriteMenu();
|
||||
static void traceRouteMenu();
|
||||
static void testMenu();
|
||||
static void numberTest();
|
||||
static void wifiBaseMenu();
|
||||
static void wifiToggleMenu();
|
||||
static void screenOptionsMenu();
|
||||
static void powerMenu();
|
||||
static void nodeNameLengthMenu();
|
||||
static void FrameToggles_menu();
|
||||
static void DisplayUnits_menu();
|
||||
static void textMessageMenu();
|
||||
static void OnboardMessage();
|
||||
static void LoraRegionPicker(uint32_t duration = 30000);
|
||||
static void loraMenu();
|
||||
static void DeviceRolePicker();
|
||||
static void RadioPresetPicker();
|
||||
static void handleMenuSwitch(OLEDDisplay *display);
|
||||
static void showConfirmationBanner(const char *message, std::function<void()> onConfirm);
|
||||
static void clockMenu();
|
||||
static void TZPicker();
|
||||
static void TwelveHourPicker();
|
||||
static void ClockFacePicker();
|
||||
static void messageResponseMenu();
|
||||
static void messageViewModeMenu();
|
||||
static void replyMenu();
|
||||
static void deleteMessagesMenu();
|
||||
static void homeBaseMenu();
|
||||
static void textMessageBaseMenu();
|
||||
static void systemBaseMenu();
|
||||
static void favoriteBaseMenu();
|
||||
static void positionBaseMenu();
|
||||
static void compassNorthMenu();
|
||||
static void GPSToggleMenu();
|
||||
static void GPSFormatMenu();
|
||||
static void GPSSmartPositionMenu();
|
||||
static void GPSUpdateIntervalMenu();
|
||||
static void GPSPositionBroadcastMenu();
|
||||
static void BuzzerModeMenu();
|
||||
static void switchToMUIMenu();
|
||||
static void TFTColorPickerMenu(OLEDDisplay *display);
|
||||
static void nodeListMenu();
|
||||
static void resetNodeDBMenu();
|
||||
static void BrightnessPickerMenu();
|
||||
static void rebootMenu();
|
||||
static void shutdownMenu();
|
||||
static void addFavoriteMenu();
|
||||
static void removeFavoriteMenu();
|
||||
static void traceRouteMenu();
|
||||
static void testMenu();
|
||||
static void numberTest();
|
||||
static void wifiBaseMenu();
|
||||
static void wifiToggleMenu();
|
||||
static void screenOptionsMenu();
|
||||
static void powerMenu();
|
||||
static void nodeNameLengthMenu();
|
||||
static void FrameToggles_menu();
|
||||
static void DisplayUnits_menu();
|
||||
static void textMessageMenu();
|
||||
|
||||
private:
|
||||
static void saveUIConfig();
|
||||
static void keyVerificationInitMenu();
|
||||
static void keyVerificationFinalPrompt();
|
||||
static void BluetoothToggleMenu();
|
||||
private:
|
||||
static void saveUIConfig();
|
||||
static void keyVerificationInitMenu();
|
||||
static void keyVerificationFinalPrompt();
|
||||
static void BluetoothToggleMenu();
|
||||
};
|
||||
|
||||
/* Generic Menu Options designations */
|
||||
enum class OptionsAction { Back, Select };
|
||||
|
||||
template <typename T> struct MenuOption {
|
||||
const char *label;
|
||||
OptionsAction action;
|
||||
bool hasValue;
|
||||
T value;
|
||||
const char *label;
|
||||
OptionsAction action;
|
||||
bool hasValue;
|
||||
T value;
|
||||
|
||||
MenuOption(const char *labelIn, OptionsAction actionIn, T valueIn)
|
||||
: label(labelIn), action(actionIn), hasValue(true), value(valueIn)
|
||||
{
|
||||
}
|
||||
MenuOption(const char *labelIn, OptionsAction actionIn, T valueIn) : label(labelIn), action(actionIn), hasValue(true), value(valueIn) {}
|
||||
|
||||
MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {}
|
||||
MenuOption(const char *labelIn, OptionsAction actionIn) : label(labelIn), action(actionIn), hasValue(false), value() {}
|
||||
};
|
||||
|
||||
struct ScreenColor {
|
||||
uint8_t r;
|
||||
uint8_t g;
|
||||
uint8_t b;
|
||||
bool useVariant;
|
||||
uint8_t r;
|
||||
uint8_t g;
|
||||
uint8_t b;
|
||||
bool useVariant;
|
||||
|
||||
ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false)
|
||||
: r(rIn), g(gIn), b(bIn), useVariant(variantIn)
|
||||
{
|
||||
}
|
||||
ScreenColor(uint8_t rIn = 0, uint8_t gIn = 0, uint8_t bIn = 0, bool variantIn = false) : r(rIn), g(gIn), b(bIn), useVariant(variantIn) {}
|
||||
};
|
||||
|
||||
using RadioPresetOption = MenuOption<meshtastic_Config_LoRaConfig_ModemPreset>;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,10 +9,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace MessageRenderer
|
||||
{
|
||||
namespace graphics {
|
||||
namespace MessageRenderer {
|
||||
|
||||
// Thread filter modes
|
||||
enum class ThreadMode { ALL, CHANNEL, DIRECT };
|
||||
@@ -45,8 +43,7 @@ void drawTextMessageFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16
|
||||
std::vector<std::string> generateLines(OLEDDisplay *display, const char *headerStr, const char *messageBuf, int textWidth);
|
||||
|
||||
// Function to calculate heights for each line
|
||||
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes,
|
||||
const std::vector<bool> &isHeaderVec);
|
||||
std::vector<int> calculateLineHeights(const std::vector<std::string> &lines, const Emote *emotes, const std::vector<bool> &isHeaderVec);
|
||||
|
||||
// Reset scroll state when new messages arrive
|
||||
void resetScrollState();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,8 +5,7 @@
|
||||
#include <OLEDDisplay.h>
|
||||
#include <OLEDDisplayUi.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace graphics {
|
||||
|
||||
/// Forward declarations
|
||||
class Screen;
|
||||
@@ -17,8 +16,7 @@ class Screen;
|
||||
* Contains all functions related to drawing node lists and individual node entries
|
||||
* including last heard, hop signal, distance, and compass views.
|
||||
*/
|
||||
namespace NodeListRenderer
|
||||
{
|
||||
namespace NodeListRenderer {
|
||||
// Entry renderer function types
|
||||
typedef void (*EntryRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int);
|
||||
typedef void (*NodeExtrasRenderer)(OLEDDisplay *, meshtastic_NodeInfoLite *, int16_t, int16_t, int, float, double, double);
|
||||
@@ -30,9 +28,8 @@ enum ListMode_Node { MODE_LAST_HEARD = 0, MODE_HOP_SIGNAL = 1, MODE_COUNT_NODE =
|
||||
enum ListMode_Location { MODE_DISTANCE = 0, MODE_BEARING = 1, MODE_COUNT_LOCATION = 2 };
|
||||
|
||||
// Main node list screen function
|
||||
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title,
|
||||
EntryRenderer renderer, NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0,
|
||||
double lon = 0);
|
||||
void drawNodeListScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *title, EntryRenderer renderer,
|
||||
NodeExtrasRenderer extras = nullptr, float heading = 0, double lat = 0, double lon = 0);
|
||||
|
||||
// Entry renderers
|
||||
void drawEntryLastHeard(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
@@ -42,8 +39,8 @@ void drawEntryDynamic_Nodes(OLEDDisplay *display, meshtastic_NodeInfoLite *node,
|
||||
void drawEntryCompass(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth);
|
||||
|
||||
// Extras renderers
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading,
|
||||
double userLat, double userLon);
|
||||
void drawCompassArrow(OLEDDisplay *display, meshtastic_NodeInfoLite *node, int16_t x, int16_t y, int columnWidth, float myHeading, double userLat,
|
||||
double userLon);
|
||||
|
||||
// Screen frame functions
|
||||
void drawLastHeardScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,44 +9,42 @@
|
||||
#include <string>
|
||||
#define MAX_LINES 5
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace graphics {
|
||||
|
||||
class NotificationRenderer
|
||||
{
|
||||
public:
|
||||
static InputEvent inEvent;
|
||||
static char inKeypress;
|
||||
static int8_t curSelected;
|
||||
static char alertBannerMessage[256];
|
||||
static uint32_t alertBannerUntil; // 0 is a special case meaning forever
|
||||
static const char **optionsArrayPtr;
|
||||
static const int *optionsEnumPtr;
|
||||
static uint8_t alertBannerOptions; // last x lines are seelctable options
|
||||
static std::function<void(int)> alertBannerCallback;
|
||||
static uint32_t numDigits;
|
||||
static uint32_t currentNumber;
|
||||
static VirtualKeyboard *virtualKeyboard;
|
||||
static std::function<void(const std::string &)> textInputCallback;
|
||||
class NotificationRenderer {
|
||||
public:
|
||||
static InputEvent inEvent;
|
||||
static char inKeypress;
|
||||
static int8_t curSelected;
|
||||
static char alertBannerMessage[256];
|
||||
static uint32_t alertBannerUntil; // 0 is a special case meaning forever
|
||||
static const char **optionsArrayPtr;
|
||||
static const int *optionsEnumPtr;
|
||||
static uint8_t alertBannerOptions; // last x lines are seelctable options
|
||||
static std::function<void(int)> alertBannerCallback;
|
||||
static uint32_t numDigits;
|
||||
static uint32_t currentNumber;
|
||||
static VirtualKeyboard *virtualKeyboard;
|
||||
static std::function<void(const std::string &)> textInputCallback;
|
||||
|
||||
static bool pauseBanner;
|
||||
static bool pauseBanner;
|
||||
|
||||
static void resetBanner();
|
||||
static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs);
|
||||
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1],
|
||||
uint16_t totalLines, uint8_t firstOptionToShow, uint16_t maxWidth = 0);
|
||||
static void resetBanner();
|
||||
static void showKeyboardMessagePopupWithTitle(const char *title, const char *content, uint32_t durationMs);
|
||||
static void drawBannercallback(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawAlertBannerOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNumberPicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNodePicker(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawTextInput(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
static void drawNotificationBox(OLEDDisplay *display, OLEDDisplayUiState *state, const char *lines[MAX_LINES + 1], uint16_t totalLines,
|
||||
uint8_t firstOptionToShow, uint16_t maxWidth = 0);
|
||||
|
||||
static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static bool isOverlayBannerShowing();
|
||||
static void drawCriticalFaultFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawSSLScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawFrameFirmware(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static bool isOverlayBannerShowing();
|
||||
|
||||
static graphics::notificationTypeEnum current_notification_type;
|
||||
static graphics::notificationTypeEnum current_notification_type;
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
+1066
-1110
File diff suppressed because it is too large
Load Diff
@@ -10,15 +10,13 @@
|
||||
#define HOURS_IN_MONTH 730
|
||||
|
||||
// Forward declarations for status types
|
||||
namespace meshtastic
|
||||
{
|
||||
namespace meshtastic {
|
||||
class PowerStatus;
|
||||
class NodeStatus;
|
||||
class GPSStatus;
|
||||
} // namespace meshtastic
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace graphics {
|
||||
|
||||
/// Forward declarations
|
||||
class Screen;
|
||||
@@ -29,59 +27,57 @@ class Screen;
|
||||
* Contains utility functions for drawing common UI elements, overlays,
|
||||
* battery indicators, and other shared graphical components.
|
||||
*/
|
||||
class UIRenderer
|
||||
{
|
||||
public:
|
||||
// Common UI elements
|
||||
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus,
|
||||
int node_offset = 0, bool show_total = true, const char *additional_words = "");
|
||||
class UIRenderer {
|
||||
public:
|
||||
// Common UI elements
|
||||
static void drawNodes(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::NodeStatus *nodeStatus, int node_offset = 0,
|
||||
bool show_total = true, const char *additional_words = "");
|
||||
|
||||
// GPS status functions
|
||||
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus,
|
||||
const char *mode = "line1");
|
||||
static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
// GPS status functions
|
||||
static void drawGps(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
static void drawGpsCoordinates(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus, const char *mode = "line1");
|
||||
static void drawGpsAltitude(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
static void drawGpsPowerStatus(OLEDDisplay *display, int16_t x, int16_t y, const meshtastic::GPSStatus *gpsStatus);
|
||||
|
||||
// Overlay and special screens
|
||||
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text);
|
||||
// Overlay and special screens
|
||||
static void drawFrameText(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y, const char *text);
|
||||
|
||||
// Navigation bar overlay
|
||||
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
// Navigation bar overlay
|
||||
static void drawNavigationBar(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
|
||||
static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawNodeInfo(OLEDDisplay *display, const OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawDeviceFocused(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Icon and screen drawing functions
|
||||
static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
// Icon and screen drawing functions
|
||||
static void drawIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
// Compass and location screen
|
||||
static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
// Compass and location screen
|
||||
static void drawCompassAndLocationScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
static NodeNum currentFavoriteNodeNum;
|
||||
static std::vector<meshtastic_NodeInfoLite *> favoritedNodes;
|
||||
static void rebuildFavoritedNodes();
|
||||
static NodeNum currentFavoriteNodeNum;
|
||||
static std::vector<meshtastic_NodeInfoLite *> favoritedNodes;
|
||||
static void rebuildFavoritedNodes();
|
||||
|
||||
// OEM screens
|
||||
#ifdef USERPREFS_OEM_TEXT
|
||||
static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawOEMIconScreen(const char *upperMsg, OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
static void drawOEMBootScreen(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
#endif
|
||||
|
||||
#ifdef USE_EINK
|
||||
/// Used on eink displays while in deep sleep
|
||||
static void drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
/// Used on eink displays while in deep sleep
|
||||
static void drawDeepSleepFrame(OLEDDisplay *display, OLEDDisplayUiState *state, int16_t x, int16_t y);
|
||||
|
||||
/// Used on eink displays when screen updates are paused
|
||||
static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
/// Used on eink displays when screen updates are paused
|
||||
static void drawScreensaverOverlay(OLEDDisplay *display, OLEDDisplayUiState *state);
|
||||
#endif
|
||||
|
||||
static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds);
|
||||
static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime);
|
||||
static std::string drawTimeDelta(uint32_t days, uint32_t hours, uint32_t minutes, uint32_t seconds);
|
||||
static int formatDateTime(char *buffer, size_t bufferSize, uint32_t rtc_sec, OLEDDisplay *display, bool showTime);
|
||||
|
||||
// Check if the display can render a string (detect special chars; emoji)
|
||||
static bool haveGlyphs(const char *str);
|
||||
// Check if the display can render a string (detect special chars; emoji)
|
||||
static bool haveGlyphs(const char *str);
|
||||
}; // namespace UIRenderer
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
+193
-276
@@ -2,8 +2,7 @@
|
||||
#if HAS_SCREEN
|
||||
#include "emotes.h"
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace graphics {
|
||||
|
||||
// Always define Emote list and count
|
||||
const Emote emotes[] = {
|
||||
@@ -13,13 +12,13 @@ const Emote emotes[] = {
|
||||
{"\U0001F44E", thumbdown, thumbs_width, thumbs_height}, // 👎 Thumbs Down
|
||||
|
||||
// --- Smileys (Multiple Unicode Aliases) ---
|
||||
{"\U0001F60A", smiling_eyes, smiling_eyes_width, smiling_eyes_height}, // 😊 Smiling Eyes
|
||||
{"\U0001F600", grinning, grinning_width, grinning_height}, // 😀 Grinning Face
|
||||
{"\U0001F642", slightly_smiling, slightly_smiling_width, slightly_smiling_height}, // 🙂 Slightly Smiling Face
|
||||
{"\U0001F609", winking_face, winking_face_width, winking_face_height}, // 😉 Winking Face
|
||||
{"\U0001F60A", smiling_eyes, smiling_eyes_width, smiling_eyes_height}, // 😊 Smiling Eyes
|
||||
{"\U0001F600", grinning, grinning_width, grinning_height}, // 😀 Grinning Face
|
||||
{"\U0001F642", slightly_smiling, slightly_smiling_width, slightly_smiling_height}, // 🙂 Slightly Smiling Face
|
||||
{"\U0001F609", winking_face, winking_face_width, winking_face_height}, // 😉 Winking Face
|
||||
{"\U0001F601", grinning_smiling_eyes, grinning_smiling_eyes_width, grinning_smiling_eyes_height}, // 😁 Grinning Smiling Eyes
|
||||
{"\U0001F60D", heart_eyes, heart_eyes_width, heart_eyes_height}, // 😍 Heart Eyes
|
||||
{"\U0001F970", heart_smile, heart_smile_width, heart_smile_height}, // 🥰 Smiling Face with Hearts
|
||||
{"\U0001F970", heart_smile, heart_smile_width, heart_smile_height}, // 🥰 Smiling Face with Hearts
|
||||
|
||||
// --- Question/Alert ---
|
||||
{"\u2753", question, question_width, question_height}, // ❓ Question Mark
|
||||
@@ -27,17 +26,16 @@ const Emote emotes[] = {
|
||||
{"\u26A0\uFE0F", caution, caution_width, caution_height}, // ⚠️ Warning Sign
|
||||
|
||||
// --- Laughing Faces ---
|
||||
{"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy
|
||||
{"\U0001F923", rofl, rofl_width, rofl_height}, // 🤣 Rolling on the Floor Laughing
|
||||
{"\U0001F606", smiling_closed_eyes, smiling_closed_eyes_width, smiling_closed_eyes_height}, // 😆 Smiling Closed Eyes
|
||||
{"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat
|
||||
{"\U0001F604", grinning_smiling_eyes_2, grinning_smiling_eyes_2_width,
|
||||
grinning_smiling_eyes_2_height}, // 😄 Grinning Face with Smiling Eyes
|
||||
{"\U0001F62D", loudly_crying_face, loudly_crying_face_width, loudly_crying_face_height}, // 😭 Loudly Crying Face
|
||||
{"\U0001F92E", vomiting, vomiting_width, vomiting_height}, // 🤮 Face Vomiting
|
||||
{"\U0001F60E", cool, cool_width, cool_height}, // 😎 Smiling Face with Sunglasses
|
||||
{"\U0001F440", eyes, eyes_width, eyes_height}, // 👀 Eyes
|
||||
{"\U0001F441\uFE0F", eye, eye_width, eye_height}, // 👁️ Eye
|
||||
{"\U0001F602", haha, haha_width, haha_height}, // 😂 Face with Tears of Joy
|
||||
{"\U0001F923", rofl, rofl_width, rofl_height}, // 🤣 Rolling on the Floor Laughing
|
||||
{"\U0001F606", smiling_closed_eyes, smiling_closed_eyes_width, smiling_closed_eyes_height}, // 😆 Smiling Closed Eyes
|
||||
{"\U0001F605", haha, haha_width, haha_height}, // 😅 Smiling with Sweat
|
||||
{"\U0001F604", grinning_smiling_eyes_2, grinning_smiling_eyes_2_width, grinning_smiling_eyes_2_height}, // 😄 Grinning Face with Smiling Eyes
|
||||
{"\U0001F62D", loudly_crying_face, loudly_crying_face_width, loudly_crying_face_height}, // 😭 Loudly Crying Face
|
||||
{"\U0001F92E", vomiting, vomiting_width, vomiting_height}, // 🤮 Face Vomiting
|
||||
{"\U0001F60E", cool, cool_width, cool_height}, // 😎 Smiling Face with Sunglasses
|
||||
{"\U0001F440", eyes, eyes_width, eyes_height}, // 👀 Eyes
|
||||
{"\U0001F441\uFE0F", eye, eye_width, eye_height}, // 👁️ Eye
|
||||
|
||||
// --- Gestures and People ---
|
||||
{"\U0001F44B", wave_icon, wave_icon_width, wave_icon_height}, // 👋 Waving Hand
|
||||
@@ -56,38 +54,33 @@ const Emote emotes[] = {
|
||||
{"\U0001F3E0", house, house_width, house_height}, // 🏠 House
|
||||
|
||||
// --- Weather ---
|
||||
{"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector)
|
||||
{"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀️ Sun (with variation selector)
|
||||
{"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain
|
||||
{"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud
|
||||
{"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog
|
||||
{"\u2744\uFE0F", snowflake, snowflake_width, snowflake_height}, // ❄️ Snowflake
|
||||
{"\U0001F4A7", drop, drop_width, drop_height}, // 💧 Droplet
|
||||
{"\U0001F321\uFE0F", thermometer, thermometer_width, thermometer_height}, // 🌡️ Thermometer
|
||||
{"\U0001F326\uFE0F", sun_behind_raincloud, sun_behind_raincloud_width,
|
||||
sun_behind_raincloud_height}, // 🌦️ Sun Behind Rain Cloud
|
||||
{"\u26C5", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅ Sun Behind Cloud
|
||||
{"\u26C5\uFE0F", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅️ Sun Behind Cloud
|
||||
{"\U0001F328\uFE0F", cloud_with_snow, cloud_with_snow_width, cloud_with_snow_height}, // 🌨️ Cloud with Snow
|
||||
{"\U0001F329\uFE0F", cloud_with_lightning, cloud_with_lightning_width,
|
||||
cloud_with_lightning_height}, // 🌩️ Cloud with Lightning
|
||||
{"\u26C8", cloud_with_lightning_rain, cloud_with_lightning_rain_width,
|
||||
cloud_with_lightning_rain_height}, // ⛈ Cloud with Lightning and Rain
|
||||
{"\u26C8\uFE0F", cloud_with_lightning_rain, cloud_with_lightning_rain_width,
|
||||
cloud_with_lightning_rain_height}, // ⛈️ Cloud with Lightning and Rain
|
||||
{"\U0001F32C\uFE0F", wind_face, wind_face_width, wind_face_height}, // 🌬️ Wind Face
|
||||
{"\u2600", sun, sun_width, sun_height}, // ☀ Sun (without variation selector)
|
||||
{"\u2600\uFE0F", sun, sun_width, sun_height}, // ☀️ Sun (with variation selector)
|
||||
{"\U0001F327\uFE0F", rain, rain_width, rain_height}, // 🌧️ Cloud with Rain
|
||||
{"\u2601\uFE0F", cloud, cloud_width, cloud_height}, // ☁️ Cloud
|
||||
{"\U0001F32B\uFE0F", fog, fog_width, fog_height}, // 🌫️ Fog
|
||||
{"\u2744\uFE0F", snowflake, snowflake_width, snowflake_height}, // ❄️ Snowflake
|
||||
{"\U0001F4A7", drop, drop_width, drop_height}, // 💧 Droplet
|
||||
{"\U0001F321\uFE0F", thermometer, thermometer_width, thermometer_height}, // 🌡️ Thermometer
|
||||
{"\U0001F326\uFE0F", sun_behind_raincloud, sun_behind_raincloud_width, sun_behind_raincloud_height}, // 🌦️ Sun Behind Rain Cloud
|
||||
{"\u26C5", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅ Sun Behind Cloud
|
||||
{"\u26C5\uFE0F", sun_behind_cloud, sun_behind_cloud_width, sun_behind_cloud_height}, // ⛅️ Sun Behind Cloud
|
||||
{"\U0001F328\uFE0F", cloud_with_snow, cloud_with_snow_width, cloud_with_snow_height}, // 🌨️ Cloud with Snow
|
||||
{"\U0001F329\uFE0F", cloud_with_lightning, cloud_with_lightning_width, cloud_with_lightning_height}, // 🌩️ Cloud with Lightning
|
||||
{"\u26C8", cloud_with_lightning_rain, cloud_with_lightning_rain_width, cloud_with_lightning_rain_height}, // ⛈ Cloud with Lightning and Rain
|
||||
{"\u26C8\uFE0F", cloud_with_lightning_rain, cloud_with_lightning_rain_width, cloud_with_lightning_rain_height}, // ⛈️ Cloud with Lightning and Rain
|
||||
{"\U0001F32C\uFE0F", wind_face, wind_face_width, wind_face_height}, // 🌬️ Wind Face
|
||||
|
||||
// --- Moon Phases ---
|
||||
{"\U0001F311", new_moon, new_moon_width, new_moon_height}, // 🌑 New Moon
|
||||
{"\U0001F312", waxing_crescent_moon, waxing_crescent_moon_width, waxing_crescent_moon_height}, // 🌒 Waxing Crescent Moon
|
||||
{"\U0001F313", first_quarter_moon, first_quarter_moon_width, first_quarter_moon_height}, // 🌓 First Quarter Moon
|
||||
{"\U0001F314", waxing_gibbous_moon, waxing_gibbous_moon_width, waxing_gibbous_moon_height}, // 🌔 Waxing Gibbous Moon
|
||||
{"\U0001F315", full_moon, full_moon_width, full_moon_height}, // 🌕 Full Moon
|
||||
{"\U0001F316", waning_gibbous_moon, waning_gibbous_moon_width, waning_gibbous_moon_height}, // 🌖 Waning Gibbous Moon
|
||||
{"\U0001F317", last_quarter_moon, last_quarter_moon_width, last_quarter_moon_height}, // 🌗 Last Quarter Moon
|
||||
{"\U0001F318", waning_crescent_moon, waning_crescent_moon_width, waning_crescent_moon_height}, // 🌘 Waning Crescent Moon
|
||||
{"\U0001F31B", first_quarter_moon_face, first_quarter_moon_face_width,
|
||||
first_quarter_moon_face_height}, // 🌛 First Quarter Moon Face
|
||||
{"\U0001F311", new_moon, new_moon_width, new_moon_height}, // 🌑 New Moon
|
||||
{"\U0001F312", waxing_crescent_moon, waxing_crescent_moon_width, waxing_crescent_moon_height}, // 🌒 Waxing Crescent Moon
|
||||
{"\U0001F313", first_quarter_moon, first_quarter_moon_width, first_quarter_moon_height}, // 🌓 First Quarter Moon
|
||||
{"\U0001F314", waxing_gibbous_moon, waxing_gibbous_moon_width, waxing_gibbous_moon_height}, // 🌔 Waxing Gibbous Moon
|
||||
{"\U0001F315", full_moon, full_moon_width, full_moon_height}, // 🌕 Full Moon
|
||||
{"\U0001F316", waning_gibbous_moon, waning_gibbous_moon_width, waning_gibbous_moon_height}, // 🌖 Waning Gibbous Moon
|
||||
{"\U0001F317", last_quarter_moon, last_quarter_moon_width, last_quarter_moon_height}, // 🌗 Last Quarter Moon
|
||||
{"\U0001F318", waning_crescent_moon, waning_crescent_moon_width, waning_crescent_moon_height}, // 🌘 Waning Crescent Moon
|
||||
{"\U0001F31B", first_quarter_moon_face, first_quarter_moon_face_width, first_quarter_moon_face_height}, // 🌛 First Quarter Moon Face
|
||||
|
||||
// --- Misc Faces ---
|
||||
{"\U0001F608", devil, devil_width, devil_height}, // 😈 Smiling Face with Horns
|
||||
@@ -156,325 +149,249 @@ const Emote emotes[] = {
|
||||
const int numEmotes = sizeof(emotes) / sizeof(emotes[0]);
|
||||
|
||||
#ifndef EXCLUDE_EMOJI
|
||||
const unsigned char thumbup[] PROGMEM = {0x00, 0x03, 0x80, 0x04, 0x80, 0x04, 0x40, 0x04, 0x20, 0x02, 0x18,
|
||||
0x02, 0x06, 0x3F, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x70,
|
||||
0x06, 0x40, 0x06, 0x30, 0x08, 0x20, 0xF0, 0x1F, 0x00, 0x00};
|
||||
const unsigned char thumbup[] PROGMEM = {0x00, 0x03, 0x80, 0x04, 0x80, 0x04, 0x40, 0x04, 0x20, 0x02, 0x18, 0x02, 0x06, 0x3F, 0x06, 0x40,
|
||||
0x06, 0x70, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x30, 0x08, 0x20, 0xF0, 0x1F, 0x00, 0x00};
|
||||
|
||||
const unsigned char thumbdown[] PROGMEM = {0xF0, 0x1F, 0x08, 0x20, 0x06, 0x30, 0x06, 0x40, 0x06, 0x70, 0x06,
|
||||
0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x3F, 0x18, 0x02, 0x20, 0x02,
|
||||
0x40, 0x04, 0x80, 0x04, 0x80, 0x04, 0x00, 0x03, 0x00, 0x00};
|
||||
const unsigned char thumbdown[] PROGMEM = {0xF0, 0x1F, 0x08, 0x20, 0x06, 0x30, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40, 0x06, 0x70, 0x06, 0x40,
|
||||
0x06, 0x3F, 0x18, 0x02, 0x20, 0x02, 0x40, 0x04, 0x80, 0x04, 0x80, 0x04, 0x00, 0x03, 0x00, 0x00};
|
||||
|
||||
const unsigned char smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
|
||||
0x4A, 0x02, 0x40, 0x02, 0x40, 0x22, 0x44, 0x22, 0x44, 0xC2, 0x43,
|
||||
0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x02, 0x40, 0x02, 0x40,
|
||||
0x22, 0x44, 0x22, 0x44, 0xC2, 0x43, 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42,
|
||||
0x42, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44,
|
||||
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char grinning[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, 0x42, 0x02, 0x40, 0x02, 0x40,
|
||||
0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char slightly_smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42,
|
||||
0x42, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44,
|
||||
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char slightly_smiling[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x22, 0x42, 0x42, 0x02, 0x40, 0x02, 0x40,
|
||||
0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char winking_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42,
|
||||
0x46, 0x02, 0x40, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44,
|
||||
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char winking_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x44, 0x20, 0x42, 0x46, 0x02, 0x40, 0x02, 0x40,
|
||||
0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char grinning_smiling_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
|
||||
0x4A, 0x02, 0x40, 0xFA, 0x5F, 0x0A, 0x50, 0x0A, 0x50, 0x12, 0x48,
|
||||
0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char heart_smile[] PROGMEM = {0x00, 0x00, 0x6C, 0x07, 0x7C, 0x18, 0x7C, 0x20, 0x38, 0x24, 0x52,
|
||||
0x0A, 0x02, 0xD8, 0x02, 0xF8, 0x22, 0xFC, 0x20, 0x74, 0xDB, 0x23,
|
||||
0x1F, 0x00, 0x1F, 0x20, 0x0E, 0x18, 0xE4, 0x07, 0x00, 0x00};
|
||||
const unsigned char heart_smile[] PROGMEM = {0x00, 0x00, 0x6C, 0x07, 0x7C, 0x18, 0x7C, 0x20, 0x38, 0x24, 0x52, 0x0A, 0x02, 0xD8, 0x02, 0xF8,
|
||||
0x22, 0xFC, 0x20, 0x74, 0xDB, 0x23, 0x1F, 0x00, 0x1F, 0x20, 0x0E, 0x18, 0xE4, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA,
|
||||
0x5F, 0x72, 0x4E, 0x22, 0x44, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48,
|
||||
0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char heart_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x54, 0x2A, 0xFA, 0x5F, 0x72, 0x4E, 0x22, 0x44,
|
||||
0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x24, 0x24, 0xC4, 0x23, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char question[] PROGMEM = {0xE0, 0x07, 0x10, 0x08, 0x08, 0x10, 0x88, 0x11, 0x48, 0x12, 0x48,
|
||||
0x12, 0x48, 0x12, 0x30, 0x11, 0x80, 0x08, 0x40, 0x04, 0x40, 0x02,
|
||||
0xC0, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x40, 0x02, 0x80, 0x01};
|
||||
const unsigned char question[] PROGMEM = {0xE0, 0x07, 0x10, 0x08, 0x08, 0x10, 0x88, 0x11, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x30, 0x11,
|
||||
0x80, 0x08, 0x40, 0x04, 0x40, 0x02, 0xC0, 0x03, 0x00, 0x00, 0xC0, 0x03, 0x40, 0x02, 0x80, 0x01};
|
||||
|
||||
const unsigned char bang[] PROGMEM = {0x30, 0x0C, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48,
|
||||
0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x30, 0x0C,
|
||||
0x00, 0x00, 0x30, 0x0C, 0x48, 0x12, 0x30, 0x0C, 0x00, 0x00};
|
||||
const unsigned char bang[] PROGMEM = {0x30, 0x0C, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x12,
|
||||
0x48, 0x12, 0x48, 0x12, 0x30, 0x0C, 0x00, 0x00, 0x30, 0x0C, 0x48, 0x12, 0x30, 0x0C, 0x00, 0x00};
|
||||
|
||||
const unsigned char haha[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
|
||||
0x4A, 0x0A, 0x50, 0x0E, 0x70, 0xF2, 0x4F, 0x12, 0x48, 0x32, 0x44,
|
||||
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char haha[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52, 0x4A, 0x0A, 0x50, 0x0E, 0x70,
|
||||
0xF2, 0x4F, 0x12, 0x48, 0x32, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char rofl[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02,
|
||||
0x4C, 0x02, 0x4A, 0x1A, 0x49, 0x8A, 0x48, 0x42, 0x48, 0x22, 0x44,
|
||||
0xE4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char rofl[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x84, 0x21, 0x84, 0x20, 0x02, 0x4C, 0x02, 0x4A, 0x1A, 0x49,
|
||||
0x8A, 0x48, 0x42, 0x48, 0x22, 0x44, 0xE4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char smiling_closed_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42,
|
||||
0x42, 0x22, 0x44, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44,
|
||||
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char smiling_closed_eyes[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x42, 0x42, 0x22, 0x44, 0x02, 0x40,
|
||||
0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char grinning_smiling_eyes_2[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x24, 0x24, 0x52,
|
||||
0x4A, 0x02, 0x40, 0x02, 0x40, 0xF2, 0x4F, 0x12, 0x48, 0x22, 0x44,
|
||||
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char loudly_crying_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A,
|
||||
0x52, 0x12, 0x48, 0x12, 0x48, 0x92, 0x49, 0x52, 0x4A, 0x52, 0x4A,
|
||||
0x54, 0x2A, 0x94, 0x29, 0x18, 0x18, 0xF0, 0x0F, 0x00, 0x00};
|
||||
const unsigned char loudly_crying_face[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x2C, 0x4A, 0x52, 0x12, 0x48, 0x12, 0x48,
|
||||
0x92, 0x49, 0x52, 0x4A, 0x52, 0x4A, 0x54, 0x2A, 0x94, 0x29, 0x18, 0x18, 0xF0, 0x0F, 0x00, 0x00};
|
||||
|
||||
const unsigned char wave_icon[] PROGMEM = {0x00, 0x00, 0xC0, 0x18, 0x30, 0x21, 0x48, 0x5A, 0x94, 0x64, 0x24,
|
||||
0x25, 0x4A, 0x24, 0x12, 0x44, 0x22, 0x44, 0x04, 0x40, 0x08, 0x40,
|
||||
0x12, 0x40, 0x22, 0x20, 0xC4, 0x10, 0x18, 0x0F, 0x00, 0x00};
|
||||
const unsigned char wave_icon[] PROGMEM = {0x00, 0x00, 0xC0, 0x18, 0x30, 0x21, 0x48, 0x5A, 0x94, 0x64, 0x24, 0x25, 0x4A, 0x24, 0x12, 0x44,
|
||||
0x22, 0x44, 0x04, 0x40, 0x08, 0x40, 0x12, 0x40, 0x22, 0x20, 0xC4, 0x10, 0x18, 0x0F, 0x00, 0x00};
|
||||
|
||||
const unsigned char cowboy[] PROGMEM = {0x70, 0x0E, 0x8F, 0xF1, 0x11, 0x88, 0x21, 0x84, 0xC2, 0x43, 0x1E,
|
||||
0x78, 0xE2, 0x47, 0x42, 0x42, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44,
|
||||
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char cowboy[] PROGMEM = {0x70, 0x0E, 0x8F, 0xF1, 0x11, 0x88, 0x21, 0x84, 0xC2, 0x43, 0x1E, 0x78, 0xE2, 0x47, 0x42, 0x42,
|
||||
0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char deadmau5[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0xE4, 0x27, 0x12, 0x48, 0x0A,
|
||||
0x50, 0x0E, 0x70, 0x11, 0x88, 0x19, 0x98, 0x19, 0x98, 0x19, 0x98,
|
||||
0x19, 0x98, 0x19, 0x98, 0x11, 0x88, 0x0E, 0x70, 0x00, 0x00};
|
||||
const unsigned char deadmau5[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0xE4, 0x27, 0x12, 0x48, 0x0A, 0x50, 0x0E, 0x70, 0x11, 0x88,
|
||||
0x19, 0x98, 0x19, 0x98, 0x19, 0x98, 0x19, 0x98, 0x19, 0x98, 0x11, 0x88, 0x0E, 0x70, 0x00, 0x00};
|
||||
|
||||
const unsigned char sun[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xEC, 0x37, 0xFC, 0x3F, 0xF8, 0x1F, 0xFC,
|
||||
0x3F, 0xFE, 0x7F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFC, 0x3F,
|
||||
0xF8, 0x1F, 0xFC, 0x3F, 0xEC, 0x37, 0x80, 0x01, 0x00, 0x00};
|
||||
const unsigned char sun[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xEC, 0x37, 0xFC, 0x3F, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFC, 0x3F,
|
||||
0xFC, 0x3F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, 0xFC, 0x3F, 0xEC, 0x37, 0x80, 0x01, 0x00, 0x00};
|
||||
|
||||
const unsigned char rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE,
|
||||
0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12,
|
||||
0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00};
|
||||
const unsigned char rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F,
|
||||
0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12, 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
const unsigned char cloud[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC,
|
||||
0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const unsigned char cloud[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F,
|
||||
0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
const unsigned char fog[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x88, 0x88, 0x54, 0x55, 0x22, 0x22, 0x00,
|
||||
0x00, 0x44, 0x44, 0xAA, 0x2A, 0x11, 0x11, 0x00, 0x00, 0x88, 0x88,
|
||||
0x54, 0x55, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const unsigned char fog[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x88, 0x88, 0x54, 0x55, 0x22, 0x22, 0x00, 0x00, 0x44, 0x44, 0xAA, 0x2A,
|
||||
0x11, 0x11, 0x00, 0x00, 0x88, 0x88, 0x54, 0x55, 0x22, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
const unsigned char devil[] PROGMEM = {0x06, 0x60, 0xCA, 0x53, 0x32, 0x4C, 0x22, 0x44, 0x44, 0x22, 0x3A,
|
||||
0x5C, 0x32, 0x4C, 0x52, 0x4A, 0x72, 0x4E, 0x02, 0x40, 0x22, 0x44,
|
||||
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char devil[] PROGMEM = {0x06, 0x60, 0xCA, 0x53, 0x32, 0x4C, 0x22, 0x44, 0x44, 0x22, 0x3A, 0x5C, 0x32, 0x4C, 0x52, 0x4A,
|
||||
0x72, 0x4E, 0x02, 0x40, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char heart[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x3C, 0x3C, 0x7E, 0x7E, 0xFE, 0x7F, 0xFE,
|
||||
0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xF8, 0x1F, 0xF8, 0x1F,
|
||||
0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01, 0x00, 0x00};
|
||||
const unsigned char heart[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x3C, 0x3C, 0x7E, 0x7E, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F,
|
||||
0xFC, 0x3F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01, 0x00, 0x00};
|
||||
|
||||
const unsigned char poo[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x20, 0x04, 0x10, 0x04, 0xF0,
|
||||
0x08, 0x10, 0x10, 0x48, 0x12, 0x08, 0x18, 0xE8, 0x21, 0x1C, 0x40,
|
||||
0x42, 0x42, 0x82, 0x41, 0x02, 0x30, 0xFC, 0x0F, 0x00, 0x00};
|
||||
const unsigned char poo[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x40, 0x02, 0x20, 0x04, 0x10, 0x04, 0xF0, 0x08, 0x10, 0x10, 0x48, 0x12,
|
||||
0x08, 0x18, 0xE8, 0x21, 0x1C, 0x40, 0x42, 0x42, 0x82, 0x41, 0x02, 0x30, 0xFC, 0x0F, 0x00, 0x00};
|
||||
|
||||
const unsigned char bell_icon[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x80, 0x01, 0xE0, 0x07, 0xF0, 0x0F, 0xF0,
|
||||
0x0F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xFC, 0x3F,
|
||||
0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0x80, 0x01, 0x00, 0x00};
|
||||
const unsigned char bell_icon[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0x80, 0x01, 0xE0, 0x07, 0xF0, 0x0F, 0xF0, 0x0F, 0xF8, 0x1F, 0xF8, 0x1F,
|
||||
0xF8, 0x1F, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0x80, 0x01, 0x00, 0x00};
|
||||
|
||||
const unsigned char cookie[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x22, 0x32,
|
||||
0x40, 0x02, 0x58, 0x82, 0x5B, 0x92, 0x43, 0x82, 0x43, 0x02, 0x40,
|
||||
0x64, 0x28, 0x64, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char cookie[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x34, 0x22, 0x32, 0x40, 0x02, 0x58, 0x82, 0x5B,
|
||||
0x92, 0x43, 0x82, 0x43, 0x02, 0x40, 0x64, 0x28, 0x64, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC,
|
||||
0x1F, 0xFE, 0x3E, 0x7E, 0x3E, 0x3E, 0x7C, 0x1E, 0x78, 0x1E, 0x70,
|
||||
0x1C, 0x70, 0x1C, 0x70, 0x38, 0x38, 0x30, 0x38, 0x60, 0x0C};
|
||||
const unsigned char fire[] PROGMEM = {0x30, 0x00, 0xF0, 0x00, 0xF8, 0x03, 0xF8, 0x07, 0xFC, 0x1F, 0xFC, 0x1F, 0xFE, 0x3E, 0x7E, 0x3E,
|
||||
0x3E, 0x7C, 0x1E, 0x78, 0x1E, 0x70, 0x1C, 0x70, 0x1C, 0x70, 0x38, 0x38, 0x30, 0x38, 0x60, 0x0C};
|
||||
|
||||
const unsigned char peace_sign[] PROGMEM = {0xC0, 0x30, 0x40, 0x29, 0x40, 0x25, 0x40, 0x15, 0x40, 0x12, 0x38,
|
||||
0x0A, 0x54, 0x68, 0x54, 0x58, 0x54, 0x44, 0x3C, 0x22, 0x04, 0x22,
|
||||
0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char peace_sign[] PROGMEM = {0xC0, 0x30, 0x40, 0x29, 0x40, 0x25, 0x40, 0x15, 0x40, 0x12, 0x38, 0x0A, 0x54, 0x68, 0x54, 0x58,
|
||||
0x54, 0x44, 0x3C, 0x22, 0x04, 0x22, 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90,
|
||||
0x09, 0x98, 0x19, 0x94, 0x29, 0xA4, 0x25, 0xA4, 0x25, 0x84, 0x21,
|
||||
0x84, 0x21, 0x86, 0x61, 0x4E, 0x72, 0x7F, 0x7E, 0x3F, 0xFC};
|
||||
const unsigned char praying[] PROGMEM = {0x00, 0x00, 0x40, 0x02, 0xA0, 0x05, 0x90, 0x09, 0x90, 0x09, 0x90, 0x09, 0x98, 0x19, 0x94, 0x29,
|
||||
0xA4, 0x25, 0xA4, 0x25, 0x84, 0x21, 0x84, 0x21, 0x86, 0x61, 0x4E, 0x72, 0x7F, 0x7E, 0x3F, 0xFC};
|
||||
|
||||
const unsigned char sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00,
|
||||
0x1F, 0x80, 0x3F, 0xE0, 0xFF, 0x80, 0x3F, 0x10, 0x1F, 0x10, 0x0E,
|
||||
0x38, 0x04, 0xFE, 0x04, 0x38, 0x00, 0x10, 0x00, 0x10, 0x00};
|
||||
const unsigned char sparkles[] PROGMEM = {0x00, 0x00, 0x10, 0x00, 0x38, 0x04, 0x10, 0x04, 0x00, 0x0E, 0x00, 0x1F, 0x80, 0x3F, 0xE0, 0xFF,
|
||||
0x80, 0x3F, 0x10, 0x1F, 0x10, 0x0E, 0x38, 0x04, 0xFE, 0x04, 0x38, 0x00, 0x10, 0x00, 0x10, 0x00};
|
||||
|
||||
const unsigned char clown[] PROGMEM = {0x00, 0x00, 0xEE, 0x77, 0x1A, 0x58, 0x06, 0x60, 0x24, 0x24, 0x72,
|
||||
0x4E, 0x22, 0x44, 0x82, 0x41, 0x82, 0x41, 0x1A, 0x58, 0xF2, 0x4F,
|
||||
0x14, 0x28, 0xE4, 0x27, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char clown[] PROGMEM = {0x00, 0x00, 0xEE, 0x77, 0x1A, 0x58, 0x06, 0x60, 0x24, 0x24, 0x72, 0x4E, 0x22, 0x44, 0x82, 0x41,
|
||||
0x82, 0x41, 0x1A, 0x58, 0xF2, 0x4F, 0x14, 0x28, 0xE4, 0x27, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char robo[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0x80, 0x01, 0xFC, 0x3F, 0x04, 0x20, 0x74,
|
||||
0x2E, 0x52, 0x4A, 0x72, 0x4E, 0x02, 0x40, 0x02, 0x40, 0xA2, 0x4A,
|
||||
0x52, 0x45, 0x04, 0x20, 0x04, 0x20, 0xFC, 0x3F, 0x00, 0x00};
|
||||
const unsigned char robo[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0x80, 0x01, 0xFC, 0x3F, 0x04, 0x20, 0x74, 0x2E, 0x52, 0x4A, 0x72, 0x4E,
|
||||
0x02, 0x40, 0x02, 0x40, 0xA2, 0x4A, 0x52, 0x45, 0x04, 0x20, 0x04, 0x20, 0xFC, 0x3F, 0x00, 0x00};
|
||||
|
||||
const unsigned char hole[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x3C, 0x3C,
|
||||
0x06, 0x60, 0x0C, 0x30, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x00};
|
||||
const unsigned char hole[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0xF0, 0x0F, 0x3C, 0x3C, 0x06, 0x60, 0x0C, 0x30, 0xF0, 0x0F, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
const unsigned char bowling[] PROGMEM = {0x00, 0x38, 0x00, 0x44, 0x00, 0x44, 0x00, 0x44, 0x00, 0x28, 0x00,
|
||||
0x38, 0x00, 0x28, 0x78, 0x44, 0x84, 0x82, 0x22, 0x83, 0x52, 0x83,
|
||||
0x02, 0x83, 0x02, 0x45, 0x84, 0x44, 0x78, 0x38, 0x00, 0x00};
|
||||
const unsigned char bowling[] PROGMEM = {0x00, 0x38, 0x00, 0x44, 0x00, 0x44, 0x00, 0x44, 0x00, 0x28, 0x00, 0x38, 0x00, 0x28, 0x78, 0x44,
|
||||
0x84, 0x82, 0x22, 0x83, 0x52, 0x83, 0x02, 0x83, 0x02, 0x45, 0x84, 0x44, 0x78, 0x38, 0x00, 0x00};
|
||||
|
||||
const unsigned char vulcan_salute[] PROGMEM = {0x08, 0x02, 0x16, 0x0D, 0x15, 0x15, 0x15, 0x15, 0xA9, 0x12, 0x4A,
|
||||
0x0A, 0x02, 0x38, 0x04, 0x48, 0x04, 0x44, 0x04, 0x22, 0x04, 0x22,
|
||||
0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char vulcan_salute[] PROGMEM = {0x08, 0x02, 0x16, 0x0D, 0x15, 0x15, 0x15, 0x15, 0xA9, 0x12, 0x4A, 0x0A, 0x02, 0x38, 0x04, 0x48,
|
||||
0x04, 0x44, 0x04, 0x22, 0x04, 0x22, 0x04, 0x12, 0x08, 0x10, 0x10, 0x08, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char jack_o_lantern[] PROGMEM = {0xC0, 0x00, 0x80, 0x01, 0xB8, 0x1D, 0xC4, 0x23, 0x22, 0x44, 0x05,
|
||||
0xA0, 0x31, 0x8C, 0x51, 0x8A, 0x61, 0x86, 0x09, 0x90, 0xB9, 0x9D,
|
||||
0x49, 0x92, 0xB2, 0x4D, 0x42, 0x42, 0x04, 0x20, 0xF8, 0x1F};
|
||||
const unsigned char jack_o_lantern[] PROGMEM = {0xC0, 0x00, 0x80, 0x01, 0xB8, 0x1D, 0xC4, 0x23, 0x22, 0x44, 0x05, 0xA0, 0x31, 0x8C, 0x51, 0x8A,
|
||||
0x61, 0x86, 0x09, 0x90, 0xB9, 0x9D, 0x49, 0x92, 0xB2, 0x4D, 0x42, 0x42, 0x04, 0x20, 0xF8, 0x1F};
|
||||
|
||||
const unsigned char ghost[] PROGMEM = {0xC0, 0x03, 0xF0, 0x0F, 0xF8, 0x1F, 0xDC, 0x3B, 0xBC, 0x3D, 0xDF,
|
||||
0xFB, 0xFF, 0xFF, 0x1F, 0xF8, 0x1E, 0x78, 0x1C, 0x38, 0x3C, 0x3C,
|
||||
0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0x8C, 0x31};
|
||||
const unsigned char ghost[] PROGMEM = {0xC0, 0x03, 0xF0, 0x0F, 0xF8, 0x1F, 0xDC, 0x3B, 0xBC, 0x3D, 0xDF, 0xFB, 0xFF, 0xFF, 0x1F, 0xF8,
|
||||
0x1E, 0x78, 0x1C, 0x38, 0x3C, 0x3C, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0x8C, 0x31};
|
||||
|
||||
const unsigned char skull[] PROGMEM = {0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xC7,
|
||||
0xE3, 0x87, 0xE1, 0x87, 0xE1, 0x8F, 0xF1, 0xFE, 0x7F, 0x7C, 0x3E,
|
||||
0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xB0, 0x0D};
|
||||
const unsigned char skull[] PROGMEM = {0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xC7, 0xE3, 0x87, 0xE1, 0x87, 0xE1,
|
||||
0x8F, 0xF1, 0xFE, 0x7F, 0x7C, 0x3E, 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xB0, 0x0D};
|
||||
|
||||
const unsigned char vomiting[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x22,
|
||||
0x44, 0x42, 0x42, 0x22, 0x44, 0x02, 0x40, 0x02, 0x40, 0xC2, 0x43,
|
||||
0x64, 0x26, 0x64, 0x26, 0x68, 0x16, 0x50, 0x0A, 0xF8, 0x1F};
|
||||
const unsigned char vomiting[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x22, 0x44, 0x42, 0x42, 0x22, 0x44,
|
||||
0x02, 0x40, 0x02, 0x40, 0xC2, 0x43, 0x64, 0x26, 0x64, 0x26, 0x68, 0x16, 0x50, 0x0A, 0xF8, 0x1F};
|
||||
|
||||
const unsigned char cool[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0xFC, 0x3F, 0xFA,
|
||||
0x5F, 0x72, 0x4E, 0x02, 0x40, 0x12, 0x48, 0x12, 0x48, 0x22, 0x44,
|
||||
0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char cool[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0xFC, 0x3F, 0xFA, 0x5F, 0x72, 0x4E, 0x02, 0x40,
|
||||
0x12, 0x48, 0x12, 0x48, 0x22, 0x44, 0xC4, 0x23, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char shortcake[] PROGMEM = {0x00, 0x00, 0x00, 0x0F, 0x80, 0x3F, 0xE0, 0xFC, 0xE0, 0xE1, 0xF0,
|
||||
0xB8, 0x10, 0x87, 0xC8, 0x80, 0x3C, 0xE0, 0x06, 0x98, 0x02, 0xC7,
|
||||
0xE2, 0x30, 0x1A, 0x0E, 0xC6, 0x01, 0x32, 0x00, 0x0E, 0x00};
|
||||
const unsigned char shortcake[] PROGMEM = {0x00, 0x00, 0x00, 0x0F, 0x80, 0x3F, 0xE0, 0xFC, 0xE0, 0xE1, 0xF0, 0xB8, 0x10, 0x87, 0xC8, 0x80,
|
||||
0x3C, 0xE0, 0x06, 0x98, 0x02, 0xC7, 0xE2, 0x30, 0x1A, 0x0E, 0xC6, 0x01, 0x32, 0x00, 0x0E, 0x00};
|
||||
|
||||
const unsigned char caution[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x60,
|
||||
0x06, 0x70, 0x0E, 0x70, 0x0E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E,
|
||||
0xFC, 0x3F, 0x7E, 0x7E, 0x7E, 0x7E, 0xFC, 0x3F, 0x00, 0x00};
|
||||
const unsigned char caution[] PROGMEM = {0x00, 0x00, 0x80, 0x01, 0xC0, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x60, 0x06, 0x70, 0x0E, 0x70, 0x0E,
|
||||
0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0xFC, 0x3F, 0x7E, 0x7E, 0x7E, 0x7E, 0xFC, 0x3F, 0x00, 0x00};
|
||||
|
||||
const unsigned char clipboard[] PROGMEM = {0xC0, 0x03, 0x7E, 0x7E, 0xC2, 0x43, 0xFA, 0x5F, 0x0A, 0x5B, 0xFA,
|
||||
0x5F, 0x8A, 0x54, 0xFA, 0x5F, 0x4A, 0x58, 0xFA, 0x5F, 0x2A, 0x51,
|
||||
0xFA, 0x5F, 0x0A, 0x59, 0xFA, 0x5F, 0x02, 0x40, 0xFE, 0x7F};
|
||||
const unsigned char clipboard[] PROGMEM = {0xC0, 0x03, 0x7E, 0x7E, 0xC2, 0x43, 0xFA, 0x5F, 0x0A, 0x5B, 0xFA, 0x5F, 0x8A, 0x54, 0xFA, 0x5F,
|
||||
0x4A, 0x58, 0xFA, 0x5F, 0x2A, 0x51, 0xFA, 0x5F, 0x0A, 0x59, 0xFA, 0x5F, 0x02, 0x40, 0xFE, 0x7F};
|
||||
|
||||
const unsigned char snowflake[] PROGMEM = {0x00, 0x00, 0x40, 0x01, 0x88, 0x08, 0x8C, 0x18, 0xD0, 0x05, 0x60,
|
||||
0x03, 0x32, 0x26, 0x1C, 0x1C, 0x32, 0x26, 0x60, 0x03, 0xD0, 0x05,
|
||||
0x8C, 0x18, 0x88, 0x08, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00};
|
||||
const unsigned char snowflake[] PROGMEM = {0x00, 0x00, 0x40, 0x01, 0x88, 0x08, 0x8C, 0x18, 0xD0, 0x05, 0x60, 0x03, 0x32, 0x26, 0x1C, 0x1C,
|
||||
0x32, 0x26, 0x60, 0x03, 0xD0, 0x05, 0x8C, 0x18, 0x88, 0x08, 0x40, 0x01, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
const unsigned char drop[] PROGMEM = {0x00, 0x00, 0x00, 0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xE0,
|
||||
0x0F, 0xF0, 0x1F, 0xF0, 0x1F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F,
|
||||
0xF8, 0x3F, 0xF0, 0x1F, 0xE0, 0x0F, 0x80, 0x03, 0x00, 0x00};
|
||||
const unsigned char drop[] PROGMEM = {0x00, 0x00, 0x00, 0x01, 0x80, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xE0, 0x0F, 0xF0, 0x1F, 0xF0, 0x1F,
|
||||
0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF8, 0x3F, 0xF0, 0x1F, 0xE0, 0x0F, 0x80, 0x03, 0x00, 0x00};
|
||||
|
||||
const unsigned char thermometer[] PROGMEM = {0x00, 0x00, 0x0C, 0x00, 0x16, 0x00, 0x2E, 0x00, 0x5C, 0x00, 0xB8,
|
||||
0x00, 0x70, 0x01, 0xE0, 0x02, 0xC0, 0x05, 0x80, 0x3B, 0x00, 0x47,
|
||||
0x00, 0xBE, 0x00, 0x9E, 0x00, 0xBE, 0x00, 0x7C, 0x00, 0x38};
|
||||
const unsigned char thermometer[] PROGMEM = {0x00, 0x00, 0x0C, 0x00, 0x16, 0x00, 0x2E, 0x00, 0x5C, 0x00, 0xB8, 0x00, 0x70, 0x01, 0xE0, 0x02,
|
||||
0xC0, 0x05, 0x80, 0x3B, 0x00, 0x47, 0x00, 0xBE, 0x00, 0x9E, 0x00, 0xBE, 0x00, 0x7C, 0x00, 0x38};
|
||||
|
||||
const unsigned char sun_behind_raincloud[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x0E, 0x38, 0x1F, 0xFC, 0x37, 0xEE,
|
||||
0x77, 0xDE, 0x7B, 0x3E, 0x7C, 0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12,
|
||||
0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00};
|
||||
const unsigned char sun_behind_raincloud[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x0E, 0x38, 0x1F, 0xFC, 0x37, 0xEE, 0x77, 0xDE, 0x7B, 0x3E, 0x7C,
|
||||
0xFC, 0x3F, 0x00, 0x00, 0x48, 0x12, 0x48, 0x12, 0x24, 0x09, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
const unsigned char sun_behind_cloud[] PROGMEM = {0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x04, 0x0E, 0x3C, 0x1B, 0xFC,
|
||||
0x3B, 0xFE, 0x7B, 0xFA, 0x7B, 0xF6, 0x7D, 0x0C, 0x3E, 0xF8, 0x1F,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const unsigned char sun_behind_cloud[] PROGMEM = {0x00, 0x00, 0xF0, 0x01, 0x08, 0x02, 0x04, 0x0E, 0x3C, 0x1B, 0xFC, 0x3B, 0xFE, 0x7B, 0xFA, 0x7B,
|
||||
0xF6, 0x7D, 0x0C, 0x3E, 0xF8, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
const unsigned char cloud_with_snow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE,
|
||||
0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x00, 0x08, 0x02,
|
||||
0x40, 0x10, 0x00, 0x00, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00};
|
||||
const unsigned char cloud_with_snow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F,
|
||||
0xFC, 0x3F, 0x00, 0x00, 0x08, 0x02, 0x40, 0x10, 0x00, 0x00, 0x24, 0x09, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
const unsigned char cloud_with_lightning[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE,
|
||||
0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x80, 0x01,
|
||||
0x80, 0x01, 0xC0, 0x07, 0x00, 0x03, 0x00, 0x03, 0x00, 0x01};
|
||||
const unsigned char cloud_with_lightning[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F,
|
||||
0xFC, 0x3F, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0xC0, 0x07, 0x00, 0x03, 0x00, 0x03, 0x00, 0x01};
|
||||
|
||||
const unsigned char cloud_with_lightning_rain[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x38, 0x1F, 0xFC, 0x3F, 0xFE,
|
||||
0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0x00, 0x01, 0x90, 0x21,
|
||||
0x90, 0x21, 0xC8, 0x17, 0x08, 0x13, 0x00, 0x03, 0x00, 0x01};
|
||||
|
||||
const unsigned char wind_face[] PROGMEM = {0xFF, 0x00, 0x01, 0x01, 0x01, 0x01, 0xF9, 0x00, 0xF9, 0x01, 0xD9,
|
||||
0x01, 0x99, 0x01, 0xF9, 0x01, 0xF9, 0x33, 0xFD, 0x4B, 0xFD, 0x85,
|
||||
0xFD, 0x9A, 0xFD, 0x75, 0xFD, 0x09, 0xFD, 0x01, 0xFF, 0x00};
|
||||
const unsigned char wind_face[] PROGMEM = {0xFF, 0x00, 0x01, 0x01, 0x01, 0x01, 0xF9, 0x00, 0xF9, 0x01, 0xD9, 0x01, 0x99, 0x01, 0xF9, 0x01,
|
||||
0xF9, 0x33, 0xFD, 0x4B, 0xFD, 0x85, 0xFD, 0x9A, 0xFD, 0x75, 0xFD, 0x09, 0xFD, 0x01, 0xFF, 0x00};
|
||||
|
||||
const unsigned char new_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x02,
|
||||
0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40,
|
||||
0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char new_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x18, 0x04, 0x20, 0x04, 0x20, 0x02, 0x40, 0x02, 0x40, 0x02, 0x40,
|
||||
0x02, 0x40, 0x02, 0x40, 0x02, 0x40, 0x04, 0x20, 0x04, 0x20, 0x18, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char waxing_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3E, 0x04, 0x3C, 0x02,
|
||||
0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78,
|
||||
0x04, 0x3C, 0x04, 0x3E, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char waxing_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3E, 0x04, 0x3C, 0x02, 0x78, 0x02, 0x78, 0x02, 0x78,
|
||||
0x02, 0x78, 0x02, 0x78, 0x02, 0x78, 0x04, 0x3C, 0x04, 0x3E, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char first_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3F, 0x04, 0x3F, 0x02,
|
||||
0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F,
|
||||
0x04, 0x3F, 0x04, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char first_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x04, 0x3F, 0x04, 0x3F, 0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F,
|
||||
0x02, 0x7F, 0x02, 0x7F, 0x02, 0x7F, 0x04, 0x3F, 0x04, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char waxing_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x84, 0x3F, 0xC4, 0x3F, 0xC2,
|
||||
0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F,
|
||||
0xC4, 0x3F, 0x84, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char waxing_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0x18, 0x1F, 0x84, 0x3F, 0xC4, 0x3F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F,
|
||||
0xC2, 0x7F, 0xC2, 0x7F, 0xC2, 0x7F, 0xC4, 0x3F, 0x84, 0x3F, 0x18, 0x1F, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char full_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE,
|
||||
0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F,
|
||||
0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char full_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F,
|
||||
0xFE, 0x7F, 0xFE, 0x7F, 0xFE, 0x7F, 0xFC, 0x3F, 0xFC, 0x3F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char waning_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x21, 0xFC, 0x23, 0xFE,
|
||||
0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43,
|
||||
0xFC, 0x23, 0xFC, 0x21, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char waning_gibbous_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x21, 0xFC, 0x23, 0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43,
|
||||
0xFE, 0x43, 0xFE, 0x43, 0xFE, 0x43, 0xFC, 0x23, 0xFC, 0x21, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char last_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x20, 0xFC, 0x20, 0xFE,
|
||||
0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40,
|
||||
0xFC, 0x20, 0xFC, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char last_quarter_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0xFC, 0x20, 0xFC, 0x20, 0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40,
|
||||
0xFE, 0x40, 0xFE, 0x40, 0xFE, 0x40, 0xFC, 0x20, 0xFC, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char waning_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0x7C, 0x20, 0x3C, 0x20, 0x1E,
|
||||
0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40,
|
||||
0x3C, 0x20, 0x7C, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
const unsigned char waning_crescent_moon[] PROGMEM = {0x00, 0x00, 0xE0, 0x07, 0xF8, 0x18, 0x7C, 0x20, 0x3C, 0x20, 0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40,
|
||||
0x1E, 0x40, 0x1E, 0x40, 0x1E, 0x40, 0x3C, 0x20, 0x7C, 0x20, 0xF8, 0x18, 0xE0, 0x07, 0x00, 0x00};
|
||||
|
||||
const unsigned char first_quarter_moon_face[] PROGMEM = {0x00, 0x0F, 0x00, 0x12, 0x00, 0x24, 0x00, 0x44, 0x00, 0x48, 0x00,
|
||||
0x88, 0x00, 0x84, 0x80, 0x93, 0x80, 0x80, 0x03, 0x81, 0x8D, 0x80,
|
||||
0x71, 0x40, 0x82, 0x41, 0x02, 0x20, 0x0C, 0x18, 0xF0, 0x07};
|
||||
|
||||
const unsigned char peach[] PROGMEM = {0x70, 0x0F, 0x88, 0x10, 0x78, 0x1F, 0x88, 0x11, 0x04, 0x22, 0x02,
|
||||
0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x44, 0x02, 0x42, 0x02, 0x40,
|
||||
0x04, 0x20, 0x04, 0x20, 0x08, 0x10, 0x30, 0x0C, 0xC0, 0x03};
|
||||
const unsigned char peach[] PROGMEM = {0x70, 0x0F, 0x88, 0x10, 0x78, 0x1F, 0x88, 0x11, 0x04, 0x22, 0x02, 0x44, 0x02, 0x44, 0x02, 0x44,
|
||||
0x02, 0x44, 0x02, 0x42, 0x02, 0x40, 0x04, 0x20, 0x04, 0x20, 0x08, 0x10, 0x30, 0x0C, 0xC0, 0x03};
|
||||
|
||||
const unsigned char turkey[] PROGMEM = {0x00, 0x00, 0x38, 0x00, 0x44, 0x38, 0x56, 0x54, 0x45, 0x52, 0xE2,
|
||||
0x21, 0x2C, 0x56, 0x14, 0x58, 0x0A, 0x37, 0x86, 0x68, 0x82, 0x50,
|
||||
0x82, 0x20, 0x04, 0x41, 0xF8, 0x7F, 0x40, 0x02, 0xF0, 0x07};
|
||||
const unsigned char turkey[] PROGMEM = {0x00, 0x00, 0x38, 0x00, 0x44, 0x38, 0x56, 0x54, 0x45, 0x52, 0xE2, 0x21, 0x2C, 0x56, 0x14, 0x58,
|
||||
0x0A, 0x37, 0x86, 0x68, 0x82, 0x50, 0x82, 0x20, 0x04, 0x41, 0xF8, 0x7F, 0x40, 0x02, 0xF0, 0x07};
|
||||
|
||||
const unsigned char turkey_leg[] PROGMEM = {0x0C, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x2F, 0x00, 0x46, 0x00, 0x88,
|
||||
0x01, 0x10, 0x0E, 0x20, 0x30, 0x20, 0x40, 0x40, 0x40, 0x40, 0x80,
|
||||
0x40, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x43, 0x00, 0x3C};
|
||||
const unsigned char turkey_leg[] PROGMEM = {0x0C, 0x00, 0x1E, 0x00, 0x1F, 0x00, 0x2F, 0x00, 0x46, 0x00, 0x88, 0x01, 0x10, 0x0E, 0x20, 0x30,
|
||||
0x20, 0x40, 0x40, 0x40, 0x40, 0x80, 0x40, 0x80, 0x80, 0x80, 0x80, 0x80, 0x00, 0x43, 0x00, 0x3C};
|
||||
|
||||
const unsigned char south_west_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1C, 0x00, 0x3E, 0x00,
|
||||
0x1F, 0x80, 0x0F, 0xC2, 0x07, 0xE6, 0x03, 0xFE, 0x01, 0xFE, 0x00,
|
||||
0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0x00, 0x00};
|
||||
const unsigned char south_west_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1C, 0x00, 0x3E, 0x00, 0x1F, 0x80, 0x0F, 0xC2, 0x07,
|
||||
0xE6, 0x03, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0x00, 0x00};
|
||||
|
||||
const unsigned char south_east_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x38, 0x00, 0x7C, 0x00, 0xF8,
|
||||
0x00, 0xF0, 0x01, 0xE0, 0x43, 0xC0, 0x67, 0x80, 0x7F, 0x00, 0x7F,
|
||||
0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0x00, 0x00};
|
||||
const unsigned char south_east_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x38, 0x00, 0x7C, 0x00, 0xF8, 0x00, 0xF0, 0x01, 0xE0, 0x43,
|
||||
0xC0, 0x67, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0x00, 0x00};
|
||||
|
||||
const unsigned char north_west_arrow[] PROGMEM = {0x00, 0x00, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE,
|
||||
0x00, 0xFE, 0x01, 0xE6, 0x03, 0xC2, 0x07, 0x80, 0x0F, 0x00, 0x1F,
|
||||
0x00, 0x3E, 0x00, 0x1C, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00};
|
||||
const unsigned char north_west_arrow[] PROGMEM = {0x00, 0x00, 0xFE, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0xFE, 0x00, 0xFE, 0x01, 0xE6, 0x03,
|
||||
0xC2, 0x07, 0x80, 0x0F, 0x00, 0x1F, 0x00, 0x3E, 0x00, 0x1C, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
const unsigned char north_east_arrow[] PROGMEM = {0x00, 0x00, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00,
|
||||
0x7F, 0x80, 0x7F, 0xC0, 0x67, 0xE0, 0x43, 0xF0, 0x01, 0xF8, 0x00,
|
||||
0x7C, 0x00, 0x38, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const unsigned char north_east_arrow[] PROGMEM = {0x00, 0x00, 0x80, 0x7F, 0x00, 0x7F, 0x00, 0x7E, 0x00, 0x7E, 0x00, 0x7F, 0x80, 0x7F, 0xC0, 0x67,
|
||||
0xE0, 0x43, 0xF0, 0x01, 0xF8, 0x00, 0x7C, 0x00, 0x38, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
const unsigned char downwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0,
|
||||
0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xFC, 0x3F,
|
||||
0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01};
|
||||
const unsigned char downwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03,
|
||||
0xC0, 0x03, 0xC0, 0x03, 0xFC, 0x3F, 0xF8, 0x1F, 0xF0, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0x80, 0x01};
|
||||
|
||||
const unsigned char leftwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x30, 0x00, 0x38, 0x00, 0x3C,
|
||||
0x00, 0xFE, 0x3F, 0xFF, 0x3F, 0xFF, 0x3F, 0xFE, 0x3F, 0x3C, 0x00,
|
||||
0x38, 0x00, 0x30, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const unsigned char leftwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x30, 0x00, 0x38, 0x00, 0x3C, 0x00, 0xFE, 0x3F, 0xFF, 0x3F,
|
||||
0xFF, 0x3F, 0xFE, 0x3F, 0x3C, 0x00, 0x38, 0x00, 0x30, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
const unsigned char upwards_arrow[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC,
|
||||
0x3F, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03,
|
||||
0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00};
|
||||
const unsigned char upwards_arrow[] PROGMEM = {0x80, 0x01, 0xC0, 0x03, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xFC, 0x3F, 0xC0, 0x03, 0xC0, 0x03,
|
||||
0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
const unsigned char rightwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0C, 0x00, 0x1C, 0x00,
|
||||
0x3C, 0xFC, 0x7F, 0xFC, 0xFF, 0xFC, 0xFF, 0xFC, 0x7F, 0x00, 0x3C,
|
||||
0x00, 0x1C, 0x00, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00};
|
||||
const unsigned char rightwards_arrow[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0C, 0x00, 0x1C, 0x00, 0x3C, 0xFC, 0x7F, 0xFC, 0xFF,
|
||||
0xFC, 0xFF, 0xFC, 0x7F, 0x00, 0x3C, 0x00, 0x1C, 0x00, 0x0C, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00};
|
||||
|
||||
const unsigned char strong[] PROGMEM = {0x38, 0x00, 0x44, 0x00, 0x62, 0x00, 0x42, 0x00, 0x42, 0x00, 0x3A,
|
||||
0x00, 0x11, 0x3C, 0x11, 0x42, 0xD1, 0x81, 0x31, 0x82, 0x11, 0x82,
|
||||
0x21, 0x80, 0x01, 0x80, 0x01, 0x80, 0x02, 0x40, 0xFC, 0x3F};
|
||||
const unsigned char strong[] PROGMEM = {0x38, 0x00, 0x44, 0x00, 0x62, 0x00, 0x42, 0x00, 0x42, 0x00, 0x3A, 0x00, 0x11, 0x3C, 0x11, 0x42,
|
||||
0xD1, 0x81, 0x31, 0x82, 0x11, 0x82, 0x21, 0x80, 0x01, 0x80, 0x01, 0x80, 0x02, 0x40, 0xFC, 0x3F};
|
||||
|
||||
const unsigned char check_mark[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x70, 0x00, 0x3C, 0x00,
|
||||
0x1E, 0x00, 0x0F, 0x80, 0x07, 0xC3, 0x03, 0xEE, 0x03, 0xFC, 0x01,
|
||||
0xF8, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x60, 0x00, 0x20, 0x00};
|
||||
const unsigned char check_mark[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x70, 0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0F, 0x80, 0x07,
|
||||
0xC3, 0x03, 0xEE, 0x03, 0xFC, 0x01, 0xF8, 0x00, 0xF0, 0x00, 0x70, 0x00, 0x60, 0x00, 0x20, 0x00};
|
||||
|
||||
const unsigned char house[] PROGMEM = {0x80, 0x01, 0x5C, 0x02, 0x34, 0x04, 0x14, 0x08, 0x0C, 0x10, 0x04,
|
||||
0x20, 0x02, 0x40, 0xFF, 0xFF, 0x02, 0x40, 0x7A, 0x5F, 0x4A, 0x55,
|
||||
0x4A, 0x5F, 0x6A, 0x55, 0x4A, 0x5F, 0x4A, 0x40, 0xFE, 0x7F};
|
||||
const unsigned char house[] PROGMEM = {0x80, 0x01, 0x5C, 0x02, 0x34, 0x04, 0x14, 0x08, 0x0C, 0x10, 0x04, 0x20, 0x02, 0x40, 0xFF, 0xFF,
|
||||
0x02, 0x40, 0x7A, 0x5F, 0x4A, 0x55, 0x4A, 0x5F, 0x6A, 0x55, 0x4A, 0x5F, 0x4A, 0x40, 0xFE, 0x7F};
|
||||
|
||||
const unsigned char shrug[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x08, 0x50, 0x0A, 0x10, 0x08, 0x90,
|
||||
0x09, 0x27, 0xE4, 0x49, 0x92, 0xAA, 0x55, 0x16, 0x68, 0x12, 0x48,
|
||||
0x02, 0x40, 0x02, 0x40, 0x0C, 0x30, 0x08, 0x10, 0xF8, 0x1F};
|
||||
const unsigned char shrug[] PROGMEM = {0xC0, 0x03, 0x20, 0x04, 0x10, 0x08, 0x50, 0x0A, 0x10, 0x08, 0x90, 0x09, 0x27, 0xE4, 0x49, 0x92,
|
||||
0xAA, 0x55, 0x16, 0x68, 0x12, 0x48, 0x02, 0x40, 0x02, 0x40, 0x0C, 0x30, 0x08, 0x10, 0xF8, 0x1F};
|
||||
|
||||
const unsigned char eyes[] PROGMEM = {0x00, 0x00, 0x3C, 0x3C, 0x42, 0x42, 0x81, 0x81, 0x85, 0x85, 0x8F,
|
||||
0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F,
|
||||
0x85, 0x85, 0x81, 0x81, 0x42, 0x42, 0x3C, 0x3C, 0x00, 0x00};
|
||||
const unsigned char eyes[] PROGMEM = {0x00, 0x00, 0x3C, 0x3C, 0x42, 0x42, 0x81, 0x81, 0x85, 0x85, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F,
|
||||
0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x8F, 0x85, 0x85, 0x81, 0x81, 0x42, 0x42, 0x3C, 0x3C, 0x00, 0x00};
|
||||
|
||||
const unsigned char eye[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xF4,
|
||||
0x2F, 0x7A, 0x5E, 0x39, 0x9C, 0x39, 0x9C, 0x7A, 0x5E, 0xF4, 0x2F,
|
||||
0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
const unsigned char eye[] PROGMEM = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x07, 0xF8, 0x1F, 0xF4, 0x2F, 0x7A, 0x5E, 0x39, 0x9C,
|
||||
0x39, 0x9C, 0x7A, 0x5E, 0xF4, 0x2F, 0xF8, 0x1F, 0xE0, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
#endif
|
||||
|
||||
} // namespace graphics
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
|
||||
namespace graphics
|
||||
{
|
||||
namespace graphics {
|
||||
|
||||
// === Emote List ===
|
||||
struct Emote {
|
||||
const char *label;
|
||||
const unsigned char *bitmap;
|
||||
int width;
|
||||
int height;
|
||||
const char *label;
|
||||
const unsigned char *bitmap;
|
||||
int width;
|
||||
int height;
|
||||
};
|
||||
|
||||
extern const Emote emotes[/* numEmotes */];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1036
-1107
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1071
-1128
File diff suppressed because it is too large
Load Diff
+43
-57
@@ -2,9 +2,8 @@
|
||||
|
||||
#define SATELLITE_IMAGE_WIDTH 16
|
||||
#define SATELLITE_IMAGE_HEIGHT 15
|
||||
const uint8_t SATELLITE_IMAGE[] PROGMEM = {0x00, 0x08, 0x00, 0x1C, 0x00, 0x0E, 0x20, 0x07, 0x70, 0x02,
|
||||
0xF8, 0x00, 0xF0, 0x01, 0xE0, 0x03, 0xC8, 0x01, 0x9C, 0x54,
|
||||
0x0E, 0x52, 0x07, 0x48, 0x02, 0x26, 0x00, 0x10, 0x00, 0x0E};
|
||||
const uint8_t SATELLITE_IMAGE[] PROGMEM = {0x00, 0x08, 0x00, 0x1C, 0x00, 0x0E, 0x20, 0x07, 0x70, 0x02, 0xF8, 0x00, 0xF0, 0x01, 0xE0,
|
||||
0x03, 0xC8, 0x01, 0x9C, 0x54, 0x0E, 0x52, 0x07, 0x48, 0x02, 0x26, 0x00, 0x10, 0x00, 0x0E};
|
||||
|
||||
#define imgSatellite_width 8
|
||||
#define imgSatellite_height 8
|
||||
@@ -16,8 +15,7 @@ const uint8_t imgUSB[] PROGMEM = {0x00, 0xfc, 0xf0, 0xfc, 0x88, 0xff, 0x86, 0xfe
|
||||
const uint8_t imgUSB_HighResolution[] PROGMEM = {0x00, 0x3e, 0xf8, 0x80, 0x43, 0xf8, 0xc0, 0xc2, 0xff, 0x60, 0x42, 0xfc,
|
||||
0x3c, 0xc2, 0xff, 0x22, 0x42, 0xf8, 0x3d, 0x42, 0xf8, 0x22, 0xc2, 0xff,
|
||||
0x61, 0x42, 0xfc, 0xc0, 0xc2, 0xff, 0x80, 0x43, 0xf8, 0x00, 0x3e, 0xf8};
|
||||
const uint8_t imgPower[] PROGMEM = {0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08,
|
||||
0x1C, 0x22, 0x22, 0x41, 0x7F, 0x22, 0x22, 0x22};
|
||||
const uint8_t imgPower[] PROGMEM = {0x40, 0x40, 0x40, 0x58, 0x48, 0x08, 0x08, 0x08, 0x1C, 0x22, 0x22, 0x41, 0x7F, 0x22, 0x22, 0x22};
|
||||
const uint8_t imgUser[] PROGMEM = {0x3C, 0x42, 0x99, 0xA5, 0xA5, 0x99, 0x42, 0x3C};
|
||||
const uint8_t imgPositionEmpty[] PROGMEM = {0x20, 0x30, 0x28, 0x24, 0x42, 0xFF};
|
||||
const uint8_t imgPositionSolid[] PROGMEM = {0x20, 0x30, 0x38, 0x3C, 0x7E, 0xFF};
|
||||
@@ -26,18 +24,16 @@ const uint8_t bluetoothConnectedIcon[36] PROGMEM = {0xfe, 0x01, 0xff, 0x03, 0x03
|
||||
0xf3, 0x3f, 0x33, 0x30, 0x33, 0x33, 0x33, 0x33, 0x03, 0x33, 0xff, 0x33,
|
||||
0xfe, 0x31, 0x00, 0x30, 0x30, 0x30, 0x30, 0x30, 0xf0, 0x3f, 0xe0, 0x1f};
|
||||
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || \
|
||||
defined(ST7789_CS) || defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || \
|
||||
defined(USE_ST7796) || defined(HACKADAY_COMMUNICATOR) || ARCH_PORTDUINO) && \
|
||||
#if (defined(USE_EINK) || defined(ILI9341_DRIVER) || defined(ILI9342_DRIVER) || defined(ST7701_CS) || defined(ST7735_CS) || defined(ST7789_CS) || \
|
||||
defined(USE_ST7789) || defined(HX8357_CS) || defined(ILI9488_CS) || defined(ST7796_CS) || defined(USE_ST7796) || \
|
||||
defined(HACKADAY_COMMUNICATOR) || ARCH_PORTDUINO) && \
|
||||
!defined(DISPLAY_FORCE_SMALL_FONTS)
|
||||
const uint8_t imgQuestionL1[] PROGMEM = {0xff, 0x01, 0x01, 0x32, 0x7b, 0x49, 0x49, 0x6f, 0x26, 0x01, 0x01, 0xff};
|
||||
const uint8_t imgQuestionL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f};
|
||||
const uint8_t imgInfoL1[] PROGMEM = {0xff, 0x01, 0x01, 0x01, 0x1e, 0x7f, 0x1e, 0x01, 0x01, 0x01, 0x01, 0xff};
|
||||
const uint8_t imgInfoL2[] PROGMEM = {0x0f, 0x08, 0x08, 0x08, 0x06, 0x0f, 0x0f, 0x06, 0x08, 0x08, 0x08, 0x0f};
|
||||
const uint8_t imgSFL1[] PROGMEM = {0xb6, 0x8f, 0x19, 0x11, 0x31, 0xe3, 0xc2, 0x01,
|
||||
0x01, 0xf9, 0xf9, 0x89, 0x89, 0x89, 0x09, 0xeb};
|
||||
const uint8_t imgSFL2[] PROGMEM = {0x0e, 0x09, 0x09, 0x09, 0x09, 0x09, 0x08, 0x08,
|
||||
0x00, 0x0f, 0x0f, 0x00, 0x08, 0x08, 0x08, 0x0f};
|
||||
const uint8_t imgSFL1[] PROGMEM = {0xb6, 0x8f, 0x19, 0x11, 0x31, 0xe3, 0xc2, 0x01, 0x01, 0xf9, 0xf9, 0x89, 0x89, 0x89, 0x09, 0xeb};
|
||||
const uint8_t imgSFL2[] PROGMEM = {0x0e, 0x09, 0x09, 0x09, 0x09, 0x09, 0x08, 0x08, 0x00, 0x0f, 0x0f, 0x00, 0x08, 0x08, 0x08, 0x0f};
|
||||
#else
|
||||
const uint8_t imgInfo[] PROGMEM = {0xff, 0x81, 0x00, 0xfb, 0xfb, 0x00, 0x81, 0xff};
|
||||
const uint8_t imgQuestion[] PROGMEM = {0xbf, 0x41, 0xc0, 0x8b, 0xdb, 0x70, 0xa1, 0xdf};
|
||||
@@ -46,21 +42,21 @@ const uint8_t imgSF[] PROGMEM = {0xd2, 0xb7, 0xad, 0xbb, 0x92, 0x01, 0xfd, 0xfd,
|
||||
|
||||
// === Horizontal battery ===
|
||||
// Basic battery design and all related pieces
|
||||
const unsigned char batteryBitmap_h_bottom[] PROGMEM = {
|
||||
0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001,
|
||||
0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
||||
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00011110, 0b00000000};
|
||||
const unsigned char batteryBitmap_h_bottom[] PROGMEM = {0b00011110, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001,
|
||||
0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000,
|
||||
0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001, 0b00000000, 0b00000001,
|
||||
0b00000000, 0b00000001, 0b00000000, 0b00011110, 0b00000000};
|
||||
|
||||
const unsigned char batteryBitmap_h_top[] PROGMEM = {
|
||||
0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000,
|
||||
0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000, 0b01000000, 0b00000000,
|
||||
0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b00111100, 0b00000000};
|
||||
const unsigned char batteryBitmap_h_top[] PROGMEM = {0b00111100, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000,
|
||||
0b00000000, 0b01000000, 0b00000000, 0b11000000, 0b00000000, 0b11000000, 0b00000000,
|
||||
0b11000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000, 0b00000000, 0b01000000,
|
||||
0b00000000, 0b01000000, 0b00000000, 0b00111100, 0b00000000};
|
||||
|
||||
// Lightning Bolt
|
||||
const unsigned char lightning_bolt_h[] PROGMEM = {
|
||||
0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000, 0b00111000, 0b00000000, 0b00111100,
|
||||
0b00000000, 0b00011110, 0b00000000, 0b11111111, 0b00000000, 0b01111000, 0b00000000, 0b00111100, 0b00000000,
|
||||
0b00011100, 0b00000000, 0b00001100, 0b00000000, 0b00000100, 0b00000000, 0b00000000, 0b00000000};
|
||||
const unsigned char lightning_bolt_h[] PROGMEM = {0b00000000, 0b00000000, 0b00100000, 0b00000000, 0b00110000, 0b00000000, 0b00111000,
|
||||
0b00000000, 0b00111100, 0b00000000, 0b00011110, 0b00000000, 0b11111111, 0b00000000,
|
||||
0b01111000, 0b00000000, 0b00111100, 0b00000000, 0b00011100, 0b00000000, 0b00001100,
|
||||
0b00000000, 0b00000100, 0b00000000, 0b00000000, 0b00000000};
|
||||
|
||||
// === Vertical battery ===
|
||||
// Basic battery design and all related pieces
|
||||
@@ -131,8 +127,7 @@ const uint8_t icon_system[] PROGMEM = {
|
||||
};
|
||||
|
||||
// 🌐 Wi-Fi
|
||||
const uint8_t icon_wifi[] PROGMEM = {0b00000000, 0b00011000, 0b00111100, 0b01111110,
|
||||
0b11011011, 0b00011000, 0b00011000, 0b00000000};
|
||||
const uint8_t icon_wifi[] PROGMEM = {0b00000000, 0b00011000, 0b00111100, 0b01111110, 0b11011011, 0b00011000, 0b00011000, 0b00000000};
|
||||
|
||||
const uint8_t icon_nodes[] PROGMEM = {
|
||||
0xF9, // Row 0 #..#######
|
||||
@@ -232,27 +227,23 @@ const uint8_t mute_symbol[] PROGMEM = {
|
||||
|
||||
#define mute_symbol_big_width 16
|
||||
#define mute_symbol_big_height 16
|
||||
const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0b00000011, 0b00110100, 0b00001100, 0b00011000,
|
||||
0b00001000, 0b00011000, 0b00010000, 0b00101000, 0b00010000, 0b01001000, 0b00010000,
|
||||
0b10001000, 0b00010000, 0b00001000, 0b00010001, 0b00001000, 0b00010010, 0b00001000,
|
||||
0b00010100, 0b00000100, 0b00101000, 0b11111100, 0b00111111, 0b01000000, 0b00100010,
|
||||
0b10000000, 0b01000001, 0b00000000, 0b10000000};
|
||||
const uint8_t mute_symbol_big[] PROGMEM = {0b00000001, 0b00000000, 0b11000010, 0b00000011, 0b00110100, 0b00001100, 0b00011000, 0b00001000,
|
||||
0b00011000, 0b00010000, 0b00101000, 0b00010000, 0b01001000, 0b00010000, 0b10001000, 0b00010000,
|
||||
0b00001000, 0b00010001, 0b00001000, 0b00010010, 0b00001000, 0b00010100, 0b00000100, 0b00101000,
|
||||
0b11111100, 0b00111111, 0b01000000, 0b00100010, 0b10000000, 0b01000001, 0b00000000, 0b10000000};
|
||||
|
||||
// Bell icon for Alert Message
|
||||
#define bell_alert_width 8
|
||||
#define bell_alert_height 8
|
||||
const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010,
|
||||
0b01000010, 0b01000010, 0b11111111, 0b00011000};
|
||||
const unsigned char bell_alert[] PROGMEM = {0b00011000, 0b00100100, 0b00100100, 0b01000010, 0b01000010, 0b01000010, 0b11111111, 0b00011000};
|
||||
|
||||
#define key_symbol_width 8
|
||||
#define key_symbol_height 8
|
||||
const uint8_t key_symbol[] PROGMEM = {0b00000000, 0b00000000, 0b00000110, 0b11111001,
|
||||
0b10101001, 0b10000110, 0b00000000, 0b00000000};
|
||||
const uint8_t key_symbol[] PROGMEM = {0b00000000, 0b00000000, 0b00000110, 0b11111001, 0b10101001, 0b10000110, 0b00000000, 0b00000000};
|
||||
|
||||
#define placeholder_width 8
|
||||
#define placeholder_height 8
|
||||
const uint8_t placeholder[] PROGMEM = {0b11111111, 0b11111111, 0b11111111, 0b11111111,
|
||||
0b11111111, 0b11111111, 0b11111111, 0b11111111};
|
||||
const uint8_t placeholder[] PROGMEM = {0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111, 0b11111111};
|
||||
|
||||
#define icon_node_width 8
|
||||
#define icon_node_height 8
|
||||
@@ -269,40 +260,35 @@ static const uint8_t icon_node[] PROGMEM = {
|
||||
|
||||
#define bluetoothdisabled_width 8
|
||||
#define bluetoothdisabled_height 8
|
||||
const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100, 0b01010100,
|
||||
0b01001100, 0b00000000, 0b00000000, 0b00000000};
|
||||
const uint8_t bluetoothdisabled[] PROGMEM = {0b11101100, 0b01010100, 0b01001100, 0b01010100, 0b01001100, 0b00000000, 0b00000000, 0b00000000};
|
||||
|
||||
#define smallbulletpoint_width 8
|
||||
#define smallbulletpoint_height 8
|
||||
const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000, 0b00000000, 0b00000000};
|
||||
const uint8_t smallbulletpoint[] PROGMEM = {0b00000011, 0b00000011, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000};
|
||||
|
||||
// Digital Clock
|
||||
#define digital_icon_clock_width 8
|
||||
#define digital_icon_clock_height 8
|
||||
const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001,
|
||||
0b10010001, 0b10000001, 0b01000010, 0b00111100};
|
||||
const uint8_t digital_icon_clock[] PROGMEM = {0b00111100, 0b01000010, 0b10000101, 0b10101001, 0b10010001, 0b10000001, 0b01000010, 0b00111100};
|
||||
// Analog Clock
|
||||
#define analog_icon_clock_width 8
|
||||
#define analog_icon_clock_height 8
|
||||
const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000,
|
||||
0b00100100, 0b01000010, 0b01000010, 0b11111111};
|
||||
const uint8_t analog_icon_clock[] PROGMEM = {0b11111111, 0b01000010, 0b00100100, 0b00011000, 0b00100100, 0b01000010, 0b01000010, 0b11111111};
|
||||
|
||||
#define chirpy_width 38
|
||||
#define chirpy_height 50
|
||||
const uint8_t chirpy[] = {
|
||||
0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01,
|
||||
0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00,
|
||||
0x00, 0x00, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f,
|
||||
0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc,
|
||||
0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0,
|
||||
0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1,
|
||||
0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff,
|
||||
0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3,
|
||||
0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0xc0, 0x30, 0x03,
|
||||
0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0,
|
||||
0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01,
|
||||
0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf};
|
||||
0xfe, 0xff, 0xff, 0xff, 0xdf, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, 0x00,
|
||||
0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0xc0, 0xe7, 0x01, 0x00, 0x00, 0x80, 0xe3, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x81,
|
||||
0xff, 0xff, 0x7f, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0x87, 0x3f, 0xfc,
|
||||
0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87,
|
||||
0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0,
|
||||
0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0x87, 0x3f, 0xfc, 0xe0, 0xc1, 0xcf, 0x7f, 0xfe, 0xe0, 0xc1, 0xff, 0xff,
|
||||
0xff, 0xe0, 0xc1, 0xff, 0xff, 0xff, 0xe0, 0x81, 0xff, 0xff, 0x7f, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01,
|
||||
0x00, 0xc3, 0x00, 0xe0, 0x01, 0x00, 0xc3, 0x00, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0x80, 0xe1, 0x01, 0xe0, 0x01, 0xc0, 0x30, 0x03,
|
||||
0xe0, 0x01, 0xc0, 0x30, 0x03, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x60, 0x18, 0x06, 0xe0, 0x01, 0x30, 0x0c, 0x0c, 0xe0, 0x01, 0x30,
|
||||
0x0c, 0x0c, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x18, 0x06, 0x18, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0, 0x01, 0x0c, 0x03, 0x30, 0xe0,
|
||||
0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0x01, 0x00, 0x00, 0x00, 0xe0, 0xfe, 0xff, 0xff, 0xff, 0xdf};
|
||||
|
||||
#define chirpy_small_image_width 8
|
||||
#define chirpy_small_image_height 8
|
||||
|
||||
@@ -10,99 +10,86 @@ using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Private constructor
|
||||
// Called by getInstance
|
||||
LatchingBacklight::LatchingBacklight()
|
||||
{
|
||||
// Attach the deep sleep callback
|
||||
deepSleepObserver.observe(¬ifyDeepSleep);
|
||||
LatchingBacklight::LatchingBacklight() {
|
||||
// Attach the deep sleep callback
|
||||
deepSleepObserver.observe(¬ifyDeepSleep);
|
||||
}
|
||||
|
||||
// Get access to (or create) the singleton instance of this class
|
||||
LatchingBacklight *LatchingBacklight::getInstance()
|
||||
{
|
||||
// Instantiate the class the first time this method is called
|
||||
static LatchingBacklight *const singletonInstance = new LatchingBacklight;
|
||||
LatchingBacklight *LatchingBacklight::getInstance() {
|
||||
// Instantiate the class the first time this method is called
|
||||
static LatchingBacklight *const singletonInstance = new LatchingBacklight;
|
||||
|
||||
return singletonInstance;
|
||||
return singletonInstance;
|
||||
}
|
||||
|
||||
// Which pin controls the backlight?
|
||||
// Is the light active HIGH (default) or active LOW?
|
||||
void LatchingBacklight::setPin(uint8_t pin, bool activeWhen)
|
||||
{
|
||||
this->pin = pin;
|
||||
this->logicActive = activeWhen;
|
||||
void LatchingBacklight::setPin(uint8_t pin, bool activeWhen) {
|
||||
this->pin = pin;
|
||||
this->logicActive = activeWhen;
|
||||
|
||||
pinMode(pin, OUTPUT);
|
||||
off(); // Explicit off seem required by T-Echo?
|
||||
pinMode(pin, OUTPUT);
|
||||
off(); // Explicit off seem required by T-Echo?
|
||||
}
|
||||
|
||||
// Called when device is shutting down
|
||||
// Ensures the backlight is off
|
||||
int LatchingBacklight::beforeDeepSleep(void *unused)
|
||||
{
|
||||
// Contingency only
|
||||
// - pin wasn't set
|
||||
if (pin != (uint8_t)-1) {
|
||||
off();
|
||||
pinMode(pin, INPUT); // High impedance - unnecessary?
|
||||
} else
|
||||
LOG_WARN("LatchingBacklight instantiated, but pin not set");
|
||||
return 0; // Continue with deep sleep
|
||||
int LatchingBacklight::beforeDeepSleep(void *unused) {
|
||||
// Contingency only
|
||||
// - pin wasn't set
|
||||
if (pin != (uint8_t)-1) {
|
||||
off();
|
||||
pinMode(pin, INPUT); // High impedance - unnecessary?
|
||||
} else
|
||||
LOG_WARN("LatchingBacklight instantiated, but pin not set");
|
||||
return 0; // Continue with deep sleep
|
||||
}
|
||||
|
||||
// Turn the backlight on *temporarily*
|
||||
// This should be used for momentary illumination, such as while a button is held
|
||||
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
|
||||
void LatchingBacklight::peek()
|
||||
{
|
||||
assert(pin != (uint8_t)-1);
|
||||
digitalWrite(pin, logicActive); // On
|
||||
on = true;
|
||||
latched = false;
|
||||
void LatchingBacklight::peek() {
|
||||
assert(pin != (uint8_t)-1);
|
||||
digitalWrite(pin, logicActive); // On
|
||||
on = true;
|
||||
latched = false;
|
||||
}
|
||||
|
||||
// Turn the backlight on, and keep it on
|
||||
// This should be used when the backlight should remain active, even after user input ends
|
||||
// e.g. when enabled via the menu
|
||||
// The effect on the backlight is the same; peek and latch are separated to simplify short vs long press button handling
|
||||
void LatchingBacklight::latch()
|
||||
{
|
||||
assert(pin != (uint8_t)-1);
|
||||
|
||||
// Blink if moving from peek to latch
|
||||
// Indicates to user that the transition has taken place
|
||||
if (on && !latched) {
|
||||
digitalWrite(pin, !logicActive); // Off
|
||||
delay(25);
|
||||
digitalWrite(pin, logicActive); // On
|
||||
delay(25);
|
||||
digitalWrite(pin, !logicActive); // Off
|
||||
delay(25);
|
||||
}
|
||||
void LatchingBacklight::latch() {
|
||||
assert(pin != (uint8_t)-1);
|
||||
|
||||
// Blink if moving from peek to latch
|
||||
// Indicates to user that the transition has taken place
|
||||
if (on && !latched) {
|
||||
digitalWrite(pin, !logicActive); // Off
|
||||
delay(25);
|
||||
digitalWrite(pin, logicActive); // On
|
||||
on = true;
|
||||
latched = true;
|
||||
delay(25);
|
||||
digitalWrite(pin, !logicActive); // Off
|
||||
delay(25);
|
||||
}
|
||||
|
||||
digitalWrite(pin, logicActive); // On
|
||||
on = true;
|
||||
latched = true;
|
||||
}
|
||||
|
||||
// Turn the backlight off
|
||||
// Suitable for ending both peek and latch
|
||||
void LatchingBacklight::off()
|
||||
{
|
||||
assert(pin != (uint8_t)-1);
|
||||
digitalWrite(pin, !logicActive); // Off
|
||||
on = false;
|
||||
latched = false;
|
||||
void LatchingBacklight::off() {
|
||||
assert(pin != (uint8_t)-1);
|
||||
digitalWrite(pin, !logicActive); // Off
|
||||
on = false;
|
||||
latched = false;
|
||||
}
|
||||
|
||||
bool LatchingBacklight::isOn()
|
||||
{
|
||||
return on;
|
||||
}
|
||||
bool LatchingBacklight::isOn() { return on; }
|
||||
|
||||
bool LatchingBacklight::isLatched()
|
||||
{
|
||||
return latched;
|
||||
}
|
||||
bool LatchingBacklight::isLatched() { return latched; }
|
||||
|
||||
#endif
|
||||
|
||||
@@ -15,36 +15,34 @@
|
||||
|
||||
#include "Observer.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
namespace NicheGraphics::Drivers {
|
||||
|
||||
class LatchingBacklight
|
||||
{
|
||||
public:
|
||||
static LatchingBacklight *getInstance(); // Create or get the singleton instance
|
||||
void setPin(uint8_t pin, bool activeWhen = HIGH);
|
||||
class LatchingBacklight {
|
||||
public:
|
||||
static LatchingBacklight *getInstance(); // Create or get the singleton instance
|
||||
void setPin(uint8_t pin, bool activeWhen = HIGH);
|
||||
|
||||
int beforeDeepSleep(void *unused); // Callback for auto-shutoff
|
||||
int beforeDeepSleep(void *unused); // Callback for auto-shutoff
|
||||
|
||||
void peek(); // Backlight on temporarily, e.g. while button held
|
||||
void latch(); // Backlight on permanently, e.g. toggled via menu
|
||||
void off(); // Backlight off. Suitable for both peek and latch
|
||||
void peek(); // Backlight on temporarily, e.g. while button held
|
||||
void latch(); // Backlight on permanently, e.g. toggled via menu
|
||||
void off(); // Backlight off. Suitable for both peek and latch
|
||||
|
||||
bool isOn(); // Either peek or latch
|
||||
bool isLatched();
|
||||
bool isOn(); // Either peek or latch
|
||||
bool isLatched();
|
||||
|
||||
private:
|
||||
LatchingBacklight(); // Constructor made private: force use of getInstance
|
||||
private:
|
||||
LatchingBacklight(); // Constructor made private: force use of getInstance
|
||||
|
||||
// Get notified when the system is shutting down
|
||||
CallbackObserver<LatchingBacklight, void *> deepSleepObserver =
|
||||
CallbackObserver<LatchingBacklight, void *>(this, &LatchingBacklight::beforeDeepSleep);
|
||||
// Get notified when the system is shutting down
|
||||
CallbackObserver<LatchingBacklight, void *> deepSleepObserver =
|
||||
CallbackObserver<LatchingBacklight, void *>(this, &LatchingBacklight::beforeDeepSleep);
|
||||
|
||||
uint8_t pin = (uint8_t)-1;
|
||||
bool logicActive = HIGH; // Is light active HIGH or active LOW
|
||||
uint8_t pin = (uint8_t)-1;
|
||||
bool logicActive = HIGH; // Is light active HIGH or active LOW
|
||||
|
||||
bool on = false; // Is light on (either peek or latched)
|
||||
bool latched = false; // Is light latched on
|
||||
bool on = false; // Is light on (either peek or latched)
|
||||
bool latched = false; // Is light latched on
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
@@ -30,103 +30,99 @@ static const uint8_t LUT_FAST[] = {
|
||||
};
|
||||
|
||||
// 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
|
||||
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
|
||||
// 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;
|
||||
// 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;
|
||||
}
|
||||
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
|
||||
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;
|
||||
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;
|
||||
}
|
||||
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;
|
||||
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;
|
||||
}
|
||||
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
|
||||
}
|
||||
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();
|
||||
}
|
||||
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();
|
||||
// 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
|
||||
@@ -19,25 +19,23 @@ E-Ink display driver
|
||||
|
||||
#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);
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -31,95 +31,91 @@ static const uint8_t LUT_FAST[] = {
|
||||
};
|
||||
|
||||
// How strongly the pixels are pulled and pushed
|
||||
void DEPG0290BNS800::configVoltages()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
// Listed as "typical" in datasheet
|
||||
sendCommand(0x04);
|
||||
sendData(0x41); // VSH1 15V
|
||||
sendData(0x00); // VSH2 NA
|
||||
sendData(0x32); // VSL -15V
|
||||
break;
|
||||
void DEPG0290BNS800::configVoltages() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
// Listed as "typical" in datasheet
|
||||
sendCommand(0x04);
|
||||
sendData(0x41); // VSH1 15V
|
||||
sendData(0x00); // VSH2 NA
|
||||
sendData(0x32); // VSL -15V
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
// From OTP memory
|
||||
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 DEPG0290BNS800::configWaveform()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x60); // Actively hold screen border during update
|
||||
void DEPG0290BNS800::configWaveform() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x60); // Actively hold screen border during update
|
||||
|
||||
sendCommand(0x32); // Write LUT register from MCU:
|
||||
sendData(LUT_FAST, sizeof(LUT_FAST)); // (describes operation for a FAST refresh)
|
||||
break;
|
||||
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;
|
||||
}
|
||||
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 DEPG0290BNS800::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xCF); // Differential, use manually loaded waveform
|
||||
break;
|
||||
void DEPG0290BNS800::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;
|
||||
}
|
||||
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 DEPG0290BNS800::detachFromUpdate()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 450); // At least 450ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 3000); // At least 3 seconds for full refresh
|
||||
}
|
||||
void DEPG0290BNS800::detachFromUpdate() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 450); // At least 450ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 3000); // At least 3 seconds for full refresh
|
||||
}
|
||||
}
|
||||
|
||||
// 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 DEPG0290BNS800::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();
|
||||
}
|
||||
void DEPG0290BNS800::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();
|
||||
// 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
|
||||
@@ -17,25 +17,23 @@ E-Ink display driver
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class DEPG0290BNS800 : public SSD16XX
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 128;
|
||||
static constexpr uint32_t height = 296;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
namespace NicheGraphics::Drivers {
|
||||
class DEPG0290BNS800 : public SSD16XX {
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 128;
|
||||
static constexpr uint32_t height = 296;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
|
||||
public:
|
||||
DEPG0290BNS800() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte
|
||||
public:
|
||||
DEPG0290BNS800() : 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
|
||||
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
|
||||
|
||||
@@ -5,80 +5,77 @@
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Map the display controller IC's output to the connected panel
|
||||
void E0213A367::configScanning()
|
||||
{
|
||||
// "Driver output control"
|
||||
// Scan gates from 0 to 249 (vertical resolution 250px)
|
||||
sendCommand(0x01);
|
||||
sendData(0xF9);
|
||||
sendData(0x00);
|
||||
void E0213A367::configScanning() {
|
||||
// "Driver output control"
|
||||
// Scan gates from 0 to 249 (vertical resolution 250px)
|
||||
sendCommand(0x01);
|
||||
sendData(0xF9);
|
||||
sendData(0x00);
|
||||
}
|
||||
|
||||
// Specify which information is used to control the sequence of voltages applied to move the pixels
|
||||
void E0213A367::configWaveform()
|
||||
{
|
||||
// This command (0x37) is poorly documented
|
||||
// As of July 2025, the datasheet for this display's controller IC is unavailable
|
||||
// The values are supplied by Heltec, who presumably have privileged access to information from the display manufacturer
|
||||
// Datasheet for the similar SSD1680 IC hints at the function of this command:
|
||||
void E0213A367::configWaveform() {
|
||||
// This command (0x37) is poorly documented
|
||||
// As of July 2025, the datasheet for this display's controller IC is unavailable
|
||||
// The values are supplied by Heltec, who presumably have privileged access to information from the display
|
||||
// manufacturer Datasheet for the similar SSD1680 IC hints at the function of this command:
|
||||
|
||||
// "Spare VCOM OTP selection":
|
||||
// Unclear why 0x40 is set. Sane values for related SSD1680 seem to be 0x80 or 0x00.
|
||||
// Maybe value is redundant? No noticeable impact when set to 0x00.
|
||||
// We'll leave it set to 0x40, following Heltec's lead, just in case.
|
||||
// "Spare VCOM OTP selection":
|
||||
// Unclear why 0x40 is set. Sane values for related SSD1680 seem to be 0x80 or 0x00.
|
||||
// Maybe value is redundant? No noticeable impact when set to 0x00.
|
||||
// We'll leave it set to 0x40, following Heltec's lead, just in case.
|
||||
|
||||
// "Display Mode"
|
||||
// Seems to specify whether a waveform stored in OTP should use display mode 1 or 2 (full refresh or differential refresh)
|
||||
// "Display Mode"
|
||||
// Seems to specify whether a waveform stored in OTP should use display mode 1 or 2 (full refresh or differential
|
||||
// refresh)
|
||||
|
||||
// Unusual that waveforms are programmed to OTP, but this meta information is not ..?
|
||||
// Unusual that waveforms are programmed to OTP, but this meta information is not ..?
|
||||
|
||||
sendCommand(0x37); // "Write Register for Display Option" ?
|
||||
sendData(0x40); // "Spare VCOM OTP selection" ?
|
||||
sendData(0x80); // "Display Mode for WS[7:0]" ?
|
||||
sendData(0x03); // "Display Mode for WS[15:8]" ?
|
||||
sendData(0x0E); // "Display Mode [23:16]" ?
|
||||
sendCommand(0x37); // "Write Register for Display Option" ?
|
||||
sendData(0x40); // "Spare VCOM OTP selection" ?
|
||||
sendData(0x80); // "Display Mode for WS[7:0]" ?
|
||||
sendData(0x03); // "Display Mode for WS[15:8]" ?
|
||||
sendData(0x0E); // "Display Mode [23:16]" ?
|
||||
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x81); // As specified by Heltec. Actually VCOM (0x80)?. Bit 0 seems redundant here.
|
||||
break;
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x01); // Follow LUT 1 (blink same as white pixels)
|
||||
break;
|
||||
}
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x81); // As specified by Heltec. Actually VCOM (0x80)?. Bit 0 seems redundant here.
|
||||
break;
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x01); // Follow LUT 1 (blink same as white pixels)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Tell controller IC which operations to run
|
||||
void E0213A367::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory, Display mode 1 "full refresh"
|
||||
break;
|
||||
}
|
||||
void E0213A367::configUpdateSequence() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory, Display mode 1 "full refresh"
|
||||
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 E0213A367::detachFromUpdate()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 500); // At least 500ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 1500); // At least 1.5 seconds for full refresh
|
||||
}
|
||||
void E0213A367::detachFromUpdate() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 500); // At least 500ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 1500); // At least 1.5 seconds for full refresh
|
||||
}
|
||||
}
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -17,24 +17,22 @@ E-Ink display driver
|
||||
|
||||
#include "./SSD1682.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class E0213A367 : public SSD1682
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 122;
|
||||
static constexpr uint32_t height = 250;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
namespace NicheGraphics::Drivers {
|
||||
class E0213A367 : public SSD1682 {
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 122;
|
||||
static constexpr uint32_t height = 250;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
|
||||
public:
|
||||
E0213A367() : SSD1682(width, height, supported, 0) {}
|
||||
public:
|
||||
E0213A367() : SSD1682(width, height, supported, 0) {}
|
||||
|
||||
protected:
|
||||
void configScanning() override;
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
protected:
|
||||
void configScanning() override;
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
@@ -6,81 +6,76 @@ 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("EInkDriver"), width(width), height(height), supportedUpdateTypes(supported)
|
||||
{
|
||||
OSThread::disable();
|
||||
: concurrency::OSThread("EInkDriver"), width(width), height(height), supportedUpdateTypes(supported) {
|
||||
OSThread::disable();
|
||||
}
|
||||
|
||||
// Used by NicheGraphics implementations to check if a display supports a specific refresh operation.
|
||||
// Whether or not the update type is supported is specified in the constructor
|
||||
bool EInk::supports(UpdateTypes type)
|
||||
{
|
||||
// The EInkUpdateTypes enum assigns each type a unique bit. We are checking if that bit is set.
|
||||
if (supportedUpdateTypes & type)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
bool EInk::supports(UpdateTypes type) {
|
||||
// The EInkUpdateTypes enum assigns each type a unique bit. We are checking if that bit is set.
|
||||
if (supportedUpdateTypes & type)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
||||
// Begins using the OSThread to detect when a display update is complete
|
||||
// This allows the refresh operation to run "asynchronously".
|
||||
// Rather than blocking execution waiting for the update to complete, we are periodically checking the hardware's BUSY pin
|
||||
// The expectedDuration argument allows us to delay the start of this checking, if we know "roughly" how long an update takes.
|
||||
// Potentially, a display without hardware BUSY could rely entirely on "expectedDuration",
|
||||
// provided its isUpdateDone() override always returns true.
|
||||
void EInk::beginPolling(uint32_t interval, uint32_t expectedDuration)
|
||||
{
|
||||
updateRunning = true;
|
||||
pollingInterval = interval;
|
||||
pollingBegunAt = millis();
|
||||
// Rather than blocking execution waiting for the update to complete, we are periodically checking the hardware's BUSY
|
||||
// pin The expectedDuration argument allows us to delay the start of this checking, if we know "roughly" how long an
|
||||
// update takes. Potentially, a display without hardware BUSY could rely entirely on "expectedDuration", provided its
|
||||
// isUpdateDone() override always returns true.
|
||||
void EInk::beginPolling(uint32_t interval, uint32_t expectedDuration) {
|
||||
updateRunning = true;
|
||||
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
|
||||
OSThread::setIntervalFromNow(expectedDuration);
|
||||
OSThread::enabled = true;
|
||||
// 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
|
||||
OSThread::setIntervalFromNow(expectedDuration);
|
||||
OSThread::enabled = true;
|
||||
}
|
||||
|
||||
// Meshtastic's pseudo-threading layer
|
||||
// We're using this as a timer, to periodically check if an update is complete
|
||||
// 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;
|
||||
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();
|
||||
}
|
||||
// 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 not yet done
|
||||
if (!isUpdateDone())
|
||||
return pollingInterval; // Poll again in a few ms
|
||||
|
||||
// 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
|
||||
// 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
|
||||
}
|
||||
|
||||
// Wait for an in progress update to complete before continuing
|
||||
// Run a normal (async) update first, *then* call await
|
||||
void EInk::await()
|
||||
{
|
||||
// Stop our concurrency thread
|
||||
OSThread::disable();
|
||||
void EInk::await() {
|
||||
// Stop our concurrency thread
|
||||
OSThread::disable();
|
||||
|
||||
// Sit and block until the update is complete
|
||||
while (updateRunning) {
|
||||
runOnce();
|
||||
yield();
|
||||
}
|
||||
// Sit and block until the update is complete
|
||||
while (updateRunning) {
|
||||
runOnce();
|
||||
yield();
|
||||
}
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -12,44 +12,42 @@
|
||||
#include "concurrency/OSThread.h"
|
||||
#include <SPI.h>
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
namespace NicheGraphics::Drivers {
|
||||
|
||||
class EInk : private concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
// Different possible operations used to update an E-Ink display
|
||||
// Some displays will not support all operations
|
||||
// Each value needs a unique bit. In some cases, we might set more than one bit (e.g. EInk::supportedUpdateType)
|
||||
enum UpdateTypes : uint8_t {
|
||||
UNSPECIFIED = 0,
|
||||
FULL = 1 << 0,
|
||||
FAST = 1 << 1, // "Partial Refresh"
|
||||
};
|
||||
class EInk : private concurrency::OSThread {
|
||||
public:
|
||||
// Different possible operations used to update an E-Ink display
|
||||
// Some displays will not support all operations
|
||||
// Each value needs a unique bit. In some cases, we might set more than one bit (e.g. EInk::supportedUpdateType)
|
||||
enum UpdateTypes : uint8_t {
|
||||
UNSPECIFIED = 0,
|
||||
FULL = 1 << 0,
|
||||
FAST = 1 << 1, // "Partial Refresh"
|
||||
};
|
||||
|
||||
EInk(uint16_t width, uint16_t height, UpdateTypes supported);
|
||||
virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) = 0;
|
||||
virtual void update(uint8_t *imageData, UpdateTypes type) = 0; // Change the display image
|
||||
void await(); // Wait for an in-progress update to complete before proceeding
|
||||
bool supports(UpdateTypes type); // Can display perform a certain update type
|
||||
bool busy() { return updateRunning; } // Display able to update right now?
|
||||
EInk(uint16_t width, uint16_t height, UpdateTypes supported);
|
||||
virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1) = 0;
|
||||
virtual void update(uint8_t *imageData, UpdateTypes type) = 0; // Change the display image
|
||||
void await(); // Wait for an in-progress update to complete before proceeding
|
||||
bool supports(UpdateTypes type); // Can display perform a certain update type
|
||||
bool busy() { return updateRunning; } // Display able to update right now?
|
||||
|
||||
const uint16_t width; // Public so that NicheGraphics implementations can access. Safe because const.
|
||||
const uint16_t height;
|
||||
const uint16_t width; // Public so that NicheGraphics implementations can access. Safe because const.
|
||||
const uint16_t height;
|
||||
|
||||
protected:
|
||||
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
|
||||
protected:
|
||||
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
|
||||
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 pollingInterval = 0; // How often to check if update complete (ms)
|
||||
uint32_t pollingBegunAt = 0; // To timeout during polling
|
||||
const UpdateTypes supportedUpdateTypes; // Capabilities of a derived display class
|
||||
bool updateRunning = false; // see EInk::busy()
|
||||
uint32_t pollingInterval = 0; // How often to check if update complete (ms)
|
||||
uint32_t pollingBegunAt = 0; // To timeout during polling
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
@@ -5,54 +5,50 @@
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Map the display controller IC's output to the connected panel
|
||||
void GDEY0154D67::configScanning()
|
||||
{
|
||||
// "Driver output control"
|
||||
sendCommand(0x01);
|
||||
sendData(0xC7); // Scan until gate 199 (200px vertical res.)
|
||||
sendData(0x00);
|
||||
sendData(0x00);
|
||||
void GDEY0154D67::configScanning() {
|
||||
// "Driver output control"
|
||||
sendCommand(0x01);
|
||||
sendData(0xC7); // Scan until gate 199 (200px vertical res.)
|
||||
sendData(0x00);
|
||||
sendData(0x00);
|
||||
}
|
||||
|
||||
// Specify which information is used to control the sequence of voltages applied to move the pixels
|
||||
// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from
|
||||
// the controller IC's OTP memory, when the update procedure begins.
|
||||
void GDEY0154D67::configWaveform()
|
||||
{
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white)
|
||||
void GDEY0154D67::configWaveform() {
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white)
|
||||
|
||||
sendCommand(0x18); // Temperature sensor:
|
||||
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
|
||||
sendCommand(0x18); // Temperature sensor:
|
||||
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
|
||||
}
|
||||
|
||||
void GDEY0154D67::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
void GDEY0154D67::configUpdateSequence() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory
|
||||
break;
|
||||
}
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory
|
||||
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 GDEY0154D67::detachFromUpdate()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 300); // At least 300ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 1500); // At least 1.5 seconds for full refresh
|
||||
}
|
||||
void GDEY0154D67::detachFromUpdate() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 300); // At least 300ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 1500); // At least 1.5 seconds for full refresh
|
||||
}
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -17,24 +17,22 @@ E-Ink display driver
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class GDEY0154D67 : public SSD16XX
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 200;
|
||||
static constexpr uint32_t height = 200;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
namespace NicheGraphics::Drivers {
|
||||
class GDEY0154D67 : public SSD16XX {
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 200;
|
||||
static constexpr uint32_t height = 200;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
|
||||
public:
|
||||
GDEY0154D67() : SSD16XX(width, height, supported) {}
|
||||
public:
|
||||
GDEY0154D67() : SSD16XX(width, height, supported) {}
|
||||
|
||||
protected:
|
||||
void configScanning() override;
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
protected:
|
||||
void configScanning() override;
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
@@ -5,54 +5,50 @@
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Map the display controller IC's output to the connected panel
|
||||
void GDEY0213B74::configScanning()
|
||||
{
|
||||
// "Driver output control"
|
||||
sendCommand(0x01);
|
||||
sendData(0xF9);
|
||||
sendData(0x00);
|
||||
sendData(0x00);
|
||||
void GDEY0213B74::configScanning() {
|
||||
// "Driver output control"
|
||||
sendCommand(0x01);
|
||||
sendData(0xF9);
|
||||
sendData(0x00);
|
||||
sendData(0x00);
|
||||
}
|
||||
|
||||
// Specify which information is used to control the sequence of voltages applied to move the pixels
|
||||
// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from
|
||||
// the controller IC's OTP memory, when the update procedure begins.
|
||||
void GDEY0213B74::configWaveform()
|
||||
{
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white)
|
||||
void GDEY0213B74::configWaveform() {
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white)
|
||||
|
||||
sendCommand(0x18); // Temperature sensor:
|
||||
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
|
||||
sendCommand(0x18); // Temperature sensor:
|
||||
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
|
||||
}
|
||||
|
||||
void GDEY0213B74::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
void GDEY0213B74::configUpdateSequence() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory
|
||||
break;
|
||||
}
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory
|
||||
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 GDEY0213B74::detachFromUpdate()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 500); // At least 500ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 2000); // At least 2 seconds for full refresh
|
||||
}
|
||||
void GDEY0213B74::detachFromUpdate() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 500); // At least 500ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 2000); // At least 2 seconds for full refresh
|
||||
}
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -19,24 +19,22 @@ E-Ink display driver
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class GDEY0213B74 : public SSD16XX
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 122;
|
||||
static constexpr uint32_t height = 250;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
namespace NicheGraphics::Drivers {
|
||||
class GDEY0213B74 : 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:
|
||||
GDEY0213B74() : SSD16XX(width, height, supported) {}
|
||||
public:
|
||||
GDEY0213B74() : SSD16XX(width, height, supported) {}
|
||||
|
||||
protected:
|
||||
virtual void configScanning() override;
|
||||
virtual void configWaveform() override;
|
||||
virtual void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
protected:
|
||||
virtual void configScanning() override;
|
||||
virtual void configWaveform() override;
|
||||
virtual void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
@@ -5,57 +5,53 @@
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Map the display controller IC's output to the connected panel
|
||||
void HINK_E0213A289::configScanning()
|
||||
{
|
||||
// "Driver output control"
|
||||
// Scan gates from 0 to 249 (vertical resolution 250px)
|
||||
sendCommand(0x01);
|
||||
sendData(0xF9); // Maximum gate # (249, bits 0-7)
|
||||
sendData(0x00); // Maximum gate # (bit 8)
|
||||
sendData(0x00); // (Do not invert scanning order)
|
||||
void HINK_E0213A289::configScanning() {
|
||||
// "Driver output control"
|
||||
// Scan gates from 0 to 249 (vertical resolution 250px)
|
||||
sendCommand(0x01);
|
||||
sendData(0xF9); // Maximum gate # (249, bits 0-7)
|
||||
sendData(0x00); // Maximum gate # (bit 8)
|
||||
sendData(0x00); // (Do not invert scanning order)
|
||||
}
|
||||
|
||||
// Specify which information is used to control the sequence of voltages applied to move the pixels
|
||||
// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from
|
||||
// the controller IC's OTP memory, when the update procedure begins.
|
||||
void HINK_E0213A289::configWaveform()
|
||||
{
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white)
|
||||
void HINK_E0213A289::configWaveform() {
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white)
|
||||
|
||||
sendCommand(0x18); // Temperature sensor:
|
||||
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
|
||||
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_E0213A289::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
void HINK_E0213A289::configUpdateSequence() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory
|
||||
break;
|
||||
}
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory
|
||||
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_E0213A289::detachFromUpdate()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 500); // At least 500ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 1000); // At least 1 second for full refresh (quick; display only blinks pixels once)
|
||||
}
|
||||
void HINK_E0213A289::detachFromUpdate() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 500); // At least 500ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 1000); // At least 1 second for full refresh (quick; display only blinks pixels once)
|
||||
}
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -19,24 +19,22 @@ E-Ink display driver
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class HINK_E0213A289 : public SSD16XX
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 122;
|
||||
static constexpr uint32_t height = 250;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
namespace NicheGraphics::Drivers {
|
||||
class HINK_E0213A289 : 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:
|
||||
HINK_E0213A289() : SSD16XX(width, height, supported, 1) {}
|
||||
public:
|
||||
HINK_E0213A289() : SSD16XX(width, height, supported, 1) {}
|
||||
|
||||
protected:
|
||||
void configScanning() override;
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
protected:
|
||||
void configScanning() override;
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
@@ -7,52 +7,49 @@ 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
|
||||
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
|
||||
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);
|
||||
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;
|
||||
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);
|
||||
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;
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
@@ -20,23 +20,21 @@ E-Ink display driver
|
||||
|
||||
#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);
|
||||
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) {}
|
||||
public:
|
||||
HINK_E042A87() : SSD16XX(width, height, supported) {}
|
||||
|
||||
protected:
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
protected:
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
@@ -5,64 +5,60 @@
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Map the display controller IC's output to the connected panel
|
||||
void LCMEN2R13ECC1::configScanning()
|
||||
{
|
||||
// "Driver output control"
|
||||
sendCommand(0x01);
|
||||
sendData(0xF9);
|
||||
sendData(0x00);
|
||||
sendData(0x00);
|
||||
void LCMEN2R13ECC1::configScanning() {
|
||||
// "Driver output control"
|
||||
sendCommand(0x01);
|
||||
sendData(0xF9);
|
||||
sendData(0x00);
|
||||
sendData(0x00);
|
||||
|
||||
// To-do: delete this method?
|
||||
// Values set here might be redundant: F9, 00, 00 seems to be default
|
||||
// To-do: delete this method?
|
||||
// Values set here might be redundant: F9, 00, 00 seems to be default
|
||||
}
|
||||
|
||||
// Specify which information is used to control the sequence of voltages applied to move the pixels
|
||||
// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from
|
||||
// the controller IC's OTP memory, when the update procedure begins.
|
||||
void LCMEN2R13ECC1::configWaveform()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x85);
|
||||
break;
|
||||
void LCMEN2R13ECC1::configWaveform() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x85);
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
// From OTP memory
|
||||
break;
|
||||
}
|
||||
case FULL:
|
||||
default:
|
||||
// From OTP memory
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void LCMEN2R13ECC1::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
void LCMEN2R13ECC1::configUpdateSequence() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory
|
||||
break;
|
||||
}
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory
|
||||
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 LCMEN2R13ECC1::detachFromUpdate()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 800); // At least 500ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 2500); // At least 2 seconds for full refresh
|
||||
}
|
||||
void LCMEN2R13ECC1::detachFromUpdate() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 800); // At least 500ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 2500); // At least 2 seconds for full refresh
|
||||
}
|
||||
}
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -16,24 +16,22 @@ E-Ink display driver
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class LCMEN2R13ECC1 : public SSD16XX
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 122;
|
||||
static constexpr uint32_t height = 250;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
namespace NicheGraphics::Drivers {
|
||||
class LCMEN2R13ECC1 : 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:
|
||||
LCMEN2R13ECC1() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte
|
||||
public:
|
||||
LCMEN2R13ECC1() : SSD16XX(width, height, supported, 1) {} // Note: left edge of this display is offset by 1 byte
|
||||
|
||||
protected:
|
||||
virtual void configScanning() override;
|
||||
virtual void configWaveform() override;
|
||||
virtual void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
protected:
|
||||
virtual void configScanning() override;
|
||||
virtual void configWaveform() override;
|
||||
virtual void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
@@ -68,239 +68,223 @@ static const uint8_t LUT_FAST_BB[] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, //
|
||||
};
|
||||
|
||||
LCMEN213EFC1::LCMEN213EFC1() : EInk(width, height, supported)
|
||||
{
|
||||
// Pre-calculate size of the image buffer, for convenience
|
||||
LCMEN213EFC1::LCMEN213EFC1() : EInk(width, height, supported) {
|
||||
// Pre-calculate size of the image buffer, for convenience
|
||||
|
||||
// Determine the X dimension of the image buffer, in bytes.
|
||||
// Along rows, pixels are stored 8 per byte.
|
||||
// Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these.
|
||||
bufferRowSize = ((width - 1) / 8) + 1;
|
||||
// Determine the X dimension of the image buffer, in bytes.
|
||||
// Along rows, pixels are stored 8 per byte.
|
||||
// Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these.
|
||||
bufferRowSize = ((width - 1) / 8) + 1;
|
||||
|
||||
// Total size of image buffer, in bytes.
|
||||
bufferSize = bufferRowSize * height;
|
||||
// Total size of image buffer, in bytes.
|
||||
bufferSize = bufferRowSize * height;
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
|
||||
{
|
||||
this->spi = spi;
|
||||
this->pin_dc = pin_dc;
|
||||
this->pin_cs = pin_cs;
|
||||
this->pin_busy = pin_busy;
|
||||
this->pin_rst = pin_rst;
|
||||
void LCMEN213EFC1::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) {
|
||||
this->spi = spi;
|
||||
this->pin_dc = pin_dc;
|
||||
this->pin_cs = pin_cs;
|
||||
this->pin_busy = pin_busy;
|
||||
this->pin_rst = pin_rst;
|
||||
|
||||
pinMode(pin_dc, OUTPUT);
|
||||
pinMode(pin_cs, OUTPUT);
|
||||
pinMode(pin_busy, INPUT);
|
||||
pinMode(pin_dc, OUTPUT);
|
||||
pinMode(pin_cs, OUTPUT);
|
||||
pinMode(pin_busy, INPUT);
|
||||
|
||||
// Reset is active low, hold high
|
||||
pinMode(pin_rst, INPUT_PULLUP);
|
||||
// Reset is active low, hold high
|
||||
pinMode(pin_rst, INPUT_PULLUP);
|
||||
|
||||
reset();
|
||||
reset();
|
||||
}
|
||||
|
||||
// Display an image on the display
|
||||
void LCMEN213EFC1::update(uint8_t *imageData, UpdateTypes type)
|
||||
{
|
||||
this->updateType = type;
|
||||
this->buffer = imageData;
|
||||
void LCMEN213EFC1::update(uint8_t *imageData, UpdateTypes type) {
|
||||
this->updateType = type;
|
||||
this->buffer = imageData;
|
||||
|
||||
reset();
|
||||
reset();
|
||||
|
||||
// Config
|
||||
if (updateType == FULL)
|
||||
configFull();
|
||||
else
|
||||
configFast();
|
||||
// Config
|
||||
if (updateType == FULL)
|
||||
configFull();
|
||||
else
|
||||
configFast();
|
||||
|
||||
// Transfer image data
|
||||
if (updateType == FULL) {
|
||||
writeNewImage();
|
||||
writeOldImage();
|
||||
} else {
|
||||
writeNewImage();
|
||||
}
|
||||
// Transfer image data
|
||||
if (updateType == FULL) {
|
||||
writeNewImage();
|
||||
writeOldImage();
|
||||
} else {
|
||||
writeNewImage();
|
||||
}
|
||||
|
||||
sendCommand(0x04); // Power on the panel voltage
|
||||
wait();
|
||||
sendCommand(0x04); // Power on the panel voltage
|
||||
wait();
|
||||
|
||||
sendCommand(0x12); // Begin executing the update
|
||||
sendCommand(0x12); // Begin executing the update
|
||||
|
||||
// Let the update run async, on display hardware. Base class will poll completion, then finalize.
|
||||
// For a blocking update, call await after update
|
||||
detachFromUpdate();
|
||||
// Let the update run async, on display hardware. Base class will poll completion, then finalize.
|
||||
// For a blocking update, call await after update
|
||||
detachFromUpdate();
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::wait()
|
||||
{
|
||||
// Busy when LOW
|
||||
while (digitalRead(pin_busy) == LOW)
|
||||
yield();
|
||||
void LCMEN213EFC1::wait() {
|
||||
// Busy when LOW
|
||||
while (digitalRead(pin_busy) == LOW)
|
||||
yield();
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::reset()
|
||||
{
|
||||
pinMode(pin_rst, OUTPUT);
|
||||
digitalWrite(pin_rst, LOW);
|
||||
delay(10);
|
||||
pinMode(pin_rst, INPUT_PULLUP);
|
||||
wait();
|
||||
void LCMEN213EFC1::reset() {
|
||||
pinMode(pin_rst, OUTPUT);
|
||||
digitalWrite(pin_rst, LOW);
|
||||
delay(10);
|
||||
pinMode(pin_rst, INPUT_PULLUP);
|
||||
wait();
|
||||
|
||||
sendCommand(0x12);
|
||||
wait();
|
||||
sendCommand(0x12);
|
||||
wait();
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::sendCommand(const uint8_t command)
|
||||
{
|
||||
// Take firmware's SPI lock
|
||||
spiLock->lock();
|
||||
void LCMEN213EFC1::sendCommand(const uint8_t command) {
|
||||
// Take firmware's SPI lock
|
||||
spiLock->lock();
|
||||
|
||||
spi->beginTransaction(spiSettings);
|
||||
digitalWrite(pin_dc, LOW); // DC pin low indicates command
|
||||
digitalWrite(pin_cs, LOW);
|
||||
spi->transfer(command);
|
||||
digitalWrite(pin_cs, HIGH);
|
||||
digitalWrite(pin_dc, HIGH);
|
||||
spi->endTransaction();
|
||||
spi->beginTransaction(spiSettings);
|
||||
digitalWrite(pin_dc, LOW); // DC pin low indicates command
|
||||
digitalWrite(pin_cs, LOW);
|
||||
spi->transfer(command);
|
||||
digitalWrite(pin_cs, HIGH);
|
||||
digitalWrite(pin_dc, HIGH);
|
||||
spi->endTransaction();
|
||||
|
||||
spiLock->unlock();
|
||||
spiLock->unlock();
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::sendData(uint8_t data)
|
||||
{
|
||||
sendData(&data, 1);
|
||||
}
|
||||
void LCMEN213EFC1::sendData(uint8_t data) { sendData(&data, 1); }
|
||||
|
||||
void LCMEN213EFC1::sendData(const uint8_t *data, uint32_t size)
|
||||
{
|
||||
// Take firmware's SPI lock
|
||||
spiLock->lock();
|
||||
void LCMEN213EFC1::sendData(const uint8_t *data, uint32_t size) {
|
||||
// Take firmware's SPI lock
|
||||
spiLock->lock();
|
||||
|
||||
spi->beginTransaction(spiSettings);
|
||||
digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
|
||||
digitalWrite(pin_cs, LOW);
|
||||
spi->beginTransaction(spiSettings);
|
||||
digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
|
||||
digitalWrite(pin_cs, LOW);
|
||||
|
||||
// Platform-specific SPI command
|
||||
// Mothballing. This display model is only used by Heltec Wireless Paper (ESP32)
|
||||
// Platform-specific SPI command
|
||||
// Mothballing. This display model is only used by Heltec Wireless Paper (ESP32)
|
||||
#if defined(ARCH_ESP32)
|
||||
spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer
|
||||
spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer
|
||||
#elif defined(ARCH_NRF52)
|
||||
spi->transfer(data, NULL, size); // NULL for a "write only" transfer
|
||||
spi->transfer(data, NULL, size); // NULL for a "write only" transfer
|
||||
#else
|
||||
#error Not implemented yet? Feel free to add other platforms here.
|
||||
#endif
|
||||
|
||||
digitalWrite(pin_cs, HIGH);
|
||||
digitalWrite(pin_dc, HIGH);
|
||||
spi->endTransaction();
|
||||
digitalWrite(pin_cs, HIGH);
|
||||
digitalWrite(pin_dc, HIGH);
|
||||
spi->endTransaction();
|
||||
|
||||
spiLock->unlock();
|
||||
spiLock->unlock();
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::configFull()
|
||||
{
|
||||
sendCommand(0x00); // Panel setting register
|
||||
sendData(0b11 << 6 // Display resolution
|
||||
| 1 << 4 // B&W only
|
||||
| 1 << 3 // Vertical scan direction
|
||||
| 1 << 2 // Horizontal scan direction
|
||||
| 1 << 1 // Shutdown: no
|
||||
| 1 << 0 // Reset: no
|
||||
);
|
||||
void LCMEN213EFC1::configFull() {
|
||||
sendCommand(0x00); // Panel setting register
|
||||
sendData(0b11 << 6 // Display resolution
|
||||
| 1 << 4 // B&W only
|
||||
| 1 << 3 // Vertical scan direction
|
||||
| 1 << 2 // Horizontal scan direction
|
||||
| 1 << 1 // Shutdown: no
|
||||
| 1 << 0 // Reset: no
|
||||
);
|
||||
|
||||
sendCommand(0x50); // VCOM and data interval setting register
|
||||
sendData(0b10 << 6 // Border driven white
|
||||
| 0b11 << 4 // Invert image colors: no
|
||||
| 0b0111 << 0 // Interval between VCOM on and image data (default)
|
||||
);
|
||||
sendCommand(0x50); // VCOM and data interval setting register
|
||||
sendData(0b10 << 6 // Border driven white
|
||||
| 0b11 << 4 // Invert image colors: no
|
||||
| 0b0111 << 0 // Interval between VCOM on and image data (default)
|
||||
);
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::configFast()
|
||||
{
|
||||
sendCommand(0x00); // Panel setting register
|
||||
sendData(0b11 << 6 // Display resolution
|
||||
| 1 << 5 // LUT from registers (set below)
|
||||
| 1 << 4 // B&W only
|
||||
| 1 << 3 // Vertical scan direction
|
||||
| 1 << 2 // Horizontal scan direction
|
||||
| 1 << 1 // Shutdown: no
|
||||
| 1 << 0 // Reset: no
|
||||
);
|
||||
void LCMEN213EFC1::configFast() {
|
||||
sendCommand(0x00); // Panel setting register
|
||||
sendData(0b11 << 6 // Display resolution
|
||||
| 1 << 5 // LUT from registers (set below)
|
||||
| 1 << 4 // B&W only
|
||||
| 1 << 3 // Vertical scan direction
|
||||
| 1 << 2 // Horizontal scan direction
|
||||
| 1 << 1 // Shutdown: no
|
||||
| 1 << 0 // Reset: no
|
||||
);
|
||||
|
||||
sendCommand(0x50); // VCOM and data interval setting register
|
||||
sendData(0b11 << 6 // Border floating
|
||||
| 0b01 << 4 // Invert image colors: no
|
||||
| 0b0111 << 0 // Interval between VCOM on and image data (default)
|
||||
);
|
||||
sendCommand(0x50); // VCOM and data interval setting register
|
||||
sendData(0b11 << 6 // Border floating
|
||||
| 0b01 << 4 // Invert image colors: no
|
||||
| 0b0111 << 0 // Interval between VCOM on and image data (default)
|
||||
);
|
||||
|
||||
// Load the various LUTs
|
||||
sendCommand(0x20); // VCOM
|
||||
sendData(LUT_FAST_VCOMDC, sizeof(LUT_FAST_VCOMDC));
|
||||
// Load the various LUTs
|
||||
sendCommand(0x20); // VCOM
|
||||
sendData(LUT_FAST_VCOMDC, sizeof(LUT_FAST_VCOMDC));
|
||||
|
||||
sendCommand(0x21); // White -> White
|
||||
sendData(LUT_FAST_WW, sizeof(LUT_FAST_WW));
|
||||
sendCommand(0x21); // White -> White
|
||||
sendData(LUT_FAST_WW, sizeof(LUT_FAST_WW));
|
||||
|
||||
sendCommand(0x22); // Black -> White
|
||||
sendData(LUT_FAST_BW, sizeof(LUT_FAST_BW));
|
||||
sendCommand(0x22); // Black -> White
|
||||
sendData(LUT_FAST_BW, sizeof(LUT_FAST_BW));
|
||||
|
||||
sendCommand(0x23); // White -> Black
|
||||
sendData(LUT_FAST_WB, sizeof(LUT_FAST_WB));
|
||||
sendCommand(0x23); // White -> Black
|
||||
sendData(LUT_FAST_WB, sizeof(LUT_FAST_WB));
|
||||
|
||||
sendCommand(0x24); // Black -> Black
|
||||
sendData(LUT_FAST_BB, sizeof(LUT_FAST_BB));
|
||||
sendCommand(0x24); // Black -> Black
|
||||
sendData(LUT_FAST_BB, sizeof(LUT_FAST_BB));
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::writeNewImage()
|
||||
{
|
||||
sendCommand(0x13);
|
||||
sendData(buffer, bufferSize);
|
||||
void LCMEN213EFC1::writeNewImage() {
|
||||
sendCommand(0x13);
|
||||
sendData(buffer, bufferSize);
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::writeOldImage()
|
||||
{
|
||||
sendCommand(0x10);
|
||||
sendData(buffer, bufferSize);
|
||||
void LCMEN213EFC1::writeOldImage() {
|
||||
sendCommand(0x10);
|
||||
sendData(buffer, bufferSize);
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::detachFromUpdate()
|
||||
{
|
||||
// To save power / cycles, displays can choose to specify an "expected duration" for various refresh types
|
||||
// If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed
|
||||
// If not implemented, we'll just poll right from the get-go
|
||||
switch (updateType) {
|
||||
case FULL:
|
||||
EInk::beginPolling(10, 3650);
|
||||
break;
|
||||
case FAST:
|
||||
EInk::beginPolling(10, 720);
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
void LCMEN213EFC1::detachFromUpdate() {
|
||||
// To save power / cycles, displays can choose to specify an "expected duration" for various refresh types
|
||||
// If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed
|
||||
// If not implemented, we'll just poll right from the get-go
|
||||
switch (updateType) {
|
||||
case FULL:
|
||||
EInk::beginPolling(10, 3650);
|
||||
break;
|
||||
case FAST:
|
||||
EInk::beginPolling(10, 720);
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
bool LCMEN213EFC1::isUpdateDone()
|
||||
{
|
||||
// Busy when LOW
|
||||
if (digitalRead(pin_busy) == LOW)
|
||||
return false;
|
||||
else
|
||||
return true;
|
||||
bool LCMEN213EFC1::isUpdateDone() {
|
||||
// Busy when LOW
|
||||
if (digitalRead(pin_busy) == LOW)
|
||||
return false;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
void LCMEN213EFC1::finalizeUpdate()
|
||||
{
|
||||
// Power off the panel voltages
|
||||
sendCommand(0x02);
|
||||
void LCMEN213EFC1::finalizeUpdate() {
|
||||
// Power off the panel voltages
|
||||
sendCommand(0x02);
|
||||
wait();
|
||||
|
||||
// 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) {
|
||||
writeOldImage();
|
||||
wait();
|
||||
|
||||
// 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) {
|
||||
writeOldImage();
|
||||
wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -20,50 +20,48 @@ It is implemented as a "one-off", directly inheriting the EInk base class, unlik
|
||||
|
||||
#include "./EInk.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
namespace NicheGraphics::Drivers {
|
||||
|
||||
class LCMEN213EFC1 : public EInk
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 122;
|
||||
static constexpr uint32_t height = 250;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
class LCMEN213EFC1 : public EInk {
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 122;
|
||||
static constexpr uint32_t height = 250;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
|
||||
public:
|
||||
LCMEN213EFC1();
|
||||
void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst);
|
||||
void update(uint8_t *imageData, UpdateTypes type) override;
|
||||
public:
|
||||
LCMEN213EFC1();
|
||||
void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst);
|
||||
void update(uint8_t *imageData, UpdateTypes type) override;
|
||||
|
||||
protected:
|
||||
void wait();
|
||||
void reset();
|
||||
void sendCommand(const uint8_t command);
|
||||
void sendData(const uint8_t data);
|
||||
void sendData(const uint8_t *data, uint32_t size);
|
||||
void configFull(); // Configure display for FULL refresh
|
||||
void configFast(); // Configure display for FAST refresh
|
||||
void writeNewImage();
|
||||
void writeOldImage(); // Used for "differential update", aka FAST refresh
|
||||
protected:
|
||||
void wait();
|
||||
void reset();
|
||||
void sendCommand(const uint8_t command);
|
||||
void sendData(const uint8_t data);
|
||||
void sendData(const uint8_t *data, uint32_t size);
|
||||
void configFull(); // Configure display for FULL refresh
|
||||
void configFast(); // Configure display for FAST refresh
|
||||
void writeNewImage();
|
||||
void writeOldImage(); // Used for "differential update", aka FAST refresh
|
||||
|
||||
void detachFromUpdate();
|
||||
bool isUpdateDone();
|
||||
void finalizeUpdate();
|
||||
void detachFromUpdate();
|
||||
bool isUpdateDone();
|
||||
void finalizeUpdate();
|
||||
|
||||
protected:
|
||||
uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring?
|
||||
uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes)
|
||||
uint32_t bufferSize = 0; // In bytes. Rows * Columns
|
||||
uint8_t *buffer = nullptr;
|
||||
UpdateTypes updateType = UpdateTypes::UNSPECIFIED;
|
||||
protected:
|
||||
uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring?
|
||||
uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes)
|
||||
uint32_t bufferSize = 0; // In bytes. Rows * Columns
|
||||
uint8_t *buffer = nullptr;
|
||||
UpdateTypes updateType = UpdateTypes::UNSPECIFIED;
|
||||
|
||||
uint8_t pin_dc = -1;
|
||||
uint8_t pin_cs = -1;
|
||||
uint8_t pin_busy = -1;
|
||||
uint8_t pin_rst = -1;
|
||||
SPIClass *spi = nullptr;
|
||||
SPISettings spiSettings = SPISettings(6000000, MSBFIRST, SPI_MODE0);
|
||||
uint8_t pin_dc = -1;
|
||||
uint8_t pin_cs = -1;
|
||||
uint8_t pin_busy = -1;
|
||||
uint8_t pin_rst = -1;
|
||||
SPIClass *spi = nullptr;
|
||||
SPISettings spiSettings = SPISettings(6000000, MSBFIRST, SPI_MODE0);
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
@@ -5,37 +5,34 @@
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
SSD1682::SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX)
|
||||
: SSD16XX(width, height, supported, bufferOffsetX)
|
||||
{
|
||||
}
|
||||
: SSD16XX(width, height, supported, bufferOffsetX) {}
|
||||
|
||||
// SSD1682 only accepts single-byte x and y values
|
||||
// This causes an incompatibility with the default SSD16XX::configFullscreen
|
||||
void SSD1682::configFullscreen()
|
||||
{
|
||||
// Define the boundaries of the "fullscreen" region, for the controller IC
|
||||
static const uint8_t sx = bufferOffsetX; // Notice the offset
|
||||
static const uint8_t sy = 0;
|
||||
static const uint8_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this
|
||||
static const uint8_t ey = height;
|
||||
void SSD1682::configFullscreen() {
|
||||
// Define the boundaries of the "fullscreen" region, for the controller IC
|
||||
static const uint8_t sx = bufferOffsetX; // Notice the offset
|
||||
static const uint8_t sy = 0;
|
||||
static const uint8_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this
|
||||
static const uint8_t ey = height;
|
||||
|
||||
// Data entry mode - Left to Right, Top to Bottom
|
||||
sendCommand(0x11);
|
||||
sendData(0x03);
|
||||
// Data entry mode - Left to Right, Top to Bottom
|
||||
sendCommand(0x11);
|
||||
sendData(0x03);
|
||||
|
||||
// Select controller IC memory region to display a fullscreen image
|
||||
sendCommand(0x44); // Memory X start - end
|
||||
sendData(sx);
|
||||
sendData(ex);
|
||||
sendCommand(0x45); // Memory Y start - end
|
||||
sendData(sy);
|
||||
sendData(ey);
|
||||
// Select controller IC memory region to display a fullscreen image
|
||||
sendCommand(0x44); // Memory X start - end
|
||||
sendData(sx);
|
||||
sendData(ex);
|
||||
sendCommand(0x45); // Memory Y start - end
|
||||
sendData(sy);
|
||||
sendData(ey);
|
||||
|
||||
// Place the cursor at the start of this memory region, ready to send image data x=0 y=0
|
||||
sendCommand(0x4E); // Memory cursor X
|
||||
sendData(sx);
|
||||
sendCommand(0x4F); // Memory cursor y
|
||||
sendData(sy);
|
||||
// Place the cursor at the start of this memory region, ready to send image data x=0 y=0
|
||||
sendCommand(0x4E); // Memory cursor X
|
||||
sendData(sx);
|
||||
sendCommand(0x4F); // Memory cursor y
|
||||
sendData(sy);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -15,15 +15,13 @@ to avoid re-implementing them every time we need to add a new SSD1682-based disp
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
namespace NicheGraphics::Drivers {
|
||||
|
||||
class SSD1682 : public SSD16XX
|
||||
{
|
||||
public:
|
||||
SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX = 0);
|
||||
virtual void configFullscreen(); // Select memory region on controller IC
|
||||
virtual void deepSleep() {} // Not usable (image memory not retained)
|
||||
class SSD1682 : public SSD16XX {
|
||||
public:
|
||||
SSD1682(uint16_t width, uint16_t height, EInk::UpdateTypes supported, uint8_t bufferOffsetX = 0);
|
||||
virtual void configFullscreen(); // Select memory region on controller IC
|
||||
virtual void deepSleep() {} // Not usable (image memory not retained)
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
@@ -7,266 +7,249 @@
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
SSD16XX::SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX)
|
||||
: EInk(width, height, supported), bufferOffsetX(bufferOffsetX)
|
||||
{
|
||||
// Pre-calculate size of the image buffer, for convenience
|
||||
: EInk(width, height, supported), bufferOffsetX(bufferOffsetX) {
|
||||
// Pre-calculate size of the image buffer, for convenience
|
||||
|
||||
// Determine the X dimension of the image buffer, in bytes.
|
||||
// Along rows, pixels are stored 8 per byte.
|
||||
// Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these.
|
||||
bufferRowSize = ((width - 1) / 8) + 1;
|
||||
// Determine the X dimension of the image buffer, in bytes.
|
||||
// Along rows, pixels are stored 8 per byte.
|
||||
// Not all display widths are divisible by 8. Need to make sure bytecount accommodates padding for these.
|
||||
bufferRowSize = ((width - 1) / 8) + 1;
|
||||
|
||||
// Total size of image buffer, in bytes.
|
||||
bufferSize = bufferRowSize * height;
|
||||
// Total size of image buffer, in bytes.
|
||||
bufferSize = bufferRowSize * height;
|
||||
}
|
||||
|
||||
void SSD16XX::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst)
|
||||
{
|
||||
this->spi = spi;
|
||||
this->pin_dc = pin_dc;
|
||||
this->pin_cs = pin_cs;
|
||||
this->pin_busy = pin_busy;
|
||||
this->pin_rst = pin_rst;
|
||||
void SSD16XX::begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst) {
|
||||
this->spi = spi;
|
||||
this->pin_dc = pin_dc;
|
||||
this->pin_cs = pin_cs;
|
||||
this->pin_busy = pin_busy;
|
||||
this->pin_rst = pin_rst;
|
||||
|
||||
pinMode(pin_dc, OUTPUT);
|
||||
pinMode(pin_cs, OUTPUT);
|
||||
pinMode(pin_busy, INPUT);
|
||||
pinMode(pin_dc, OUTPUT);
|
||||
pinMode(pin_cs, OUTPUT);
|
||||
pinMode(pin_busy, INPUT);
|
||||
|
||||
// If using a reset pin, hold high
|
||||
// Reset is active low for Solomon Systech ICs
|
||||
if (pin_rst != 0xFF)
|
||||
pinMode(pin_rst, INPUT_PULLUP);
|
||||
// If using a reset pin, hold high
|
||||
// Reset is active low for Solomon Systech ICs
|
||||
if (pin_rst != 0xFF)
|
||||
pinMode(pin_rst, INPUT_PULLUP);
|
||||
|
||||
reset();
|
||||
reset();
|
||||
}
|
||||
|
||||
// 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;
|
||||
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();
|
||||
uint32_t startMs = millis();
|
||||
|
||||
// Busy when HIGH
|
||||
while (digitalRead(pin_busy) == HIGH) {
|
||||
// Check for timeout
|
||||
if (millis() - startMs > timeout) {
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
yield();
|
||||
// Busy when HIGH
|
||||
while (digitalRead(pin_busy) == HIGH) {
|
||||
// Check for timeout
|
||||
if (millis() - startMs > timeout) {
|
||||
failed = true;
|
||||
break;
|
||||
}
|
||||
yield();
|
||||
}
|
||||
}
|
||||
|
||||
void SSD16XX::reset()
|
||||
{
|
||||
// Check if reset pin is defined
|
||||
if (pin_rst != 0xFF) {
|
||||
pinMode(pin_rst, OUTPUT);
|
||||
digitalWrite(pin_rst, LOW);
|
||||
delay(10);
|
||||
digitalWrite(pin_rst, HIGH);
|
||||
delay(10);
|
||||
wait();
|
||||
}
|
||||
|
||||
sendCommand(0x12);
|
||||
void SSD16XX::reset() {
|
||||
// Check if reset pin is defined
|
||||
if (pin_rst != 0xFF) {
|
||||
pinMode(pin_rst, OUTPUT);
|
||||
digitalWrite(pin_rst, LOW);
|
||||
delay(10);
|
||||
digitalWrite(pin_rst, HIGH);
|
||||
delay(10);
|
||||
wait();
|
||||
}
|
||||
|
||||
sendCommand(0x12);
|
||||
wait();
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
// Take firmware's SPI lock
|
||||
spiLock->lock();
|
||||
// Take firmware's SPI lock
|
||||
spiLock->lock();
|
||||
|
||||
spi->beginTransaction(spiSettings);
|
||||
digitalWrite(pin_dc, LOW); // DC pin low indicates command
|
||||
digitalWrite(pin_cs, LOW);
|
||||
spi->transfer(command);
|
||||
digitalWrite(pin_cs, HIGH);
|
||||
digitalWrite(pin_dc, HIGH);
|
||||
spi->endTransaction();
|
||||
spi->beginTransaction(spiSettings);
|
||||
digitalWrite(pin_dc, LOW); // DC pin low indicates command
|
||||
digitalWrite(pin_cs, LOW);
|
||||
spi->transfer(command);
|
||||
digitalWrite(pin_cs, HIGH);
|
||||
digitalWrite(pin_dc, HIGH);
|
||||
spi->endTransaction();
|
||||
|
||||
spiLock->unlock();
|
||||
spiLock->unlock();
|
||||
}
|
||||
|
||||
void SSD16XX::sendData(uint8_t data)
|
||||
{
|
||||
sendData(&data, 1);
|
||||
}
|
||||
void SSD16XX::sendData(uint8_t data) { sendData(&data, 1); }
|
||||
|
||||
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;
|
||||
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;
|
||||
|
||||
// Take firmware's SPI lock
|
||||
spiLock->lock();
|
||||
// Take firmware's SPI lock
|
||||
spiLock->lock();
|
||||
|
||||
spi->beginTransaction(spiSettings);
|
||||
digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
|
||||
digitalWrite(pin_cs, LOW);
|
||||
spi->beginTransaction(spiSettings);
|
||||
digitalWrite(pin_dc, HIGH); // DC pin HIGH indicates data, instead of command
|
||||
digitalWrite(pin_cs, LOW);
|
||||
|
||||
// Platform-specific SPI command
|
||||
// Platform-specific SPI command
|
||||
#if defined(ARCH_ESP32)
|
||||
spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer
|
||||
spi->transferBytes(data, NULL, size); // NULL for a "write only" transfer
|
||||
#elif defined(ARCH_NRF52)
|
||||
spi->transfer(data, NULL, size); // NULL for a "write only" transfer
|
||||
spi->transfer(data, NULL, size); // NULL for a "write only" transfer
|
||||
#else
|
||||
#error Not implemented yet? Feel free to add other platforms here.
|
||||
#endif
|
||||
|
||||
digitalWrite(pin_cs, HIGH);
|
||||
digitalWrite(pin_dc, HIGH);
|
||||
spi->endTransaction();
|
||||
digitalWrite(pin_cs, HIGH);
|
||||
digitalWrite(pin_dc, HIGH);
|
||||
spi->endTransaction();
|
||||
|
||||
spiLock->unlock();
|
||||
spiLock->unlock();
|
||||
}
|
||||
|
||||
void SSD16XX::configFullscreen()
|
||||
{
|
||||
// Placing this code in a separate method because it's probably pretty consistent between displays
|
||||
// Should make it tidier to override SSD16XX::configure
|
||||
void SSD16XX::configFullscreen() {
|
||||
// Placing this code in a separate method because it's probably pretty consistent between displays
|
||||
// Should make it tidier to override SSD16XX::configure
|
||||
|
||||
// Define the boundaries of the "fullscreen" region, for the controller IC
|
||||
static const uint16_t sx = bufferOffsetX; // Notice the offset
|
||||
static const uint16_t sy = 0;
|
||||
static const uint16_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this
|
||||
static const uint16_t ey = height;
|
||||
// Define the boundaries of the "fullscreen" region, for the controller IC
|
||||
static const uint16_t sx = bufferOffsetX; // Notice the offset
|
||||
static const uint16_t sy = 0;
|
||||
static const uint16_t ex = bufferRowSize + bufferOffsetX - 1; // End is "max index", not "count". Minus 1 handles this
|
||||
static const uint16_t ey = height;
|
||||
|
||||
// Split into bytes
|
||||
static const uint8_t sy1 = sy & 0xFF;
|
||||
static const uint8_t sy2 = (sy >> 8) & 0xFF;
|
||||
static const uint8_t ey1 = ey & 0xFF;
|
||||
static const uint8_t ey2 = (ey >> 8) & 0xFF;
|
||||
// Split into bytes
|
||||
static const uint8_t sy1 = sy & 0xFF;
|
||||
static const uint8_t sy2 = (sy >> 8) & 0xFF;
|
||||
static const uint8_t ey1 = ey & 0xFF;
|
||||
static const uint8_t ey2 = (ey >> 8) & 0xFF;
|
||||
|
||||
// Data entry mode - Left to Right, Top to Bottom
|
||||
sendCommand(0x11);
|
||||
sendData(0x03);
|
||||
// Data entry mode - Left to Right, Top to Bottom
|
||||
sendCommand(0x11);
|
||||
sendData(0x03);
|
||||
|
||||
// Select controller IC memory region to display a fullscreen image
|
||||
sendCommand(0x44); // Memory X start - end
|
||||
sendData(sx);
|
||||
sendData(ex);
|
||||
sendCommand(0x45); // Memory Y start - end
|
||||
sendData(sy1);
|
||||
sendData(sy2);
|
||||
sendData(ey1);
|
||||
sendData(ey2);
|
||||
// Select controller IC memory region to display a fullscreen image
|
||||
sendCommand(0x44); // Memory X start - end
|
||||
sendData(sx);
|
||||
sendData(ex);
|
||||
sendCommand(0x45); // Memory Y start - end
|
||||
sendData(sy1);
|
||||
sendData(sy2);
|
||||
sendData(ey1);
|
||||
sendData(ey2);
|
||||
|
||||
// Place the cursor at the start of this memory region, ready to send image data x=0 y=0
|
||||
sendCommand(0x4E); // Memory cursor X
|
||||
sendData(sx);
|
||||
sendCommand(0x4F); // Memory cursor y
|
||||
sendData(sy1);
|
||||
sendData(sy2);
|
||||
// Place the cursor at the start of this memory region, ready to send image data x=0 y=0
|
||||
sendCommand(0x4E); // Memory cursor X
|
||||
sendData(sx);
|
||||
sendCommand(0x4F); // Memory cursor y
|
||||
sendData(sy1);
|
||||
sendData(sy2);
|
||||
}
|
||||
|
||||
void SSD16XX::update(uint8_t *imageData, UpdateTypes type)
|
||||
{
|
||||
this->updateType = type;
|
||||
this->buffer = imageData;
|
||||
void SSD16XX::update(uint8_t *imageData, UpdateTypes type) {
|
||||
this->updateType = type;
|
||||
this->buffer = imageData;
|
||||
|
||||
reset();
|
||||
reset();
|
||||
|
||||
configFullscreen();
|
||||
configScanning(); // Virtual, unused by base class
|
||||
configVoltages(); // Virtual, unused by base class
|
||||
configWaveform(); // Virtual, unused by base class
|
||||
wait();
|
||||
configFullscreen();
|
||||
configScanning(); // Virtual, unused by base class
|
||||
configVoltages(); // Virtual, unused by base class
|
||||
configWaveform(); // Virtual, unused by base class
|
||||
wait();
|
||||
|
||||
if (updateType == FULL) {
|
||||
writeNewImage();
|
||||
writeOldImage();
|
||||
} else {
|
||||
writeNewImage();
|
||||
}
|
||||
if (updateType == FULL) {
|
||||
writeNewImage();
|
||||
writeOldImage();
|
||||
} else {
|
||||
writeNewImage();
|
||||
}
|
||||
|
||||
configUpdateSequence();
|
||||
sendCommand(0x20); // Begin executing the update
|
||||
configUpdateSequence();
|
||||
sendCommand(0x20); // Begin executing the update
|
||||
|
||||
// Let the update run async, on display hardware. Base class will poll completion, then finalize.
|
||||
// For a blocking update, call await after update
|
||||
detachFromUpdate();
|
||||
// Let the update run async, on display hardware. Base class will poll completion, then finalize.
|
||||
// For a blocking update, call await after update
|
||||
detachFromUpdate();
|
||||
}
|
||||
|
||||
// Send SPI commands for controller IC to begin executing the refresh operation
|
||||
void SSD16XX::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Non-differential, load waveform from OTP
|
||||
break;
|
||||
}
|
||||
void SSD16XX::configUpdateSequence() {
|
||||
switch (updateType) {
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Non-differential, load waveform from OTP
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SSD16XX::writeNewImage()
|
||||
{
|
||||
sendCommand(0x24);
|
||||
sendData(buffer, bufferSize);
|
||||
void SSD16XX::writeNewImage() {
|
||||
sendCommand(0x24);
|
||||
sendData(buffer, bufferSize);
|
||||
}
|
||||
|
||||
void SSD16XX::writeOldImage()
|
||||
{
|
||||
sendCommand(0x26);
|
||||
sendData(buffer, bufferSize);
|
||||
void SSD16XX::writeOldImage() {
|
||||
sendCommand(0x26);
|
||||
sendData(buffer, bufferSize);
|
||||
}
|
||||
|
||||
void SSD16XX::detachFromUpdate()
|
||||
{
|
||||
// To save power / cycles, displays can choose to specify an "expected duration" for various refresh types
|
||||
// If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed
|
||||
// If not implemented, we'll just poll right from the get-go
|
||||
switch (updateType) {
|
||||
default:
|
||||
EInk::beginPolling(100, 0);
|
||||
}
|
||||
void SSD16XX::detachFromUpdate() {
|
||||
// To save power / cycles, displays can choose to specify an "expected duration" for various refresh types
|
||||
// If we know a full-refresh takes at least 4 seconds, we can delay polling until 3 seconds have passed
|
||||
// If not implemented, we'll just poll right from the get-go
|
||||
switch (updateType) {
|
||||
default:
|
||||
EInk::beginPolling(100, 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool SSD16XX::isUpdateDone()
|
||||
{
|
||||
// Busy when HIGH
|
||||
if (digitalRead(pin_busy) == HIGH)
|
||||
return false;
|
||||
else
|
||||
return true;
|
||||
bool SSD16XX::isUpdateDone() {
|
||||
// Busy when HIGH
|
||||
if (digitalRead(pin_busy) == HIGH)
|
||||
return false;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
void SSD16XX::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(); // Only required by some controller variants. Todo: Override just for GDEY0154D678?
|
||||
writeOldImage();
|
||||
sendCommand(0x7F); // Terminate image write without update
|
||||
wait();
|
||||
}
|
||||
void SSD16XX::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(); // Only required by some controller variants. Todo: Override just for GDEY0154D678?
|
||||
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();
|
||||
// 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
|
||||
void SSD16XX::deepSleep() {
|
||||
sendCommand(0x10); // Enter deep sleep
|
||||
sendData(0x01); // Mode 1: preserve image RAM
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -16,49 +16,47 @@ See DEPG0154BNS800 and DEPG0290BNS800 for examples.
|
||||
|
||||
#include "./EInk.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
namespace NicheGraphics::Drivers {
|
||||
|
||||
class SSD16XX : public EInk
|
||||
{
|
||||
public:
|
||||
SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX = 0);
|
||||
virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1);
|
||||
virtual void update(uint8_t *imageData, UpdateTypes type) override;
|
||||
class SSD16XX : public EInk {
|
||||
public:
|
||||
SSD16XX(uint16_t width, uint16_t height, UpdateTypes supported, uint8_t bufferOffsetX = 0);
|
||||
virtual void begin(SPIClass *spi, uint8_t pin_dc, uint8_t pin_cs, uint8_t pin_busy, uint8_t pin_rst = -1);
|
||||
virtual void update(uint8_t *imageData, UpdateTypes type) override;
|
||||
|
||||
protected:
|
||||
virtual void wait(uint32_t timeout = 1000);
|
||||
virtual void reset();
|
||||
virtual void sendCommand(const uint8_t command);
|
||||
virtual void sendData(const uint8_t data);
|
||||
virtual void sendData(const uint8_t *data, uint32_t size);
|
||||
virtual void configFullscreen(); // Select memory region on controller IC
|
||||
virtual void configScanning() {} // Optional. First & last gates, scan direction, etc
|
||||
virtual void configVoltages() {} // Optional. Manual panel voltages, soft-start, etc
|
||||
virtual void configWaveform() {} // Optional. LUT, panel border, temperature sensor, etc
|
||||
virtual void configUpdateSequence(); // Tell controller IC which operations to run
|
||||
protected:
|
||||
virtual void wait(uint32_t timeout = 1000);
|
||||
virtual void reset();
|
||||
virtual void sendCommand(const uint8_t command);
|
||||
virtual void sendData(const uint8_t data);
|
||||
virtual void sendData(const uint8_t *data, uint32_t size);
|
||||
virtual void configFullscreen(); // Select memory region on controller IC
|
||||
virtual void configScanning() {} // Optional. First & last gates, scan direction, etc
|
||||
virtual void configVoltages() {} // Optional. Manual panel voltages, soft-start, etc
|
||||
virtual void configWaveform() {} // Optional. LUT, panel border, temperature sensor, etc
|
||||
virtual void configUpdateSequence(); // Tell controller IC which operations to run
|
||||
|
||||
virtual void writeNewImage();
|
||||
virtual void writeOldImage(); // Image which can be used at *next* update for "differential refresh"
|
||||
virtual void writeNewImage();
|
||||
virtual void writeOldImage(); // Image which can be used at *next* update for "differential refresh"
|
||||
|
||||
virtual void detachFromUpdate();
|
||||
virtual bool isUpdateDone() override;
|
||||
virtual void finalizeUpdate() override;
|
||||
virtual void deepSleep();
|
||||
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?
|
||||
uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes)
|
||||
uint32_t bufferSize = 0; // In bytes. Rows * Columns
|
||||
uint8_t *buffer = nullptr;
|
||||
UpdateTypes updateType = UpdateTypes::UNSPECIFIED;
|
||||
protected:
|
||||
uint8_t bufferOffsetX = 0; // In bytes. Panel x=0 does not always align with controller x=0. Quirky internal wiring?
|
||||
uint8_t bufferRowSize = 0; // In bytes. Rows store 8 pixels per byte. Rounded up to fit (e.g. 122px would require 16 bytes)
|
||||
uint32_t bufferSize = 0; // In bytes. Rows * Columns
|
||||
uint8_t *buffer = nullptr;
|
||||
UpdateTypes updateType = UpdateTypes::UNSPECIFIED;
|
||||
|
||||
uint8_t pin_dc = -1;
|
||||
uint8_t pin_cs = -1;
|
||||
uint8_t pin_busy = -1;
|
||||
uint8_t pin_rst = -1;
|
||||
SPIClass *spi = nullptr;
|
||||
SPISettings spiSettings = SPISettings(4000000, MSBFIRST, SPI_MODE0);
|
||||
uint8_t pin_dc = -1;
|
||||
uint8_t pin_cs = -1;
|
||||
uint8_t pin_busy = -1;
|
||||
uint8_t pin_rst = -1;
|
||||
SPIClass *spi = nullptr;
|
||||
SPISettings spiSettings = SPISettings(4000000, MSBFIRST, SPI_MODE0);
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
@@ -5,64 +5,60 @@
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Map the display controller IC's output to the connected panel
|
||||
void ZJY122250_0213BAAMFGN::configScanning()
|
||||
{
|
||||
// "Driver output control"
|
||||
// Scan gates from 0 to 249 (vertical resolution 250px)
|
||||
sendCommand(0x01);
|
||||
sendData(0xF9);
|
||||
sendData(0x00);
|
||||
sendData(0x00);
|
||||
void ZJY122250_0213BAAMFGN::configScanning() {
|
||||
// "Driver output control"
|
||||
// Scan gates from 0 to 249 (vertical resolution 250px)
|
||||
sendCommand(0x01);
|
||||
sendData(0xF9);
|
||||
sendData(0x00);
|
||||
sendData(0x00);
|
||||
}
|
||||
|
||||
// Specify which information is used to control the sequence of voltages applied to move the pixels
|
||||
// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from
|
||||
// the controller IC's OTP memory, when the update procedure begins.
|
||||
void ZJY122250_0213BAAMFGN::configWaveform()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x80); // VCOM
|
||||
break;
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x01); // Follow LUT 1 (blink same as white pixels)
|
||||
break;
|
||||
}
|
||||
void ZJY122250_0213BAAMFGN::configWaveform() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x80); // VCOM
|
||||
break;
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x01); // Follow LUT 1 (blink same as white pixels)
|
||||
break;
|
||||
}
|
||||
|
||||
sendCommand(0x18); // Temperature sensor:
|
||||
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
|
||||
sendCommand(0x18); // Temperature sensor:
|
||||
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
|
||||
}
|
||||
|
||||
void ZJY122250_0213BAAMFGN::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
void ZJY122250_0213BAAMFGN::configUpdateSequence() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory
|
||||
break;
|
||||
}
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory
|
||||
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 ZJY122250_0213BAAMFGN::detachFromUpdate()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 500); // At least 500ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 2000); // At least 2 seconds for full refresh
|
||||
}
|
||||
void ZJY122250_0213BAAMFGN::detachFromUpdate() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 500); // At least 500ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 2000); // At least 2 seconds for full refresh
|
||||
}
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -17,24 +17,22 @@ E-Ink display driver
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class ZJY122250_0213BAAMFGN : public SSD16XX
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 122;
|
||||
static constexpr uint32_t height = 250;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
namespace NicheGraphics::Drivers {
|
||||
class ZJY122250_0213BAAMFGN : 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:
|
||||
ZJY122250_0213BAAMFGN() : SSD16XX(width, height, supported) {}
|
||||
public:
|
||||
ZJY122250_0213BAAMFGN() : SSD16XX(width, height, supported) {}
|
||||
|
||||
protected:
|
||||
virtual void configScanning() override;
|
||||
virtual void configWaveform() override;
|
||||
virtual void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
protected:
|
||||
virtual void configScanning() override;
|
||||
virtual void configWaveform() override;
|
||||
virtual void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
@@ -5,55 +5,51 @@
|
||||
using namespace NicheGraphics::Drivers;
|
||||
|
||||
// Map the display controller IC's output to the connected panel
|
||||
void ZJY128296_029EAAMFGN::configScanning()
|
||||
{
|
||||
// "Driver output control"
|
||||
// Scan gates from 0 to 295 (vertical resolution 296px)
|
||||
sendCommand(0x01);
|
||||
sendData(0x27); // Number of gates (295, bits 0-7)
|
||||
sendData(0x01); // Number of gates (295, bit 8)
|
||||
sendData(0x00); // (Do not invert scanning order)
|
||||
void ZJY128296_029EAAMFGN::configScanning() {
|
||||
// "Driver output control"
|
||||
// Scan gates from 0 to 295 (vertical resolution 296px)
|
||||
sendCommand(0x01);
|
||||
sendData(0x27); // Number of gates (295, bits 0-7)
|
||||
sendData(0x01); // Number of gates (295, bit 8)
|
||||
sendData(0x00); // (Do not invert scanning order)
|
||||
}
|
||||
|
||||
// Specify which information is used to control the sequence of voltages applied to move the pixels
|
||||
// - For this display, configUpdateSequence() specifies that a suitable LUT will be loaded from
|
||||
// the controller IC's OTP memory, when the update procedure begins.
|
||||
void ZJY128296_029EAAMFGN::configWaveform()
|
||||
{
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white)
|
||||
void ZJY128296_029EAAMFGN::configWaveform() {
|
||||
sendCommand(0x3C); // Border waveform:
|
||||
sendData(0x05); // Screen border should follow LUT1 waveform (actively drive pixels white)
|
||||
|
||||
sendCommand(0x18); // Temperature sensor:
|
||||
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
|
||||
sendCommand(0x18); // Temperature sensor:
|
||||
sendData(0x80); // Use internal temperature sensor to select an appropriate refresh waveform
|
||||
}
|
||||
|
||||
void ZJY128296_029EAAMFGN::configUpdateSequence()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
void ZJY128296_029EAAMFGN::configUpdateSequence() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xFF); // Will load LUT from OTP memory, Display mode 2 "differential refresh"
|
||||
break;
|
||||
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory
|
||||
break;
|
||||
}
|
||||
case FULL:
|
||||
default:
|
||||
sendCommand(0x22); // Set "update sequence"
|
||||
sendData(0xF7); // Will load LUT from OTP memory
|
||||
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 ZJY128296_029EAAMFGN::detachFromUpdate()
|
||||
{
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 300); // At least 300ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 2000); // At least 2 seconds for full refresh
|
||||
}
|
||||
void ZJY128296_029EAAMFGN::detachFromUpdate() {
|
||||
switch (updateType) {
|
||||
case FAST:
|
||||
return beginPolling(50, 300); // At least 300ms for fast refresh
|
||||
case FULL:
|
||||
default:
|
||||
return beginPolling(100, 2000); // At least 2 seconds for full refresh
|
||||
}
|
||||
}
|
||||
#endif // MESHTASTIC_INCLUDE_NICHE_GRAPHICS
|
||||
@@ -19,24 +19,22 @@ E-Ink display driver
|
||||
|
||||
#include "./SSD16XX.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
class ZJY128296_029EAAMFGN : public SSD16XX
|
||||
{
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 128;
|
||||
static constexpr uint32_t height = 296;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
namespace NicheGraphics::Drivers {
|
||||
class ZJY128296_029EAAMFGN : public SSD16XX {
|
||||
// Display properties
|
||||
private:
|
||||
static constexpr uint32_t width = 128;
|
||||
static constexpr uint32_t height = 296;
|
||||
static constexpr UpdateTypes supported = (UpdateTypes)(FULL | FAST);
|
||||
|
||||
public:
|
||||
ZJY128296_029EAAMFGN() : SSD16XX(width, height, supported) {}
|
||||
public:
|
||||
ZJY128296_029EAAMFGN() : SSD16XX(width, height, supported) {}
|
||||
|
||||
protected:
|
||||
void configScanning() override;
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
protected:
|
||||
void configScanning() override;
|
||||
void configWaveform() override;
|
||||
void configUpdateSequence() override;
|
||||
void detachFromUpdate() override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::Drivers
|
||||
|
||||
@@ -22,8 +22,7 @@ E-Ink display driver
|
||||
|
||||
#include "./GDEY0154D67.h"
|
||||
|
||||
namespace NicheGraphics::Drivers
|
||||
{
|
||||
namespace NicheGraphics::Drivers {
|
||||
|
||||
typedef GDEY0154D67 ZJY200200_0154DAAMFGN;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+120
-117
@@ -24,157 +24,160 @@
|
||||
#include "./Tile.h"
|
||||
#include "graphics/niche/Drivers/EInk/EInk.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
using NicheGraphics::Drivers::EInk;
|
||||
using std::to_string;
|
||||
|
||||
class Applet : public GFX
|
||||
{
|
||||
public:
|
||||
// Which edge Applet::printAt will place on the Y parameter
|
||||
enum VerticalAlignment : uint8_t {
|
||||
TOP,
|
||||
MIDDLE,
|
||||
BOTTOM,
|
||||
};
|
||||
class Applet : public GFX {
|
||||
public:
|
||||
// Which edge Applet::printAt will place on the Y parameter
|
||||
enum VerticalAlignment : uint8_t {
|
||||
TOP,
|
||||
MIDDLE,
|
||||
BOTTOM,
|
||||
};
|
||||
|
||||
// Which edge Applet::printAt will place on the X parameter
|
||||
enum HorizontalAlignment : uint8_t {
|
||||
LEFT,
|
||||
RIGHT,
|
||||
CENTER,
|
||||
};
|
||||
// Which edge Applet::printAt will place on the X parameter
|
||||
enum HorizontalAlignment : uint8_t {
|
||||
LEFT,
|
||||
RIGHT,
|
||||
CENTER,
|
||||
};
|
||||
|
||||
// An easy-to-understand interpretation of SNR and RSSI
|
||||
// Calculate with Applet::getSignalStrength
|
||||
enum SignalStrength : int8_t {
|
||||
SIGNAL_UNKNOWN = -1,
|
||||
SIGNAL_NONE,
|
||||
SIGNAL_BAD,
|
||||
SIGNAL_FAIR,
|
||||
SIGNAL_GOOD,
|
||||
};
|
||||
// An easy-to-understand interpretation of SNR and RSSI
|
||||
// Calculate with Applet::getSignalStrength
|
||||
enum SignalStrength : int8_t {
|
||||
SIGNAL_UNKNOWN = -1,
|
||||
SIGNAL_NONE,
|
||||
SIGNAL_BAD,
|
||||
SIGNAL_FAIR,
|
||||
SIGNAL_GOOD,
|
||||
};
|
||||
|
||||
Applet();
|
||||
Applet();
|
||||
|
||||
void setTile(Tile *t); // Should only be called via Tile::setApplet
|
||||
Tile *getTile(); // Tile with which this applet is linked
|
||||
void setTile(Tile *t); // Should only be called via Tile::setApplet
|
||||
Tile *getTile(); // Tile with which this applet is linked
|
||||
|
||||
// Rendering
|
||||
// Rendering
|
||||
|
||||
void render(); // Draw the applet
|
||||
bool wantsToRender(); // Check whether applet wants to render
|
||||
bool wantsToAutoshow(); // Check whether applet wants to become foreground
|
||||
Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer
|
||||
void updateDimensions(); // Get current size from tile
|
||||
void resetDrawingSpace(); // Makes sure every render starts with same parameters
|
||||
void render(); // Draw the applet
|
||||
bool wantsToRender(); // Check whether applet wants to render
|
||||
bool wantsToAutoshow(); // Check whether applet wants to become foreground
|
||||
Drivers::EInk::UpdateTypes wantsUpdateType(); // Check which display update type the applet would prefer
|
||||
void updateDimensions(); // Get current size from tile
|
||||
void resetDrawingSpace(); // Makes sure every render starts with same parameters
|
||||
|
||||
// State of the applet
|
||||
// State of the applet
|
||||
|
||||
void activate(); // Begin running
|
||||
void deactivate(); // Stop running
|
||||
void bringToForeground(); // Show
|
||||
void sendToBackground(); // Hide
|
||||
bool isActive();
|
||||
bool isForeground();
|
||||
void activate(); // Begin running
|
||||
void deactivate(); // Stop running
|
||||
void bringToForeground(); // Show
|
||||
void sendToBackground(); // Hide
|
||||
bool isActive();
|
||||
bool isForeground();
|
||||
|
||||
// Event handlers
|
||||
// Event handlers
|
||||
|
||||
virtual void onRender() = 0; // All drawing happens here
|
||||
virtual void onActivate() {}
|
||||
virtual void onDeactivate() {}
|
||||
virtual void onForeground() {}
|
||||
virtual void onBackground() {}
|
||||
virtual void onShutdown() {}
|
||||
virtual void onButtonShortPress() {}
|
||||
virtual void onButtonLongPress() {}
|
||||
virtual void onExitShort() {}
|
||||
virtual void onExitLong() {}
|
||||
virtual void onNavUp() {}
|
||||
virtual void onNavDown() {}
|
||||
virtual void onNavLeft() {}
|
||||
virtual void onNavRight() {}
|
||||
virtual void onRender() = 0; // All drawing happens here
|
||||
virtual void onActivate() {}
|
||||
virtual void onDeactivate() {}
|
||||
virtual void onForeground() {}
|
||||
virtual void onBackground() {}
|
||||
virtual void onShutdown() {}
|
||||
virtual void onButtonShortPress() {}
|
||||
virtual void onButtonLongPress() {}
|
||||
virtual void onExitShort() {}
|
||||
virtual void onExitLong() {}
|
||||
virtual void onNavUp() {}
|
||||
virtual void onNavDown() {}
|
||||
virtual void onNavLeft() {}
|
||||
virtual void onNavRight() {}
|
||||
|
||||
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
|
||||
virtual bool approveNotification(Notification &n); // Allow an applet to veto a notification
|
||||
|
||||
static uint16_t getHeaderHeight(); // How tall the "standard" applet header is
|
||||
static uint16_t getHeaderHeight(); // How tall the "standard" applet header is
|
||||
|
||||
static AppletFont fontSmall, fontMedium, fontLarge; // The general purpose fonts, used by all applets
|
||||
static AppletFont fontSmall, fontMedium, fontLarge; // The general purpose fonts, used by all applets
|
||||
|
||||
const char *name = nullptr; // Shown in applet selection menu. Also used as an identifier by InkHUD::getSystemApplet
|
||||
const char *name = nullptr; // Shown in applet selection menu. Also used as an identifier by InkHUD::getSystemApplet
|
||||
|
||||
protected:
|
||||
void drawPixel(int16_t x, int16_t y, uint16_t color) override; // Place a single pixel. All drawing output passes through here
|
||||
protected:
|
||||
void drawPixel(int16_t x, int16_t y,
|
||||
uint16_t color) override; // Place a single pixel. All drawing output passes through here
|
||||
|
||||
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update
|
||||
void requestAutoshow(); // Ask for applet to be moved to foreground
|
||||
void requestUpdate(EInk::UpdateTypes type = EInk::UpdateTypes::UNSPECIFIED); // Ask WindowManager to schedule a display update
|
||||
void requestAutoshow(); // Ask for applet to be moved to foreground
|
||||
|
||||
uint16_t X(float f); // Map applet width, mapped from 0 to 1.0
|
||||
uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0
|
||||
void setCrop(int16_t left, int16_t top, uint16_t width, uint16_t height); // Ignore pixels drawn outside a certain region
|
||||
void resetCrop(); // Removes setCrop()
|
||||
uint16_t X(float f); // Map applet width, mapped from 0 to 1.0
|
||||
uint16_t Y(float f); // Map applet height, mapped from 0 to 1.0
|
||||
void setCrop(int16_t left, int16_t top, uint16_t width,
|
||||
uint16_t height); // Ignore pixels drawn outside a certain region
|
||||
void resetCrop(); // Removes setCrop()
|
||||
|
||||
// Text
|
||||
// Text
|
||||
|
||||
void setFont(AppletFont f);
|
||||
AppletFont getFont();
|
||||
uint16_t getTextWidth(std::string text);
|
||||
uint16_t getTextWidth(const char *text);
|
||||
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped
|
||||
void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||
void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||
void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX, uint8_t thicknessY); // Faux bold
|
||||
void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping
|
||||
void setFont(AppletFont f);
|
||||
AppletFont getFont();
|
||||
uint16_t getTextWidth(std::string text);
|
||||
uint16_t getTextWidth(const char *text);
|
||||
uint32_t getWrappedTextHeight(int16_t left, uint16_t width, std::string text); // Result of printWrapped
|
||||
void printAt(int16_t x, int16_t y, const char *text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||
void printAt(int16_t x, int16_t y, std::string text, HorizontalAlignment ha = LEFT, VerticalAlignment va = TOP);
|
||||
void printThick(int16_t xCenter, int16_t yCenter, std::string text, uint8_t thicknessX,
|
||||
uint8_t thicknessY); // Faux bold
|
||||
void printWrapped(int16_t left, int16_t top, uint16_t width, std::string text); // Per-word line wrapping
|
||||
|
||||
void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing, Color color); // Fill with sparse lines
|
||||
void drawHeader(std::string text); // Draw the standard applet header
|
||||
void hatchRegion(int16_t x, int16_t y, uint16_t w, uint16_t h, uint8_t spacing,
|
||||
Color color); // Fill with sparse lines
|
||||
void drawHeader(std::string text); // Draw the standard applet header
|
||||
|
||||
// Meshtastic Logo
|
||||
// Meshtastic Logo
|
||||
|
||||
static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo
|
||||
uint16_t getLogoWidth(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
|
||||
uint16_t getLogoHeight(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
|
||||
void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height,
|
||||
Color color = BLACK); // Draw the Meshtastic logo
|
||||
static constexpr float LOGO_ASPECT_RATIO = 1.9; // Width:Height for drawing the Meshtastic logo
|
||||
uint16_t getLogoWidth(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
|
||||
uint16_t getLogoHeight(uint16_t limitWidth, uint16_t limitHeight); // Size Meshtastic logo to fit within region
|
||||
void drawLogo(int16_t centerX, int16_t centerY, uint16_t width, uint16_t height,
|
||||
Color color = BLACK); // Draw the Meshtastic logo
|
||||
|
||||
std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc
|
||||
SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value
|
||||
std::string getTimeString(uint32_t epochSeconds); // Human readable
|
||||
std::string getTimeString(); // Current time, human readable
|
||||
uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu
|
||||
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
|
||||
std::string parse(std::string text); // Handle text which might contain special chars
|
||||
std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars
|
||||
bool isPrintable(std::string); // Check for characters which the font can't print
|
||||
std::string hexifyNodeNum(NodeNum num); // Style as !0123abdc
|
||||
SignalStrength getSignalStrength(float snr, float rssi); // Interpret SNR and RSSI, as an easy to understand value
|
||||
std::string getTimeString(uint32_t epochSeconds); // Human readable
|
||||
std::string getTimeString(); // Current time, human readable
|
||||
uint16_t getActiveNodeCount(); // Duration determined by user, in onscreen menu
|
||||
std::string localizeDistance(uint32_t meters); // Human readable distance, imperial or metric
|
||||
std::string parse(std::string text); // Handle text which might contain special chars
|
||||
std::string parseShortName(meshtastic_NodeInfoLite *node); // Get the shortname, or a substitute if has unprintable chars
|
||||
bool isPrintable(std::string); // Check for characters which the font can't print
|
||||
|
||||
// Convenient references
|
||||
// Convenient references
|
||||
|
||||
InkHUD *inkhud = nullptr;
|
||||
Persistence::Settings *settings = nullptr;
|
||||
Persistence::LatestMessage *latestMessage = nullptr;
|
||||
InkHUD *inkhud = nullptr;
|
||||
Persistence::Settings *settings = nullptr;
|
||||
Persistence::LatestMessage *latestMessage = nullptr;
|
||||
|
||||
private:
|
||||
Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object, which translates them, then passes to WM
|
||||
bool active = false; // Has the user enabled this applet (at run-time)?
|
||||
bool foreground = false; // Is the applet currently drawn on a tile?
|
||||
private:
|
||||
Tile *assignedTile = nullptr; // Rendered pixels are fed into a Tile object, which translates them, then passes to WM
|
||||
bool active = false; // Has the user enabled this applet (at run-time)?
|
||||
bool foreground = false; // Is the applet currently drawn on a tile?
|
||||
|
||||
bool wantRender = false; // In some situations, checked by WindowManager when updating, to skip unneeded redrawing.
|
||||
bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground?
|
||||
NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType =
|
||||
NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the display
|
||||
bool wantRender = false; // In some situations, checked by WindowManager when updating, to skip unneeded redrawing.
|
||||
bool wantAutoshow = false; // Does the applet have new data it would like to display in foreground?
|
||||
NicheGraphics::Drivers::EInk::UpdateTypes wantUpdateType =
|
||||
NicheGraphics::Drivers::EInk::UpdateTypes::UNSPECIFIED; // Which update method we'd prefer when redrawing the
|
||||
// display
|
||||
|
||||
using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly
|
||||
using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager.
|
||||
using GFX::setFont; // Make sure derived classes use AppletFont instead of AdafruitGFX fonts directly
|
||||
using GFX::setRotation; // Block setRotation calls. Rotation is handled globally by WindowManager.
|
||||
|
||||
AppletFont currentFont; // As passed to setFont
|
||||
AppletFont currentFont; // As passed to setFont
|
||||
|
||||
// As set by setCrop
|
||||
int16_t cropLeft = 0;
|
||||
int16_t cropTop = 0;
|
||||
uint16_t cropWidth = 0;
|
||||
uint16_t cropHeight = 0;
|
||||
// As set by setCrop
|
||||
int16_t cropLeft = 0;
|
||||
int16_t cropTop = 0;
|
||||
uint16_t cropWidth = 0;
|
||||
uint16_t cropHeight = 0;
|
||||
};
|
||||
|
||||
}; // namespace NicheGraphics::InkHUD
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,42 +14,40 @@
|
||||
|
||||
#include <GFX.h> // GFXRoot drawing lib
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
// An AdafruitGFX font, bundled with precalculated dimensions which are used frequently by InkHUD
|
||||
class AppletFont
|
||||
{
|
||||
public:
|
||||
enum Encoding {
|
||||
ASCII,
|
||||
WINDOWS_1250,
|
||||
WINDOWS_1251,
|
||||
WINDOWS_1252,
|
||||
};
|
||||
class AppletFont {
|
||||
public:
|
||||
enum Encoding {
|
||||
ASCII,
|
||||
WINDOWS_1250,
|
||||
WINDOWS_1251,
|
||||
WINDOWS_1252,
|
||||
};
|
||||
|
||||
AppletFont();
|
||||
AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, int8_t paddingBottom = 0);
|
||||
AppletFont();
|
||||
AppletFont(const GFXfont &adafruitGFXFont, Encoding encoding = ASCII, int8_t paddingTop = 0, int8_t paddingBottom = 0);
|
||||
|
||||
uint8_t lineHeight();
|
||||
uint8_t heightAboveCursor();
|
||||
uint8_t heightBelowCursor();
|
||||
uint8_t widthBetweenWords(); // Width of the space character
|
||||
uint8_t lineHeight();
|
||||
uint8_t heightAboveCursor();
|
||||
uint8_t heightBelowCursor();
|
||||
uint8_t widthBetweenWords(); // Width of the space character
|
||||
|
||||
std::string decodeUTF8(std::string encoded);
|
||||
std::string decodeUTF8(std::string encoded);
|
||||
|
||||
const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font
|
||||
const GFXfont *gfxFont = NULL; // Default value: in-built AdafruitGFX font
|
||||
|
||||
private:
|
||||
uint32_t toUtf32(std::string utf8);
|
||||
char applyEncoding(std::string utf8);
|
||||
private:
|
||||
uint32_t toUtf32(std::string utf8);
|
||||
char applyEncoding(std::string utf8);
|
||||
|
||||
uint8_t height = 8; // Default value: in-built AdafruitGFX font
|
||||
uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font
|
||||
uint8_t descenderHeight = 8; // Default value: in-built AdafruitGFX font
|
||||
uint8_t spaceCharWidth = 8; // Default value: in-built AdafruitGFX font
|
||||
uint8_t height = 8; // Default value: in-built AdafruitGFX font
|
||||
uint8_t ascenderHeight = 0; // Default value: in-built AdafruitGFX font
|
||||
uint8_t descenderHeight = 8; // Default value: in-built AdafruitGFX font
|
||||
uint8_t spaceCharWidth = 8; // Default value: in-built AdafruitGFX font
|
||||
|
||||
Encoding encoding = ASCII;
|
||||
Encoding encoding = ASCII;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -4,157 +4,156 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::MapApplet::onRender()
|
||||
{
|
||||
// Abort if no markers to render
|
||||
if (!enoughMarkers()) {
|
||||
printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Node positions", CENTER, MIDDLE);
|
||||
printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE);
|
||||
return;
|
||||
void InkHUD::MapApplet::onRender() {
|
||||
// Abort if no markers to render
|
||||
if (!enoughMarkers()) {
|
||||
printAt(X(0.5), Y(0.5) - (getFont().lineHeight() / 2), "Node positions", CENTER, MIDDLE);
|
||||
printAt(X(0.5), Y(0.5) + (getFont().lineHeight() / 2), "will appear here", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper: draw rounded rectangle centered at x,y
|
||||
auto fillRoundedRect = [&](int16_t cx, int16_t cy, int16_t w, int16_t h, int16_t r, uint16_t color) {
|
||||
int16_t x = cx - (w / 2);
|
||||
int16_t y = cy - (h / 2);
|
||||
|
||||
// center rects
|
||||
fillRect(x + r, y, w - 2 * r, h, color);
|
||||
fillRect(x, y + r, r, h - 2 * r, color);
|
||||
fillRect(x + w - r, y + r, r, h - 2 * r, color);
|
||||
|
||||
// corners
|
||||
fillCircle(x + r, y + r, r, color);
|
||||
fillCircle(x + w - r - 1, y + r, r, color);
|
||||
fillCircle(x + r, y + h - r - 1, r, color);
|
||||
fillCircle(x + w - r - 1, y + h - r - 1, r, color);
|
||||
};
|
||||
|
||||
// Find center of map
|
||||
getMapCenter(&latCenter, &lngCenter);
|
||||
calculateAllMarkers();
|
||||
getMapSize(&widthMeters, &heightMeters);
|
||||
calculateMapScale();
|
||||
|
||||
// Draw all markers first
|
||||
for (Marker m : markers) {
|
||||
int16_t x = X(0.5) + (m.eastMeters * metersToPx);
|
||||
int16_t y = Y(0.5) - (m.northMeters * metersToPx);
|
||||
|
||||
// Add white halo outline first
|
||||
constexpr int outlinePad = 1;
|
||||
int boxSize = 11;
|
||||
int radius = 2; // rounded corner radius
|
||||
|
||||
// White halo background
|
||||
fillRoundedRect(x, y, boxSize + (outlinePad * 2), boxSize + (outlinePad * 2), radius + 1, WHITE);
|
||||
|
||||
// Draw inner box
|
||||
fillRoundedRect(x, y, boxSize, boxSize, radius, BLACK);
|
||||
|
||||
// Text inside
|
||||
setFont(fontSmall);
|
||||
setTextColor(WHITE);
|
||||
|
||||
// Draw actual marker on top
|
||||
if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) {
|
||||
printAt(x + 1, y + 1, "X", CENTER, MIDDLE);
|
||||
} else if (!m.hasHopsAway) {
|
||||
printAt(x + 1, y + 1, "?", CENTER, MIDDLE);
|
||||
} else {
|
||||
char hopStr[4];
|
||||
snprintf(hopStr, sizeof(hopStr), "%d", m.hopsAway);
|
||||
printAt(x, y + 1, hopStr, CENTER, MIDDLE);
|
||||
}
|
||||
|
||||
// Helper: draw rounded rectangle centered at x,y
|
||||
auto fillRoundedRect = [&](int16_t cx, int16_t cy, int16_t w, int16_t h, int16_t r, uint16_t color) {
|
||||
int16_t x = cx - (w / 2);
|
||||
int16_t y = cy - (h / 2);
|
||||
// Restore default font and color
|
||||
setFont(fontSmall);
|
||||
setTextColor(BLACK);
|
||||
}
|
||||
|
||||
// center rects
|
||||
fillRect(x + r, y, w - 2 * r, h, color);
|
||||
fillRect(x, y + r, r, h - 2 * r, color);
|
||||
fillRect(x + w - r, y + r, r, h - 2 * r, color);
|
||||
// Dual map scale bars
|
||||
int16_t horizPx = width() * 0.25f;
|
||||
int16_t vertPx = height() * 0.25f;
|
||||
float horizMeters = horizPx / metersToPx;
|
||||
float vertMeters = vertPx / metersToPx;
|
||||
|
||||
// corners
|
||||
fillCircle(x + r, y + r, r, color);
|
||||
fillCircle(x + w - r - 1, y + r, r, color);
|
||||
fillCircle(x + r, y + h - r - 1, r, color);
|
||||
fillCircle(x + w - r - 1, y + h - r - 1, r, color);
|
||||
};
|
||||
|
||||
// Find center of map
|
||||
getMapCenter(&latCenter, &lngCenter);
|
||||
calculateAllMarkers();
|
||||
getMapSize(&widthMeters, &heightMeters);
|
||||
calculateMapScale();
|
||||
|
||||
// Draw all markers first
|
||||
for (Marker m : markers) {
|
||||
int16_t x = X(0.5) + (m.eastMeters * metersToPx);
|
||||
int16_t y = Y(0.5) - (m.northMeters * metersToPx);
|
||||
|
||||
// Add white halo outline first
|
||||
constexpr int outlinePad = 1;
|
||||
int boxSize = 11;
|
||||
int radius = 2; // rounded corner radius
|
||||
|
||||
// White halo background
|
||||
fillRoundedRect(x, y, boxSize + (outlinePad * 2), boxSize + (outlinePad * 2), radius + 1, WHITE);
|
||||
|
||||
// Draw inner box
|
||||
fillRoundedRect(x, y, boxSize, boxSize, radius, BLACK);
|
||||
|
||||
// Text inside
|
||||
setFont(fontSmall);
|
||||
setTextColor(WHITE);
|
||||
|
||||
// Draw actual marker on top
|
||||
if (m.hasHopsAway && m.hopsAway > config.lora.hop_limit) {
|
||||
printAt(x + 1, y + 1, "X", CENTER, MIDDLE);
|
||||
} else if (!m.hasHopsAway) {
|
||||
printAt(x + 1, y + 1, "?", CENTER, MIDDLE);
|
||||
} else {
|
||||
char hopStr[4];
|
||||
snprintf(hopStr, sizeof(hopStr), "%d", m.hopsAway);
|
||||
printAt(x, y + 1, hopStr, CENTER, MIDDLE);
|
||||
}
|
||||
|
||||
// Restore default font and color
|
||||
setFont(fontSmall);
|
||||
setTextColor(BLACK);
|
||||
auto formatDistance = [&](float meters, char *out, size_t len) {
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
float feet = meters * 3.28084f;
|
||||
if (feet < 528)
|
||||
snprintf(out, len, "%.0f ft", feet);
|
||||
else {
|
||||
float miles = feet / 5280.0f;
|
||||
snprintf(out, len, miles < 10 ? "%.1f mi" : "%.0f mi", miles);
|
||||
}
|
||||
} else {
|
||||
if (meters >= 1000)
|
||||
snprintf(out, len, "%.1f km", meters / 1000.0f);
|
||||
else
|
||||
snprintf(out, len, "%.0f m", meters);
|
||||
}
|
||||
};
|
||||
|
||||
// Dual map scale bars
|
||||
int16_t horizPx = width() * 0.25f;
|
||||
int16_t vertPx = height() * 0.25f;
|
||||
float horizMeters = horizPx / metersToPx;
|
||||
float vertMeters = vertPx / metersToPx;
|
||||
// Horizontal scale bar
|
||||
int16_t horizBarY = height() - 2;
|
||||
int16_t horizBarX = 1;
|
||||
drawLine(horizBarX, horizBarY, horizBarX + horizPx, horizBarY, BLACK);
|
||||
drawLine(horizBarX, horizBarY - 3, horizBarX, horizBarY + 3, BLACK);
|
||||
drawLine(horizBarX + horizPx, horizBarY - 3, horizBarX + horizPx, horizBarY + 3, BLACK);
|
||||
|
||||
auto formatDistance = [&](float meters, char *out, size_t len) {
|
||||
if (config.display.units == meshtastic_Config_DisplayConfig_DisplayUnits_IMPERIAL) {
|
||||
float feet = meters * 3.28084f;
|
||||
if (feet < 528)
|
||||
snprintf(out, len, "%.0f ft", feet);
|
||||
else {
|
||||
float miles = feet / 5280.0f;
|
||||
snprintf(out, len, miles < 10 ? "%.1f mi" : "%.0f mi", miles);
|
||||
}
|
||||
} else {
|
||||
if (meters >= 1000)
|
||||
snprintf(out, len, "%.1f km", meters / 1000.0f);
|
||||
else
|
||||
snprintf(out, len, "%.0f m", meters);
|
||||
}
|
||||
};
|
||||
char horizLabel[32];
|
||||
formatDistance(horizMeters, horizLabel, sizeof(horizLabel));
|
||||
int16_t horizLabelW = getTextWidth(horizLabel);
|
||||
int16_t horizLabelH = getFont().lineHeight();
|
||||
int16_t horizLabelX = horizBarX + horizPx + 4;
|
||||
int16_t horizLabelY = horizBarY - horizLabelH + 1;
|
||||
fillRect(horizLabelX - 2, horizLabelY - 1, horizLabelW + 4, horizLabelH + 2, WHITE);
|
||||
printAt(horizLabelX, horizBarY, horizLabel, LEFT, BOTTOM);
|
||||
|
||||
// Horizontal scale bar
|
||||
int16_t horizBarY = height() - 2;
|
||||
int16_t horizBarX = 1;
|
||||
drawLine(horizBarX, horizBarY, horizBarX + horizPx, horizBarY, BLACK);
|
||||
drawLine(horizBarX, horizBarY - 3, horizBarX, horizBarY + 3, BLACK);
|
||||
drawLine(horizBarX + horizPx, horizBarY - 3, horizBarX + horizPx, horizBarY + 3, BLACK);
|
||||
// Vertical scale bar
|
||||
int16_t vertBarX = 1;
|
||||
int16_t vertBarBottom = horizBarY;
|
||||
int16_t vertBarTop = vertBarBottom - vertPx;
|
||||
drawLine(vertBarX, vertBarBottom, vertBarX, vertBarTop, BLACK);
|
||||
drawLine(vertBarX - 3, vertBarBottom, vertBarX + 3, vertBarBottom, BLACK);
|
||||
drawLine(vertBarX - 3, vertBarTop, vertBarX + 3, vertBarTop, BLACK);
|
||||
|
||||
char horizLabel[32];
|
||||
formatDistance(horizMeters, horizLabel, sizeof(horizLabel));
|
||||
int16_t horizLabelW = getTextWidth(horizLabel);
|
||||
int16_t horizLabelH = getFont().lineHeight();
|
||||
int16_t horizLabelX = horizBarX + horizPx + 4;
|
||||
int16_t horizLabelY = horizBarY - horizLabelH + 1;
|
||||
fillRect(horizLabelX - 2, horizLabelY - 1, horizLabelW + 4, horizLabelH + 2, WHITE);
|
||||
printAt(horizLabelX, horizBarY, horizLabel, LEFT, BOTTOM);
|
||||
char vertTopLabel[32];
|
||||
formatDistance(vertMeters, vertTopLabel, sizeof(vertTopLabel));
|
||||
int16_t topLabelY = vertBarTop - getFont().lineHeight() - 2;
|
||||
int16_t topLabelW = getTextWidth(vertTopLabel);
|
||||
int16_t topLabelH = getFont().lineHeight();
|
||||
fillRect(vertBarX - 2, topLabelY - 1, topLabelW + 6, topLabelH + 2, WHITE);
|
||||
printAt(vertBarX + (topLabelW / 2) + 1, topLabelY + (topLabelH / 2), vertTopLabel, CENTER, MIDDLE);
|
||||
|
||||
// Vertical scale bar
|
||||
int16_t vertBarX = 1;
|
||||
int16_t vertBarBottom = horizBarY;
|
||||
int16_t vertBarTop = vertBarBottom - vertPx;
|
||||
drawLine(vertBarX, vertBarBottom, vertBarX, vertBarTop, BLACK);
|
||||
drawLine(vertBarX - 3, vertBarBottom, vertBarX + 3, vertBarBottom, BLACK);
|
||||
drawLine(vertBarX - 3, vertBarTop, vertBarX + 3, vertBarTop, BLACK);
|
||||
char vertBottomLabel[32];
|
||||
formatDistance(vertMeters, vertBottomLabel, sizeof(vertBottomLabel));
|
||||
int16_t bottomLabelY = vertBarBottom + 4;
|
||||
int16_t bottomLabelW = getTextWidth(vertBottomLabel);
|
||||
int16_t bottomLabelH = getFont().lineHeight();
|
||||
fillRect(vertBarX - 2, bottomLabelY - 1, bottomLabelW + 6, bottomLabelH + 2, WHITE);
|
||||
printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE);
|
||||
|
||||
char vertTopLabel[32];
|
||||
formatDistance(vertMeters, vertTopLabel, sizeof(vertTopLabel));
|
||||
int16_t topLabelY = vertBarTop - getFont().lineHeight() - 2;
|
||||
int16_t topLabelW = getTextWidth(vertTopLabel);
|
||||
int16_t topLabelH = getFont().lineHeight();
|
||||
fillRect(vertBarX - 2, topLabelY - 1, topLabelW + 6, topLabelH + 2, WHITE);
|
||||
printAt(vertBarX + (topLabelW / 2) + 1, topLabelY + (topLabelH / 2), vertTopLabel, CENTER, MIDDLE);
|
||||
// Draw our node LAST with full white fill + outline
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (ourNode && nodeDB->hasValidPosition(ourNode)) {
|
||||
Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0);
|
||||
|
||||
char vertBottomLabel[32];
|
||||
formatDistance(vertMeters, vertBottomLabel, sizeof(vertBottomLabel));
|
||||
int16_t bottomLabelY = vertBarBottom + 4;
|
||||
int16_t bottomLabelW = getTextWidth(vertBottomLabel);
|
||||
int16_t bottomLabelH = getFont().lineHeight();
|
||||
fillRect(vertBarX - 2, bottomLabelY - 1, bottomLabelW + 6, bottomLabelH + 2, WHITE);
|
||||
printAt(vertBarX + (bottomLabelW / 2) + 1, bottomLabelY + (bottomLabelH / 2), vertBottomLabel, CENTER, MIDDLE);
|
||||
int16_t centerX = X(0.5) + (self.eastMeters * metersToPx);
|
||||
int16_t centerY = Y(0.5) - (self.northMeters * metersToPx);
|
||||
|
||||
// Draw our node LAST with full white fill + outline
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (ourNode && nodeDB->hasValidPosition(ourNode)) {
|
||||
Marker self = calculateMarker(ourNode->position.latitude_i * 1e-7, ourNode->position.longitude_i * 1e-7, false, 0);
|
||||
// White fill background + halo
|
||||
fillCircle(centerX, centerY, 8, WHITE); // big white base
|
||||
drawCircle(centerX, centerY, 8, WHITE); // crisp edge
|
||||
|
||||
int16_t centerX = X(0.5) + (self.eastMeters * metersToPx);
|
||||
int16_t centerY = Y(0.5) - (self.northMeters * metersToPx);
|
||||
// Black bullseye on top
|
||||
drawCircle(centerX, centerY, 6, BLACK);
|
||||
fillCircle(centerX, centerY, 2, BLACK);
|
||||
|
||||
// White fill background + halo
|
||||
fillCircle(centerX, centerY, 8, WHITE); // big white base
|
||||
drawCircle(centerX, centerY, 8, WHITE); // crisp edge
|
||||
|
||||
// Black bullseye on top
|
||||
drawCircle(centerX, centerY, 6, BLACK);
|
||||
fillCircle(centerX, centerY, 2, BLACK);
|
||||
|
||||
// Crosshairs
|
||||
drawLine(centerX - 8, centerY, centerX + 8, centerY, BLACK);
|
||||
drawLine(centerX, centerY - 8, centerX, centerY + 8, BLACK);
|
||||
}
|
||||
// Crosshairs
|
||||
drawLine(centerX - 8, centerY, centerX + 8, centerY, BLACK);
|
||||
drawLine(centerX, centerY - 8, centerX, centerY + 8, BLACK);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the center point, in the middle of all node positions
|
||||
@@ -163,396 +162,387 @@ void InkHUD::MapApplet::onRender()
|
||||
// - Calculates furthest nodes from "mean lat long"
|
||||
// - Place map center directly between these furthest nodes
|
||||
|
||||
void InkHUD::MapApplet::getMapCenter(float *lat, float *lng)
|
||||
{
|
||||
// If we have a valid position for our own node, use that as the anchor
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (ourNode && nodeDB->hasValidPosition(ourNode)) {
|
||||
*lat = ourNode->position.latitude_i * 1e-7;
|
||||
*lng = ourNode->position.longitude_i * 1e-7;
|
||||
} else {
|
||||
// Find mean lat long coords
|
||||
// ============================
|
||||
// - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet
|
||||
// - averages the x, y and z coords
|
||||
// - uses tan to find angles for lat / long degrees
|
||||
// - longitude: triangle formed by x and y (on plane of the equator)
|
||||
// - latitude: triangle formed by z (north south),
|
||||
// and the line along plane of equator which stretches from earth's axis to where point xyz intersects planet's
|
||||
// surface
|
||||
void InkHUD::MapApplet::getMapCenter(float *lat, float *lng) {
|
||||
// If we have a valid position for our own node, use that as the anchor
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (ourNode && nodeDB->hasValidPosition(ourNode)) {
|
||||
*lat = ourNode->position.latitude_i * 1e-7;
|
||||
*lng = ourNode->position.longitude_i * 1e-7;
|
||||
} else {
|
||||
// Find mean lat long coords
|
||||
// ============================
|
||||
// - assigning X, Y and Z values to position on Earth's surface in 3D space, relative to center of planet
|
||||
// - averages the x, y and z coords
|
||||
// - uses tan to find angles for lat / long degrees
|
||||
// - longitude: triangle formed by x and y (on plane of the equator)
|
||||
// - latitude: triangle formed by z (north south),
|
||||
// and the line along plane of equator which stretches from earth's axis to where point xyz intersects planet's
|
||||
// surface
|
||||
|
||||
// Working totals, averaged after nodeDB processed
|
||||
uint32_t positionCount = 0;
|
||||
float xAvg = 0;
|
||||
float yAvg = 0;
|
||||
float zAvg = 0;
|
||||
// Working totals, averaged after nodeDB processed
|
||||
uint32_t positionCount = 0;
|
||||
float xAvg = 0;
|
||||
float yAvg = 0;
|
||||
float zAvg = 0;
|
||||
|
||||
// For each node in db
|
||||
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
// For each node in db
|
||||
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
// Skip if no position
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
continue;
|
||||
// Skip if no position
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
continue;
|
||||
|
||||
// Skip if derived applet doesn't want to show this node on the map
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
// Skip if derived applet doesn't want to show this node on the map
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
|
||||
// Latitude and Longitude of node, in radians
|
||||
float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD;
|
||||
float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD;
|
||||
// Latitude and Longitude of node, in radians
|
||||
float latRad = node->position.latitude_i * (1e-7) * DEG_TO_RAD;
|
||||
float lngRad = node->position.longitude_i * (1e-7) * DEG_TO_RAD;
|
||||
|
||||
// Convert to cartesian points, with center of earth at 0, 0, 0
|
||||
// Exact distance from center is irrelevant, as we're only interested in the vector
|
||||
float x = cos(latRad) * cos(lngRad);
|
||||
float y = cos(latRad) * sin(lngRad);
|
||||
float z = sin(latRad);
|
||||
// Convert to cartesian points, with center of earth at 0, 0, 0
|
||||
// Exact distance from center is irrelevant, as we're only interested in the vector
|
||||
float x = cos(latRad) * cos(lngRad);
|
||||
float y = cos(latRad) * sin(lngRad);
|
||||
float z = sin(latRad);
|
||||
|
||||
// To find mean values shortly
|
||||
xAvg += x;
|
||||
yAvg += y;
|
||||
zAvg += z;
|
||||
positionCount++;
|
||||
}
|
||||
|
||||
// All NodeDB processed, find mean values
|
||||
xAvg /= positionCount;
|
||||
yAvg /= positionCount;
|
||||
zAvg /= positionCount;
|
||||
|
||||
// Longitude from cartesian coords
|
||||
// (Angle from 3D coords describing a point of globe's surface)
|
||||
/*
|
||||
UK
|
||||
/-------\
|
||||
(Top View) /- -\
|
||||
/- (You) -\
|
||||
/- . -\
|
||||
/- . X -\
|
||||
Asia - ... - USA
|
||||
\- Y -/
|
||||
\- -/
|
||||
\- -/
|
||||
\- -/
|
||||
\- -----/
|
||||
Pacific
|
||||
|
||||
*/
|
||||
|
||||
*lng = atan2(yAvg, xAvg) * RAD_TO_DEG;
|
||||
|
||||
// Latitude from cartesian coords
|
||||
// (Angle from 3D coords describing a point on the globe's surface)
|
||||
// As latitude increases, distance from the Earth's north-south axis out to our surface point decreases.
|
||||
// Means we need to first find the hypotenuse which becomes base of our triangle in the second step
|
||||
/*
|
||||
UK North
|
||||
/-------\ (Front View) /-------\
|
||||
(Top View) /- -\ /- -\
|
||||
/- (You) -\ /-(You) -\
|
||||
/- /. -\ /- . -\
|
||||
/- √X²+Y²/ . X -\ /- Z . -\
|
||||
Asia - /... - USA - ..... -
|
||||
\- Y -/ \- √X²+Y² -/
|
||||
\- -/ \- -/
|
||||
\- -/ \- -/
|
||||
\- -/ \- -/
|
||||
\- -----/ \- -----/
|
||||
Pacific South
|
||||
*/
|
||||
|
||||
float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect
|
||||
*lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG;
|
||||
// To find mean values shortly
|
||||
xAvg += x;
|
||||
yAvg += y;
|
||||
zAvg += z;
|
||||
positionCount++;
|
||||
}
|
||||
|
||||
// Use either our node position, or the mean fallback as the center
|
||||
latCenter = *lat;
|
||||
lngCenter = *lng;
|
||||
// All NodeDB processed, find mean values
|
||||
xAvg /= positionCount;
|
||||
yAvg /= positionCount;
|
||||
zAvg /= positionCount;
|
||||
|
||||
// ----------------------------------------------
|
||||
// This has given us either:
|
||||
// - our actual position (preferred), or
|
||||
// - a mean position (fallback if we had no fix)
|
||||
//
|
||||
// What we actually want is to place our center so that our outermost nodes
|
||||
// end up on the border of our map. The only real use of our "center" is to give
|
||||
// us a reference frame: which direction is east, and which is west.
|
||||
//------------------------------------------------
|
||||
// Longitude from cartesian coords
|
||||
// (Angle from 3D coords describing a point of globe's surface)
|
||||
/*
|
||||
UK
|
||||
/-------\
|
||||
(Top View) /- -\
|
||||
/- (You) -\
|
||||
/- . -\
|
||||
/- . X -\
|
||||
Asia - ... - USA
|
||||
\- Y -/
|
||||
\- -/
|
||||
\- -/
|
||||
\- -/
|
||||
\- -----/
|
||||
Pacific
|
||||
|
||||
// Find furthest nodes from our center
|
||||
// ========================================
|
||||
float northernmost = latCenter;
|
||||
float southernmost = latCenter;
|
||||
float easternmost = lngCenter;
|
||||
float westernmost = lngCenter;
|
||||
*/
|
||||
|
||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
*lng = atan2(yAvg, xAvg) * RAD_TO_DEG;
|
||||
|
||||
// Skip if no position
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
continue;
|
||||
// Latitude from cartesian coords
|
||||
// (Angle from 3D coords describing a point on the globe's surface)
|
||||
// As latitude increases, distance from the Earth's north-south axis out to our surface point decreases.
|
||||
// Means we need to first find the hypotenuse which becomes base of our triangle in the second step
|
||||
/*
|
||||
UK North
|
||||
/-------\ (Front View) /-------\
|
||||
(Top View) /- -\ /- -\
|
||||
/- (You) -\ /-(You) -\
|
||||
/- /. -\ /- . -\
|
||||
/- √X²+Y²/ . X -\ /- Z . -\
|
||||
Asia - /... - USA - ..... -
|
||||
\- Y -/ \- √X²+Y² -/
|
||||
\- -/ \- -/
|
||||
\- -/ \- -/
|
||||
\- -/ \- -/
|
||||
\- -----/ \- -----/
|
||||
Pacific South
|
||||
*/
|
||||
|
||||
// Skip if derived applet doesn't want to show this node on the map
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
float hypotenuse = sqrt((xAvg * xAvg) + (yAvg * yAvg)); // Distance from globe's north-south axis to surface intersect
|
||||
*lat = atan2(zAvg, hypotenuse) * RAD_TO_DEG;
|
||||
}
|
||||
|
||||
// Check for a new top or bottom latitude
|
||||
float latNode = node->position.latitude_i * 1e-7;
|
||||
northernmost = max(northernmost, latNode);
|
||||
southernmost = min(southernmost, latNode);
|
||||
// Use either our node position, or the mean fallback as the center
|
||||
latCenter = *lat;
|
||||
lngCenter = *lng;
|
||||
|
||||
// Longitude is trickier
|
||||
float lngNode = node->position.longitude_i * 1e-7;
|
||||
float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node
|
||||
float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node
|
||||
if (degEastward < degWestward)
|
||||
easternmost = max(easternmost, lngCenter + degEastward);
|
||||
else
|
||||
westernmost = min(westernmost, lngCenter - degWestward);
|
||||
}
|
||||
// ----------------------------------------------
|
||||
// This has given us either:
|
||||
// - our actual position (preferred), or
|
||||
// - a mean position (fallback if we had no fix)
|
||||
//
|
||||
// What we actually want is to place our center so that our outermost nodes
|
||||
// end up on the border of our map. The only real use of our "center" is to give
|
||||
// us a reference frame: which direction is east, and which is west.
|
||||
//------------------------------------------------
|
||||
|
||||
// Todo: check for issues with map spans >180 deg. MQTT only..
|
||||
latCenter = (northernmost + southernmost) / 2;
|
||||
lngCenter = (westernmost + easternmost) / 2;
|
||||
// Find furthest nodes from our center
|
||||
// ========================================
|
||||
float northernmost = latCenter;
|
||||
float southernmost = latCenter;
|
||||
float easternmost = lngCenter;
|
||||
float westernmost = lngCenter;
|
||||
|
||||
// In case our new center is west of -180, or east of +180, for some reason
|
||||
lngCenter = fmod(lngCenter, 180);
|
||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
// Skip if no position
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
continue;
|
||||
|
||||
// Skip if derived applet doesn't want to show this node on the map
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
|
||||
// Check for a new top or bottom latitude
|
||||
float latNode = node->position.latitude_i * 1e-7;
|
||||
northernmost = max(northernmost, latNode);
|
||||
southernmost = min(southernmost, latNode);
|
||||
|
||||
// Longitude is trickier
|
||||
float lngNode = node->position.longitude_i * 1e-7;
|
||||
float degEastward = fmod(((lngNode - lngCenter) + 360), 360); // Degrees traveled east from lngCenter to reach node
|
||||
float degWestward = abs(fmod(((lngNode - lngCenter) - 360), 360)); // Degrees traveled west from lngCenter to reach node
|
||||
if (degEastward < degWestward)
|
||||
easternmost = max(easternmost, lngCenter + degEastward);
|
||||
else
|
||||
westernmost = min(westernmost, lngCenter - degWestward);
|
||||
}
|
||||
|
||||
// Todo: check for issues with map spans >180 deg. MQTT only..
|
||||
latCenter = (northernmost + southernmost) / 2;
|
||||
lngCenter = (westernmost + easternmost) / 2;
|
||||
|
||||
// In case our new center is west of -180, or east of +180, for some reason
|
||||
lngCenter = fmod(lngCenter, 180);
|
||||
}
|
||||
|
||||
// Size of map in meters
|
||||
// Grown to fit the nodes furthest from map center
|
||||
// Overridable if derived applet wants a custom map size (fixed size?)
|
||||
void InkHUD::MapApplet::getMapSize(uint32_t *widthMeters, uint32_t *heightMeters)
|
||||
{
|
||||
// Reset the value
|
||||
*widthMeters = 0;
|
||||
*heightMeters = 0;
|
||||
void InkHUD::MapApplet::getMapSize(uint32_t *widthMeters, uint32_t *heightMeters) {
|
||||
// Reset the value
|
||||
*widthMeters = 0;
|
||||
*heightMeters = 0;
|
||||
|
||||
// Find the greatest distance horizontally and vertically from map center
|
||||
for (Marker m : markers) {
|
||||
*widthMeters = max(*widthMeters, (uint32_t)abs(m.eastMeters) * 2);
|
||||
*heightMeters = max(*heightMeters, (uint32_t)abs(m.northMeters) * 2);
|
||||
}
|
||||
// Find the greatest distance horizontally and vertically from map center
|
||||
for (Marker m : markers) {
|
||||
*widthMeters = max(*widthMeters, (uint32_t)abs(m.eastMeters) * 2);
|
||||
*heightMeters = max(*heightMeters, (uint32_t)abs(m.northMeters) * 2);
|
||||
}
|
||||
|
||||
// Add padding
|
||||
*widthMeters *= 1.1;
|
||||
*heightMeters *= 1.1;
|
||||
// Add padding
|
||||
*widthMeters *= 1.1;
|
||||
*heightMeters *= 1.1;
|
||||
}
|
||||
|
||||
// Convert and store info we need for drawing a marker
|
||||
// Lat / long to "meters relative to map center", for position on screen
|
||||
// Info about hopsAway, for marker size
|
||||
InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway)
|
||||
{
|
||||
assert(lat != 0 || lng != 0); // Not null island. Applets should check this before calling.
|
||||
InkHUD::MapApplet::Marker InkHUD::MapApplet::calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway) {
|
||||
assert(lat != 0 || lng != 0); // Not null island. Applets should check this before calling.
|
||||
|
||||
// Bearing and distance from map center to node
|
||||
float distanceFromCenter = GeoCoord::latLongToMeter(latCenter, lngCenter, lat, lng);
|
||||
float bearingFromCenter = GeoCoord::bearing(latCenter, lngCenter, lat, lng); // in radians
|
||||
// Bearing and distance from map center to node
|
||||
float distanceFromCenter = GeoCoord::latLongToMeter(latCenter, lngCenter, lat, lng);
|
||||
float bearingFromCenter = GeoCoord::bearing(latCenter, lngCenter, lat, lng); // in radians
|
||||
|
||||
// Split into meters north and meters east components (signed)
|
||||
// - signedness of cos / sin automatically sets negative if south or west
|
||||
float northMeters = cos(bearingFromCenter) * distanceFromCenter;
|
||||
float eastMeters = sin(bearingFromCenter) * distanceFromCenter;
|
||||
// Split into meters north and meters east components (signed)
|
||||
// - signedness of cos / sin automatically sets negative if south or west
|
||||
float northMeters = cos(bearingFromCenter) * distanceFromCenter;
|
||||
float eastMeters = sin(bearingFromCenter) * distanceFromCenter;
|
||||
|
||||
// Store this as a new marker
|
||||
Marker m;
|
||||
m.eastMeters = eastMeters;
|
||||
m.northMeters = northMeters;
|
||||
m.hasHopsAway = hasHopsAway;
|
||||
m.hopsAway = hopsAway;
|
||||
return m;
|
||||
// Store this as a new marker
|
||||
Marker m;
|
||||
m.eastMeters = eastMeters;
|
||||
m.northMeters = northMeters;
|
||||
m.hasHopsAway = hasHopsAway;
|
||||
m.hopsAway = hopsAway;
|
||||
return m;
|
||||
}
|
||||
// Draw a marker on the map for a node, with a shortname label, and backing box
|
||||
void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node)
|
||||
{
|
||||
// Find x and y position based on node's position in nodeDB
|
||||
assert(nodeDB->hasValidPosition(node));
|
||||
Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
||||
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
|
||||
node->has_hops_away, // Is the hopsAway number valid
|
||||
node->hops_away // Hops away
|
||||
);
|
||||
void InkHUD::MapApplet::drawLabeledMarker(meshtastic_NodeInfoLite *node) {
|
||||
// Find x and y position based on node's position in nodeDB
|
||||
assert(nodeDB->hasValidPosition(node));
|
||||
Marker m = calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
||||
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
|
||||
node->has_hops_away, // Is the hopsAway number valid
|
||||
node->hops_away // Hops away
|
||||
);
|
||||
|
||||
// Convert to pixel coords
|
||||
int16_t markerX = X(0.5) + (m.eastMeters * metersToPx);
|
||||
int16_t markerY = Y(0.5) - (m.northMeters * metersToPx);
|
||||
// Convert to pixel coords
|
||||
int16_t markerX = X(0.5) + (m.eastMeters * metersToPx);
|
||||
int16_t markerY = Y(0.5) - (m.northMeters * metersToPx);
|
||||
|
||||
constexpr uint16_t paddingH = 2;
|
||||
constexpr uint16_t paddingW = 4;
|
||||
uint16_t paddingInnerW = 2; // Zero'd out if no text
|
||||
constexpr uint16_t markerSizeMax = 12; // Size of cross (if marker uses a cross)
|
||||
constexpr uint16_t markerSizeMin = 5;
|
||||
constexpr uint16_t paddingH = 2;
|
||||
constexpr uint16_t paddingW = 4;
|
||||
uint16_t paddingInnerW = 2; // Zero'd out if no text
|
||||
constexpr uint16_t markerSizeMax = 12; // Size of cross (if marker uses a cross)
|
||||
constexpr uint16_t markerSizeMin = 5;
|
||||
|
||||
int16_t textX;
|
||||
int16_t textY;
|
||||
uint16_t textW;
|
||||
uint16_t textH;
|
||||
int16_t labelX;
|
||||
int16_t labelY;
|
||||
uint16_t labelW;
|
||||
uint16_t labelH;
|
||||
uint8_t markerSize;
|
||||
int16_t textX;
|
||||
int16_t textY;
|
||||
uint16_t textW;
|
||||
uint16_t textH;
|
||||
int16_t labelX;
|
||||
int16_t labelY;
|
||||
uint16_t labelW;
|
||||
uint16_t labelH;
|
||||
uint8_t markerSize;
|
||||
|
||||
bool tooManyHops = node->hops_away > config.lora.hop_limit;
|
||||
bool isOurNode = node->num == nodeDB->getNodeNum();
|
||||
bool unknownHops = !node->has_hops_away && !isOurNode;
|
||||
bool tooManyHops = node->hops_away > config.lora.hop_limit;
|
||||
bool isOurNode = node->num == nodeDB->getNodeNum();
|
||||
bool unknownHops = !node->has_hops_away && !isOurNode;
|
||||
|
||||
// Parse any non-ascii chars in the short name,
|
||||
// and use last 4 instead if unknown / can't render
|
||||
std::string shortName = parseShortName(node);
|
||||
// Parse any non-ascii chars in the short name,
|
||||
// and use last 4 instead if unknown / can't render
|
||||
std::string shortName = parseShortName(node);
|
||||
|
||||
// We will draw a left or right hand variant, to place text towards screen center
|
||||
// Hopefully avoid text spilling off screen
|
||||
// Most values are the same, regardless of left-right handedness
|
||||
// We will draw a left or right hand variant, to place text towards screen center
|
||||
// Hopefully avoid text spilling off screen
|
||||
// Most values are the same, regardless of left-right handedness
|
||||
|
||||
// Pick emblem style
|
||||
if (tooManyHops)
|
||||
markerSize = getTextWidth("!");
|
||||
else if (unknownHops)
|
||||
markerSize = markerSizeMin;
|
||||
else
|
||||
markerSize = map(node->hops_away, 0, config.lora.hop_limit, markerSizeMax, markerSizeMin);
|
||||
// Pick emblem style
|
||||
if (tooManyHops)
|
||||
markerSize = getTextWidth("!");
|
||||
else if (unknownHops)
|
||||
markerSize = markerSizeMin;
|
||||
else
|
||||
markerSize = map(node->hops_away, 0, config.lora.hop_limit, markerSizeMax, markerSizeMin);
|
||||
|
||||
// Common dimensions (left or right variant)
|
||||
textW = getTextWidth(shortName);
|
||||
if (textW == 0)
|
||||
paddingInnerW = 0; // If no text, no padding for text
|
||||
textH = fontSmall.lineHeight();
|
||||
labelH = paddingH + max((int16_t)(textH), (int16_t)markerSize) + paddingH;
|
||||
labelY = markerY - (labelH / 2);
|
||||
textY = markerY;
|
||||
labelW = paddingW + markerSize + paddingInnerW + textW + paddingW; // Width is same whether right or left hand variant
|
||||
// Common dimensions (left or right variant)
|
||||
textW = getTextWidth(shortName);
|
||||
if (textW == 0)
|
||||
paddingInnerW = 0; // If no text, no padding for text
|
||||
textH = fontSmall.lineHeight();
|
||||
labelH = paddingH + max((int16_t)(textH), (int16_t)markerSize) + paddingH;
|
||||
labelY = markerY - (labelH / 2);
|
||||
textY = markerY;
|
||||
labelW = paddingW + markerSize + paddingInnerW + textW + paddingW; // Width is same whether right or left hand variant
|
||||
|
||||
// Left-side variant
|
||||
if (markerX < width() / 2) {
|
||||
labelX = markerX - (markerSize / 2) - paddingW;
|
||||
textX = labelX + paddingW + markerSize + paddingInnerW;
|
||||
}
|
||||
// Left-side variant
|
||||
if (markerX < width() / 2) {
|
||||
labelX = markerX - (markerSize / 2) - paddingW;
|
||||
textX = labelX + paddingW + markerSize + paddingInnerW;
|
||||
}
|
||||
|
||||
// Right-side variant
|
||||
else {
|
||||
labelX = markerX - (markerSize / 2) - paddingInnerW - textW - paddingW;
|
||||
textX = labelX + paddingW;
|
||||
}
|
||||
// Right-side variant
|
||||
else {
|
||||
labelX = markerX - (markerSize / 2) - paddingInnerW - textW - paddingW;
|
||||
textX = labelX + paddingW;
|
||||
}
|
||||
|
||||
// Prevent overlap with scale bars and their labels
|
||||
// Define a "safe zone" in the bottom-left where the scale bars and text are drawn
|
||||
constexpr int16_t safeZoneHeight = 28; // adjust based on your label font height
|
||||
constexpr int16_t safeZoneWidth = 60; // adjust based on horizontal label width zone
|
||||
bool overlapsScale = (labelY + labelH > height() - safeZoneHeight) && (labelX < safeZoneWidth);
|
||||
// Prevent overlap with scale bars and their labels
|
||||
// Define a "safe zone" in the bottom-left where the scale bars and text are drawn
|
||||
constexpr int16_t safeZoneHeight = 28; // adjust based on your label font height
|
||||
constexpr int16_t safeZoneWidth = 60; // adjust based on horizontal label width zone
|
||||
bool overlapsScale = (labelY + labelH > height() - safeZoneHeight) && (labelX < safeZoneWidth);
|
||||
|
||||
// If it overlaps, shift label upward slightly above the safe zone
|
||||
if (overlapsScale) {
|
||||
labelY = height() - safeZoneHeight - labelH - 2;
|
||||
textY = labelY + (labelH / 2);
|
||||
}
|
||||
// If it overlaps, shift label upward slightly above the safe zone
|
||||
if (overlapsScale) {
|
||||
labelY = height() - safeZoneHeight - labelH - 2;
|
||||
textY = labelY + (labelH / 2);
|
||||
}
|
||||
|
||||
// Backing box
|
||||
fillRect(labelX, labelY, labelW, labelH, WHITE);
|
||||
drawRect(labelX, labelY, labelW, labelH, BLACK);
|
||||
// Backing box
|
||||
fillRect(labelX, labelY, labelW, labelH, WHITE);
|
||||
drawRect(labelX, labelY, labelW, labelH, BLACK);
|
||||
|
||||
// Short name
|
||||
printAt(textX, textY, shortName, LEFT, MIDDLE);
|
||||
// Short name
|
||||
printAt(textX, textY, shortName, LEFT, MIDDLE);
|
||||
|
||||
// If the label is for our own node,
|
||||
// fade it by overdrawing partially with white
|
||||
if (node == nodeDB->getMeshNode(nodeDB->getNodeNum()))
|
||||
hatchRegion(labelX, labelY, labelW, labelH, 2, WHITE);
|
||||
// If the label is for our own node,
|
||||
// fade it by overdrawing partially with white
|
||||
if (node == nodeDB->getMeshNode(nodeDB->getNodeNum()))
|
||||
hatchRegion(labelX, labelY, labelW, labelH, 2, WHITE);
|
||||
|
||||
// Draw the marker emblem
|
||||
// - after the fading, because hatching (own node) can align with cross and make it look weird
|
||||
if (tooManyHops)
|
||||
printAt(markerX, markerY, "!", CENTER, MIDDLE);
|
||||
else
|
||||
drawCross(markerX, markerY, markerSize); // The fewer the hops, the larger the marker. Also handles unknownHops
|
||||
// Draw the marker emblem
|
||||
// - after the fading, because hatching (own node) can align with cross and make it look weird
|
||||
if (tooManyHops)
|
||||
printAt(markerX, markerY, "!", CENTER, MIDDLE);
|
||||
else
|
||||
drawCross(markerX, markerY, markerSize); // The fewer the hops, the larger the marker. Also handles unknownHops
|
||||
}
|
||||
|
||||
// Check if we actually have enough nodes which would be shown on the map
|
||||
// Need at least two, to draw a sensible map
|
||||
bool InkHUD::MapApplet::enoughMarkers()
|
||||
{
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
bool InkHUD::MapApplet::enoughMarkers() {
|
||||
size_t count = 0;
|
||||
for (size_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
// Count nodes
|
||||
if (nodeDB->hasValidPosition(node) && shouldDrawNode(node))
|
||||
count++;
|
||||
// Count nodes
|
||||
if (nodeDB->hasValidPosition(node) && shouldDrawNode(node))
|
||||
count++;
|
||||
|
||||
// We need to find two
|
||||
if (count == 2)
|
||||
return true; // Two nodes is enough for a sensible map
|
||||
}
|
||||
// We need to find two
|
||||
if (count == 2)
|
||||
return true; // Two nodes is enough for a sensible map
|
||||
}
|
||||
|
||||
return false; // No nodes would be drawn (or just the one, uselessly at 0,0)
|
||||
return false; // No nodes would be drawn (or just the one, uselessly at 0,0)
|
||||
}
|
||||
|
||||
// Calculate how far north and east of map center each node is
|
||||
// Derived applets can control which nodes to calculate (and later, draw) by overriding MapApplet::shouldDrawNode
|
||||
void InkHUD::MapApplet::calculateAllMarkers()
|
||||
{
|
||||
// Clear old markers
|
||||
markers.clear();
|
||||
void InkHUD::MapApplet::calculateAllMarkers() {
|
||||
// Clear old markers
|
||||
markers.clear();
|
||||
|
||||
// For each node in db
|
||||
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
// For each node in db
|
||||
for (uint32_t i = 0; i < nodeDB->getNumMeshNodes(); i++) {
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNodeByIndex(i);
|
||||
|
||||
// Skip if no position
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
continue;
|
||||
// Skip if no position
|
||||
if (!nodeDB->hasValidPosition(node))
|
||||
continue;
|
||||
|
||||
// Skip if derived applet doesn't want to show this node on the map
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
// Skip if derived applet doesn't want to show this node on the map
|
||||
if (!shouldDrawNode(node))
|
||||
continue;
|
||||
|
||||
// Skip if our own node
|
||||
// - special handling in render()
|
||||
if (node->num == nodeDB->getNodeNum())
|
||||
continue;
|
||||
// Skip if our own node
|
||||
// - special handling in render()
|
||||
if (node->num == nodeDB->getNodeNum())
|
||||
continue;
|
||||
|
||||
// Calculate marker and store it
|
||||
markers.push_back(
|
||||
calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
||||
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
|
||||
node->has_hops_away, // Is the hopsAway number valid
|
||||
node->hops_away // Hops away
|
||||
));
|
||||
}
|
||||
// Calculate marker and store it
|
||||
markers.push_back(calculateMarker(node->position.latitude_i * 1e-7, // Lat, converted from Meshtastic's internal int32 style
|
||||
node->position.longitude_i * 1e-7, // Long, converted from Meshtastic's internal int32 style
|
||||
node->has_hops_away, // Is the hopsAway number valid
|
||||
node->hops_away // Hops away
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the conversion factor between metres, and pixels on screen
|
||||
// May be overriden by derived applet, if custom scale required (fixed map size?)
|
||||
void InkHUD::MapApplet::calculateMapScale()
|
||||
{
|
||||
// Aspect ratio of map and screen
|
||||
// - larger = wide, smaller = tall
|
||||
// - used to set scale, so that widest map dimension fits in applet
|
||||
float mapAspectRatio = (float)widthMeters / heightMeters;
|
||||
float appletAspectRatio = (float)width() / height();
|
||||
void InkHUD::MapApplet::calculateMapScale() {
|
||||
// Aspect ratio of map and screen
|
||||
// - larger = wide, smaller = tall
|
||||
// - used to set scale, so that widest map dimension fits in applet
|
||||
float mapAspectRatio = (float)widthMeters / heightMeters;
|
||||
float appletAspectRatio = (float)width() / height();
|
||||
|
||||
// "Shrink to fit"
|
||||
// Scale the map so that the largest dimension is fully displayed
|
||||
// Because aspect ratio will be maintained, the other dimension will appear "padded"
|
||||
if (mapAspectRatio > appletAspectRatio)
|
||||
metersToPx = (float)width() / widthMeters; // Too wide for applet. Constrain to fit width.
|
||||
else
|
||||
metersToPx = (float)height() / heightMeters; // Too tall for applet. Constrain to fit height.
|
||||
// "Shrink to fit"
|
||||
// Scale the map so that the largest dimension is fully displayed
|
||||
// Because aspect ratio will be maintained, the other dimension will appear "padded"
|
||||
if (mapAspectRatio > appletAspectRatio)
|
||||
metersToPx = (float)width() / widthMeters; // Too wide for applet. Constrain to fit width.
|
||||
else
|
||||
metersToPx = (float)height() / heightMeters; // Too tall for applet. Constrain to fit height.
|
||||
}
|
||||
|
||||
// Draw an x, centered on a specific point
|
||||
// Most markers will draw with this method
|
||||
void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size)
|
||||
{
|
||||
int16_t x0 = x - (size / 2);
|
||||
int16_t y0 = y - (size / 2);
|
||||
int16_t x1 = x0 + size - 1;
|
||||
int16_t y1 = y0 + size - 1;
|
||||
drawLine(x0, y0, x1, y1, BLACK);
|
||||
drawLine(x0, y1, x1, y0, BLACK);
|
||||
void InkHUD::MapApplet::drawCross(int16_t x, int16_t y, uint8_t size) {
|
||||
int16_t x0 = x - (size / 2);
|
||||
int16_t y0 = y - (size / 2);
|
||||
int16_t x1 = x0 + size - 1;
|
||||
int16_t y1 = y0 + size - 1;
|
||||
drawLine(x0, y0, x1, y1, BLACK);
|
||||
drawLine(x0, y1, x1, y0, BLACK);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -21,43 +21,41 @@ The base applet doesn't handle any events; this is left to the derived applets.
|
||||
#include "MeshModule.h"
|
||||
#include "gps/GeoCoord.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class MapApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
class MapApplet : public Applet {
|
||||
public:
|
||||
void onRender() override;
|
||||
|
||||
protected:
|
||||
virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes
|
||||
virtual void getMapCenter(float *lat, float *lng);
|
||||
virtual void getMapSize(uint32_t *widthMeters, uint32_t *heightMeters);
|
||||
protected:
|
||||
virtual bool shouldDrawNode(meshtastic_NodeInfoLite *node) { return true; } // Allow derived applets to filter the nodes
|
||||
virtual void getMapCenter(float *lat, float *lng);
|
||||
virtual void getMapSize(uint32_t *widthMeters, uint32_t *heightMeters);
|
||||
|
||||
bool enoughMarkers(); // Anything to draw?
|
||||
void drawLabeledMarker(meshtastic_NodeInfoLite *node); // Highlight a specific marker
|
||||
bool enoughMarkers(); // Anything to draw?
|
||||
void drawLabeledMarker(meshtastic_NodeInfoLite *node); // Highlight a specific marker
|
||||
|
||||
private:
|
||||
// Position and size of a marker to be drawn
|
||||
struct Marker {
|
||||
float eastMeters = 0; // Meters east of map center. Negative if west.
|
||||
float northMeters = 0; // Meters north of map center. Negative if south.
|
||||
bool hasHopsAway = false;
|
||||
uint8_t hopsAway = 0; // Determines marker size
|
||||
};
|
||||
private:
|
||||
// Position and size of a marker to be drawn
|
||||
struct Marker {
|
||||
float eastMeters = 0; // Meters east of map center. Negative if west.
|
||||
float northMeters = 0; // Meters north of map center. Negative if south.
|
||||
bool hasHopsAway = false;
|
||||
uint8_t hopsAway = 0; // Determines marker size
|
||||
};
|
||||
|
||||
Marker calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway);
|
||||
void calculateAllMarkers();
|
||||
void calculateMapScale(); // Conversion factor for meters to pixels
|
||||
void drawCross(int16_t x, int16_t y, uint8_t size); // Draw the X used for most markers
|
||||
Marker calculateMarker(float lat, float lng, bool hasHopsAway, uint8_t hopsAway);
|
||||
void calculateAllMarkers();
|
||||
void calculateMapScale(); // Conversion factor for meters to pixels
|
||||
void drawCross(int16_t x, int16_t y, uint8_t size); // Draw the X used for most markers
|
||||
|
||||
float metersToPx = 0; // Conversion factor for meters to pixels
|
||||
float latCenter = 0; // Map center: latitude
|
||||
float lngCenter = 0; // Map center: longitude
|
||||
float metersToPx = 0; // Conversion factor for meters to pixels
|
||||
float latCenter = 0; // Map center: latitude
|
||||
float lngCenter = 0; // Map center: longitude
|
||||
|
||||
std::list<Marker> markers;
|
||||
uint32_t widthMeters = 0; // Map width: meters
|
||||
uint32_t heightMeters = 0; // Map height: meters
|
||||
std::list<Marker> markers;
|
||||
uint32_t widthMeters = 0; // Map width: meters
|
||||
uint32_t heightMeters = 0; // Map height: meters
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -9,283 +9,276 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::NodeListApplet::NodeListApplet(const char *name) : MeshModule(name)
|
||||
{
|
||||
// We only need to be promiscuous in order to hear NodeInfo, apparently. See NodeInfoModule
|
||||
// For all other packets, we manually act as if isPromiscuous=false, in wantPacket
|
||||
MeshModule::isPromiscuous = true;
|
||||
InkHUD::NodeListApplet::NodeListApplet(const char *name) : MeshModule(name) {
|
||||
// We only need to be promiscuous in order to hear NodeInfo, apparently. See NodeInfoModule
|
||||
// For all other packets, we manually act as if isPromiscuous=false, in wantPacket
|
||||
MeshModule::isPromiscuous = true;
|
||||
}
|
||||
|
||||
// Do we want to process this packet with handleReceived()?
|
||||
bool InkHUD::NodeListApplet::wantPacket(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
// Only interested if:
|
||||
return isActive() // Applet is active
|
||||
&& !isFromUs(p) // Packet is incoming (not outgoing)
|
||||
&& (isToUs(p) || isBroadcast(p->to) || // Either: intended for us,
|
||||
p->decoded.portnum == meshtastic_PortNum_NODEINFO_APP); // or nodeinfo
|
||||
bool InkHUD::NodeListApplet::wantPacket(const meshtastic_MeshPacket *p) {
|
||||
// Only interested if:
|
||||
return isActive() // Applet is active
|
||||
&& !isFromUs(p) // Packet is incoming (not outgoing)
|
||||
&& (isToUs(p) || isBroadcast(p->to) || // Either: intended for us,
|
||||
p->decoded.portnum == meshtastic_PortNum_NODEINFO_APP); // or nodeinfo
|
||||
|
||||
// To match the behavior seen in the client apps:
|
||||
// - NodeInfoModule's ProtoBufModule base is "promiscuous"
|
||||
// - All other activity is *not* promiscuous
|
||||
// To match the behavior seen in the client apps:
|
||||
// - NodeInfoModule's ProtoBufModule base is "promiscuous"
|
||||
// - All other activity is *not* promiscuous
|
||||
|
||||
// To achieve this, our MeshModule *is* promiscuous, and we're manually reimplementing non-promiscuous behavior here,
|
||||
// to match the code in MeshModule::callModules
|
||||
// To achieve this, our MeshModule *is* promiscuous, and we're manually reimplementing non-promiscuous behavior here,
|
||||
// to match the code in MeshModule::callModules
|
||||
}
|
||||
|
||||
// MeshModule packets arrive here
|
||||
// Extract the info and pass it to the derived applet
|
||||
// Derived applet will store the CardInfo, and perform any required sorting of the CardInfo collection
|
||||
// Derived applet might also need to keep other tallies (active nodes count?)
|
||||
ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
// Abort if applet fully deactivated
|
||||
// Already handled by wantPacket in this case, but good practice for all applets, as some *do* require this early return
|
||||
if (!isActive())
|
||||
return ProcessMessage::CONTINUE;
|
||||
ProcessMessage InkHUD::NodeListApplet::handleReceived(const meshtastic_MeshPacket &mp) {
|
||||
// Abort if applet fully deactivated
|
||||
// Already handled by wantPacket in this case, but good practice for all applets, as some *do* require this early
|
||||
// return
|
||||
if (!isActive())
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Assemble info: from this event
|
||||
CardInfo c;
|
||||
c.nodeNum = mp.from;
|
||||
c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi);
|
||||
// Assemble info: from this event
|
||||
CardInfo c;
|
||||
c.nodeNum = mp.from;
|
||||
c.signal = getSignalStrength(mp.rx_snr, mp.rx_rssi);
|
||||
|
||||
// Assemble info: from nodeDB (needed to detect changes)
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum);
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (node) {
|
||||
if (node->has_hops_away)
|
||||
c.hopsAway = node->hops_away;
|
||||
// Assemble info: from nodeDB (needed to detect changes)
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(c.nodeNum);
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
if (node) {
|
||||
if (node->has_hops_away)
|
||||
c.hopsAway = node->hops_away;
|
||||
|
||||
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
|
||||
// Get lat and long as float
|
||||
// Meshtastic stores these as integers internally
|
||||
float ourLat = ourNode->position.latitude_i * 1e-7;
|
||||
float ourLong = ourNode->position.longitude_i * 1e-7;
|
||||
float theirLat = node->position.latitude_i * 1e-7;
|
||||
float theirLong = node->position.longitude_i * 1e-7;
|
||||
if (nodeDB->hasValidPosition(node) && nodeDB->hasValidPosition(ourNode)) {
|
||||
// Get lat and long as float
|
||||
// Meshtastic stores these as integers internally
|
||||
float ourLat = ourNode->position.latitude_i * 1e-7;
|
||||
float ourLong = ourNode->position.longitude_i * 1e-7;
|
||||
float theirLat = node->position.latitude_i * 1e-7;
|
||||
float theirLong = node->position.longitude_i * 1e-7;
|
||||
|
||||
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
||||
}
|
||||
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
||||
}
|
||||
}
|
||||
|
||||
// Pass to the derived applet
|
||||
// Derived applet is responsible for requesting update, if justified
|
||||
// That request will eventually trigger our class' onRender method
|
||||
handleParsed(c);
|
||||
// Pass to the derived applet
|
||||
// Derived applet is responsible for requesting update, if justified
|
||||
// That request will eventually trigger our class' onRender method
|
||||
handleParsed(c);
|
||||
|
||||
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||
return ProcessMessage::CONTINUE; // Let others look at this message also if they want
|
||||
}
|
||||
|
||||
// Calculate maximum number of cards we may ever need to render, in our tallest layout config
|
||||
// Number might be slightly in excess of the true value: applet header text not accounted for
|
||||
uint8_t InkHUD::NodeListApplet::maxCards()
|
||||
{
|
||||
// Cache result. Shouldn't change during execution
|
||||
static uint8_t cards = 0;
|
||||
uint8_t InkHUD::NodeListApplet::maxCards() {
|
||||
// Cache result. Shouldn't change during execution
|
||||
static uint8_t cards = 0;
|
||||
|
||||
if (!cards) {
|
||||
const uint16_t height = Tile::maxDisplayDimension();
|
||||
if (!cards) {
|
||||
const uint16_t height = Tile::maxDisplayDimension();
|
||||
|
||||
// Use a loop instead of arithmetic, because it's easier for my brain to follow
|
||||
// Add cards one by one, until the latest card extends below screen
|
||||
// Use a loop instead of arithmetic, because it's easier for my brain to follow
|
||||
// Add cards one by one, until the latest card extends below screen
|
||||
|
||||
uint16_t y = cardH; // First card: no margin above
|
||||
cards = 1;
|
||||
uint16_t y = cardH; // First card: no margin above
|
||||
cards = 1;
|
||||
|
||||
while (y < height) {
|
||||
y += cardMarginH;
|
||||
y += cardH;
|
||||
cards++;
|
||||
}
|
||||
while (y < height) {
|
||||
y += cardMarginH;
|
||||
y += cardH;
|
||||
cards++;
|
||||
}
|
||||
}
|
||||
|
||||
return cards;
|
||||
return cards;
|
||||
}
|
||||
|
||||
// Draw, using info which derived applet placed into NodeListApplet::cards for us
|
||||
void InkHUD::NodeListApplet::onRender()
|
||||
{
|
||||
void InkHUD::NodeListApplet::onRender() {
|
||||
|
||||
// ================================
|
||||
// Draw the standard applet header
|
||||
// ================================
|
||||
// ================================
|
||||
// Draw the standard applet header
|
||||
// ================================
|
||||
|
||||
drawHeader(getHeaderText()); // Ask derived applet for the title
|
||||
drawHeader(getHeaderText()); // Ask derived applet for the title
|
||||
|
||||
// Dimensions of the header
|
||||
int16_t headerDivY = getHeaderHeight() - 1;
|
||||
constexpr uint16_t padDivH = 2;
|
||||
// Dimensions of the header
|
||||
int16_t headerDivY = getHeaderHeight() - 1;
|
||||
constexpr uint16_t padDivH = 2;
|
||||
|
||||
// ========================
|
||||
// Draw the main node list
|
||||
// ========================
|
||||
// ========================
|
||||
// Draw the main node list
|
||||
// ========================
|
||||
|
||||
// Imaginary vertical line dividing left-side and right-side info
|
||||
// Long-name will crop here
|
||||
const uint16_t dividerX = (width() - 1) - getTextWidth("X Hops");
|
||||
// Imaginary vertical line dividing left-side and right-side info
|
||||
// Long-name will crop here
|
||||
const uint16_t dividerX = (width() - 1) - getTextWidth("X Hops");
|
||||
|
||||
// Y value (top) of the current card. Increases as we draw.
|
||||
uint16_t cardTopY = headerDivY + padDivH;
|
||||
// Y value (top) of the current card. Increases as we draw.
|
||||
uint16_t cardTopY = headerDivY + padDivH;
|
||||
|
||||
// Clean up deleted nodes before drawing
|
||||
cards.erase(
|
||||
std::remove_if(cards.begin(), cards.end(), [](const CardInfo &c) { return nodeDB->getMeshNode(c.nodeNum) == nullptr; }),
|
||||
cards.end());
|
||||
// Clean up deleted nodes before drawing
|
||||
cards.erase(std::remove_if(cards.begin(), cards.end(), [](const CardInfo &c) { return nodeDB->getMeshNode(c.nodeNum) == nullptr; }), cards.end());
|
||||
|
||||
// -- Each node in list --
|
||||
for (auto card = cards.begin(); card != cards.end(); ++card) {
|
||||
// -- Each node in list --
|
||||
for (auto card = cards.begin(); card != cards.end(); ++card) {
|
||||
|
||||
// Gather info
|
||||
// ========================================
|
||||
NodeNum &nodeNum = card->nodeNum;
|
||||
SignalStrength &signal = card->signal;
|
||||
std::string longName; // handled below
|
||||
std::string shortName; // handled below
|
||||
std::string distance; // handled below;
|
||||
uint8_t &hopsAway = card->hopsAway;
|
||||
// Gather info
|
||||
// ========================================
|
||||
NodeNum &nodeNum = card->nodeNum;
|
||||
SignalStrength &signal = card->signal;
|
||||
std::string longName; // handled below
|
||||
std::string shortName; // handled below
|
||||
std::string distance; // handled below;
|
||||
uint8_t &hopsAway = card->hopsAway;
|
||||
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(nodeNum);
|
||||
|
||||
// Skip deleted nodes
|
||||
if (!node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// -- Shortname --
|
||||
// Parse special chars in the short name
|
||||
// Use "?" if unknown
|
||||
if (node)
|
||||
shortName = parseShortName(node);
|
||||
else
|
||||
shortName = "?";
|
||||
|
||||
// -- Longname --
|
||||
// Parse special chars in long name
|
||||
// Use node id if unknown
|
||||
if (node && node->has_user)
|
||||
longName = parse(node->user.long_name); // Found in nodeDB
|
||||
else {
|
||||
// Not found in nodeDB, show a hex nodeid instead
|
||||
longName = hexifyNodeNum(nodeNum);
|
||||
}
|
||||
|
||||
// -- Distance --
|
||||
if (card->distanceMeters != CardInfo::DISTANCE_UNKNOWN)
|
||||
distance = localizeDistance(card->distanceMeters);
|
||||
|
||||
// Draw the info
|
||||
// ====================================
|
||||
|
||||
// Define two lines of text for the card
|
||||
// We will center our text on these lines
|
||||
uint16_t lineAY = cardTopY + (fontMedium.lineHeight() / 2);
|
||||
uint16_t lineBY = cardTopY + fontMedium.lineHeight() + (fontSmall.lineHeight() / 2);
|
||||
|
||||
// Print the short name
|
||||
setFont(fontMedium);
|
||||
printAt(0, lineAY, shortName, LEFT, MIDDLE);
|
||||
|
||||
// Print the distance
|
||||
setFont(fontSmall);
|
||||
printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE);
|
||||
|
||||
// If we have a direct connection to the node, draw the signal indicator
|
||||
if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) {
|
||||
uint16_t signalW = getTextWidth("Xkm"); // Indicator should be similar width to distance label
|
||||
uint16_t signalH = fontMedium.lineHeight() * 0.75;
|
||||
int16_t signalY = lineAY + (fontMedium.lineHeight() / 2) - (fontMedium.lineHeight() * 0.75);
|
||||
int16_t signalX = width() - signalW;
|
||||
drawSignalIndicator(signalX, signalY, signalW, signalH, signal);
|
||||
}
|
||||
// Otherwise, print "hops away" info, if available
|
||||
else if (hopsAway != CardInfo::HOPS_UNKNOWN && node) {
|
||||
std::string hopString = to_string(node->hops_away);
|
||||
hopString += " Hop";
|
||||
if (node->hops_away != 1)
|
||||
hopString += "s"; // Append s for "Hops", rather than "Hop"
|
||||
|
||||
printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE);
|
||||
}
|
||||
|
||||
// Print the long name, cropping to prevent overflow onto the right-side info
|
||||
setCrop(0, 0, dividerX - 1, height());
|
||||
printAt(0, lineBY, longName, LEFT, MIDDLE);
|
||||
|
||||
// GFX effect: "hatch" the right edge of longName area
|
||||
// If a longName has been cropped, it will appear to fade out,
|
||||
// creating a soft barrier with the right-side info
|
||||
const int16_t hatchLeft = dividerX - 1 - (fontSmall.lineHeight());
|
||||
const int16_t hatchWidth = fontSmall.lineHeight();
|
||||
hatchRegion(hatchLeft, cardTopY, hatchWidth, cardH, 2, WHITE);
|
||||
|
||||
// Prepare to draw the next card
|
||||
resetCrop();
|
||||
cardTopY += cardH;
|
||||
|
||||
// Once we've run out of screen, stop drawing cards
|
||||
// Depending on tiles / rotation, this may be before we hit maxCards
|
||||
if (cardTopY > height())
|
||||
break;
|
||||
// Skip deleted nodes
|
||||
if (!node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// -- Shortname --
|
||||
// Parse special chars in the short name
|
||||
// Use "?" if unknown
|
||||
if (node)
|
||||
shortName = parseShortName(node);
|
||||
else
|
||||
shortName = "?";
|
||||
|
||||
// -- Longname --
|
||||
// Parse special chars in long name
|
||||
// Use node id if unknown
|
||||
if (node && node->has_user)
|
||||
longName = parse(node->user.long_name); // Found in nodeDB
|
||||
else {
|
||||
// Not found in nodeDB, show a hex nodeid instead
|
||||
longName = hexifyNodeNum(nodeNum);
|
||||
}
|
||||
|
||||
// -- Distance --
|
||||
if (card->distanceMeters != CardInfo::DISTANCE_UNKNOWN)
|
||||
distance = localizeDistance(card->distanceMeters);
|
||||
|
||||
// Draw the info
|
||||
// ====================================
|
||||
|
||||
// Define two lines of text for the card
|
||||
// We will center our text on these lines
|
||||
uint16_t lineAY = cardTopY + (fontMedium.lineHeight() / 2);
|
||||
uint16_t lineBY = cardTopY + fontMedium.lineHeight() + (fontSmall.lineHeight() / 2);
|
||||
|
||||
// Print the short name
|
||||
setFont(fontMedium);
|
||||
printAt(0, lineAY, shortName, LEFT, MIDDLE);
|
||||
|
||||
// Print the distance
|
||||
setFont(fontSmall);
|
||||
printAt(width() - 1, lineBY, distance, RIGHT, MIDDLE);
|
||||
|
||||
// If we have a direct connection to the node, draw the signal indicator
|
||||
if (hopsAway == 0 && signal != SIGNAL_UNKNOWN) {
|
||||
uint16_t signalW = getTextWidth("Xkm"); // Indicator should be similar width to distance label
|
||||
uint16_t signalH = fontMedium.lineHeight() * 0.75;
|
||||
int16_t signalY = lineAY + (fontMedium.lineHeight() / 2) - (fontMedium.lineHeight() * 0.75);
|
||||
int16_t signalX = width() - signalW;
|
||||
drawSignalIndicator(signalX, signalY, signalW, signalH, signal);
|
||||
}
|
||||
// Otherwise, print "hops away" info, if available
|
||||
else if (hopsAway != CardInfo::HOPS_UNKNOWN && node) {
|
||||
std::string hopString = to_string(node->hops_away);
|
||||
hopString += " Hop";
|
||||
if (node->hops_away != 1)
|
||||
hopString += "s"; // Append s for "Hops", rather than "Hop"
|
||||
|
||||
printAt(width() - 1, lineAY, hopString, RIGHT, MIDDLE);
|
||||
}
|
||||
|
||||
// Print the long name, cropping to prevent overflow onto the right-side info
|
||||
setCrop(0, 0, dividerX - 1, height());
|
||||
printAt(0, lineBY, longName, LEFT, MIDDLE);
|
||||
|
||||
// GFX effect: "hatch" the right edge of longName area
|
||||
// If a longName has been cropped, it will appear to fade out,
|
||||
// creating a soft barrier with the right-side info
|
||||
const int16_t hatchLeft = dividerX - 1 - (fontSmall.lineHeight());
|
||||
const int16_t hatchWidth = fontSmall.lineHeight();
|
||||
hatchRegion(hatchLeft, cardTopY, hatchWidth, cardH, 2, WHITE);
|
||||
|
||||
// Prepare to draw the next card
|
||||
resetCrop();
|
||||
cardTopY += cardH;
|
||||
|
||||
// Once we've run out of screen, stop drawing cards
|
||||
// Depending on tiles / rotation, this may be before we hit maxCards
|
||||
if (cardTopY > height())
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw element: a "mobile phone" style signal indicator
|
||||
// We will calculate values as floats, then "rasterize" at the last moment, relative to x and w, etc
|
||||
// This prevents issues with premature rounding when rendering tiny elements
|
||||
void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength strength)
|
||||
{
|
||||
void InkHUD::NodeListApplet::drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h, SignalStrength strength) {
|
||||
|
||||
/*
|
||||
+-------------------------------------------+
|
||||
| |
|
||||
| |
|
||||
| barHeightRelative=1.0
|
||||
| +--+ ^ |
|
||||
| gutterW +--+ | | | |
|
||||
| <--> +--+ | | | | | |
|
||||
| +--+ | | | | | | | |
|
||||
| | | | | | | | | | |
|
||||
| <-> +--+ +--+ +--+ +--+ v |
|
||||
| paddingW ^ |
|
||||
| paddingH | |
|
||||
| v |
|
||||
+-------------------------------------------+
|
||||
*/
|
||||
/*
|
||||
+-------------------------------------------+
|
||||
| |
|
||||
| |
|
||||
| barHeightRelative=1.0
|
||||
| +--+ ^ |
|
||||
| gutterW +--+ | | | |
|
||||
| <--> +--+ | | | | | |
|
||||
| +--+ | | | | | | | |
|
||||
| | | | | | | | | | |
|
||||
| <-> +--+ +--+ +--+ +--+ v |
|
||||
| paddingW ^ |
|
||||
| paddingH | |
|
||||
| v |
|
||||
+-------------------------------------------+
|
||||
*/
|
||||
|
||||
constexpr float paddingW = 0.1; // Either side
|
||||
constexpr float paddingH = 0.1; // Above and below
|
||||
constexpr float gutterW = 0.1; // Between bars
|
||||
constexpr float paddingW = 0.1; // Either side
|
||||
constexpr float paddingH = 0.1; // Above and below
|
||||
constexpr float gutterW = 0.1; // Between bars
|
||||
|
||||
constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the tallest
|
||||
constexpr uint8_t barCount = 4; // How many bars we draw. Reference only: changing value won't change the count.
|
||||
constexpr float barHRel[] = {0.3, 0.5, 0.7, 1.0}; // Heights of the signal bars, relative to the tallest
|
||||
constexpr uint8_t barCount = 4; // How many bars we draw. Reference only: changing value won't change the count.
|
||||
|
||||
// Dynamically calculate the width of the bars, and height of the rightmost, relative to other dimensions
|
||||
float barW = (1.0 - (paddingW + ((barCount - 1) * gutterW) + paddingW)) / barCount;
|
||||
float barHMax = 1.0 - (paddingH + paddingH);
|
||||
// Dynamically calculate the width of the bars, and height of the rightmost, relative to other dimensions
|
||||
float barW = (1.0 - (paddingW + ((barCount - 1) * gutterW) + paddingW)) / barCount;
|
||||
float barHMax = 1.0 - (paddingH + paddingH);
|
||||
|
||||
// Draw signal bar rectangles, then placeholder lines once strength reached
|
||||
for (uint8_t i = 0; i < barCount; i++) {
|
||||
// Coords for this specific bar
|
||||
float barH = barHMax * barHRel[i];
|
||||
float barX = paddingW + (i * (gutterW + barW));
|
||||
float barY = paddingH + (barHMax - barH);
|
||||
// Draw signal bar rectangles, then placeholder lines once strength reached
|
||||
for (uint8_t i = 0; i < barCount; i++) {
|
||||
// Coords for this specific bar
|
||||
float barH = barHMax * barHRel[i];
|
||||
float barX = paddingW + (i * (gutterW + barW));
|
||||
float barY = paddingH + (barHMax - barH);
|
||||
|
||||
// Rasterize to px coords at the last moment
|
||||
int16_t rX = (x + (w * barX)) + 0.5;
|
||||
int16_t rY = (y + (h * barY)) + 0.5;
|
||||
uint16_t rW = (w * barW) + 0.5;
|
||||
uint16_t rH = (h * barH) + 0.5;
|
||||
// Rasterize to px coords at the last moment
|
||||
int16_t rX = (x + (w * barX)) + 0.5;
|
||||
int16_t rY = (y + (h * barY)) + 0.5;
|
||||
uint16_t rW = (w * barW) + 0.5;
|
||||
uint16_t rH = (h * barH) + 0.5;
|
||||
|
||||
// Draw signal bars, until we are displaying the correct "signal strength", then just draw placeholder lines
|
||||
if (i <= strength)
|
||||
drawRect(rX, rY, rW, rH, BLACK);
|
||||
else {
|
||||
// Just draw a placeholder line
|
||||
float lineY = barY + barH;
|
||||
uint16_t rLineY = (y + (h * lineY)) + 0.5; // Rasterize
|
||||
drawLine(rX, rLineY, rX + rW - 1, rLineY, BLACK);
|
||||
}
|
||||
// Draw signal bars, until we are displaying the correct "signal strength", then just draw placeholder lines
|
||||
if (i <= strength)
|
||||
drawRect(rX, rY, rW, rH, BLACK);
|
||||
else {
|
||||
// Just draw a placeholder line
|
||||
float lineY = barY + barH;
|
||||
uint16_t rLineY = (y + (h * lineY)) + 0.5; // Rasterize
|
||||
drawLine(rX, rLineY, rX + rW - 1, rLineY, BLACK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -25,48 +25,46 @@ Used by the "Recents" and "Heard" applets. Possibly more in future?
|
||||
|
||||
#include "main.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class NodeListApplet : public Applet, public MeshModule
|
||||
{
|
||||
protected:
|
||||
// Info needed to draw a node card to the list
|
||||
// - generated each time we hear a node
|
||||
struct CardInfo {
|
||||
static constexpr uint8_t HOPS_UNKNOWN = -1;
|
||||
static constexpr uint32_t DISTANCE_UNKNOWN = -1;
|
||||
class NodeListApplet : public Applet, public MeshModule {
|
||||
protected:
|
||||
// Info needed to draw a node card to the list
|
||||
// - generated each time we hear a node
|
||||
struct CardInfo {
|
||||
static constexpr uint8_t HOPS_UNKNOWN = -1;
|
||||
static constexpr uint32_t DISTANCE_UNKNOWN = -1;
|
||||
|
||||
NodeNum nodeNum = 0;
|
||||
SignalStrength signal = SignalStrength::SIGNAL_UNKNOWN;
|
||||
uint32_t distanceMeters = DISTANCE_UNKNOWN;
|
||||
uint8_t hopsAway = HOPS_UNKNOWN;
|
||||
};
|
||||
NodeNum nodeNum = 0;
|
||||
SignalStrength signal = SignalStrength::SIGNAL_UNKNOWN;
|
||||
uint32_t distanceMeters = DISTANCE_UNKNOWN;
|
||||
uint8_t hopsAway = HOPS_UNKNOWN;
|
||||
};
|
||||
|
||||
public:
|
||||
NodeListApplet(const char *name);
|
||||
public:
|
||||
NodeListApplet(const char *name);
|
||||
|
||||
void onRender() override;
|
||||
void onRender() override;
|
||||
|
||||
bool wantPacket(const meshtastic_MeshPacket *p) override;
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
bool wantPacket(const meshtastic_MeshPacket *p) override;
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
protected:
|
||||
virtual void handleParsed(CardInfo c) = 0; // Tell derived applet that we heard a node
|
||||
virtual std::string getHeaderText() = 0; // Ask derived class what the applet's title should be
|
||||
protected:
|
||||
virtual void handleParsed(CardInfo c) = 0; // Tell derived applet that we heard a node
|
||||
virtual std::string getHeaderText() = 0; // Ask derived class what the applet's title should be
|
||||
|
||||
uint8_t maxCards(); // Max number of cards which could ever fit on screen
|
||||
uint8_t maxCards(); // Max number of cards which could ever fit on screen
|
||||
|
||||
std::deque<CardInfo> cards; // Cards to be rendered. Derived applet fills this.
|
||||
std::deque<CardInfo> cards; // Cards to be rendered. Derived applet fills this.
|
||||
|
||||
private:
|
||||
void drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h,
|
||||
SignalStrength signal); // Draw a "mobile phone" style signal indicator
|
||||
private:
|
||||
void drawSignalIndicator(int16_t x, int16_t y, uint16_t w, uint16_t h,
|
||||
SignalStrength signal); // Draw a "mobile phone" style signal indicator
|
||||
|
||||
// Card Dimensions
|
||||
// - for rendering and for maxCards calc
|
||||
uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards
|
||||
uint16_t cardH = fontMedium.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card
|
||||
// Card Dimensions
|
||||
// - for rendering and for maxCards calc
|
||||
uint8_t cardMarginH = fontSmall.lineHeight() / 2; // Gap between cards
|
||||
uint16_t cardH = fontMedium.lineHeight() + fontSmall.lineHeight() + cardMarginH; // Height of card
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -6,15 +6,14 @@ using namespace NicheGraphics;
|
||||
|
||||
// All drawing happens here
|
||||
// Our basic example doesn't do anything useful. It just passively prints some text.
|
||||
void InkHUD::BasicExampleApplet::onRender()
|
||||
{
|
||||
printAt(0, 0, "Hello, World!");
|
||||
void InkHUD::BasicExampleApplet::onRender() {
|
||||
printAt(0, 0, "Hello, World!");
|
||||
|
||||
// If text might contain "special characters", is needs parsing first
|
||||
// This applies to data such as text-messages and and node names
|
||||
// If text might contain "special characters", is needs parsing first
|
||||
// This applies to data such as text-messages and and node names
|
||||
|
||||
// std::string greeting = parse("Grüezi mitenand!");
|
||||
// printAt(0, 0, greeting);
|
||||
// std::string greeting = parse("Grüezi mitenand!");
|
||||
// printAt(0, 0, greeting);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -19,16 +19,14 @@ In variants/<your device>/nicheGraphics.h:
|
||||
|
||||
#include "graphics/niche/InkHUD/Applet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class BasicExampleApplet : public Applet
|
||||
{
|
||||
public:
|
||||
// You must have an onRender() method
|
||||
// All drawing happens here
|
||||
class BasicExampleApplet : public Applet {
|
||||
public:
|
||||
// You must have an onRender() method
|
||||
// All drawing happens here
|
||||
|
||||
void onRender() override;
|
||||
void onRender() override;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -5,49 +5,47 @@
|
||||
using namespace NicheGraphics;
|
||||
|
||||
// We configured the Module API to call this method when we receive a new text message
|
||||
ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
ProcessMessage InkHUD::NewMsgExampleApplet::handleReceived(const meshtastic_MeshPacket &mp) {
|
||||
|
||||
// Abort if applet fully deactivated
|
||||
// Don't waste time: we wouldn't be rendered anyway
|
||||
if (!isActive())
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Check that this is an incoming message
|
||||
// Outgoing messages (sent by us) will also call handleReceived
|
||||
|
||||
if (!isFromUs(&mp)) {
|
||||
// Store the sender's nodenum
|
||||
// We need to keep this information, so we can re-use it anytime render() is called
|
||||
haveMessage = true;
|
||||
fromWho = mp.from;
|
||||
|
||||
// Tell InkHUD that we have something new to show on the screen
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
// Tell Module API to continue informing other firmware components about this message
|
||||
// We're not the only component which is interested in new text messages
|
||||
// Abort if applet fully deactivated
|
||||
// Don't waste time: we wouldn't be rendered anyway
|
||||
if (!isActive())
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Check that this is an incoming message
|
||||
// Outgoing messages (sent by us) will also call handleReceived
|
||||
|
||||
if (!isFromUs(&mp)) {
|
||||
// Store the sender's nodenum
|
||||
// We need to keep this information, so we can re-use it anytime render() is called
|
||||
haveMessage = true;
|
||||
fromWho = mp.from;
|
||||
|
||||
// Tell InkHUD that we have something new to show on the screen
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
// Tell Module API to continue informing other firmware components about this message
|
||||
// We're not the only component which is interested in new text messages
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
// All drawing happens here
|
||||
// We can trigger a render by calling requestUpdate()
|
||||
// Render might be called by some external source
|
||||
// We should always be ready to draw
|
||||
void InkHUD::NewMsgExampleApplet::onRender()
|
||||
{
|
||||
printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0)
|
||||
void InkHUD::NewMsgExampleApplet::onRender() {
|
||||
printAt(0, 0, "Example: NewMsg", LEFT, TOP); // Print top-left corner of text at (0,0)
|
||||
|
||||
int16_t centerX = X(0.5); // Same as width() / 2
|
||||
int16_t centerY = Y(0.5); // Same as height() / 2
|
||||
int16_t centerX = X(0.5); // Same as width() / 2
|
||||
int16_t centerY = Y(0.5); // Same as height() / 2
|
||||
|
||||
if (haveMessage) {
|
||||
printAt(centerX, centerY, "New Message", CENTER, BOTTOM);
|
||||
printAt(centerX, centerY, "From: " + hexifyNodeNum(fromWho), CENTER, TOP);
|
||||
} else {
|
||||
printAt(centerX, centerY, "No Message", CENTER, MIDDLE); // Place center of string at (centerX, centerY)
|
||||
}
|
||||
if (haveMessage) {
|
||||
printAt(centerX, centerY, "New Message", CENTER, BOTTOM);
|
||||
printAt(centerX, centerY, "From: " + hexifyNodeNum(fromWho), CENTER, TOP);
|
||||
} else {
|
||||
printAt(centerX, centerY, "No Message", CENTER, MIDDLE); // Place center of string at (centerX, centerY)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -24,36 +24,34 @@ In variants/<your device>/nicheGraphics.h:
|
||||
|
||||
#include "mesh/SinglePortModule.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class NewMsgExampleApplet : public Applet, public SinglePortModule
|
||||
{
|
||||
public:
|
||||
// The MeshModule API requires us to have a constructor, to specify that we're interested in Text Messages.
|
||||
NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {}
|
||||
class NewMsgExampleApplet : public Applet, public SinglePortModule {
|
||||
public:
|
||||
// The MeshModule API requires us to have a constructor, to specify that we're interested in Text Messages.
|
||||
NewMsgExampleApplet() : SinglePortModule("NewMsgExampleApplet", meshtastic_PortNum_TEXT_MESSAGE_APP) {}
|
||||
|
||||
// All drawing happens here
|
||||
void onRender() override;
|
||||
// All drawing happens here
|
||||
void onRender() override;
|
||||
|
||||
// Your applet might also want to use some of these
|
||||
// Useful for setting up or tidying up
|
||||
// Your applet might also want to use some of these
|
||||
// Useful for setting up or tidying up
|
||||
|
||||
/*
|
||||
void onActivate(); // When started
|
||||
void onDeactivate(); // When stopped
|
||||
void onForeground(); // When shown by short-press
|
||||
void onBackground(); // When hidden by short-press
|
||||
*/
|
||||
/*
|
||||
void onActivate(); // When started
|
||||
void onDeactivate(); // When stopped
|
||||
void onForeground(); // When shown by short-press
|
||||
void onBackground(); // When hidden by short-press
|
||||
*/
|
||||
|
||||
private:
|
||||
// Called when we receive new text messages
|
||||
// Part of the MeshModule API
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
private:
|
||||
// Called when we receive new text messages
|
||||
// Part of the MeshModule API
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
// Store info from handleReceived
|
||||
bool haveMessage = false;
|
||||
NodeNum fromWho = 0;
|
||||
// Store info from handleReceived
|
||||
bool haveMessage = false;
|
||||
NodeNum fromWho = 0;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -4,79 +4,76 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::AlignStickApplet::AlignStickApplet()
|
||||
{
|
||||
if (!settings->joystick.aligned)
|
||||
bringToForeground();
|
||||
InkHUD::AlignStickApplet::AlignStickApplet() {
|
||||
if (!settings->joystick.aligned)
|
||||
bringToForeground();
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onRender()
|
||||
{
|
||||
setFont(fontMedium);
|
||||
printAt(0, 0, "Align Joystick:");
|
||||
setFont(fontSmall);
|
||||
std::string instructions = "Move joystick in the direction indicated";
|
||||
printWrapped(0, fontMedium.lineHeight() * 1.5, width(), instructions);
|
||||
void InkHUD::AlignStickApplet::onRender() {
|
||||
setFont(fontMedium);
|
||||
printAt(0, 0, "Align Joystick:");
|
||||
setFont(fontSmall);
|
||||
std::string instructions = "Move joystick in the direction indicated";
|
||||
printWrapped(0, fontMedium.lineHeight() * 1.5, width(), instructions);
|
||||
|
||||
// Size of the region in which the joystick graphic should fit
|
||||
uint16_t joyXLimit = X(0.8);
|
||||
uint16_t contentH = fontMedium.lineHeight() * 1.5 + fontSmall.lineHeight() * 1;
|
||||
if (getTextWidth(instructions) > width())
|
||||
contentH += fontSmall.lineHeight();
|
||||
uint16_t freeY = height() - contentH - fontSmall.lineHeight() * 1.2;
|
||||
uint16_t joyYLimit = freeY * 0.8;
|
||||
// Size of the region in which the joystick graphic should fit
|
||||
uint16_t joyXLimit = X(0.8);
|
||||
uint16_t contentH = fontMedium.lineHeight() * 1.5 + fontSmall.lineHeight() * 1;
|
||||
if (getTextWidth(instructions) > width())
|
||||
contentH += fontSmall.lineHeight();
|
||||
uint16_t freeY = height() - contentH - fontSmall.lineHeight() * 1.2;
|
||||
uint16_t joyYLimit = freeY * 0.8;
|
||||
|
||||
// Use the shorter of the two
|
||||
uint16_t joyWidth = joyXLimit < joyYLimit ? joyXLimit : joyYLimit;
|
||||
// Use the shorter of the two
|
||||
uint16_t joyWidth = joyXLimit < joyYLimit ? joyXLimit : joyYLimit;
|
||||
|
||||
// Center the joystick graphic
|
||||
uint16_t centerX = X(0.5);
|
||||
uint16_t centerY = contentH + freeY * 0.5;
|
||||
// Center the joystick graphic
|
||||
uint16_t centerX = X(0.5);
|
||||
uint16_t centerY = contentH + freeY * 0.5;
|
||||
|
||||
// Draw joystick graphic
|
||||
drawStick(centerX, centerY, joyWidth);
|
||||
// Draw joystick graphic
|
||||
drawStick(centerX, centerY, joyWidth);
|
||||
|
||||
setFont(fontSmall);
|
||||
printAt(X(0.5), Y(1.0) - fontSmall.lineHeight() * 0.2, "Long press to skip", CENTER, BOTTOM);
|
||||
setFont(fontSmall);
|
||||
printAt(X(0.5), Y(1.0) - fontSmall.lineHeight() * 0.2, "Long press to skip", CENTER, BOTTOM);
|
||||
}
|
||||
|
||||
// Draw a scalable joystick graphic
|
||||
void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uint16_t width)
|
||||
{
|
||||
if (width < 9) // too small to draw
|
||||
return;
|
||||
void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uint16_t width) {
|
||||
if (width < 9) // too small to draw
|
||||
return;
|
||||
|
||||
else if (width < 40) { // only draw up arrow
|
||||
uint16_t chamfer = width < 20 ? 1 : 2;
|
||||
else if (width < 40) { // only draw up arrow
|
||||
uint16_t chamfer = width < 20 ? 1 : 2;
|
||||
|
||||
// Draw filled up arrow
|
||||
drawDirection(centerX, centerY - width / 4, Direction::UP, width, chamfer, BLACK);
|
||||
// Draw filled up arrow
|
||||
drawDirection(centerX, centerY - width / 4, Direction::UP, width, chamfer, BLACK);
|
||||
|
||||
} else { // large enough to draw the full thing
|
||||
uint16_t chamfer = width < 80 ? 1 : 2;
|
||||
uint16_t stroke = 3; // pixels
|
||||
uint16_t arrowW = width * 0.22;
|
||||
uint16_t hollowW = arrowW - stroke * 2;
|
||||
} else { // large enough to draw the full thing
|
||||
uint16_t chamfer = width < 80 ? 1 : 2;
|
||||
uint16_t stroke = 3; // pixels
|
||||
uint16_t arrowW = width * 0.22;
|
||||
uint16_t hollowW = arrowW - stroke * 2;
|
||||
|
||||
// Draw center circle
|
||||
fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2), BLACK);
|
||||
fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2) - stroke, WHITE);
|
||||
// Draw center circle
|
||||
fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2), BLACK);
|
||||
fillCircle((int16_t)centerX, (int16_t)centerY, (int16_t)(width * 0.2) - stroke, WHITE);
|
||||
|
||||
// Draw filled up arrow
|
||||
drawDirection(centerX, centerY - width / 2, Direction::UP, arrowW, chamfer, BLACK);
|
||||
// Draw filled up arrow
|
||||
drawDirection(centerX, centerY - width / 2, Direction::UP, arrowW, chamfer, BLACK);
|
||||
|
||||
// Draw down arrow
|
||||
drawDirection(centerX, centerY + width / 2, Direction::DOWN, arrowW, chamfer, BLACK);
|
||||
drawDirection(centerX, centerY + width / 2 - stroke, Direction::DOWN, hollowW, 0, WHITE);
|
||||
// Draw down arrow
|
||||
drawDirection(centerX, centerY + width / 2, Direction::DOWN, arrowW, chamfer, BLACK);
|
||||
drawDirection(centerX, centerY + width / 2 - stroke, Direction::DOWN, hollowW, 0, WHITE);
|
||||
|
||||
// Draw left arrow
|
||||
drawDirection(centerX - width / 2, centerY, Direction::LEFT, arrowW, chamfer, BLACK);
|
||||
drawDirection(centerX - width / 2 + stroke, centerY, Direction::LEFT, hollowW, 0, WHITE);
|
||||
// Draw left arrow
|
||||
drawDirection(centerX - width / 2, centerY, Direction::LEFT, arrowW, chamfer, BLACK);
|
||||
drawDirection(centerX - width / 2 + stroke, centerY, Direction::LEFT, hollowW, 0, WHITE);
|
||||
|
||||
// Draw right arrow
|
||||
drawDirection(centerX + width / 2, centerY, Direction::RIGHT, arrowW, chamfer, BLACK);
|
||||
drawDirection(centerX + width / 2 - stroke, centerY, Direction::RIGHT, hollowW, 0, WHITE);
|
||||
}
|
||||
// Draw right arrow
|
||||
drawDirection(centerX + width / 2, centerY, Direction::RIGHT, arrowW, chamfer, BLACK);
|
||||
drawDirection(centerX + width / 2 - stroke, centerY, Direction::RIGHT, hollowW, 0, WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a scalable joystick direction arrow
|
||||
@@ -90,116 +87,98 @@ void InkHUD::AlignStickApplet::drawStick(uint16_t centerX, uint16_t centerY, uin
|
||||
v |_________|
|
||||
|
||||
*/
|
||||
void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size,
|
||||
uint16_t chamfer, Color color)
|
||||
{
|
||||
uint16_t chamferW = chamfer * 2 + 1;
|
||||
uint16_t triangleW = size - chamferW;
|
||||
void InkHUD::AlignStickApplet::drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color) {
|
||||
uint16_t chamferW = chamfer * 2 + 1;
|
||||
uint16_t triangleW = size - chamferW;
|
||||
|
||||
// Draw arrow
|
||||
switch (direction) {
|
||||
case Direction::UP:
|
||||
fillRect(pointX - chamfer, pointY, chamferW, triangleW, color);
|
||||
fillRect(pointX - chamfer - triangleW, pointY + triangleW, chamferW + triangleW * 2, chamferW, color);
|
||||
fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY + triangleW, pointX - chamfer,
|
||||
pointY + triangleW, color);
|
||||
fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY + triangleW, pointX + chamfer,
|
||||
pointY + triangleW, color);
|
||||
break;
|
||||
case Direction::DOWN:
|
||||
fillRect(pointX - chamfer, pointY - triangleW + 1, chamferW, triangleW, color);
|
||||
fillRect(pointX - chamfer - triangleW, pointY - size + 1, chamferW + triangleW * 2, chamferW, color);
|
||||
fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY - triangleW, pointX - chamfer,
|
||||
pointY - triangleW, color);
|
||||
fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY - triangleW, pointX + chamfer,
|
||||
pointY - triangleW, color);
|
||||
break;
|
||||
case Direction::LEFT:
|
||||
fillRect(pointX, pointY - chamfer, triangleW, chamferW, color);
|
||||
fillRect(pointX + triangleW, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color);
|
||||
fillTriangle(pointX, pointY - chamfer, pointX + triangleW, pointY - chamfer - triangleW, pointX + triangleW,
|
||||
pointY - chamfer, color);
|
||||
fillTriangle(pointX, pointY + chamfer, pointX + triangleW, pointY + chamfer + triangleW, pointX + triangleW,
|
||||
pointY + chamfer, color);
|
||||
break;
|
||||
case Direction::RIGHT:
|
||||
fillRect(pointX - triangleW + 1, pointY - chamfer, triangleW, chamferW, color);
|
||||
fillRect(pointX - size + 1, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color);
|
||||
fillTriangle(pointX, pointY - chamfer, pointX - triangleW, pointY - chamfer - triangleW, pointX - triangleW,
|
||||
pointY - chamfer, color);
|
||||
fillTriangle(pointX, pointY + chamfer, pointX - triangleW, pointY + chamfer + triangleW, pointX - triangleW,
|
||||
pointY + chamfer, color);
|
||||
break;
|
||||
}
|
||||
// Draw arrow
|
||||
switch (direction) {
|
||||
case Direction::UP:
|
||||
fillRect(pointX - chamfer, pointY, chamferW, triangleW, color);
|
||||
fillRect(pointX - chamfer - triangleW, pointY + triangleW, chamferW + triangleW * 2, chamferW, color);
|
||||
fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY + triangleW, pointX - chamfer, pointY + triangleW, color);
|
||||
fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY + triangleW, pointX + chamfer, pointY + triangleW, color);
|
||||
break;
|
||||
case Direction::DOWN:
|
||||
fillRect(pointX - chamfer, pointY - triangleW + 1, chamferW, triangleW, color);
|
||||
fillRect(pointX - chamfer - triangleW, pointY - size + 1, chamferW + triangleW * 2, chamferW, color);
|
||||
fillTriangle(pointX - chamfer, pointY, pointX - chamfer - triangleW, pointY - triangleW, pointX - chamfer, pointY - triangleW, color);
|
||||
fillTriangle(pointX + chamfer, pointY, pointX + chamfer + triangleW, pointY - triangleW, pointX + chamfer, pointY - triangleW, color);
|
||||
break;
|
||||
case Direction::LEFT:
|
||||
fillRect(pointX, pointY - chamfer, triangleW, chamferW, color);
|
||||
fillRect(pointX + triangleW, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color);
|
||||
fillTriangle(pointX, pointY - chamfer, pointX + triangleW, pointY - chamfer - triangleW, pointX + triangleW, pointY - chamfer, color);
|
||||
fillTriangle(pointX, pointY + chamfer, pointX + triangleW, pointY + chamfer + triangleW, pointX + triangleW, pointY + chamfer, color);
|
||||
break;
|
||||
case Direction::RIGHT:
|
||||
fillRect(pointX - triangleW + 1, pointY - chamfer, triangleW, chamferW, color);
|
||||
fillRect(pointX - size + 1, pointY - chamfer - triangleW, chamferW, chamferW + triangleW * 2, color);
|
||||
fillTriangle(pointX, pointY - chamfer, pointX - triangleW, pointY - chamfer - triangleW, pointX - triangleW, pointY - chamfer, color);
|
||||
fillTriangle(pointX, pointY + chamfer, pointX - triangleW, pointY + chamfer + triangleW, pointX - triangleW, pointY + chamfer, color);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onForeground()
|
||||
{
|
||||
// Prevent most other applets from requesting update, and skip their rendering entirely
|
||||
// Another system applet with a higher precedence can potentially ignore this
|
||||
SystemApplet::lockRendering = true;
|
||||
SystemApplet::lockRequests = true;
|
||||
void InkHUD::AlignStickApplet::onForeground() {
|
||||
// Prevent most other applets from requesting update, and skip their rendering entirely
|
||||
// Another system applet with a higher precedence can potentially ignore this
|
||||
SystemApplet::lockRendering = true;
|
||||
SystemApplet::lockRequests = true;
|
||||
|
||||
handleInput = true; // Intercept the button input for our applet
|
||||
handleInput = true; // Intercept the button input for our applet
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onBackground()
|
||||
{
|
||||
// Allow normal update behavior to resume
|
||||
SystemApplet::lockRendering = false;
|
||||
SystemApplet::lockRequests = false;
|
||||
SystemApplet::handleInput = false;
|
||||
void InkHUD::AlignStickApplet::onBackground() {
|
||||
// Allow normal update behavior to resume
|
||||
SystemApplet::lockRendering = false;
|
||||
SystemApplet::lockRequests = false;
|
||||
SystemApplet::handleInput = false;
|
||||
|
||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onButtonLongPress()
|
||||
{
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
void InkHUD::AlignStickApplet::onButtonLongPress() {
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onExitLong()
|
||||
{
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
void InkHUD::AlignStickApplet::onExitLong() {
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onNavUp()
|
||||
{
|
||||
settings->joystick.aligned = true;
|
||||
void InkHUD::AlignStickApplet::onNavUp() {
|
||||
settings->joystick.aligned = true;
|
||||
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onNavDown()
|
||||
{
|
||||
inkhud->rotateJoystick(2); // 180 deg
|
||||
settings->joystick.aligned = true;
|
||||
void InkHUD::AlignStickApplet::onNavDown() {
|
||||
inkhud->rotateJoystick(2); // 180 deg
|
||||
settings->joystick.aligned = true;
|
||||
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onNavLeft()
|
||||
{
|
||||
inkhud->rotateJoystick(3); // 270 deg
|
||||
settings->joystick.aligned = true;
|
||||
void InkHUD::AlignStickApplet::onNavLeft() {
|
||||
inkhud->rotateJoystick(3); // 270 deg
|
||||
settings->joystick.aligned = true;
|
||||
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::AlignStickApplet::onNavRight()
|
||||
{
|
||||
inkhud->rotateJoystick(1); // 90 deg
|
||||
settings->joystick.aligned = true;
|
||||
void InkHUD::AlignStickApplet::onNavRight() {
|
||||
inkhud->rotateJoystick(1); // 90 deg
|
||||
settings->joystick.aligned = true;
|
||||
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -15,34 +15,32 @@ and not aligned to the screen
|
||||
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class AlignStickApplet : public SystemApplet
|
||||
{
|
||||
public:
|
||||
AlignStickApplet();
|
||||
class AlignStickApplet : public SystemApplet {
|
||||
public:
|
||||
AlignStickApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonLongPress() override;
|
||||
void onExitLong() override;
|
||||
void onNavUp() override;
|
||||
void onNavDown() override;
|
||||
void onNavLeft() override;
|
||||
void onNavRight() override;
|
||||
void onRender() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonLongPress() override;
|
||||
void onExitLong() override;
|
||||
void onNavUp() override;
|
||||
void onNavDown() override;
|
||||
void onNavLeft() override;
|
||||
void onNavRight() override;
|
||||
|
||||
protected:
|
||||
enum Direction {
|
||||
UP,
|
||||
DOWN,
|
||||
LEFT,
|
||||
RIGHT,
|
||||
};
|
||||
protected:
|
||||
enum Direction {
|
||||
UP,
|
||||
DOWN,
|
||||
LEFT,
|
||||
RIGHT,
|
||||
};
|
||||
|
||||
void drawStick(uint16_t centerX, uint16_t centerY, uint16_t width);
|
||||
void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color);
|
||||
void drawStick(uint16_t centerX, uint16_t centerY, uint16_t width);
|
||||
void drawDirection(uint16_t pointX, uint16_t pointY, Direction direction, uint16_t size, uint16_t chamfer, Color color);
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -4,98 +4,95 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::BatteryIconApplet::BatteryIconApplet()
|
||||
{
|
||||
// Show at boot, if user has previously enabled the feature
|
||||
if (settings->optionalFeatures.batteryIcon)
|
||||
bringToForeground();
|
||||
InkHUD::BatteryIconApplet::BatteryIconApplet() {
|
||||
// Show at boot, if user has previously enabled the feature
|
||||
if (settings->optionalFeatures.batteryIcon)
|
||||
bringToForeground();
|
||||
|
||||
// Register to our have BatteryIconApplet::onPowerStatusUpdate method called when new power info is available
|
||||
// This happens whether or not the battery icon feature is enabled
|
||||
powerStatusObserver.observe(&powerStatus->onNewStatus);
|
||||
// Register to our have BatteryIconApplet::onPowerStatusUpdate method called when new power info is available
|
||||
// This happens whether or not the battery icon feature is enabled
|
||||
powerStatusObserver.observe(&powerStatus->onNewStatus);
|
||||
}
|
||||
|
||||
// We handle power status' even when the feature is disabled,
|
||||
// so that we have up to date data ready if the feature is enabled later.
|
||||
// Otherwise could be 30s before new status update, with weird battery value displayed
|
||||
int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *status)
|
||||
{
|
||||
// System applets are always active
|
||||
assert(isActive());
|
||||
int InkHUD::BatteryIconApplet::onPowerStatusUpdate(const meshtastic::Status *status) {
|
||||
// System applets are always active
|
||||
assert(isActive());
|
||||
|
||||
// This method should only receive power statuses
|
||||
// If we get a different type of status, something has gone weird elsewhere
|
||||
assert(status->getStatusType() == STATUS_TYPE_POWER);
|
||||
// This method should only receive power statuses
|
||||
// If we get a different type of status, something has gone weird elsewhere
|
||||
assert(status->getStatusType() == STATUS_TYPE_POWER);
|
||||
|
||||
meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status;
|
||||
meshtastic::PowerStatus *powerStatus = (meshtastic::PowerStatus *)status;
|
||||
|
||||
// Get the new state of charge %, and round to the nearest 10%
|
||||
uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10;
|
||||
// Get the new state of charge %, and round to the nearest 10%
|
||||
uint8_t newSocRounded = ((powerStatus->getBatteryChargePercent() + 5) / 10) * 10;
|
||||
|
||||
// If rounded value has changed, trigger a display update
|
||||
// It's okay to requestUpdate before we store the new value, as the update won't run until next loop()
|
||||
// Don't trigger an update if the feature is disabled
|
||||
if (this->socRounded != newSocRounded && settings->optionalFeatures.batteryIcon)
|
||||
requestUpdate();
|
||||
// If rounded value has changed, trigger a display update
|
||||
// It's okay to requestUpdate before we store the new value, as the update won't run until next loop()
|
||||
// Don't trigger an update if the feature is disabled
|
||||
if (this->socRounded != newSocRounded && settings->optionalFeatures.batteryIcon)
|
||||
requestUpdate();
|
||||
|
||||
// Store the new value
|
||||
this->socRounded = newSocRounded;
|
||||
// Store the new value
|
||||
this->socRounded = newSocRounded;
|
||||
|
||||
return 0; // Tell Observable to continue informing other observers
|
||||
return 0; // Tell Observable to continue informing other observers
|
||||
}
|
||||
|
||||
void InkHUD::BatteryIconApplet::onRender()
|
||||
{
|
||||
// Fill entire tile
|
||||
// - size of icon controlled by size of tile
|
||||
int16_t l = 0;
|
||||
int16_t t = 0;
|
||||
uint16_t w = width();
|
||||
int16_t h = height();
|
||||
void InkHUD::BatteryIconApplet::onRender() {
|
||||
// Fill entire tile
|
||||
// - size of icon controlled by size of tile
|
||||
int16_t l = 0;
|
||||
int16_t t = 0;
|
||||
uint16_t w = width();
|
||||
int16_t h = height();
|
||||
|
||||
// Clear the region beneath the tile
|
||||
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
||||
// We do need to do this with the battery though, as it is an "overlay"
|
||||
fillRect(l, t, w, h, WHITE);
|
||||
// Clear the region beneath the tile
|
||||
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
||||
// We do need to do this with the battery though, as it is an "overlay"
|
||||
fillRect(l, t, w, h, WHITE);
|
||||
|
||||
// Vertical centerline
|
||||
const int16_t m = t + (h / 2);
|
||||
// Vertical centerline
|
||||
const int16_t m = t + (h / 2);
|
||||
|
||||
// =====================
|
||||
// Draw battery outline
|
||||
// =====================
|
||||
// =====================
|
||||
// Draw battery outline
|
||||
// =====================
|
||||
|
||||
// Positive terminal "bump"
|
||||
const int16_t &bumpL = l;
|
||||
const uint16_t bumpH = h / 2;
|
||||
const int16_t bumpT = m - (bumpH / 2);
|
||||
constexpr uint16_t bumpW = 2;
|
||||
fillRect(bumpL, bumpT, bumpW, bumpH, BLACK);
|
||||
// Positive terminal "bump"
|
||||
const int16_t &bumpL = l;
|
||||
const uint16_t bumpH = h / 2;
|
||||
const int16_t bumpT = m - (bumpH / 2);
|
||||
constexpr uint16_t bumpW = 2;
|
||||
fillRect(bumpL, bumpT, bumpW, bumpH, BLACK);
|
||||
|
||||
// Main body of battery
|
||||
const int16_t bodyL = bumpL + bumpW;
|
||||
const int16_t &bodyT = t;
|
||||
const int16_t &bodyH = h;
|
||||
const int16_t bodyW = w - bumpW;
|
||||
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
|
||||
// Main body of battery
|
||||
const int16_t bodyL = bumpL + bumpW;
|
||||
const int16_t &bodyT = t;
|
||||
const int16_t &bodyH = h;
|
||||
const int16_t bodyW = w - bumpW;
|
||||
drawRect(bodyL, bodyT, bodyW, bodyH, BLACK);
|
||||
|
||||
// Erase join between bump and body
|
||||
drawLine(bodyL, bumpT, bodyL, bumpT + bumpH - 1, WHITE);
|
||||
// Erase join between bump and body
|
||||
drawLine(bodyL, bumpT, bodyL, bumpT + bumpH - 1, WHITE);
|
||||
|
||||
// ===================
|
||||
// Draw battery level
|
||||
// ===================
|
||||
// ===================
|
||||
// Draw battery level
|
||||
// ===================
|
||||
|
||||
constexpr int16_t slicePad = 2;
|
||||
const int16_t sliceL = bodyL + slicePad;
|
||||
const int16_t sliceT = bodyT + slicePad;
|
||||
const uint16_t sliceH = bodyH - (slicePad * 2);
|
||||
uint16_t sliceW = bodyW - (slicePad * 2);
|
||||
constexpr int16_t slicePad = 2;
|
||||
const int16_t sliceL = bodyL + slicePad;
|
||||
const int16_t sliceT = bodyT + slicePad;
|
||||
const uint16_t sliceH = bodyH - (slicePad * 2);
|
||||
uint16_t sliceW = bodyW - (slicePad * 2);
|
||||
|
||||
sliceW = (sliceW * socRounded) / 100; // Apply percentage
|
||||
sliceW = (sliceW * socRounded) / 100; // Apply percentage
|
||||
|
||||
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
|
||||
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
|
||||
hatchRegion(sliceL, sliceT, sliceW, sliceH, 2, BLACK);
|
||||
drawRect(sliceL, sliceT, sliceW, sliceH, BLACK);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -15,23 +15,21 @@ It should be optional, enabled by the on-screen menu
|
||||
|
||||
#include "PowerStatus.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class BatteryIconApplet : public SystemApplet
|
||||
{
|
||||
public:
|
||||
BatteryIconApplet();
|
||||
class BatteryIconApplet : public SystemApplet {
|
||||
public:
|
||||
BatteryIconApplet();
|
||||
|
||||
void onRender() override;
|
||||
int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available
|
||||
void onRender() override;
|
||||
int onPowerStatusUpdate(const meshtastic::Status *status); // Called when new info about battery is available
|
||||
|
||||
private:
|
||||
// Get informed when new information about the battery is available (via onPowerStatusUpdate method)
|
||||
CallbackObserver<BatteryIconApplet, const meshtastic::Status *> powerStatusObserver =
|
||||
CallbackObserver<BatteryIconApplet, const meshtastic::Status *>(this, &BatteryIconApplet::onPowerStatusUpdate);
|
||||
private:
|
||||
// Get informed when new information about the battery is available (via onPowerStatusUpdate method)
|
||||
CallbackObserver<BatteryIconApplet, const meshtastic::Status *> powerStatusObserver =
|
||||
CallbackObserver<BatteryIconApplet, const meshtastic::Status *>(this, &BatteryIconApplet::onPowerStatusUpdate);
|
||||
|
||||
uint8_t socRounded = 0; // Battery state of charge, rounded to nearest 10%
|
||||
uint8_t socRounded = 0; // Battery state of charge, rounded to nearest 10%
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -6,172 +6,166 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet")
|
||||
{
|
||||
OSThread::setIntervalFromNow(8 * 1000UL);
|
||||
OSThread::enabled = true;
|
||||
InkHUD::LogoApplet::LogoApplet() : concurrency::OSThread("LogoApplet") {
|
||||
OSThread::setIntervalFromNow(8 * 1000UL);
|
||||
OSThread::enabled = true;
|
||||
|
||||
// During onboarding, show the default short name as well as the version string
|
||||
// This behavior assists manufacturers during mass production, and should not be modified without good reason
|
||||
if (!settings->tips.safeShutdownSeen) {
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
fontTitle = fontMedium;
|
||||
textLeft = xstr(APP_VERSION_SHORT);
|
||||
textRight = parseShortName(ourNode);
|
||||
textTitle = "Meshtastic";
|
||||
} else {
|
||||
fontTitle = fontSmall;
|
||||
textLeft = "";
|
||||
textRight = "";
|
||||
textTitle = xstr(APP_VERSION_SHORT);
|
||||
}
|
||||
// During onboarding, show the default short name as well as the version string
|
||||
// This behavior assists manufacturers during mass production, and should not be modified without good reason
|
||||
if (!settings->tips.safeShutdownSeen) {
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
fontTitle = fontMedium;
|
||||
textLeft = xstr(APP_VERSION_SHORT);
|
||||
textRight = parseShortName(ourNode);
|
||||
textTitle = "Meshtastic";
|
||||
} else {
|
||||
fontTitle = fontSmall;
|
||||
textLeft = "";
|
||||
textRight = "";
|
||||
textTitle = xstr(APP_VERSION_SHORT);
|
||||
}
|
||||
|
||||
bringToForeground();
|
||||
// This is then drawn with a FULL refresh by Renderer::begin
|
||||
bringToForeground();
|
||||
// This is then drawn with a FULL refresh by Renderer::begin
|
||||
}
|
||||
|
||||
void InkHUD::LogoApplet::onRender()
|
||||
{
|
||||
// Size of the region which the logo should "scale to fit"
|
||||
uint16_t logoWLimit = X(0.8);
|
||||
uint16_t logoHLimit = Y(0.5);
|
||||
void InkHUD::LogoApplet::onRender() {
|
||||
// Size of the region which the logo should "scale to fit"
|
||||
uint16_t logoWLimit = X(0.8);
|
||||
uint16_t logoHLimit = Y(0.5);
|
||||
|
||||
// Get the max width and height we can manage within the region, while still maintaining aspect ratio
|
||||
uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit);
|
||||
uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit);
|
||||
// Get the max width and height we can manage within the region, while still maintaining aspect ratio
|
||||
uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit);
|
||||
uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit);
|
||||
|
||||
// Where to place the center of the logo
|
||||
int16_t logoCX = X(0.5);
|
||||
int16_t logoCY = Y(0.5 - 0.05);
|
||||
// Where to place the center of the logo
|
||||
int16_t logoCX = X(0.5);
|
||||
int16_t logoCY = Y(0.5 - 0.05);
|
||||
|
||||
// Invert colors if black-on-white
|
||||
// Used during shutdown, to resport display health
|
||||
// Todo: handle this in InkHUD::Renderer instead
|
||||
if (inverted) {
|
||||
fillScreen(BLACK);
|
||||
setTextColor(WHITE);
|
||||
}
|
||||
// Invert colors if black-on-white
|
||||
// Used during shutdown, to resport display health
|
||||
// Todo: handle this in InkHUD::Renderer instead
|
||||
if (inverted) {
|
||||
fillScreen(BLACK);
|
||||
setTextColor(WHITE);
|
||||
}
|
||||
|
||||
#ifdef USERPREFS_OEM_IMAGE_DATA // Custom boot screen, if defined in userPrefs.jsonc
|
||||
|
||||
// Only show the custom screen at startup
|
||||
// This allows us to draw the usual Meshtastic logo at shutdown
|
||||
// The effect is similar to the two-stage userPrefs boot screen used by BaseUI
|
||||
if (millis() < 10 * 1000UL) {
|
||||
// Only show the custom screen at startup
|
||||
// This allows us to draw the usual Meshtastic logo at shutdown
|
||||
// The effect is similar to the two-stage userPrefs boot screen used by BaseUI
|
||||
if (millis() < 10 * 1000UL) {
|
||||
|
||||
// Draw the custom logo
|
||||
const uint8_t logo[] = USERPREFS_OEM_IMAGE_DATA;
|
||||
drawXBitmap(logoCX - (USERPREFS_OEM_IMAGE_WIDTH / 2), // Left
|
||||
logoCY - (USERPREFS_OEM_IMAGE_HEIGHT / 2), // Top
|
||||
logo, // XBM data
|
||||
USERPREFS_OEM_IMAGE_WIDTH, // Width
|
||||
USERPREFS_OEM_IMAGE_HEIGHT, // Height
|
||||
inverted ? WHITE : BLACK // Color
|
||||
);
|
||||
// Draw the custom logo
|
||||
const uint8_t logo[] = USERPREFS_OEM_IMAGE_DATA;
|
||||
drawXBitmap(logoCX - (USERPREFS_OEM_IMAGE_WIDTH / 2), // Left
|
||||
logoCY - (USERPREFS_OEM_IMAGE_HEIGHT / 2), // Top
|
||||
logo, // XBM data
|
||||
USERPREFS_OEM_IMAGE_WIDTH, // Width
|
||||
USERPREFS_OEM_IMAGE_HEIGHT, // Height
|
||||
inverted ? WHITE : BLACK // Color
|
||||
);
|
||||
|
||||
// Select the largest font which will still comfortably fit the custom text
|
||||
setFont(fontLarge);
|
||||
if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width())
|
||||
setFont(fontMedium);
|
||||
if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width())
|
||||
setFont(fontSmall);
|
||||
// Select the largest font which will still comfortably fit the custom text
|
||||
setFont(fontLarge);
|
||||
if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width())
|
||||
setFont(fontMedium);
|
||||
if (getTextWidth(USERPREFS_OEM_TEXT) > 0.8 * width())
|
||||
setFont(fontSmall);
|
||||
|
||||
// Draw custom text below logo
|
||||
int16_t logoB = logoCY + (USERPREFS_OEM_IMAGE_HEIGHT / 2); // Bottom of the logo
|
||||
printAt(X(0.5), logoB + Y(0.1), USERPREFS_OEM_TEXT, CENTER, TOP);
|
||||
// Draw custom text below logo
|
||||
int16_t logoB = logoCY + (USERPREFS_OEM_IMAGE_HEIGHT / 2); // Bottom of the logo
|
||||
printAt(X(0.5), logoB + Y(0.1), USERPREFS_OEM_TEXT, CENTER, TOP);
|
||||
|
||||
// Don't draw the normal boot screen, we've already drawn our custom version
|
||||
return;
|
||||
}
|
||||
// Don't draw the normal boot screen, we've already drawn our custom version
|
||||
return;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
drawLogo(logoCX, logoCY, logoW, logoH, inverted ? WHITE : BLACK);
|
||||
drawLogo(logoCX, logoCY, logoW, logoH, inverted ? WHITE : BLACK);
|
||||
|
||||
if (!textLeft.empty()) {
|
||||
setFont(fontSmall);
|
||||
printAt(0, 0, textLeft, LEFT, TOP);
|
||||
}
|
||||
if (!textLeft.empty()) {
|
||||
setFont(fontSmall);
|
||||
printAt(0, 0, textLeft, LEFT, TOP);
|
||||
}
|
||||
|
||||
if (!textRight.empty()) {
|
||||
setFont(fontSmall);
|
||||
printAt(X(1), 0, textRight, RIGHT, TOP);
|
||||
}
|
||||
if (!textRight.empty()) {
|
||||
setFont(fontSmall);
|
||||
printAt(X(1), 0, textRight, RIGHT, TOP);
|
||||
}
|
||||
|
||||
if (!textTitle.empty()) {
|
||||
int16_t logoB = logoCY + (logoH / 2); // Bottom of the logo
|
||||
setFont(fontTitle);
|
||||
printAt(X(0.5), logoB + Y(0.1), textTitle, CENTER, TOP);
|
||||
}
|
||||
if (!textTitle.empty()) {
|
||||
int16_t logoB = logoCY + (logoH / 2); // Bottom of the logo
|
||||
setFont(fontTitle);
|
||||
printAt(X(0.5), logoB + Y(0.1), textTitle, CENTER, TOP);
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::LogoApplet::onForeground()
|
||||
{
|
||||
SystemApplet::lockRendering = true;
|
||||
SystemApplet::lockRequests = true;
|
||||
SystemApplet::handleInput = true; // We don't actually use this input. Just blocking other applets from using it.
|
||||
void InkHUD::LogoApplet::onForeground() {
|
||||
SystemApplet::lockRendering = true;
|
||||
SystemApplet::lockRequests = true;
|
||||
SystemApplet::handleInput = true; // We don't actually use this input. Just blocking other applets from using it.
|
||||
}
|
||||
|
||||
void InkHUD::LogoApplet::onBackground()
|
||||
{
|
||||
SystemApplet::lockRendering = false;
|
||||
SystemApplet::lockRequests = false;
|
||||
SystemApplet::handleInput = false;
|
||||
void InkHUD::LogoApplet::onBackground() {
|
||||
SystemApplet::lockRendering = false;
|
||||
SystemApplet::lockRequests = false;
|
||||
SystemApplet::handleInput = false;
|
||||
|
||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
// Begin displaying the screen which is shown at shutdown
|
||||
void InkHUD::LogoApplet::onShutdown()
|
||||
{
|
||||
bringToForeground();
|
||||
void InkHUD::LogoApplet::onShutdown() {
|
||||
bringToForeground();
|
||||
|
||||
textLeft = "";
|
||||
textRight = "";
|
||||
textTitle = "Shutting Down...";
|
||||
fontTitle = fontSmall;
|
||||
textLeft = "";
|
||||
textRight = "";
|
||||
textTitle = "Shutting Down...";
|
||||
fontTitle = fontSmall;
|
||||
|
||||
// Draw a shutting down screen, twice.
|
||||
// Once white on black, once black on white.
|
||||
// Intention is to restore display health.
|
||||
// Draw a shutting down screen, twice.
|
||||
// Once white on black, once black on white.
|
||||
// Intention is to restore display health.
|
||||
|
||||
inverted = true;
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, false);
|
||||
delay(1000); // Cooldown. Back to back updates aren't great for health.
|
||||
inverted = false;
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, false);
|
||||
delay(1000); // Cooldown
|
||||
inverted = true;
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, false);
|
||||
delay(1000); // Cooldown. Back to back updates aren't great for health.
|
||||
inverted = false;
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, false);
|
||||
delay(1000); // Cooldown
|
||||
|
||||
// Prepare for the powered-off screen now
|
||||
// We can change these values because the initial "shutting down" screen has already rendered at this point
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
textLeft = "";
|
||||
textRight = "";
|
||||
textTitle = parseShortName(ourNode);
|
||||
fontTitle = fontMedium;
|
||||
// Prepare for the powered-off screen now
|
||||
// We can change these values because the initial "shutting down" screen has already rendered at this point
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
textLeft = "";
|
||||
textRight = "";
|
||||
textTitle = parseShortName(ourNode);
|
||||
fontTitle = fontMedium;
|
||||
|
||||
// This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is complete
|
||||
// This is then drawn by InkHUD::Events::onShutdown, with a blocking FULL update, after InkHUD's flash write is
|
||||
// complete
|
||||
}
|
||||
|
||||
void InkHUD::LogoApplet::onReboot()
|
||||
{
|
||||
bringToForeground();
|
||||
void InkHUD::LogoApplet::onReboot() {
|
||||
bringToForeground();
|
||||
|
||||
textLeft = "";
|
||||
textRight = "";
|
||||
textTitle = "Rebooting...";
|
||||
fontTitle = fontSmall;
|
||||
textLeft = "";
|
||||
textRight = "";
|
||||
textTitle = "Rebooting...";
|
||||
fontTitle = fontSmall;
|
||||
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, false);
|
||||
// Perform the update right now, waiting here until complete
|
||||
inkhud->forceUpdate(Drivers::EInk::FULL, false);
|
||||
// Perform the update right now, waiting here until complete
|
||||
}
|
||||
|
||||
int32_t InkHUD::LogoApplet::runOnce()
|
||||
{
|
||||
sendToBackground();
|
||||
return OSThread::disable();
|
||||
int32_t InkHUD::LogoApplet::runOnce() {
|
||||
sendToBackground();
|
||||
return OSThread::disable();
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -14,27 +14,25 @@
|
||||
#include "concurrency/OSThread.h"
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class LogoApplet : public SystemApplet, public concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
LogoApplet();
|
||||
void onRender() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onShutdown() override;
|
||||
void onReboot() override;
|
||||
class LogoApplet : public SystemApplet, public concurrency::OSThread {
|
||||
public:
|
||||
LogoApplet();
|
||||
void onRender() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onShutdown() override;
|
||||
void onReboot() override;
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override;
|
||||
protected:
|
||||
int32_t runOnce() override;
|
||||
|
||||
std::string textLeft;
|
||||
std::string textRight;
|
||||
std::string textTitle;
|
||||
AppletFont fontTitle;
|
||||
bool inverted = false; // Invert colors. Used during shutdown, to restore display health.
|
||||
std::string textLeft;
|
||||
std::string textRight;
|
||||
std::string textTitle;
|
||||
AppletFont fontTitle;
|
||||
bool inverted = false; // Invert colors. Used during shutdown, to restore display health.
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -13,29 +13,28 @@ Behaviors assigned in MenuApplet::execute
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
enum MenuAction {
|
||||
NO_ACTION,
|
||||
SEND_PING,
|
||||
STORE_CANNEDMESSAGE_SELECTION,
|
||||
SEND_CANNEDMESSAGE,
|
||||
SHUTDOWN,
|
||||
NEXT_TILE,
|
||||
TOGGLE_BACKLIGHT,
|
||||
TOGGLE_GPS,
|
||||
ENABLE_BLUETOOTH,
|
||||
TOGGLE_APPLET,
|
||||
TOGGLE_AUTOSHOW_APPLET,
|
||||
SET_RECENTS,
|
||||
ROTATE,
|
||||
ALIGN_JOYSTICK,
|
||||
LAYOUT,
|
||||
TOGGLE_BATTERY_ICON,
|
||||
TOGGLE_NOTIFICATIONS,
|
||||
TOGGLE_INVERT_COLOR,
|
||||
TOGGLE_12H_CLOCK,
|
||||
NO_ACTION,
|
||||
SEND_PING,
|
||||
STORE_CANNEDMESSAGE_SELECTION,
|
||||
SEND_CANNEDMESSAGE,
|
||||
SHUTDOWN,
|
||||
NEXT_TILE,
|
||||
TOGGLE_BACKLIGHT,
|
||||
TOGGLE_GPS,
|
||||
ENABLE_BLUETOOTH,
|
||||
TOGGLE_APPLET,
|
||||
TOGGLE_AUTOSHOW_APPLET,
|
||||
SET_RECENTS,
|
||||
ROTATE,
|
||||
ALIGN_JOYSTICK,
|
||||
LAYOUT,
|
||||
TOGGLE_BATTERY_ICON,
|
||||
TOGGLE_NOTIFICATIONS,
|
||||
TOGGLE_INVERT_COLOR,
|
||||
TOGGLE_12H_CLOCK,
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,91 +14,88 @@
|
||||
#include "Channels.h"
|
||||
#include "concurrency/OSThread.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class Applet;
|
||||
|
||||
class MenuApplet : public SystemApplet, public concurrency::OSThread
|
||||
{
|
||||
class MenuApplet : public SystemApplet, public concurrency::OSThread {
|
||||
public:
|
||||
MenuApplet();
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonShortPress() override;
|
||||
void onButtonLongPress() override;
|
||||
void onExitShort() override;
|
||||
void onNavUp() override;
|
||||
void onNavDown() override;
|
||||
void onNavLeft() override;
|
||||
void onNavRight() override;
|
||||
void onRender() override;
|
||||
|
||||
void show(Tile *t); // Open the menu, onto a user tile
|
||||
|
||||
protected:
|
||||
Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton
|
||||
|
||||
int32_t runOnce() override;
|
||||
|
||||
void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any
|
||||
void showPage(MenuPage page); // Load and display a MenuPage
|
||||
|
||||
void populateSendPage(); // Dynamically create MenuItems including canned messages
|
||||
void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message
|
||||
void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets
|
||||
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
|
||||
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
||||
|
||||
uint16_t getSystemInfoPanelHeight();
|
||||
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
|
||||
uint16_t *height = nullptr); // Info panel at top of root menu
|
||||
void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh
|
||||
void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data
|
||||
|
||||
MenuPage currentPage = MenuPage::ROOT;
|
||||
MenuPage previousPage = MenuPage::EXIT;
|
||||
uint8_t cursor = 0; // Which menu item is currently highlighted
|
||||
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
|
||||
|
||||
uint16_t systemInfoPanelHeight = 0; // Need to know before we render
|
||||
|
||||
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
|
||||
|
||||
// Data for selecting and sending canned messages via the menu
|
||||
// Placed into a sub-class for organization only
|
||||
class CannedMessages {
|
||||
public:
|
||||
MenuApplet();
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonShortPress() override;
|
||||
void onButtonLongPress() override;
|
||||
void onExitShort() override;
|
||||
void onNavUp() override;
|
||||
void onNavDown() override;
|
||||
void onNavLeft() override;
|
||||
void onNavRight() override;
|
||||
void onRender() override;
|
||||
// Share NicheGraphics component
|
||||
// Handles loading, getting, setting
|
||||
CannedMessageStore *store;
|
||||
|
||||
void show(Tile *t); // Open the menu, onto a user tile
|
||||
// One canned message
|
||||
// Links the menu item to the true message text
|
||||
struct MessageItem {
|
||||
std::string label; // Shown in menu. Prefixed, and UTF-8 chars parsed
|
||||
std::string rawText; // The message which will be sent, if this item is selected
|
||||
} *selectedMessageItem;
|
||||
|
||||
protected:
|
||||
Drivers::LatchingBacklight *backlight = nullptr; // Convenient access to the backlight singleton
|
||||
// One possible destination for a canned message
|
||||
// Links the menu item to the intended recipient
|
||||
// May represent either broadcast or DM
|
||||
struct RecipientItem {
|
||||
std::string label; // Shown in menu
|
||||
NodeNum dest = NODENUM_BROADCAST;
|
||||
uint8_t channelIndex = 0;
|
||||
} *selectedRecipientItem;
|
||||
|
||||
int32_t runOnce() override;
|
||||
// These lists are generated when the menu page is populated
|
||||
// Cleared onBackground (when MenuApplet closes)
|
||||
std::vector<MessageItem> messageItems;
|
||||
std::vector<RecipientItem> recipientItems;
|
||||
} cm;
|
||||
|
||||
void execute(MenuItem item); // Perform the MenuAction associated with a MenuItem, if any
|
||||
void showPage(MenuPage page); // Load and display a MenuPage
|
||||
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
|
||||
|
||||
void populateSendPage(); // Dynamically create MenuItems including canned messages
|
||||
void populateRecipientPage(); // Dynamically create a page of possible destinations for a canned message
|
||||
void populateAppletPage(); // Dynamically create MenuItems for toggling loaded applets
|
||||
void populateAutoshowPage(); // Dynamically create MenuItems for selecting which applets can autoshow
|
||||
void populateRecentsPage(); // Create menu items: a choice of values for settings.recentlyActiveSeconds
|
||||
|
||||
uint16_t getSystemInfoPanelHeight();
|
||||
void drawSystemInfoPanel(int16_t left, int16_t top, uint16_t width,
|
||||
uint16_t *height = nullptr); // Info panel at top of root menu
|
||||
void sendText(NodeNum dest, ChannelIndex channel, const char *message); // Send a text message to mesh
|
||||
void freeCannedMessageResources(); // Clear MenuApplet's canned message processing data
|
||||
|
||||
MenuPage currentPage = MenuPage::ROOT;
|
||||
MenuPage previousPage = MenuPage::EXIT;
|
||||
uint8_t cursor = 0; // Which menu item is currently highlighted
|
||||
bool cursorShown = false; // Is *any* item highlighted? (Root menu: no initial selection)
|
||||
|
||||
uint16_t systemInfoPanelHeight = 0; // Need to know before we render
|
||||
|
||||
std::vector<MenuItem> items; // MenuItems for the current page. Filled by ShowPage
|
||||
|
||||
// Data for selecting and sending canned messages via the menu
|
||||
// Placed into a sub-class for organization only
|
||||
class CannedMessages
|
||||
{
|
||||
public:
|
||||
// Share NicheGraphics component
|
||||
// Handles loading, getting, setting
|
||||
CannedMessageStore *store;
|
||||
|
||||
// One canned message
|
||||
// Links the menu item to the true message text
|
||||
struct MessageItem {
|
||||
std::string label; // Shown in menu. Prefixed, and UTF-8 chars parsed
|
||||
std::string rawText; // The message which will be sent, if this item is selected
|
||||
} *selectedMessageItem;
|
||||
|
||||
// One possible destination for a canned message
|
||||
// Links the menu item to the intended recipient
|
||||
// May represent either broadcast or DM
|
||||
struct RecipientItem {
|
||||
std::string label; // Shown in menu
|
||||
NodeNum dest = NODENUM_BROADCAST;
|
||||
uint8_t channelIndex = 0;
|
||||
} *selectedRecipientItem;
|
||||
|
||||
// These lists are generated when the menu page is populated
|
||||
// Cleared onBackground (when MenuApplet closes)
|
||||
std::vector<MessageItem> messageItems;
|
||||
std::vector<RecipientItem> recipientItems;
|
||||
} cm;
|
||||
|
||||
Applet *borrowedTileOwner = nullptr; // Which applet we have temporarily replaced while displaying menu
|
||||
|
||||
bool invertedColors = false; // Helper to display current state of config.display.displaymode in InkHUD options
|
||||
bool invertedColors = false; // Helper to display current state of config.display.displaymode in InkHUD options
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -19,27 +19,23 @@ Added to MenuPages in InkHUD::showPage
|
||||
#include "./MenuAction.h"
|
||||
#include "./MenuPage.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
// One item of a MenuPage
|
||||
class MenuItem
|
||||
{
|
||||
public:
|
||||
std::string label;
|
||||
MenuAction action = NO_ACTION;
|
||||
MenuPage nextPage = EXIT;
|
||||
bool *checkState = nullptr;
|
||||
class MenuItem {
|
||||
public:
|
||||
std::string label;
|
||||
MenuAction action = NO_ACTION;
|
||||
MenuPage nextPage = EXIT;
|
||||
bool *checkState = nullptr;
|
||||
|
||||
// Various constructors, depending on the intended function of the item
|
||||
// Various constructors, depending on the intended function of the item
|
||||
|
||||
MenuItem(const char *label, MenuPage nextPage) : label(label), nextPage(nextPage) {}
|
||||
MenuItem(const char *label, MenuAction action) : label(label), action(action) {}
|
||||
MenuItem(const char *label, MenuAction action, MenuPage nextPage) : label(label), action(action), nextPage(nextPage) {}
|
||||
MenuItem(const char *label, MenuAction action, MenuPage nextPage, bool *checkState)
|
||||
: label(label), action(action), nextPage(nextPage), checkState(checkState)
|
||||
{
|
||||
}
|
||||
MenuItem(const char *label, MenuPage nextPage) : label(label), nextPage(nextPage) {}
|
||||
MenuItem(const char *label, MenuAction action) : label(label), action(action) {}
|
||||
MenuItem(const char *label, MenuAction action, MenuPage nextPage) : label(label), action(action), nextPage(nextPage) {}
|
||||
MenuItem(const char *label, MenuAction action, MenuPage nextPage, bool *checkState)
|
||||
: label(label), action(action), nextPage(nextPage), checkState(checkState) {}
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -11,19 +11,18 @@ Structure of the menu is defined in InkHUD::showPage
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
// Sub-menu for MenuApplet
|
||||
enum MenuPage : uint8_t {
|
||||
ROOT, // Initial menu page
|
||||
SEND,
|
||||
CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message
|
||||
OPTIONS,
|
||||
APPLETS,
|
||||
AUTOSHOW,
|
||||
RECENTS, // Select length of "recentlyActiveSeconds"
|
||||
EXIT, // Dismiss the menu applet
|
||||
ROOT, // Initial menu page
|
||||
SEND,
|
||||
CANNEDMESSAGE_RECIPIENT, // Select destination for a canned message
|
||||
OPTIONS,
|
||||
APPLETS,
|
||||
AUTOSHOW,
|
||||
RECENTS, // Select length of "recentlyActiveSeconds"
|
||||
EXIT, // Dismiss the menu applet
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
A notification which might be displayed by the NotificationApplet
|
||||
|
||||
An instance of this class is offered to Applets via Applet::approveNotification, in case they want to veto the notification.
|
||||
An Applet should veto a notification if it is already displaying the same info which the notification would convey.
|
||||
An instance of this class is offered to Applets via Applet::approveNotification, in case they want to veto the
|
||||
notification. An Applet should veto a notification if it is already displaying the same info which the notification
|
||||
would convey.
|
||||
|
||||
*/
|
||||
|
||||
@@ -13,26 +14,24 @@ An Applet should veto a notification if it is already displaying the same info w
|
||||
|
||||
#include "configuration.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class Notification
|
||||
{
|
||||
public:
|
||||
enum Type : uint8_t { NOTIFICATION_MESSAGE_BROADCAST, NOTIFICATION_MESSAGE_DIRECT, NOTIFICATION_BATTERY } type;
|
||||
class Notification {
|
||||
public:
|
||||
enum Type : uint8_t { NOTIFICATION_MESSAGE_BROADCAST, NOTIFICATION_MESSAGE_DIRECT, NOTIFICATION_BATTERY } type;
|
||||
|
||||
uint32_t timestamp;
|
||||
uint32_t timestamp;
|
||||
|
||||
uint8_t getChannel() { return channel; }
|
||||
uint32_t getSender() { return sender; }
|
||||
uint8_t getBatteryPercentage() { return batteryPercentage; }
|
||||
uint8_t getChannel() { return channel; }
|
||||
uint32_t getSender() { return sender; }
|
||||
uint8_t getBatteryPercentage() { return batteryPercentage; }
|
||||
|
||||
friend class NotificationApplet;
|
||||
friend class NotificationApplet;
|
||||
|
||||
protected:
|
||||
uint8_t channel;
|
||||
uint32_t sender;
|
||||
uint8_t batteryPercentage;
|
||||
protected:
|
||||
uint8_t channel;
|
||||
uint32_t sender;
|
||||
uint8_t batteryPercentage;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -12,268 +12,247 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::NotificationApplet::NotificationApplet()
|
||||
{
|
||||
textMessageObserver.observe(textMessageModule);
|
||||
}
|
||||
InkHUD::NotificationApplet::NotificationApplet() { textMessageObserver.observe(textMessageModule); }
|
||||
|
||||
// Collect meta-info about the text message, and ask for approval for the notification
|
||||
// No need to save the message itself; we can use the cached InkHUD::latestMessage data during render()
|
||||
int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
// System applets are always active
|
||||
assert(isActive());
|
||||
int InkHUD::NotificationApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) {
|
||||
// System applets are always active
|
||||
assert(isActive());
|
||||
|
||||
// Abort if feature disabled
|
||||
// This is a bit clumsy, but avoids complicated handling when the feature is enabled / disabled
|
||||
if (!settings->optionalFeatures.notifications)
|
||||
return 0;
|
||||
|
||||
// Abort if this is an outgoing message
|
||||
if (getFrom(p) == nodeDB->getNodeNum())
|
||||
return 0;
|
||||
|
||||
Notification n;
|
||||
n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
||||
|
||||
// Gather info: in-channel message
|
||||
if (isBroadcast(p->to)) {
|
||||
n.type = Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
||||
n.channel = p->channel;
|
||||
}
|
||||
|
||||
// Gather info: DM
|
||||
else {
|
||||
n.type = Notification::Type::NOTIFICATION_MESSAGE_DIRECT;
|
||||
n.sender = p->from;
|
||||
}
|
||||
|
||||
// Close an old notification, if shown
|
||||
dismiss();
|
||||
|
||||
// Check if we should display the notification
|
||||
// A foreground applet might already be displaying this info
|
||||
hasNotification = true;
|
||||
currentNotification = n;
|
||||
if (isApproved()) {
|
||||
bringToForeground();
|
||||
inkhud->forceUpdate();
|
||||
} else
|
||||
hasNotification = false; // Clear the pending notification: it was rejected
|
||||
|
||||
// Return zero: no issues here, carry on notifying other observers!
|
||||
// Abort if feature disabled
|
||||
// This is a bit clumsy, but avoids complicated handling when the feature is enabled / disabled
|
||||
if (!settings->optionalFeatures.notifications)
|
||||
return 0;
|
||||
|
||||
// Abort if this is an outgoing message
|
||||
if (getFrom(p) == nodeDB->getNodeNum())
|
||||
return 0;
|
||||
|
||||
Notification n;
|
||||
n.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
||||
|
||||
// Gather info: in-channel message
|
||||
if (isBroadcast(p->to)) {
|
||||
n.type = Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
||||
n.channel = p->channel;
|
||||
}
|
||||
|
||||
// Gather info: DM
|
||||
else {
|
||||
n.type = Notification::Type::NOTIFICATION_MESSAGE_DIRECT;
|
||||
n.sender = p->from;
|
||||
}
|
||||
|
||||
// Close an old notification, if shown
|
||||
dismiss();
|
||||
|
||||
// Check if we should display the notification
|
||||
// A foreground applet might already be displaying this info
|
||||
hasNotification = true;
|
||||
currentNotification = n;
|
||||
if (isApproved()) {
|
||||
bringToForeground();
|
||||
inkhud->forceUpdate();
|
||||
} else
|
||||
hasNotification = false; // Clear the pending notification: it was rejected
|
||||
|
||||
// Return zero: no issues here, carry on notifying other observers!
|
||||
return 0;
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onRender()
|
||||
{
|
||||
// Clear the region beneath the tile
|
||||
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
||||
// We do need to do this with the battery though, as it is an "overlay"
|
||||
fillRect(0, 0, width(), height(), WHITE);
|
||||
void InkHUD::NotificationApplet::onRender() {
|
||||
// Clear the region beneath the tile
|
||||
// Most applets are drawing onto an empty frame buffer and don't need to do this
|
||||
// We do need to do this with the battery though, as it is an "overlay"
|
||||
fillRect(0, 0, width(), height(), WHITE);
|
||||
|
||||
// Padding (horizontal)
|
||||
const uint16_t padW = 4;
|
||||
// Padding (horizontal)
|
||||
const uint16_t padW = 4;
|
||||
|
||||
// Main border
|
||||
drawRect(0, 0, width(), height(), BLACK);
|
||||
// drawRect(1, 1, width() - 2, height() - 2, BLACK);
|
||||
// Main border
|
||||
drawRect(0, 0, width(), height(), BLACK);
|
||||
// drawRect(1, 1, width() - 2, height() - 2, BLACK);
|
||||
|
||||
// Timestamp (potentially)
|
||||
// ====================
|
||||
std::string ts = getTimeString(currentNotification.timestamp);
|
||||
uint16_t tsW = 0;
|
||||
int16_t divX = 0;
|
||||
// Timestamp (potentially)
|
||||
// ====================
|
||||
std::string ts = getTimeString(currentNotification.timestamp);
|
||||
uint16_t tsW = 0;
|
||||
int16_t divX = 0;
|
||||
|
||||
// Timestamp available
|
||||
if (ts.length() > 0) {
|
||||
tsW = getTextWidth(ts);
|
||||
divX = padW + tsW + padW;
|
||||
// Timestamp available
|
||||
if (ts.length() > 0) {
|
||||
tsW = getTextWidth(ts);
|
||||
divX = padW + tsW + padW;
|
||||
|
||||
hatchRegion(0, 0, divX, height(), 2, BLACK); // Fill with a dark background
|
||||
drawLine(divX, 0, divX, height() - 1, BLACK); // Draw divider between timestamp and main text
|
||||
hatchRegion(0, 0, divX, height(), 2, BLACK); // Fill with a dark background
|
||||
drawLine(divX, 0, divX, height() - 1, BLACK); // Draw divider between timestamp and main text
|
||||
|
||||
setCrop(1, 1, divX - 1, height() - 2);
|
||||
|
||||
// Drop shadow
|
||||
setTextColor(WHITE);
|
||||
printThick(padW + (tsW / 2), height() / 2, ts, 4, 4);
|
||||
|
||||
// Bold text
|
||||
setTextColor(BLACK);
|
||||
printThick(padW + (tsW / 2), height() / 2, ts, 2, 1);
|
||||
}
|
||||
|
||||
// Main text
|
||||
// =====================
|
||||
|
||||
// Background fill
|
||||
// - medium dark (1/3)
|
||||
hatchRegion(divX, 0, width() - divX - 1, height(), 3, BLACK);
|
||||
|
||||
uint16_t availableWidth = width() - divX - padW;
|
||||
std::string text = getNotificationText(availableWidth);
|
||||
|
||||
int16_t textM = divX + padW + (getTextWidth(text) / 2);
|
||||
|
||||
// Restrict area for printing
|
||||
// - don't overlap border, or divider
|
||||
setCrop(divX + 1, 1, (width() - (divX + 1) - 1), height() - 2);
|
||||
setCrop(1, 1, divX - 1, height() - 2);
|
||||
|
||||
// Drop shadow
|
||||
// - thick white text
|
||||
setTextColor(WHITE);
|
||||
printThick(textM, height() / 2, text, 4, 4);
|
||||
printThick(padW + (tsW / 2), height() / 2, ts, 4, 4);
|
||||
|
||||
// Main text
|
||||
// - faux bold: double width
|
||||
// Bold text
|
||||
setTextColor(BLACK);
|
||||
printThick(textM, height() / 2, text, 2, 1);
|
||||
printThick(padW + (tsW / 2), height() / 2, ts, 2, 1);
|
||||
}
|
||||
|
||||
// Main text
|
||||
// =====================
|
||||
|
||||
// Background fill
|
||||
// - medium dark (1/3)
|
||||
hatchRegion(divX, 0, width() - divX - 1, height(), 3, BLACK);
|
||||
|
||||
uint16_t availableWidth = width() - divX - padW;
|
||||
std::string text = getNotificationText(availableWidth);
|
||||
|
||||
int16_t textM = divX + padW + (getTextWidth(text) / 2);
|
||||
|
||||
// Restrict area for printing
|
||||
// - don't overlap border, or divider
|
||||
setCrop(divX + 1, 1, (width() - (divX + 1) - 1), height() - 2);
|
||||
|
||||
// Drop shadow
|
||||
// - thick white text
|
||||
setTextColor(WHITE);
|
||||
printThick(textM, height() / 2, text, 4, 4);
|
||||
|
||||
// Main text
|
||||
// - faux bold: double width
|
||||
setTextColor(BLACK);
|
||||
printThick(textM, height() / 2, text, 2, 1);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onForeground()
|
||||
{
|
||||
handleInput = true; // Intercept the button input for our applet, so we can dismiss the notification
|
||||
void InkHUD::NotificationApplet::onForeground() {
|
||||
handleInput = true; // Intercept the button input for our applet, so we can dismiss the notification
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onBackground()
|
||||
{
|
||||
handleInput = false;
|
||||
void InkHUD::NotificationApplet::onBackground() { handleInput = false; }
|
||||
|
||||
void InkHUD::NotificationApplet::onButtonShortPress() {
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onButtonShortPress()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
void InkHUD::NotificationApplet::onButtonLongPress() {
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onButtonLongPress()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
void InkHUD::NotificationApplet::onExitShort() {
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onExitShort()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
void InkHUD::NotificationApplet::onExitLong() {
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onExitLong()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
void InkHUD::NotificationApplet::onNavUp() {
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onNavUp()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
void InkHUD::NotificationApplet::onNavDown() {
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onNavDown()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
void InkHUD::NotificationApplet::onNavLeft() {
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onNavLeft()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::NotificationApplet::onNavRight()
|
||||
{
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
void InkHUD::NotificationApplet::onNavRight() {
|
||||
dismiss();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
// Ask the WindowManager to check whether any displayed applets are already displaying the info from this notification
|
||||
// Called internally when we first get a "notifiable event", and then again before render,
|
||||
// in case autoshow swapped which applet was displayed
|
||||
bool InkHUD::NotificationApplet::isApproved()
|
||||
{
|
||||
// Instead of an assert
|
||||
if (!hasNotification) {
|
||||
LOG_WARN("No notif to approve");
|
||||
return false;
|
||||
}
|
||||
bool InkHUD::NotificationApplet::isApproved() {
|
||||
// Instead of an assert
|
||||
if (!hasNotification) {
|
||||
LOG_WARN("No notif to approve");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ask all visible user applets for approval
|
||||
for (Applet *ua : inkhud->userApplets) {
|
||||
if (ua->isForeground() && !ua->approveNotification(currentNotification))
|
||||
return false;
|
||||
}
|
||||
// Ask all visible user applets for approval
|
||||
for (Applet *ua : inkhud->userApplets) {
|
||||
if (ua->isForeground() && !ua->approveNotification(currentNotification))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mark that the notification should no-longer be rendered
|
||||
// In addition to calling thing method, code needs to request a re-render of all applets
|
||||
void InkHUD::NotificationApplet::dismiss()
|
||||
{
|
||||
sendToBackground();
|
||||
hasNotification = false;
|
||||
// Not requesting update directly from this method,
|
||||
// as it is used to dismiss notifications which have been made redundant by autoshow settings, before they are ever drawn
|
||||
void InkHUD::NotificationApplet::dismiss() {
|
||||
sendToBackground();
|
||||
hasNotification = false;
|
||||
// Not requesting update directly from this method,
|
||||
// as it is used to dismiss notifications which have been made redundant by autoshow settings, before they are ever
|
||||
// drawn
|
||||
}
|
||||
|
||||
// Get a string for the main body text of a notification
|
||||
// Formatted to suit screen width
|
||||
// Takes info from InkHUD::currentNotification
|
||||
std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvailable)
|
||||
{
|
||||
assert(hasNotification);
|
||||
std::string InkHUD::NotificationApplet::getNotificationText(uint16_t widthAvailable) {
|
||||
assert(hasNotification);
|
||||
|
||||
std::string text;
|
||||
std::string text;
|
||||
|
||||
// Text message
|
||||
// ==============
|
||||
// Text message
|
||||
// ==============
|
||||
|
||||
if (IS_ONE_OF(currentNotification.type, Notification::Type::NOTIFICATION_MESSAGE_DIRECT,
|
||||
Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) {
|
||||
if (IS_ONE_OF(currentNotification.type, Notification::Type::NOTIFICATION_MESSAGE_DIRECT, Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)) {
|
||||
|
||||
// Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently
|
||||
bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
||||
// Although we are handling DM and broadcast notifications together, we do need to treat them slightly differently
|
||||
bool isBroadcast = currentNotification.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST;
|
||||
|
||||
// Pick source of message
|
||||
MessageStore::Message *message =
|
||||
isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
|
||||
// Pick source of message
|
||||
MessageStore::Message *message = isBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
|
||||
|
||||
// Find info about the sender
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);
|
||||
// Find info about the sender
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(message->sender);
|
||||
|
||||
// Leading tag (channel vs. DM)
|
||||
text += isBroadcast ? "From:" : "DM: ";
|
||||
// Leading tag (channel vs. DM)
|
||||
text += isBroadcast ? "From:" : "DM: ";
|
||||
|
||||
// Sender id
|
||||
if (node && node->has_user)
|
||||
text += parseShortName(node);
|
||||
else
|
||||
text += hexifyNodeNum(message->sender);
|
||||
// Sender id
|
||||
if (node && node->has_user)
|
||||
text += parseShortName(node);
|
||||
else
|
||||
text += hexifyNodeNum(message->sender);
|
||||
|
||||
// Check if text fits
|
||||
// - use a longer string, if we have the space
|
||||
if (getTextWidth(text) < widthAvailable * 0.5) {
|
||||
text.clear();
|
||||
// Check if text fits
|
||||
// - use a longer string, if we have the space
|
||||
if (getTextWidth(text) < widthAvailable * 0.5) {
|
||||
text.clear();
|
||||
|
||||
// Leading tag (channel vs. DM)
|
||||
text += isBroadcast ? "Msg from " : "DM from ";
|
||||
// Leading tag (channel vs. DM)
|
||||
text += isBroadcast ? "Msg from " : "DM from ";
|
||||
|
||||
// Sender id
|
||||
if (node && node->has_user)
|
||||
text += parseShortName(node);
|
||||
else
|
||||
text += hexifyNodeNum(message->sender);
|
||||
// Sender id
|
||||
if (node && node->has_user)
|
||||
text += parseShortName(node);
|
||||
else
|
||||
text += hexifyNodeNum(message->sender);
|
||||
|
||||
text += ": ";
|
||||
text += message->text;
|
||||
}
|
||||
text += ": ";
|
||||
text += message->text;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse any non-ascii characters and return
|
||||
return parse(text);
|
||||
// Parse any non-ascii characters and return
|
||||
return parse(text);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -18,40 +18,38 @@ Feature should be optional; enable disable via on-screen menu
|
||||
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class NotificationApplet : public SystemApplet
|
||||
{
|
||||
public:
|
||||
NotificationApplet();
|
||||
class NotificationApplet : public SystemApplet {
|
||||
public:
|
||||
NotificationApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonShortPress() override;
|
||||
void onButtonLongPress() override;
|
||||
void onExitShort() override;
|
||||
void onExitLong() override;
|
||||
void onNavUp() override;
|
||||
void onNavDown() override;
|
||||
void onNavLeft() override;
|
||||
void onNavRight() override;
|
||||
void onRender() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonShortPress() override;
|
||||
void onButtonLongPress() override;
|
||||
void onExitShort() override;
|
||||
void onExitLong() override;
|
||||
void onNavUp() override;
|
||||
void onNavDown() override;
|
||||
void onNavLeft() override;
|
||||
void onNavRight() override;
|
||||
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
|
||||
|
||||
bool isApproved(); // Does a foreground applet make notification redundant?
|
||||
void dismiss(); // Close the Notification Popup
|
||||
bool isApproved(); // Does a foreground applet make notification redundant?
|
||||
void dismiss(); // Close the Notification Popup
|
||||
|
||||
protected:
|
||||
// Get notified when a new text message arrives
|
||||
CallbackObserver<NotificationApplet, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<NotificationApplet, const meshtastic_MeshPacket *>(this, &NotificationApplet::onReceiveTextMessage);
|
||||
protected:
|
||||
// Get notified when a new text message arrives
|
||||
CallbackObserver<NotificationApplet, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<NotificationApplet, const meshtastic_MeshPacket *>(this, &NotificationApplet::onReceiveTextMessage);
|
||||
|
||||
std::string getNotificationText(uint16_t widthAvailable); // Get text for notification, to suit screen width
|
||||
std::string getNotificationText(uint16_t widthAvailable); // Get text for notification, to suit screen width
|
||||
|
||||
bool hasNotification = false; // Only used for assert. Todo: remove?
|
||||
Notification currentNotification = Notification(); // Set when something notification-worthy happens. Used by render()
|
||||
bool hasNotification = false; // Only used for assert. Todo: remove?
|
||||
Notification currentNotification = Notification(); // Set when something notification-worthy happens. Used by render()
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -4,74 +4,67 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::PairingApplet::PairingApplet()
|
||||
{
|
||||
bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus);
|
||||
InkHUD::PairingApplet::PairingApplet() { bluetoothStatusObserver.observe(&bluetoothStatus->onNewStatus); }
|
||||
|
||||
void InkHUD::PairingApplet::onRender() {
|
||||
// Header
|
||||
setFont(fontMedium);
|
||||
printAt(X(0.5), Y(0.25), "Bluetooth", CENTER, BOTTOM);
|
||||
setFont(fontSmall);
|
||||
printAt(X(0.5), Y(0.25), "Enter this code", CENTER, TOP);
|
||||
|
||||
// Passkey
|
||||
setFont(fontMedium);
|
||||
printThick(X(0.5), Y(0.5), passkey.substr(0, 3) + " " + passkey.substr(3), 3, 2);
|
||||
|
||||
// Device's bluetooth name, if it will fit
|
||||
setFont(fontSmall);
|
||||
std::string name = "Name: " + parse(getDeviceName());
|
||||
if (getTextWidth(name) > width()) // Too wide, try without the leading "Name: "
|
||||
name = parse(getDeviceName());
|
||||
if (getTextWidth(name) < width()) // Does it fit?
|
||||
printAt(X(0.5), Y(0.75), name, CENTER, MIDDLE);
|
||||
}
|
||||
|
||||
void InkHUD::PairingApplet::onRender()
|
||||
{
|
||||
// Header
|
||||
setFont(fontMedium);
|
||||
printAt(X(0.5), Y(0.25), "Bluetooth", CENTER, BOTTOM);
|
||||
setFont(fontSmall);
|
||||
printAt(X(0.5), Y(0.25), "Enter this code", CENTER, TOP);
|
||||
void InkHUD::PairingApplet::onForeground() {
|
||||
// Prevent most other applets from requesting update, and skip their rendering entirely
|
||||
// Another system applet with a higher precedence can potentially ignore this
|
||||
SystemApplet::lockRendering = true;
|
||||
SystemApplet::lockRequests = true;
|
||||
}
|
||||
void InkHUD::PairingApplet::onBackground() {
|
||||
// Allow normal update behavior to resume
|
||||
SystemApplet::lockRendering = false;
|
||||
SystemApplet::lockRequests = false;
|
||||
|
||||
// Passkey
|
||||
setFont(fontMedium);
|
||||
printThick(X(0.5), Y(0.5), passkey.substr(0, 3) + " " + passkey.substr(3), 3, 2);
|
||||
|
||||
// Device's bluetooth name, if it will fit
|
||||
setFont(fontSmall);
|
||||
std::string name = "Name: " + parse(getDeviceName());
|
||||
if (getTextWidth(name) > width()) // Too wide, try without the leading "Name: "
|
||||
name = parse(getDeviceName());
|
||||
if (getTextWidth(name) < width()) // Does it fit?
|
||||
printAt(X(0.5), Y(0.75), name, CENTER, MIDDLE);
|
||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
void InkHUD::PairingApplet::onForeground()
|
||||
{
|
||||
// Prevent most other applets from requesting update, and skip their rendering entirely
|
||||
// Another system applet with a higher precedence can potentially ignore this
|
||||
SystemApplet::lockRendering = true;
|
||||
SystemApplet::lockRequests = true;
|
||||
}
|
||||
void InkHUD::PairingApplet::onBackground()
|
||||
{
|
||||
// Allow normal update behavior to resume
|
||||
SystemApplet::lockRendering = false;
|
||||
SystemApplet::lockRequests = false;
|
||||
int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status) {
|
||||
// The standard Meshtastic convention is to pass these "generic" Status objects,
|
||||
// check their type, and then cast them.
|
||||
// We'll mimic that behavior, just to keep in line with the other Statuses,
|
||||
// even though I'm not sure what the original reason for jumping through these extra hoops was.
|
||||
assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH);
|
||||
meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status;
|
||||
|
||||
// Need to force an update, as a polite request wouldn't be honored, seeing how we are now in the background
|
||||
// Usually, onBackground is followed by another applet's onForeground (which requests update), but not in this case
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
// When pairing begins
|
||||
if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) {
|
||||
// Store the passkey for rendering
|
||||
passkey = bluetoothStatus->getPasskey();
|
||||
|
||||
int InkHUD::PairingApplet::onBluetoothStatusUpdate(const meshtastic::Status *status)
|
||||
{
|
||||
// The standard Meshtastic convention is to pass these "generic" Status objects,
|
||||
// check their type, and then cast them.
|
||||
// We'll mimic that behavior, just to keep in line with the other Statuses,
|
||||
// even though I'm not sure what the original reason for jumping through these extra hoops was.
|
||||
assert(status->getStatusType() == STATUS_TYPE_BLUETOOTH);
|
||||
meshtastic::BluetoothStatus *bluetoothStatus = (meshtastic::BluetoothStatus *)status;
|
||||
// Show pairing screen
|
||||
bringToForeground();
|
||||
}
|
||||
|
||||
// When pairing begins
|
||||
if (bluetoothStatus->getConnectionState() == meshtastic::BluetoothStatus::ConnectionState::PAIRING) {
|
||||
// Store the passkey for rendering
|
||||
passkey = bluetoothStatus->getPasskey();
|
||||
// When pairing ends
|
||||
// or rather, when something changes, and we shouldn't be showing the pairing screen
|
||||
else if (isForeground())
|
||||
sendToBackground();
|
||||
|
||||
// Show pairing screen
|
||||
bringToForeground();
|
||||
}
|
||||
|
||||
// When pairing ends
|
||||
// or rather, when something changes, and we shouldn't be showing the pairing screen
|
||||
else if (isForeground())
|
||||
sendToBackground();
|
||||
|
||||
return 0; // No special result to report back to Observable
|
||||
return 0; // No special result to report back to Observable
|
||||
}
|
||||
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user