mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-26 17:38:16 +00:00
Four defects found by hardware testing of the Expansion Kit V2. Touch never registered. The panel's controller does not use the point-count encoding the reference CHSC6X drivers document: byte 0 reads 0x00 idle and 0x1F while a finger is down, so testing for a count of 1 never fired. A partly-failed read leaves 0xFF, which must not count as a press either, so the test is now != 0x00 && != 0xFF. Touch polling could stall the UI loop for ~1 s at a time. The controller NACKs its address whenever it has nothing to report, and calling requestFrom() unconditionally logged a bus error on every 50 ms poll and, once the bus wedged, burned a full ESP_ERR_TIMEOUT inside loop(). Probe the address first, which reports the same NACK quietly, and bound the read with setTimeOut(). The display could not be woken once it blanked; only RST brought it back. turnOn() re-ran the whole display.init(), which re-enters SPI setup, spends ~500 ms in Adafruit's reset delays and pulses GPIO 21 - the line shared with TP_RST, so it reset the touch controller on every wake. Since turnOff() no longer parks that line low, the panel stays configured while dark and waking is just the backlight. Toggling also clears the refresh deadline so the current frame is drawn immediately instead of the stale one. Power-off rebooted instead of staying off. powerOff() went through enterDeepSleep(), which always arms an ext1 wake on P_LORA_DIO_1; a deep-sleep wake is a full reboot, so a node in live traffic restarted within seconds of showing "Turning OFF". It now disables every wake source, so the node stays down until RST or a power cycle. Note that the display off/on cycle had never been exercised on this board before: observer builds pinned AUTO_OFF_MILLIS=0, so the panel never blanked until display.timeout made it a runtime setting.
405 lines
13 KiB
C++
405 lines
13 KiB
C++
#include "UITask.h"
|
|
#include <Arduino.h>
|
|
#include <helpers/CommonCLI.h>
|
|
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
#include <helpers/ui/DisplayFrameSignature.h>
|
|
#endif
|
|
|
|
#ifndef USER_BTN_PRESSED
|
|
#define USER_BTN_PRESSED LOW
|
|
#endif
|
|
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
#include <WiFi.h>
|
|
#include <helpers/esp32/WebConfigServer.h> // defines WITH_WEBCONFIG on ESP32
|
|
#endif
|
|
|
|
#ifndef AUTO_OFF_MILLIS
|
|
#define AUTO_OFF_MILLIS 20000 // 20 seconds; 0 keeps the screen on
|
|
#endif
|
|
|
|
#ifdef DISPLAY_TOUCH_TOGGLE
|
|
#define TOUCH_POLL_MILLIS 50
|
|
#endif
|
|
|
|
// Wrap-safe deadline test. `millis() >= deadline` fires early for the whole
|
|
// interval before a rollover, because the deadline has already wrapped to a
|
|
// small value while millis() is still near UINT32_MAX; the signed difference
|
|
// stays correct across it.
|
|
static inline bool millisReached(unsigned long now, unsigned long deadline) {
|
|
return (int32_t)((uint32_t)now - (uint32_t)deadline) >= 0;
|
|
}
|
|
|
|
// `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.
|
|
unsigned long UITask::displayTimeoutMillis() const {
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
if (_observer_prefs) return (unsigned long)_observer_prefs->display_timeout_secs * 1000UL;
|
|
#endif
|
|
return AUTO_OFF_MILLIS;
|
|
}
|
|
#define BOOT_SCREEN_MILLIS 4000 // 4 seconds
|
|
|
|
// 'meshcore', 128x13px
|
|
static const uint8_t meshcore_logo [] PROGMEM = {
|
|
0x3c, 0x01, 0xe3, 0xff, 0xc7, 0xff, 0x8f, 0x03, 0x87, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe,
|
|
0x3c, 0x03, 0xe3, 0xff, 0xc7, 0xff, 0x8e, 0x03, 0x8f, 0xfe, 0x3f, 0xfe, 0x1f, 0xff, 0x1f, 0xfe,
|
|
0x3e, 0x03, 0xc3, 0xff, 0x8f, 0xff, 0x0e, 0x07, 0x8f, 0xfe, 0x7f, 0xfe, 0x1f, 0xff, 0x1f, 0xfc,
|
|
0x3e, 0x07, 0xc7, 0x80, 0x0e, 0x00, 0x0e, 0x07, 0x9e, 0x00, 0x78, 0x0e, 0x3c, 0x0f, 0x1c, 0x00,
|
|
0x3e, 0x0f, 0xc7, 0x80, 0x1e, 0x00, 0x0e, 0x07, 0x1e, 0x00, 0x70, 0x0e, 0x38, 0x0f, 0x3c, 0x00,
|
|
0x7f, 0x0f, 0xc7, 0xfe, 0x1f, 0xfc, 0x1f, 0xff, 0x1c, 0x00, 0x70, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
|
|
0x7f, 0x1f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x0e, 0x38, 0x0e, 0x3f, 0xf8,
|
|
0x7f, 0x3f, 0xc7, 0xfe, 0x0f, 0xff, 0x1f, 0xff, 0x1c, 0x00, 0xf0, 0x1e, 0x3f, 0xfe, 0x3f, 0xf0,
|
|
0x77, 0x3b, 0x87, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xfc, 0x38, 0x00,
|
|
0x77, 0xfb, 0x8f, 0x00, 0x00, 0x07, 0x1c, 0x0f, 0x3c, 0x00, 0xe0, 0x1c, 0x7f, 0xf8, 0x38, 0x00,
|
|
0x73, 0xf3, 0x8f, 0xff, 0x0f, 0xff, 0x1c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x78, 0x7f, 0xf8,
|
|
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfe, 0x3c, 0x0e, 0x3f, 0xf8, 0xff, 0xfc, 0x70, 0x3c, 0x7f, 0xf8,
|
|
0xe3, 0xe3, 0x8f, 0xff, 0x1f, 0xfc, 0x3c, 0x0e, 0x1f, 0xf8, 0xff, 0xf8, 0x70, 0x3c, 0x7f, 0xf8,
|
|
};
|
|
|
|
void UITask::begin(NodePrefs* node_prefs, const char* build_date, const char* firmware_version) {
|
|
_prevBtnState = HIGH;
|
|
_timeout_seen = displayTimeoutMillis();
|
|
_auto_off = millis() + displayTimeoutMillis();
|
|
_node_prefs = node_prefs;
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
ObserverDashboard::applyDarkPalette(); // retunes UIColor for this target only
|
|
#endif
|
|
_display->turnOn();
|
|
#ifdef DISPLAY_TOUCH_TOGGLE
|
|
_touch.begin();
|
|
#endif
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#endif
|
|
|
|
// strip off dash and commit hash by changing dash to null terminator
|
|
// e.g: v1.2.3-abcdef -> v1.2.3
|
|
char *version = strdup(firmware_version);
|
|
char *dash = strchr(version, '-');
|
|
if(dash){
|
|
*dash = 0;
|
|
}
|
|
|
|
// v1.2.3 (1 Jan 2025)
|
|
snprintf(_version_info, sizeof(_version_info), "%s (%s)", version, build_date);
|
|
free(version);
|
|
}
|
|
|
|
void UITask::renderCurrScreen() {
|
|
char tmp[80];
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
if (millis() < BOOT_SCREEN_MILLIS) { // boot screen
|
|
// meshcore logo
|
|
_display->setColor(UIColor::corp_blue);
|
|
int logoWidth = 128;
|
|
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13);
|
|
|
|
// meshcore website
|
|
const char* website = "https://meshcore.io";
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->setTextSize(1);
|
|
uint16_t websiteWidth = _display->getTextWidth(website);
|
|
_display->setCursor((_display->width() - websiteWidth) / 2, 22);
|
|
_display->print(website);
|
|
|
|
// version info
|
|
_display->setTextSize(1);
|
|
uint16_t versionWidth = _display->getTextWidth(_version_info);
|
|
_display->setCursor((_display->width() - versionWidth) / 2, 35);
|
|
_display->print(_version_info);
|
|
|
|
// node type
|
|
const char* node_type = "< Room Server >";
|
|
uint16_t typeWidth = _display->getTextWidth(node_type);
|
|
_display->setCursor((_display->width() - typeWidth) / 2, 48);
|
|
_display->print(node_type);
|
|
} else { // home screen
|
|
#ifdef WITH_WEBCONFIG
|
|
if (WebConfigServer::isRebootPending()) {
|
|
// save confirmed on-device: show ground truth even if the browser
|
|
// lost its connection before the confirmation reached it
|
|
_display->setTextSize(1);
|
|
_display->setColor(UIColor::corp_blue);
|
|
_display->setCursor(0, 14);
|
|
_display->print("Config saved!");
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->setCursor(0, 30);
|
|
_display->print("Rebooting...");
|
|
return;
|
|
}
|
|
char wc_ssid[33], wc_ip[16];
|
|
if (WebConfigServer::getSetupInfo(wc_ssid, sizeof(wc_ssid), wc_ip, sizeof(wc_ip))) {
|
|
// setup portal active: show join instructions instead of the home screen
|
|
_display->setTextSize(1);
|
|
_display->setColor(UIColor::corp_blue);
|
|
_display->setCursor(0, 0);
|
|
_display->print("Observer WiFi Setup");
|
|
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->setCursor(0, 14);
|
|
_display->print("Join WiFi:");
|
|
_display->setColor(UIColor::warning_txt);
|
|
_display->setCursor(6, 24);
|
|
_display->print(wc_ssid);
|
|
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->setCursor(0, 40);
|
|
_display->print("Then browse to:");
|
|
_display->setColor(UIColor::warning_txt);
|
|
_display->setCursor(6, 50);
|
|
_display->print(wc_ip);
|
|
return;
|
|
}
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
renderDashboard();
|
|
return;
|
|
#endif
|
|
// node name
|
|
_display->setCursor(0, 0);
|
|
_display->setTextSize(1);
|
|
_display->setColor(UIColor::primary_txt);
|
|
_display->print(_node_prefs->node_name);
|
|
|
|
// freq / sf
|
|
_display->setCursor(0, 20);
|
|
sprintf(tmp, "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
|
|
_display->print(tmp);
|
|
|
|
// bw / cr
|
|
_display->setCursor(0, 30);
|
|
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
|
|
_display->print(tmp);
|
|
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
// Display IP address for MQTT bridge devices
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
IPAddress ip = WiFi.localIP();
|
|
_display->setCursor(0, 40);
|
|
_display->setColor(UIColor::primary_txt);
|
|
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
|
_display->print(tmp);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
uint32_t UITask::getFrameSignature() {
|
|
uint32_t signature = DisplayFrameSignature::INITIAL;
|
|
char tmp[80];
|
|
|
|
if (millis() < BOOT_SCREEN_MILLIS) {
|
|
signature = DisplayFrameSignature::append(signature, "boot");
|
|
return DisplayFrameSignature::append(signature, _version_info);
|
|
}
|
|
|
|
#ifdef WITH_WEBCONFIG
|
|
if (WebConfigServer::isRebootPending()) {
|
|
return DisplayFrameSignature::append(signature, "rebooting");
|
|
}
|
|
|
|
char wc_ssid[33], wc_ip[16];
|
|
if (WebConfigServer::getSetupInfo(wc_ssid, sizeof(wc_ssid), wc_ip, sizeof(wc_ip))) {
|
|
signature = DisplayFrameSignature::append(signature, "setup");
|
|
signature = DisplayFrameSignature::append(signature, wc_ssid);
|
|
return DisplayFrameSignature::append(signature, wc_ip);
|
|
}
|
|
#endif
|
|
|
|
signature = DisplayFrameSignature::append(signature, "home");
|
|
signature = DisplayFrameSignature::append(signature, _node_prefs->node_name);
|
|
snprintf(tmp, sizeof(tmp), "FREQ: %06.3f SF%d", _node_prefs->freq, _node_prefs->sf);
|
|
signature = DisplayFrameSignature::append(signature, tmp);
|
|
snprintf(tmp, sizeof(tmp), "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
|
|
signature = DisplayFrameSignature::append(signature, tmp);
|
|
|
|
#if defined(WITH_MQTT_BRIDGE) && !defined(DISPLAY_ACTIVITY_DASHBOARD)
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
IPAddress ip = WiFi.localIP();
|
|
snprintf(tmp, sizeof(tmp), "IP: %d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
|
|
signature = DisplayFrameSignature::append(signature, tmp);
|
|
} else {
|
|
signature = DisplayFrameSignature::append(signature, "wifi-disconnected");
|
|
}
|
|
#endif
|
|
|
|
return signature;
|
|
}
|
|
#endif
|
|
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
#define ACTIVITY_REFRESH_MILLIS 5000
|
|
|
|
bool UITask::buildDashboardContext(ObserverDashboard::Context* ctx) {
|
|
if (_node_prefs == NULL) return false;
|
|
ctx->node_name = _node_prefs->node_name;
|
|
ctx->role_label = "ROOM SERVER";
|
|
ctx->freq = _node_prefs->freq;
|
|
ctx->sf = _node_prefs->sf;
|
|
ctx->bw = _node_prefs->bw;
|
|
#ifdef WITH_MQTT_BRIDGE
|
|
ctx->link_up = (WiFi.status() == WL_CONNECTED);
|
|
#else
|
|
ctx->link_up = false;
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
void UITask::renderDashboard() {
|
|
ObserverDashboard::Context ctx;
|
|
if (!buildDashboardContext(&ctx)) return;
|
|
|
|
RadioActivitySnapshot snap;
|
|
if (_activity) {
|
|
_activity->snapshot(millis(), &snap);
|
|
} else {
|
|
memset(&snap, 0, sizeof(snap));
|
|
}
|
|
|
|
const ObserverDashboard::Layout& layout = ObserverDashboard::activeLayout();
|
|
ObserverDashboard::drawFull(*_display, layout, ctx, snap);
|
|
ObserverDashboard::allRowSignatures(layout, ctx, snap, _row_signatures);
|
|
_rows_valid = true;
|
|
_next_activity = millis() + ACTIVITY_REFRESH_MILLIS;
|
|
}
|
|
|
|
// Repaints just the analytics rows whose contents moved. No startFrame(), so
|
|
// the header, the radio strip and the rest of the panel are never cleared.
|
|
void UITask::updateActivityRows() {
|
|
if (!_rows_valid || _activity == NULL) return; // not showing the dashboard
|
|
|
|
ObserverDashboard::Context ctx;
|
|
if (!buildDashboardContext(&ctx)) return;
|
|
|
|
RadioActivitySnapshot snap;
|
|
_activity->snapshot(millis(), &snap);
|
|
ObserverDashboard::drawChangedRows(*_display, ObserverDashboard::activeLayout(), ctx, snap,
|
|
_row_signatures);
|
|
}
|
|
#endif
|
|
|
|
#ifdef DISPLAY_TOUCH_TOGGLE
|
|
void UITask::toggleDisplay() {
|
|
if (_display->isOn()) {
|
|
_display->turnOff();
|
|
} else {
|
|
_display->turnOn();
|
|
}
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false; // wake draws one complete current frame
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
_next_refresh = 0; // redraw at once rather than showing the stale frame
|
|
_auto_off = millis() + displayTimeoutMillis();
|
|
}
|
|
#endif
|
|
|
|
void UITask::loop() {
|
|
#ifdef PIN_USER_BTN
|
|
if (millis() >= _next_read) {
|
|
int btnState = digitalRead(PIN_USER_BTN);
|
|
if (btnState != _prevBtnState) {
|
|
if (btnState == USER_BTN_PRESSED) { // pressed?
|
|
#ifdef DISPLAY_TOUCH_TOGGLE
|
|
toggleDisplay(); // same action as tapping the panel
|
|
#else
|
|
if (_display->isOn()) {
|
|
// TODO: any action ?
|
|
} else {
|
|
_display->turnOn();
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
}
|
|
_auto_off = millis() + displayTimeoutMillis(); // extend auto-off timer
|
|
#endif
|
|
}
|
|
_prevBtnState = btnState;
|
|
}
|
|
_next_read = millis() + 200; // 5 reads per second
|
|
}
|
|
#endif
|
|
|
|
#ifdef WITH_WEBCONFIG
|
|
// While the setup portal is up there's no user button to wake the screen
|
|
// reliably - keep it on so the join instructions stay visible.
|
|
if (WebConfigServer::getSetupInfo(NULL, 0, NULL, 0)) {
|
|
if (!_display->isOn()) {
|
|
_display->turnOn();
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
}
|
|
_auto_off = millis() + displayTimeoutMillis();
|
|
}
|
|
#endif
|
|
|
|
#ifdef DISPLAY_TOUCH_TOGGLE
|
|
{
|
|
unsigned long now = millis();
|
|
if (millisReached(now, _next_touch)) {
|
|
_next_touch = now + TOUCH_POLL_MILLIS;
|
|
if (_touch.checkTap(now)) toggleDisplay();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (_display->isOn()) {
|
|
if (millis() >= _next_refresh) {
|
|
bool redraw = true;
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
uint32_t frame_signature = getFrameSignature();
|
|
redraw = !_frame_valid || frame_signature != _last_frame_signature;
|
|
#endif
|
|
if (redraw) {
|
|
_display->startFrame();
|
|
renderCurrScreen();
|
|
_display->endFrame();
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_last_frame_signature = frame_signature;
|
|
_frame_valid = true;
|
|
#endif
|
|
}
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
else if (millisReached(millis(), _next_activity)) {
|
|
updateActivityRows();
|
|
_next_activity = millis() + ACTIVITY_REFRESH_MILLIS;
|
|
}
|
|
#endif
|
|
|
|
_next_refresh = millis() + 1000; // check for visible changes every second
|
|
}
|
|
// `_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.
|
|
unsigned long timeout = displayTimeoutMillis();
|
|
if (timeout != _timeout_seen) {
|
|
_timeout_seen = timeout;
|
|
_auto_off = millis() + timeout;
|
|
}
|
|
if (timeout > 0 && millisReached(millis(), _auto_off)) {
|
|
_display->turnOff();
|
|
#ifdef DISPLAY_REDRAW_ON_CHANGE
|
|
_frame_valid = false;
|
|
#endif
|
|
#ifdef DISPLAY_ACTIVITY_DASHBOARD
|
|
_rows_valid = false;
|
|
#endif
|
|
}
|
|
}
|
|
}
|