Merge pull request #3180 from fdlamotte/thinknode_m8

thinknode_m8: initial support

adds some functionalities to ui_task and epddisplay:

epd display, 
- supports screen off, and clear screen
- supports forcing full refresh every N partial refreshes (off by default, configured to 60 for M8)

ui_task
- UI_SHOW_CLOCK flag to display clock on first screen
- rotary button can act like joystick (with regards to Enter key), defaults to old config for existing nodes
- screen off on long press when using rotary button (very handy for the watch)
This commit is contained in:
fdlamotte authored and GitHub committed 2026-08-18 09:33:42 -04:00
commit 29b33467fb
15 files changed
+709 -13

No files matched your search

+1
View File
@@ -23,6 +23,7 @@ public:
virtual bool isOn() = 0;
virtual bool isEink() { return false; } // default to non-eink, override in eink drivers
virtual void forceFullRefresh() {} // next refresh will be full for eink
virtual void turnOn() = 0;
virtual void turnOff() = 0;
virtual void clear() = 0;
+44 -6
View File
@@ -14,6 +14,10 @@
SPIClass SPI1 = SPIClass(FSPI);
#endif
#ifndef EPD_WASHING_MACHINE_CYCLES
#define EPD_WASHING_MACHINE_CYCLES 0
#endif
// Color scheme
ColorVal UIColor::window_bkg = GxEPD_WHITE;
ColorVal UIColor::title_bkg = GxEPD_WHITE;
@@ -25,7 +29,6 @@ ColorVal UIColor::popup_bkg = GxEPD_WHITE;
ColorVal UIColor::popup_txt = GxEPD_BLACK;
ColorVal UIColor::corp_blue = GxEPD_BLACK;
bool GxEPDDisplay::begin() {
display.epd2.selectSPI(SPI1, SPISettings(4000000, MSBFIRST, SPI_MODE0));
#ifdef ESP32
@@ -36,15 +39,27 @@ bool GxEPDDisplay::begin() {
display.init(115200, true, 2, false);
display.setRotation(DISPLAY_ROTATION);
setTextSize(1); // Default to size 1
display.setPartialWindow(0, 0, display.width(), display.height());
display.fillScreen(GxEPD_WHITE);
display.display(true);
display.setFullWindow();
for (int i = 0; i < EPD_WASHING_MACHINE_CYCLES; i++) {
display.fillScreen(GxEPD_BLACK);
display.display(false);
delay(2000);
display.fillScreen(GxEPD_WHITE);
display.display(false);
delay(2000);
}
display.setPartialWindow(0, 0, display.width(), display.height());
resetPartialRefreshCounter();
#if DISP_BACKLIGHT
digitalWrite(DISP_BACKLIGHT, LOW);
pinMode(DISP_BACKLIGHT, OUTPUT);
#endif
_init = true;
_isOn = true;
return true;
}
@@ -55,7 +70,9 @@ void GxEPDDisplay::turnOn() {
#elif defined(EXP_PIN_BACKLIGHT) && !defined(BACKLIGHT_BTN)
expander.digitalWrite(EXP_PIN_BACKLIGHT, HIGH);
#endif
_isOn = true;
if (!_isOn) {
_isOn = true;
}
}
void GxEPDDisplay::turnOff() {
@@ -65,6 +82,13 @@ void GxEPDDisplay::turnOff() {
expander.digitalWrite(EXP_PIN_BACKLIGHT, LOW);
#endif
_isOn = false;
// do full refresh before powering off to clear screen
// no full refresh needed at wakeup
display.clearScreen(0xFF); // Clears microcontroller side RAM
display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers
resetPartialRefreshCounter();
last_display_crc_value=0;
display.hibernate();
}
void GxEPDDisplay::clear() {
@@ -77,6 +101,16 @@ void GxEPDDisplay::startFrame(ColorVal bkg) {
display.fillScreen(bkg);
display.setTextColor(_curr_color = UIColor::primary_txt);
display_crc.reset();
if (_cycles_before_full_refresh != 0) {
display.setPartialWindow(0, 0, display.width(), display.height());
} else {
// forces a full wipe of the screen ...
display.clearScreen(0xFF); // Clears microcontroller side RAM
display.writeScreenBuffer(0xFF); // Forces 0xFF (White) into the display controller's history registers
// we'll need a partial refresh after that (whatever crc value is)
last_display_crc_value = 0;
resetPartialRefreshCounter();
}
}
void GxEPDDisplay::setTextSize(int sz) {
@@ -178,9 +212,13 @@ uint16_t GxEPDDisplay::getTextWidth(const char* str) {
}
void GxEPDDisplay::endFrame() {
if (_isOn == false) return;
uint32_t crc = display_crc.finalize();
if (crc != last_display_crc_value) {
display.display(true);
last_display_crc_value = crc;
if (_cycles_before_full_refresh > 0) {
_cycles_before_full_refresh--;
}
}
last_display_crc_value = crc;
}
+10
View File
@@ -16,6 +16,12 @@
#include "DisplayDriver.h"
#ifndef EINK_MAX_PARTIAL_REFRESH
// 60 to prevent ghosting when refreshing every minute ...
// set to -1 to disable the counter
#define EINK_MAX_PARTIAL_REFRESH -1
#endif
class GxEPDDisplay : public DisplayDriver {
#if defined(EINK_DISPLAY_MODEL)
@@ -36,6 +42,8 @@ class GxEPDDisplay : public DisplayDriver {
uint16_t _curr_color;
CRC32 display_crc;
int last_display_crc_value = 0;
int _cycles_before_full_refresh;
int max_partial_refresh = EINK_MAX_PARTIAL_REFRESH; // so this can be changed by an option after
public:
#if defined(EINK_DISPLAY_MODEL)
@@ -48,6 +56,8 @@ public:
bool isOn() override { return _isOn; }
bool isEink() override { return true; }
void forceFullRefresh() override { _cycles_before_full_refresh = 0; };
void resetPartialRefreshCounter() { _cycles_before_full_refresh = max_partial_refresh; };
void turnOn() override;
void turnOff() override;
void clear() override;
+2 -2
View File
@@ -25,8 +25,8 @@ class MomentaryButton {
public:
MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false, bool pulldownup=false, bool multiclick=true);
MomentaryButton(int8_t pin, int long_press_mills, int analog_threshold);
void begin();
int check(bool repeat_click=false); // returns one of BUTTON_EVENT_*
virtual void begin();
virtual int check(bool repeat_click=false); // returns one of BUTTON_EVENT_*
void cancelClick(); // suppress next BUTTON_EVENT_CLICK (if already in DOWN state)
uint8_t getPin() { return _pin; }
bool isPressed() const;
+34
View File
@@ -0,0 +1,34 @@
#include "RotaryInputGPIO.h"
bool RotaryInputGPIO::begin() {
if (_pin_a >= 0) {
pinMode(_pin_a, _pull_a ? INPUT_PULLUP : INPUT);
}
if (_pin_b >= 0) {
pinMode(_pin_b, _pull_b ? INPUT_PULLUP : INPUT);
}
b_prec = digitalRead(_pin_b);
return true;
}
RotaryInputEvent RotaryInputGPIO::poll() {
RotaryInputEvent ev = RotaryInputEvent::None;
bool a = digitalRead(_pin_a);
bool b = digitalRead(_pin_b);
// this is the simplest scheme and it works well
// for thinknode M8, just read A when B rises ;)
if (!b_prec && b) { // rising edge of A
if (a) {
ev = RotaryInputEvent::Next;
} else {
ev = RotaryInputEvent::Prev;
}
}
b_prec = b;
return ev;
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include <Arduino.h>
#include "RotaryInput.h"
#define BUTTON_EVENT_UP 5
#define BUTTON_EVENT_DOWN 6
class RotaryInputGPIO: public RotaryInput {
int8_t _pin_a;
bool _pull_a;
int8_t _pin_b;
bool _pull_b;
bool b_prec;
public:
RotaryInputGPIO(int8_t pin_a, int8_t pin_b, bool pull_a=false, bool pull_b=false): _pin_a(pin_a), _pin_b(pin_b), _pull_a(pull_a), _pull_b(pull_b) {}
bool begin() override;
RotaryInputEvent poll() override;
bool isReady() const override { return true;}
};