mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 13:22:22 +00:00
feat(display): add runtime display.flip for panel orientation
`set display.flip 0|1` (also off/on) turns the panel 180 degrees from its compiled DISPLAY_ROTATION, persisted in MQTTPrefs alongside display.timeout and applied live without a reboot. Adding 2 to the compiled rotation rather than setting an absolute value keeps portrait portrait and landscape landscape, so the DisplayViewport geometry never changes with it and the setting cannot produce a nonsensical mix. DisplayDriver gains a defaulted no-op setFlipped(), so no other display driver is affected. The compiled rotation was verified identical across `pio run` and `build.sh` on two machines (movi a11, 2 at the setRotation call site), yet the panel read upside down for one tester and upright for another - which is what a board mounted either way up looks like. No single compiled constant satisfies both, so orientation becomes a setting rather than another rebuild. Runtime-only, like display_timeout_secs: LegacyV1MQTTPrefs and the frozen binary payload sizes are untouched, and the JSON group is an append that older firmware skips.
This commit is contained in:
1 parent
86c4849e55
commit
f3c81b559b
12 files changed
+120
-9
No files matched your search
@@ -32,6 +32,23 @@ static inline bool millisReached(unsigned long now, unsigned long deadline) {
|
||||
return (int32_t)((uint32_t)now - (uint32_t)deadline) >= 0;
|
||||
}
|
||||
|
||||
// Applies `display.flip` when it changes, forcing a complete repaint because
|
||||
// the panel's existing contents are now the wrong way up.
|
||||
void UITask::applyDisplayFlip() {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (_observer_prefs == NULL || _observer_prefs->display_flip == _flip_seen) return;
|
||||
_flip_seen = _observer_prefs->display_flip;
|
||||
_display->setFlipped(_flip_seen != 0);
|
||||
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
||||
_frame_valid = false;
|
||||
#endif
|
||||
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
||||
_rows_valid = false;
|
||||
#endif
|
||||
_next_refresh = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// `display.timeout` when the observer prefs are available, otherwise the
|
||||
// compiled-in default. Read on every use so a `set display.timeout` takes
|
||||
// effect immediately.
|
||||
@@ -72,6 +89,7 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi
|
||||
ObserverDashboard::applyDarkPalette(); // retunes UIColor for this target only
|
||||
#endif
|
||||
_display->turnOn();
|
||||
applyDisplayFlip();
|
||||
#ifdef DISPLAY_TOUCH_TOGGLE
|
||||
_touch.begin();
|
||||
#endif
|
||||
@@ -407,6 +425,7 @@ void UITask::loop() {
|
||||
// `_auto_off` is only armed on activity, so a timeout changed at runtime has
|
||||
// to restart the countdown here - otherwise 0 -> 60 blanks instantly off a
|
||||
// boot-time deadline, and 60 -> 3600 still blanks at the old 60 s mark.
|
||||
applyDisplayFlip();
|
||||
unsigned long timeout = displayTimeoutMillis();
|
||||
if (timeout != _timeout_seen) {
|
||||
_timeout_seen = timeout;
|
||||
|
||||
@@ -55,6 +55,8 @@ class UITask {
|
||||
MQTTPrefs* _observer_prefs = NULL;
|
||||
#endif
|
||||
unsigned long _timeout_seen = 0; // to notice a live `display.timeout` change
|
||||
uint8_t _flip_seen = 0xFF; // 0xFF forces the first apply
|
||||
void applyDisplayFlip();
|
||||
|
||||
unsigned long displayTimeoutMillis() const;
|
||||
|
||||
|
||||
@@ -31,6 +31,23 @@ static inline bool millisReached(unsigned long now, unsigned long deadline) {
|
||||
return (int32_t)((uint32_t)now - (uint32_t)deadline) >= 0;
|
||||
}
|
||||
|
||||
// Applies `display.flip` when it changes, forcing a complete repaint because
|
||||
// the panel's existing contents are now the wrong way up.
|
||||
void UITask::applyDisplayFlip() {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (_observer_prefs == NULL || _observer_prefs->display_flip == _flip_seen) return;
|
||||
_flip_seen = _observer_prefs->display_flip;
|
||||
_display->setFlipped(_flip_seen != 0);
|
||||
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
||||
_frame_valid = false;
|
||||
#endif
|
||||
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
||||
_rows_valid = false;
|
||||
#endif
|
||||
_next_refresh = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
// `display.timeout` when the observer prefs are available, otherwise the
|
||||
// compiled-in default. Read on every use so a `set display.timeout` takes
|
||||
// effect immediately.
|
||||
@@ -68,6 +85,7 @@ void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* fi
|
||||
ObserverDashboard::applyDarkPalette(); // retunes UIColor for this target only
|
||||
#endif
|
||||
_display->turnOn();
|
||||
applyDisplayFlip();
|
||||
#ifdef DISPLAY_TOUCH_TOGGLE
|
||||
_touch.begin();
|
||||
#endif
|
||||
@@ -386,6 +404,7 @@ void UITask::loop() {
|
||||
// `_auto_off` is only armed on activity, so a timeout changed at runtime has
|
||||
// to restart the countdown here - otherwise 0 -> 60 blanks instantly off a
|
||||
// boot-time deadline, and 60 -> 3600 still blanks at the old 60 s mark.
|
||||
applyDisplayFlip();
|
||||
unsigned long timeout = displayTimeoutMillis();
|
||||
if (timeout != _timeout_seen) {
|
||||
_timeout_seen = timeout;
|
||||
|
||||
@@ -52,6 +52,8 @@ class UITask {
|
||||
MQTTPrefs* _observer_prefs = NULL;
|
||||
#endif
|
||||
unsigned long _timeout_seen = 0; // to notice a live `display.timeout` change
|
||||
uint8_t _flip_seen = 0xFF; // 0xFF forces the first apply
|
||||
void applyDisplayFlip();
|
||||
|
||||
unsigned long displayTimeoutMillis() const;
|
||||
|
||||
|
||||
@@ -266,6 +266,18 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (memcmp(config, "display.flip ", 13) == 0) {
|
||||
const char* val = &config[13];
|
||||
if (strcmp(val, "0") == 0 || strcmp(val, "off") == 0) {
|
||||
_mqtt_prefs.display_flip = 0;
|
||||
} else if (strcmp(val, "1") == 0 || strcmp(val, "on") == 0) {
|
||||
_mqtt_prefs.display_flip = 1;
|
||||
} else {
|
||||
strcpy(reply, "Error: display.flip must be 0/1 (or off/on)");
|
||||
return true;
|
||||
}
|
||||
if (!persistObserverPrefs(reply)) return true;
|
||||
strcpy(reply, _mqtt_prefs.display_flip ? "OK - display rotated 180" : "OK - display upright");
|
||||
} else if (memcmp(config, "display.timeout ", 16) == 0) {
|
||||
const char* val = &config[16];
|
||||
bool all_digits = (*val != '\0');
|
||||
@@ -887,6 +899,8 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
|
||||
sprintf(reply, "> %d", (uint32_t)_mqtt_prefs.radio_watchdog_minutes);
|
||||
} else if (memcmp(config, "display.timeout", 15) == 0) {
|
||||
sprintf(reply, "> %d", (uint32_t)_mqtt_prefs.display_timeout_secs);
|
||||
} else if (memcmp(config, "display.flip", 12) == 0) {
|
||||
strcpy(reply, _mqtt_prefs.display_flip ? "> on" : "> off");
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
} else if (memcmp(config, "mqtt.origin", 11) == 0) {
|
||||
char effective_origin[32];
|
||||
|
||||
@@ -111,6 +111,7 @@ static inline void applyMQTTDefaults(MQTTPrefs* prefs) {
|
||||
prefs->mqtt_neighbors_interval = MQTT_NEIGHBORS_DEFAULT_INTERVAL_MS;
|
||||
|
||||
prefs->display_timeout_secs = DISPLAY_TIMEOUT_DEFAULT_SECS;
|
||||
prefs->display_flip = 0;
|
||||
}
|
||||
|
||||
#endif // WITH_MQTT_BRIDGE
|
||||
@@ -368,19 +368,24 @@ class MQTTPrefsSerializer : public ConfigSerializer {
|
||||
|
||||
class DisplayPrefs : public ConfigSerializer {
|
||||
MQTTPrefs* _prefs;
|
||||
int32_t _timeout_s;
|
||||
bool _seen_timeout = false;
|
||||
int32_t _timeout_s, _flip;
|
||||
bool _seen_timeout = false, _seen_flip = false;
|
||||
protected:
|
||||
void structure() override { defStrict("timeout_s", _timeout_s, _seen_timeout); }
|
||||
void structure() override {
|
||||
defStrict("timeout_s", _timeout_s, _seen_timeout);
|
||||
defStrict("flip", _flip, _seen_flip);
|
||||
}
|
||||
public:
|
||||
explicit DisplayPrefs(MQTTPrefs* prefs)
|
||||
: _prefs(prefs), _timeout_s(prefs->display_timeout_secs) {}
|
||||
: _prefs(prefs), _timeout_s(prefs->display_timeout_secs), _flip(prefs->display_flip) {}
|
||||
void apply(bool* repaired) {
|
||||
if (_timeout_s < 0 || _timeout_s > DISPLAY_TIMEOUT_MAX_SECS) {
|
||||
_timeout_s = DISPLAY_TIMEOUT_DEFAULT_SECS;
|
||||
*repaired = true;
|
||||
}
|
||||
if (_flip < 0 || _flip > 1) { _flip = 0; *repaired = true; }
|
||||
_prefs->display_timeout_secs = static_cast<uint16_t>(_timeout_s);
|
||||
_prefs->display_flip = static_cast<uint8_t>(_flip);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -122,6 +122,10 @@ struct MQTTPrefs {
|
||||
// only - deliberately absent from LegacyV1MQTTPrefs, so the frozen binary
|
||||
// layout and its four payload sizes are unchanged.
|
||||
uint16_t display_timeout_secs;
|
||||
|
||||
// Rotate the panel 180 degrees from its compiled DISPLAY_ROTATION, for boards
|
||||
// mounted the other way up. Runtime only, like display_timeout_secs.
|
||||
uint8_t display_flip;
|
||||
};
|
||||
|
||||
static const uint16_t DISPLAY_TIMEOUT_DEFAULT_SECS = 60;
|
||||
|
||||
@@ -27,6 +27,8 @@ public:
|
||||
virtual void turnOff() = 0;
|
||||
virtual void clear() = 0;
|
||||
virtual void startFrame(ColorVal bkg = UIColor::window_bkg) = 0;
|
||||
// Rotate 180 degrees from the compiled orientation. No-op where unsupported.
|
||||
virtual void setFlipped(bool flipped) { }
|
||||
virtual void setTextSize(int sz) = 0;
|
||||
virtual void setColor(ColorVal c) = 0;
|
||||
virtual void setCursor(int x, int y) = 0;
|
||||
|
||||
@@ -33,6 +33,19 @@ static DisplayViewport::Geometry portraitViewport(int16_t physical_width, int16_
|
||||
}
|
||||
#endif
|
||||
|
||||
// The compiled orientation, optionally turned 180 degrees by `display.flip`.
|
||||
// Adding 2 keeps portrait portrait and landscape landscape, so the viewport
|
||||
// geometry below never has to change with it.
|
||||
uint8_t ST7789LCDDisplay::effectiveRotation() const {
|
||||
return (uint8_t)((DISPLAY_ROTATION + (_flipped ? 2 : 0)) & 3);
|
||||
}
|
||||
|
||||
void ST7789LCDDisplay::setFlipped(bool flipped) {
|
||||
if (_flipped == flipped) return;
|
||||
_flipped = flipped;
|
||||
if (_panel_ready) display.setRotation(effectiveRotation());
|
||||
}
|
||||
|
||||
bool ST7789LCDDisplay::i2c_probe(TwoWire& wire, uint8_t addr) {
|
||||
return true;
|
||||
}
|
||||
@@ -88,7 +101,7 @@ bool ST7789LCDDisplay::begin() {
|
||||
#endif
|
||||
|
||||
display.init(DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
||||
display.setRotation(DISPLAY_ROTATION);
|
||||
display.setRotation(effectiveRotation());
|
||||
|
||||
display.setSPISpeed(40e6);
|
||||
|
||||
@@ -107,9 +120,9 @@ bool ST7789LCDDisplay::begin() {
|
||||
if (PIN_TFT_LEDA_CTL != -1) {
|
||||
digitalWrite(PIN_TFT_LEDA_CTL, PIN_TFT_LEDA_CTL_ACTIVE);
|
||||
}
|
||||
_panel_ready = true;
|
||||
#endif
|
||||
|
||||
_panel_ready = true;
|
||||
_isOn = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,8 @@ class ST7789LCDDisplay : public DisplayDriver {
|
||||
#endif
|
||||
Adafruit_ST7789 display;
|
||||
bool _isOn;
|
||||
#ifdef HELTEC_V4_R8_TFT
|
||||
bool _panel_ready = false; // panel configured once; wake is backlight-only
|
||||
#endif
|
||||
bool _panel_ready = false; // panel has been configured at least once
|
||||
bool _flipped = false;
|
||||
uint16_t _color;
|
||||
RefCountedDigitalPin* _peripher_power;
|
||||
|
||||
@@ -27,6 +26,7 @@ class ST7789LCDDisplay : public DisplayDriver {
|
||||
void printFitted(const char* str, uint16_t available_width);
|
||||
#endif
|
||||
|
||||
uint8_t effectiveRotation() const;
|
||||
bool i2c_probe(TwoWire& wire, uint8_t addr);
|
||||
public:
|
||||
#ifdef USE_PIN_TFT
|
||||
@@ -59,6 +59,7 @@ public:
|
||||
void turnOff() override;
|
||||
void clear() override;
|
||||
void startFrame(ColorVal bkg = UIColor::window_bkg) override;
|
||||
void setFlipped(bool flipped) override;
|
||||
void setTextSize(int sz) override;
|
||||
void setColor(ColorVal c) override;
|
||||
void setCursor(int x, int y) override;
|
||||
|
||||
@@ -79,6 +79,7 @@ static MQTTPrefs defaults() {
|
||||
prefs.alert_min_interval_min = 60;
|
||||
prefs.mqtt_neighbors_interval = MQTT_NEIGHBORS_DEFAULT_INTERVAL_MS;
|
||||
prefs.display_timeout_secs = DISPLAY_TIMEOUT_DEFAULT_SECS;
|
||||
prefs.display_flip = 0;
|
||||
strcpy(prefs.snmp_community, "public");
|
||||
for (int i = 0; i < MQTT_PREFS_SLOT_COUNT; ++i) {
|
||||
strcpy(prefs.mqtt_slot_preset[i], "none");
|
||||
@@ -396,6 +397,34 @@ TEST(MQTTPrefsSerializer, DisplayTimeoutRoundTrips) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(MQTTPrefsSerializer, DisplayFlipRoundTripsAndRepairs) {
|
||||
MQTTPrefs source = defaults();
|
||||
EXPECT_EQ(0, source.display_flip);
|
||||
source.display_flip = 1;
|
||||
|
||||
OutputStream output;
|
||||
MQTTPrefsSerializer writer(&source);
|
||||
ASSERT_TRUE(writer.saveSerial(output));
|
||||
|
||||
MQTTPrefs loaded = defaults();
|
||||
InputStream input(output.text());
|
||||
MQTTPrefsSerializer reader(&loaded);
|
||||
ASSERT_TRUE(reader.loadSerial(input));
|
||||
bool repaired = false;
|
||||
ASSERT_TRUE(reader.apply(&repaired));
|
||||
EXPECT_FALSE(repaired);
|
||||
EXPECT_EQ(1, loaded.display_flip);
|
||||
|
||||
MQTTPrefs prefs = defaults();
|
||||
InputStream bogus("{version:1,display:{flip:7}}");
|
||||
MQTTPrefsSerializer bogus_serializer(&prefs);
|
||||
ASSERT_TRUE(bogus_serializer.loadSerial(bogus));
|
||||
repaired = false;
|
||||
ASSERT_TRUE(bogus_serializer.apply(&repaired));
|
||||
EXPECT_TRUE(repaired);
|
||||
EXPECT_EQ(0, prefs.display_flip);
|
||||
}
|
||||
|
||||
TEST(MQTTPrefsSerializer, RepairsDisplayTimeoutOutOfRange) {
|
||||
MQTTPrefs prefs = defaults();
|
||||
InputStream input("{version:1,display:{timeout_s:99999}}");
|
||||
|
||||
Reference in new issue
Block a user