mirror of
https://github.com/meshtastic/firmware.git
synced 2026-01-29 21:22:03 +00:00
add a .clang-format file (#9154)
This commit is contained in:
@@ -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
@@ -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
|
||||
@@ -14,26 +14,24 @@
|
||||
|
||||
#include "main.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class PairingApplet : public SystemApplet
|
||||
{
|
||||
public:
|
||||
PairingApplet();
|
||||
class PairingApplet : public SystemApplet {
|
||||
public:
|
||||
PairingApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onRender() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
|
||||
int onBluetoothStatusUpdate(const meshtastic::Status *status);
|
||||
int onBluetoothStatusUpdate(const meshtastic::Status *status);
|
||||
|
||||
protected:
|
||||
// Get notified when status of the Bluetooth connection changes
|
||||
CallbackObserver<PairingApplet, const meshtastic::Status *> bluetoothStatusObserver =
|
||||
CallbackObserver<PairingApplet, const meshtastic::Status *>(this, &PairingApplet::onBluetoothStatusUpdate);
|
||||
protected:
|
||||
// Get notified when status of the Bluetooth connection changes
|
||||
CallbackObserver<PairingApplet, const meshtastic::Status *> bluetoothStatusObserver =
|
||||
CallbackObserver<PairingApplet, const meshtastic::Status *>(this, &PairingApplet::onBluetoothStatusUpdate);
|
||||
|
||||
std::string passkey = ""; // Passkey. Six digits, possibly with leading zeros
|
||||
std::string passkey = ""; // Passkey. Six digits, possibly with leading zeros
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::PlaceholderApplet::onRender()
|
||||
{
|
||||
// This placeholder applet fills its area with sparse diagonal lines
|
||||
hatchRegion(0, 0, width(), height(), 8, BLACK);
|
||||
void InkHUD::PlaceholderApplet::onRender() {
|
||||
// This placeholder applet fills its area with sparse diagonal lines
|
||||
hatchRegion(0, 0, width(), height(), 8, BLACK);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -11,17 +11,15 @@ Fills the area with diagonal lines
|
||||
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class PlaceholderApplet : public SystemApplet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
class PlaceholderApplet : public SystemApplet {
|
||||
public:
|
||||
void onRender() override;
|
||||
|
||||
// Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet.
|
||||
// The window manager decides when and where it should be rendered
|
||||
// It may be drawn to several different tiles during an Renderer::render call
|
||||
// Note: onForeground, onBackground, and wantsToRender are not meaningful for this applet.
|
||||
// The window manager decides when and where it should be rendered
|
||||
// It may be drawn to several different tiles during an Renderer::render call
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -8,248 +8,240 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::TipsApplet::TipsApplet()
|
||||
{
|
||||
// Decide which tips (if any) should be shown to user after the boot screen
|
||||
InkHUD::TipsApplet::TipsApplet() {
|
||||
// Decide which tips (if any) should be shown to user after the boot screen
|
||||
|
||||
// Welcome screen
|
||||
if (settings->tips.firstBoot)
|
||||
tipQueue.push_back(Tip::WELCOME);
|
||||
// Welcome screen
|
||||
if (settings->tips.firstBoot)
|
||||
tipQueue.push_back(Tip::WELCOME);
|
||||
|
||||
// Antenna, region, timezone
|
||||
// Shown at boot if region not yet set
|
||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
||||
tipQueue.push_back(Tip::FINISH_SETUP);
|
||||
// Antenna, region, timezone
|
||||
// Shown at boot if region not yet set
|
||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
|
||||
tipQueue.push_back(Tip::FINISH_SETUP);
|
||||
|
||||
// Shutdown info
|
||||
// Shown until user performs one valid shutdown
|
||||
if (!settings->tips.safeShutdownSeen)
|
||||
tipQueue.push_back(Tip::SAFE_SHUTDOWN);
|
||||
// Shutdown info
|
||||
// Shown until user performs one valid shutdown
|
||||
if (!settings->tips.safeShutdownSeen)
|
||||
tipQueue.push_back(Tip::SAFE_SHUTDOWN);
|
||||
|
||||
// Using the UI
|
||||
if (settings->tips.firstBoot) {
|
||||
tipQueue.push_back(Tip::CUSTOMIZATION);
|
||||
tipQueue.push_back(Tip::BUTTONS);
|
||||
}
|
||||
// Using the UI
|
||||
if (settings->tips.firstBoot) {
|
||||
tipQueue.push_back(Tip::CUSTOMIZATION);
|
||||
tipQueue.push_back(Tip::BUTTONS);
|
||||
}
|
||||
|
||||
// Catch an incorrect attempt at rotating display
|
||||
if (config.display.flip_screen)
|
||||
tipQueue.push_back(Tip::ROTATION);
|
||||
// Catch an incorrect attempt at rotating display
|
||||
if (config.display.flip_screen)
|
||||
tipQueue.push_back(Tip::ROTATION);
|
||||
|
||||
// Applet is foreground immediately at boot, but is obscured by LogoApplet, which is also foreground
|
||||
// LogoApplet can be considered to have a higher Z-index, because it is placed before TipsApplet in the systemApplets vector
|
||||
if (!tipQueue.empty())
|
||||
bringToForeground();
|
||||
// Applet is foreground immediately at boot, but is obscured by LogoApplet, which is also foreground
|
||||
// LogoApplet can be considered to have a higher Z-index, because it is placed before TipsApplet in the systemApplets
|
||||
// vector
|
||||
if (!tipQueue.empty())
|
||||
bringToForeground();
|
||||
}
|
||||
|
||||
void InkHUD::TipsApplet::onRender()
|
||||
{
|
||||
switch (tipQueue.front()) {
|
||||
case Tip::WELCOME:
|
||||
renderWelcome();
|
||||
break;
|
||||
void InkHUD::TipsApplet::onRender() {
|
||||
switch (tipQueue.front()) {
|
||||
case Tip::WELCOME:
|
||||
renderWelcome();
|
||||
break;
|
||||
|
||||
case Tip::FINISH_SETUP: {
|
||||
setFont(fontMedium);
|
||||
printAt(0, 0, "Tip: Finish Setup");
|
||||
case Tip::FINISH_SETUP: {
|
||||
setFont(fontMedium);
|
||||
printAt(0, 0, "Tip: Finish Setup");
|
||||
|
||||
setFont(fontSmall);
|
||||
int16_t cursorY = fontMedium.lineHeight() * 1.5;
|
||||
printAt(0, cursorY, "- connect antenna");
|
||||
setFont(fontSmall);
|
||||
int16_t cursorY = fontMedium.lineHeight() * 1.5;
|
||||
printAt(0, cursorY, "- connect antenna");
|
||||
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- connect a client app");
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- connect a client app");
|
||||
|
||||
// Only if region not set
|
||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- set region");
|
||||
}
|
||||
|
||||
// Only if tz not set
|
||||
if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) {
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- set timezone");
|
||||
}
|
||||
|
||||
cursorY += fontSmall.lineHeight() * 1.5;
|
||||
printAt(0, cursorY, "More info at meshtastic.org");
|
||||
|
||||
setFont(fontSmall);
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
} break;
|
||||
|
||||
case Tip::SAFE_SHUTDOWN: {
|
||||
setFont(fontMedium);
|
||||
printAt(0, 0, "Tip: Shutdown");
|
||||
|
||||
setFont(fontSmall);
|
||||
std::string shutdown;
|
||||
shutdown += "Before removing power, please shut down from InkHUD menu, or a client app. \n";
|
||||
shutdown += "\n";
|
||||
shutdown += "This ensures data is saved.";
|
||||
printWrapped(0, fontMedium.lineHeight() * 1.5, width(), shutdown);
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
|
||||
} break;
|
||||
|
||||
case Tip::CUSTOMIZATION: {
|
||||
setFont(fontMedium);
|
||||
printAt(0, 0, "Tip: Customization");
|
||||
|
||||
setFont(fontSmall);
|
||||
printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
|
||||
"Configure & control display with the InkHUD menu. Optional features, layout, rotation, and more.");
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
} break;
|
||||
|
||||
case Tip::BUTTONS: {
|
||||
setFont(fontMedium);
|
||||
printAt(0, 0, "Tip: Buttons");
|
||||
|
||||
setFont(fontSmall);
|
||||
int16_t cursorY = fontMedium.lineHeight() * 1.5;
|
||||
|
||||
if (!settings->joystick.enabled) {
|
||||
printAt(0, cursorY, "User Button");
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- short press: next");
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- long press: select / open menu");
|
||||
} else {
|
||||
printAt(0, cursorY, "Joystick");
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- open menu / select");
|
||||
cursorY += fontSmall.lineHeight() * 1.5;
|
||||
printAt(0, cursorY, "Exit Button");
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- switch tile / close menu");
|
||||
}
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
} break;
|
||||
|
||||
case Tip::ROTATION: {
|
||||
setFont(fontMedium);
|
||||
printAt(0, 0, "Tip: Rotation");
|
||||
|
||||
setFont(fontSmall);
|
||||
if (!settings->joystick.enabled) {
|
||||
printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
|
||||
"To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate.");
|
||||
} else {
|
||||
printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
|
||||
"To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate.");
|
||||
}
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
|
||||
// Revert the "flip screen" setting, preventing this message showing again
|
||||
config.display.flip_screen = false;
|
||||
nodeDB->saveToDisk(SEGMENT_DEVICESTATE);
|
||||
} break;
|
||||
// Only if region not set
|
||||
if (config.lora.region == meshtastic_Config_LoRaConfig_RegionCode_UNSET) {
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- set region");
|
||||
}
|
||||
|
||||
// Only if tz not set
|
||||
if (!(*config.device.tzdef && config.device.tzdef[0] != 0)) {
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- set timezone");
|
||||
}
|
||||
|
||||
cursorY += fontSmall.lineHeight() * 1.5;
|
||||
printAt(0, cursorY, "More info at meshtastic.org");
|
||||
|
||||
setFont(fontSmall);
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
} break;
|
||||
|
||||
case Tip::SAFE_SHUTDOWN: {
|
||||
setFont(fontMedium);
|
||||
printAt(0, 0, "Tip: Shutdown");
|
||||
|
||||
setFont(fontSmall);
|
||||
std::string shutdown;
|
||||
shutdown += "Before removing power, please shut down from InkHUD menu, or a client app. \n";
|
||||
shutdown += "\n";
|
||||
shutdown += "This ensures data is saved.";
|
||||
printWrapped(0, fontMedium.lineHeight() * 1.5, width(), shutdown);
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
|
||||
} break;
|
||||
|
||||
case Tip::CUSTOMIZATION: {
|
||||
setFont(fontMedium);
|
||||
printAt(0, 0, "Tip: Customization");
|
||||
|
||||
setFont(fontSmall);
|
||||
printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
|
||||
"Configure & control display with the InkHUD menu. Optional features, layout, rotation, and more.");
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
} break;
|
||||
|
||||
case Tip::BUTTONS: {
|
||||
setFont(fontMedium);
|
||||
printAt(0, 0, "Tip: Buttons");
|
||||
|
||||
setFont(fontSmall);
|
||||
int16_t cursorY = fontMedium.lineHeight() * 1.5;
|
||||
|
||||
if (!settings->joystick.enabled) {
|
||||
printAt(0, cursorY, "User Button");
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- short press: next");
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- long press: select / open menu");
|
||||
} else {
|
||||
printAt(0, cursorY, "Joystick");
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- open menu / select");
|
||||
cursorY += fontSmall.lineHeight() * 1.5;
|
||||
printAt(0, cursorY, "Exit Button");
|
||||
cursorY += fontSmall.lineHeight() * 1.2;
|
||||
printAt(0, cursorY, "- switch tile / close menu");
|
||||
}
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
} break;
|
||||
|
||||
case Tip::ROTATION: {
|
||||
setFont(fontMedium);
|
||||
printAt(0, 0, "Tip: Rotation");
|
||||
|
||||
setFont(fontSmall);
|
||||
if (!settings->joystick.enabled) {
|
||||
printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
|
||||
"To rotate the display, use the InkHUD menu. Long-press the user button > Options > Rotate.");
|
||||
} else {
|
||||
printWrapped(0, fontMedium.lineHeight() * 1.5, width(),
|
||||
"To rotate the display, use the InkHUD menu. Press the user button > Options > Rotate.");
|
||||
}
|
||||
|
||||
printAt(0, Y(1.0), "Press button to continue", LEFT, BOTTOM);
|
||||
|
||||
// Revert the "flip screen" setting, preventing this message showing again
|
||||
config.display.flip_screen = false;
|
||||
nodeDB->saveToDisk(SEGMENT_DEVICESTATE);
|
||||
} break;
|
||||
}
|
||||
}
|
||||
|
||||
// This tip has its own render method, only because it's a big block of code
|
||||
// Didn't want to clutter up the switch in onRender too much
|
||||
void InkHUD::TipsApplet::renderWelcome()
|
||||
{
|
||||
uint16_t padW = X(0.05);
|
||||
void InkHUD::TipsApplet::renderWelcome() {
|
||||
uint16_t padW = X(0.05);
|
||||
|
||||
// Block 1 - logo & title
|
||||
// ========================
|
||||
// Block 1 - logo & title
|
||||
// ========================
|
||||
|
||||
// Logo size
|
||||
uint16_t logoWLimit = X(0.3);
|
||||
uint16_t logoHLimit = Y(0.3);
|
||||
uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit);
|
||||
uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit);
|
||||
// Logo size
|
||||
uint16_t logoWLimit = X(0.3);
|
||||
uint16_t logoHLimit = Y(0.3);
|
||||
uint16_t logoW = getLogoWidth(logoWLimit, logoHLimit);
|
||||
uint16_t logoH = getLogoHeight(logoWLimit, logoHLimit);
|
||||
|
||||
// Title size
|
||||
setFont(fontMedium);
|
||||
std::string title;
|
||||
if (width() >= 200) // Future proofing: hide if *tiny* display
|
||||
title = "meshtastic.org";
|
||||
uint16_t titleW = getTextWidth(title);
|
||||
// Title size
|
||||
setFont(fontMedium);
|
||||
std::string title;
|
||||
if (width() >= 200) // Future proofing: hide if *tiny* display
|
||||
title = "meshtastic.org";
|
||||
uint16_t titleW = getTextWidth(title);
|
||||
|
||||
// Center the block
|
||||
// Desired effect: equal margin from display edge for logo left and title right
|
||||
int16_t block1Y = Y(0.3);
|
||||
int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2);
|
||||
int16_t logoCX = block1CX - (logoW / 2) - (padW / 2);
|
||||
int16_t titleCX = block1CX + (titleW / 2) + (padW / 2);
|
||||
// Center the block
|
||||
// Desired effect: equal margin from display edge for logo left and title right
|
||||
int16_t block1Y = Y(0.3);
|
||||
int16_t block1CX = X(0.5) + (logoW / 2) - (titleW / 2);
|
||||
int16_t logoCX = block1CX - (logoW / 2) - (padW / 2);
|
||||
int16_t titleCX = block1CX + (titleW / 2) + (padW / 2);
|
||||
|
||||
// Draw block
|
||||
drawLogo(logoCX, block1Y, logoW, logoH);
|
||||
printAt(titleCX, block1Y, title, CENTER, MIDDLE);
|
||||
// Draw block
|
||||
drawLogo(logoCX, block1Y, logoW, logoH);
|
||||
printAt(titleCX, block1Y, title, CENTER, MIDDLE);
|
||||
|
||||
// Block 2 - subtitle
|
||||
// =======================
|
||||
setFont(fontSmall);
|
||||
std::string subtitle = "InkHUD";
|
||||
if (width() >= 200)
|
||||
subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display
|
||||
printAt(X(0.5), Y(0.6), subtitle, CENTER, MIDDLE);
|
||||
// Block 2 - subtitle
|
||||
// =======================
|
||||
setFont(fontSmall);
|
||||
std::string subtitle = "InkHUD";
|
||||
if (width() >= 200)
|
||||
subtitle += " - A Heads-Up Display"; // Future proofing: narrower for tiny display
|
||||
printAt(X(0.5), Y(0.6), subtitle, CENTER, MIDDLE);
|
||||
|
||||
// Block 3 - press to continue
|
||||
// ============================
|
||||
printAt(X(0.5), Y(1), "Press button to continue", CENTER, BOTTOM);
|
||||
// Block 3 - press to continue
|
||||
// ============================
|
||||
printAt(X(0.5), Y(1), "Press button to continue", CENTER, BOTTOM);
|
||||
}
|
||||
|
||||
void InkHUD::TipsApplet::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::TipsApplet::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;
|
||||
|
||||
SystemApplet::handleInput = true; // Our applet should handle button input (unless another system applet grabs it first)
|
||||
SystemApplet::handleInput = true; // Our applet should handle button input (unless another system applet grabs it first)
|
||||
}
|
||||
|
||||
void InkHUD::TipsApplet::onBackground()
|
||||
{
|
||||
// Allow normal update behavior to resume
|
||||
SystemApplet::lockRendering = false;
|
||||
SystemApplet::lockRequests = false;
|
||||
SystemApplet::handleInput = false;
|
||||
void InkHUD::TipsApplet::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);
|
||||
}
|
||||
|
||||
// While our SystemApplet::handleInput flag is true
|
||||
void InkHUD::TipsApplet::onButtonShortPress()
|
||||
{
|
||||
tipQueue.pop_front();
|
||||
void InkHUD::TipsApplet::onButtonShortPress() {
|
||||
tipQueue.pop_front();
|
||||
|
||||
// All tips done
|
||||
if (tipQueue.empty()) {
|
||||
// Record that user has now seen the "tutorial" set of tips
|
||||
// Don't show them on subsequent boots
|
||||
if (settings->tips.firstBoot) {
|
||||
settings->tips.firstBoot = false;
|
||||
inkhud->persistence->saveSettings();
|
||||
}
|
||||
|
||||
// Close applet, and full refresh to clean the screen
|
||||
// Need to force update, because our request would be ignored otherwise, as we are now background
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
// All tips done
|
||||
if (tipQueue.empty()) {
|
||||
// Record that user has now seen the "tutorial" set of tips
|
||||
// Don't show them on subsequent boots
|
||||
if (settings->tips.firstBoot) {
|
||||
settings->tips.firstBoot = false;
|
||||
inkhud->persistence->saveSettings();
|
||||
}
|
||||
|
||||
// More tips left
|
||||
else
|
||||
requestUpdate();
|
||||
// Close applet, and full refresh to clean the screen
|
||||
// Need to force update, because our request would be ignored otherwise, as we are now background
|
||||
sendToBackground();
|
||||
inkhud->forceUpdate(EInk::UpdateTypes::FULL);
|
||||
}
|
||||
|
||||
// More tips left
|
||||
else
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
// Functions the same as the user button in this instance
|
||||
void InkHUD::TipsApplet::onExitShort()
|
||||
{
|
||||
onButtonShortPress();
|
||||
}
|
||||
void InkHUD::TipsApplet::onExitShort() { onButtonShortPress(); }
|
||||
|
||||
#endif
|
||||
@@ -14,36 +14,34 @@
|
||||
|
||||
#include "graphics/niche/InkHUD/SystemApplet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class TipsApplet : public SystemApplet
|
||||
{
|
||||
protected:
|
||||
enum class Tip {
|
||||
WELCOME,
|
||||
FINISH_SETUP,
|
||||
SAFE_SHUTDOWN,
|
||||
CUSTOMIZATION,
|
||||
BUTTONS,
|
||||
ROTATION,
|
||||
};
|
||||
class TipsApplet : public SystemApplet {
|
||||
protected:
|
||||
enum class Tip {
|
||||
WELCOME,
|
||||
FINISH_SETUP,
|
||||
SAFE_SHUTDOWN,
|
||||
CUSTOMIZATION,
|
||||
BUTTONS,
|
||||
ROTATION,
|
||||
};
|
||||
|
||||
public:
|
||||
TipsApplet();
|
||||
public:
|
||||
TipsApplet();
|
||||
|
||||
void onRender() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonShortPress() override;
|
||||
void onExitShort() override;
|
||||
void onRender() override;
|
||||
void onForeground() override;
|
||||
void onBackground() override;
|
||||
void onButtonShortPress() override;
|
||||
void onExitShort() override;
|
||||
|
||||
protected:
|
||||
void renderWelcome(); // Very first screen of tutorial
|
||||
protected:
|
||||
void renderWelcome(); // Very first screen of tutorial
|
||||
|
||||
std::deque<Tip> tipQueue; // List of tips to show, one after another
|
||||
std::deque<Tip> tipQueue; // List of tips to show, one after another
|
||||
|
||||
WindowManager *windowManager = nullptr; // For convenience. Set in constructor.
|
||||
WindowManager *windowManager = nullptr; // For convenience. Set in constructor.
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -4,136 +4,127 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::AllMessageApplet::onActivate()
|
||||
{
|
||||
textMessageObserver.observe(textMessageModule);
|
||||
}
|
||||
void InkHUD::AllMessageApplet::onActivate() { textMessageObserver.observe(textMessageModule); }
|
||||
|
||||
void InkHUD::AllMessageApplet::onDeactivate()
|
||||
{
|
||||
textMessageObserver.unobserve(textMessageModule);
|
||||
}
|
||||
void InkHUD::AllMessageApplet::onDeactivate() { textMessageObserver.unobserve(textMessageModule); }
|
||||
|
||||
// We're not consuming the data passed to this method;
|
||||
// we're just just using it to trigger a render
|
||||
int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
// Abort if applet fully deactivated
|
||||
// Already handled by onActivate and onDeactivate, but good practice for all applets
|
||||
if (!isActive())
|
||||
return 0;
|
||||
|
||||
// Abort if this is an outgoing message
|
||||
if (getFrom(p) == nodeDB->getNodeNum())
|
||||
return 0;
|
||||
|
||||
requestAutoshow(); // Want to become foreground, if permitted
|
||||
requestUpdate(); // Want to update display, if applet is foreground
|
||||
|
||||
// Return zero: no issues here, carry on notifying other observers!
|
||||
int InkHUD::AllMessageApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) {
|
||||
// Abort if applet fully deactivated
|
||||
// Already handled by onActivate and onDeactivate, but good practice for all applets
|
||||
if (!isActive())
|
||||
return 0;
|
||||
|
||||
// Abort if this is an outgoing message
|
||||
if (getFrom(p) == nodeDB->getNodeNum())
|
||||
return 0;
|
||||
|
||||
requestAutoshow(); // Want to become foreground, if permitted
|
||||
requestUpdate(); // Want to update display, if applet is foreground
|
||||
|
||||
// Return zero: no issues here, carry on notifying other observers!
|
||||
return 0;
|
||||
}
|
||||
|
||||
void InkHUD::AllMessageApplet::onRender()
|
||||
{
|
||||
// Find newest message, regardless of whether DM or broadcast
|
||||
MessageStore::Message *message;
|
||||
if (latestMessage->wasBroadcast)
|
||||
message = &latestMessage->broadcast;
|
||||
else
|
||||
message = &latestMessage->dm;
|
||||
void InkHUD::AllMessageApplet::onRender() {
|
||||
// Find newest message, regardless of whether DM or broadcast
|
||||
MessageStore::Message *message;
|
||||
if (latestMessage->wasBroadcast)
|
||||
message = &latestMessage->broadcast;
|
||||
else
|
||||
message = &latestMessage->dm;
|
||||
|
||||
// Short circuit: no text message
|
||||
if (!message->sender) {
|
||||
printAt(X(0.5), Y(0.5), "No Message", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
// Short circuit: no text message
|
||||
if (!message->sender) {
|
||||
printAt(X(0.5), Y(0.5), "No Message", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Header (sender, timestamp)
|
||||
// ===========================
|
||||
// ===========================
|
||||
// Header (sender, timestamp)
|
||||
// ===========================
|
||||
|
||||
// Y position for divider
|
||||
// - between header text and messages
|
||||
// Y position for divider
|
||||
// - between header text and messages
|
||||
|
||||
std::string header;
|
||||
std::string header;
|
||||
|
||||
// RX Time
|
||||
// - if valid
|
||||
std::string timeString = getTimeString(message->timestamp);
|
||||
if (timeString.length() > 0) {
|
||||
header += timeString;
|
||||
header += ": ";
|
||||
}
|
||||
// RX Time
|
||||
// - if valid
|
||||
std::string timeString = getTimeString(message->timestamp);
|
||||
if (timeString.length() > 0) {
|
||||
header += timeString;
|
||||
header += ": ";
|
||||
}
|
||||
|
||||
// Sender's id
|
||||
// - short name and long name, if available, or
|
||||
// - node id
|
||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender);
|
||||
if (sender && sender->has_user) {
|
||||
header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
|
||||
header += " (";
|
||||
header += parse(sender->user.long_name);
|
||||
header += ")";
|
||||
} else
|
||||
header += hexifyNodeNum(message->sender);
|
||||
// Sender's id
|
||||
// - short name and long name, if available, or
|
||||
// - node id
|
||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(message->sender);
|
||||
if (sender && sender->has_user) {
|
||||
header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
|
||||
header += " (";
|
||||
header += parse(sender->user.long_name);
|
||||
header += ")";
|
||||
} else
|
||||
header += hexifyNodeNum(message->sender);
|
||||
|
||||
// Draw a "standard" applet header
|
||||
drawHeader(header);
|
||||
// Draw a "standard" applet header
|
||||
drawHeader(header);
|
||||
|
||||
// Fade the right edge of the header, if text spills over edge
|
||||
uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect
|
||||
uint8_t hF = getHeaderHeight(); // Height of fade effect
|
||||
if (getCursorX() > width())
|
||||
hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE);
|
||||
// Fade the right edge of the header, if text spills over edge
|
||||
uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect
|
||||
uint8_t hF = getHeaderHeight(); // Height of fade effect
|
||||
if (getCursorX() > width())
|
||||
hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE);
|
||||
|
||||
// Dimensions of the header
|
||||
constexpr int16_t padDivH = 2;
|
||||
const int16_t headerDivY = Applet::getHeaderHeight() - 1;
|
||||
// Dimensions of the header
|
||||
constexpr int16_t padDivH = 2;
|
||||
const int16_t headerDivY = Applet::getHeaderHeight() - 1;
|
||||
|
||||
// ===================
|
||||
// Print message text
|
||||
// ===================
|
||||
// ===================
|
||||
// Print message text
|
||||
// ===================
|
||||
|
||||
// Parse any non-ascii chars in the message
|
||||
std::string text = parse(message->text);
|
||||
// Parse any non-ascii chars in the message
|
||||
std::string text = parse(message->text);
|
||||
|
||||
// Extra gap below the header
|
||||
int16_t textTop = headerDivY + padDivH;
|
||||
// Extra gap below the header
|
||||
int16_t textTop = headerDivY + padDivH;
|
||||
|
||||
// Attempt to print with fontLarge
|
||||
uint32_t textHeight;
|
||||
setFont(fontLarge);
|
||||
textHeight = getWrappedTextHeight(0, width(), text);
|
||||
if (textHeight <= (uint32_t)height()) {
|
||||
printWrapped(0, textTop, width(), text);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback (too large): attempt to print with fontMedium
|
||||
setFont(fontMedium);
|
||||
textHeight = getWrappedTextHeight(0, width(), text);
|
||||
if (textHeight <= (uint32_t)height()) {
|
||||
printWrapped(0, textTop, width(), text);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback (too large): print with fontSmall
|
||||
setFont(fontSmall);
|
||||
// Attempt to print with fontLarge
|
||||
uint32_t textHeight;
|
||||
setFont(fontLarge);
|
||||
textHeight = getWrappedTextHeight(0, width(), text);
|
||||
if (textHeight <= (uint32_t)height()) {
|
||||
printWrapped(0, textTop, width(), text);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback (too large): attempt to print with fontMedium
|
||||
setFont(fontMedium);
|
||||
textHeight = getWrappedTextHeight(0, width(), text);
|
||||
if (textHeight <= (uint32_t)height()) {
|
||||
printWrapped(0, textTop, width(), text);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback (too large): print with fontSmall
|
||||
setFont(fontSmall);
|
||||
printWrapped(0, textTop, width(), text);
|
||||
}
|
||||
|
||||
// Don't show notifications for text messages when our applet is displayed
|
||||
bool InkHUD::AllMessageApplet::approveNotification(Notification &n)
|
||||
{
|
||||
if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)
|
||||
return false;
|
||||
bool InkHUD::AllMessageApplet::approveNotification(Notification &n) {
|
||||
if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST)
|
||||
return false;
|
||||
|
||||
else if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT)
|
||||
return false;
|
||||
else if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT)
|
||||
return false;
|
||||
|
||||
else
|
||||
return true;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -6,8 +6,9 @@ Shows the latest incoming text message, as well as sender.
|
||||
Both broadcast and direct messages will be shown here, from all channels.
|
||||
|
||||
This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages
|
||||
This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message.
|
||||
This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage
|
||||
This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text
|
||||
message. This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via
|
||||
InkHUD::latestMessage
|
||||
|
||||
We do still receive notifications from the text message module though,
|
||||
to know when a new message has arrived, and trigger the update.
|
||||
@@ -22,26 +23,24 @@ to know when a new message has arrived, and trigger the update.
|
||||
|
||||
#include "modules/TextMessageModule.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class Applet;
|
||||
|
||||
class AllMessageApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
class AllMessageApplet : public Applet {
|
||||
public:
|
||||
void onRender() override;
|
||||
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
|
||||
|
||||
bool approveNotification(Notification &n) override; // Which notifications to suppress
|
||||
bool approveNotification(Notification &n) override; // Which notifications to suppress
|
||||
|
||||
protected:
|
||||
// Used to register our text message callback
|
||||
CallbackObserver<AllMessageApplet, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<AllMessageApplet, const meshtastic_MeshPacket *>(this, &AllMessageApplet::onReceiveTextMessage);
|
||||
protected:
|
||||
// Used to register our text message callback
|
||||
CallbackObserver<AllMessageApplet, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<AllMessageApplet, const meshtastic_MeshPacket *>(this, &AllMessageApplet::onReceiveTextMessage);
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -4,129 +4,120 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::DMApplet::onActivate()
|
||||
{
|
||||
textMessageObserver.observe(textMessageModule);
|
||||
}
|
||||
void InkHUD::DMApplet::onActivate() { textMessageObserver.observe(textMessageModule); }
|
||||
|
||||
void InkHUD::DMApplet::onDeactivate()
|
||||
{
|
||||
textMessageObserver.unobserve(textMessageModule);
|
||||
}
|
||||
void InkHUD::DMApplet::onDeactivate() { textMessageObserver.unobserve(textMessageModule); }
|
||||
|
||||
// We're not consuming the data passed to this method;
|
||||
// we're just just using it to trigger a render
|
||||
int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p)
|
||||
{
|
||||
// Abort if applet fully deactivated
|
||||
// Already handled by onActivate and onDeactivate, but good practice for all applets
|
||||
if (!isActive())
|
||||
return 0;
|
||||
|
||||
// If DM (not broadcast)
|
||||
if (!isBroadcast(p->to)) {
|
||||
// Want to update display, if applet is foreground
|
||||
requestUpdate();
|
||||
|
||||
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
|
||||
if (getFrom(p) != nodeDB->getNodeNum())
|
||||
requestAutoshow();
|
||||
}
|
||||
|
||||
// Return zero: no issues here, carry on notifying other observers!
|
||||
int InkHUD::DMApplet::onReceiveTextMessage(const meshtastic_MeshPacket *p) {
|
||||
// Abort if applet fully deactivated
|
||||
// Already handled by onActivate and onDeactivate, but good practice for all applets
|
||||
if (!isActive())
|
||||
return 0;
|
||||
|
||||
// If DM (not broadcast)
|
||||
if (!isBroadcast(p->to)) {
|
||||
// Want to update display, if applet is foreground
|
||||
requestUpdate();
|
||||
|
||||
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
|
||||
if (getFrom(p) != nodeDB->getNodeNum())
|
||||
requestAutoshow();
|
||||
}
|
||||
|
||||
// Return zero: no issues here, carry on notifying other observers!
|
||||
return 0;
|
||||
}
|
||||
|
||||
void InkHUD::DMApplet::onRender()
|
||||
{
|
||||
// Abort if no text message
|
||||
if (!latestMessage->dm.sender) {
|
||||
printAt(X(0.5), Y(0.5), "No DMs", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
void InkHUD::DMApplet::onRender() {
|
||||
// Abort if no text message
|
||||
if (!latestMessage->dm.sender) {
|
||||
printAt(X(0.5), Y(0.5), "No DMs", CENTER, MIDDLE);
|
||||
return;
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Header (sender, timestamp)
|
||||
// ===========================
|
||||
// ===========================
|
||||
// Header (sender, timestamp)
|
||||
// ===========================
|
||||
|
||||
// Y position for divider
|
||||
// - between header text and messages
|
||||
// Y position for divider
|
||||
// - between header text and messages
|
||||
|
||||
std::string header;
|
||||
std::string header;
|
||||
|
||||
// RX Time
|
||||
// - if valid
|
||||
std::string timeString = getTimeString(latestMessage->dm.timestamp);
|
||||
if (timeString.length() > 0) {
|
||||
header += timeString;
|
||||
header += ": ";
|
||||
}
|
||||
// RX Time
|
||||
// - if valid
|
||||
std::string timeString = getTimeString(latestMessage->dm.timestamp);
|
||||
if (timeString.length() > 0) {
|
||||
header += timeString;
|
||||
header += ": ";
|
||||
}
|
||||
|
||||
// Sender's id
|
||||
// - shortname and long name, if available, or
|
||||
// - node id
|
||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender);
|
||||
if (sender && sender->has_user) {
|
||||
header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
|
||||
header += " (";
|
||||
header += parse(sender->user.long_name);
|
||||
header += ")";
|
||||
} else
|
||||
header += hexifyNodeNum(latestMessage->dm.sender);
|
||||
// Sender's id
|
||||
// - shortname and long name, if available, or
|
||||
// - node id
|
||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(latestMessage->dm.sender);
|
||||
if (sender && sender->has_user) {
|
||||
header += parseShortName(sender); // May be last-four of node if unprintable (emoji, etc)
|
||||
header += " (";
|
||||
header += parse(sender->user.long_name);
|
||||
header += ")";
|
||||
} else
|
||||
header += hexifyNodeNum(latestMessage->dm.sender);
|
||||
|
||||
// Draw a "standard" applet header
|
||||
drawHeader(header);
|
||||
// Draw a "standard" applet header
|
||||
drawHeader(header);
|
||||
|
||||
// Fade the right edge of the header, if text spills over edge
|
||||
uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect
|
||||
uint8_t hF = getHeaderHeight(); // Height of fade effect
|
||||
if (getCursorX() > width())
|
||||
hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE);
|
||||
// Fade the right edge of the header, if text spills over edge
|
||||
uint8_t wF = getFont().lineHeight() / 2; // Width of fade effect
|
||||
uint8_t hF = getHeaderHeight(); // Height of fade effect
|
||||
if (getCursorX() > width())
|
||||
hatchRegion(width() - wF - 1, 1, wF, hF, 2, WHITE);
|
||||
|
||||
// Dimensions of the header
|
||||
constexpr int16_t padDivH = 2;
|
||||
const int16_t headerDivY = Applet::getHeaderHeight() - 1;
|
||||
// Dimensions of the header
|
||||
constexpr int16_t padDivH = 2;
|
||||
const int16_t headerDivY = Applet::getHeaderHeight() - 1;
|
||||
|
||||
// ===================
|
||||
// Print message text
|
||||
// ===================
|
||||
// ===================
|
||||
// Print message text
|
||||
// ===================
|
||||
|
||||
// Parse any non-ascii chars in the message
|
||||
std::string text = parse(latestMessage->dm.text);
|
||||
// Parse any non-ascii chars in the message
|
||||
std::string text = parse(latestMessage->dm.text);
|
||||
|
||||
// Extra gap below the header
|
||||
int16_t textTop = headerDivY + padDivH;
|
||||
// Extra gap below the header
|
||||
int16_t textTop = headerDivY + padDivH;
|
||||
|
||||
// Attempt to print with fontLarge
|
||||
uint32_t textHeight;
|
||||
setFont(fontLarge);
|
||||
textHeight = getWrappedTextHeight(0, width(), text);
|
||||
if (textHeight <= (uint32_t)height()) {
|
||||
printWrapped(0, textTop, width(), text);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback (too large): attempt to print with fontMedium
|
||||
setFont(fontMedium);
|
||||
textHeight = getWrappedTextHeight(0, width(), text);
|
||||
if (textHeight <= (uint32_t)height()) {
|
||||
printWrapped(0, textTop, width(), text);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback (too large): print with fontSmall
|
||||
setFont(fontSmall);
|
||||
// Attempt to print with fontLarge
|
||||
uint32_t textHeight;
|
||||
setFont(fontLarge);
|
||||
textHeight = getWrappedTextHeight(0, width(), text);
|
||||
if (textHeight <= (uint32_t)height()) {
|
||||
printWrapped(0, textTop, width(), text);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback (too large): attempt to print with fontMedium
|
||||
setFont(fontMedium);
|
||||
textHeight = getWrappedTextHeight(0, width(), text);
|
||||
if (textHeight <= (uint32_t)height()) {
|
||||
printWrapped(0, textTop, width(), text);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback (too large): print with fontSmall
|
||||
setFont(fontSmall);
|
||||
printWrapped(0, textTop, width(), text);
|
||||
}
|
||||
|
||||
// Don't show notifications for direct messages when our applet is displayed
|
||||
bool InkHUD::DMApplet::approveNotification(Notification &n)
|
||||
{
|
||||
if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT)
|
||||
return false;
|
||||
bool InkHUD::DMApplet::approveNotification(Notification &n) {
|
||||
if (n.type == Notification::Type::NOTIFICATION_MESSAGE_DIRECT)
|
||||
return false;
|
||||
|
||||
else
|
||||
return true;
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -6,8 +6,9 @@ Shows the latest incoming *Direct Message* (DM), as well as sender.
|
||||
This compliments the threaded message applets
|
||||
|
||||
This module doesn't doesn't use the devicestate.rx_text_message,' as this is overwritten to contain outgoing messages
|
||||
This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text message.
|
||||
This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via InkHUD::latestMessage
|
||||
This module doesn't collect its own text message. Instead, the WindowManager stores the most recent incoming text
|
||||
message. This is available to any interested modules (SingeMessageApplet, NotificationApplet etc.) via
|
||||
InkHUD::latestMessage
|
||||
|
||||
We do still receive notifications from the text message module though,
|
||||
to know when a new message has arrived, and trigger the update.
|
||||
@@ -22,26 +23,24 @@ to know when a new message has arrived, and trigger the update.
|
||||
|
||||
#include "modules/TextMessageModule.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class Applet;
|
||||
|
||||
class DMApplet : public Applet
|
||||
{
|
||||
public:
|
||||
void onRender() override;
|
||||
class DMApplet : public Applet {
|
||||
public:
|
||||
void onRender() override;
|
||||
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *p);
|
||||
|
||||
bool approveNotification(Notification &n) override; // Which notifications to suppress
|
||||
bool approveNotification(Notification &n) override; // Which notifications to suppress
|
||||
|
||||
protected:
|
||||
// Used to register our text message callback
|
||||
CallbackObserver<DMApplet, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<DMApplet, const meshtastic_MeshPacket *>(this, &DMApplet::onReceiveTextMessage);
|
||||
protected:
|
||||
// Used to register our text message callback
|
||||
CallbackObserver<DMApplet, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<DMApplet, const meshtastic_MeshPacket *>(this, &DMApplet::onReceiveTextMessage);
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -8,117 +8,112 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::HeardApplet::onActivate()
|
||||
{
|
||||
// When applet begins, pre-fill with stale info from NodeDB
|
||||
populateFromNodeDB();
|
||||
void InkHUD::HeardApplet::onActivate() {
|
||||
// When applet begins, pre-fill with stale info from NodeDB
|
||||
populateFromNodeDB();
|
||||
}
|
||||
|
||||
void InkHUD::HeardApplet::onDeactivate()
|
||||
{
|
||||
// Avoid an unlikely situation where frequent activation / deactivation populates duplicate info from node DB
|
||||
cards.clear();
|
||||
void InkHUD::HeardApplet::onDeactivate() {
|
||||
// Avoid an unlikely situation where frequent activation / deactivation populates duplicate info from node DB
|
||||
cards.clear();
|
||||
}
|
||||
|
||||
// When base applet hears a new packet, it extracts the info and passes it to us as CardInfo
|
||||
// We need to store it (at front to sort recent), and request display update if our list has visibly changed as a result
|
||||
void InkHUD::HeardApplet::handleParsed(CardInfo c)
|
||||
{
|
||||
// Grab the previous entry.
|
||||
// To check if the new data is different enough to justify re-render
|
||||
// Need to cache now, before we manipulate the deque
|
||||
CardInfo previous;
|
||||
if (!cards.empty())
|
||||
previous = cards.at(0);
|
||||
void InkHUD::HeardApplet::handleParsed(CardInfo c) {
|
||||
// Grab the previous entry.
|
||||
// To check if the new data is different enough to justify re-render
|
||||
// Need to cache now, before we manipulate the deque
|
||||
CardInfo previous;
|
||||
if (!cards.empty())
|
||||
previous = cards.at(0);
|
||||
|
||||
// If we're updating an existing entry, remove the old one. Will reinsert at front
|
||||
for (auto it = cards.begin(); it != cards.end(); ++it) {
|
||||
if (it->nodeNum == c.nodeNum) {
|
||||
cards.erase(it);
|
||||
break;
|
||||
}
|
||||
// If we're updating an existing entry, remove the old one. Will reinsert at front
|
||||
for (auto it = cards.begin(); it != cards.end(); ++it) {
|
||||
if (it->nodeNum == c.nodeNum) {
|
||||
cards.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cards.push_front(c); // Insert into base class' card collection
|
||||
cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen
|
||||
cards.shrink_to_fit();
|
||||
cards.push_front(c); // Insert into base class' card collection
|
||||
cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen
|
||||
cards.shrink_to_fit();
|
||||
|
||||
// Our rendered image needs to change if:
|
||||
if (previous.nodeNum != c.nodeNum // Different node
|
||||
|| previous.signal != c.signal // or different signal strength
|
||||
|| previous.distanceMeters != c.distanceMeters // or different position
|
||||
|| previous.hopsAway != c.hopsAway) // or different hops away
|
||||
{
|
||||
requestAutoshow();
|
||||
requestUpdate();
|
||||
}
|
||||
// Our rendered image needs to change if:
|
||||
if (previous.nodeNum != c.nodeNum // Different node
|
||||
|| previous.signal != c.signal // or different signal strength
|
||||
|| previous.distanceMeters != c.distanceMeters // or different position
|
||||
|| previous.hopsAway != c.hopsAway) // or different hops away
|
||||
{
|
||||
requestAutoshow();
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
// When applet is activated, pre-fill with stale data from NodeDB
|
||||
// We're sorting using the last_heard value. Susceptible to weirdness if node's RTC changes.
|
||||
// No SNR is available in node db, so we can't calculate signal either
|
||||
// These initial cards from node db will be gradually pushed out by new packets which originate from out base applet instead
|
||||
void InkHUD::HeardApplet::populateFromNodeDB()
|
||||
{
|
||||
// Fill a collection with pointers to each node in db
|
||||
std::vector<meshtastic_NodeInfoLite *> ordered;
|
||||
for (auto mn = nodeDB->meshNodes->begin(); mn != nodeDB->meshNodes->end(); ++mn) {
|
||||
// Only copy if valid, and not our own node
|
||||
if (mn->num != 0 && mn->num != nodeDB->getNodeNum())
|
||||
ordered.push_back(&*mn);
|
||||
// These initial cards from node db will be gradually pushed out by new packets which originate from out base applet
|
||||
// instead
|
||||
void InkHUD::HeardApplet::populateFromNodeDB() {
|
||||
// Fill a collection with pointers to each node in db
|
||||
std::vector<meshtastic_NodeInfoLite *> ordered;
|
||||
for (auto mn = nodeDB->meshNodes->begin(); mn != nodeDB->meshNodes->end(); ++mn) {
|
||||
// Only copy if valid, and not our own node
|
||||
if (mn->num != 0 && mn->num != nodeDB->getNodeNum())
|
||||
ordered.push_back(&*mn);
|
||||
}
|
||||
|
||||
// Sort the collection by age
|
||||
std::sort(ordered.begin(), ordered.end(),
|
||||
[](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool { return (top->last_heard > bottom->last_heard); });
|
||||
|
||||
// Keep the most recent entries only
|
||||
// Just enough to fill the screen
|
||||
if (ordered.size() > maxCards())
|
||||
ordered.resize(maxCards());
|
||||
|
||||
// Create card info for these (stale) node observations
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
for (meshtastic_NodeInfoLite *node : ordered) {
|
||||
CardInfo c;
|
||||
c.nodeNum = node->num;
|
||||
|
||||
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;
|
||||
|
||||
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
||||
}
|
||||
|
||||
// Sort the collection by age
|
||||
std::sort(ordered.begin(), ordered.end(), [](meshtastic_NodeInfoLite *top, meshtastic_NodeInfoLite *bottom) -> bool {
|
||||
return (top->last_heard > bottom->last_heard);
|
||||
});
|
||||
|
||||
// Keep the most recent entries only
|
||||
// Just enough to fill the screen
|
||||
if (ordered.size() > maxCards())
|
||||
ordered.resize(maxCards());
|
||||
|
||||
// Create card info for these (stale) node observations
|
||||
meshtastic_NodeInfoLite *ourNode = nodeDB->getMeshNode(nodeDB->getNodeNum());
|
||||
for (meshtastic_NodeInfoLite *node : ordered) {
|
||||
CardInfo c;
|
||||
c.nodeNum = node->num;
|
||||
|
||||
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;
|
||||
|
||||
c.distanceMeters = (int32_t)GeoCoord::latLongToMeter(theirLat, theirLong, ourLat, ourLong);
|
||||
}
|
||||
|
||||
// Insert into the card collection (member of base class)
|
||||
cards.push_back(c);
|
||||
}
|
||||
// Insert into the card collection (member of base class)
|
||||
cards.push_back(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Text drawn in the usual applet header
|
||||
// Handled by base class: ChronoListApplet
|
||||
std::string InkHUD::HeardApplet::getHeaderText()
|
||||
{
|
||||
uint16_t nodeCount = nodeDB->getNumMeshNodes() - 1; // Don't count our own node
|
||||
std::string InkHUD::HeardApplet::getHeaderText() {
|
||||
uint16_t nodeCount = nodeDB->getNumMeshNodes() - 1; // Don't count our own node
|
||||
|
||||
std::string text = "Heard: ";
|
||||
std::string text = "Heard: ";
|
||||
|
||||
// Print node count, if nodeDB not yet nearing full
|
||||
if (nodeCount < MAX_NUM_NODES) {
|
||||
text += to_string(nodeCount); // Max nodes
|
||||
text += " ";
|
||||
text += (nodeCount == 1) ? "node" : "nodes";
|
||||
}
|
||||
// Print node count, if nodeDB not yet nearing full
|
||||
if (nodeCount < MAX_NUM_NODES) {
|
||||
text += to_string(nodeCount); // Max nodes
|
||||
text += " ";
|
||||
text += (nodeCount == 1) ? "node" : "nodes";
|
||||
}
|
||||
|
||||
return text;
|
||||
return text;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -13,21 +13,19 @@ Most of the work is done by the InkHUD::NodeListApplet base class
|
||||
|
||||
#include "graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class HeardApplet : public NodeListApplet
|
||||
{
|
||||
public:
|
||||
HeardApplet() : NodeListApplet("HeardApplet") {}
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
class HeardApplet : public NodeListApplet {
|
||||
public:
|
||||
HeardApplet() : NodeListApplet("HeardApplet") {}
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
|
||||
protected:
|
||||
void handleParsed(CardInfo c) override; // Store new info, and update display if needed
|
||||
std::string getHeaderText() override; // Set title for this applet
|
||||
protected:
|
||||
void handleParsed(CardInfo c) override; // Store new info, and update display if needed
|
||||
std::string getHeaderText() override; // Set title for this applet
|
||||
|
||||
void populateFromNodeDB(); // Pre-fill the CardInfo collection from NodeDB
|
||||
void populateFromNodeDB(); // Pre-fill the CardInfo collection from NodeDB
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -5,107 +5,105 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
void InkHUD::PositionsApplet::onRender()
|
||||
{
|
||||
// Draw the usual map applet first
|
||||
MapApplet::onRender();
|
||||
void InkHUD::PositionsApplet::onRender() {
|
||||
// Draw the usual map applet first
|
||||
MapApplet::onRender();
|
||||
|
||||
// Draw our latest "node of interest" as a special marker
|
||||
// -------------------------------------------------------
|
||||
// We might be rendering because we got a position packet from them
|
||||
// We might be rendering because our own position updated
|
||||
// Either way, we still highlight which node most recently sent us a position packet
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom);
|
||||
if (node && nodeDB->hasValidPosition(node) && enoughMarkers())
|
||||
drawLabeledMarker(node);
|
||||
// Draw our latest "node of interest" as a special marker
|
||||
// -------------------------------------------------------
|
||||
// We might be rendering because we got a position packet from them
|
||||
// We might be rendering because our own position updated
|
||||
// Either way, we still highlight which node most recently sent us a position packet
|
||||
meshtastic_NodeInfoLite *node = nodeDB->getMeshNode(lastFrom);
|
||||
if (node && nodeDB->hasValidPosition(node) && enoughMarkers())
|
||||
drawLabeledMarker(node);
|
||||
}
|
||||
|
||||
// Determine if we need to redraw the map, when we receive a new position packet
|
||||
ProcessMessage InkHUD::PositionsApplet::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
// If applet is not active, we shouldn't be handling any data
|
||||
// It's good practice for all applets to implement an early return like this
|
||||
// for PositionsApplet, this is **required** - it's where we're handling active vs deactive
|
||||
if (!isActive())
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Try decode a position from the packet
|
||||
bool hasPosition = false;
|
||||
float lat;
|
||||
float lng;
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) {
|
||||
meshtastic_Position position = meshtastic_Position_init_default;
|
||||
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) {
|
||||
if (position.has_latitude_i && position.has_longitude_i // Actually has position
|
||||
&& (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island"
|
||||
{
|
||||
hasPosition = true;
|
||||
lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format
|
||||
lng = position.longitude_i * 1e-7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if we didn't get a valid position
|
||||
if (!hasPosition)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
const int8_t hopsAway = getHopsAway(mp);
|
||||
const bool hasHopsAway = hopsAway >= 0;
|
||||
|
||||
// Determine if the position packet would change anything on-screen
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
bool somethingChanged = false;
|
||||
|
||||
// If our own position
|
||||
if (isFromUs(&mp)) {
|
||||
// We get frequent position updates from connected phone
|
||||
// Only update if we're travelled some distance, for rate limiting
|
||||
// Todo: smarter detection of position changes
|
||||
if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) {
|
||||
somethingChanged = true;
|
||||
ourLastLat = lat;
|
||||
ourLastLng = lng;
|
||||
}
|
||||
}
|
||||
|
||||
// If someone else's position
|
||||
else {
|
||||
// Check if this position is from someone different than our previous position packet
|
||||
if (mp.from != lastFrom) {
|
||||
somethingChanged = true;
|
||||
lastFrom = mp.from;
|
||||
lastLat = lat;
|
||||
lastLng = lng;
|
||||
lastHopsAway = hopsAway;
|
||||
}
|
||||
|
||||
// Same sender: check if position changed
|
||||
// Todo: smarter detection of position changes
|
||||
else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) {
|
||||
somethingChanged = true;
|
||||
lastLat = lat;
|
||||
lastLng = lng;
|
||||
}
|
||||
|
||||
// Same sender, same position: check if hops changed
|
||||
// Only pay attention if the hopsAway value is valid
|
||||
else if (hasHopsAway && (hopsAway != lastHopsAway)) {
|
||||
somethingChanged = true;
|
||||
lastHopsAway = hopsAway;
|
||||
}
|
||||
}
|
||||
|
||||
// Decision reached
|
||||
// -----------------
|
||||
|
||||
if (somethingChanged) {
|
||||
requestAutoshow(); // Todo: only request this in some situations?
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
ProcessMessage InkHUD::PositionsApplet::handleReceived(const meshtastic_MeshPacket &mp) {
|
||||
// If applet is not active, we shouldn't be handling any data
|
||||
// It's good practice for all applets to implement an early return like this
|
||||
// for PositionsApplet, this is **required** - it's where we're handling active vs deactive
|
||||
if (!isActive())
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Try decode a position from the packet
|
||||
bool hasPosition = false;
|
||||
float lat;
|
||||
float lng;
|
||||
if (mp.which_payload_variant == meshtastic_MeshPacket_decoded_tag && mp.decoded.portnum == meshtastic_PortNum_POSITION_APP) {
|
||||
meshtastic_Position position = meshtastic_Position_init_default;
|
||||
if (pb_decode_from_bytes(mp.decoded.payload.bytes, mp.decoded.payload.size, &meshtastic_Position_msg, &position)) {
|
||||
if (position.has_latitude_i && position.has_longitude_i // Actually has position
|
||||
&& (position.latitude_i != 0 || position.longitude_i != 0)) // Position isn't "null island"
|
||||
{
|
||||
hasPosition = true;
|
||||
lat = position.latitude_i * 1e-7; // Convert from Meshtastic's internal int32_t format
|
||||
lng = position.longitude_i * 1e-7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if we didn't get a valid position
|
||||
if (!hasPosition)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
const int8_t hopsAway = getHopsAway(mp);
|
||||
const bool hasHopsAway = hopsAway >= 0;
|
||||
|
||||
// Determine if the position packet would change anything on-screen
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
bool somethingChanged = false;
|
||||
|
||||
// If our own position
|
||||
if (isFromUs(&mp)) {
|
||||
// We get frequent position updates from connected phone
|
||||
// Only update if we're travelled some distance, for rate limiting
|
||||
// Todo: smarter detection of position changes
|
||||
if (GeoCoord::latLongToMeter(ourLastLat, ourLastLng, lat, lng) > 50) {
|
||||
somethingChanged = true;
|
||||
ourLastLat = lat;
|
||||
ourLastLng = lng;
|
||||
}
|
||||
}
|
||||
|
||||
// If someone else's position
|
||||
else {
|
||||
// Check if this position is from someone different than our previous position packet
|
||||
if (mp.from != lastFrom) {
|
||||
somethingChanged = true;
|
||||
lastFrom = mp.from;
|
||||
lastLat = lat;
|
||||
lastLng = lng;
|
||||
lastHopsAway = hopsAway;
|
||||
}
|
||||
|
||||
// Same sender: check if position changed
|
||||
// Todo: smarter detection of position changes
|
||||
else if (GeoCoord::latLongToMeter(lastLat, lastLng, lat, lng) > 10) {
|
||||
somethingChanged = true;
|
||||
lastLat = lat;
|
||||
lastLng = lng;
|
||||
}
|
||||
|
||||
// Same sender, same position: check if hops changed
|
||||
// Only pay attention if the hopsAway value is valid
|
||||
else if (hasHopsAway && (hopsAway != lastHopsAway)) {
|
||||
somethingChanged = true;
|
||||
lastHopsAway = hopsAway;
|
||||
}
|
||||
}
|
||||
|
||||
// Decision reached
|
||||
// -----------------
|
||||
|
||||
if (somethingChanged) {
|
||||
requestAutoshow(); // Todo: only request this in some situations?
|
||||
requestUpdate();
|
||||
}
|
||||
|
||||
return ProcessMessage::CONTINUE;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -17,25 +17,23 @@ The node which has most recently sent a position will be labeled.
|
||||
|
||||
#include "SinglePortModule.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class PositionsApplet : public MapApplet, public SinglePortModule
|
||||
{
|
||||
public:
|
||||
PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {}
|
||||
void onRender() override;
|
||||
class PositionsApplet : public MapApplet, public SinglePortModule {
|
||||
public:
|
||||
PositionsApplet() : SinglePortModule("PositionsApplet", meshtastic_PortNum_POSITION_APP) {}
|
||||
void onRender() override;
|
||||
|
||||
protected:
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
protected:
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
NodeNum lastFrom = 0; // Sender of most recent (non-local) position packet
|
||||
float lastLat = 0.0;
|
||||
float lastLng = 0.0;
|
||||
float lastHopsAway = 0;
|
||||
NodeNum lastFrom = 0; // Sender of most recent (non-local) position packet
|
||||
float lastLat = 0.0;
|
||||
float lastLng = 0.0;
|
||||
float lastHopsAway = 0;
|
||||
|
||||
float ourLastLat = 0.0; // Info about the most recent (non-local) position packet
|
||||
float ourLastLng = 0.0; // Info about most recent *local* position
|
||||
float ourLastLat = 0.0; // Info about the most recent (non-local) position packet
|
||||
float ourLastLng = 0.0; // Info about most recent *local* position
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -6,148 +6,140 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::RecentsListApplet::RecentsListApplet() : NodeListApplet("RecentsListApplet"), concurrency::OSThread("RecentsListApplet")
|
||||
{
|
||||
// No scheduled tasks initially
|
||||
OSThread::disable();
|
||||
InkHUD::RecentsListApplet::RecentsListApplet() : NodeListApplet("RecentsListApplet"), concurrency::OSThread("RecentsListApplet") {
|
||||
// No scheduled tasks initially
|
||||
OSThread::disable();
|
||||
}
|
||||
|
||||
void InkHUD::RecentsListApplet::onActivate()
|
||||
{
|
||||
// When the applet is activated, begin scheduled purging of any nodes which are no longer "active"
|
||||
OSThread::enabled = true;
|
||||
OSThread::setIntervalFromNow(60 * 1000UL); // Every minute
|
||||
void InkHUD::RecentsListApplet::onActivate() {
|
||||
// When the applet is activated, begin scheduled purging of any nodes which are no longer "active"
|
||||
OSThread::enabled = true;
|
||||
OSThread::setIntervalFromNow(60 * 1000UL); // Every minute
|
||||
}
|
||||
|
||||
void InkHUD::RecentsListApplet::onDeactivate()
|
||||
{
|
||||
// Halt scheduled purging
|
||||
OSThread::disable();
|
||||
void InkHUD::RecentsListApplet::onDeactivate() {
|
||||
// Halt scheduled purging
|
||||
OSThread::disable();
|
||||
}
|
||||
|
||||
int32_t InkHUD::RecentsListApplet::runOnce()
|
||||
{
|
||||
prune(); // Remove CardInfo and Age record for nodes which we haven't heard recently
|
||||
return OSThread::interval;
|
||||
int32_t InkHUD::RecentsListApplet::runOnce() {
|
||||
prune(); // Remove CardInfo and Age record for nodes which we haven't heard recently
|
||||
return OSThread::interval;
|
||||
}
|
||||
|
||||
// When base applet hears a new packet, it extracts the info and passes it to us as CardInfo
|
||||
// We need to store it (at front to sort recent), and request display update if our list has visibly changed as a result
|
||||
// We also need to record the current time against the nodenum, so we know when it becomes inactive
|
||||
void InkHUD::RecentsListApplet::handleParsed(CardInfo c)
|
||||
{
|
||||
// Grab the previous entry.
|
||||
// To check if the new data is different enough to justify re-render
|
||||
// Need to cache now, before we manipulate the deque
|
||||
CardInfo previous;
|
||||
if (!cards.empty())
|
||||
previous = cards.at(0);
|
||||
void InkHUD::RecentsListApplet::handleParsed(CardInfo c) {
|
||||
// Grab the previous entry.
|
||||
// To check if the new data is different enough to justify re-render
|
||||
// Need to cache now, before we manipulate the deque
|
||||
CardInfo previous;
|
||||
if (!cards.empty())
|
||||
previous = cards.at(0);
|
||||
|
||||
// If we're updating an existing entry, remove the old one. Will reinsert at front
|
||||
for (auto it = cards.begin(); it != cards.end(); ++it) {
|
||||
if (it->nodeNum == c.nodeNum) {
|
||||
cards.erase(it);
|
||||
break;
|
||||
}
|
||||
// If we're updating an existing entry, remove the old one. Will reinsert at front
|
||||
for (auto it = cards.begin(); it != cards.end(); ++it) {
|
||||
if (it->nodeNum == c.nodeNum) {
|
||||
cards.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
cards.push_front(c); // Store this CardInfo
|
||||
cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen
|
||||
cards.shrink_to_fit();
|
||||
cards.push_front(c); // Store this CardInfo
|
||||
cards.resize(min(maxCards(), (uint8_t)cards.size())); // Don't keep more cards than we could *ever* fit on screen
|
||||
cards.shrink_to_fit();
|
||||
|
||||
// Record the time of this observation
|
||||
// Used to count active nodes, and to know when to prune inactive nodes
|
||||
seenNow(c.nodeNum);
|
||||
// Record the time of this observation
|
||||
// Used to count active nodes, and to know when to prune inactive nodes
|
||||
seenNow(c.nodeNum);
|
||||
|
||||
// Our rendered image needs to change if:
|
||||
if (previous.nodeNum != c.nodeNum // Different node
|
||||
|| previous.signal != c.signal // or different signal strength
|
||||
|| previous.distanceMeters != c.distanceMeters // or different position
|
||||
|| previous.hopsAway != c.hopsAway) // or different hops away
|
||||
{
|
||||
prune(); // Take the opportunity now to remove inactive nodes
|
||||
requestAutoshow();
|
||||
requestUpdate();
|
||||
}
|
||||
// Our rendered image needs to change if:
|
||||
if (previous.nodeNum != c.nodeNum // Different node
|
||||
|| previous.signal != c.signal // or different signal strength
|
||||
|| previous.distanceMeters != c.distanceMeters // or different position
|
||||
|| previous.hopsAway != c.hopsAway) // or different hops away
|
||||
{
|
||||
prune(); // Take the opportunity now to remove inactive nodes
|
||||
requestAutoshow();
|
||||
requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
// Record the time (millis, right now) that we hear a node
|
||||
// If we do not hear from a node for a while, its card and age info will be removed by the purge method, which runs regularly
|
||||
void InkHUD::RecentsListApplet::seenNow(NodeNum nodeNum)
|
||||
{
|
||||
// If we're updating an existing entry, remove the old one. Will reinsert at front
|
||||
for (auto it = ages.begin(); it != ages.end(); ++it) {
|
||||
if (it->nodeNum == nodeNum) {
|
||||
ages.erase(it);
|
||||
break;
|
||||
}
|
||||
// If we do not hear from a node for a while, its card and age info will be removed by the purge method, which runs
|
||||
// regularly
|
||||
void InkHUD::RecentsListApplet::seenNow(NodeNum nodeNum) {
|
||||
// If we're updating an existing entry, remove the old one. Will reinsert at front
|
||||
for (auto it = ages.begin(); it != ages.end(); ++it) {
|
||||
if (it->nodeNum == nodeNum) {
|
||||
ages.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Age a;
|
||||
a.nodeNum = nodeNum;
|
||||
a.seenAtMs = millis();
|
||||
Age a;
|
||||
a.nodeNum = nodeNum;
|
||||
a.seenAtMs = millis();
|
||||
|
||||
ages.push_front(a);
|
||||
ages.push_front(a);
|
||||
}
|
||||
|
||||
// Remove Card and Age info for any nodes which are now inactive
|
||||
// Determined by when a node was last heard, in our internal record (not from nodeDB)
|
||||
void InkHUD::RecentsListApplet::prune()
|
||||
{
|
||||
// Iterate age records from newest to oldest
|
||||
for (uint16_t i = 0; i < ages.size(); i++) {
|
||||
// Found the first record which is too old
|
||||
if (!isActive(ages.at(i).seenAtMs)) {
|
||||
// Drop this item, and all others behind it
|
||||
ages.resize(i);
|
||||
ages.shrink_to_fit();
|
||||
cards.resize(i);
|
||||
cards.shrink_to_fit();
|
||||
void InkHUD::RecentsListApplet::prune() {
|
||||
// Iterate age records from newest to oldest
|
||||
for (uint16_t i = 0; i < ages.size(); i++) {
|
||||
// Found the first record which is too old
|
||||
if (!isActive(ages.at(i).seenAtMs)) {
|
||||
// Drop this item, and all others behind it
|
||||
ages.resize(i);
|
||||
ages.shrink_to_fit();
|
||||
cards.resize(i);
|
||||
cards.shrink_to_fit();
|
||||
|
||||
// Request an update, if pruning did modify our data
|
||||
// Required if pruning was scheduled. Redundant if pruning was prior to rendering.
|
||||
requestAutoshow();
|
||||
requestUpdate();
|
||||
// Request an update, if pruning did modify our data
|
||||
// Required if pruning was scheduled. Redundant if pruning was prior to rendering.
|
||||
requestAutoshow();
|
||||
requestUpdate();
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Push next scheduled pruning back
|
||||
// Pruning may be called from by handleParsed, immediately prior to rendering
|
||||
// In that case, we can slightly delay our scheduled pruning
|
||||
OSThread::setIntervalFromNow(60 * 1000UL);
|
||||
// Push next scheduled pruning back
|
||||
// Pruning may be called from by handleParsed, immediately prior to rendering
|
||||
// In that case, we can slightly delay our scheduled pruning
|
||||
OSThread::setIntervalFromNow(60 * 1000UL);
|
||||
}
|
||||
|
||||
// Is a timestamp old enough that it would make a node inactive, and in need of purging?
|
||||
bool InkHUD::RecentsListApplet::isActive(uint32_t seenAtMs)
|
||||
{
|
||||
uint32_t now = millis();
|
||||
uint32_t secsAgo = (now - seenAtMs) / 1000UL; // millis() overflow safe
|
||||
bool InkHUD::RecentsListApplet::isActive(uint32_t seenAtMs) {
|
||||
uint32_t now = millis();
|
||||
uint32_t secsAgo = (now - seenAtMs) / 1000UL; // millis() overflow safe
|
||||
|
||||
return (secsAgo < settings->recentlyActiveSeconds);
|
||||
return (secsAgo < settings->recentlyActiveSeconds);
|
||||
}
|
||||
|
||||
// Text to be shown at top of applet
|
||||
// ChronoListApplet base class allows us to set this dynamically
|
||||
// Might want to adjust depending on node count, RTC status, etc
|
||||
std::string InkHUD::RecentsListApplet::getHeaderText()
|
||||
{
|
||||
std::string text;
|
||||
std::string InkHUD::RecentsListApplet::getHeaderText() {
|
||||
std::string text;
|
||||
|
||||
// Print the length of our "Recents" time-window
|
||||
text += "Last ";
|
||||
text += to_string(settings->recentlyActiveSeconds / 60);
|
||||
text += " mins";
|
||||
// Print the length of our "Recents" time-window
|
||||
text += "Last ";
|
||||
text += to_string(settings->recentlyActiveSeconds / 60);
|
||||
text += " mins";
|
||||
|
||||
// Print the node count
|
||||
const uint16_t nodeCount = ages.size();
|
||||
text += ": ";
|
||||
text += to_string(nodeCount);
|
||||
text += " ";
|
||||
text += (nodeCount == 1) ? "node" : "nodes";
|
||||
// Print the node count
|
||||
const uint16_t nodeCount = ages.size();
|
||||
text += ": ";
|
||||
text += to_string(nodeCount);
|
||||
text += " ";
|
||||
text += (nodeCount == 1) ? "node" : "nodes";
|
||||
|
||||
return text;
|
||||
return text;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -15,36 +15,34 @@ Most of the work is done by the shared InkHUD::NodeListApplet base class
|
||||
|
||||
#include "graphics/niche/InkHUD/Applets/Bases/NodeList/NodeListApplet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class RecentsListApplet : public NodeListApplet, public concurrency::OSThread
|
||||
{
|
||||
protected:
|
||||
// Used internally to count the number of active nodes
|
||||
// We count for ourselves, instead of using the value provided by NodeDB,
|
||||
// as the values occasionally differ, due to the timing of our Applet's purge method
|
||||
struct Age {
|
||||
uint32_t nodeNum;
|
||||
uint32_t seenAtMs;
|
||||
};
|
||||
class RecentsListApplet : public NodeListApplet, public concurrency::OSThread {
|
||||
protected:
|
||||
// Used internally to count the number of active nodes
|
||||
// We count for ourselves, instead of using the value provided by NodeDB,
|
||||
// as the values occasionally differ, due to the timing of our Applet's purge method
|
||||
struct Age {
|
||||
uint32_t nodeNum;
|
||||
uint32_t seenAtMs;
|
||||
};
|
||||
|
||||
public:
|
||||
RecentsListApplet();
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
public:
|
||||
RecentsListApplet();
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
|
||||
protected:
|
||||
int32_t runOnce() override;
|
||||
protected:
|
||||
int32_t runOnce() override;
|
||||
|
||||
void handleParsed(CardInfo c) override; // Store new info, update active count, update display if needed
|
||||
std::string getHeaderText() override; // Set title for this applet
|
||||
void handleParsed(CardInfo c) override; // Store new info, update active count, update display if needed
|
||||
std::string getHeaderText() override; // Set title for this applet
|
||||
|
||||
void seenNow(NodeNum nodeNum); // Record that we have just seen this node, for active node count
|
||||
void prune(); // Remove cards for nodes which we haven't seen recently
|
||||
bool isActive(uint32_t seenAtMillis); // Is a node still active, based on when we last heard it?
|
||||
void seenNow(NodeNum nodeNum); // Record that we have just seen this node, for active node count
|
||||
void prune(); // Remove cards for nodes which we haven't seen recently
|
||||
bool isActive(uint32_t seenAtMillis); // Is a node still active, based on when we last heard it?
|
||||
|
||||
std::deque<Age> ages; // Information about when we last heard nodes. Independent of NodeDB
|
||||
std::deque<Age> ages; // Information about when we last heard nodes. Independent of NodeDB
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -14,255 +14,246 @@ constexpr uint8_t MAX_MESSAGES_SAVED = 10;
|
||||
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
|
||||
|
||||
InkHUD::ThreadedMessageApplet::ThreadedMessageApplet(uint8_t channelIndex)
|
||||
: SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex)
|
||||
{
|
||||
// Create the message store
|
||||
// Will shortly attempt to load messages from RAM, if applet is active
|
||||
// Label (filename in flash) is set from channel index
|
||||
store = new MessageStore("ch" + to_string(channelIndex));
|
||||
: SinglePortModule("ThreadedMessageApplet", meshtastic_PortNum_TEXT_MESSAGE_APP), channelIndex(channelIndex) {
|
||||
// Create the message store
|
||||
// Will shortly attempt to load messages from RAM, if applet is active
|
||||
// Label (filename in flash) is set from channel index
|
||||
store = new MessageStore("ch" + to_string(channelIndex));
|
||||
}
|
||||
|
||||
void InkHUD::ThreadedMessageApplet::onRender()
|
||||
{
|
||||
// =============
|
||||
// Draw a header
|
||||
// =============
|
||||
void InkHUD::ThreadedMessageApplet::onRender() {
|
||||
// =============
|
||||
// Draw a header
|
||||
// =============
|
||||
|
||||
// Header text
|
||||
std::string headerText;
|
||||
headerText += "Channel ";
|
||||
headerText += to_string(channelIndex);
|
||||
headerText += ": ";
|
||||
if (channels.isDefaultChannel(channelIndex))
|
||||
headerText += "Public";
|
||||
else
|
||||
headerText += channels.getByIndex(channelIndex).settings.name;
|
||||
// Header text
|
||||
std::string headerText;
|
||||
headerText += "Channel ";
|
||||
headerText += to_string(channelIndex);
|
||||
headerText += ": ";
|
||||
if (channels.isDefaultChannel(channelIndex))
|
||||
headerText += "Public";
|
||||
else
|
||||
headerText += channels.getByIndex(channelIndex).settings.name;
|
||||
|
||||
// Draw a "standard" applet header
|
||||
drawHeader(headerText);
|
||||
// Draw a "standard" applet header
|
||||
drawHeader(headerText);
|
||||
|
||||
// Y position for divider
|
||||
const int16_t dividerY = Applet::getHeaderHeight() - 1;
|
||||
// Y position for divider
|
||||
const int16_t dividerY = Applet::getHeaderHeight() - 1;
|
||||
|
||||
// ==================
|
||||
// Draw each message
|
||||
// ==================
|
||||
// ==================
|
||||
// Draw each message
|
||||
// ==================
|
||||
|
||||
// Restrict drawing area
|
||||
// - don't overdraw the header
|
||||
// - small gap below divider
|
||||
setCrop(0, dividerY + 2, width(), height() - (dividerY + 2));
|
||||
// Restrict drawing area
|
||||
// - don't overdraw the header
|
||||
// - small gap below divider
|
||||
setCrop(0, dividerY + 2, width(), height() - (dividerY + 2));
|
||||
|
||||
// Set padding
|
||||
// - separates text from the vertical line which marks its edge
|
||||
constexpr uint16_t padW = 2;
|
||||
constexpr int16_t msgL = padW;
|
||||
const int16_t msgR = (width() - 1) - padW;
|
||||
const uint16_t msgW = (msgR - msgL) + 1;
|
||||
// Set padding
|
||||
// - separates text from the vertical line which marks its edge
|
||||
constexpr uint16_t padW = 2;
|
||||
constexpr int16_t msgL = padW;
|
||||
const int16_t msgR = (width() - 1) - padW;
|
||||
const uint16_t msgW = (msgR - msgL) + 1;
|
||||
|
||||
int16_t msgB = height() - 1; // Vertical cursor for drawing. Messages are bottom-aligned to this value.
|
||||
uint8_t i = 0; // Index of stored message
|
||||
int16_t msgB = height() - 1; // Vertical cursor for drawing. Messages are bottom-aligned to this value.
|
||||
uint8_t i = 0; // Index of stored message
|
||||
|
||||
// Loop over messages
|
||||
// - until no messages left, or
|
||||
// - until no part of message fits on screen
|
||||
while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) {
|
||||
// Loop over messages
|
||||
// - until no messages left, or
|
||||
// - until no part of message fits on screen
|
||||
while (msgB >= (0 - fontSmall.lineHeight()) && i < store->messages.size()) {
|
||||
|
||||
// Grab data for message
|
||||
MessageStore::Message &m = store->messages.at(i);
|
||||
bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message
|
||||
std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message
|
||||
// Grab data for message
|
||||
MessageStore::Message &m = store->messages.at(i);
|
||||
bool outgoing = (m.sender == 0) || (m.sender == myNodeInfo.my_node_num); // Own NodeNum if canned message
|
||||
std::string bodyText = parse(m.text); // Parse any non-ascii chars in the message
|
||||
|
||||
// Cache bottom Y of message text
|
||||
// - Used when drawing vertical line alongside
|
||||
const int16_t dotsB = msgB;
|
||||
// Cache bottom Y of message text
|
||||
// - Used when drawing vertical line alongside
|
||||
const int16_t dotsB = msgB;
|
||||
|
||||
// Get dimensions for message text
|
||||
uint16_t bodyH = getWrappedTextHeight(msgL, msgW, bodyText);
|
||||
int16_t bodyT = msgB - bodyH;
|
||||
// Get dimensions for message text
|
||||
uint16_t bodyH = getWrappedTextHeight(msgL, msgW, bodyText);
|
||||
int16_t bodyT = msgB - bodyH;
|
||||
|
||||
// Print message
|
||||
// - if incoming
|
||||
if (!outgoing)
|
||||
printWrapped(msgL, bodyT, msgW, bodyText);
|
||||
// Print message
|
||||
// - if incoming
|
||||
if (!outgoing)
|
||||
printWrapped(msgL, bodyT, msgW, bodyText);
|
||||
|
||||
// Print message
|
||||
// - if outgoing
|
||||
else {
|
||||
if (getTextWidth(bodyText) < width()) // If short,
|
||||
printAt(msgR, bodyT, bodyText, RIGHT); // print right align
|
||||
else // If long,
|
||||
printWrapped(msgL, bodyT, msgW, bodyText); // need printWrapped(), which doesn't support right align
|
||||
}
|
||||
// Print message
|
||||
// - if outgoing
|
||||
else {
|
||||
if (getTextWidth(bodyText) < width()) // If short,
|
||||
printAt(msgR, bodyT, bodyText, RIGHT); // print right align
|
||||
else // If long,
|
||||
printWrapped(msgL, bodyT, msgW, bodyText); // need printWrapped(), which doesn't support right align
|
||||
}
|
||||
|
||||
// Move cursor up
|
||||
// - above message text
|
||||
msgB -= bodyH;
|
||||
msgB -= getFont().lineHeight() * 0.2; // Padding between message and header
|
||||
// Move cursor up
|
||||
// - above message text
|
||||
msgB -= bodyH;
|
||||
msgB -= getFont().lineHeight() * 0.2; // Padding between message and header
|
||||
|
||||
// Compose info string
|
||||
// - shortname, if possible, or "me"
|
||||
// - time received, if possible
|
||||
std::string info;
|
||||
if (outgoing)
|
||||
info += "Me";
|
||||
else {
|
||||
// Check if sender is node db
|
||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender);
|
||||
if (sender)
|
||||
info += parseShortName(sender); // Handle any unprintable chars in short name
|
||||
else
|
||||
info += hexifyNodeNum(m.sender); // No node info at all. Print the node num
|
||||
}
|
||||
// Compose info string
|
||||
// - shortname, if possible, or "me"
|
||||
// - time received, if possible
|
||||
std::string info;
|
||||
if (outgoing)
|
||||
info += "Me";
|
||||
else {
|
||||
// Check if sender is node db
|
||||
meshtastic_NodeInfoLite *sender = nodeDB->getMeshNode(m.sender);
|
||||
if (sender)
|
||||
info += parseShortName(sender); // Handle any unprintable chars in short name
|
||||
else
|
||||
info += hexifyNodeNum(m.sender); // No node info at all. Print the node num
|
||||
}
|
||||
|
||||
std::string timeString = getTimeString(m.timestamp);
|
||||
if (timeString.length() > 0) {
|
||||
info += " - ";
|
||||
info += timeString;
|
||||
}
|
||||
std::string timeString = getTimeString(m.timestamp);
|
||||
if (timeString.length() > 0) {
|
||||
info += " - ";
|
||||
info += timeString;
|
||||
}
|
||||
|
||||
// Print the info string
|
||||
// - Faux bold: printed twice, shifted horizontally by one px
|
||||
printAt(outgoing ? msgR : msgL, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM);
|
||||
printAt(outgoing ? msgR - 1 : msgL + 1, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM);
|
||||
// Print the info string
|
||||
// - Faux bold: printed twice, shifted horizontally by one px
|
||||
printAt(outgoing ? msgR : msgL, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM);
|
||||
printAt(outgoing ? msgR - 1 : msgL + 1, msgB, info, outgoing ? RIGHT : LEFT, BOTTOM);
|
||||
|
||||
// Underline the info string
|
||||
const int16_t divY = msgB;
|
||||
int16_t divL;
|
||||
int16_t divR;
|
||||
if (!outgoing) {
|
||||
// Left side - incoming
|
||||
divL = msgL;
|
||||
divR = getTextWidth(info) + getFont().lineHeight() / 2;
|
||||
} else {
|
||||
// Right side - outgoing
|
||||
divR = msgR;
|
||||
divL = divR - getTextWidth(info) - getFont().lineHeight() / 2;
|
||||
}
|
||||
for (int16_t x = divL; x <= divR; x += 2)
|
||||
drawPixel(x, divY, BLACK);
|
||||
// Underline the info string
|
||||
const int16_t divY = msgB;
|
||||
int16_t divL;
|
||||
int16_t divR;
|
||||
if (!outgoing) {
|
||||
// Left side - incoming
|
||||
divL = msgL;
|
||||
divR = getTextWidth(info) + getFont().lineHeight() / 2;
|
||||
} else {
|
||||
// Right side - outgoing
|
||||
divR = msgR;
|
||||
divL = divR - getTextWidth(info) - getFont().lineHeight() / 2;
|
||||
}
|
||||
for (int16_t x = divL; x <= divR; x += 2)
|
||||
drawPixel(x, divY, BLACK);
|
||||
|
||||
// Move cursor up: above info string
|
||||
msgB -= fontSmall.lineHeight();
|
||||
// Move cursor up: above info string
|
||||
msgB -= fontSmall.lineHeight();
|
||||
|
||||
// Vertical line alongside message
|
||||
for (int16_t y = msgB; y < dotsB; y += 1)
|
||||
drawPixel(outgoing ? width() - 1 : 0, y, BLACK);
|
||||
// Vertical line alongside message
|
||||
for (int16_t y = msgB; y < dotsB; y += 1)
|
||||
drawPixel(outgoing ? width() - 1 : 0, y, BLACK);
|
||||
|
||||
// Move cursor up: padding before next message
|
||||
msgB -= fontSmall.lineHeight() * 0.5;
|
||||
// Move cursor up: padding before next message
|
||||
msgB -= fontSmall.lineHeight() * 0.5;
|
||||
|
||||
i++;
|
||||
} // End of loop: drawing each message
|
||||
i++;
|
||||
} // End of loop: drawing each message
|
||||
|
||||
// Fade effect:
|
||||
// Area immediately below the divider. Overdraw with sparse white lines.
|
||||
// Make text appear to pass behind the header
|
||||
hatchRegion(0, dividerY + 1, width(), fontSmall.lineHeight() / 3, 2, WHITE);
|
||||
// Fade effect:
|
||||
// Area immediately below the divider. Overdraw with sparse white lines.
|
||||
// Make text appear to pass behind the header
|
||||
hatchRegion(0, dividerY + 1, width(), fontSmall.lineHeight() / 3, 2, WHITE);
|
||||
|
||||
// If we've run out of screen to draw messages, we can drop any leftover data from the queue
|
||||
// Those messages have been pushed off the screen-top by newer ones
|
||||
while (i < store->messages.size())
|
||||
store->messages.pop_back();
|
||||
// If we've run out of screen to draw messages, we can drop any leftover data from the queue
|
||||
// Those messages have been pushed off the screen-top by newer ones
|
||||
while (i < store->messages.size())
|
||||
store->messages.pop_back();
|
||||
}
|
||||
|
||||
// Code which runs when the applet begins running
|
||||
// This might happen at boot, or if user enables the applet at run-time, via the menu
|
||||
void InkHUD::ThreadedMessageApplet::onActivate()
|
||||
{
|
||||
loadMessagesFromFlash();
|
||||
loopbackOk = true; // Allow us to handle messages generated on the node (canned messages)
|
||||
void InkHUD::ThreadedMessageApplet::onActivate() {
|
||||
loadMessagesFromFlash();
|
||||
loopbackOk = true; // Allow us to handle messages generated on the node (canned messages)
|
||||
}
|
||||
|
||||
// Code which runs when the applet stop running
|
||||
// This might be at shutdown, or if the user disables the applet at run-time, via the menu
|
||||
void InkHUD::ThreadedMessageApplet::onDeactivate()
|
||||
{
|
||||
loopbackOk = false; // Slightly reduce our impact if the applet is disabled
|
||||
void InkHUD::ThreadedMessageApplet::onDeactivate() {
|
||||
loopbackOk = false; // Slightly reduce our impact if the applet is disabled
|
||||
}
|
||||
|
||||
// Handle new text messages
|
||||
// These might be incoming, from the mesh, or outgoing from phone
|
||||
// Each instance of the ThreadMessageApplet will only listen on one specific channel
|
||||
ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_MeshPacket &mp)
|
||||
{
|
||||
// Abort if applet fully deactivated
|
||||
if (!isActive())
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Abort if wrong channel
|
||||
if (mp.channel != this->channelIndex)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Abort if message was a DM
|
||||
if (mp.to != NODENUM_BROADCAST)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Extract info into our slimmed-down "StoredMessage" type
|
||||
MessageStore::Message newMessage;
|
||||
newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
||||
newMessage.sender = mp.from;
|
||||
newMessage.channelIndex = mp.channel;
|
||||
newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size);
|
||||
|
||||
// Store newest message at front
|
||||
// These records are used when rendering, and also stored in flash at shutdown
|
||||
store->messages.push_front(newMessage);
|
||||
|
||||
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
|
||||
if (getFrom(&mp) != nodeDB->getNodeNum())
|
||||
requestAutoshow();
|
||||
|
||||
// Redraw the applet, perhaps.
|
||||
requestUpdate(); // Want to update display, if applet is foreground
|
||||
|
||||
// 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
|
||||
ProcessMessage InkHUD::ThreadedMessageApplet::handleReceived(const meshtastic_MeshPacket &mp) {
|
||||
// Abort if applet fully deactivated
|
||||
if (!isActive())
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Abort if wrong channel
|
||||
if (mp.channel != this->channelIndex)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Abort if message was a DM
|
||||
if (mp.to != NODENUM_BROADCAST)
|
||||
return ProcessMessage::CONTINUE;
|
||||
|
||||
// Extract info into our slimmed-down "StoredMessage" type
|
||||
MessageStore::Message newMessage;
|
||||
newMessage.timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
||||
newMessage.sender = mp.from;
|
||||
newMessage.channelIndex = mp.channel;
|
||||
newMessage.text = std::string((const char *)mp.decoded.payload.bytes, mp.decoded.payload.size);
|
||||
|
||||
// Store newest message at front
|
||||
// These records are used when rendering, and also stored in flash at shutdown
|
||||
store->messages.push_front(newMessage);
|
||||
|
||||
// If this was an incoming message, suggest that our applet becomes foreground, if permitted
|
||||
if (getFrom(&mp) != nodeDB->getNodeNum())
|
||||
requestAutoshow();
|
||||
|
||||
// Redraw the applet, perhaps.
|
||||
requestUpdate(); // Want to update display, if applet is foreground
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Don't show notifications for text messages broadcast to our channel, when the applet is displayed
|
||||
bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n)
|
||||
{
|
||||
if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST && n.getChannel() == channelIndex)
|
||||
return false;
|
||||
bool InkHUD::ThreadedMessageApplet::approveNotification(Notification &n) {
|
||||
if (n.type == Notification::Type::NOTIFICATION_MESSAGE_BROADCAST && n.getChannel() == channelIndex)
|
||||
return false;
|
||||
|
||||
// None of our business. Allow the notification.
|
||||
else
|
||||
return true;
|
||||
// None of our business. Allow the notification.
|
||||
else
|
||||
return true;
|
||||
}
|
||||
|
||||
// Save several recent messages to flash
|
||||
// Stores the contents of ThreadedMessageApplet::messages
|
||||
// Just enough messages to fill the display
|
||||
// Messages are packed "back-to-back", to minimize blocks of flash used
|
||||
void InkHUD::ThreadedMessageApplet::saveMessagesToFlash()
|
||||
{
|
||||
// Create a label (will become the filename in flash)
|
||||
std::string label = "ch" + to_string(channelIndex);
|
||||
void InkHUD::ThreadedMessageApplet::saveMessagesToFlash() {
|
||||
// Create a label (will become the filename in flash)
|
||||
std::string label = "ch" + to_string(channelIndex);
|
||||
|
||||
store->saveToFlash();
|
||||
store->saveToFlash();
|
||||
}
|
||||
|
||||
// Load recent messages to flash
|
||||
// Fills ThreadedMessageApplet::messages with previous messages
|
||||
// Just enough messages have been stored to cover the display
|
||||
void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash()
|
||||
{
|
||||
// Create a label (will become the filename in flash)
|
||||
std::string label = "ch" + to_string(channelIndex);
|
||||
void InkHUD::ThreadedMessageApplet::loadMessagesFromFlash() {
|
||||
// Create a label (will become the filename in flash)
|
||||
std::string label = "ch" + to_string(channelIndex);
|
||||
|
||||
store->loadFromFlash();
|
||||
store->loadFromFlash();
|
||||
}
|
||||
|
||||
// Code to run when device is shutting down
|
||||
// This is in addition to any onDeactivate() code, which will also run
|
||||
// Todo: implement before a reboot also
|
||||
void InkHUD::ThreadedMessageApplet::onShutdown()
|
||||
{
|
||||
// Save our current set of messages to flash, provided the applet isn't disabled
|
||||
if (isActive())
|
||||
saveMessagesToFlash();
|
||||
void InkHUD::ThreadedMessageApplet::onShutdown() {
|
||||
// Save our current set of messages to flash, provided the applet isn't disabled
|
||||
if (isActive())
|
||||
saveMessagesToFlash();
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -25,32 +25,30 @@ Suggest a max of two channel, to minimize fs usage?
|
||||
|
||||
#include "modules/TextMessageModule.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class Applet;
|
||||
|
||||
class ThreadedMessageApplet : public Applet, public SinglePortModule
|
||||
{
|
||||
public:
|
||||
explicit ThreadedMessageApplet(uint8_t channelIndex);
|
||||
ThreadedMessageApplet() = delete;
|
||||
class ThreadedMessageApplet : public Applet, public SinglePortModule {
|
||||
public:
|
||||
explicit ThreadedMessageApplet(uint8_t channelIndex);
|
||||
ThreadedMessageApplet() = delete;
|
||||
|
||||
void onRender() override;
|
||||
void onRender() override;
|
||||
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
void onShutdown() override;
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
void onActivate() override;
|
||||
void onDeactivate() override;
|
||||
void onShutdown() override;
|
||||
ProcessMessage handleReceived(const meshtastic_MeshPacket &mp) override;
|
||||
|
||||
bool approveNotification(Notification &n) override; // Which notifications to suppress
|
||||
bool approveNotification(Notification &n) override; // Which notifications to suppress
|
||||
|
||||
protected:
|
||||
void saveMessagesToFlash();
|
||||
void loadMessagesFromFlash();
|
||||
protected:
|
||||
void saveMessagesToFlash();
|
||||
void loadMessagesFromFlash();
|
||||
|
||||
MessageStore *store; // Messages, held in RAM for use, ready to save to flash on shutdown
|
||||
uint8_t channelIndex = 0;
|
||||
MessageStore *store; // Messages, held in RAM for use, ready to save to flash on shutdown
|
||||
uint8_t channelIndex = 0;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -10,167 +10,157 @@ using namespace NicheGraphics;
|
||||
static constexpr uint32_t MAINTENANCE_MS_INITIAL = 60 * 1000UL;
|
||||
static constexpr uint32_t MAINTENANCE_MS = 60 * 60 * 1000UL;
|
||||
|
||||
InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator")
|
||||
{
|
||||
// Timer disabled by default
|
||||
OSThread::disable();
|
||||
InkHUD::DisplayHealth::DisplayHealth() : concurrency::OSThread("Mediator") {
|
||||
// Timer disabled by default
|
||||
OSThread::disable();
|
||||
}
|
||||
|
||||
// Request which update type we would prefer, when the display image next changes
|
||||
// DisplayHealth class will consider our suggestion, and weigh it against other requests
|
||||
void InkHUD::DisplayHealth::requestUpdateType(Drivers::EInk::UpdateTypes type)
|
||||
{
|
||||
// Update our "working decision", to decide if this request is important enough to change our plan
|
||||
if (!forced)
|
||||
workingDecision = prioritize(workingDecision, type);
|
||||
void InkHUD::DisplayHealth::requestUpdateType(Drivers::EInk::UpdateTypes type) {
|
||||
// Update our "working decision", to decide if this request is important enough to change our plan
|
||||
if (!forced)
|
||||
workingDecision = prioritize(workingDecision, type);
|
||||
}
|
||||
|
||||
// Demand that a specific update type be used, when the display image next changes
|
||||
// Note: multiple DisplayHealth::force calls should not be made,
|
||||
// but if they are, the importance of the type will be weighed the same as if both calls were to DisplayHealth::request
|
||||
void InkHUD::DisplayHealth::forceUpdateType(Drivers::EInk::UpdateTypes type)
|
||||
{
|
||||
if (!forced)
|
||||
workingDecision = type;
|
||||
else
|
||||
workingDecision = prioritize(workingDecision, type);
|
||||
void InkHUD::DisplayHealth::forceUpdateType(Drivers::EInk::UpdateTypes type) {
|
||||
if (!forced)
|
||||
workingDecision = type;
|
||||
else
|
||||
workingDecision = prioritize(workingDecision, type);
|
||||
|
||||
forced = true;
|
||||
forced = true;
|
||||
}
|
||||
|
||||
// Find out which update type the DisplayHealth has chosen for us
|
||||
// Calling this method consumes the result, and resets for the next update
|
||||
Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::decideUpdateType()
|
||||
{
|
||||
LOG_DEBUG("FULL-update debt:%f", debt);
|
||||
Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::decideUpdateType() {
|
||||
LOG_DEBUG("FULL-update debt:%f", debt);
|
||||
|
||||
// For convenience
|
||||
typedef Drivers::EInk::UpdateTypes UpdateTypes;
|
||||
// For convenience
|
||||
typedef Drivers::EInk::UpdateTypes UpdateTypes;
|
||||
|
||||
// Grab our final decision for the update type, so we can reset now, for the next update
|
||||
// We do this at top of the method, so we can return early
|
||||
UpdateTypes finalDecision = workingDecision;
|
||||
workingDecision = UpdateTypes::UNSPECIFIED;
|
||||
forced = false;
|
||||
// Grab our final decision for the update type, so we can reset now, for the next update
|
||||
// We do this at top of the method, so we can return early
|
||||
UpdateTypes finalDecision = workingDecision;
|
||||
workingDecision = UpdateTypes::UNSPECIFIED;
|
||||
forced = false;
|
||||
|
||||
// Check whether we've paid off enough debt to stop unprovoked refreshing (if in progress)
|
||||
// This maintenance behavior will also have opportunity to halt itself when the timer next fires,
|
||||
// but that could be an hour away, so we can stop it early here and free up resources
|
||||
if (OSThread::enabled && debt == 0.0)
|
||||
endMaintenance();
|
||||
// Check whether we've paid off enough debt to stop unprovoked refreshing (if in progress)
|
||||
// This maintenance behavior will also have opportunity to halt itself when the timer next fires,
|
||||
// but that could be an hour away, so we can stop it early here and free up resources
|
||||
if (OSThread::enabled && debt == 0.0)
|
||||
endMaintenance();
|
||||
|
||||
// Explicitly requested FULL
|
||||
if (finalDecision == UpdateTypes::FULL) {
|
||||
LOG_DEBUG("Explicit FULL");
|
||||
debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt
|
||||
return UpdateTypes::FULL;
|
||||
}
|
||||
// Explicitly requested FULL
|
||||
if (finalDecision == UpdateTypes::FULL) {
|
||||
LOG_DEBUG("Explicit FULL");
|
||||
debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt
|
||||
return UpdateTypes::FULL;
|
||||
}
|
||||
|
||||
// Explicitly requested FAST
|
||||
if (finalDecision == UpdateTypes::FAST) {
|
||||
LOG_DEBUG("Explicit FAST");
|
||||
// Add to the FULL refresh debt
|
||||
if (debt < 1.0)
|
||||
debt += 1.0 / fastPerFull;
|
||||
else
|
||||
debt += stressMultiplier * (1.0 / fastPerFull); // More debt if too many consecutive FAST refreshes
|
||||
// Explicitly requested FAST
|
||||
if (finalDecision == UpdateTypes::FAST) {
|
||||
LOG_DEBUG("Explicit FAST");
|
||||
// Add to the FULL refresh debt
|
||||
if (debt < 1.0)
|
||||
debt += 1.0 / fastPerFull;
|
||||
else
|
||||
debt += stressMultiplier * (1.0 / fastPerFull); // More debt if too many consecutive FAST refreshes
|
||||
|
||||
// If *significant debt*, begin occasionally refreshing *unprovoked*
|
||||
// This maintenance behavior is only triggered here, by periods of user interaction
|
||||
// Debt would otherwise not be able to climb above 1.0
|
||||
if (debt >= 2.0)
|
||||
beginMaintenance();
|
||||
// If *significant debt*, begin occasionally refreshing *unprovoked*
|
||||
// This maintenance behavior is only triggered here, by periods of user interaction
|
||||
// Debt would otherwise not be able to climb above 1.0
|
||||
if (debt >= 2.0)
|
||||
beginMaintenance();
|
||||
|
||||
return UpdateTypes::FAST; // Give them what the asked for
|
||||
}
|
||||
return UpdateTypes::FAST; // Give them what the asked for
|
||||
}
|
||||
|
||||
// Handling UpdateTypes::UNSPECIFIED
|
||||
// -----------------------------------
|
||||
// In this case, the UI doesn't care which refresh we use
|
||||
// Handling UpdateTypes::UNSPECIFIED
|
||||
// -----------------------------------
|
||||
// In this case, the UI doesn't care which refresh we use
|
||||
|
||||
// Not much debt: suggest FAST
|
||||
if (debt < 1.0) {
|
||||
LOG_DEBUG("UNSPECIFIED: using FAST");
|
||||
debt += 1.0 / fastPerFull;
|
||||
return UpdateTypes::FAST;
|
||||
}
|
||||
// Not much debt: suggest FAST
|
||||
if (debt < 1.0) {
|
||||
LOG_DEBUG("UNSPECIFIED: using FAST");
|
||||
debt += 1.0 / fastPerFull;
|
||||
return UpdateTypes::FAST;
|
||||
}
|
||||
|
||||
// In debt: suggest FULL
|
||||
else {
|
||||
LOG_DEBUG("UNSPECIFIED: using FULL");
|
||||
debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt
|
||||
// In debt: suggest FULL
|
||||
else {
|
||||
LOG_DEBUG("UNSPECIFIED: using FULL");
|
||||
debt = max(debt - 1.0, 0.0); // Record that we have paid back (some of) the FULL refresh debt
|
||||
|
||||
// When maintenance begins, the first refresh happens shortly after user interaction ceases (a minute or so)
|
||||
// If we *are* given an opportunity to refresh before that, we'll skip that initial maintenance refresh
|
||||
// We were intending to use that initial refresh to redraw the screen as FULL, but we're doing that now, organically
|
||||
if (OSThread::enabled && OSThread::interval == MAINTENANCE_MS_INITIAL)
|
||||
OSThread::setInterval(MAINTENANCE_MS); // Note: not intervalFromNow
|
||||
// When maintenance begins, the first refresh happens shortly after user interaction ceases (a minute or so)
|
||||
// If we *are* given an opportunity to refresh before that, we'll skip that initial maintenance refresh
|
||||
// We were intending to use that initial refresh to redraw the screen as FULL, but we're doing that now, organically
|
||||
if (OSThread::enabled && OSThread::interval == MAINTENANCE_MS_INITIAL)
|
||||
OSThread::setInterval(MAINTENANCE_MS); // Note: not intervalFromNow
|
||||
|
||||
return UpdateTypes::FULL;
|
||||
}
|
||||
return UpdateTypes::FULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine which of two update types is more important to honor
|
||||
// Explicit FAST is more important than UNSPECIFIED - prioritize responsiveness
|
||||
// Explicit FULL is more important than explicit FAST - prioritize image quality: explicit FULL is rare
|
||||
// Used when multiple applets have all requested update simultaneously, each with their own preferred UpdateType
|
||||
Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::prioritize(Drivers::EInk::UpdateTypes type1, Drivers::EInk::UpdateTypes type2)
|
||||
{
|
||||
switch (type1) {
|
||||
case Drivers::EInk::UpdateTypes::UNSPECIFIED:
|
||||
return type2;
|
||||
Drivers::EInk::UpdateTypes InkHUD::DisplayHealth::prioritize(Drivers::EInk::UpdateTypes type1, Drivers::EInk::UpdateTypes type2) {
|
||||
switch (type1) {
|
||||
case Drivers::EInk::UpdateTypes::UNSPECIFIED:
|
||||
return type2;
|
||||
|
||||
case Drivers::EInk::UpdateTypes::FAST:
|
||||
return (type2 == Drivers::EInk::UpdateTypes::FULL) ? Drivers::EInk::UpdateTypes::FULL : Drivers::EInk::UpdateTypes::FAST;
|
||||
case Drivers::EInk::UpdateTypes::FAST:
|
||||
return (type2 == Drivers::EInk::UpdateTypes::FULL) ? Drivers::EInk::UpdateTypes::FULL : Drivers::EInk::UpdateTypes::FAST;
|
||||
|
||||
case Drivers::EInk::UpdateTypes::FULL:
|
||||
return type1;
|
||||
}
|
||||
case Drivers::EInk::UpdateTypes::FULL:
|
||||
return type1;
|
||||
}
|
||||
|
||||
return Drivers::EInk::UpdateTypes::UNSPECIFIED; // Suppress compiler warning only
|
||||
return Drivers::EInk::UpdateTypes::UNSPECIFIED; // Suppress compiler warning only
|
||||
}
|
||||
|
||||
// We're using the timer to perform "maintenance"
|
||||
// If significant FULL-refresh debt has accumulated, we will occasionally run FULL refreshes unprovoked.
|
||||
// This prevents gradual build-up of debt,
|
||||
// in case we aren't doing enough UNSPECIFIED refreshes to pay the debt back organically.
|
||||
// The first refresh takes place shortly after user finishes interacting with the device; this does the bulk of the restoration
|
||||
// Subsequent refreshes take place *much* less frequently.
|
||||
// Hopefully an applet will want to render before this, meaning we can cancel the maintenance.
|
||||
int32_t InkHUD::DisplayHealth::runOnce()
|
||||
{
|
||||
if (debt > 0.0) {
|
||||
LOG_DEBUG("debt=%f: performing maintenance", debt);
|
||||
// The first refresh takes place shortly after user finishes interacting with the device; this does the bulk of the
|
||||
// restoration Subsequent refreshes take place *much* less frequently. Hopefully an applet will want to render before
|
||||
// this, meaning we can cancel the maintenance.
|
||||
int32_t InkHUD::DisplayHealth::runOnce() {
|
||||
if (debt > 0.0) {
|
||||
LOG_DEBUG("debt=%f: performing maintenance", debt);
|
||||
|
||||
// Ask WindowManager to redraw everything, purely for the refresh
|
||||
// Todo: optimize? Could update without re-rendering
|
||||
InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FULL);
|
||||
// Ask WindowManager to redraw everything, purely for the refresh
|
||||
// Todo: optimize? Could update without re-rendering
|
||||
InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FULL);
|
||||
|
||||
// Record that we have paid back (some of) the FULL refresh debt
|
||||
debt = max(debt - 1.0, 0.0);
|
||||
// Record that we have paid back (some of) the FULL refresh debt
|
||||
debt = max(debt - 1.0, 0.0);
|
||||
|
||||
// Next maintenance refresh scheduled - long wait (an hour?)
|
||||
return MAINTENANCE_MS;
|
||||
}
|
||||
// Next maintenance refresh scheduled - long wait (an hour?)
|
||||
return MAINTENANCE_MS;
|
||||
}
|
||||
|
||||
else
|
||||
return endMaintenance();
|
||||
else
|
||||
return endMaintenance();
|
||||
}
|
||||
|
||||
// Begin periodically refreshing the display, to repay FULL-refresh debt
|
||||
// We do this in case user doesn't have enough activity to repay it organically, with UpdateTypes::UNSPECIFIED
|
||||
// After an initial refresh, to redraw as FULL, we only perform these maintenance refreshes very infrequently
|
||||
// This gives the display a chance to heal by evaluating UNSPECIFIED as FULL, which is preferable
|
||||
void InkHUD::DisplayHealth::beginMaintenance()
|
||||
{
|
||||
OSThread::setIntervalFromNow(MAINTENANCE_MS_INITIAL);
|
||||
OSThread::enabled = true;
|
||||
void InkHUD::DisplayHealth::beginMaintenance() {
|
||||
OSThread::setIntervalFromNow(MAINTENANCE_MS_INITIAL);
|
||||
OSThread::enabled = true;
|
||||
}
|
||||
|
||||
// FULL-refresh debt is low enough that we no longer need to pay it back with periodic updates
|
||||
int32_t InkHUD::DisplayHealth::endMaintenance()
|
||||
{
|
||||
return OSThread::disable();
|
||||
}
|
||||
int32_t InkHUD::DisplayHealth::endMaintenance() { return OSThread::disable(); }
|
||||
|
||||
#endif
|
||||
@@ -18,34 +18,31 @@ Responsible for maintaining display health, by optimizing the ratio of FAST vs F
|
||||
|
||||
#include "graphics/niche/Drivers/EInk/EInk.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class DisplayHealth : protected concurrency::OSThread
|
||||
{
|
||||
public:
|
||||
DisplayHealth();
|
||||
class DisplayHealth : protected concurrency::OSThread {
|
||||
public:
|
||||
DisplayHealth();
|
||||
|
||||
void requestUpdateType(Drivers::EInk::UpdateTypes type);
|
||||
void forceUpdateType(Drivers::EInk::UpdateTypes type);
|
||||
Drivers::EInk::UpdateTypes decideUpdateType();
|
||||
void requestUpdateType(Drivers::EInk::UpdateTypes type);
|
||||
void forceUpdateType(Drivers::EInk::UpdateTypes type);
|
||||
Drivers::EInk::UpdateTypes decideUpdateType();
|
||||
|
||||
uint8_t fastPerFull = 5; // Ideal number of fast refreshes between full refreshes
|
||||
float stressMultiplier = 2.0; // How bad for the display are extra fast refreshes beyond fastPerFull?
|
||||
uint8_t fastPerFull = 5; // Ideal number of fast refreshes between full refreshes
|
||||
float stressMultiplier = 2.0; // How bad for the display are extra fast refreshes beyond fastPerFull?
|
||||
|
||||
private:
|
||||
int32_t runOnce() override;
|
||||
void beginMaintenance(); // Excessive debt: begin unprovoked refreshing of display, for health
|
||||
int32_t endMaintenance(); // End unprovoked refreshing: debt paid
|
||||
private:
|
||||
int32_t runOnce() override;
|
||||
void beginMaintenance(); // Excessive debt: begin unprovoked refreshing of display, for health
|
||||
int32_t endMaintenance(); // End unprovoked refreshing: debt paid
|
||||
|
||||
Drivers::EInk::UpdateTypes
|
||||
prioritize(Drivers::EInk::UpdateTypes type1,
|
||||
Drivers::EInk::UpdateTypes type2); // Determine which of two update types is more important to honor
|
||||
Drivers::EInk::UpdateTypes prioritize(Drivers::EInk::UpdateTypes type1,
|
||||
Drivers::EInk::UpdateTypes type2); // Determine which of two update types is more important to honor
|
||||
|
||||
bool forced = false;
|
||||
Drivers::EInk::UpdateTypes workingDecision = Drivers::EInk::UpdateTypes::UNSPECIFIED;
|
||||
bool forced = false;
|
||||
Drivers::EInk::UpdateTypes workingDecision = Drivers::EInk::UpdateTypes::UNSPECIFIED;
|
||||
|
||||
float debt = 0.0; // How many full refreshes are due
|
||||
float debt = 0.0; // How many full refreshes are due
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -14,30 +14,78 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::Events::Events()
|
||||
{
|
||||
// Get convenient references
|
||||
inkhud = InkHUD::getInstance();
|
||||
settings = &inkhud->persistence->settings;
|
||||
InkHUD::Events::Events() {
|
||||
// Get convenient references
|
||||
inkhud = InkHUD::getInstance();
|
||||
settings = &inkhud->persistence->settings;
|
||||
}
|
||||
|
||||
void InkHUD::Events::begin()
|
||||
{
|
||||
// Register our callbacks for the various events
|
||||
void InkHUD::Events::begin() {
|
||||
// Register our callbacks for the various events
|
||||
|
||||
deepSleepObserver.observe(¬ifyDeepSleep);
|
||||
rebootObserver.observe(¬ifyReboot);
|
||||
textMessageObserver.observe(textMessageModule);
|
||||
deepSleepObserver.observe(¬ifyDeepSleep);
|
||||
rebootObserver.observe(¬ifyReboot);
|
||||
textMessageObserver.observe(textMessageModule);
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
adminMessageObserver.observe((Observable<AdminModule_ObserverData *> *)adminModule);
|
||||
adminMessageObserver.observe((Observable<AdminModule_ObserverData *> *)adminModule);
|
||||
#endif
|
||||
#ifdef ARCH_ESP32
|
||||
lightSleepObserver.observe(¬ifyLightSleep);
|
||||
lightSleepObserver.observe(¬ifyLightSleep);
|
||||
#endif
|
||||
}
|
||||
|
||||
void InkHUD::Events::onButtonShort()
|
||||
{
|
||||
void InkHUD::Events::onButtonShort() {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no system applet is handling input, default behavior instead is to cycle applets
|
||||
// or open menu if joystick is enabled
|
||||
if (consumer) {
|
||||
consumer->onButtonShortPress();
|
||||
} else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
|
||||
if (!settings->joystick.enabled)
|
||||
inkhud->nextApplet();
|
||||
else
|
||||
inkhud->openMenu();
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onButtonLong() {
|
||||
// Audio feedback (via buzzer)
|
||||
// Slightly longer than playChirp
|
||||
playBoop();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no system applet is handling input, default behavior instead is to open the menu
|
||||
if (consumer)
|
||||
consumer->onButtonLongPress();
|
||||
else
|
||||
inkhud->openMenu();
|
||||
}
|
||||
|
||||
void InkHUD::Events::onExitShort() {
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
@@ -48,26 +96,22 @@ void InkHUD::Events::onButtonShort()
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no system applet is handling input, default behavior instead is to cycle applets
|
||||
// or open menu if joystick is enabled
|
||||
if (consumer) {
|
||||
consumer->onButtonShortPress();
|
||||
} else if (!dismissedExt) { // Don't change applet if this button press silenced the external notification module
|
||||
if (!settings->joystick.enabled)
|
||||
inkhud->nextApplet();
|
||||
else
|
||||
inkhud->openMenu();
|
||||
}
|
||||
// If no system applet is handling input, default behavior instead is change tiles
|
||||
if (consumer)
|
||||
consumer->onExitShort();
|
||||
else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module
|
||||
inkhud->nextTile();
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onButtonLong()
|
||||
{
|
||||
void InkHUD::Events::onExitLong() {
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Slightly longer than playChirp
|
||||
playBoop();
|
||||
@@ -75,325 +119,265 @@ void InkHUD::Events::onButtonLong()
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no system applet is handling input, default behavior instead is to open the menu
|
||||
if (consumer)
|
||||
consumer->onButtonLongPress();
|
||||
else
|
||||
inkhud->openMenu();
|
||||
consumer->onExitLong();
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onExitShort()
|
||||
{
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
void InkHUD::Events::onNavUp() {
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no system applet is handling input, default behavior instead is change tiles
|
||||
if (consumer)
|
||||
consumer->onExitShort();
|
||||
else if (!dismissedExt) // Don't change tile if this button press silenced the external notification module
|
||||
inkhud->nextTile();
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (consumer)
|
||||
consumer->onNavUp();
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onExitLong()
|
||||
{
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Slightly longer than playChirp
|
||||
playBoop();
|
||||
void InkHUD::Events::onNavDown() {
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (consumer)
|
||||
consumer->onExitLong();
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (consumer)
|
||||
consumer->onNavDown();
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onNavUp()
|
||||
{
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
void InkHUD::Events::onNavLeft() {
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (consumer)
|
||||
consumer->onNavUp();
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no system applet is handling input, default behavior instead is to cycle applets
|
||||
if (consumer)
|
||||
consumer->onNavLeft();
|
||||
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
|
||||
inkhud->prevApplet();
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onNavDown()
|
||||
{
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
void InkHUD::Events::onNavRight() {
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (consumer)
|
||||
consumer->onNavDown();
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onNavLeft()
|
||||
{
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no system applet is handling input, default behavior instead is to cycle applets
|
||||
if (consumer)
|
||||
consumer->onNavLeft();
|
||||
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
|
||||
inkhud->prevApplet();
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Events::onNavRight()
|
||||
{
|
||||
if (settings->joystick.enabled) {
|
||||
// Audio feedback (via buzzer)
|
||||
// Short tone
|
||||
playChirp();
|
||||
// Cancel any beeping, buzzing, blinking
|
||||
// Some button handling suppressed if we are dismissing an external notification (see below)
|
||||
bool dismissedExt = dismissExternalNotification();
|
||||
|
||||
// Check which system applet wants to handle the button press (if any)
|
||||
SystemApplet *consumer = nullptr;
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->handleInput) {
|
||||
consumer = sa;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no system applet is handling input, default behavior instead is to cycle applets
|
||||
if (consumer)
|
||||
consumer->onNavRight();
|
||||
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
|
||||
inkhud->nextApplet();
|
||||
}
|
||||
// If no system applet is handling input, default behavior instead is to cycle applets
|
||||
if (consumer)
|
||||
consumer->onNavRight();
|
||||
else if (!dismissedExt) // Don't change applet if this button press silenced the external notification module
|
||||
inkhud->nextApplet();
|
||||
}
|
||||
}
|
||||
|
||||
// Callback for deepSleepObserver
|
||||
// Returns 0 to signal that we agree to sleep now
|
||||
int InkHUD::Events::beforeDeepSleep(void *unused)
|
||||
{
|
||||
// If a previous display update is in progress, wait for it to complete.
|
||||
inkhud->awaitUpdate();
|
||||
int InkHUD::Events::beforeDeepSleep(void *unused) {
|
||||
// If a previous display update is in progress, wait for it to complete.
|
||||
inkhud->awaitUpdate();
|
||||
|
||||
// Notify all applets that we're shutting down
|
||||
for (Applet *ua : inkhud->userApplets) {
|
||||
ua->onDeactivate();
|
||||
ua->onShutdown();
|
||||
}
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
// Note: no onDeactivate. System applets are always active.
|
||||
sa->onShutdown();
|
||||
}
|
||||
// Notify all applets that we're shutting down
|
||||
for (Applet *ua : inkhud->userApplets) {
|
||||
ua->onDeactivate();
|
||||
ua->onShutdown();
|
||||
}
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
// Note: no onDeactivate. System applets are always active.
|
||||
sa->onShutdown();
|
||||
}
|
||||
|
||||
// User has successful executed a safe shutdown
|
||||
// We don't need to nag at boot anymore
|
||||
settings->tips.safeShutdownSeen = true;
|
||||
// User has successful executed a safe shutdown
|
||||
// We don't need to nag at boot anymore
|
||||
settings->tips.safeShutdownSeen = true;
|
||||
|
||||
inkhud->persistence->saveSettings();
|
||||
inkhud->persistence->saveLatestMessage();
|
||||
inkhud->persistence->saveSettings();
|
||||
inkhud->persistence->saveLatestMessage();
|
||||
|
||||
// LogoApplet::onShutdown attempted to heal the display by drawing a "shutting down" screen twice,
|
||||
// then prepared a final powered-off screen for us, which shows device shortname.
|
||||
// We're updating to show that one now.
|
||||
// LogoApplet::onShutdown attempted to heal the display by drawing a "shutting down" screen twice,
|
||||
// then prepared a final powered-off screen for us, which shows device shortname.
|
||||
// We're updating to show that one now.
|
||||
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
|
||||
delay(1000); // Cooldown, before potentially yanking display power
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
|
||||
delay(1000); // Cooldown, before potentially yanking display power
|
||||
|
||||
// InkHUD shutdown complete
|
||||
// Firmware shutdown continues for several seconds more; flash write still pending
|
||||
playShutdownMelody();
|
||||
// InkHUD shutdown complete
|
||||
// Firmware shutdown continues for several seconds more; flash write still pending
|
||||
playShutdownMelody();
|
||||
|
||||
return 0; // We agree: deep sleep now
|
||||
return 0; // We agree: deep sleep now
|
||||
}
|
||||
|
||||
// Callback for rebootObserver
|
||||
// Same as shutdown, without drawing the logoApplet
|
||||
// Makes sure we don't lose message history / InkHUD config
|
||||
int InkHUD::Events::beforeReboot(void *unused)
|
||||
{
|
||||
int InkHUD::Events::beforeReboot(void *unused) {
|
||||
|
||||
// Notify all applets that we're "shutting down"
|
||||
// They don't need to know that it's really a reboot
|
||||
for (Applet *a : inkhud->userApplets) {
|
||||
a->onDeactivate();
|
||||
a->onShutdown();
|
||||
}
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
// Note: no onDeactivate. System applets are always active.
|
||||
sa->onReboot();
|
||||
}
|
||||
// Notify all applets that we're "shutting down"
|
||||
// They don't need to know that it's really a reboot
|
||||
for (Applet *a : inkhud->userApplets) {
|
||||
a->onDeactivate();
|
||||
a->onShutdown();
|
||||
}
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
// Note: no onDeactivate. System applets are always active.
|
||||
sa->onReboot();
|
||||
}
|
||||
|
||||
// Save settings to flash, or erase if factory reset in progress
|
||||
if (!eraseOnReboot) {
|
||||
inkhud->persistence->saveSettings();
|
||||
inkhud->persistence->saveLatestMessage();
|
||||
} else {
|
||||
NicheGraphics::clearFlashData();
|
||||
}
|
||||
// Save settings to flash, or erase if factory reset in progress
|
||||
if (!eraseOnReboot) {
|
||||
inkhud->persistence->saveSettings();
|
||||
inkhud->persistence->saveLatestMessage();
|
||||
} else {
|
||||
NicheGraphics::clearFlashData();
|
||||
}
|
||||
|
||||
// Note: no forceUpdate call here
|
||||
// We don't have any final screen to draw, although LogoApplet::onReboot did already display a "rebooting" screen
|
||||
// Note: no forceUpdate call here
|
||||
// We don't have any final screen to draw, although LogoApplet::onReboot did already display a "rebooting" screen
|
||||
|
||||
return 0; // No special status to report. Ignored anyway by this Observable
|
||||
return 0; // No special status to report. Ignored anyway by this Observable
|
||||
}
|
||||
|
||||
// Callback when a new text message is received
|
||||
// Caches the most recently received message, for use by applets
|
||||
// Rx does not trigger a save to flash, however the data *will* be saved alongside other during shutdown, etc.
|
||||
// Note: this is different from devicestate.rx_text_message, which may contain an *outgoing* message
|
||||
int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet)
|
||||
{
|
||||
// Short circuit: don't store outgoing messages
|
||||
if (getFrom(packet) == nodeDB->getNodeNum())
|
||||
return 0;
|
||||
int InkHUD::Events::onReceiveTextMessage(const meshtastic_MeshPacket *packet) {
|
||||
// Short circuit: don't store outgoing messages
|
||||
if (getFrom(packet) == nodeDB->getNodeNum())
|
||||
return 0;
|
||||
|
||||
// Determine whether the message is broadcast or a DM
|
||||
// Store this info to prevent confusion after a reboot
|
||||
// Avoids need to compare timestamps, because of situation where "future" messages block newly received, if time not set
|
||||
inkhud->persistence->latestMessage.wasBroadcast = isBroadcast(packet->to);
|
||||
// Determine whether the message is broadcast or a DM
|
||||
// Store this info to prevent confusion after a reboot
|
||||
// Avoids need to compare timestamps, because of situation where "future" messages block newly received, if time not
|
||||
// set
|
||||
inkhud->persistence->latestMessage.wasBroadcast = isBroadcast(packet->to);
|
||||
|
||||
// Pick the appropriate variable to store the message in
|
||||
MessageStore::Message *storedMessage = inkhud->persistence->latestMessage.wasBroadcast
|
||||
? &inkhud->persistence->latestMessage.broadcast
|
||||
: &inkhud->persistence->latestMessage.dm;
|
||||
// Pick the appropriate variable to store the message in
|
||||
MessageStore::Message *storedMessage =
|
||||
inkhud->persistence->latestMessage.wasBroadcast ? &inkhud->persistence->latestMessage.broadcast : &inkhud->persistence->latestMessage.dm;
|
||||
|
||||
// Store nodenum of the sender
|
||||
// Applets can use this to fetch user data from nodedb, if they want
|
||||
storedMessage->sender = packet->from;
|
||||
// Store nodenum of the sender
|
||||
// Applets can use this to fetch user data from nodedb, if they want
|
||||
storedMessage->sender = packet->from;
|
||||
|
||||
// Store the time (epoch seconds) when message received
|
||||
storedMessage->timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
||||
// Store the time (epoch seconds) when message received
|
||||
storedMessage->timestamp = getValidTime(RTCQuality::RTCQualityDevice, true); // Current RTC time
|
||||
|
||||
// Store the channel
|
||||
// - (potentially) used to determine whether notification shows
|
||||
// - (potentially) used to determine which applet to focus
|
||||
storedMessage->channelIndex = packet->channel;
|
||||
// Store the channel
|
||||
// - (potentially) used to determine whether notification shows
|
||||
// - (potentially) used to determine which applet to focus
|
||||
storedMessage->channelIndex = packet->channel;
|
||||
|
||||
// Store the text
|
||||
// Need to specify manually how many bytes, because source not null-terminated
|
||||
storedMessage->text =
|
||||
std::string(&packet->decoded.payload.bytes[0], &packet->decoded.payload.bytes[packet->decoded.payload.size]);
|
||||
// Store the text
|
||||
// Need to specify manually how many bytes, because source not null-terminated
|
||||
storedMessage->text = std::string(&packet->decoded.payload.bytes[0], &packet->decoded.payload.bytes[packet->decoded.payload.size]);
|
||||
|
||||
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
|
||||
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
|
||||
}
|
||||
|
||||
int InkHUD::Events::onAdminMessage(AdminModule_ObserverData *data)
|
||||
{
|
||||
switch (data->request->which_payload_variant) {
|
||||
// Factory reset
|
||||
// Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data.
|
||||
case meshtastic_AdminMessage_factory_reset_device_tag:
|
||||
case meshtastic_AdminMessage_factory_reset_config_tag:
|
||||
eraseOnReboot = true;
|
||||
*data->result = AdminMessageHandleResult::HANDLED;
|
||||
break;
|
||||
int InkHUD::Events::onAdminMessage(AdminModule_ObserverData *data) {
|
||||
switch (data->request->which_payload_variant) {
|
||||
// Factory reset
|
||||
// Two possible messages. One preserves BLE bonds, other wipes. Both should clear InkHUD data.
|
||||
case meshtastic_AdminMessage_factory_reset_device_tag:
|
||||
case meshtastic_AdminMessage_factory_reset_config_tag:
|
||||
eraseOnReboot = true;
|
||||
*data->result = AdminMessageHandleResult::HANDLED;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
|
||||
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Callback for lightSleepObserver
|
||||
// Make sure the display is not partway through an update when we begin light sleep
|
||||
// This is because some displays require active input from us to terminate the update process, and protect the panel hardware
|
||||
int InkHUD::Events::beforeLightSleep(void *unused)
|
||||
{
|
||||
inkhud->awaitUpdate();
|
||||
return 0; // No special status to report. Ignored anyway by this Observable
|
||||
// This is because some displays require active input from us to terminate the update process, and protect the panel
|
||||
// hardware
|
||||
int InkHUD::Events::beforeLightSleep(void *unused) {
|
||||
inkhud->awaitUpdate();
|
||||
return 0; // No special status to report. Ignored anyway by this Observable
|
||||
}
|
||||
#endif
|
||||
|
||||
// Silence all ongoing beeping, blinking, buzzing, coming from the external notification module
|
||||
// Returns true if an external notification was active, and we dismissed it
|
||||
// Button handling changes depending on our result
|
||||
bool InkHUD::Events::dismissExternalNotification()
|
||||
{
|
||||
// Abort if not using external notifications
|
||||
if (!moduleConfig.external_notification.enabled)
|
||||
return false;
|
||||
bool InkHUD::Events::dismissExternalNotification() {
|
||||
// Abort if not using external notifications
|
||||
if (!moduleConfig.external_notification.enabled)
|
||||
return false;
|
||||
|
||||
// Abort if nothing to dismiss
|
||||
if (!externalNotificationModule->nagging())
|
||||
return false;
|
||||
// Abort if nothing to dismiss
|
||||
if (!externalNotificationModule->nagging())
|
||||
return false;
|
||||
|
||||
// Stop the beep buzz blink
|
||||
externalNotificationModule->stopNow();
|
||||
// Stop the beep buzz blink
|
||||
externalNotificationModule->stopNow();
|
||||
|
||||
// Inform that we did indeed dismiss an external notification
|
||||
return true;
|
||||
// Inform that we did indeed dismiss an external notification
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -18,61 +18,59 @@ however this class handles general events which concern InkHUD as a whole, e.g.
|
||||
#include "./InkHUD.h"
|
||||
#include "./Persistence.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class Events
|
||||
{
|
||||
public:
|
||||
Events();
|
||||
void begin();
|
||||
class Events {
|
||||
public:
|
||||
Events();
|
||||
void begin();
|
||||
|
||||
void onButtonShort(); // User button: short press
|
||||
void onButtonLong(); // User button: long press
|
||||
void onExitShort(); // Exit button: short press
|
||||
void onExitLong(); // Exit button: long press
|
||||
void onNavUp(); // Navigate up
|
||||
void onNavDown(); // Navigate down
|
||||
void onNavLeft(); // Navigate left
|
||||
void onNavRight(); // Navigate right
|
||||
void onButtonShort(); // User button: short press
|
||||
void onButtonLong(); // User button: long press
|
||||
void onExitShort(); // Exit button: short press
|
||||
void onExitLong(); // Exit button: long press
|
||||
void onNavUp(); // Navigate up
|
||||
void onNavDown(); // Navigate down
|
||||
void onNavLeft(); // Navigate left
|
||||
void onNavRight(); // Navigate right
|
||||
|
||||
int beforeDeepSleep(void *unused); // Prepare for shutdown
|
||||
int beforeReboot(void *unused); // Prepare for reboot
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message
|
||||
int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages
|
||||
int beforeDeepSleep(void *unused); // Prepare for shutdown
|
||||
int beforeReboot(void *unused); // Prepare for reboot
|
||||
int onReceiveTextMessage(const meshtastic_MeshPacket *packet); // Store most recent text message
|
||||
int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages
|
||||
#ifdef ARCH_ESP32
|
||||
int beforeLightSleep(void *unused); // Prepare for light sleep
|
||||
int beforeLightSleep(void *unused); // Prepare for light sleep
|
||||
#endif
|
||||
|
||||
private:
|
||||
// For convenience
|
||||
InkHUD *inkhud = nullptr;
|
||||
Persistence::Settings *settings = nullptr;
|
||||
private:
|
||||
// For convenience
|
||||
InkHUD *inkhud = nullptr;
|
||||
Persistence::Settings *settings = nullptr;
|
||||
|
||||
// Get notified when the system is shutting down
|
||||
CallbackObserver<Events, void *> deepSleepObserver = CallbackObserver<Events, void *>(this, &Events::beforeDeepSleep);
|
||||
// Get notified when the system is shutting down
|
||||
CallbackObserver<Events, void *> deepSleepObserver = CallbackObserver<Events, void *>(this, &Events::beforeDeepSleep);
|
||||
|
||||
// Get notified when the system is rebooting
|
||||
CallbackObserver<Events, void *> rebootObserver = CallbackObserver<Events, void *>(this, &Events::beforeReboot);
|
||||
// Get notified when the system is rebooting
|
||||
CallbackObserver<Events, void *> rebootObserver = CallbackObserver<Events, void *>(this, &Events::beforeReboot);
|
||||
|
||||
// Cache *incoming* text messages, for use by applets
|
||||
CallbackObserver<Events, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<Events, const meshtastic_MeshPacket *>(this, &Events::onReceiveTextMessage);
|
||||
// Cache *incoming* text messages, for use by applets
|
||||
CallbackObserver<Events, const meshtastic_MeshPacket *> textMessageObserver =
|
||||
CallbackObserver<Events, const meshtastic_MeshPacket *>(this, &Events::onReceiveTextMessage);
|
||||
|
||||
// Get notified of incoming admin messages, and handle any which are relevant to InkHUD
|
||||
CallbackObserver<Events, AdminModule_ObserverData *> adminMessageObserver =
|
||||
CallbackObserver<Events, AdminModule_ObserverData *>(this, &Events::onAdminMessage);
|
||||
// Get notified of incoming admin messages, and handle any which are relevant to InkHUD
|
||||
CallbackObserver<Events, AdminModule_ObserverData *> adminMessageObserver =
|
||||
CallbackObserver<Events, AdminModule_ObserverData *>(this, &Events::onAdminMessage);
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Get notified when the system is entering light sleep
|
||||
CallbackObserver<Events, void *> lightSleepObserver = CallbackObserver<Events, void *>(this, &Events::beforeLightSleep);
|
||||
// Get notified when the system is entering light sleep
|
||||
CallbackObserver<Events, void *> lightSleepObserver = CallbackObserver<Events, void *>(this, &Events::beforeLightSleep);
|
||||
#endif
|
||||
|
||||
// End any externalNotification beeping, buzzing, blinking etc
|
||||
bool dismissExternalNotification();
|
||||
// End any externalNotification beeping, buzzing, blinking etc
|
||||
bool dismissExternalNotification();
|
||||
|
||||
// If set, InkHUD's data will be erased during onReboot
|
||||
bool eraseOnReboot = false;
|
||||
// If set, InkHUD's data will be erased during onReboot
|
||||
bool eraseOnReboot = false;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -13,222 +13,174 @@
|
||||
using namespace NicheGraphics;
|
||||
|
||||
// Get or create the singleton
|
||||
InkHUD::InkHUD *InkHUD::InkHUD::getInstance()
|
||||
{
|
||||
// Create the singleton instance of our class, if not yet done
|
||||
static InkHUD *instance = nullptr;
|
||||
if (!instance) {
|
||||
instance = new InkHUD;
|
||||
InkHUD::InkHUD *InkHUD::InkHUD::getInstance() {
|
||||
// Create the singleton instance of our class, if not yet done
|
||||
static InkHUD *instance = nullptr;
|
||||
if (!instance) {
|
||||
instance = new InkHUD;
|
||||
|
||||
instance->persistence = new Persistence;
|
||||
instance->windowManager = new WindowManager;
|
||||
instance->renderer = new Renderer;
|
||||
instance->events = new Events;
|
||||
}
|
||||
instance->persistence = new Persistence;
|
||||
instance->windowManager = new WindowManager;
|
||||
instance->renderer = new Renderer;
|
||||
instance->events = new Events;
|
||||
}
|
||||
|
||||
return instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Connect the (fully set-up) E-Ink driver to InkHUD
|
||||
// Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is called
|
||||
void InkHUD::InkHUD::setDriver(Drivers::EInk *driver)
|
||||
{
|
||||
renderer->setDriver(driver);
|
||||
}
|
||||
void InkHUD::InkHUD::setDriver(Drivers::EInk *driver) { renderer->setDriver(driver); }
|
||||
|
||||
// Set the target number of FAST display updates in a row, before a FULL update is used for display health
|
||||
// This value applies only to updates with an UNSPECIFIED update type
|
||||
// If explicitly requested FAST updates exceed this target, the stressMultiplier parameter determines how many
|
||||
// subsequent FULL updates will be performed, in an attempt to restore the display's health
|
||||
void InkHUD::InkHUD::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier)
|
||||
{
|
||||
renderer->setDisplayResilience(fastPerFull, stressMultiplier);
|
||||
void InkHUD::InkHUD::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier) {
|
||||
renderer->setDisplayResilience(fastPerFull, stressMultiplier);
|
||||
}
|
||||
|
||||
// Register a user applet with InkHUD
|
||||
// A variant's nicheGraphics.h file should instantiate your chosen applets, then pass them to this method
|
||||
// Passing an applet to this method is all that is required to make it available to the user in your InkHUD build
|
||||
void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile)
|
||||
{
|
||||
windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile);
|
||||
void InkHUD::InkHUD::addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile) {
|
||||
windowManager->addApplet(name, a, defaultActive, defaultAutoshow, onTile);
|
||||
}
|
||||
|
||||
// Start InkHUD!
|
||||
// Call this only after you have configured InkHUD
|
||||
void InkHUD::InkHUD::begin()
|
||||
{
|
||||
persistence->loadSettings();
|
||||
persistence->loadLatestMessage();
|
||||
void InkHUD::InkHUD::begin() {
|
||||
persistence->loadSettings();
|
||||
persistence->loadLatestMessage();
|
||||
|
||||
windowManager->begin();
|
||||
events->begin();
|
||||
renderer->begin();
|
||||
// LogoApplet shows boot screen here
|
||||
windowManager->begin();
|
||||
events->begin();
|
||||
renderer->begin();
|
||||
// LogoApplet shows boot screen here
|
||||
}
|
||||
|
||||
// Call this when your user button gets a short press
|
||||
// Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?)
|
||||
void InkHUD::InkHUD::shortpress()
|
||||
{
|
||||
events->onButtonShort();
|
||||
}
|
||||
void InkHUD::InkHUD::shortpress() { events->onButtonShort(); }
|
||||
|
||||
// Call this when your user button gets a long press
|
||||
// Should be connected to an input source in nicheGraphics.h (NicheGraphics::Inputs::TwoButton?)
|
||||
void InkHUD::InkHUD::longpress()
|
||||
{
|
||||
events->onButtonLong();
|
||||
}
|
||||
void InkHUD::InkHUD::longpress() { events->onButtonLong(); }
|
||||
|
||||
// Call this when your exit button gets a short press
|
||||
void InkHUD::InkHUD::exitShort()
|
||||
{
|
||||
events->onExitShort();
|
||||
}
|
||||
void InkHUD::InkHUD::exitShort() { events->onExitShort(); }
|
||||
|
||||
// Call this when your exit button gets a long press
|
||||
void InkHUD::InkHUD::exitLong()
|
||||
{
|
||||
events->onExitLong();
|
||||
}
|
||||
void InkHUD::InkHUD::exitLong() { events->onExitLong(); }
|
||||
|
||||
// Call this when your joystick gets an up input
|
||||
void InkHUD::InkHUD::navUp()
|
||||
{
|
||||
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
|
||||
case 1: // 90 deg
|
||||
events->onNavLeft();
|
||||
break;
|
||||
case 2: // 180 deg
|
||||
events->onNavDown();
|
||||
break;
|
||||
case 3: // 270 deg
|
||||
events->onNavRight();
|
||||
break;
|
||||
default: // 0 deg
|
||||
events->onNavUp();
|
||||
break;
|
||||
}
|
||||
void InkHUD::InkHUD::navUp() {
|
||||
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
|
||||
case 1: // 90 deg
|
||||
events->onNavLeft();
|
||||
break;
|
||||
case 2: // 180 deg
|
||||
events->onNavDown();
|
||||
break;
|
||||
case 3: // 270 deg
|
||||
events->onNavRight();
|
||||
break;
|
||||
default: // 0 deg
|
||||
events->onNavUp();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Call this when your joystick gets a down input
|
||||
void InkHUD::InkHUD::navDown()
|
||||
{
|
||||
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
|
||||
case 1: // 90 deg
|
||||
events->onNavRight();
|
||||
break;
|
||||
case 2: // 180 deg
|
||||
events->onNavUp();
|
||||
break;
|
||||
case 3: // 270 deg
|
||||
events->onNavLeft();
|
||||
break;
|
||||
default: // 0 deg
|
||||
events->onNavDown();
|
||||
break;
|
||||
}
|
||||
void InkHUD::InkHUD::navDown() {
|
||||
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
|
||||
case 1: // 90 deg
|
||||
events->onNavRight();
|
||||
break;
|
||||
case 2: // 180 deg
|
||||
events->onNavUp();
|
||||
break;
|
||||
case 3: // 270 deg
|
||||
events->onNavLeft();
|
||||
break;
|
||||
default: // 0 deg
|
||||
events->onNavDown();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Call this when your joystick gets a left input
|
||||
void InkHUD::InkHUD::navLeft()
|
||||
{
|
||||
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
|
||||
case 1: // 90 deg
|
||||
events->onNavDown();
|
||||
break;
|
||||
case 2: // 180 deg
|
||||
events->onNavRight();
|
||||
break;
|
||||
case 3: // 270 deg
|
||||
events->onNavUp();
|
||||
break;
|
||||
default: // 0 deg
|
||||
events->onNavLeft();
|
||||
break;
|
||||
}
|
||||
void InkHUD::InkHUD::navLeft() {
|
||||
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
|
||||
case 1: // 90 deg
|
||||
events->onNavDown();
|
||||
break;
|
||||
case 2: // 180 deg
|
||||
events->onNavRight();
|
||||
break;
|
||||
case 3: // 270 deg
|
||||
events->onNavUp();
|
||||
break;
|
||||
default: // 0 deg
|
||||
events->onNavLeft();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Call this when your joystick gets a right input
|
||||
void InkHUD::InkHUD::navRight()
|
||||
{
|
||||
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
|
||||
case 1: // 90 deg
|
||||
events->onNavUp();
|
||||
break;
|
||||
case 2: // 180 deg
|
||||
events->onNavLeft();
|
||||
break;
|
||||
case 3: // 270 deg
|
||||
events->onNavDown();
|
||||
break;
|
||||
default: // 0 deg
|
||||
events->onNavRight();
|
||||
break;
|
||||
}
|
||||
void InkHUD::InkHUD::navRight() {
|
||||
switch ((persistence->settings.rotation + persistence->settings.joystick.alignment) % 4) {
|
||||
case 1: // 90 deg
|
||||
events->onNavUp();
|
||||
break;
|
||||
case 2: // 180 deg
|
||||
events->onNavLeft();
|
||||
break;
|
||||
case 3: // 270 deg
|
||||
events->onNavDown();
|
||||
break;
|
||||
default: // 0 deg
|
||||
events->onNavRight();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle the next user applet to the foreground
|
||||
// Only activated applets are cycled
|
||||
// If user has a multi-applet layout, the applets will cycle on the "focused tile"
|
||||
void InkHUD::InkHUD::nextApplet()
|
||||
{
|
||||
windowManager->nextApplet();
|
||||
}
|
||||
void InkHUD::InkHUD::nextApplet() { windowManager->nextApplet(); }
|
||||
|
||||
// Cycle the previous user applet to the foreground
|
||||
// Only activated applets are cycled
|
||||
// If user has a multi-applet layout, the applets will cycle on the "focused tile"
|
||||
void InkHUD::InkHUD::prevApplet()
|
||||
{
|
||||
windowManager->prevApplet();
|
||||
}
|
||||
void InkHUD::InkHUD::prevApplet() { windowManager->prevApplet(); }
|
||||
|
||||
// Show the menu (on the the focused tile)
|
||||
// The applet previously displayed there will be restored once the menu closes
|
||||
void InkHUD::InkHUD::openMenu()
|
||||
{
|
||||
windowManager->openMenu();
|
||||
}
|
||||
void InkHUD::InkHUD::openMenu() { windowManager->openMenu(); }
|
||||
|
||||
// Bring AlignStick applet to the foreground
|
||||
void InkHUD::InkHUD::openAlignStick()
|
||||
{
|
||||
windowManager->openAlignStick();
|
||||
}
|
||||
void InkHUD::InkHUD::openAlignStick() { windowManager->openAlignStick(); }
|
||||
|
||||
// In layouts where multiple applets are shown at once, change which tile is focused
|
||||
// The focused tile in the one which cycles applets on button short press, and displays menu on long press
|
||||
void InkHUD::InkHUD::nextTile()
|
||||
{
|
||||
windowManager->nextTile();
|
||||
}
|
||||
void InkHUD::InkHUD::nextTile() { windowManager->nextTile(); }
|
||||
|
||||
// In layouts where multiple applets are shown at once, change which tile is focused
|
||||
// The focused tile in the one which cycles applets on button short press, and displays menu on long press
|
||||
void InkHUD::InkHUD::prevTile()
|
||||
{
|
||||
windowManager->prevTile();
|
||||
}
|
||||
void InkHUD::InkHUD::prevTile() { windowManager->prevTile(); }
|
||||
|
||||
// Rotate the display image by 90 degrees
|
||||
void InkHUD::InkHUD::rotate()
|
||||
{
|
||||
windowManager->rotate();
|
||||
}
|
||||
void InkHUD::InkHUD::rotate() { windowManager->rotate(); }
|
||||
|
||||
// rotate the joystick in 90 degree increments
|
||||
void InkHUD::InkHUD::rotateJoystick(uint8_t angle)
|
||||
{
|
||||
persistence->settings.joystick.alignment += angle;
|
||||
persistence->settings.joystick.alignment %= 4;
|
||||
void InkHUD::InkHUD::rotateJoystick(uint8_t angle) {
|
||||
persistence->settings.joystick.alignment += angle;
|
||||
persistence->settings.joystick.alignment %= 4;
|
||||
}
|
||||
|
||||
// Show / hide the battery indicator in top-right
|
||||
void InkHUD::InkHUD::toggleBatteryIcon()
|
||||
{
|
||||
windowManager->toggleBatteryIcon();
|
||||
}
|
||||
void InkHUD::InkHUD::toggleBatteryIcon() { windowManager->toggleBatteryIcon(); }
|
||||
|
||||
// An applet asking for the display to be updated
|
||||
// This does not occur immediately
|
||||
@@ -236,64 +188,40 @@ void InkHUD::InkHUD::toggleBatteryIcon()
|
||||
// This allows multiple applets to observe the same event, and then share the same opportunity to update
|
||||
// Applets should requestUpdate, whether or not they are currently displayed ("foreground")
|
||||
// This is because they *might* be automatically brought to foreground by WindowManager::autoshow
|
||||
void InkHUD::InkHUD::requestUpdate()
|
||||
{
|
||||
renderer->requestUpdate();
|
||||
}
|
||||
void InkHUD::InkHUD::requestUpdate() { renderer->requestUpdate(); }
|
||||
|
||||
// Demand that the display be updated
|
||||
// Ignores all diplomacy:
|
||||
// - the display *will* update
|
||||
// - the specified update type *will* be used
|
||||
// If the async parameter is false, code flow is blocked while the update takes place
|
||||
void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool async)
|
||||
{
|
||||
renderer->forceUpdate(type, async);
|
||||
}
|
||||
void InkHUD::InkHUD::forceUpdate(EInk::UpdateTypes type, bool async) { renderer->forceUpdate(type, async); }
|
||||
|
||||
// Wait for any in-progress display update to complete before continuing
|
||||
void InkHUD::InkHUD::awaitUpdate()
|
||||
{
|
||||
renderer->awaitUpdate();
|
||||
}
|
||||
void InkHUD::InkHUD::awaitUpdate() { renderer->awaitUpdate(); }
|
||||
|
||||
// Ask the window manager to potentially bring a different user applet to foreground
|
||||
// An applet will be brought to foreground if it has just received new and relevant info
|
||||
// For Example: AllMessagesApplet has just received a new text message
|
||||
// Permission for this autoshow behavior is granted by the user, on an applet-by-applet basis
|
||||
// If autoshow brings an applet to foreground, an InkHUD notification will not be generated for the same event
|
||||
void InkHUD::InkHUD::autoshow()
|
||||
{
|
||||
windowManager->autoshow();
|
||||
}
|
||||
void InkHUD::InkHUD::autoshow() { windowManager->autoshow(); }
|
||||
|
||||
// Tell the window manager that the Persistence::Settings value for applet activation has changed,
|
||||
// and that it should reconfigure accordingly.
|
||||
// This is triggered at boot, or when the user enables / disabled applets via the on-screen menu
|
||||
void InkHUD::InkHUD::updateAppletSelection()
|
||||
{
|
||||
windowManager->changeActivatedApplets();
|
||||
}
|
||||
void InkHUD::InkHUD::updateAppletSelection() { windowManager->changeActivatedApplets(); }
|
||||
|
||||
// Tell the window manager that the Persistence::Settings value for layout or rotation has changed,
|
||||
// and that it should reconfigure accordingly.
|
||||
// This is triggered at boot, or by rotate / layout options in the on-screen menu
|
||||
void InkHUD::InkHUD::updateLayout()
|
||||
{
|
||||
windowManager->changeLayout();
|
||||
}
|
||||
void InkHUD::InkHUD::updateLayout() { windowManager->changeLayout(); }
|
||||
|
||||
// Width of the display, in the context of the current rotation
|
||||
uint16_t InkHUD::InkHUD::width()
|
||||
{
|
||||
return renderer->width();
|
||||
}
|
||||
uint16_t InkHUD::InkHUD::width() { return renderer->width(); }
|
||||
|
||||
// Height of the display, in the context of the current rotation
|
||||
uint16_t InkHUD::InkHUD::height()
|
||||
{
|
||||
return renderer->height();
|
||||
}
|
||||
uint16_t InkHUD::InkHUD::height() { return renderer->height(); }
|
||||
|
||||
// A collection of any user tiles which do not have a valid user applet
|
||||
// This can occur in various situations, such as when a user enables fewer applets than their layout has tiles
|
||||
@@ -301,23 +229,19 @@ uint16_t InkHUD::InkHUD::height()
|
||||
// The renderer needs to know which regions (if any) are empty,
|
||||
// in order to fill them with a "placeholder" pattern.
|
||||
// -- There may be a tidier way to accomplish this --
|
||||
std::vector<InkHUD::Tile *> InkHUD::InkHUD::getEmptyTiles()
|
||||
{
|
||||
return windowManager->getEmptyTiles();
|
||||
}
|
||||
std::vector<InkHUD::Tile *> InkHUD::InkHUD::getEmptyTiles() { return windowManager->getEmptyTiles(); }
|
||||
|
||||
// Get a system applet by its name
|
||||
// This isn't particularly elegant, but it does avoid:
|
||||
// - passing around a big set of references
|
||||
// - having two sets of references (systemApplet vector for iteration)
|
||||
InkHUD::SystemApplet *InkHUD::InkHUD::getSystemApplet(const char *name)
|
||||
{
|
||||
for (SystemApplet *sa : systemApplets) {
|
||||
if (strcmp(name, sa->name) == 0)
|
||||
return sa;
|
||||
}
|
||||
InkHUD::SystemApplet *InkHUD::InkHUD::getSystemApplet(const char *name) {
|
||||
for (SystemApplet *sa : systemApplets) {
|
||||
if (strcmp(name, sa->name) == 0)
|
||||
return sa;
|
||||
}
|
||||
|
||||
assert(false); // Invalid name
|
||||
assert(false); // Invalid name
|
||||
}
|
||||
|
||||
// Place a pixel into the image buffer
|
||||
@@ -326,9 +250,6 @@ InkHUD::SystemApplet *InkHUD::InkHUD::getSystemApplet(const char *name)
|
||||
// - Tiles pass translated pixels to this method
|
||||
// - this methods (Renderer) places rotated pixels into the image buffer
|
||||
// This method provides the final formatting step required. The image buffer is suitable for writing to display
|
||||
void InkHUD::InkHUD::drawPixel(int16_t x, int16_t y, Color c)
|
||||
{
|
||||
renderer->handlePixel(x, y, c);
|
||||
}
|
||||
void InkHUD::InkHUD::drawPixel(int16_t x, int16_t y, Color c) { renderer->handlePixel(x, y, c); }
|
||||
|
||||
#endif
|
||||
@@ -18,14 +18,13 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
// Color, understood by display controller IC (as bit values)
|
||||
// Also suitable for use as AdafruitGFX colors
|
||||
enum Color : uint8_t {
|
||||
BLACK = 0,
|
||||
WHITE = 1,
|
||||
BLACK = 0,
|
||||
WHITE = 1,
|
||||
};
|
||||
|
||||
class Applet;
|
||||
@@ -36,83 +35,82 @@ class SystemApplet;
|
||||
class Tile;
|
||||
class WindowManager;
|
||||
|
||||
class InkHUD
|
||||
{
|
||||
public:
|
||||
static InkHUD *getInstance(); // Access to this singleton class
|
||||
class InkHUD {
|
||||
public:
|
||||
static InkHUD *getInstance(); // Access to this singleton class
|
||||
|
||||
// Configuration
|
||||
// - before InkHUD::begin, in variant nicheGraphics.h,
|
||||
// Configuration
|
||||
// - before InkHUD::begin, in variant nicheGraphics.h,
|
||||
|
||||
void setDriver(Drivers::EInk *driver);
|
||||
void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0);
|
||||
void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1);
|
||||
void setDriver(Drivers::EInk *driver);
|
||||
void setDisplayResilience(uint8_t fastPerFull = 5, float stressMultiplier = 2.0);
|
||||
void addApplet(const char *name, Applet *a, bool defaultActive = false, bool defaultAutoshow = false, uint8_t onTile = -1);
|
||||
|
||||
void begin();
|
||||
void begin();
|
||||
|
||||
// Handle user-button press
|
||||
// - connected to an input source, in variant nicheGraphics.h
|
||||
// Handle user-button press
|
||||
// - connected to an input source, in variant nicheGraphics.h
|
||||
|
||||
void shortpress();
|
||||
void longpress();
|
||||
void exitShort();
|
||||
void exitLong();
|
||||
void navUp();
|
||||
void navDown();
|
||||
void navLeft();
|
||||
void navRight();
|
||||
void shortpress();
|
||||
void longpress();
|
||||
void exitShort();
|
||||
void exitLong();
|
||||
void navUp();
|
||||
void navDown();
|
||||
void navLeft();
|
||||
void navRight();
|
||||
|
||||
// Trigger UI changes
|
||||
// - called by various InkHUD components
|
||||
// - suitable(?) for use by aux button, connected in variant nicheGraphics.h
|
||||
// Trigger UI changes
|
||||
// - called by various InkHUD components
|
||||
// - suitable(?) for use by aux button, connected in variant nicheGraphics.h
|
||||
|
||||
void nextApplet();
|
||||
void prevApplet();
|
||||
void openMenu();
|
||||
void openAlignStick();
|
||||
void nextTile();
|
||||
void prevTile();
|
||||
void rotate();
|
||||
void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default
|
||||
void toggleBatteryIcon();
|
||||
void nextApplet();
|
||||
void prevApplet();
|
||||
void openMenu();
|
||||
void openAlignStick();
|
||||
void nextTile();
|
||||
void prevTile();
|
||||
void rotate();
|
||||
void rotateJoystick(uint8_t angle = 1); // rotate 90 deg by default
|
||||
void toggleBatteryIcon();
|
||||
|
||||
// Updating the display
|
||||
// - called by various InkHUD components
|
||||
// Updating the display
|
||||
// - called by various InkHUD components
|
||||
|
||||
void requestUpdate();
|
||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool async = true);
|
||||
void awaitUpdate();
|
||||
void requestUpdate();
|
||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED, bool async = true);
|
||||
void awaitUpdate();
|
||||
|
||||
// (Re)configuring WindowManager
|
||||
// (Re)configuring WindowManager
|
||||
|
||||
void autoshow(); // Bring an applet to foreground
|
||||
void updateAppletSelection(); // Change which applets are active
|
||||
void updateLayout(); // Change multiplexing (count, rotation)
|
||||
void autoshow(); // Bring an applet to foreground
|
||||
void updateAppletSelection(); // Change which applets are active
|
||||
void updateLayout(); // Change multiplexing (count, rotation)
|
||||
|
||||
// Information passed between components
|
||||
// Information passed between components
|
||||
|
||||
uint16_t width(); // From E-Ink driver
|
||||
uint16_t height(); // From E-Ink driver
|
||||
std::vector<Tile *> getEmptyTiles(); // From WindowManager
|
||||
uint16_t width(); // From E-Ink driver
|
||||
uint16_t height(); // From E-Ink driver
|
||||
std::vector<Tile *> getEmptyTiles(); // From WindowManager
|
||||
|
||||
// Applets
|
||||
// Applets
|
||||
|
||||
SystemApplet *getSystemApplet(const char *name);
|
||||
std::vector<Applet *> userApplets;
|
||||
std::vector<SystemApplet *> systemApplets;
|
||||
SystemApplet *getSystemApplet(const char *name);
|
||||
std::vector<Applet *> userApplets;
|
||||
std::vector<SystemApplet *> systemApplets;
|
||||
|
||||
// Pass drawing output to Renderer
|
||||
void drawPixel(int16_t x, int16_t y, Color c);
|
||||
// Pass drawing output to Renderer
|
||||
void drawPixel(int16_t x, int16_t y, Color c);
|
||||
|
||||
// Shared data which persists between boots
|
||||
Persistence *persistence = nullptr;
|
||||
// Shared data which persists between boots
|
||||
Persistence *persistence = nullptr;
|
||||
|
||||
private:
|
||||
InkHUD() {} // Constructor made private to force use of InkHUD::getInstance
|
||||
private:
|
||||
InkHUD() {} // Constructor made private to force use of InkHUD::getInstance
|
||||
|
||||
Events *events = nullptr; // Handle non-specific firmware events
|
||||
Renderer *renderer = nullptr; // Co-ordinate display updates
|
||||
WindowManager *windowManager = nullptr; // Multiplexing of applets
|
||||
Events *events = nullptr; // Handle non-specific firmware events
|
||||
Renderer *renderer = nullptr; // Co-ordinate display updates
|
||||
WindowManager *windowManager = nullptr; // Multiplexing of applets
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -12,142 +12,140 @@ using namespace NicheGraphics;
|
||||
constexpr uint8_t MAX_MESSAGES_SAVED = 10;
|
||||
constexpr uint32_t MAX_MESSAGE_SIZE = 250;
|
||||
|
||||
InkHUD::MessageStore::MessageStore(std::string label)
|
||||
{
|
||||
filename = "";
|
||||
filename += "/NicheGraphics";
|
||||
filename += "/";
|
||||
filename += label;
|
||||
filename += ".msgs";
|
||||
InkHUD::MessageStore::MessageStore(std::string label) {
|
||||
filename = "";
|
||||
filename += "/NicheGraphics";
|
||||
filename += "/";
|
||||
filename += label;
|
||||
filename += ".msgs";
|
||||
}
|
||||
|
||||
// Write the contents of the MessageStore::messages object to flash
|
||||
// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card.
|
||||
// Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally
|
||||
void InkHUD::MessageStore::saveToFlash()
|
||||
{
|
||||
assert(!filename.empty());
|
||||
// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD
|
||||
// card. Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally
|
||||
void InkHUD::MessageStore::saveToFlash() {
|
||||
assert(!filename.empty());
|
||||
|
||||
#ifdef FSCom
|
||||
// Make the directory, if doesn't already exist
|
||||
// This is the same directory accessed by NicheGraphics::FlashData
|
||||
spiLock->lock();
|
||||
FSCom.mkdir("/NicheGraphics");
|
||||
spiLock->unlock();
|
||||
// Make the directory, if doesn't already exist
|
||||
// This is the same directory accessed by NicheGraphics::FlashData
|
||||
spiLock->lock();
|
||||
FSCom.mkdir("/NicheGraphics");
|
||||
spiLock->unlock();
|
||||
|
||||
// Open or create the file
|
||||
// No "full atomic": don't save then rename
|
||||
auto f = SafeFile(filename.c_str(), false);
|
||||
// Open or create the file
|
||||
// No "full atomic": don't save then rename
|
||||
auto f = SafeFile(filename.c_str(), false);
|
||||
|
||||
LOG_INFO("Saving messages in %s", filename.c_str());
|
||||
LOG_INFO("Saving messages in %s", filename.c_str());
|
||||
|
||||
// Take firmware's SPI Lock while writing
|
||||
spiLock->lock();
|
||||
// Take firmware's SPI Lock while writing
|
||||
spiLock->lock();
|
||||
|
||||
// 1st byte: how many messages will be written to store
|
||||
f.write(messages.size());
|
||||
// 1st byte: how many messages will be written to store
|
||||
f.write(messages.size());
|
||||
|
||||
// For each message
|
||||
for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) {
|
||||
Message &m = messages.at(i);
|
||||
f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); // Write timestamp. 4 bytes
|
||||
f.write((uint8_t *)&m.sender, sizeof(m.sender)); // Write sender NodeId. 4 Bytes
|
||||
f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); // Write channel index. 1 Byte
|
||||
f.write((uint8_t *)m.text.c_str(), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text. Variable length
|
||||
f.write('\0'); // Append null term
|
||||
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str());
|
||||
}
|
||||
// For each message
|
||||
for (uint8_t i = 0; i < messages.size() && i < MAX_MESSAGES_SAVED; i++) {
|
||||
Message &m = messages.at(i);
|
||||
f.write((uint8_t *)&m.timestamp, sizeof(m.timestamp)); // Write timestamp. 4 bytes
|
||||
f.write((uint8_t *)&m.sender, sizeof(m.sender)); // Write sender NodeId. 4 Bytes
|
||||
f.write((uint8_t *)&m.channelIndex, sizeof(m.channelIndex)); // Write channel index. 1 Byte
|
||||
f.write((uint8_t *)m.text.c_str(), min(MAX_MESSAGE_SIZE, m.text.size())); // Write message text. Variable length
|
||||
f.write('\0'); // Append null term
|
||||
LOG_DEBUG("Wrote message %u, length %u, text \"%s\"", (uint32_t)i, min(MAX_MESSAGE_SIZE, m.text.size()), m.text.c_str());
|
||||
}
|
||||
|
||||
// Release firmware's SPI lock, because SafeFile::close needs it
|
||||
spiLock->unlock();
|
||||
// Release firmware's SPI lock, because SafeFile::close needs it
|
||||
spiLock->unlock();
|
||||
|
||||
bool writeSucceeded = f.close();
|
||||
bool writeSucceeded = f.close();
|
||||
|
||||
if (!writeSucceeded) {
|
||||
LOG_ERROR("Can't write data!");
|
||||
}
|
||||
if (!writeSucceeded) {
|
||||
LOG_ERROR("Can't write data!");
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("ERROR: Filesystem not implemented\n");
|
||||
LOG_ERROR("ERROR: Filesystem not implemented\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
// Attempt to load the previous contents of the MessageStore:message deque from flash.
|
||||
// Filename is controlled by the "label" parameter
|
||||
// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD card.
|
||||
void InkHUD::MessageStore::loadFromFlash()
|
||||
{
|
||||
// Hopefully redundant. Initial intention is to only load / save once per boot.
|
||||
messages.clear();
|
||||
// Takes the firmware's SPI lock during FS operations. Implemented for consistency, but only relevant when using SD
|
||||
// card.
|
||||
void InkHUD::MessageStore::loadFromFlash() {
|
||||
// Hopefully redundant. Initial intention is to only load / save once per boot.
|
||||
messages.clear();
|
||||
|
||||
#ifdef FSCom
|
||||
|
||||
// Take the firmware's SPI Lock, in case filesystem is on SD card
|
||||
concurrency::LockGuard guard(spiLock);
|
||||
// Take the firmware's SPI Lock, in case filesystem is on SD card
|
||||
concurrency::LockGuard guard(spiLock);
|
||||
|
||||
// Check that the file *does* actually exist
|
||||
if (!FSCom.exists(filename.c_str())) {
|
||||
LOG_WARN("'%s' not found. Using default values", filename.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the file *does* actually exist
|
||||
if (!FSCom.exists(filename.c_str())) {
|
||||
LOG_INFO("'%s' not found.", filename.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the file
|
||||
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
|
||||
|
||||
if (f.size() == 0) {
|
||||
LOG_INFO("%s is empty", filename.c_str());
|
||||
f.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// If opened, start reading
|
||||
if (f) {
|
||||
LOG_INFO("Loading threaded messages '%s'", filename.c_str());
|
||||
|
||||
// First byte: how many messages are in the flash store
|
||||
uint8_t flashMessageCount = 0;
|
||||
f.readBytes((char *)&flashMessageCount, 1);
|
||||
LOG_DEBUG("Messages available: %u", (uint32_t)flashMessageCount);
|
||||
|
||||
// For each message
|
||||
for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) {
|
||||
Message m;
|
||||
|
||||
// Read meta data (fixed width)
|
||||
f.readBytes((char *)&m.timestamp, sizeof(m.timestamp));
|
||||
f.readBytes((char *)&m.sender, sizeof(m.sender));
|
||||
f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex));
|
||||
|
||||
// Read characters until we find a null term
|
||||
char c;
|
||||
while (m.text.size() < MAX_MESSAGE_SIZE) {
|
||||
f.readBytes(&c, 1);
|
||||
if (c != '\0')
|
||||
m.text += c;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
// Store in RAM
|
||||
messages.push_back(m);
|
||||
|
||||
LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", (uint32_t)i, m.timestamp, m.sender, m.text.c_str());
|
||||
}
|
||||
|
||||
f.close();
|
||||
} else {
|
||||
LOG_ERROR("Could not open / read %s", filename.c_str());
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("Filesystem not implemented");
|
||||
state = LoadFileState::NO_FILESYSTEM;
|
||||
#endif
|
||||
// Check that the file *does* actually exist
|
||||
if (!FSCom.exists(filename.c_str())) {
|
||||
LOG_WARN("'%s' not found. Using default values", filename.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the file *does* actually exist
|
||||
if (!FSCom.exists(filename.c_str())) {
|
||||
LOG_INFO("'%s' not found.", filename.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// Open the file
|
||||
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
|
||||
|
||||
if (f.size() == 0) {
|
||||
LOG_INFO("%s is empty", filename.c_str());
|
||||
f.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// If opened, start reading
|
||||
if (f) {
|
||||
LOG_INFO("Loading threaded messages '%s'", filename.c_str());
|
||||
|
||||
// First byte: how many messages are in the flash store
|
||||
uint8_t flashMessageCount = 0;
|
||||
f.readBytes((char *)&flashMessageCount, 1);
|
||||
LOG_DEBUG("Messages available: %u", (uint32_t)flashMessageCount);
|
||||
|
||||
// For each message
|
||||
for (uint8_t i = 0; i < flashMessageCount && i < MAX_MESSAGES_SAVED; i++) {
|
||||
Message m;
|
||||
|
||||
// Read meta data (fixed width)
|
||||
f.readBytes((char *)&m.timestamp, sizeof(m.timestamp));
|
||||
f.readBytes((char *)&m.sender, sizeof(m.sender));
|
||||
f.readBytes((char *)&m.channelIndex, sizeof(m.channelIndex));
|
||||
|
||||
// Read characters until we find a null term
|
||||
char c;
|
||||
while (m.text.size() < MAX_MESSAGE_SIZE) {
|
||||
f.readBytes(&c, 1);
|
||||
if (c != '\0')
|
||||
m.text += c;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
// Store in RAM
|
||||
messages.push_back(m);
|
||||
|
||||
LOG_DEBUG("#%u, timestamp=%u, sender(num)=%u, text=\"%s\"", (uint32_t)i, m.timestamp, m.sender, m.text.c_str());
|
||||
}
|
||||
|
||||
f.close();
|
||||
} else {
|
||||
LOG_ERROR("Could not open / read %s", filename.c_str());
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("Filesystem not implemented");
|
||||
state = LoadFileState::NO_FILESYSTEM;
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -16,30 +16,28 @@ and methods for serializing them to flash.
|
||||
|
||||
#include "mesh/MeshTypes.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class MessageStore
|
||||
{
|
||||
public:
|
||||
// A stored message
|
||||
struct Message {
|
||||
uint32_t timestamp; // Epoch seconds
|
||||
NodeNum sender = 0;
|
||||
uint8_t channelIndex;
|
||||
std::string text;
|
||||
};
|
||||
class MessageStore {
|
||||
public:
|
||||
// A stored message
|
||||
struct Message {
|
||||
uint32_t timestamp; // Epoch seconds
|
||||
NodeNum sender = 0;
|
||||
uint8_t channelIndex;
|
||||
std::string text;
|
||||
};
|
||||
|
||||
MessageStore() = delete;
|
||||
explicit MessageStore(std::string label); // Label determines filename in flash
|
||||
MessageStore() = delete;
|
||||
explicit MessageStore(std::string label); // Label determines filename in flash
|
||||
|
||||
void saveToFlash();
|
||||
void loadFromFlash();
|
||||
void saveToFlash();
|
||||
void loadFromFlash();
|
||||
|
||||
std::deque<Message> messages; // Interact with this object!
|
||||
std::deque<Message> messages; // Interact with this object!
|
||||
|
||||
private:
|
||||
std::string filename;
|
||||
private:
|
||||
std::string filename;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -5,52 +5,47 @@
|
||||
using namespace NicheGraphics;
|
||||
|
||||
// Load settings and latestMessage data
|
||||
void InkHUD::Persistence::loadSettings()
|
||||
{
|
||||
// Load the InkHUD settings from flash, and check version number
|
||||
// We should only consider the version number if the InkHUD flashdata component reports that we *did* actually load flash data
|
||||
Settings loadedSettings;
|
||||
bool loadSucceeded = FlashData<Settings>::load(&loadedSettings, "settings");
|
||||
if (loadSucceeded && loadedSettings.meta.version == SETTINGS_VERSION && loadedSettings.meta.version != 0)
|
||||
settings = loadedSettings; // Version matched, replace the defaults with the loaded values
|
||||
else
|
||||
LOG_WARN("Settings version changed. Using defaults");
|
||||
void InkHUD::Persistence::loadSettings() {
|
||||
// Load the InkHUD settings from flash, and check version number
|
||||
// We should only consider the version number if the InkHUD flashdata component reports that we *did* actually load
|
||||
// flash data
|
||||
Settings loadedSettings;
|
||||
bool loadSucceeded = FlashData<Settings>::load(&loadedSettings, "settings");
|
||||
if (loadSucceeded && loadedSettings.meta.version == SETTINGS_VERSION && loadedSettings.meta.version != 0)
|
||||
settings = loadedSettings; // Version matched, replace the defaults with the loaded values
|
||||
else
|
||||
LOG_WARN("Settings version changed. Using defaults");
|
||||
}
|
||||
|
||||
// Load settings and latestMessage data
|
||||
void InkHUD::Persistence::loadLatestMessage()
|
||||
{
|
||||
// Load previous "latestMessages" data from flash
|
||||
MessageStore store("latest");
|
||||
store.loadFromFlash();
|
||||
void InkHUD::Persistence::loadLatestMessage() {
|
||||
// Load previous "latestMessages" data from flash
|
||||
MessageStore store("latest");
|
||||
store.loadFromFlash();
|
||||
|
||||
// Place into latestMessage struct, for convenient access
|
||||
// Number of strings loaded determines whether last message was broadcast or dm
|
||||
if (store.messages.size() == 1) {
|
||||
latestMessage.dm = store.messages.at(0);
|
||||
latestMessage.wasBroadcast = false;
|
||||
} else if (store.messages.size() == 2) {
|
||||
latestMessage.dm = store.messages.at(0);
|
||||
latestMessage.broadcast = store.messages.at(1);
|
||||
latestMessage.wasBroadcast = true;
|
||||
}
|
||||
// Place into latestMessage struct, for convenient access
|
||||
// Number of strings loaded determines whether last message was broadcast or dm
|
||||
if (store.messages.size() == 1) {
|
||||
latestMessage.dm = store.messages.at(0);
|
||||
latestMessage.wasBroadcast = false;
|
||||
} else if (store.messages.size() == 2) {
|
||||
latestMessage.dm = store.messages.at(0);
|
||||
latestMessage.broadcast = store.messages.at(1);
|
||||
latestMessage.wasBroadcast = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Save the InkHUD settings to flash
|
||||
void InkHUD::Persistence::saveSettings()
|
||||
{
|
||||
FlashData<Settings>::save(&settings, "settings");
|
||||
}
|
||||
void InkHUD::Persistence::saveSettings() { FlashData<Settings>::save(&settings, "settings"); }
|
||||
|
||||
// Save latestMessage data to flash
|
||||
void InkHUD::Persistence::saveLatestMessage()
|
||||
{
|
||||
// Number of strings saved determines whether last message was broadcast or dm
|
||||
MessageStore store("latest");
|
||||
store.messages.push_back(latestMessage.dm);
|
||||
if (latestMessage.wasBroadcast)
|
||||
store.messages.push_back(latestMessage.broadcast);
|
||||
store.saveToFlash();
|
||||
void InkHUD::Persistence::saveLatestMessage() {
|
||||
// Number of strings saved determines whether last message was broadcast or dm
|
||||
MessageStore store("latest");
|
||||
store.messages.push_back(latestMessage.dm);
|
||||
if (latestMessage.wasBroadcast)
|
||||
store.messages.push_back(latestMessage.broadcast);
|
||||
store.saveToFlash();
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -18,126 +18,124 @@ The save / load mechanism is a shared NicheGraphics feature.
|
||||
#include "graphics/niche/InkHUD/MessageStore.h"
|
||||
#include "graphics/niche/Utils/FlashData.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class Persistence
|
||||
{
|
||||
public:
|
||||
static constexpr uint8_t MAX_TILES_GLOBAL = 4;
|
||||
static constexpr uint8_t MAX_USERAPPLETS_GLOBAL = 16;
|
||||
class Persistence {
|
||||
public:
|
||||
static constexpr uint8_t MAX_TILES_GLOBAL = 4;
|
||||
static constexpr uint8_t MAX_USERAPPLETS_GLOBAL = 16;
|
||||
|
||||
// Used to invalidate old settings, if needed
|
||||
// Version 0 is reserved for testing, and will always load defaults
|
||||
static constexpr uint32_t SETTINGS_VERSION = 3;
|
||||
// Used to invalidate old settings, if needed
|
||||
// Version 0 is reserved for testing, and will always load defaults
|
||||
static constexpr uint32_t SETTINGS_VERSION = 3;
|
||||
|
||||
struct Settings {
|
||||
struct Meta {
|
||||
// Used to invalidate old savefiles, if we make breaking changes
|
||||
uint32_t version = SETTINGS_VERSION;
|
||||
} meta;
|
||||
struct Settings {
|
||||
struct Meta {
|
||||
// Used to invalidate old savefiles, if we make breaking changes
|
||||
uint32_t version = SETTINGS_VERSION;
|
||||
} meta;
|
||||
|
||||
struct UserTiles {
|
||||
// How many tiles are shown
|
||||
uint8_t count = 1;
|
||||
struct UserTiles {
|
||||
// How many tiles are shown
|
||||
uint8_t count = 1;
|
||||
|
||||
// Maximum amount of tiles for this display
|
||||
uint8_t maxCount = 4;
|
||||
// Maximum amount of tiles for this display
|
||||
uint8_t maxCount = 4;
|
||||
|
||||
// Which tile is focused (responding to user button input)
|
||||
uint8_t focused = 0;
|
||||
// Which tile is focused (responding to user button input)
|
||||
uint8_t focused = 0;
|
||||
|
||||
// Which applet is displayed on which tile
|
||||
// Index of array: which tile, as indexed in WindowManager::userTiles
|
||||
// Value of array: which applet, as indexed in InkHUD::userApplets
|
||||
uint8_t displayedUserApplet[MAX_TILES_GLOBAL] = {0, 1, 2, 3};
|
||||
} userTiles;
|
||||
// Which applet is displayed on which tile
|
||||
// Index of array: which tile, as indexed in WindowManager::userTiles
|
||||
// Value of array: which applet, as indexed in InkHUD::userApplets
|
||||
uint8_t displayedUserApplet[MAX_TILES_GLOBAL] = {0, 1, 2, 3};
|
||||
} userTiles;
|
||||
|
||||
struct UserApplets {
|
||||
// Which applets are running (either displayed, or in the background)
|
||||
// Index of array: which applet, as indexed in InkHUD::userApplets
|
||||
// Initial value is set by the "activeByDefault" parameter of InkHUD::addApplet, in setupNicheGraphics method
|
||||
bool active[MAX_USERAPPLETS_GLOBAL]{false};
|
||||
struct UserApplets {
|
||||
// Which applets are running (either displayed, or in the background)
|
||||
// Index of array: which applet, as indexed in InkHUD::userApplets
|
||||
// Initial value is set by the "activeByDefault" parameter of InkHUD::addApplet, in setupNicheGraphics method
|
||||
bool active[MAX_USERAPPLETS_GLOBAL]{false};
|
||||
|
||||
// Which user applets should be automatically shown when they have important data to show
|
||||
// If none set, foreground applets should remain foreground without manual user input
|
||||
// If multiple applets request this at once,
|
||||
// priority is the order which they were passed to InkHUD::addApplets, in setupNicheGraphics method
|
||||
bool autoshow[MAX_USERAPPLETS_GLOBAL]{false};
|
||||
} userApplets;
|
||||
// Which user applets should be automatically shown when they have important data to show
|
||||
// If none set, foreground applets should remain foreground without manual user input
|
||||
// If multiple applets request this at once,
|
||||
// priority is the order which they were passed to InkHUD::addApplets, in setupNicheGraphics method
|
||||
bool autoshow[MAX_USERAPPLETS_GLOBAL]{false};
|
||||
} userApplets;
|
||||
|
||||
// Features which the user can enable / disable via the on-screen menu
|
||||
struct OptionalFeatures {
|
||||
bool notifications = true;
|
||||
bool batteryIcon = false;
|
||||
} optionalFeatures;
|
||||
// Features which the user can enable / disable via the on-screen menu
|
||||
struct OptionalFeatures {
|
||||
bool notifications = true;
|
||||
bool batteryIcon = false;
|
||||
} optionalFeatures;
|
||||
|
||||
// Some menu items may not be required, based on device / configuration
|
||||
// We can enable them only when needed, to de-clutter the menu
|
||||
struct OptionalMenuItems {
|
||||
// If aux button is used to swap between tiles, we have no need for this menu item
|
||||
bool nextTile = true;
|
||||
// Some menu items may not be required, based on device / configuration
|
||||
// We can enable them only when needed, to de-clutter the menu
|
||||
struct OptionalMenuItems {
|
||||
// If aux button is used to swap between tiles, we have no need for this menu item
|
||||
bool nextTile = true;
|
||||
|
||||
// Used if backlight present, and not controlled by AUX button
|
||||
// If this item is added to menu: backlight is always active when menu is open
|
||||
// The added menu items then allows the user to "Keep Backlight On", globally.
|
||||
bool backlight = false;
|
||||
} optionalMenuItems;
|
||||
// Used if backlight present, and not controlled by AUX button
|
||||
// If this item is added to menu: backlight is always active when menu is open
|
||||
// The added menu items then allows the user to "Keep Backlight On", globally.
|
||||
bool backlight = false;
|
||||
} optionalMenuItems;
|
||||
|
||||
// Allows tips to be run once only
|
||||
struct Tips {
|
||||
// Enables the longer "tutorial" shown only on first boot
|
||||
// Once tutorial has been completed, it is no longer shown
|
||||
bool firstBoot = true;
|
||||
// Allows tips to be run once only
|
||||
struct Tips {
|
||||
// Enables the longer "tutorial" shown only on first boot
|
||||
// Once tutorial has been completed, it is no longer shown
|
||||
bool firstBoot = true;
|
||||
|
||||
// User is advised to shut down before removing device power
|
||||
// Once user executes a shutdown (either via menu or client app),
|
||||
// this tip is no longer shown
|
||||
bool safeShutdownSeen = false;
|
||||
} tips;
|
||||
// User is advised to shut down before removing device power
|
||||
// Once user executes a shutdown (either via menu or client app),
|
||||
// this tip is no longer shown
|
||||
bool safeShutdownSeen = false;
|
||||
} tips;
|
||||
|
||||
// Joystick settings for enabling and aligning to the screen
|
||||
struct Joystick {
|
||||
// Modifies the UI for joystick use
|
||||
bool enabled = false;
|
||||
// Joystick settings for enabling and aligning to the screen
|
||||
struct Joystick {
|
||||
// Modifies the UI for joystick use
|
||||
bool enabled = false;
|
||||
|
||||
// gets set to true when AlignStick applet is completed
|
||||
bool aligned = false;
|
||||
// gets set to true when AlignStick applet is completed
|
||||
bool aligned = false;
|
||||
|
||||
// Rotation of the joystick
|
||||
// Multiples of 90 degrees clockwise
|
||||
uint8_t alignment = 0;
|
||||
} joystick;
|
||||
// Rotation of the joystick
|
||||
// Multiples of 90 degrees clockwise
|
||||
uint8_t alignment = 0;
|
||||
} joystick;
|
||||
|
||||
// Rotation of the display
|
||||
// Multiples of 90 degrees clockwise
|
||||
// Most commonly: rotation is 0 when flex connector is oriented below display
|
||||
uint8_t rotation = 0;
|
||||
// Rotation of the display
|
||||
// Multiples of 90 degrees clockwise
|
||||
// Most commonly: rotation is 0 when flex connector is oriented below display
|
||||
uint8_t rotation = 0;
|
||||
|
||||
// How long do we consider another node to be "active"?
|
||||
// Used when applets want to filter for "active nodes" only
|
||||
uint32_t recentlyActiveSeconds = 2 * 60;
|
||||
};
|
||||
// How long do we consider another node to be "active"?
|
||||
// Used when applets want to filter for "active nodes" only
|
||||
uint32_t recentlyActiveSeconds = 2 * 60;
|
||||
};
|
||||
|
||||
// Most recently received text message
|
||||
// Value is updated by InkHUD::WindowManager, as a courtesy to applets
|
||||
// Note: different from devicestate.rx_text_message,
|
||||
// which may contain an *outgoing message* to broadcast
|
||||
struct LatestMessage {
|
||||
MessageStore::Message broadcast; // Most recent message received broadcast
|
||||
MessageStore::Message dm; // Most recent received DM
|
||||
bool wasBroadcast; // True if most recent broadcast is newer than most recent dm
|
||||
};
|
||||
// Most recently received text message
|
||||
// Value is updated by InkHUD::WindowManager, as a courtesy to applets
|
||||
// Note: different from devicestate.rx_text_message,
|
||||
// which may contain an *outgoing message* to broadcast
|
||||
struct LatestMessage {
|
||||
MessageStore::Message broadcast; // Most recent message received broadcast
|
||||
MessageStore::Message dm; // Most recent received DM
|
||||
bool wasBroadcast; // True if most recent broadcast is newer than most recent dm
|
||||
};
|
||||
|
||||
void loadSettings();
|
||||
void saveSettings();
|
||||
void loadLatestMessage();
|
||||
void saveLatestMessage();
|
||||
void loadSettings();
|
||||
void saveSettings();
|
||||
void loadLatestMessage();
|
||||
void saveLatestMessage();
|
||||
|
||||
// void printSettings(Settings *settings); // Debugging use only
|
||||
// void printSettings(Settings *settings); // Debugging use only
|
||||
|
||||
Settings settings;
|
||||
LatestMessage latestMessage;
|
||||
Settings settings;
|
||||
LatestMessage latestMessage;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -10,67 +10,61 @@
|
||||
|
||||
using namespace NicheGraphics;
|
||||
|
||||
InkHUD::Renderer::Renderer() : concurrency::OSThread("Renderer")
|
||||
{
|
||||
// Nothing for the timer to do just yet
|
||||
OSThread::disable();
|
||||
InkHUD::Renderer::Renderer() : concurrency::OSThread("Renderer") {
|
||||
// Nothing for the timer to do just yet
|
||||
OSThread::disable();
|
||||
|
||||
// Convenient references
|
||||
inkhud = InkHUD::getInstance();
|
||||
settings = &inkhud->persistence->settings;
|
||||
// Convenient references
|
||||
inkhud = InkHUD::getInstance();
|
||||
settings = &inkhud->persistence->settings;
|
||||
}
|
||||
|
||||
// Connect the (fully set-up) E-Ink driver to InkHUD
|
||||
// Should happen in your variant's nicheGraphics.h file, before InkHUD::begin is called
|
||||
void InkHUD::Renderer::setDriver(Drivers::EInk *driver)
|
||||
{
|
||||
// Make sure not already set
|
||||
if (this->driver) {
|
||||
LOG_ERROR("Driver already set");
|
||||
delay(2000); // Wait for native serial..
|
||||
assert(false);
|
||||
}
|
||||
void InkHUD::Renderer::setDriver(Drivers::EInk *driver) {
|
||||
// Make sure not already set
|
||||
if (this->driver) {
|
||||
LOG_ERROR("Driver already set");
|
||||
delay(2000); // Wait for native serial..
|
||||
assert(false);
|
||||
}
|
||||
|
||||
// Store the driver which was created in setupNicheGraphics()
|
||||
this->driver = driver;
|
||||
// Store the driver which was created in setupNicheGraphics()
|
||||
this->driver = driver;
|
||||
|
||||
// Determine the dimensions 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.
|
||||
imageBufferWidth = ((driver->width - 1) / 8) + 1;
|
||||
imageBufferHeight = driver->height;
|
||||
// Determine the dimensions 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.
|
||||
imageBufferWidth = ((driver->width - 1) / 8) + 1;
|
||||
imageBufferHeight = driver->height;
|
||||
|
||||
// Allocate the image buffer
|
||||
imageBuffer = new uint8_t[imageBufferWidth * imageBufferHeight];
|
||||
// Allocate the image buffer
|
||||
imageBuffer = new uint8_t[imageBufferWidth * imageBufferHeight];
|
||||
}
|
||||
|
||||
// Set the target number of FAST display updates in a row, before a FULL update is used for display health
|
||||
// This value applies only to updates with an UNSPECIFIED update type
|
||||
// If explicitly requested FAST updates exceed this target, the stressMultiplier parameter determines how many
|
||||
// subsequent FULL updates will be performed, in an attempt to restore the display's health
|
||||
void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier)
|
||||
{
|
||||
displayHealth.fastPerFull = fastPerFull;
|
||||
displayHealth.stressMultiplier = stressMultiplier;
|
||||
void InkHUD::Renderer::setDisplayResilience(uint8_t fastPerFull, float stressMultiplier) {
|
||||
displayHealth.fastPerFull = fastPerFull;
|
||||
displayHealth.stressMultiplier = stressMultiplier;
|
||||
}
|
||||
|
||||
void InkHUD::Renderer::begin()
|
||||
{
|
||||
forceUpdate(Drivers::EInk::UpdateTypes::FULL, false);
|
||||
}
|
||||
void InkHUD::Renderer::begin() { forceUpdate(Drivers::EInk::UpdateTypes::FULL, false); }
|
||||
|
||||
// Set a flag, which will be picked up by runOnce, ASAP.
|
||||
// Quite likely, multiple applets will all want to respond to one event (Observable, etc)
|
||||
// Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next runOnce
|
||||
void InkHUD::Renderer::requestUpdate()
|
||||
{
|
||||
requested = true;
|
||||
// Each affected applet can independently call requestUpdate(), and all share the one opportunity to render, at next
|
||||
// runOnce
|
||||
void InkHUD::Renderer::requestUpdate() {
|
||||
requested = true;
|
||||
|
||||
// We will run the thread as soon as we loop(),
|
||||
// after all Applets have had a chance to observe whatever event set this off
|
||||
OSThread::setIntervalFromNow(0);
|
||||
OSThread::enabled = true;
|
||||
runASAP = true;
|
||||
// We will run the thread as soon as we loop(),
|
||||
// after all Applets have had a chance to observe whatever event set this off
|
||||
OSThread::setIntervalFromNow(0);
|
||||
OSThread::enabled = true;
|
||||
runASAP = true;
|
||||
}
|
||||
|
||||
// requestUpdate will not actually update if no requests were made by applets which are actually visible
|
||||
@@ -79,174 +73,166 @@ void InkHUD::Renderer::requestUpdate()
|
||||
// Sometimes, however, we will want to trigger a display update manually, in the absence of any sort of applet event
|
||||
// Display health, for example.
|
||||
// In these situations, we use forceUpdate
|
||||
void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool async)
|
||||
{
|
||||
requested = true;
|
||||
forced = true;
|
||||
displayHealth.forceUpdateType(type);
|
||||
void InkHUD::Renderer::forceUpdate(Drivers::EInk::UpdateTypes type, bool async) {
|
||||
requested = true;
|
||||
forced = true;
|
||||
displayHealth.forceUpdateType(type);
|
||||
|
||||
// Normally, we need to start the timer, in case the display is busy and we briefly defer the update
|
||||
if (async) {
|
||||
// We will run the thread as soon as we loop(),
|
||||
// after all Applets have had a chance to observe whatever event set this off
|
||||
OSThread::setIntervalFromNow(0);
|
||||
OSThread::enabled = true;
|
||||
runASAP = true;
|
||||
}
|
||||
// Normally, we need to start the timer, in case the display is busy and we briefly defer the update
|
||||
if (async) {
|
||||
// We will run the thread as soon as we loop(),
|
||||
// after all Applets have had a chance to observe whatever event set this off
|
||||
OSThread::setIntervalFromNow(0);
|
||||
OSThread::enabled = true;
|
||||
runASAP = true;
|
||||
}
|
||||
|
||||
// If the update is *not* asynchronous, we begin the render process directly here
|
||||
// so that it can block code flow while running
|
||||
else
|
||||
render(false);
|
||||
// If the update is *not* asynchronous, we begin the render process directly here
|
||||
// so that it can block code flow while running
|
||||
else
|
||||
render(false);
|
||||
}
|
||||
|
||||
// Wait for any in-progress display update to complete before continuing
|
||||
void InkHUD::Renderer::awaitUpdate()
|
||||
{
|
||||
if (driver->busy()) {
|
||||
LOG_INFO("Waiting for display");
|
||||
driver->await(); // Wait here for update to complete
|
||||
}
|
||||
void InkHUD::Renderer::awaitUpdate() {
|
||||
if (driver->busy()) {
|
||||
LOG_INFO("Waiting for display");
|
||||
driver->await(); // Wait here for update to complete
|
||||
}
|
||||
}
|
||||
|
||||
// Set a ready-to-draw pixel into the image buffer
|
||||
// All rotations / translations have already taken place: this buffer data is formatted ready for the driver
|
||||
void InkHUD::Renderer::handlePixel(int16_t x, int16_t y, Color c)
|
||||
{
|
||||
rotatePixelCoords(&x, &y);
|
||||
void InkHUD::Renderer::handlePixel(int16_t x, int16_t y, Color c) {
|
||||
rotatePixelCoords(&x, &y);
|
||||
|
||||
uint32_t byteNum = (y * imageBufferWidth) + (x / 8); // X data is 8 pixels per byte
|
||||
uint8_t bitNum = 7 - (x % 8); // Invert order: leftmost bit (most significant) is leftmost pixel of byte.
|
||||
uint32_t byteNum = (y * imageBufferWidth) + (x / 8); // X data is 8 pixels per byte
|
||||
uint8_t bitNum = 7 - (x % 8); // Invert order: leftmost bit (most significant) is leftmost pixel of byte.
|
||||
|
||||
bitWrite(imageBuffer[byteNum], bitNum, c);
|
||||
bitWrite(imageBuffer[byteNum], bitNum, c);
|
||||
}
|
||||
|
||||
// Width of the display, relative to rotation
|
||||
uint16_t InkHUD::Renderer::width()
|
||||
{
|
||||
if (settings->rotation % 2)
|
||||
return driver->height;
|
||||
else
|
||||
return driver->width;
|
||||
uint16_t InkHUD::Renderer::width() {
|
||||
if (settings->rotation % 2)
|
||||
return driver->height;
|
||||
else
|
||||
return driver->width;
|
||||
}
|
||||
|
||||
// Height of the display, relative to rotation
|
||||
uint16_t InkHUD::Renderer::height()
|
||||
{
|
||||
if (settings->rotation % 2)
|
||||
return driver->width;
|
||||
else
|
||||
return driver->height;
|
||||
uint16_t InkHUD::Renderer::height() {
|
||||
if (settings->rotation % 2)
|
||||
return driver->width;
|
||||
else
|
||||
return driver->height;
|
||||
}
|
||||
|
||||
// Runs at regular intervals
|
||||
// - postponing render: until next loop(), allowing all applets to be notified of some Mesh event before render
|
||||
// - queuing another render: while one is already is progress
|
||||
int32_t InkHUD::Renderer::runOnce()
|
||||
{
|
||||
// If an applet asked to render, and hardware is able, lets try now
|
||||
if (requested && !driver->busy()) {
|
||||
render();
|
||||
}
|
||||
int32_t InkHUD::Renderer::runOnce() {
|
||||
// If an applet asked to render, and hardware is able, lets try now
|
||||
if (requested && !driver->busy()) {
|
||||
render();
|
||||
}
|
||||
|
||||
// If our render() call failed, try again shortly
|
||||
// otherwise, stop our thread until next update due
|
||||
if (requested)
|
||||
return 250UL;
|
||||
else
|
||||
return OSThread::disable();
|
||||
// If our render() call failed, try again shortly
|
||||
// otherwise, stop our thread until next update due
|
||||
if (requested)
|
||||
return 250UL;
|
||||
else
|
||||
return OSThread::disable();
|
||||
}
|
||||
|
||||
// Applies the system-wide rotation to pixel positions
|
||||
// This step is applied to image data which has already been translated by a Tile object
|
||||
// This is the final step before the pixel is placed into the image buffer
|
||||
// No return: values of the *x and *y parameters are modified by the method
|
||||
void InkHUD::Renderer::rotatePixelCoords(int16_t *x, int16_t *y)
|
||||
{
|
||||
// Apply a global rotation to pixel locations
|
||||
int16_t x1 = 0;
|
||||
int16_t y1 = 0;
|
||||
switch (settings->rotation) {
|
||||
case 0:
|
||||
x1 = *x;
|
||||
y1 = *y;
|
||||
break;
|
||||
case 1:
|
||||
x1 = (driver->width - 1) - *y;
|
||||
y1 = *x;
|
||||
break;
|
||||
case 2:
|
||||
x1 = (driver->width - 1) - *x;
|
||||
y1 = (driver->height - 1) - *y;
|
||||
break;
|
||||
case 3:
|
||||
x1 = *y;
|
||||
y1 = (driver->height - 1) - *x;
|
||||
break;
|
||||
}
|
||||
*x = x1;
|
||||
*y = y1;
|
||||
void InkHUD::Renderer::rotatePixelCoords(int16_t *x, int16_t *y) {
|
||||
// Apply a global rotation to pixel locations
|
||||
int16_t x1 = 0;
|
||||
int16_t y1 = 0;
|
||||
switch (settings->rotation) {
|
||||
case 0:
|
||||
x1 = *x;
|
||||
y1 = *y;
|
||||
break;
|
||||
case 1:
|
||||
x1 = (driver->width - 1) - *y;
|
||||
y1 = *x;
|
||||
break;
|
||||
case 2:
|
||||
x1 = (driver->width - 1) - *x;
|
||||
y1 = (driver->height - 1) - *y;
|
||||
break;
|
||||
case 3:
|
||||
x1 = *y;
|
||||
y1 = (driver->height - 1) - *x;
|
||||
break;
|
||||
}
|
||||
*x = x1;
|
||||
*y = y1;
|
||||
}
|
||||
|
||||
// Make an attempt to gather image data from some / all applets, and update the display
|
||||
// Might not be possible right now, if update already is progress.
|
||||
void InkHUD::Renderer::render(bool async)
|
||||
{
|
||||
// Make sure the display is ready for a new update
|
||||
if (async) {
|
||||
// Previous update still running, Will try again shortly, via runOnce()
|
||||
if (driver->busy())
|
||||
return;
|
||||
} else {
|
||||
// Wait here for previous update to complete
|
||||
driver->await();
|
||||
void InkHUD::Renderer::render(bool async) {
|
||||
// Make sure the display is ready for a new update
|
||||
if (async) {
|
||||
// Previous update still running, Will try again shortly, via runOnce()
|
||||
if (driver->busy())
|
||||
return;
|
||||
} else {
|
||||
// Wait here for previous update to complete
|
||||
driver->await();
|
||||
}
|
||||
|
||||
// Determine if a system applet has requested exclusive rights to request an update,
|
||||
// or exclusive rights to render
|
||||
checkLocks();
|
||||
|
||||
// (Potentially) change applet to display new info,
|
||||
// then check if this newly displayed applet makes a pending notification redundant
|
||||
inkhud->autoshow();
|
||||
|
||||
// If an update is justified.
|
||||
// We don't know this until after autoshow has run, as new applets may now be in foreground
|
||||
if (shouldUpdate()) {
|
||||
|
||||
// Decide which technique the display will use to change image
|
||||
// Done early, as rendering resets the Applets' requested types
|
||||
Drivers::EInk::UpdateTypes updateType = decideUpdateType();
|
||||
|
||||
// Render the new image
|
||||
clearBuffer();
|
||||
renderUserApplets();
|
||||
renderPlaceholders();
|
||||
renderSystemApplets();
|
||||
|
||||
// Invert Buffer if set by user
|
||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
|
||||
for (size_t i = 0; i < imageBufferWidth * imageBufferHeight; ++i) {
|
||||
imageBuffer[i] = ~imageBuffer[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if a system applet has requested exclusive rights to request an update,
|
||||
// or exclusive rights to render
|
||||
checkLocks();
|
||||
// Tell display to begin process of drawing new image
|
||||
LOG_INFO("Updating display");
|
||||
driver->update(imageBuffer, updateType);
|
||||
|
||||
// (Potentially) change applet to display new info,
|
||||
// then check if this newly displayed applet makes a pending notification redundant
|
||||
inkhud->autoshow();
|
||||
// If not async, wait here until the update is complete
|
||||
if (!async)
|
||||
driver->await();
|
||||
}
|
||||
|
||||
// If an update is justified.
|
||||
// We don't know this until after autoshow has run, as new applets may now be in foreground
|
||||
if (shouldUpdate()) {
|
||||
// Our part is done now.
|
||||
// If update is async, the display hardware is still performing the update process,
|
||||
// but that's all handled by NicheGraphics::Drivers::EInk
|
||||
|
||||
// Decide which technique the display will use to change image
|
||||
// Done early, as rendering resets the Applets' requested types
|
||||
Drivers::EInk::UpdateTypes updateType = decideUpdateType();
|
||||
|
||||
// Render the new image
|
||||
clearBuffer();
|
||||
renderUserApplets();
|
||||
renderPlaceholders();
|
||||
renderSystemApplets();
|
||||
|
||||
// Invert Buffer if set by user
|
||||
if (config.display.displaymode == meshtastic_Config_DisplayConfig_DisplayMode_INVERTED) {
|
||||
for (size_t i = 0; i < imageBufferWidth * imageBufferHeight; ++i) {
|
||||
imageBuffer[i] = ~imageBuffer[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Tell display to begin process of drawing new image
|
||||
LOG_INFO("Updating display");
|
||||
driver->update(imageBuffer, updateType);
|
||||
|
||||
// If not async, wait here until the update is complete
|
||||
if (!async)
|
||||
driver->await();
|
||||
}
|
||||
|
||||
// Our part is done now.
|
||||
// If update is async, the display hardware is still performing the update process,
|
||||
// but that's all handled by NicheGraphics::Drivers::EInk
|
||||
|
||||
// Tidy up, ready for a new request
|
||||
requested = false;
|
||||
forced = false;
|
||||
// Tidy up, ready for a new request
|
||||
requested = false;
|
||||
forced = false;
|
||||
}
|
||||
|
||||
// Manually fill the image buffer with WHITE
|
||||
@@ -254,166 +240,157 @@ void InkHUD::Renderer::render(bool async)
|
||||
// Note: benchmarking revealed that this is *much* faster than setting pixels individually
|
||||
// So much so that it's more efficient to re-render all applets,
|
||||
// rather than rendering selectively, and manually blanking a portion of the display
|
||||
void InkHUD::Renderer::clearBuffer()
|
||||
{
|
||||
memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth);
|
||||
void InkHUD::Renderer::clearBuffer() { memset(imageBuffer, 0xFF, imageBufferHeight * imageBufferWidth); }
|
||||
|
||||
void InkHUD::Renderer::checkLocks() {
|
||||
lockRendering = nullptr;
|
||||
lockRequests = nullptr;
|
||||
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (!lockRendering && sa->lockRendering && sa->isForeground()) {
|
||||
lockRendering = sa;
|
||||
}
|
||||
if (!lockRequests && sa->lockRequests && sa->isForeground()) {
|
||||
lockRequests = sa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InkHUD::Renderer::checkLocks()
|
||||
{
|
||||
lockRendering = nullptr;
|
||||
lockRequests = nullptr;
|
||||
bool InkHUD::Renderer::shouldUpdate() {
|
||||
bool should = false;
|
||||
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (!lockRendering && sa->lockRendering && sa->isForeground()) {
|
||||
lockRendering = sa;
|
||||
}
|
||||
if (!lockRequests && sa->lockRequests && sa->isForeground()) {
|
||||
lockRequests = sa;
|
||||
}
|
||||
// via forceUpdate
|
||||
should |= forced;
|
||||
|
||||
// via a system applet (which has locked update requests)
|
||||
if (lockRequests) {
|
||||
should |= lockRequests->wantsToRender();
|
||||
return should; // Early exit - no other requests considered
|
||||
}
|
||||
|
||||
// via system applet (not locked)
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->wantsToRender() // This applet requested
|
||||
&& sa->isForeground()) // This applet is currently shown
|
||||
{
|
||||
should = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool InkHUD::Renderer::shouldUpdate()
|
||||
{
|
||||
bool should = false;
|
||||
|
||||
// via forceUpdate
|
||||
should |= forced;
|
||||
|
||||
// via a system applet (which has locked update requests)
|
||||
if (lockRequests) {
|
||||
should |= lockRequests->wantsToRender();
|
||||
return should; // Early exit - no other requests considered
|
||||
// via user applet
|
||||
for (Applet *ua : inkhud->userApplets) {
|
||||
if (ua // Tile has valid applet
|
||||
&& ua->wantsToRender() // This applet requested display update
|
||||
&& ua->isForeground()) // This applet is currently shown
|
||||
{
|
||||
should = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// via system applet (not locked)
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa->wantsToRender() // This applet requested
|
||||
&& sa->isForeground()) // This applet is currently shown
|
||||
{
|
||||
should = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// via user applet
|
||||
for (Applet *ua : inkhud->userApplets) {
|
||||
if (ua // Tile has valid applet
|
||||
&& ua->wantsToRender() // This applet requested display update
|
||||
&& ua->isForeground()) // This applet is currently shown
|
||||
{
|
||||
should = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return should;
|
||||
return should;
|
||||
}
|
||||
|
||||
// Determine which type of E-Ink update the display will perform, to change the image.
|
||||
// Considers the needs of the various applets, then weighs against display health.
|
||||
// An update type specified by forceUpdate will be granted with no further questioning.
|
||||
Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType()
|
||||
{
|
||||
// Ask applets which update type they would prefer
|
||||
// Some update types take priority over others
|
||||
Drivers::EInk::UpdateTypes InkHUD::Renderer::decideUpdateType() {
|
||||
// Ask applets which update type they would prefer
|
||||
// Some update types take priority over others
|
||||
|
||||
// No need to consider the "requests" if somebody already forced an update
|
||||
if (!forced) {
|
||||
// User applets
|
||||
for (Applet *ua : inkhud->userApplets) {
|
||||
if (ua && ua->isForeground())
|
||||
displayHealth.requestUpdateType(ua->wantsUpdateType());
|
||||
}
|
||||
// System Applets
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa && sa->isForeground())
|
||||
displayHealth.requestUpdateType(sa->wantsUpdateType());
|
||||
}
|
||||
// No need to consider the "requests" if somebody already forced an update
|
||||
if (!forced) {
|
||||
// User applets
|
||||
for (Applet *ua : inkhud->userApplets) {
|
||||
if (ua && ua->isForeground())
|
||||
displayHealth.requestUpdateType(ua->wantsUpdateType());
|
||||
}
|
||||
// System Applets
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
if (sa && sa->isForeground())
|
||||
displayHealth.requestUpdateType(sa->wantsUpdateType());
|
||||
}
|
||||
}
|
||||
|
||||
return displayHealth.decideUpdateType();
|
||||
return displayHealth.decideUpdateType();
|
||||
}
|
||||
|
||||
// Run the drawing operations of any user applets which are currently displayed
|
||||
// Pixel output is placed into the framebuffer, ready for handoff to the EInk driver
|
||||
void InkHUD::Renderer::renderUserApplets()
|
||||
{
|
||||
// Don't render user applets if a system applet has demanded the whole display to itself
|
||||
if (lockRendering)
|
||||
return;
|
||||
void InkHUD::Renderer::renderUserApplets() {
|
||||
// Don't render user applets if a system applet has demanded the whole display to itself
|
||||
if (lockRendering)
|
||||
return;
|
||||
|
||||
// Render any user applets which are currently visible
|
||||
for (Applet *ua : inkhud->userApplets) {
|
||||
if (ua && ua->isActive() && ua->isForeground()) {
|
||||
uint32_t start = millis();
|
||||
ua->render(); // Draw!
|
||||
uint32_t stop = millis();
|
||||
LOG_DEBUG("%s took %dms to render", ua->name, stop - start);
|
||||
}
|
||||
// Render any user applets which are currently visible
|
||||
for (Applet *ua : inkhud->userApplets) {
|
||||
if (ua && ua->isActive() && ua->isForeground()) {
|
||||
uint32_t start = millis();
|
||||
ua->render(); // Draw!
|
||||
uint32_t stop = millis();
|
||||
LOG_DEBUG("%s took %dms to render", ua->name, stop - start);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the drawing operations of any system applets which are currently displayed
|
||||
// Pixel output is placed into the framebuffer, ready for handoff to the EInk driver
|
||||
void InkHUD::Renderer::renderSystemApplets()
|
||||
{
|
||||
SystemApplet *battery = inkhud->getSystemApplet("BatteryIcon");
|
||||
SystemApplet *menu = inkhud->getSystemApplet("Menu");
|
||||
SystemApplet *notifications = inkhud->getSystemApplet("Notification");
|
||||
void InkHUD::Renderer::renderSystemApplets() {
|
||||
SystemApplet *battery = inkhud->getSystemApplet("BatteryIcon");
|
||||
SystemApplet *menu = inkhud->getSystemApplet("Menu");
|
||||
SystemApplet *notifications = inkhud->getSystemApplet("Notification");
|
||||
|
||||
// Each system applet
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
// Each system applet
|
||||
for (SystemApplet *sa : inkhud->systemApplets) {
|
||||
|
||||
// Skip if not shown
|
||||
if (!sa->isForeground())
|
||||
continue;
|
||||
// Skip if not shown
|
||||
if (!sa->isForeground())
|
||||
continue;
|
||||
|
||||
// Skip if locked by another applet
|
||||
if (lockRendering && lockRendering != sa)
|
||||
continue;
|
||||
// Skip if locked by another applet
|
||||
if (lockRendering && lockRendering != sa)
|
||||
continue;
|
||||
|
||||
// Don't draw the battery or notifications overtop the menu
|
||||
// Todo: smarter way to handle this
|
||||
if (menu->isForeground() && (sa == battery || sa == notifications))
|
||||
continue;
|
||||
// Don't draw the battery or notifications overtop the menu
|
||||
// Todo: smarter way to handle this
|
||||
if (menu->isForeground() && (sa == battery || sa == notifications))
|
||||
continue;
|
||||
|
||||
assert(sa->getTile());
|
||||
assert(sa->getTile());
|
||||
|
||||
// uint32_t start = millis();
|
||||
sa->render(); // Draw!
|
||||
// uint32_t stop = millis();
|
||||
// LOG_DEBUG("%s took %dms to render", sa->name, stop - start);
|
||||
}
|
||||
// uint32_t start = millis();
|
||||
sa->render(); // Draw!
|
||||
// uint32_t stop = millis();
|
||||
// LOG_DEBUG("%s took %dms to render", sa->name, stop - start);
|
||||
}
|
||||
}
|
||||
|
||||
// In some situations (e.g. layout or applet selection changes),
|
||||
// a user tile can end up without an assigned applet.
|
||||
// In this case, we will fill the empty space with diagonal lines.
|
||||
void InkHUD::Renderer::renderPlaceholders()
|
||||
{
|
||||
// Don't fill empty space with placeholders if a system applet wants exclusive use of the display
|
||||
if (lockRendering)
|
||||
return;
|
||||
void InkHUD::Renderer::renderPlaceholders() {
|
||||
// Don't fill empty space with placeholders if a system applet wants exclusive use of the display
|
||||
if (lockRendering)
|
||||
return;
|
||||
|
||||
// Ask the window manager which tiles are empty
|
||||
std::vector<Tile *> emptyTiles = inkhud->getEmptyTiles();
|
||||
// Ask the window manager which tiles are empty
|
||||
std::vector<Tile *> emptyTiles = inkhud->getEmptyTiles();
|
||||
|
||||
// No empty tiles
|
||||
if (emptyTiles.size() == 0)
|
||||
return;
|
||||
// No empty tiles
|
||||
if (emptyTiles.size() == 0)
|
||||
return;
|
||||
|
||||
SystemApplet *placeholder = inkhud->getSystemApplet("Placeholder");
|
||||
SystemApplet *placeholder = inkhud->getSystemApplet("Placeholder");
|
||||
|
||||
// uint32_t start = millis();
|
||||
for (Tile *t : emptyTiles) {
|
||||
t->assignApplet(placeholder);
|
||||
placeholder->render();
|
||||
t->assignApplet(nullptr);
|
||||
}
|
||||
// uint32_t stop = millis();
|
||||
// LOG_DEBUG("Placeholders took %dms to render", stop - start);
|
||||
// uint32_t start = millis();
|
||||
for (Tile *t : emptyTiles) {
|
||||
t->assignApplet(placeholder);
|
||||
placeholder->render();
|
||||
t->assignApplet(nullptr);
|
||||
}
|
||||
// uint32_t stop = millis();
|
||||
// LOG_DEBUG("Placeholders took %dms to render", stop - start);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -19,76 +19,74 @@ Orchestrates updating of the display image
|
||||
#include "./Persistence.h"
|
||||
#include "graphics/niche/Drivers/EInk/EInk.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class Renderer : protected concurrency::OSThread
|
||||
{
|
||||
class Renderer : protected concurrency::OSThread {
|
||||
|
||||
public:
|
||||
Renderer();
|
||||
public:
|
||||
Renderer();
|
||||
|
||||
// Configuration, before begin
|
||||
// Configuration, before begin
|
||||
|
||||
void setDriver(Drivers::EInk *driver);
|
||||
void setDisplayResilience(uint8_t fastPerFull, float stressMultiplier);
|
||||
void setDriver(Drivers::EInk *driver);
|
||||
void setDisplayResilience(uint8_t fastPerFull, float stressMultiplier);
|
||||
|
||||
void begin();
|
||||
void begin();
|
||||
|
||||
// Call these to make the image change
|
||||
// Call these to make the image change
|
||||
|
||||
void requestUpdate(); // Update display, if a foreground applet has info it wants to show
|
||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED,
|
||||
bool async = true); // Update display, regardless of whether any applets requested this
|
||||
void requestUpdate(); // Update display, if a foreground applet has info it wants to show
|
||||
void forceUpdate(Drivers::EInk::UpdateTypes type = Drivers::EInk::UpdateTypes::UNSPECIFIED,
|
||||
bool async = true); // Update display, regardless of whether any applets requested this
|
||||
|
||||
// Wait for an update to complete
|
||||
void awaitUpdate();
|
||||
// Wait for an update to complete
|
||||
void awaitUpdate();
|
||||
|
||||
// Receives pixel output from an applet (via a tile, which translates the coordinates)
|
||||
void handlePixel(int16_t x, int16_t y, Color c);
|
||||
// Receives pixel output from an applet (via a tile, which translates the coordinates)
|
||||
void handlePixel(int16_t x, int16_t y, Color c);
|
||||
|
||||
// Size of display, in context of current rotation
|
||||
// Size of display, in context of current rotation
|
||||
|
||||
uint16_t width();
|
||||
uint16_t height();
|
||||
uint16_t width();
|
||||
uint16_t height();
|
||||
|
||||
private:
|
||||
// Make attemps to render / update, once triggered by requestUpdate or forceUpdate
|
||||
int32_t runOnce() override;
|
||||
private:
|
||||
// Make attemps to render / update, once triggered by requestUpdate or forceUpdate
|
||||
int32_t runOnce() override;
|
||||
|
||||
// Apply the display rotation to handled pixels
|
||||
void rotatePixelCoords(int16_t *x, int16_t *y);
|
||||
// Apply the display rotation to handled pixels
|
||||
void rotatePixelCoords(int16_t *x, int16_t *y);
|
||||
|
||||
// Execute the render process now, then hand off to driver for display update
|
||||
void render(bool async = true);
|
||||
// Execute the render process now, then hand off to driver for display update
|
||||
void render(bool async = true);
|
||||
|
||||
// Steps of the rendering process
|
||||
// Steps of the rendering process
|
||||
|
||||
void clearBuffer();
|
||||
void checkLocks();
|
||||
bool shouldUpdate();
|
||||
Drivers::EInk::UpdateTypes decideUpdateType();
|
||||
void renderUserApplets();
|
||||
void renderSystemApplets();
|
||||
void renderPlaceholders();
|
||||
void clearBuffer();
|
||||
void checkLocks();
|
||||
bool shouldUpdate();
|
||||
Drivers::EInk::UpdateTypes decideUpdateType();
|
||||
void renderUserApplets();
|
||||
void renderSystemApplets();
|
||||
void renderPlaceholders();
|
||||
|
||||
Drivers::EInk *driver = nullptr; // Interacts with your variants display hardware
|
||||
DisplayHealth displayHealth; // Manages display health by controlling type of update
|
||||
Drivers::EInk *driver = nullptr; // Interacts with your variants display hardware
|
||||
DisplayHealth displayHealth; // Manages display health by controlling type of update
|
||||
|
||||
uint8_t *imageBuffer = nullptr; // Fed into driver
|
||||
uint16_t imageBufferHeight = 0;
|
||||
uint16_t imageBufferWidth = 0;
|
||||
uint32_t imageBufferSize = 0; // Bytes
|
||||
uint8_t *imageBuffer = nullptr; // Fed into driver
|
||||
uint16_t imageBufferHeight = 0;
|
||||
uint16_t imageBufferWidth = 0;
|
||||
uint32_t imageBufferSize = 0; // Bytes
|
||||
|
||||
SystemApplet *lockRendering = nullptr; // Render this applet *only*
|
||||
SystemApplet *lockRequests = nullptr; // Honor update requests from this applet *only*
|
||||
SystemApplet *lockRendering = nullptr; // Render this applet *only*
|
||||
SystemApplet *lockRequests = nullptr; // Honor update requests from this applet *only*
|
||||
|
||||
bool requested = false;
|
||||
bool forced = false;
|
||||
bool requested = false;
|
||||
bool forced = false;
|
||||
|
||||
// For convenience
|
||||
InkHUD *inkhud = nullptr;
|
||||
Persistence::Settings *settings = nullptr;
|
||||
// For convenience
|
||||
InkHUD *inkhud = nullptr;
|
||||
Persistence::Settings *settings = nullptr;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -14,28 +14,27 @@ For features like the menu, and the battery icon.
|
||||
|
||||
#include "./Applet.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class SystemApplet : public Applet
|
||||
{
|
||||
public:
|
||||
// System applets have the right to:
|
||||
class SystemApplet : public Applet {
|
||||
public:
|
||||
// System applets have the right to:
|
||||
|
||||
bool handleInput = false; // - respond to input from the user button
|
||||
bool lockRendering = false; // - prevent other applets from being rendered during an update
|
||||
bool lockRequests = false; // - prevent other applets from triggering display updates
|
||||
bool handleInput = false; // - respond to input from the user button
|
||||
bool lockRendering = false; // - prevent other applets from being rendered during an update
|
||||
bool lockRequests = false; // - prevent other applets from triggering display updates
|
||||
|
||||
virtual void onReboot() { onShutdown(); } // - handle reboot specially
|
||||
virtual void onReboot() { onShutdown(); } // - handle reboot specially
|
||||
|
||||
// Other system applets may take precedence over our own system applet though
|
||||
// The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher rank)
|
||||
// Other system applets may take precedence over our own system applet though
|
||||
// The order an applet is passed to WindowManager::addSystemApplet determines this hierarchy (added earlier = higher
|
||||
// rank)
|
||||
|
||||
private:
|
||||
// System applets are always running (active), but may not be visible (foreground)
|
||||
private:
|
||||
// System applets are always running (active), but may not be visible (foreground)
|
||||
|
||||
void onActivate() override {}
|
||||
void onDeactivate() override {}
|
||||
void onActivate() override {}
|
||||
void onDeactivate() override {}
|
||||
};
|
||||
|
||||
}; // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -13,138 +13,132 @@ bool InkHUD::Tile::highlightShown;
|
||||
// For dismissing the highlight indicator, after a few seconds
|
||||
// Highlighting is used to inform user of which tile is now focused
|
||||
static concurrency::Periodic *taskHighlight;
|
||||
static int32_t runtaskHighlight()
|
||||
{
|
||||
LOG_DEBUG("Dismissing Highlight");
|
||||
InkHUD::Tile::highlightShown = false;
|
||||
InkHUD::Tile::highlightTarget = nullptr;
|
||||
InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting
|
||||
return taskHighlight->disable();
|
||||
static int32_t runtaskHighlight() {
|
||||
LOG_DEBUG("Dismissing Highlight");
|
||||
InkHUD::Tile::highlightShown = false;
|
||||
InkHUD::Tile::highlightTarget = nullptr;
|
||||
InkHUD::InkHUD::getInstance()->forceUpdate(Drivers::EInk::UpdateTypes::FAST); // Re-render, clearing the highlighting
|
||||
return taskHighlight->disable();
|
||||
}
|
||||
static void inittaskHighlight()
|
||||
{
|
||||
static bool doneOnce = false;
|
||||
if (!doneOnce) {
|
||||
taskHighlight = new concurrency::Periodic("Highlight", runtaskHighlight);
|
||||
taskHighlight->disable();
|
||||
doneOnce = true;
|
||||
}
|
||||
static void inittaskHighlight() {
|
||||
static bool doneOnce = false;
|
||||
if (!doneOnce) {
|
||||
taskHighlight = new concurrency::Periodic("Highlight", runtaskHighlight);
|
||||
taskHighlight->disable();
|
||||
doneOnce = true;
|
||||
}
|
||||
}
|
||||
|
||||
InkHUD::Tile::Tile()
|
||||
{
|
||||
inkhud = InkHUD::getInstance();
|
||||
InkHUD::Tile::Tile() {
|
||||
inkhud = InkHUD::getInstance();
|
||||
|
||||
inittaskHighlight();
|
||||
Tile::highlightTarget = nullptr;
|
||||
Tile::highlightShown = false;
|
||||
inittaskHighlight();
|
||||
Tile::highlightTarget = nullptr;
|
||||
Tile::highlightShown = false;
|
||||
}
|
||||
|
||||
InkHUD::Tile::Tile(int16_t left, int16_t top, uint16_t width, uint16_t height)
|
||||
{
|
||||
assert(width > 0 && height > 0);
|
||||
InkHUD::Tile::Tile(int16_t left, int16_t top, uint16_t width, uint16_t height) {
|
||||
assert(width > 0 && height > 0);
|
||||
|
||||
this->left = left;
|
||||
this->top = top;
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
this->left = left;
|
||||
this->top = top;
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
}
|
||||
|
||||
// Set the region of the tile automatically, based on the user's chosen layout
|
||||
// This method places tiles which will host user applets
|
||||
// The WindowManager multiplexes the applets to these tiles automatically
|
||||
void InkHUD::Tile::setRegion(uint8_t userTileCount, uint8_t tileIndex)
|
||||
{
|
||||
uint16_t displayWidth = inkhud->width();
|
||||
uint16_t displayHeight = inkhud->height();
|
||||
void InkHUD::Tile::setRegion(uint8_t userTileCount, uint8_t tileIndex) {
|
||||
uint16_t displayWidth = inkhud->width();
|
||||
uint16_t displayHeight = inkhud->height();
|
||||
|
||||
bool landscape = displayWidth > displayHeight;
|
||||
bool landscape = displayWidth > displayHeight;
|
||||
|
||||
// Check for any stray tiles
|
||||
if (tileIndex > (userTileCount - 1)) {
|
||||
// Dummy values to prevent rendering
|
||||
LOG_WARN("Tile index out of bounds");
|
||||
left = -2;
|
||||
top = -2;
|
||||
width = 1;
|
||||
height = 1;
|
||||
return;
|
||||
// Check for any stray tiles
|
||||
if (tileIndex > (userTileCount - 1)) {
|
||||
// Dummy values to prevent rendering
|
||||
LOG_WARN("Tile index out of bounds");
|
||||
left = -2;
|
||||
top = -2;
|
||||
width = 1;
|
||||
height = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Todo: special handling for 3 tile layout
|
||||
|
||||
// Gutters between tiles
|
||||
const uint16_t spacing = 4;
|
||||
|
||||
switch (userTileCount) {
|
||||
// One tile only
|
||||
case 1:
|
||||
left = 0;
|
||||
top = 0;
|
||||
width = displayWidth;
|
||||
height = displayHeight;
|
||||
break;
|
||||
|
||||
// Two tiles
|
||||
case 2:
|
||||
if (landscape) {
|
||||
// Side by side
|
||||
left = ((displayWidth / 2) + (spacing / 2)) * tileIndex;
|
||||
top = 0;
|
||||
width = (displayWidth / 2) - (spacing / 2);
|
||||
height = displayHeight;
|
||||
} else {
|
||||
// Above and below
|
||||
left = 0;
|
||||
top = 0 + (((displayHeight / 2) + (spacing / 2)) * tileIndex);
|
||||
width = displayWidth;
|
||||
height = (displayHeight / 2) - (spacing / 2);
|
||||
}
|
||||
break;
|
||||
|
||||
// Todo: special handling for 3 tile layout
|
||||
|
||||
// Gutters between tiles
|
||||
const uint16_t spacing = 4;
|
||||
|
||||
switch (userTileCount) {
|
||||
// One tile only
|
||||
// Four tiles
|
||||
case 4:
|
||||
width = (displayWidth / 2) - (spacing / 2);
|
||||
height = (displayHeight / 2) - (spacing / 2);
|
||||
switch (tileIndex) {
|
||||
case 0:
|
||||
left = 0;
|
||||
top = 0;
|
||||
break;
|
||||
case 1:
|
||||
left = 0;
|
||||
top = 0;
|
||||
width = displayWidth;
|
||||
height = displayHeight;
|
||||
break;
|
||||
|
||||
// Two tiles
|
||||
left = 0 + (width - 1) + spacing;
|
||||
top = 0;
|
||||
break;
|
||||
case 2:
|
||||
if (landscape) {
|
||||
// Side by side
|
||||
left = ((displayWidth / 2) + (spacing / 2)) * tileIndex;
|
||||
top = 0;
|
||||
width = (displayWidth / 2) - (spacing / 2);
|
||||
height = displayHeight;
|
||||
} else {
|
||||
// Above and below
|
||||
left = 0;
|
||||
top = 0 + (((displayHeight / 2) + (spacing / 2)) * tileIndex);
|
||||
width = displayWidth;
|
||||
height = (displayHeight / 2) - (spacing / 2);
|
||||
}
|
||||
break;
|
||||
|
||||
// Four tiles
|
||||
case 4:
|
||||
width = (displayWidth / 2) - (spacing / 2);
|
||||
height = (displayHeight / 2) - (spacing / 2);
|
||||
switch (tileIndex) {
|
||||
case 0:
|
||||
left = 0;
|
||||
top = 0;
|
||||
break;
|
||||
case 1:
|
||||
left = 0 + (width - 1) + spacing;
|
||||
top = 0;
|
||||
break;
|
||||
case 2:
|
||||
left = 0;
|
||||
top = 0 + (height - 1) + spacing;
|
||||
break;
|
||||
case 3:
|
||||
left = 0 + (width - 1) + spacing;
|
||||
top = 0 + (height - 1) + spacing;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
LOG_ERROR("Unsupported tile layout");
|
||||
assert(0);
|
||||
left = 0;
|
||||
top = 0 + (height - 1) + spacing;
|
||||
break;
|
||||
case 3:
|
||||
left = 0 + (width - 1) + spacing;
|
||||
top = 0 + (height - 1) + spacing;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
assert(width > 0 && height > 0);
|
||||
default:
|
||||
LOG_ERROR("Unsupported tile layout");
|
||||
assert(0);
|
||||
}
|
||||
|
||||
assert(width > 0 && height > 0);
|
||||
}
|
||||
|
||||
// Manually set the region for a tile
|
||||
// This is only done for tiles which will host certain "System Applets", which have unique position / sizes:
|
||||
// Things like the NotificationApplet, BatteryIconApplet, etc
|
||||
void InkHUD::Tile::setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height)
|
||||
{
|
||||
assert(width > 0 && height > 0);
|
||||
void InkHUD::Tile::setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height) {
|
||||
assert(width > 0 && height > 0);
|
||||
|
||||
this->left = left;
|
||||
this->top = top;
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
this->left = left;
|
||||
this->top = top;
|
||||
this->width = width;
|
||||
this->height = height;
|
||||
}
|
||||
|
||||
// Place an applet onto a tile
|
||||
@@ -154,88 +148,73 @@ void InkHUD::Tile::setRegion(int16_t left, int16_t top, uint16_t width, uint16_t
|
||||
// This is enforced with asserts
|
||||
// Assigning a new applet will break a previous link
|
||||
// Link may also be broken by assigning a nullptr
|
||||
void InkHUD::Tile::assignApplet(Applet *a)
|
||||
{
|
||||
// Break the link between old applet and this tile
|
||||
if (assignedApplet)
|
||||
assignedApplet->setTile(nullptr);
|
||||
void InkHUD::Tile::assignApplet(Applet *a) {
|
||||
// Break the link between old applet and this tile
|
||||
if (assignedApplet)
|
||||
assignedApplet->setTile(nullptr);
|
||||
|
||||
// Store the new applet
|
||||
assignedApplet = a;
|
||||
// Store the new applet
|
||||
assignedApplet = a;
|
||||
|
||||
// Create the reciprocal link between the new applet and this tile
|
||||
if (a)
|
||||
a->setTile(this);
|
||||
// Create the reciprocal link between the new applet and this tile
|
||||
if (a)
|
||||
a->setTile(this);
|
||||
}
|
||||
|
||||
// Get pointer to whichever applet is displayed on this tile
|
||||
InkHUD::Applet *InkHUD::Tile::getAssignedApplet()
|
||||
{
|
||||
return assignedApplet;
|
||||
}
|
||||
InkHUD::Applet *InkHUD::Tile::getAssignedApplet() { return assignedApplet; }
|
||||
|
||||
// Receive drawing output from the assigned applet,
|
||||
// and translate it from "applet-space" coordinates, to it's true location.
|
||||
// The final "rotation" step is performed by the windowManager
|
||||
void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c)
|
||||
{
|
||||
// Move pixels from applet-space to tile-space
|
||||
x += left;
|
||||
y += top;
|
||||
void InkHUD::Tile::handleAppletPixel(int16_t x, int16_t y, Color c) {
|
||||
// Move pixels from applet-space to tile-space
|
||||
x += left;
|
||||
y += top;
|
||||
|
||||
// Crop to tile borders
|
||||
if (x >= left && x < (left + width) && y >= top && y < (top + height)) {
|
||||
// Pass to the renderer
|
||||
inkhud->drawPixel(x, y, c);
|
||||
}
|
||||
// Crop to tile borders
|
||||
if (x >= left && x < (left + width) && y >= top && y < (top + height)) {
|
||||
// Pass to the renderer
|
||||
inkhud->drawPixel(x, y, c);
|
||||
}
|
||||
}
|
||||
|
||||
// Called by Applet base class, when setting applet dimensions, immediately before render
|
||||
uint16_t InkHUD::Tile::getWidth()
|
||||
{
|
||||
return width;
|
||||
}
|
||||
uint16_t InkHUD::Tile::getWidth() { return width; }
|
||||
|
||||
// Called by Applet base class, when setting applet dimensions, immediately before render
|
||||
uint16_t InkHUD::Tile::getHeight()
|
||||
{
|
||||
return height;
|
||||
}
|
||||
uint16_t InkHUD::Tile::getHeight() { return height; }
|
||||
|
||||
// Longest edge of the display, in pixels
|
||||
// A 296px x 250px display will return 296, for example
|
||||
// Maximum possible size of any tile's width / height
|
||||
// Used by some components to allocate resources for the "worst possible situation"
|
||||
// "Sizing the cathedral for christmas eve"
|
||||
uint16_t InkHUD::Tile::maxDisplayDimension()
|
||||
{
|
||||
InkHUD *inkhud = InkHUD::getInstance();
|
||||
return max(inkhud->height(), inkhud->width());
|
||||
uint16_t InkHUD::Tile::maxDisplayDimension() {
|
||||
InkHUD *inkhud = InkHUD::getInstance();
|
||||
return max(inkhud->height(), inkhud->width());
|
||||
}
|
||||
|
||||
// Ask for this tile to be highlighted
|
||||
// Used to indicate which tile is now indicated after focus changes
|
||||
// Only used for aux button focus changes, not changes via menu
|
||||
void InkHUD::Tile::requestHighlight()
|
||||
{
|
||||
Tile::highlightTarget = this;
|
||||
Tile::highlightShown = false;
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
void InkHUD::Tile::requestHighlight() {
|
||||
Tile::highlightTarget = this;
|
||||
Tile::highlightShown = false;
|
||||
inkhud->forceUpdate(Drivers::EInk::UpdateTypes::FAST);
|
||||
}
|
||||
|
||||
// Starts the timer which will automatically dismiss the highlighting, if the tile doesn't organically redraw first
|
||||
void InkHUD::Tile::startHighlightTimeout()
|
||||
{
|
||||
taskHighlight->setIntervalFromNow(5 * 1000UL);
|
||||
taskHighlight->enabled = true;
|
||||
void InkHUD::Tile::startHighlightTimeout() {
|
||||
taskHighlight->setIntervalFromNow(5 * 1000UL);
|
||||
taskHighlight->enabled = true;
|
||||
}
|
||||
|
||||
// Stop the timer which would automatically dismiss the highlighting
|
||||
// Called if the tile organically renders before the timer is up
|
||||
void InkHUD::Tile::cancelHighlightTimeout()
|
||||
{
|
||||
if (taskHighlight->enabled)
|
||||
taskHighlight->disable();
|
||||
void InkHUD::Tile::cancelHighlightTimeout() {
|
||||
if (taskHighlight->enabled)
|
||||
taskHighlight->disable();
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -17,41 +17,39 @@
|
||||
|
||||
#include "./InkHUD.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class Tile
|
||||
{
|
||||
public:
|
||||
Tile();
|
||||
Tile(int16_t left, int16_t top, uint16_t width, uint16_t height);
|
||||
class Tile {
|
||||
public:
|
||||
Tile();
|
||||
Tile(int16_t left, int16_t top, uint16_t width, uint16_t height);
|
||||
|
||||
void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout
|
||||
void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually
|
||||
void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet
|
||||
uint16_t getWidth();
|
||||
uint16_t getHeight();
|
||||
static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter
|
||||
void setRegion(uint8_t layoutSize, uint8_t tileIndex); // Assign region automatically, based on layout
|
||||
void setRegion(int16_t left, int16_t top, uint16_t width, uint16_t height); // Assign region manually
|
||||
void handleAppletPixel(int16_t x, int16_t y, Color c); // Receive px output from assigned applet
|
||||
uint16_t getWidth();
|
||||
uint16_t getHeight();
|
||||
static uint16_t maxDisplayDimension(); // Largest possible width / height any tile may ever encounter
|
||||
|
||||
void assignApplet(Applet *a); // Link an applet with this tile
|
||||
Applet *getAssignedApplet(); // Applet which is currently linked with this tile
|
||||
void assignApplet(Applet *a); // Link an applet with this tile
|
||||
Applet *getAssignedApplet(); // Applet which is currently linked with this tile
|
||||
|
||||
void requestHighlight(); // Ask for this tile to be highlighted
|
||||
static void startHighlightTimeout(); // Start the auto-dismissal timer
|
||||
static void cancelHighlightTimeout(); // Cancel the auto-dismissal timer early; already dismissed
|
||||
void requestHighlight(); // Ask for this tile to be highlighted
|
||||
static void startHighlightTimeout(); // Start the auto-dismissal timer
|
||||
static void cancelHighlightTimeout(); // Cancel the auto-dismissal timer early; already dismissed
|
||||
|
||||
static Tile *highlightTarget; // Which tile are we highlighting? (Intending to highlight?)
|
||||
static bool highlightShown; // Is the tile highlighted yet? Controls highlight vs dismiss
|
||||
static Tile *highlightTarget; // Which tile are we highlighting? (Intending to highlight?)
|
||||
static bool highlightShown; // Is the tile highlighted yet? Controls highlight vs dismiss
|
||||
|
||||
private:
|
||||
InkHUD *inkhud = nullptr;
|
||||
private:
|
||||
InkHUD *inkhud = nullptr;
|
||||
|
||||
int16_t left = 0;
|
||||
int16_t top = 0;
|
||||
uint16_t width = 0;
|
||||
uint16_t height = 0;
|
||||
int16_t left = 0;
|
||||
int16_t top = 0;
|
||||
uint16_t width = 0;
|
||||
uint16_t height = 0;
|
||||
|
||||
Applet *assignedApplet = nullptr; // Pointer to the applet which is currently linked with the tile
|
||||
Applet *assignedApplet = nullptr; // Pointer to the applet which is currently linked with the tile
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,59 +15,57 @@ Responsible for managing which applets are shown, and their sizes / positions
|
||||
#include "./Persistence.h"
|
||||
#include "./Tile.h"
|
||||
|
||||
namespace NicheGraphics::InkHUD
|
||||
{
|
||||
namespace NicheGraphics::InkHUD {
|
||||
|
||||
class WindowManager
|
||||
{
|
||||
public:
|
||||
WindowManager();
|
||||
void addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile);
|
||||
void begin();
|
||||
class WindowManager {
|
||||
public:
|
||||
WindowManager();
|
||||
void addApplet(const char *name, Applet *a, bool defaultActive, bool defaultAutoshow, uint8_t onTile);
|
||||
void begin();
|
||||
|
||||
// - call these to make stuff change
|
||||
// - call these to make stuff change
|
||||
|
||||
void nextTile();
|
||||
void prevTile();
|
||||
void openMenu();
|
||||
void openAlignStick();
|
||||
void nextApplet();
|
||||
void prevApplet();
|
||||
void rotate();
|
||||
void toggleBatteryIcon();
|
||||
void nextTile();
|
||||
void prevTile();
|
||||
void openMenu();
|
||||
void openAlignStick();
|
||||
void nextApplet();
|
||||
void prevApplet();
|
||||
void rotate();
|
||||
void toggleBatteryIcon();
|
||||
|
||||
// - call these to manifest changes already made to the relevant Persistence::Settings values
|
||||
// - call these to manifest changes already made to the relevant Persistence::Settings values
|
||||
|
||||
void changeLayout(); // Change tile layout or count
|
||||
void changeActivatedApplets(); // Change which applets are activated
|
||||
void changeLayout(); // Change tile layout or count
|
||||
void changeActivatedApplets(); // Change which applets are activated
|
||||
|
||||
// - called during the rendering operation
|
||||
// - called during the rendering operation
|
||||
|
||||
void autoshow(); // Show a different applet, to display new info
|
||||
std::vector<Tile *> getEmptyTiles(); // Any user tiles without a valid applet
|
||||
void autoshow(); // Show a different applet, to display new info
|
||||
std::vector<Tile *> getEmptyTiles(); // Any user tiles without a valid applet
|
||||
|
||||
private:
|
||||
// Steps for configuring (or reconfiguring) the window manager
|
||||
// - all steps required at startup
|
||||
// - various combinations of steps required for on-the-fly reconfiguration (by user, via menu)
|
||||
private:
|
||||
// Steps for configuring (or reconfiguring) the window manager
|
||||
// - all steps required at startup
|
||||
// - various combinations of steps required for on-the-fly reconfiguration (by user, via menu)
|
||||
|
||||
void addSystemApplet(const char *name, SystemApplet *applet, Tile *tile);
|
||||
void createSystemApplets(); // Instantiate the system applets
|
||||
void placeSystemTiles(); // Assign manual positions to (most) system applets
|
||||
void addSystemApplet(const char *name, SystemApplet *applet, Tile *tile);
|
||||
void createSystemApplets(); // Instantiate the system applets
|
||||
void placeSystemTiles(); // Assign manual positions to (most) system applets
|
||||
|
||||
void createUserApplets(); // Activate user's selected applets
|
||||
void createUserTiles(); // Instantiate enough tiles for user's selected layout
|
||||
void assignUserAppletsToTiles();
|
||||
void placeUserTiles(); // Automatically place tiles, according to user's layout
|
||||
void refocusTile(); // Ensure focused tile has a valid applet
|
||||
void createUserApplets(); // Activate user's selected applets
|
||||
void createUserTiles(); // Instantiate enough tiles for user's selected layout
|
||||
void assignUserAppletsToTiles();
|
||||
void placeUserTiles(); // Automatically place tiles, according to user's layout
|
||||
void refocusTile(); // Ensure focused tile has a valid applet
|
||||
|
||||
void findOrphanApplets(); // Find any applets left-behind when layout changes
|
||||
void findOrphanApplets(); // Find any applets left-behind when layout changes
|
||||
|
||||
std::vector<Tile *> userTiles; // Tiles which can host user applets
|
||||
std::vector<Tile *> userTiles; // Tiles which can host user applets
|
||||
|
||||
// For convenience
|
||||
InkHUD *inkhud = nullptr;
|
||||
Persistence::Settings *settings = nullptr;
|
||||
// For convenience
|
||||
InkHUD *inkhud = nullptr;
|
||||
Persistence::Settings *settings = nullptr;
|
||||
};
|
||||
|
||||
} // namespace NicheGraphics::InkHUD
|
||||
|
||||
@@ -8,302 +8,284 @@
|
||||
|
||||
using namespace NicheGraphics::Inputs;
|
||||
|
||||
TwoButton::TwoButton() : concurrency::OSThread("TwoButton")
|
||||
{
|
||||
// Don't start polling buttons for release immediately
|
||||
// Assume they are in a "released" state at boot
|
||||
OSThread::disable();
|
||||
TwoButton::TwoButton() : concurrency::OSThread("TwoButton") {
|
||||
// Don't start polling buttons for release immediately
|
||||
// Assume they are in a "released" state at boot
|
||||
OSThread::disable();
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Register callbacks for before and after lightsleep
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
// Register callbacks for before and after lightsleep
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
#endif
|
||||
|
||||
// Explicitly initialize these, just to keep cppcheck quiet..
|
||||
buttons[0] = Button();
|
||||
buttons[1] = Button();
|
||||
// Explicitly initialize these, just to keep cppcheck quiet..
|
||||
buttons[0] = Button();
|
||||
buttons[1] = Button();
|
||||
}
|
||||
|
||||
// Get access to (or create) the singleton instance of this class
|
||||
// Accessible inside the ISRs, even though we maybe shouldn't
|
||||
TwoButton *TwoButton::getInstance()
|
||||
{
|
||||
// Instantiate the class the first time this method is called
|
||||
static TwoButton *const singletonInstance = new TwoButton;
|
||||
TwoButton *TwoButton::getInstance() {
|
||||
// Instantiate the class the first time this method is called
|
||||
static TwoButton *const singletonInstance = new TwoButton;
|
||||
|
||||
return singletonInstance;
|
||||
return singletonInstance;
|
||||
}
|
||||
|
||||
// Begin receiving button input
|
||||
// We probably need to do this after sleep, as well as at boot
|
||||
void TwoButton::start()
|
||||
{
|
||||
if (buttons[0].pin != 0xFF)
|
||||
attachInterrupt(buttons[0].pin, TwoButton::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING);
|
||||
void TwoButton::start() {
|
||||
if (buttons[0].pin != 0xFF)
|
||||
attachInterrupt(buttons[0].pin, TwoButton::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING);
|
||||
|
||||
if (buttons[1].pin != 0xFF)
|
||||
attachInterrupt(buttons[1].pin, TwoButton::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING);
|
||||
if (buttons[1].pin != 0xFF)
|
||||
attachInterrupt(buttons[1].pin, TwoButton::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING);
|
||||
}
|
||||
|
||||
// Stop receiving button input, and run custom sleep code
|
||||
// Called before device sleeps. This might be power-off, or just ESP32 light sleep
|
||||
// Some devices will want to attach interrupts here, for the user button to wake from sleep
|
||||
void TwoButton::stop()
|
||||
{
|
||||
if (buttons[0].pin != 0xFF)
|
||||
detachInterrupt(buttons[0].pin);
|
||||
void TwoButton::stop() {
|
||||
if (buttons[0].pin != 0xFF)
|
||||
detachInterrupt(buttons[0].pin);
|
||||
|
||||
if (buttons[1].pin != 0xFF)
|
||||
detachInterrupt(buttons[1].pin);
|
||||
if (buttons[1].pin != 0xFF)
|
||||
detachInterrupt(buttons[1].pin);
|
||||
}
|
||||
|
||||
// Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings
|
||||
// This helper method isn't used by the TweButton class itself, it could be moved elsewhere.
|
||||
// Intention is to pass this value to TwoButton::setWiring in the setupNicheGraphics method.
|
||||
uint8_t TwoButton::getUserButtonPin()
|
||||
{
|
||||
uint8_t pin = 0xFF; // Unset
|
||||
uint8_t TwoButton::getUserButtonPin() {
|
||||
uint8_t pin = 0xFF; // Unset
|
||||
|
||||
// Use default pin for variant, if no better source
|
||||
// Use default pin for variant, if no better source
|
||||
#ifdef BUTTON_PIN
|
||||
pin = BUTTON_PIN;
|
||||
pin = BUTTON_PIN;
|
||||
#endif
|
||||
|
||||
// From userPrefs.jsonc, if set
|
||||
// From userPrefs.jsonc, if set
|
||||
#ifdef USERPREFS_BUTTON_PIN
|
||||
pin = USERPREFS_BUTTON_PIN;
|
||||
pin = USERPREFS_BUTTON_PIN;
|
||||
#endif
|
||||
|
||||
// From user's override in device settings, if set
|
||||
if (config.device.button_gpio)
|
||||
pin = config.device.button_gpio;
|
||||
// From user's override in device settings, if set
|
||||
if (config.device.button_gpio)
|
||||
pin = config.device.button_gpio;
|
||||
|
||||
return pin;
|
||||
return pin;
|
||||
}
|
||||
|
||||
// Configures the wiring and logic of either button
|
||||
// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp
|
||||
void TwoButton::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup)
|
||||
{
|
||||
// Prevent the same GPIO being assigned to multiple buttons
|
||||
// Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button
|
||||
for (uint8_t i = 0; i < whichButton; i++) {
|
||||
if (buttons[i].pin == pin) {
|
||||
LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton);
|
||||
return;
|
||||
}
|
||||
void TwoButton::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) {
|
||||
// Prevent the same GPIO being assigned to multiple buttons
|
||||
// Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button
|
||||
for (uint8_t i = 0; i < whichButton; i++) {
|
||||
if (buttons[i].pin == pin) {
|
||||
LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].pin = pin;
|
||||
buttons[whichButton].activeLogic = LOW; // Unimplemented
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].pin = pin;
|
||||
buttons[whichButton].activeLogic = LOW; // Unimplemented
|
||||
|
||||
pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT);
|
||||
pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT);
|
||||
}
|
||||
|
||||
void TwoButton::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].debounceLength = debounceMs;
|
||||
buttons[whichButton].longpressLength = longpressMs;
|
||||
void TwoButton::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) {
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].debounceLength = debounceMs;
|
||||
buttons[whichButton].longpressLength = longpressMs;
|
||||
}
|
||||
|
||||
// Set what should happen when a button becomes pressed
|
||||
// Use this to implement a "while held" behavior
|
||||
void TwoButton::setHandlerDown(uint8_t whichButton, Callback onDown)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onDown = onDown;
|
||||
void TwoButton::setHandlerDown(uint8_t whichButton, Callback onDown) {
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onDown = onDown;
|
||||
}
|
||||
|
||||
// Set what should happen when a button becomes unpressed
|
||||
// Use this to implement a "While held" behavior
|
||||
void TwoButton::setHandlerUp(uint8_t whichButton, Callback onUp)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onUp = onUp;
|
||||
void TwoButton::setHandlerUp(uint8_t whichButton, Callback onUp) {
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onUp = onUp;
|
||||
}
|
||||
|
||||
// Set what should happen when a "short press" event has occurred
|
||||
void TwoButton::setHandlerShortPress(uint8_t whichButton, Callback onShortPress)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onShortPress = onShortPress;
|
||||
void TwoButton::setHandlerShortPress(uint8_t whichButton, Callback onShortPress) {
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onShortPress = onShortPress;
|
||||
}
|
||||
|
||||
// Set what should happen when a "long press" event has fired
|
||||
// Note: this will occur while the button is still held
|
||||
void TwoButton::setHandlerLongPress(uint8_t whichButton, Callback onLongPress)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onLongPress = onLongPress;
|
||||
void TwoButton::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) {
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onLongPress = onLongPress;
|
||||
}
|
||||
|
||||
// Handle the start of a press to the primary button
|
||||
// Wakes our button thread
|
||||
void TwoButton::isrPrimary()
|
||||
{
|
||||
static volatile bool isrRunning = false;
|
||||
void TwoButton::isrPrimary() {
|
||||
static volatile bool isrRunning = false;
|
||||
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButton *b = TwoButton::getInstance();
|
||||
if (b->buttons[0].state == State::REST) {
|
||||
b->buttons[0].state = State::IRQ;
|
||||
b->buttons[0].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButton *b = TwoButton::getInstance();
|
||||
if (b->buttons[0].state == State::REST) {
|
||||
b->buttons[0].state = State::IRQ;
|
||||
b->buttons[0].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the start of a press to the secondary button
|
||||
// Wakes our button thread
|
||||
void TwoButton::isrSecondary()
|
||||
{
|
||||
static volatile bool isrRunning = false;
|
||||
void TwoButton::isrSecondary() {
|
||||
static volatile bool isrRunning = false;
|
||||
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButton *b = TwoButton::getInstance();
|
||||
if (b->buttons[1].state == State::REST) {
|
||||
b->buttons[1].state = State::IRQ;
|
||||
b->buttons[1].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButton *b = TwoButton::getInstance();
|
||||
if (b->buttons[1].state == State::REST) {
|
||||
b->buttons[1].state = State::IRQ;
|
||||
b->buttons[1].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Concise method to start our button thread
|
||||
// Follows an ISR, listening for button release
|
||||
void TwoButton::startThread()
|
||||
{
|
||||
if (!OSThread::enabled) {
|
||||
OSThread::setInterval(10);
|
||||
OSThread::enabled = true;
|
||||
}
|
||||
void TwoButton::startThread() {
|
||||
if (!OSThread::enabled) {
|
||||
OSThread::setInterval(10);
|
||||
OSThread::enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Concise method to stop our button thread
|
||||
// Called when we no longer need to poll for button release
|
||||
void TwoButton::stopThread()
|
||||
{
|
||||
if (OSThread::enabled) {
|
||||
OSThread::disable();
|
||||
}
|
||||
void TwoButton::stopThread() {
|
||||
if (OSThread::enabled) {
|
||||
OSThread::disable();
|
||||
}
|
||||
|
||||
// Reset both buttons manually
|
||||
// Just in case an IRQ fires during the process of resetting the system
|
||||
// Can occur with super rapid presses?
|
||||
buttons[0].state = REST;
|
||||
buttons[1].state = REST;
|
||||
// Reset both buttons manually
|
||||
// Just in case an IRQ fires during the process of resetting the system
|
||||
// Can occur with super rapid presses?
|
||||
buttons[0].state = REST;
|
||||
buttons[1].state = REST;
|
||||
}
|
||||
|
||||
// Our button thread
|
||||
// Started by an IRQ, on either button
|
||||
// Polls for button releases
|
||||
// Stops when both buttons released
|
||||
int32_t TwoButton::runOnce()
|
||||
{
|
||||
constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button);
|
||||
int32_t TwoButton::runOnce() {
|
||||
constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button);
|
||||
|
||||
// Allow either button to request that our thread should continue polling
|
||||
bool awaitingRelease = false;
|
||||
// Allow either button to request that our thread should continue polling
|
||||
bool awaitingRelease = false;
|
||||
|
||||
// Check both primary and secondary buttons
|
||||
for (uint8_t i = 0; i < BUTTON_COUNT; i++) {
|
||||
switch (buttons[i].state) {
|
||||
// No action: button has not been pressed
|
||||
case REST:
|
||||
break;
|
||||
// Check both primary and secondary buttons
|
||||
for (uint8_t i = 0; i < BUTTON_COUNT; i++) {
|
||||
switch (buttons[i].state) {
|
||||
// No action: button has not been pressed
|
||||
case REST:
|
||||
break;
|
||||
|
||||
// New press detected by interrupt
|
||||
case IRQ:
|
||||
powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer)
|
||||
buttons[i].onDown(); // Run callback: press has begun (possible hold behavior)
|
||||
buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled
|
||||
awaitingRelease = true; // Mark that polling-for-release should continue
|
||||
break;
|
||||
// New press detected by interrupt
|
||||
case IRQ:
|
||||
powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer)
|
||||
buttons[i].onDown(); // Run callback: press has begun (possible hold behavior)
|
||||
buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled
|
||||
awaitingRelease = true; // Mark that polling-for-release should continue
|
||||
break;
|
||||
|
||||
// An existing press continues
|
||||
// Not held long enough to register as longpress
|
||||
case POLLING_UNFIRED: {
|
||||
uint32_t length = millis() - buttons[i].irqAtMillis;
|
||||
// An existing press continues
|
||||
// Not held long enough to register as longpress
|
||||
case POLLING_UNFIRED: {
|
||||
uint32_t length = millis() - buttons[i].irqAtMillis;
|
||||
|
||||
// If button released since last thread tick,
|
||||
if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
|
||||
buttons[i].onUp(); // Run callback: press has ended (possible release of a hold)
|
||||
buttons[i].state = State::REST; // Mark that the button has reset
|
||||
if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress,
|
||||
buttons[i].onShortPress(); // Run callback: short press
|
||||
}
|
||||
// If button released since last thread tick,
|
||||
if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
|
||||
buttons[i].onUp(); // Run callback: press has ended (possible release of a hold)
|
||||
buttons[i].state = State::REST; // Mark that the button has reset
|
||||
if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress,
|
||||
buttons[i].onShortPress(); // Run callback: short press
|
||||
}
|
||||
|
||||
// If button not yet released
|
||||
else {
|
||||
awaitingRelease = true; // Mark that polling-for-release should continue
|
||||
if (length >= buttons[i].longpressLength) {
|
||||
// Run callback: long press (once)
|
||||
// Then continue waiting for release, to rearm
|
||||
buttons[i].state = State::POLLING_FIRED;
|
||||
buttons[i].onLongPress();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Button still held, but duration long enough that longpress event already fired
|
||||
// Just waiting for release
|
||||
case POLLING_FIRED:
|
||||
// Release detected
|
||||
if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
|
||||
buttons[i].state = State::REST;
|
||||
buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired)
|
||||
}
|
||||
// Not yet released, keep polling
|
||||
else
|
||||
awaitingRelease = true;
|
||||
break;
|
||||
// If button not yet released
|
||||
else {
|
||||
awaitingRelease = true; // Mark that polling-for-release should continue
|
||||
if (length >= buttons[i].longpressLength) {
|
||||
// Run callback: long press (once)
|
||||
// Then continue waiting for release, to rearm
|
||||
buttons[i].state = State::POLLING_FIRED;
|
||||
buttons[i].onLongPress();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If both buttons are now released
|
||||
// we don't need to waste cpu resources polling
|
||||
// IRQ will restart this thread when we next need it
|
||||
if (!awaitingRelease)
|
||||
stopThread();
|
||||
// Button still held, but duration long enough that longpress event already fired
|
||||
// Just waiting for release
|
||||
case POLLING_FIRED:
|
||||
// Release detected
|
||||
if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
|
||||
buttons[i].state = State::REST;
|
||||
buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired)
|
||||
}
|
||||
// Not yet released, keep polling
|
||||
else
|
||||
awaitingRelease = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Run this method again, or don't..
|
||||
// Use whatever behavior was previously set by stopThread() or startThread()
|
||||
return OSThread::interval;
|
||||
// If both buttons are now released
|
||||
// we don't need to waste cpu resources polling
|
||||
// IRQ will restart this thread when we next need it
|
||||
if (!awaitingRelease)
|
||||
stopThread();
|
||||
|
||||
// Run this method again, or don't..
|
||||
// Use whatever behavior was previously set by stopThread() or startThread()
|
||||
return OSThread::interval;
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
// Detach our class' interrupts before lightsleep
|
||||
// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
|
||||
int TwoButton::beforeLightSleep(void *unused)
|
||||
{
|
||||
stop();
|
||||
return 0; // Indicates success
|
||||
int TwoButton::beforeLightSleep(void *unused) {
|
||||
stop();
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
// Reconfigure our interrupts
|
||||
// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
|
||||
int TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause)
|
||||
{
|
||||
start();
|
||||
int TwoButton::afterLightSleep(esp_sleep_wakeup_cause_t cause) {
|
||||
start();
|
||||
|
||||
// Manually trigger the button-down ISR
|
||||
// - during light sleep, our ISR is disabled
|
||||
// - if light sleep ends by button press, pretend our own ISR caught it
|
||||
// - need to manually confirm by reading pin ourselves, to avoid occasional false positives
|
||||
// (false positive only when using internal pullup resistors?)
|
||||
if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic)
|
||||
isrPrimary();
|
||||
// Manually trigger the button-down ISR
|
||||
// - during light sleep, our ISR is disabled
|
||||
// - if light sleep ends by button press, pretend our own ISR caught it
|
||||
// - need to manually confirm by reading pin ourselves, to avoid occasional false positives
|
||||
// (false positive only when using internal pullup resistors?)
|
||||
if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic)
|
||||
isrPrimary();
|
||||
|
||||
return 0; // Indicates success
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -22,81 +22,78 @@ Interrupt driven
|
||||
|
||||
#include "Observer.h"
|
||||
|
||||
namespace NicheGraphics::Inputs
|
||||
{
|
||||
namespace NicheGraphics::Inputs {
|
||||
|
||||
class TwoButton : protected concurrency::OSThread
|
||||
{
|
||||
class TwoButton : protected concurrency::OSThread {
|
||||
public:
|
||||
typedef std::function<void()> Callback;
|
||||
|
||||
static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition
|
||||
|
||||
static TwoButton *getInstance(); // Create or get the singleton instance
|
||||
void start(); // Start handling button input
|
||||
void stop(); // Stop handling button input (disconnect ISRs for sleep)
|
||||
void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false);
|
||||
void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs);
|
||||
void setHandlerDown(uint8_t whichButton, Callback onDown);
|
||||
void setHandlerUp(uint8_t whichButton, Callback onUp);
|
||||
void setHandlerShortPress(uint8_t whichButton, Callback onShortPress);
|
||||
void setHandlerLongPress(uint8_t whichButton, Callback onLongPress);
|
||||
|
||||
// Disconnect and reconnect interrupts for light sleep
|
||||
#ifdef ARCH_ESP32
|
||||
int beforeLightSleep(void *unused);
|
||||
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
|
||||
#endif
|
||||
|
||||
private:
|
||||
// Internal state of a specific button
|
||||
enum State {
|
||||
REST, // Up, no activity
|
||||
IRQ, // Down detected, not yet handled
|
||||
POLLING_UNFIRED, // Down handled, polling for release
|
||||
POLLING_FIRED, // Longpress fired, button still held
|
||||
};
|
||||
|
||||
// Contains info about a specific button
|
||||
// (Array of this struct below)
|
||||
class Button {
|
||||
public:
|
||||
typedef std::function<void()> Callback;
|
||||
// Per-button config
|
||||
uint8_t pin = 0xFF; // 0xFF: unset
|
||||
bool activeLogic = LOW; // Active LOW by default. Currently unimplemented.
|
||||
uint32_t debounceLength = 50; // Minimum length for shortpress, in ms
|
||||
uint32_t longpressLength = 500; // How long after button down to fire longpress, in ms
|
||||
volatile State state = State::REST; // Internal state
|
||||
volatile uint32_t irqAtMillis; // millis() when button went down
|
||||
|
||||
static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition
|
||||
|
||||
static TwoButton *getInstance(); // Create or get the singleton instance
|
||||
void start(); // Start handling button input
|
||||
void stop(); // Stop handling button input (disconnect ISRs for sleep)
|
||||
void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false);
|
||||
void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs);
|
||||
void setHandlerDown(uint8_t whichButton, Callback onDown);
|
||||
void setHandlerUp(uint8_t whichButton, Callback onUp);
|
||||
void setHandlerShortPress(uint8_t whichButton, Callback onShortPress);
|
||||
void setHandlerLongPress(uint8_t whichButton, Callback onLongPress);
|
||||
|
||||
// Disconnect and reconnect interrupts for light sleep
|
||||
#ifdef ARCH_ESP32
|
||||
int beforeLightSleep(void *unused);
|
||||
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
|
||||
#endif
|
||||
|
||||
private:
|
||||
// Internal state of a specific button
|
||||
enum State {
|
||||
REST, // Up, no activity
|
||||
IRQ, // Down detected, not yet handled
|
||||
POLLING_UNFIRED, // Down handled, polling for release
|
||||
POLLING_FIRED, // Longpress fired, button still held
|
||||
};
|
||||
|
||||
// Contains info about a specific button
|
||||
// (Array of this struct below)
|
||||
class Button
|
||||
{
|
||||
public:
|
||||
// Per-button config
|
||||
uint8_t pin = 0xFF; // 0xFF: unset
|
||||
bool activeLogic = LOW; // Active LOW by default. Currently unimplemented.
|
||||
uint32_t debounceLength = 50; // Minimum length for shortpress, in ms
|
||||
uint32_t longpressLength = 500; // How long after button down to fire longpress, in ms
|
||||
volatile State state = State::REST; // Internal state
|
||||
volatile uint32_t irqAtMillis; // millis() when button went down
|
||||
|
||||
// Per-button event callbacks
|
||||
static void noop(){};
|
||||
std::function<void()> onDown = noop;
|
||||
std::function<void()> onUp = noop;
|
||||
std::function<void()> onShortPress = noop;
|
||||
std::function<void()> onLongPress = noop;
|
||||
};
|
||||
// Per-button event callbacks
|
||||
static void noop(){};
|
||||
std::function<void()> onDown = noop;
|
||||
std::function<void()> onUp = noop;
|
||||
std::function<void()> onShortPress = noop;
|
||||
std::function<void()> onLongPress = noop;
|
||||
};
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Get notified when lightsleep begins and ends
|
||||
CallbackObserver<TwoButton, void *> lsObserver = CallbackObserver<TwoButton, void *>(this, &TwoButton::beforeLightSleep);
|
||||
CallbackObserver<TwoButton, esp_sleep_wakeup_cause_t> lsEndObserver =
|
||||
CallbackObserver<TwoButton, esp_sleep_wakeup_cause_t>(this, &TwoButton::afterLightSleep);
|
||||
// Get notified when lightsleep begins and ends
|
||||
CallbackObserver<TwoButton, void *> lsObserver = CallbackObserver<TwoButton, void *>(this, &TwoButton::beforeLightSleep);
|
||||
CallbackObserver<TwoButton, esp_sleep_wakeup_cause_t> lsEndObserver =
|
||||
CallbackObserver<TwoButton, esp_sleep_wakeup_cause_t>(this, &TwoButton::afterLightSleep);
|
||||
#endif
|
||||
|
||||
int32_t runOnce() override; // Timer method. Polls for button release
|
||||
int32_t runOnce() override; // Timer method. Polls for button release
|
||||
|
||||
void startThread(); // Start polling for release
|
||||
void stopThread(); // Stop polling for release
|
||||
void startThread(); // Start polling for release
|
||||
void stopThread(); // Stop polling for release
|
||||
|
||||
static void isrPrimary(); // Detect start of press
|
||||
static void isrSecondary(); // Detect start of press (optional aux button)
|
||||
static void isrPrimary(); // Detect start of press
|
||||
static void isrSecondary(); // Detect start of press (optional aux button)
|
||||
|
||||
TwoButton(); // Constructor made private: force use of Button::instance()
|
||||
TwoButton(); // Constructor made private: force use of Button::instance()
|
||||
|
||||
// Info about both buttons
|
||||
Button buttons[2];
|
||||
// Info about both buttons
|
||||
Button buttons[2];
|
||||
};
|
||||
|
||||
}; // namespace NicheGraphics::Inputs
|
||||
|
||||
@@ -8,514 +8,481 @@
|
||||
|
||||
using namespace NicheGraphics::Inputs;
|
||||
|
||||
TwoButtonExtended::TwoButtonExtended() : concurrency::OSThread("TwoButtonExtended")
|
||||
{
|
||||
// Don't start polling buttons for release immediately
|
||||
// Assume they are in a "released" state at boot
|
||||
OSThread::disable();
|
||||
TwoButtonExtended::TwoButtonExtended() : concurrency::OSThread("TwoButtonExtended") {
|
||||
// Don't start polling buttons for release immediately
|
||||
// Assume they are in a "released" state at boot
|
||||
OSThread::disable();
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Register callbacks for before and after lightsleep
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
// Register callbacks for before and after lightsleep
|
||||
lsObserver.observe(¬ifyLightSleep);
|
||||
lsEndObserver.observe(¬ifyLightSleepEnd);
|
||||
#endif
|
||||
|
||||
// Explicitly initialize these, just to keep cppcheck quiet..
|
||||
buttons[0] = Button();
|
||||
buttons[1] = Button();
|
||||
joystick[Direction::UP] = SimpleButton();
|
||||
joystick[Direction::DOWN] = SimpleButton();
|
||||
joystick[Direction::LEFT] = SimpleButton();
|
||||
joystick[Direction::RIGHT] = SimpleButton();
|
||||
// Explicitly initialize these, just to keep cppcheck quiet..
|
||||
buttons[0] = Button();
|
||||
buttons[1] = Button();
|
||||
joystick[Direction::UP] = SimpleButton();
|
||||
joystick[Direction::DOWN] = SimpleButton();
|
||||
joystick[Direction::LEFT] = SimpleButton();
|
||||
joystick[Direction::RIGHT] = SimpleButton();
|
||||
}
|
||||
|
||||
// Get access to (or create) the singleton instance of this class
|
||||
// Accessible inside the ISRs, even though we maybe shouldn't
|
||||
TwoButtonExtended *TwoButtonExtended::getInstance()
|
||||
{
|
||||
// Instantiate the class the first time this method is called
|
||||
static TwoButtonExtended *const singletonInstance = new TwoButtonExtended;
|
||||
TwoButtonExtended *TwoButtonExtended::getInstance() {
|
||||
// Instantiate the class the first time this method is called
|
||||
static TwoButtonExtended *const singletonInstance = new TwoButtonExtended;
|
||||
|
||||
return singletonInstance;
|
||||
return singletonInstance;
|
||||
}
|
||||
|
||||
// Begin receiving button input
|
||||
// We probably need to do this after sleep, as well as at boot
|
||||
void TwoButtonExtended::start()
|
||||
{
|
||||
if (buttons[0].pin != 0xFF)
|
||||
attachInterrupt(buttons[0].pin, TwoButtonExtended::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING);
|
||||
void TwoButtonExtended::start() {
|
||||
if (buttons[0].pin != 0xFF)
|
||||
attachInterrupt(buttons[0].pin, TwoButtonExtended::isrPrimary, buttons[0].activeLogic == LOW ? FALLING : RISING);
|
||||
|
||||
if (buttons[1].pin != 0xFF)
|
||||
attachInterrupt(buttons[1].pin, TwoButtonExtended::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING);
|
||||
if (buttons[1].pin != 0xFF)
|
||||
attachInterrupt(buttons[1].pin, TwoButtonExtended::isrSecondary, buttons[1].activeLogic == LOW ? FALLING : RISING);
|
||||
|
||||
if (joystick[Direction::UP].pin != 0xFF)
|
||||
attachInterrupt(joystick[Direction::UP].pin, TwoButtonExtended::isrJoystickUp,
|
||||
joystickActiveLogic == LOW ? FALLING : RISING);
|
||||
if (joystick[Direction::UP].pin != 0xFF)
|
||||
attachInterrupt(joystick[Direction::UP].pin, TwoButtonExtended::isrJoystickUp, joystickActiveLogic == LOW ? FALLING : RISING);
|
||||
|
||||
if (joystick[Direction::DOWN].pin != 0xFF)
|
||||
attachInterrupt(joystick[Direction::DOWN].pin, TwoButtonExtended::isrJoystickDown,
|
||||
joystickActiveLogic == LOW ? FALLING : RISING);
|
||||
if (joystick[Direction::DOWN].pin != 0xFF)
|
||||
attachInterrupt(joystick[Direction::DOWN].pin, TwoButtonExtended::isrJoystickDown, joystickActiveLogic == LOW ? FALLING : RISING);
|
||||
|
||||
if (joystick[Direction::LEFT].pin != 0xFF)
|
||||
attachInterrupt(joystick[Direction::LEFT].pin, TwoButtonExtended::isrJoystickLeft,
|
||||
joystickActiveLogic == LOW ? FALLING : RISING);
|
||||
if (joystick[Direction::LEFT].pin != 0xFF)
|
||||
attachInterrupt(joystick[Direction::LEFT].pin, TwoButtonExtended::isrJoystickLeft, joystickActiveLogic == LOW ? FALLING : RISING);
|
||||
|
||||
if (joystick[Direction::RIGHT].pin != 0xFF)
|
||||
attachInterrupt(joystick[Direction::RIGHT].pin, TwoButtonExtended::isrJoystickRight,
|
||||
joystickActiveLogic == LOW ? FALLING : RISING);
|
||||
if (joystick[Direction::RIGHT].pin != 0xFF)
|
||||
attachInterrupt(joystick[Direction::RIGHT].pin, TwoButtonExtended::isrJoystickRight, joystickActiveLogic == LOW ? FALLING : RISING);
|
||||
}
|
||||
|
||||
// Stop receiving button input, and run custom sleep code
|
||||
// Called before device sleeps. This might be power-off, or just ESP32 light sleep
|
||||
// Some devices will want to attach interrupts here, for the user button to wake from sleep
|
||||
void TwoButtonExtended::stop()
|
||||
{
|
||||
if (buttons[0].pin != 0xFF)
|
||||
detachInterrupt(buttons[0].pin);
|
||||
void TwoButtonExtended::stop() {
|
||||
if (buttons[0].pin != 0xFF)
|
||||
detachInterrupt(buttons[0].pin);
|
||||
|
||||
if (buttons[1].pin != 0xFF)
|
||||
detachInterrupt(buttons[1].pin);
|
||||
if (buttons[1].pin != 0xFF)
|
||||
detachInterrupt(buttons[1].pin);
|
||||
|
||||
if (joystick[Direction::UP].pin != 0xFF)
|
||||
detachInterrupt(joystick[Direction::UP].pin);
|
||||
if (joystick[Direction::UP].pin != 0xFF)
|
||||
detachInterrupt(joystick[Direction::UP].pin);
|
||||
|
||||
if (joystick[Direction::DOWN].pin != 0xFF)
|
||||
detachInterrupt(joystick[Direction::DOWN].pin);
|
||||
if (joystick[Direction::DOWN].pin != 0xFF)
|
||||
detachInterrupt(joystick[Direction::DOWN].pin);
|
||||
|
||||
if (joystick[Direction::LEFT].pin != 0xFF)
|
||||
detachInterrupt(joystick[Direction::LEFT].pin);
|
||||
if (joystick[Direction::LEFT].pin != 0xFF)
|
||||
detachInterrupt(joystick[Direction::LEFT].pin);
|
||||
|
||||
if (joystick[Direction::RIGHT].pin != 0xFF)
|
||||
detachInterrupt(joystick[Direction::RIGHT].pin);
|
||||
if (joystick[Direction::RIGHT].pin != 0xFF)
|
||||
detachInterrupt(joystick[Direction::RIGHT].pin);
|
||||
}
|
||||
|
||||
// Attempt to resolve a GPIO pin for the user button, honoring userPrefs.jsonc and device settings
|
||||
// This helper method isn't used by the TwoButtonExtended class itself, it could be moved elsewhere.
|
||||
// Intention is to pass this value to TwoButtonExtended::setWiring in the setupNicheGraphics method.
|
||||
uint8_t TwoButtonExtended::getUserButtonPin()
|
||||
{
|
||||
uint8_t pin = 0xFF; // Unset
|
||||
uint8_t TwoButtonExtended::getUserButtonPin() {
|
||||
uint8_t pin = 0xFF; // Unset
|
||||
|
||||
// Use default pin for variant, if no better source
|
||||
// Use default pin for variant, if no better source
|
||||
#ifdef BUTTON_PIN
|
||||
pin = BUTTON_PIN;
|
||||
pin = BUTTON_PIN;
|
||||
#endif
|
||||
|
||||
// From userPrefs.jsonc, if set
|
||||
// From userPrefs.jsonc, if set
|
||||
#ifdef USERPREFS_BUTTON_PIN
|
||||
pin = USERPREFS_BUTTON_PIN;
|
||||
pin = USERPREFS_BUTTON_PIN;
|
||||
#endif
|
||||
|
||||
// From user's override in device settings, if set
|
||||
if (config.device.button_gpio)
|
||||
pin = config.device.button_gpio;
|
||||
// From user's override in device settings, if set
|
||||
if (config.device.button_gpio)
|
||||
pin = config.device.button_gpio;
|
||||
|
||||
return pin;
|
||||
return pin;
|
||||
}
|
||||
|
||||
// Configures the wiring and logic of either button
|
||||
// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp
|
||||
void TwoButtonExtended::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup)
|
||||
{
|
||||
// Prevent the same GPIO being assigned to multiple buttons
|
||||
// Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button
|
||||
for (uint8_t i = 0; i < whichButton; i++) {
|
||||
if (buttons[i].pin == pin) {
|
||||
LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton);
|
||||
return;
|
||||
}
|
||||
void TwoButtonExtended::setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup) {
|
||||
// Prevent the same GPIO being assigned to multiple buttons
|
||||
// Allows an edge case when the user remaps hardware buttons using device settings, due to a broken user button
|
||||
for (uint8_t i = 0; i < whichButton; i++) {
|
||||
if (buttons[i].pin == pin) {
|
||||
LOG_WARN("Attempted reuse of GPIO %d. Ignoring assignment whichButton=%d", pin, whichButton);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].pin = pin;
|
||||
buttons[whichButton].activeLogic = LOW;
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].pin = pin;
|
||||
buttons[whichButton].activeLogic = LOW;
|
||||
|
||||
pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT);
|
||||
pinMode(buttons[whichButton].pin, internalPullup ? INPUT_PULLUP : INPUT);
|
||||
}
|
||||
|
||||
// Configures the wiring and logic of the joystick buttons
|
||||
// Called when outlining your NicheGraphics implementation, in variant/nicheGraphics.cpp
|
||||
void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup)
|
||||
{
|
||||
if (joystick[Direction::UP].pin == uPin || joystick[Direction::DOWN].pin == dPin || joystick[Direction::LEFT].pin == lPin ||
|
||||
joystick[Direction::RIGHT].pin == rPin) {
|
||||
LOG_WARN("Attempted reuse of Joystick GPIO. Ignoring assignment");
|
||||
return;
|
||||
}
|
||||
void TwoButtonExtended::setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup) {
|
||||
if (joystick[Direction::UP].pin == uPin || joystick[Direction::DOWN].pin == dPin || joystick[Direction::LEFT].pin == lPin ||
|
||||
joystick[Direction::RIGHT].pin == rPin) {
|
||||
LOG_WARN("Attempted reuse of Joystick GPIO. Ignoring assignment");
|
||||
return;
|
||||
}
|
||||
|
||||
joystick[Direction::UP].pin = uPin;
|
||||
joystick[Direction::DOWN].pin = dPin;
|
||||
joystick[Direction::LEFT].pin = lPin;
|
||||
joystick[Direction::RIGHT].pin = rPin;
|
||||
joystickActiveLogic = LOW;
|
||||
joystick[Direction::UP].pin = uPin;
|
||||
joystick[Direction::DOWN].pin = dPin;
|
||||
joystick[Direction::LEFT].pin = lPin;
|
||||
joystick[Direction::RIGHT].pin = rPin;
|
||||
joystickActiveLogic = LOW;
|
||||
|
||||
pinMode(joystick[Direction::UP].pin, internalPullup ? INPUT_PULLUP : INPUT);
|
||||
pinMode(joystick[Direction::DOWN].pin, internalPullup ? INPUT_PULLUP : INPUT);
|
||||
pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT);
|
||||
pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT);
|
||||
pinMode(joystick[Direction::UP].pin, internalPullup ? INPUT_PULLUP : INPUT);
|
||||
pinMode(joystick[Direction::DOWN].pin, internalPullup ? INPUT_PULLUP : INPUT);
|
||||
pinMode(joystick[Direction::LEFT].pin, internalPullup ? INPUT_PULLUP : INPUT);
|
||||
pinMode(joystick[Direction::RIGHT].pin, internalPullup ? INPUT_PULLUP : INPUT);
|
||||
}
|
||||
|
||||
void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].debounceLength = debounceMs;
|
||||
buttons[whichButton].longpressLength = longpressMs;
|
||||
void TwoButtonExtended::setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs) {
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].debounceLength = debounceMs;
|
||||
buttons[whichButton].longpressLength = longpressMs;
|
||||
}
|
||||
|
||||
void TwoButtonExtended::setJoystickDebounce(uint32_t debounceMs)
|
||||
{
|
||||
joystickDebounceLength = debounceMs;
|
||||
}
|
||||
void TwoButtonExtended::setJoystickDebounce(uint32_t debounceMs) { joystickDebounceLength = debounceMs; }
|
||||
|
||||
// Set what should happen when a button becomes pressed
|
||||
// Use this to implement a "while held" behavior
|
||||
void TwoButtonExtended::setHandlerDown(uint8_t whichButton, Callback onDown)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onDown = onDown;
|
||||
void TwoButtonExtended::setHandlerDown(uint8_t whichButton, Callback onDown) {
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onDown = onDown;
|
||||
}
|
||||
|
||||
// Set what should happen when a button becomes unpressed
|
||||
// Use this to implement a "While held" behavior
|
||||
void TwoButtonExtended::setHandlerUp(uint8_t whichButton, Callback onUp)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onUp = onUp;
|
||||
void TwoButtonExtended::setHandlerUp(uint8_t whichButton, Callback onUp) {
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onUp = onUp;
|
||||
}
|
||||
|
||||
// Set what should happen when a "short press" event has occurred
|
||||
void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onPress = onPress;
|
||||
void TwoButtonExtended::setHandlerShortPress(uint8_t whichButton, Callback onPress) {
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onPress = onPress;
|
||||
}
|
||||
|
||||
// Set what should happen when a "long press" event has fired
|
||||
// Note: this will occur while the button is still held
|
||||
void TwoButtonExtended::setHandlerLongPress(uint8_t whichButton, Callback onLongPress)
|
||||
{
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onLongPress = onLongPress;
|
||||
void TwoButtonExtended::setHandlerLongPress(uint8_t whichButton, Callback onLongPress) {
|
||||
assert(whichButton < 2);
|
||||
buttons[whichButton].onLongPress = onLongPress;
|
||||
}
|
||||
|
||||
// Set what should happen when a joystick button becomes pressed
|
||||
// Use this to implement a "while held" behavior
|
||||
void TwoButtonExtended::setJoystickDownHandlers(Callback uDown, Callback dDown, Callback lDown, Callback rDown)
|
||||
{
|
||||
joystick[Direction::UP].onDown = uDown;
|
||||
joystick[Direction::DOWN].onDown = dDown;
|
||||
joystick[Direction::LEFT].onDown = lDown;
|
||||
joystick[Direction::RIGHT].onDown = rDown;
|
||||
void TwoButtonExtended::setJoystickDownHandlers(Callback uDown, Callback dDown, Callback lDown, Callback rDown) {
|
||||
joystick[Direction::UP].onDown = uDown;
|
||||
joystick[Direction::DOWN].onDown = dDown;
|
||||
joystick[Direction::LEFT].onDown = lDown;
|
||||
joystick[Direction::RIGHT].onDown = rDown;
|
||||
}
|
||||
|
||||
// Set what should happen when a joystick button becomes unpressed
|
||||
// Use this to implement a "while held" behavior
|
||||
void TwoButtonExtended::setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp)
|
||||
{
|
||||
joystick[Direction::UP].onUp = uUp;
|
||||
joystick[Direction::DOWN].onUp = dUp;
|
||||
joystick[Direction::LEFT].onUp = lUp;
|
||||
joystick[Direction::RIGHT].onUp = rUp;
|
||||
void TwoButtonExtended::setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp) {
|
||||
joystick[Direction::UP].onUp = uUp;
|
||||
joystick[Direction::DOWN].onUp = dUp;
|
||||
joystick[Direction::LEFT].onUp = lUp;
|
||||
joystick[Direction::RIGHT].onUp = rUp;
|
||||
}
|
||||
|
||||
// Set what should happen when a "press" event has fired
|
||||
// Note: this will occur while the joystick button is still held
|
||||
void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress)
|
||||
{
|
||||
joystick[Direction::UP].onPress = uPress;
|
||||
joystick[Direction::DOWN].onPress = dPress;
|
||||
joystick[Direction::LEFT].onPress = lPress;
|
||||
joystick[Direction::RIGHT].onPress = rPress;
|
||||
void TwoButtonExtended::setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress) {
|
||||
joystick[Direction::UP].onPress = uPress;
|
||||
joystick[Direction::DOWN].onPress = dPress;
|
||||
joystick[Direction::LEFT].onPress = lPress;
|
||||
joystick[Direction::RIGHT].onPress = rPress;
|
||||
}
|
||||
|
||||
// Handle the start of a press to the primary button
|
||||
// Wakes our button thread
|
||||
void TwoButtonExtended::isrPrimary()
|
||||
{
|
||||
static volatile bool isrRunning = false;
|
||||
void TwoButtonExtended::isrPrimary() {
|
||||
static volatile bool isrRunning = false;
|
||||
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButtonExtended *b = TwoButtonExtended::getInstance();
|
||||
if (b->buttons[0].state == State::REST) {
|
||||
b->buttons[0].state = State::IRQ;
|
||||
b->buttons[0].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButtonExtended *b = TwoButtonExtended::getInstance();
|
||||
if (b->buttons[0].state == State::REST) {
|
||||
b->buttons[0].state = State::IRQ;
|
||||
b->buttons[0].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the start of a press to the secondary button
|
||||
// Wakes our button thread
|
||||
void TwoButtonExtended::isrSecondary()
|
||||
{
|
||||
static volatile bool isrRunning = false;
|
||||
void TwoButtonExtended::isrSecondary() {
|
||||
static volatile bool isrRunning = false;
|
||||
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButtonExtended *b = TwoButtonExtended::getInstance();
|
||||
if (b->buttons[1].state == State::REST) {
|
||||
b->buttons[1].state = State::IRQ;
|
||||
b->buttons[1].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButtonExtended *b = TwoButtonExtended::getInstance();
|
||||
if (b->buttons[1].state == State::REST) {
|
||||
b->buttons[1].state = State::IRQ;
|
||||
b->buttons[1].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the start of a press to the joystick buttons
|
||||
// Also wakes our button thread
|
||||
void TwoButtonExtended::isrJoystickUp()
|
||||
{
|
||||
static volatile bool isrRunning = false;
|
||||
void TwoButtonExtended::isrJoystickUp() {
|
||||
static volatile bool isrRunning = false;
|
||||
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButtonExtended *b = TwoButtonExtended::getInstance();
|
||||
if (b->joystick[Direction::UP].state == State::REST) {
|
||||
b->joystick[Direction::UP].state = State::IRQ;
|
||||
b->joystick[Direction::UP].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButtonExtended *b = TwoButtonExtended::getInstance();
|
||||
if (b->joystick[Direction::UP].state == State::REST) {
|
||||
b->joystick[Direction::UP].state = State::IRQ;
|
||||
b->joystick[Direction::UP].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
void TwoButtonExtended::isrJoystickDown()
|
||||
{
|
||||
static volatile bool isrRunning = false;
|
||||
void TwoButtonExtended::isrJoystickDown() {
|
||||
static volatile bool isrRunning = false;
|
||||
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButtonExtended *b = TwoButtonExtended::getInstance();
|
||||
if (b->joystick[Direction::DOWN].state == State::REST) {
|
||||
b->joystick[Direction::DOWN].state = State::IRQ;
|
||||
b->joystick[Direction::DOWN].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButtonExtended *b = TwoButtonExtended::getInstance();
|
||||
if (b->joystick[Direction::DOWN].state == State::REST) {
|
||||
b->joystick[Direction::DOWN].state = State::IRQ;
|
||||
b->joystick[Direction::DOWN].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
void TwoButtonExtended::isrJoystickLeft()
|
||||
{
|
||||
static volatile bool isrRunning = false;
|
||||
void TwoButtonExtended::isrJoystickLeft() {
|
||||
static volatile bool isrRunning = false;
|
||||
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButtonExtended *b = TwoButtonExtended::getInstance();
|
||||
if (b->joystick[Direction::LEFT].state == State::REST) {
|
||||
b->joystick[Direction::LEFT].state = State::IRQ;
|
||||
b->joystick[Direction::LEFT].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButtonExtended *b = TwoButtonExtended::getInstance();
|
||||
if (b->joystick[Direction::LEFT].state == State::REST) {
|
||||
b->joystick[Direction::LEFT].state = State::IRQ;
|
||||
b->joystick[Direction::LEFT].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
void TwoButtonExtended::isrJoystickRight()
|
||||
{
|
||||
static volatile bool isrRunning = false;
|
||||
void TwoButtonExtended::isrJoystickRight() {
|
||||
static volatile bool isrRunning = false;
|
||||
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButtonExtended *b = TwoButtonExtended::getInstance();
|
||||
if (b->joystick[Direction::RIGHT].state == State::REST) {
|
||||
b->joystick[Direction::RIGHT].state = State::IRQ;
|
||||
b->joystick[Direction::RIGHT].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
if (!isrRunning) {
|
||||
isrRunning = true;
|
||||
TwoButtonExtended *b = TwoButtonExtended::getInstance();
|
||||
if (b->joystick[Direction::RIGHT].state == State::REST) {
|
||||
b->joystick[Direction::RIGHT].state = State::IRQ;
|
||||
b->joystick[Direction::RIGHT].irqAtMillis = millis();
|
||||
b->startThread();
|
||||
}
|
||||
isrRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Concise method to start our button thread
|
||||
// Follows an ISR, listening for button release
|
||||
void TwoButtonExtended::startThread()
|
||||
{
|
||||
if (!OSThread::enabled) {
|
||||
OSThread::setInterval(10);
|
||||
OSThread::enabled = true;
|
||||
}
|
||||
void TwoButtonExtended::startThread() {
|
||||
if (!OSThread::enabled) {
|
||||
OSThread::setInterval(10);
|
||||
OSThread::enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Concise method to stop our button thread
|
||||
// Called when we no longer need to poll for button release
|
||||
void TwoButtonExtended::stopThread()
|
||||
{
|
||||
if (OSThread::enabled) {
|
||||
OSThread::disable();
|
||||
}
|
||||
void TwoButtonExtended::stopThread() {
|
||||
if (OSThread::enabled) {
|
||||
OSThread::disable();
|
||||
}
|
||||
|
||||
// Reset both buttons manually
|
||||
// Just in case an IRQ fires during the process of resetting the system
|
||||
// Can occur with super rapid presses?
|
||||
buttons[0].state = REST;
|
||||
buttons[1].state = REST;
|
||||
joystick[Direction::UP].state = REST;
|
||||
joystick[Direction::DOWN].state = REST;
|
||||
joystick[Direction::LEFT].state = REST;
|
||||
joystick[Direction::RIGHT].state = REST;
|
||||
// Reset both buttons manually
|
||||
// Just in case an IRQ fires during the process of resetting the system
|
||||
// Can occur with super rapid presses?
|
||||
buttons[0].state = REST;
|
||||
buttons[1].state = REST;
|
||||
joystick[Direction::UP].state = REST;
|
||||
joystick[Direction::DOWN].state = REST;
|
||||
joystick[Direction::LEFT].state = REST;
|
||||
joystick[Direction::RIGHT].state = REST;
|
||||
}
|
||||
|
||||
// Our button thread
|
||||
// Started by an IRQ, on either button
|
||||
// Polls for button releases
|
||||
// Stops when both buttons released
|
||||
int32_t TwoButtonExtended::runOnce()
|
||||
{
|
||||
constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button);
|
||||
constexpr uint8_t JOYSTICK_COUNT = sizeof(joystick) / sizeof(SimpleButton);
|
||||
int32_t TwoButtonExtended::runOnce() {
|
||||
constexpr uint8_t BUTTON_COUNT = sizeof(buttons) / sizeof(Button);
|
||||
constexpr uint8_t JOYSTICK_COUNT = sizeof(joystick) / sizeof(SimpleButton);
|
||||
|
||||
// Allow either button to request that our thread should continue polling
|
||||
bool awaitingRelease = false;
|
||||
// Allow either button to request that our thread should continue polling
|
||||
bool awaitingRelease = false;
|
||||
|
||||
// Check both primary and secondary buttons
|
||||
for (uint8_t i = 0; i < BUTTON_COUNT; i++) {
|
||||
switch (buttons[i].state) {
|
||||
// No action: button has not been pressed
|
||||
case REST:
|
||||
break;
|
||||
// Check both primary and secondary buttons
|
||||
for (uint8_t i = 0; i < BUTTON_COUNT; i++) {
|
||||
switch (buttons[i].state) {
|
||||
// No action: button has not been pressed
|
||||
case REST:
|
||||
break;
|
||||
|
||||
// New press detected by interrupt
|
||||
case IRQ:
|
||||
powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer)
|
||||
buttons[i].onDown(); // Run callback: press has begun (possible hold behavior)
|
||||
buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled
|
||||
awaitingRelease = true; // Mark that polling-for-release should continue
|
||||
break;
|
||||
// New press detected by interrupt
|
||||
case IRQ:
|
||||
powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer)
|
||||
buttons[i].onDown(); // Run callback: press has begun (possible hold behavior)
|
||||
buttons[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled
|
||||
awaitingRelease = true; // Mark that polling-for-release should continue
|
||||
break;
|
||||
|
||||
// An existing press continues
|
||||
// Not held long enough to register as longpress
|
||||
case POLLING_UNFIRED: {
|
||||
uint32_t length = millis() - buttons[i].irqAtMillis;
|
||||
// An existing press continues
|
||||
// Not held long enough to register as longpress
|
||||
case POLLING_UNFIRED: {
|
||||
uint32_t length = millis() - buttons[i].irqAtMillis;
|
||||
|
||||
// If button released since last thread tick,
|
||||
if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
|
||||
buttons[i].onUp(); // Run callback: press has ended (possible release of a hold)
|
||||
buttons[i].state = State::REST; // Mark that the button has reset
|
||||
if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress,
|
||||
buttons[i].onPress(); // Run callback: press
|
||||
}
|
||||
// If button not yet released
|
||||
else {
|
||||
awaitingRelease = true; // Mark that polling-for-release should continue
|
||||
if (length >= buttons[i].longpressLength) {
|
||||
// Run callback: long press (once)
|
||||
// Then continue waiting for release, to rearm
|
||||
buttons[i].state = State::POLLING_FIRED;
|
||||
buttons[i].onLongPress();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Button still held, but duration long enough that longpress event already fired
|
||||
// Just waiting for release
|
||||
case POLLING_FIRED:
|
||||
// Release detected
|
||||
if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
|
||||
buttons[i].state = State::REST;
|
||||
buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired)
|
||||
}
|
||||
// Not yet released, keep polling
|
||||
else
|
||||
awaitingRelease = true;
|
||||
break;
|
||||
// If button released since last thread tick,
|
||||
if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
|
||||
buttons[i].onUp(); // Run callback: press has ended (possible release of a hold)
|
||||
buttons[i].state = State::REST; // Mark that the button has reset
|
||||
if (length > buttons[i].debounceLength && length < buttons[i].longpressLength) // If too short for longpress,
|
||||
buttons[i].onPress(); // Run callback: press
|
||||
}
|
||||
// If button not yet released
|
||||
else {
|
||||
awaitingRelease = true; // Mark that polling-for-release should continue
|
||||
if (length >= buttons[i].longpressLength) {
|
||||
// Run callback: long press (once)
|
||||
// Then continue waiting for release, to rearm
|
||||
buttons[i].state = State::POLLING_FIRED;
|
||||
buttons[i].onLongPress();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Check all the joystick directions
|
||||
for (uint8_t i = 0; i < JOYSTICK_COUNT; i++) {
|
||||
switch (joystick[i].state) {
|
||||
// No action: button has not been pressed
|
||||
case REST:
|
||||
break;
|
||||
// Button still held, but duration long enough that longpress event already fired
|
||||
// Just waiting for release
|
||||
case POLLING_FIRED:
|
||||
// Release detected
|
||||
if (digitalRead(buttons[i].pin) != buttons[i].activeLogic) {
|
||||
buttons[i].state = State::REST;
|
||||
buttons[i].onUp(); // Callback: release of hold (in this case: *after* longpress has fired)
|
||||
}
|
||||
// Not yet released, keep polling
|
||||
else
|
||||
awaitingRelease = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// New press detected by interrupt
|
||||
case IRQ:
|
||||
powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer)
|
||||
joystick[i].onDown(); // Run callback: press has begun (possible hold behavior)
|
||||
joystick[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled
|
||||
awaitingRelease = true; // Mark that polling-for-release should continue
|
||||
break;
|
||||
// Check all the joystick directions
|
||||
for (uint8_t i = 0; i < JOYSTICK_COUNT; i++) {
|
||||
switch (joystick[i].state) {
|
||||
// No action: button has not been pressed
|
||||
case REST:
|
||||
break;
|
||||
|
||||
// An existing press continues
|
||||
// Not held long enough to register as press
|
||||
case POLLING_UNFIRED: {
|
||||
uint32_t length = millis() - joystick[i].irqAtMillis;
|
||||
// New press detected by interrupt
|
||||
case IRQ:
|
||||
powerFSM.trigger(EVENT_PRESS); // Tell PowerFSM that press occurred (resets sleep timer)
|
||||
joystick[i].onDown(); // Run callback: press has begun (possible hold behavior)
|
||||
joystick[i].state = State::POLLING_UNFIRED; // Mark that button-down has been handled
|
||||
awaitingRelease = true; // Mark that polling-for-release should continue
|
||||
break;
|
||||
|
||||
// If button released since last thread tick,
|
||||
if (digitalRead(joystick[i].pin) != joystickActiveLogic) {
|
||||
joystick[i].onUp(); // Run callback: press has ended (possible release of a hold)
|
||||
joystick[i].state = State::REST; // Mark that the button has reset
|
||||
}
|
||||
// If button not yet released
|
||||
else {
|
||||
awaitingRelease = true; // Mark that polling-for-release should continue
|
||||
if (length >= joystickDebounceLength) {
|
||||
// Run callback: long press (once)
|
||||
// Then continue waiting for release, to rearm
|
||||
joystick[i].state = State::POLLING_FIRED;
|
||||
joystick[i].onPress();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Button still held after press
|
||||
// Just waiting for release
|
||||
case POLLING_FIRED:
|
||||
// Release detected
|
||||
if (digitalRead(joystick[i].pin) != joystickActiveLogic) {
|
||||
joystick[i].state = State::REST;
|
||||
joystick[i].onUp(); // Callback: release of hold
|
||||
}
|
||||
// Not yet released, keep polling
|
||||
else
|
||||
awaitingRelease = true;
|
||||
break;
|
||||
// An existing press continues
|
||||
// Not held long enough to register as press
|
||||
case POLLING_UNFIRED: {
|
||||
uint32_t length = millis() - joystick[i].irqAtMillis;
|
||||
|
||||
// If button released since last thread tick,
|
||||
if (digitalRead(joystick[i].pin) != joystickActiveLogic) {
|
||||
joystick[i].onUp(); // Run callback: press has ended (possible release of a hold)
|
||||
joystick[i].state = State::REST; // Mark that the button has reset
|
||||
}
|
||||
// If button not yet released
|
||||
else {
|
||||
awaitingRelease = true; // Mark that polling-for-release should continue
|
||||
if (length >= joystickDebounceLength) {
|
||||
// Run callback: long press (once)
|
||||
// Then continue waiting for release, to rearm
|
||||
joystick[i].state = State::POLLING_FIRED;
|
||||
joystick[i].onPress();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If all buttons are now released
|
||||
// we don't need to waste cpu resources polling
|
||||
// IRQ will restart this thread when we next need it
|
||||
if (!awaitingRelease)
|
||||
stopThread();
|
||||
// Button still held after press
|
||||
// Just waiting for release
|
||||
case POLLING_FIRED:
|
||||
// Release detected
|
||||
if (digitalRead(joystick[i].pin) != joystickActiveLogic) {
|
||||
joystick[i].state = State::REST;
|
||||
joystick[i].onUp(); // Callback: release of hold
|
||||
}
|
||||
// Not yet released, keep polling
|
||||
else
|
||||
awaitingRelease = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Run this method again, or don't..
|
||||
// Use whatever behavior was previously set by stopThread() or startThread()
|
||||
return OSThread::interval;
|
||||
// If all buttons are now released
|
||||
// we don't need to waste cpu resources polling
|
||||
// IRQ will restart this thread when we next need it
|
||||
if (!awaitingRelease)
|
||||
stopThread();
|
||||
|
||||
// Run this method again, or don't..
|
||||
// Use whatever behavior was previously set by stopThread() or startThread()
|
||||
return OSThread::interval;
|
||||
}
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
|
||||
// Detach our class' interrupts before lightsleep
|
||||
// Allows sleep.cpp to configure its own interrupts, which wake the device on user-button press
|
||||
int TwoButtonExtended::beforeLightSleep(void *unused)
|
||||
{
|
||||
stop();
|
||||
return 0; // Indicates success
|
||||
int TwoButtonExtended::beforeLightSleep(void *unused) {
|
||||
stop();
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
// Reconfigure our interrupts
|
||||
// Our class' interrupts were disconnected during sleep, to allow the user button to wake the device from sleep
|
||||
int TwoButtonExtended::afterLightSleep(esp_sleep_wakeup_cause_t cause)
|
||||
{
|
||||
start();
|
||||
int TwoButtonExtended::afterLightSleep(esp_sleep_wakeup_cause_t cause) {
|
||||
start();
|
||||
|
||||
// Manually trigger the button-down ISR
|
||||
// - during light sleep, our ISR is disabled
|
||||
// - if light sleep ends by button press, pretend our own ISR caught it
|
||||
// - need to manually confirm by reading pin ourselves, to avoid occasional false positives
|
||||
// (false positive only when using internal pullup resistors?)
|
||||
if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic)
|
||||
isrPrimary();
|
||||
// Manually trigger the button-down ISR
|
||||
// - during light sleep, our ISR is disabled
|
||||
// - if light sleep ends by button press, pretend our own ISR caught it
|
||||
// - need to manually confirm by reading pin ourselves, to avoid occasional false positives
|
||||
// (false positive only when using internal pullup resistors?)
|
||||
if (cause == ESP_SLEEP_WAKEUP_GPIO && digitalRead(buttons[0].pin) == buttons[0].activeLogic)
|
||||
isrPrimary();
|
||||
|
||||
return 0; // Indicates success
|
||||
return 0; // Indicates success
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -30,105 +30,100 @@ Interrupt driven
|
||||
|
||||
#include "Observer.h"
|
||||
|
||||
namespace NicheGraphics::Inputs
|
||||
{
|
||||
namespace NicheGraphics::Inputs {
|
||||
|
||||
class TwoButtonExtended : protected concurrency::OSThread
|
||||
{
|
||||
class TwoButtonExtended : protected concurrency::OSThread {
|
||||
public:
|
||||
typedef std::function<void()> Callback;
|
||||
|
||||
static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition
|
||||
|
||||
static TwoButtonExtended *getInstance(); // Create or get the singleton instance
|
||||
void start(); // Start handling button input
|
||||
void stop(); // Stop handling button input (disconnect ISRs for sleep)
|
||||
void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false);
|
||||
void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false);
|
||||
void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs);
|
||||
void setJoystickDebounce(uint32_t debounceMs);
|
||||
void setHandlerDown(uint8_t whichButton, Callback onDown);
|
||||
void setHandlerUp(uint8_t whichButton, Callback onUp);
|
||||
void setHandlerShortPress(uint8_t whichButton, Callback onShortPress);
|
||||
void setHandlerLongPress(uint8_t whichButton, Callback onLongPress);
|
||||
void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown);
|
||||
void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp);
|
||||
void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress);
|
||||
|
||||
// Disconnect and reconnect interrupts for light sleep
|
||||
#ifdef ARCH_ESP32
|
||||
int beforeLightSleep(void *unused);
|
||||
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
|
||||
#endif
|
||||
|
||||
private:
|
||||
// Internal state of a specific button
|
||||
enum State {
|
||||
REST, // Up, no activity
|
||||
IRQ, // Down detected, not yet handled
|
||||
POLLING_UNFIRED, // Down handled, polling for release
|
||||
POLLING_FIRED, // Longpress fired, button still held
|
||||
};
|
||||
|
||||
// Joystick Directions
|
||||
enum Direction { UP = 0, DOWN, LEFT, RIGHT };
|
||||
|
||||
// Data used for direction (single-action) buttons
|
||||
class SimpleButton {
|
||||
public:
|
||||
typedef std::function<void()> Callback;
|
||||
// Per-button config
|
||||
uint8_t pin = 0xFF; // 0xFF: unset
|
||||
volatile State state = State::REST; // Internal state
|
||||
volatile uint32_t irqAtMillis; // millis() when button went down
|
||||
|
||||
static uint8_t getUserButtonPin(); // Resolve the GPIO, considering the various possible source of definition
|
||||
// Per-button event callbacks
|
||||
static void noop(){};
|
||||
std::function<void()> onDown = noop;
|
||||
std::function<void()> onUp = noop;
|
||||
std::function<void()> onPress = noop;
|
||||
};
|
||||
|
||||
static TwoButtonExtended *getInstance(); // Create or get the singleton instance
|
||||
void start(); // Start handling button input
|
||||
void stop(); // Stop handling button input (disconnect ISRs for sleep)
|
||||
void setWiring(uint8_t whichButton, uint8_t pin, bool internalPullup = false);
|
||||
void setJoystickWiring(uint8_t uPin, uint8_t dPin, uint8_t lPin, uint8_t rPin, bool internalPullup = false);
|
||||
void setTiming(uint8_t whichButton, uint32_t debounceMs, uint32_t longpressMs);
|
||||
void setJoystickDebounce(uint32_t debounceMs);
|
||||
void setHandlerDown(uint8_t whichButton, Callback onDown);
|
||||
void setHandlerUp(uint8_t whichButton, Callback onUp);
|
||||
void setHandlerShortPress(uint8_t whichButton, Callback onShortPress);
|
||||
void setHandlerLongPress(uint8_t whichButton, Callback onLongPress);
|
||||
void setJoystickDownHandlers(Callback uDown, Callback dDown, Callback ldown, Callback rDown);
|
||||
void setJoystickUpHandlers(Callback uUp, Callback dUp, Callback lUp, Callback rUp);
|
||||
void setJoystickPressHandlers(Callback uPress, Callback dPress, Callback lPress, Callback rPress);
|
||||
// Data used for double-action buttons
|
||||
class Button : public SimpleButton {
|
||||
public:
|
||||
// Per-button extended config
|
||||
bool activeLogic = LOW; // Active LOW by default.
|
||||
uint32_t debounceLength = 50; // Minimum length for shortpress in ms
|
||||
uint32_t longpressLength = 500; // Time until longpress in ms
|
||||
|
||||
// Disconnect and reconnect interrupts for light sleep
|
||||
#ifdef ARCH_ESP32
|
||||
int beforeLightSleep(void *unused);
|
||||
int afterLightSleep(esp_sleep_wakeup_cause_t cause);
|
||||
#endif
|
||||
|
||||
private:
|
||||
// Internal state of a specific button
|
||||
enum State {
|
||||
REST, // Up, no activity
|
||||
IRQ, // Down detected, not yet handled
|
||||
POLLING_UNFIRED, // Down handled, polling for release
|
||||
POLLING_FIRED, // Longpress fired, button still held
|
||||
};
|
||||
|
||||
// Joystick Directions
|
||||
enum Direction { UP = 0, DOWN, LEFT, RIGHT };
|
||||
|
||||
// Data used for direction (single-action) buttons
|
||||
class SimpleButton
|
||||
{
|
||||
public:
|
||||
// Per-button config
|
||||
uint8_t pin = 0xFF; // 0xFF: unset
|
||||
volatile State state = State::REST; // Internal state
|
||||
volatile uint32_t irqAtMillis; // millis() when button went down
|
||||
|
||||
// Per-button event callbacks
|
||||
static void noop(){};
|
||||
std::function<void()> onDown = noop;
|
||||
std::function<void()> onUp = noop;
|
||||
std::function<void()> onPress = noop;
|
||||
};
|
||||
|
||||
// Data used for double-action buttons
|
||||
class Button : public SimpleButton
|
||||
{
|
||||
public:
|
||||
// Per-button extended config
|
||||
bool activeLogic = LOW; // Active LOW by default.
|
||||
uint32_t debounceLength = 50; // Minimum length for shortpress in ms
|
||||
uint32_t longpressLength = 500; // Time until longpress in ms
|
||||
|
||||
// Per-button event callbacks
|
||||
std::function<void()> onLongPress = noop;
|
||||
};
|
||||
// Per-button event callbacks
|
||||
std::function<void()> onLongPress = noop;
|
||||
};
|
||||
|
||||
#ifdef ARCH_ESP32
|
||||
// Get notified when lightsleep begins and ends
|
||||
CallbackObserver<TwoButtonExtended, void *> lsObserver =
|
||||
CallbackObserver<TwoButtonExtended, void *>(this, &TwoButtonExtended::beforeLightSleep);
|
||||
CallbackObserver<TwoButtonExtended, esp_sleep_wakeup_cause_t> lsEndObserver =
|
||||
CallbackObserver<TwoButtonExtended, esp_sleep_wakeup_cause_t>(this, &TwoButtonExtended::afterLightSleep);
|
||||
// Get notified when lightsleep begins and ends
|
||||
CallbackObserver<TwoButtonExtended, void *> lsObserver = CallbackObserver<TwoButtonExtended, void *>(this, &TwoButtonExtended::beforeLightSleep);
|
||||
CallbackObserver<TwoButtonExtended, esp_sleep_wakeup_cause_t> lsEndObserver =
|
||||
CallbackObserver<TwoButtonExtended, esp_sleep_wakeup_cause_t>(this, &TwoButtonExtended::afterLightSleep);
|
||||
#endif
|
||||
|
||||
int32_t runOnce() override; // Timer method. Polls for button release
|
||||
int32_t runOnce() override; // Timer method. Polls for button release
|
||||
|
||||
void startThread(); // Start polling for release
|
||||
void stopThread(); // Stop polling for release
|
||||
void startThread(); // Start polling for release
|
||||
void stopThread(); // Stop polling for release
|
||||
|
||||
static void isrPrimary(); // User Button ISR
|
||||
static void isrSecondary(); // optional aux button or joystick center
|
||||
static void isrJoystickUp();
|
||||
static void isrJoystickDown();
|
||||
static void isrJoystickLeft();
|
||||
static void isrJoystickRight();
|
||||
static void isrPrimary(); // User Button ISR
|
||||
static void isrSecondary(); // optional aux button or joystick center
|
||||
static void isrJoystickUp();
|
||||
static void isrJoystickDown();
|
||||
static void isrJoystickLeft();
|
||||
static void isrJoystickRight();
|
||||
|
||||
TwoButtonExtended(); // Constructor made private: force use of Button::instance()
|
||||
TwoButtonExtended(); // Constructor made private: force use of Button::instance()
|
||||
|
||||
// Info about both buttons
|
||||
Button buttons[2];
|
||||
bool joystickActiveLogic = LOW; // Active LOW by default
|
||||
uint32_t joystickDebounceLength = 50; // time until press in ms
|
||||
SimpleButton joystick[4];
|
||||
// Info about both buttons
|
||||
Button buttons[2];
|
||||
bool joystickActiveLogic = LOW; // Active LOW by default
|
||||
uint32_t joystickDebounceLength = 50; // time until press in ms
|
||||
SimpleButton joystick[4];
|
||||
};
|
||||
|
||||
}; // namespace NicheGraphics::Inputs
|
||||
|
||||
@@ -12,152 +12,142 @@ using namespace NicheGraphics;
|
||||
// Location of the file which stores the canned messages on flash
|
||||
static const char *cannedMessagesConfigFile = "/prefs/cannedConf.proto";
|
||||
|
||||
CannedMessageStore::CannedMessageStore()
|
||||
{
|
||||
CannedMessageStore::CannedMessageStore() {
|
||||
#if !MESHTASTIC_EXCLUDE_ADMIN
|
||||
adminMessageObserver.observe(adminModule);
|
||||
adminMessageObserver.observe(adminModule);
|
||||
#endif
|
||||
|
||||
// Load & parse messages from flash
|
||||
load();
|
||||
// Load & parse messages from flash
|
||||
load();
|
||||
}
|
||||
|
||||
// Get access to (or create) the singleton instance of this class
|
||||
CannedMessageStore *CannedMessageStore::getInstance()
|
||||
{
|
||||
// Instantiate the class the first time this method is called
|
||||
static CannedMessageStore *const singletonInstance = new CannedMessageStore;
|
||||
CannedMessageStore *CannedMessageStore::getInstance() {
|
||||
// Instantiate the class the first time this method is called
|
||||
static CannedMessageStore *const singletonInstance = new CannedMessageStore;
|
||||
|
||||
return singletonInstance;
|
||||
return singletonInstance;
|
||||
}
|
||||
|
||||
// Access canned messages by index
|
||||
// Consumer should check CannedMessageStore::size to avoid accessing out of bounds
|
||||
const std::string &CannedMessageStore::at(uint8_t i)
|
||||
{
|
||||
assert(i < messages.size());
|
||||
return messages.at(i);
|
||||
const std::string &CannedMessageStore::at(uint8_t i) {
|
||||
assert(i < messages.size());
|
||||
return messages.at(i);
|
||||
}
|
||||
|
||||
// Number of canned message strings available
|
||||
uint8_t CannedMessageStore::size()
|
||||
{
|
||||
return messages.size();
|
||||
}
|
||||
uint8_t CannedMessageStore::size() { return messages.size(); }
|
||||
|
||||
// Load canned message data from flash, and parse into the individual strings
|
||||
void CannedMessageStore::load()
|
||||
{
|
||||
// In case we're reloading
|
||||
messages.clear();
|
||||
void CannedMessageStore::load() {
|
||||
// In case we're reloading
|
||||
messages.clear();
|
||||
|
||||
// Attempt to load the bulk canned message data from flash
|
||||
meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig;
|
||||
LoadFileResult result = nodeDB->loadProto("/prefs/cannedConf.proto", meshtastic_CannedMessageModuleConfig_size,
|
||||
sizeof(meshtastic_CannedMessageModuleConfig),
|
||||
&meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig);
|
||||
// Attempt to load the bulk canned message data from flash
|
||||
meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig;
|
||||
LoadFileResult result =
|
||||
nodeDB->loadProto("/prefs/cannedConf.proto", meshtastic_CannedMessageModuleConfig_size, sizeof(meshtastic_CannedMessageModuleConfig),
|
||||
&meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig);
|
||||
|
||||
// Abort if nothing to load
|
||||
if (result != LoadFileResult::LOAD_SUCCESS || strlen(cannedMessageModuleConfig.messages) == 0)
|
||||
return;
|
||||
// Abort if nothing to load
|
||||
if (result != LoadFileResult::LOAD_SUCCESS || strlen(cannedMessageModuleConfig.messages) == 0)
|
||||
return;
|
||||
|
||||
// Split into individual canned messages
|
||||
// These are concatenated when stored in flash, using '|' as a delimiter
|
||||
std::string s;
|
||||
for (char c : cannedMessageModuleConfig.messages) { // Character by character
|
||||
// Split into individual canned messages
|
||||
// These are concatenated when stored in flash, using '|' as a delimiter
|
||||
std::string s;
|
||||
for (char c : cannedMessageModuleConfig.messages) { // Character by character
|
||||
|
||||
// If found end of a string
|
||||
if (c == '|' || c == '\0') {
|
||||
// Copy into the vector (if non-empty)
|
||||
if (!s.empty())
|
||||
messages.push_back(s);
|
||||
// If found end of a string
|
||||
if (c == '|' || c == '\0') {
|
||||
// Copy into the vector (if non-empty)
|
||||
if (!s.empty())
|
||||
messages.push_back(s);
|
||||
|
||||
// Reset the string builder
|
||||
s.clear();
|
||||
// Reset the string builder
|
||||
s.clear();
|
||||
|
||||
// End of data, all strings processed
|
||||
if (c == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
// Otherwise, append char (continue building string)
|
||||
else
|
||||
s.push_back(c);
|
||||
// End of data, all strings processed
|
||||
if (c == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
// Otherwise, append char (continue building string)
|
||||
else
|
||||
s.push_back(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle incoming admin messages
|
||||
// We get these as an observer of AdminModule
|
||||
// It's our responsibility to handle setting and getting of canned messages via the client API
|
||||
// Ordinarily, this would be handled by the CannedMessageModule, but it is bound to Screen.cpp, so not suitable for NicheGraphics
|
||||
int CannedMessageStore::onAdminMessage(AdminModule_ObserverData *data)
|
||||
{
|
||||
switch (data->request->which_payload_variant) {
|
||||
// Ordinarily, this would be handled by the CannedMessageModule, but it is bound to Screen.cpp, so not suitable for
|
||||
// NicheGraphics
|
||||
int CannedMessageStore::onAdminMessage(AdminModule_ObserverData *data) {
|
||||
switch (data->request->which_payload_variant) {
|
||||
|
||||
// Client API changing the canned messages
|
||||
case meshtastic_AdminMessage_set_canned_message_module_messages_tag:
|
||||
handleSet(data->request);
|
||||
*data->result = AdminMessageHandleResult::HANDLED;
|
||||
break;
|
||||
// Client API changing the canned messages
|
||||
case meshtastic_AdminMessage_set_canned_message_module_messages_tag:
|
||||
handleSet(data->request);
|
||||
*data->result = AdminMessageHandleResult::HANDLED;
|
||||
break;
|
||||
|
||||
// Client API wants to know the current canned messages
|
||||
case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag:
|
||||
handleGet(data->response);
|
||||
*data->result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE;
|
||||
break;
|
||||
// Client API wants to know the current canned messages
|
||||
case meshtastic_AdminMessage_get_canned_message_module_messages_request_tag:
|
||||
handleGet(data->response);
|
||||
*data->result = AdminMessageHandleResult::HANDLED_WITH_RESPONSE;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
|
||||
return 0; // Tell caller to continue notifying other observers. (No reason to abort this event)
|
||||
}
|
||||
|
||||
// Client API changing the canned messages
|
||||
void CannedMessageStore::handleSet(const meshtastic_AdminMessage *request)
|
||||
{
|
||||
// Copy into the correct struct (for writing to flash as protobuf)
|
||||
meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig;
|
||||
strncpy(cannedMessageModuleConfig.messages, request->set_canned_message_module_messages,
|
||||
sizeof(cannedMessageModuleConfig.messages));
|
||||
void CannedMessageStore::handleSet(const meshtastic_AdminMessage *request) {
|
||||
// Copy into the correct struct (for writing to flash as protobuf)
|
||||
meshtastic_CannedMessageModuleConfig cannedMessageModuleConfig;
|
||||
strncpy(cannedMessageModuleConfig.messages, request->set_canned_message_module_messages, sizeof(cannedMessageModuleConfig.messages));
|
||||
|
||||
// Ensure the directory exists
|
||||
// Ensure the directory exists
|
||||
#ifdef FSCom
|
||||
spiLock->lock();
|
||||
FSCom.mkdir("/prefs");
|
||||
spiLock->unlock();
|
||||
spiLock->lock();
|
||||
FSCom.mkdir("/prefs");
|
||||
spiLock->unlock();
|
||||
#endif
|
||||
|
||||
// Write to flash
|
||||
nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size,
|
||||
&meshtastic_CannedMessageModuleConfig_msg, &cannedMessageModuleConfig);
|
||||
// Write to flash
|
||||
nodeDB->saveProto(cannedMessagesConfigFile, meshtastic_CannedMessageModuleConfig_size, &meshtastic_CannedMessageModuleConfig_msg,
|
||||
&cannedMessageModuleConfig);
|
||||
|
||||
// Reload from flash, to update the canned messages in RAM
|
||||
// (This is a lazy way to handle it)
|
||||
load();
|
||||
// Reload from flash, to update the canned messages in RAM
|
||||
// (This is a lazy way to handle it)
|
||||
load();
|
||||
}
|
||||
|
||||
// Client API wants to know the current canned messages
|
||||
// We're reconstructing the monolithic canned message string from our copy of the messages in RAM
|
||||
// Lazy, but more convenient that reloading the monolithic string from flash just for this
|
||||
void CannedMessageStore::handleGet(meshtastic_AdminMessage *response)
|
||||
{
|
||||
// Merge the canned messages back into the delimited format expected
|
||||
std::string merged;
|
||||
if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0
|
||||
merged.reserve(201);
|
||||
for (std::string &s : messages) {
|
||||
merged += s;
|
||||
merged += '|';
|
||||
}
|
||||
merged.pop_back(); // Drop the final delimiter (loop added one too many)
|
||||
void CannedMessageStore::handleGet(meshtastic_AdminMessage *response) {
|
||||
// Merge the canned messages back into the delimited format expected
|
||||
std::string merged;
|
||||
if (!messages.empty()) { // Don't run if no messages: error on pop_back with size=0
|
||||
merged.reserve(201);
|
||||
for (std::string &s : messages) {
|
||||
merged += s;
|
||||
merged += '|';
|
||||
}
|
||||
merged.pop_back(); // Drop the final delimiter (loop added one too many)
|
||||
}
|
||||
|
||||
// Place the data into the response
|
||||
// This response is scoped to AdminModule::handleReceivedProtobuf
|
||||
// We were passed reference to it via the observable
|
||||
response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag;
|
||||
strncpy(response->get_canned_message_module_messages_response, merged.c_str(), strlen(merged.c_str()) + 1);
|
||||
// Place the data into the response
|
||||
// This response is scoped to AdminModule::handleReceivedProtobuf
|
||||
// We were passed reference to it via the observable
|
||||
response->which_payload_variant = meshtastic_AdminMessage_get_canned_message_module_messages_response_tag;
|
||||
strncpy(response->get_canned_message_module_messages_response, merged.c_str(), strlen(merged.c_str()) + 1);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -22,31 +22,29 @@ The necessary interaction with the AdminModule is done as an observer.
|
||||
|
||||
#include "modules/AdminModule.h"
|
||||
|
||||
namespace NicheGraphics
|
||||
{
|
||||
namespace NicheGraphics {
|
||||
|
||||
class CannedMessageStore
|
||||
{
|
||||
public:
|
||||
static CannedMessageStore *getInstance(); // Create or get the singleton instance
|
||||
const std::string &at(uint8_t i); // Get canned message at index
|
||||
uint8_t size(); // Get total number of canned messages
|
||||
class CannedMessageStore {
|
||||
public:
|
||||
static CannedMessageStore *getInstance(); // Create or get the singleton instance
|
||||
const std::string &at(uint8_t i); // Get canned message at index
|
||||
uint8_t size(); // Get total number of canned messages
|
||||
|
||||
int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages
|
||||
int onAdminMessage(AdminModule_ObserverData *data); // Handle incoming admin messages
|
||||
|
||||
private:
|
||||
CannedMessageStore(); // Constructor made private: force use of CannedMessageStore::instance()
|
||||
private:
|
||||
CannedMessageStore(); // Constructor made private: force use of CannedMessageStore::instance()
|
||||
|
||||
void load(); // Load from flash, and parse
|
||||
void load(); // Load from flash, and parse
|
||||
|
||||
void handleSet(const meshtastic_AdminMessage *request); // Client API changing the canned messages
|
||||
void handleGet(meshtastic_AdminMessage *response); // Client API wants to know current canned messages
|
||||
void handleSet(const meshtastic_AdminMessage *request); // Client API changing the canned messages
|
||||
void handleGet(meshtastic_AdminMessage *response); // Client API wants to know current canned messages
|
||||
|
||||
std::vector<std::string> messages;
|
||||
std::vector<std::string> messages;
|
||||
|
||||
// Get notified of incoming admin messages, to get / set canned messages
|
||||
CallbackObserver<CannedMessageStore, AdminModule_ObserverData *> adminMessageObserver =
|
||||
CallbackObserver<CannedMessageStore, AdminModule_ObserverData *>(this, &CannedMessageStore::onAdminMessage);
|
||||
// Get notified of incoming admin messages, to get / set canned messages
|
||||
CallbackObserver<CannedMessageStore, AdminModule_ObserverData *> adminMessageObserver =
|
||||
CallbackObserver<CannedMessageStore, AdminModule_ObserverData *>(this, &CannedMessageStore::onAdminMessage);
|
||||
};
|
||||
|
||||
}; // namespace NicheGraphics
|
||||
|
||||
@@ -16,156 +16,149 @@ Avoid bloating everyone's protobuf code for our one-off UI implementations
|
||||
#include "SPILock.h"
|
||||
#include "SafeFile.h"
|
||||
|
||||
namespace NicheGraphics
|
||||
{
|
||||
namespace NicheGraphics {
|
||||
|
||||
template <typename T> class FlashData
|
||||
{
|
||||
private:
|
||||
static std::string getFilename(const char *label)
|
||||
{
|
||||
std::string filename;
|
||||
filename += "/NicheGraphics";
|
||||
filename += "/";
|
||||
filename += label;
|
||||
filename += ".data";
|
||||
template <typename T> class FlashData {
|
||||
private:
|
||||
static std::string getFilename(const char *label) {
|
||||
std::string filename;
|
||||
filename += "/NicheGraphics";
|
||||
filename += "/";
|
||||
filename += label;
|
||||
filename += ".data";
|
||||
|
||||
return filename;
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
|
||||
static uint32_t getHash(T *data)
|
||||
{
|
||||
uint32_t hash = 0;
|
||||
static uint32_t getHash(T *data) {
|
||||
uint32_t hash = 0;
|
||||
|
||||
// Sum all bytes of the image buffer together
|
||||
for (uint32_t i = 0; i < sizeof(T); i++)
|
||||
hash ^= ((uint8_t *)data)[i] + 1;
|
||||
// Sum all bytes of the image buffer together
|
||||
for (uint32_t i = 0; i < sizeof(T); i++)
|
||||
hash ^= ((uint8_t *)data)[i] + 1;
|
||||
|
||||
return hash;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
public:
|
||||
static bool load(T *data, const char *label)
|
||||
{
|
||||
// Take firmware's SPI lock
|
||||
concurrency::LockGuard guard(spiLock);
|
||||
|
||||
// Set false if we run into issues
|
||||
bool okay = true;
|
||||
|
||||
// Get a filename based on the label
|
||||
std::string filename = getFilename(label);
|
||||
|
||||
#ifdef FSCom
|
||||
|
||||
// Check that the file *does* actually exist
|
||||
if (!FSCom.exists(filename.c_str())) {
|
||||
LOG_WARN("'%s' not found. Using default values", filename.c_str());
|
||||
okay = false;
|
||||
return okay;
|
||||
}
|
||||
|
||||
// Open the file
|
||||
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
|
||||
|
||||
// If opened, start reading
|
||||
if (f) {
|
||||
LOG_INFO("Loading NicheGraphics data '%s'", filename.c_str());
|
||||
|
||||
// Create an object which will received data from flash
|
||||
// We read here first, so we can verify the checksum, without committing to overwriting the *data object
|
||||
// Allows us to retain any defaults that might be set after we declared *data, but before loading settings,
|
||||
// in case the flash values are corrupt
|
||||
T flashData;
|
||||
|
||||
// Read the actual data
|
||||
f.readBytes((char *)&flashData, sizeof(T));
|
||||
|
||||
// Read the hash
|
||||
uint32_t savedHash = 0;
|
||||
f.readBytes((char *)&savedHash, sizeof(savedHash));
|
||||
|
||||
// Calculate hash of the loaded data, then compare with the saved hash
|
||||
// If hash looks good, copy the values to the main data object
|
||||
uint32_t calculatedHash = getHash(&flashData);
|
||||
if (savedHash != calculatedHash) {
|
||||
LOG_WARN("'%s' is corrupt (hash mismatch). Using default values", filename.c_str());
|
||||
okay = false;
|
||||
} else
|
||||
*data = flashData;
|
||||
|
||||
f.close();
|
||||
} else {
|
||||
LOG_ERROR("Could not open / read %s", filename.c_str());
|
||||
okay = false;
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("Filesystem not implemented");
|
||||
state = LoadFileState::NO_FILESYSTEM;
|
||||
okay = false;
|
||||
#endif
|
||||
return okay;
|
||||
}
|
||||
|
||||
// Save module's custom data (settings?) to flash. Doesn't use protobufs
|
||||
// Takes the firmware's SPI lock, in case the files are stored on SD card
|
||||
// Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally.
|
||||
static void save(T *data, const char *label)
|
||||
{
|
||||
// Get a filename based on the label
|
||||
std::string filename = getFilename(label);
|
||||
|
||||
#ifdef FSCom
|
||||
spiLock->lock();
|
||||
FSCom.mkdir("/NicheGraphics");
|
||||
spiLock->unlock();
|
||||
|
||||
auto f = SafeFile(filename.c_str(), true); // "true": full atomic. Write new data to temp file, then rename.
|
||||
|
||||
LOG_INFO("Saving %s", filename.c_str());
|
||||
|
||||
// Calculate a hash of the data
|
||||
uint32_t hash = getHash(data);
|
||||
|
||||
spiLock->lock();
|
||||
f.write((uint8_t *)data, sizeof(T)); // Write the actual data
|
||||
f.write((uint8_t *)&hash, sizeof(hash)); // Append the hash
|
||||
spiLock->unlock();
|
||||
|
||||
bool writeSucceeded = f.close();
|
||||
|
||||
if (!writeSucceeded) {
|
||||
LOG_ERROR("Can't write data!");
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("ERROR: Filesystem not implemented\n");
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
// Erase contents of the NicheGraphics data directory
|
||||
inline void clearFlashData()
|
||||
{
|
||||
|
||||
// Take firmware's SPI lock, in case the files are stored on SD card
|
||||
public:
|
||||
static bool load(T *data, const char *label) {
|
||||
// Take firmware's SPI lock
|
||||
concurrency::LockGuard guard(spiLock);
|
||||
|
||||
// Set false if we run into issues
|
||||
bool okay = true;
|
||||
|
||||
// Get a filename based on the label
|
||||
std::string filename = getFilename(label);
|
||||
|
||||
#ifdef FSCom
|
||||
File dir = FSCom.open("/NicheGraphics"); // Open the directory
|
||||
File file = dir.openNextFile(); // Attempt to open the first file in the directory
|
||||
|
||||
// While the directory still contains files
|
||||
while (file) {
|
||||
std::string path = "/NicheGraphics/";
|
||||
path += file.name();
|
||||
LOG_DEBUG("Erasing %s", path.c_str());
|
||||
file.close();
|
||||
FSCom.remove(path.c_str());
|
||||
// Check that the file *does* actually exist
|
||||
if (!FSCom.exists(filename.c_str())) {
|
||||
LOG_WARN("'%s' not found. Using default values", filename.c_str());
|
||||
okay = false;
|
||||
return okay;
|
||||
}
|
||||
|
||||
file = dir.openNextFile();
|
||||
// Open the file
|
||||
auto f = FSCom.open(filename.c_str(), FILE_O_READ);
|
||||
|
||||
// If opened, start reading
|
||||
if (f) {
|
||||
LOG_INFO("Loading NicheGraphics data '%s'", filename.c_str());
|
||||
|
||||
// Create an object which will received data from flash
|
||||
// We read here first, so we can verify the checksum, without committing to overwriting the *data object
|
||||
// Allows us to retain any defaults that might be set after we declared *data, but before loading settings,
|
||||
// in case the flash values are corrupt
|
||||
T flashData;
|
||||
|
||||
// Read the actual data
|
||||
f.readBytes((char *)&flashData, sizeof(T));
|
||||
|
||||
// Read the hash
|
||||
uint32_t savedHash = 0;
|
||||
f.readBytes((char *)&savedHash, sizeof(savedHash));
|
||||
|
||||
// Calculate hash of the loaded data, then compare with the saved hash
|
||||
// If hash looks good, copy the values to the main data object
|
||||
uint32_t calculatedHash = getHash(&flashData);
|
||||
if (savedHash != calculatedHash) {
|
||||
LOG_WARN("'%s' is corrupt (hash mismatch). Using default values", filename.c_str());
|
||||
okay = false;
|
||||
} else
|
||||
*data = flashData;
|
||||
|
||||
f.close();
|
||||
} else {
|
||||
LOG_ERROR("Could not open / read %s", filename.c_str());
|
||||
okay = false;
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("Filesystem not implemented");
|
||||
state = LoadFileState::NO_FILESYSTEM;
|
||||
okay = false;
|
||||
#endif
|
||||
return okay;
|
||||
}
|
||||
|
||||
// Save module's custom data (settings?) to flash. Doesn't use protobufs
|
||||
// Takes the firmware's SPI lock, in case the files are stored on SD card
|
||||
// Need to lock and unlock around specific FS methods, as the SafeFile class takes the lock for itself internally.
|
||||
static void save(T *data, const char *label) {
|
||||
// Get a filename based on the label
|
||||
std::string filename = getFilename(label);
|
||||
|
||||
#ifdef FSCom
|
||||
spiLock->lock();
|
||||
FSCom.mkdir("/NicheGraphics");
|
||||
spiLock->unlock();
|
||||
|
||||
auto f = SafeFile(filename.c_str(), true); // "true": full atomic. Write new data to temp file, then rename.
|
||||
|
||||
LOG_INFO("Saving %s", filename.c_str());
|
||||
|
||||
// Calculate a hash of the data
|
||||
uint32_t hash = getHash(data);
|
||||
|
||||
spiLock->lock();
|
||||
f.write((uint8_t *)data, sizeof(T)); // Write the actual data
|
||||
f.write((uint8_t *)&hash, sizeof(hash)); // Append the hash
|
||||
spiLock->unlock();
|
||||
|
||||
bool writeSucceeded = f.close();
|
||||
|
||||
if (!writeSucceeded) {
|
||||
LOG_ERROR("Can't write data!");
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("ERROR: Filesystem not implemented\n");
|
||||
#endif
|
||||
}
|
||||
};
|
||||
|
||||
// Erase contents of the NicheGraphics data directory
|
||||
inline void clearFlashData() {
|
||||
|
||||
// Take firmware's SPI lock, in case the files are stored on SD card
|
||||
concurrency::LockGuard guard(spiLock);
|
||||
|
||||
#ifdef FSCom
|
||||
File dir = FSCom.open("/NicheGraphics"); // Open the directory
|
||||
File file = dir.openNextFile(); // Attempt to open the first file in the directory
|
||||
|
||||
// While the directory still contains files
|
||||
while (file) {
|
||||
std::string path = "/NicheGraphics/";
|
||||
path += file.name();
|
||||
LOG_DEBUG("Erasing %s", path.c_str());
|
||||
file.close();
|
||||
FSCom.remove(path.c_str());
|
||||
|
||||
file = dir.openNextFile();
|
||||
}
|
||||
#else
|
||||
LOG_ERROR("ERROR: Filesystem not implemented\n");
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user