|
|
|
@ -1,8 +1,8 @@ |
|
|
|
#include "UITask.h" |
|
|
|
#include <Arduino.h> |
|
|
|
#include <helpers/TxtDataHelpers.h> |
|
|
|
#include "NodePrefs.h" |
|
|
|
#include "MyMesh.h" |
|
|
|
#include "target.h" |
|
|
|
|
|
|
|
#define AUTO_OFF_MILLIS 15000 // 15 seconds
|
|
|
|
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds
|
|
|
|
@ -13,80 +13,366 @@ |
|
|
|
#define LED_CYCLE_MILLIS 4000 |
|
|
|
#endif |
|
|
|
|
|
|
|
#ifndef USER_BTN_PRESSED |
|
|
|
#define USER_BTN_PRESSED LOW |
|
|
|
#endif |
|
|
|
#define LONG_PRESS_MILLIS 1200 |
|
|
|
|
|
|
|
#define PRESS_LABEL "long press" |
|
|
|
|
|
|
|
#include "icons.h" |
|
|
|
|
|
|
|
class SplashScreen : public UIScreen { |
|
|
|
UITask* _task; |
|
|
|
unsigned long dismiss_after; |
|
|
|
char _version_info[12]; |
|
|
|
|
|
|
|
public: |
|
|
|
SplashScreen(UITask* task) : _task(task) { |
|
|
|
// strip off dash and commit hash by changing dash to null terminator
|
|
|
|
// e.g: v1.2.3-abcdef -> v1.2.3
|
|
|
|
const char *ver = FIRMWARE_VERSION; |
|
|
|
const char *dash = strchr(ver, '-'); |
|
|
|
|
|
|
|
int len = dash ? dash - ver : strlen(ver); |
|
|
|
if (len >= sizeof(_version_info)) len = sizeof(_version_info) - 1; |
|
|
|
memcpy(_version_info, ver, len); |
|
|
|
_version_info[len] = 0; |
|
|
|
|
|
|
|
dismiss_after = millis() + BOOT_SCREEN_MILLIS; |
|
|
|
} |
|
|
|
|
|
|
|
int render(DisplayDriver& display) override { |
|
|
|
// meshcore logo
|
|
|
|
display.setColor(DisplayDriver::BLUE); |
|
|
|
int logoWidth = 128; |
|
|
|
display.drawXbm((display.width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); |
|
|
|
|
|
|
|
// version info
|
|
|
|
display.setColor(DisplayDriver::LIGHT); |
|
|
|
display.setTextSize(2); |
|
|
|
display.drawTextCentered(display.width()/2, 22, _version_info); |
|
|
|
|
|
|
|
display.setTextSize(1); |
|
|
|
display.drawTextCentered(display.width()/2, 42, FIRMWARE_BUILD_DATE); |
|
|
|
|
|
|
|
return 1000; |
|
|
|
} |
|
|
|
|
|
|
|
void poll() override { |
|
|
|
if (millis() >= dismiss_after) { |
|
|
|
_task->gotoHomeScreen(); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
class HomeScreen : public UIScreen { |
|
|
|
enum HomePage { |
|
|
|
FIRST, |
|
|
|
RECENT, |
|
|
|
RADIO, |
|
|
|
BLUETOOTH, |
|
|
|
ADVERT, |
|
|
|
SHUTDOWN, |
|
|
|
Count // keep as last
|
|
|
|
}; |
|
|
|
|
|
|
|
UITask* _task; |
|
|
|
mesh::RTCClock* _rtc; |
|
|
|
SensorManager* _sensors; |
|
|
|
NodePrefs* _node_prefs; |
|
|
|
uint8_t _page; |
|
|
|
bool _shutdown_init; |
|
|
|
AdvertPath recent[4]; |
|
|
|
|
|
|
|
void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { |
|
|
|
// Convert millivolts to percentage
|
|
|
|
const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V)
|
|
|
|
const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V)
|
|
|
|
int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts); |
|
|
|
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
|
|
|
|
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%
|
|
|
|
|
|
|
|
// battery icon
|
|
|
|
int iconWidth = 24; |
|
|
|
int iconHeight = 10; |
|
|
|
int iconX = display.width() - iconWidth - 5; // Position the icon near the top-right corner
|
|
|
|
int iconY = 0; |
|
|
|
display.setColor(DisplayDriver::GREEN); |
|
|
|
|
|
|
|
// battery outline
|
|
|
|
display.drawRect(iconX, iconY, iconWidth, iconHeight); |
|
|
|
|
|
|
|
// battery "cap"
|
|
|
|
display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 3, iconHeight / 2); |
|
|
|
|
|
|
|
// fill the battery based on the percentage
|
|
|
|
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; |
|
|
|
display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); |
|
|
|
} |
|
|
|
|
|
|
|
public: |
|
|
|
HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs) |
|
|
|
: _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), _shutdown_init(false) { } |
|
|
|
|
|
|
|
void poll() override { |
|
|
|
if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released
|
|
|
|
_task->shutdown(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
int render(DisplayDriver& display) override { |
|
|
|
char tmp[80]; |
|
|
|
// node name
|
|
|
|
display.setCursor(0, 0); |
|
|
|
display.setTextSize(1); |
|
|
|
display.setColor(DisplayDriver::GREEN); |
|
|
|
display.print(_node_prefs->node_name); |
|
|
|
|
|
|
|
// battery voltage
|
|
|
|
renderBatteryIndicator(display, _task->getBattMilliVolts()); |
|
|
|
|
|
|
|
// curr page indicator
|
|
|
|
int y = 14; |
|
|
|
int x = display.width() / 2 - 25; |
|
|
|
for (uint8_t i = 0; i < HomePage::Count; i++, x += 10) { |
|
|
|
if (i == _page) { |
|
|
|
display.fillRect(x-1, y-1, 3, 3); |
|
|
|
} else { |
|
|
|
display.fillRect(x, y, 1, 1); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (_page == HomePage::FIRST) { |
|
|
|
display.setColor(DisplayDriver::YELLOW); |
|
|
|
display.setTextSize(2); |
|
|
|
sprintf(tmp, "MSG: %d", _task->getMsgCount()); |
|
|
|
display.drawTextCentered(display.width() / 2, 20, tmp); |
|
|
|
|
|
|
|
if (_task->hasConnection()) { |
|
|
|
display.setColor(DisplayDriver::GREEN); |
|
|
|
display.setTextSize(1); |
|
|
|
display.drawTextCentered(display.width() / 2, 43, "< Connected >"); |
|
|
|
} else if (the_mesh.getBLEPin() != 0) { // BT pin
|
|
|
|
display.setColor(DisplayDriver::RED); |
|
|
|
display.setTextSize(2); |
|
|
|
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); |
|
|
|
display.drawTextCentered(display.width() / 2, 43, tmp); |
|
|
|
} |
|
|
|
} else if (_page == HomePage::RECENT) { |
|
|
|
the_mesh.getRecentlyHeard(recent, 4); |
|
|
|
display.setColor(DisplayDriver::GREEN); |
|
|
|
int y = 20; |
|
|
|
for (int i = 0; i < 4; i++, y += 11) { |
|
|
|
auto a = &recent[i]; |
|
|
|
if (a->name[0] == 0) continue; // empty slot
|
|
|
|
display.setCursor(0, y); |
|
|
|
display.print(a->name); |
|
|
|
int secs = _rtc->getCurrentTime() - a->recv_timestamp; |
|
|
|
if (secs < 60) { |
|
|
|
sprintf(tmp, "%ds", secs); |
|
|
|
} else if (secs < 60*60) { |
|
|
|
sprintf(tmp, "%dm", secs / 60); |
|
|
|
} else { |
|
|
|
sprintf(tmp, "%dh", secs / (60*60)); |
|
|
|
} |
|
|
|
display.setCursor(display.width() - display.getTextWidth(tmp) - 1, y); |
|
|
|
display.print(tmp); |
|
|
|
} |
|
|
|
} else if (_page == HomePage::RADIO) { |
|
|
|
display.setColor(DisplayDriver::YELLOW); |
|
|
|
display.setTextSize(1); |
|
|
|
// freq / sf
|
|
|
|
display.setCursor(0, 20); |
|
|
|
sprintf(tmp, "FQ: %06.3f SF: %d", _node_prefs->freq, _node_prefs->sf); |
|
|
|
display.print(tmp); |
|
|
|
|
|
|
|
display.setCursor(0, 31); |
|
|
|
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr); |
|
|
|
display.print(tmp); |
|
|
|
|
|
|
|
// tx power, noise floor
|
|
|
|
display.setCursor(0, 42); |
|
|
|
sprintf(tmp, "TX: %ddBm", _node_prefs->tx_power_dbm); |
|
|
|
display.print(tmp); |
|
|
|
display.setCursor(0, 53); |
|
|
|
sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor()); |
|
|
|
display.print(tmp); |
|
|
|
} else if (_page == HomePage::BLUETOOTH) { |
|
|
|
display.setColor(DisplayDriver::GREEN); |
|
|
|
display.drawXbm((display.width() - 32) / 2, 18, |
|
|
|
_task->isSerialEnabled() ? bluetooth_on : bluetooth_off, |
|
|
|
32, 32); |
|
|
|
display.setTextSize(1); |
|
|
|
display.drawTextCentered(display.width() / 2, 64 - 11, "toggle: " PRESS_LABEL); |
|
|
|
} else if (_page == HomePage::ADVERT) { |
|
|
|
display.setColor(DisplayDriver::GREEN); |
|
|
|
display.drawXbm((display.width() - 32) / 2, 18, advert_icon, 32, 32); |
|
|
|
display.drawTextCentered(display.width() / 2, 64 - 11, "advert: " PRESS_LABEL); |
|
|
|
} else if (_page == HomePage::SHUTDOWN) { |
|
|
|
display.setColor(DisplayDriver::GREEN); |
|
|
|
display.setTextSize(1); |
|
|
|
if (_shutdown_init) { |
|
|
|
display.drawTextCentered(display.width() / 2, 34, "shutting down..."); |
|
|
|
} else { |
|
|
|
display.drawXbm((display.width() - 32) / 2, 18, power_icon, 32, 32); |
|
|
|
display.drawTextCentered(display.width() / 2, 64 - 11, "off: " PRESS_LABEL); |
|
|
|
} |
|
|
|
} |
|
|
|
return 5000; // next render after 5000 ms
|
|
|
|
} |
|
|
|
|
|
|
|
// '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, |
|
|
|
bool handleInput(char c) override { |
|
|
|
if (c == KEY_LEFT) { |
|
|
|
_page = (_page + HomePage::Count - 1) % HomePage::Count; |
|
|
|
return true; |
|
|
|
} |
|
|
|
if (c == KEY_RIGHT || c == KEY_SELECT) { |
|
|
|
_page = (_page + 1) % HomePage::Count; |
|
|
|
if (_page == HomePage::RECENT) { |
|
|
|
_task->showAlert("Recent adverts", 800); |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { |
|
|
|
if (_task->isSerialEnabled()) { // toggle Bluetooth on/off
|
|
|
|
_task->disableSerial(); |
|
|
|
} else { |
|
|
|
_task->enableSerial(); |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
if (c == KEY_ENTER && _page == HomePage::ADVERT) { |
|
|
|
#ifdef PIN_BUZZER |
|
|
|
_task->soundBuzzer(UIEventType::ack); |
|
|
|
#endif |
|
|
|
if (the_mesh.advert()) { |
|
|
|
_task->showAlert("Advert sent!", 1000); |
|
|
|
} else { |
|
|
|
_task->showAlert("Advert failed..", 1000); |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
if (c == KEY_ENTER && _page == HomePage::SHUTDOWN) { |
|
|
|
_shutdown_init = true; // need to wait for button to be released
|
|
|
|
return true; |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
class MsgPreviewScreen : public UIScreen { |
|
|
|
UITask* _task; |
|
|
|
mesh::RTCClock* _rtc; |
|
|
|
|
|
|
|
struct MsgEntry { |
|
|
|
uint32_t timestamp; |
|
|
|
char origin[62]; |
|
|
|
char msg[78]; |
|
|
|
}; |
|
|
|
#define MAX_UNREAD_MSGS 32 |
|
|
|
int num_unread; |
|
|
|
MsgEntry unread[MAX_UNREAD_MSGS]; |
|
|
|
|
|
|
|
public: |
|
|
|
MsgPreviewScreen(UITask* task, mesh::RTCClock* rtc) : _task(task), _rtc(rtc) { num_unread = 0; } |
|
|
|
|
|
|
|
void addPreview(uint8_t path_len, const char* from_name, const char* msg) { |
|
|
|
if (num_unread >= MAX_UNREAD_MSGS) return; // full
|
|
|
|
|
|
|
|
auto p = &unread[num_unread++]; |
|
|
|
p->timestamp = _rtc->getCurrentTime(); |
|
|
|
if (path_len == 0xFF) { |
|
|
|
sprintf(p->origin, "(D) %s:", from_name); |
|
|
|
} else { |
|
|
|
sprintf(p->origin, "(%d) %s:", (uint32_t) path_len, from_name); |
|
|
|
} |
|
|
|
StrHelper::strncpy(p->msg, msg, sizeof(p->msg)); |
|
|
|
} |
|
|
|
|
|
|
|
int render(DisplayDriver& display) override { |
|
|
|
char tmp[16]; |
|
|
|
display.setCursor(0, 0); |
|
|
|
display.setTextSize(1); |
|
|
|
display.setColor(DisplayDriver::GREEN); |
|
|
|
sprintf(tmp, "Unread: %d", num_unread); |
|
|
|
display.print(tmp); |
|
|
|
|
|
|
|
auto p = &unread[0]; |
|
|
|
|
|
|
|
int secs = _rtc->getCurrentTime() - p->timestamp; |
|
|
|
if (secs < 60) { |
|
|
|
sprintf(tmp, "%ds", secs); |
|
|
|
} else if (secs < 60*60) { |
|
|
|
sprintf(tmp, "%dm", secs / 60); |
|
|
|
} else { |
|
|
|
sprintf(tmp, "%dh", secs / (60*60)); |
|
|
|
} |
|
|
|
display.setCursor(display.width() - display.getTextWidth(tmp), 0); |
|
|
|
display.print(tmp); |
|
|
|
|
|
|
|
display.drawRect(0, 11, display.width(), 1); // horiz line
|
|
|
|
|
|
|
|
display.setCursor(0, 14); |
|
|
|
display.setColor(DisplayDriver::YELLOW); |
|
|
|
display.print(p->origin); |
|
|
|
|
|
|
|
display.setCursor(0, 25); |
|
|
|
display.setColor(DisplayDriver::LIGHT); |
|
|
|
display.printWordWrap(p->msg, display.width()); |
|
|
|
|
|
|
|
return 1000; // next render after 1000 ms
|
|
|
|
} |
|
|
|
|
|
|
|
bool handleInput(char c) override { |
|
|
|
if (c == KEY_SELECT || c == KEY_RIGHT) { |
|
|
|
num_unread--; |
|
|
|
if (num_unread == 0) { |
|
|
|
_task->gotoHomeScreen(); |
|
|
|
} else { |
|
|
|
// delete first/curr item from unread queue
|
|
|
|
for (int i = 0; i < num_unread; i++) { |
|
|
|
unread[i] = unread[i + 1]; |
|
|
|
} |
|
|
|
} |
|
|
|
return true; |
|
|
|
} |
|
|
|
if (c == KEY_ENTER) { |
|
|
|
num_unread = 0; // clear unread queue
|
|
|
|
_task->gotoHomeScreen(); |
|
|
|
return true; |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
}; |
|
|
|
|
|
|
|
void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs) { |
|
|
|
_display = display; |
|
|
|
_sensors = sensors; |
|
|
|
_auto_off = millis() + AUTO_OFF_MILLIS; |
|
|
|
clearMsgPreview(); |
|
|
|
|
|
|
|
#if defined(PIN_USER_BTN) |
|
|
|
user_btn.begin(); |
|
|
|
#endif |
|
|
|
|
|
|
|
_node_prefs = node_prefs; |
|
|
|
if (_display != NULL) { |
|
|
|
_display->turnOn(); |
|
|
|
} |
|
|
|
|
|
|
|
// 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)
|
|
|
|
sprintf(_version_info, "%s (%s)", version, FIRMWARE_BUILD_DATE); |
|
|
|
|
|
|
|
#ifdef PIN_BUZZER |
|
|
|
buzzer.begin(); |
|
|
|
#endif |
|
|
|
|
|
|
|
// Initialize digital button if available
|
|
|
|
#ifdef PIN_USER_BTN |
|
|
|
_userButton = new Button(PIN_USER_BTN, USER_BTN_PRESSED); |
|
|
|
_userButton->begin(); |
|
|
|
|
|
|
|
// Set up digital button callbacks
|
|
|
|
_userButton->onShortPress([this]() { handleButtonShortPress(); }); |
|
|
|
_userButton->onDoublePress([this]() { handleButtonDoublePress(); }); |
|
|
|
_userButton->onTriplePress([this]() { handleButtonTriplePress(); }); |
|
|
|
_userButton->onQuadruplePress([this]() { handleButtonQuadruplePress(); }); |
|
|
|
_userButton->onLongPress([this]() { handleButtonLongPress(); }); |
|
|
|
_userButton->onAnyPress([this]() { handleButtonAnyPress(); }); |
|
|
|
#endif |
|
|
|
|
|
|
|
// Initialize analog button if available
|
|
|
|
#ifdef PIN_USER_BTN_ANA |
|
|
|
_userButtonAnalog = new Button(PIN_USER_BTN_ANA, USER_BTN_PRESSED, true, 20); |
|
|
|
_userButtonAnalog->begin(); |
|
|
|
|
|
|
|
// Set up analog button callbacks
|
|
|
|
_userButtonAnalog->onShortPress([this]() { handleButtonShortPress(); }); |
|
|
|
_userButtonAnalog->onDoublePress([this]() { handleButtonDoublePress(); }); |
|
|
|
_userButtonAnalog->onTriplePress([this]() { handleButtonTriplePress(); }); |
|
|
|
_userButtonAnalog->onQuadruplePress([this]() { handleButtonQuadruplePress(); }); |
|
|
|
_userButtonAnalog->onLongPress([this]() { handleButtonLongPress(); }); |
|
|
|
_userButtonAnalog->onAnyPress([this]() { handleButtonAnyPress(); }); |
|
|
|
#endif |
|
|
|
ui_started_at = millis(); |
|
|
|
_alert_expiry = 0; |
|
|
|
|
|
|
|
splash = new SplashScreen(this); |
|
|
|
home = new HomeScreen(this, &rtc_clock, sensors, node_prefs); |
|
|
|
msg_preview = new MsgPreviewScreen(this, &rtc_clock); |
|
|
|
setCurrScreen(splash); |
|
|
|
} |
|
|
|
|
|
|
|
void UITask::showAlert(const char* text, int duration_millis) { |
|
|
|
strcpy(_alert, text); |
|
|
|
_alert_expiry = millis() + duration_millis; |
|
|
|
} |
|
|
|
|
|
|
|
void UITask::soundBuzzer(UIEventType bet) { |
|
|
|
@ -109,147 +395,28 @@ switch(bet){ |
|
|
|
break; |
|
|
|
} |
|
|
|
#endif |
|
|
|
// Serial.print("DBG: Buzzzzzz -> ");
|
|
|
|
// Serial.println((int) bet);
|
|
|
|
} |
|
|
|
|
|
|
|
void UITask::msgRead(int msgcount) { |
|
|
|
_msgcount = msgcount; |
|
|
|
if (msgcount == 0) { |
|
|
|
clearMsgPreview(); |
|
|
|
gotoHomeScreen(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
void UITask::clearMsgPreview() { |
|
|
|
_origin[0] = 0; |
|
|
|
_msg[0] = 0; |
|
|
|
_need_refresh = true; |
|
|
|
} |
|
|
|
|
|
|
|
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) { |
|
|
|
_msgcount = msgcount; |
|
|
|
|
|
|
|
if (path_len == 0xFF) { |
|
|
|
sprintf(_origin, "(F) %s", from_name); |
|
|
|
} else { |
|
|
|
sprintf(_origin, "(%d) %s", (uint32_t) path_len, from_name); |
|
|
|
} |
|
|
|
StrHelper::strncpy(_msg, text, sizeof(_msg)); |
|
|
|
((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text); |
|
|
|
setCurrScreen(msg_preview); |
|
|
|
|
|
|
|
if (_display != NULL) { |
|
|
|
if (!_display->isOn()) _display->turnOn(); |
|
|
|
_auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer
|
|
|
|
_need_refresh = true; |
|
|
|
_next_refresh = 0; // trigger refresh
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
void UITask::renderBatteryIndicator(uint16_t batteryMilliVolts) { |
|
|
|
// Convert millivolts to percentage
|
|
|
|
const int minMilliVolts = 3000; // Minimum voltage (e.g., 3.0V)
|
|
|
|
const int maxMilliVolts = 4200; // Maximum voltage (e.g., 4.2V)
|
|
|
|
int batteryPercentage = ((batteryMilliVolts - minMilliVolts) * 100) / (maxMilliVolts - minMilliVolts); |
|
|
|
if (batteryPercentage < 0) batteryPercentage = 0; // Clamp to 0%
|
|
|
|
if (batteryPercentage > 100) batteryPercentage = 100; // Clamp to 100%
|
|
|
|
|
|
|
|
// battery icon
|
|
|
|
int iconWidth = 24; |
|
|
|
int iconHeight = 12; |
|
|
|
int iconX = _display->width() - iconWidth - 5; // Position the icon near the top-right corner
|
|
|
|
int iconY = 0; |
|
|
|
_display->setColor(DisplayDriver::GREEN); |
|
|
|
|
|
|
|
// battery outline
|
|
|
|
_display->drawRect(iconX, iconY, iconWidth, iconHeight); |
|
|
|
|
|
|
|
// battery "cap"
|
|
|
|
_display->fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 3, iconHeight / 2); |
|
|
|
|
|
|
|
// fill the battery based on the percentage
|
|
|
|
int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; |
|
|
|
_display->fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); |
|
|
|
} |
|
|
|
|
|
|
|
void UITask::renderCurrScreen() { |
|
|
|
if (_display == NULL) return; // assert() ??
|
|
|
|
|
|
|
|
char tmp[80]; |
|
|
|
if (_alert[0]) { |
|
|
|
_display->setTextSize(1.4); |
|
|
|
uint16_t textWidth = _display->getTextWidth(_alert); |
|
|
|
_display->setCursor((_display->width() - textWidth) / 2, 22); |
|
|
|
_display->setColor(DisplayDriver::GREEN); |
|
|
|
_display->print(_alert); |
|
|
|
_alert[0] = 0; |
|
|
|
_need_refresh = true; |
|
|
|
return; |
|
|
|
} else if (_origin[0] && _msg[0]) { // message preview
|
|
|
|
// render message preview
|
|
|
|
_display->setCursor(0, 0); |
|
|
|
_display->setTextSize(1); |
|
|
|
_display->setColor(DisplayDriver::GREEN); |
|
|
|
_display->print(_node_prefs->node_name); |
|
|
|
|
|
|
|
_display->setCursor(0, 12); |
|
|
|
_display->setColor(DisplayDriver::YELLOW); |
|
|
|
_display->print(_origin); |
|
|
|
_display->setCursor(0, 24); |
|
|
|
_display->setColor(DisplayDriver::LIGHT); |
|
|
|
_display->print(_msg); |
|
|
|
|
|
|
|
_display->setCursor(_display->width() - 28, 9); |
|
|
|
_display->setTextSize(2); |
|
|
|
_display->setColor(DisplayDriver::ORANGE); |
|
|
|
sprintf(tmp, "%d", _msgcount); |
|
|
|
_display->print(tmp); |
|
|
|
_display->setColor(DisplayDriver::YELLOW); // last color will be kept on T114
|
|
|
|
} else if ((millis() - ui_started_at) < BOOT_SCREEN_MILLIS) { // boot screen
|
|
|
|
// meshcore logo
|
|
|
|
_display->setColor(DisplayDriver::BLUE); |
|
|
|
int logoWidth = 128; |
|
|
|
_display->drawXbm((_display->width() - logoWidth) / 2, 3, meshcore_logo, logoWidth, 13); |
|
|
|
|
|
|
|
// version info
|
|
|
|
_display->setColor(DisplayDriver::LIGHT); |
|
|
|
_display->setTextSize(1); |
|
|
|
uint16_t textWidth = _display->getTextWidth(_version_info); |
|
|
|
_display->setCursor((_display->width() - textWidth) / 2, 22); |
|
|
|
_display->print(_version_info); |
|
|
|
} else { // home screen
|
|
|
|
// node name
|
|
|
|
_display->setCursor(0, 0); |
|
|
|
_display->setTextSize(1); |
|
|
|
_display->setColor(DisplayDriver::GREEN); |
|
|
|
_display->print(_node_prefs->node_name); |
|
|
|
|
|
|
|
// battery voltage
|
|
|
|
renderBatteryIndicator(_board->getBattMilliVolts()); |
|
|
|
|
|
|
|
// freq / sf
|
|
|
|
_display->setCursor(0, 20); |
|
|
|
_display->setColor(DisplayDriver::YELLOW); |
|
|
|
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); |
|
|
|
|
|
|
|
// BT pin
|
|
|
|
if (!_connected && the_mesh.getBLEPin() != 0) { |
|
|
|
_display->setColor(DisplayDriver::RED); |
|
|
|
_display->setTextSize(2); |
|
|
|
_display->setCursor(0, 43); |
|
|
|
sprintf(tmp, "Pin:%d", the_mesh.getBLEPin()); |
|
|
|
_display->print(tmp); |
|
|
|
_display->setColor(DisplayDriver::GREEN); |
|
|
|
} else { |
|
|
|
_display->setColor(DisplayDriver::LIGHT); |
|
|
|
} |
|
|
|
} |
|
|
|
_need_refresh = false; |
|
|
|
} |
|
|
|
|
|
|
|
void UITask::userLedHandler() { |
|
|
|
#ifdef PIN_STATUS_LED |
|
|
|
static int state = 0; |
|
|
|
@ -275,6 +442,11 @@ void UITask::userLedHandler() { |
|
|
|
#endif |
|
|
|
} |
|
|
|
|
|
|
|
void UITask::setCurrScreen(UIScreen* c) { |
|
|
|
curr = c; |
|
|
|
_next_refresh = 0; |
|
|
|
} |
|
|
|
|
|
|
|
/*
|
|
|
|
hardware-agnostic pre-shutdown activity should be done here |
|
|
|
*/ |
|
|
|
@ -293,96 +465,103 @@ void UITask::shutdown(bool restart){ |
|
|
|
|
|
|
|
#endif // PIN_BUZZER
|
|
|
|
|
|
|
|
if (restart) |
|
|
|
if (restart) { |
|
|
|
_board->reboot(); |
|
|
|
else |
|
|
|
} else { |
|
|
|
_display->turnOff(); |
|
|
|
_board->powerOff(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
bool UITask::isButtonPressed() const { |
|
|
|
#ifdef PIN_USER_BTN |
|
|
|
return user_btn.isPressed(); |
|
|
|
#else |
|
|
|
return false; |
|
|
|
#endif |
|
|
|
} |
|
|
|
|
|
|
|
void UITask::loop() { |
|
|
|
#ifdef PIN_USER_BTN |
|
|
|
if (_userButton) { |
|
|
|
_userButton->update(); |
|
|
|
} |
|
|
|
#endif |
|
|
|
#ifdef PIN_USER_BTN_ANA |
|
|
|
if (_userButtonAnalog) { |
|
|
|
_userButtonAnalog->update(); |
|
|
|
} |
|
|
|
#endif |
|
|
|
char c = 0; |
|
|
|
#if defined(PIN_USER_BTN) |
|
|
|
int ev = user_btn.check(); |
|
|
|
if (ev == BUTTON_EVENT_CLICK) { |
|
|
|
c = checkDisplayOn(KEY_SELECT); |
|
|
|
} else if (ev == BUTTON_EVENT_LONG_PRESS) { |
|
|
|
c = handleLongPress(KEY_ENTER); |
|
|
|
} |
|
|
|
#endif |
|
|
|
|
|
|
|
if (c != 0 && curr) { |
|
|
|
curr->handleInput(c); |
|
|
|
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
|
|
|
_next_refresh = 0; // trigger refresh
|
|
|
|
} |
|
|
|
|
|
|
|
userLedHandler(); |
|
|
|
|
|
|
|
#ifdef PIN_BUZZER |
|
|
|
if (buzzer.isPlaying()) buzzer.loop(); |
|
|
|
#endif |
|
|
|
|
|
|
|
if (curr) curr->poll(); |
|
|
|
|
|
|
|
if (_display != NULL && _display->isOn()) { |
|
|
|
static bool _firstBoot = true; |
|
|
|
if(_firstBoot && (millis() - ui_started_at) >= BOOT_SCREEN_MILLIS) { |
|
|
|
_need_refresh = true; |
|
|
|
_firstBoot = false; |
|
|
|
} |
|
|
|
if (millis() >= _next_refresh && _need_refresh) { |
|
|
|
if (millis() >= _next_refresh && curr) { |
|
|
|
_display->startFrame(); |
|
|
|
renderCurrScreen(); |
|
|
|
int delay_millis = curr->render(*_display); |
|
|
|
if (millis() < _alert_expiry) { // render alert popup
|
|
|
|
_display->setTextSize(1); |
|
|
|
int y = _display->height() / 3; |
|
|
|
int p = _display->height() / 32; |
|
|
|
_display->setColor(DisplayDriver::DARK); |
|
|
|
_display->fillRect(p, y, _display->width() - p*2, y); |
|
|
|
_display->setColor(DisplayDriver::LIGHT); // draw box border
|
|
|
|
_display->drawRect(p, y, _display->width() - p*2, y); |
|
|
|
_display->drawTextCentered(_display->width() / 2, y + p*3, _alert); |
|
|
|
_next_refresh = _alert_expiry; // will need refresh when alert is dismissed
|
|
|
|
} else { |
|
|
|
_next_refresh = millis() + delay_millis; |
|
|
|
} |
|
|
|
_display->endFrame(); |
|
|
|
|
|
|
|
_next_refresh = millis() + 1000; // refresh every second
|
|
|
|
} |
|
|
|
if (millis() > _auto_off) { |
|
|
|
_display->turnOff(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
void UITask::handleButtonAnyPress() { |
|
|
|
MESH_DEBUG_PRINTLN("UITask: any press triggered"); |
|
|
|
// called on any button press before other events, to wake up the display quickly
|
|
|
|
// do not refresh the display here, as it may block the button handler
|
|
|
|
if (_display != NULL) { |
|
|
|
_displayWasOn = _display->isOn(); // Track display state before any action
|
|
|
|
if (!_displayWasOn) { |
|
|
|
_display->turnOn(); |
|
|
|
#ifdef AUTO_SHUTDOWN_MILLIVOLTS |
|
|
|
if (millis() > next_batt_chck) { |
|
|
|
uint16_t milliVolts = getBattMilliVolts(); |
|
|
|
if (milliVolts > 0 && milliVolts < AUTO_SHUTDOWN_MILLIVOLTS) { |
|
|
|
shutdown(); |
|
|
|
} |
|
|
|
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
|
|
|
next_batt_chck = millis() + 8000; |
|
|
|
} |
|
|
|
#endif |
|
|
|
} |
|
|
|
|
|
|
|
void UITask::handleButtonShortPress() { |
|
|
|
MESH_DEBUG_PRINTLN("UITask: short press triggered"); |
|
|
|
char UITask::checkDisplayOn(char c) { |
|
|
|
if (_display != NULL) { |
|
|
|
// Only clear message preview if display was already on before button press
|
|
|
|
if (_displayWasOn) { |
|
|
|
// If display was on and showing message preview, clear it
|
|
|
|
if (_origin[0] && _msg[0]) { |
|
|
|
clearMsgPreview(); |
|
|
|
} else { |
|
|
|
// Otherwise, refresh the display
|
|
|
|
_need_refresh = true; |
|
|
|
} |
|
|
|
} else { |
|
|
|
_need_refresh = true; // display just turned on, so we need to refresh
|
|
|
|
if (!_display->isOn()) { |
|
|
|
_display->turnOn(); // turn display on and consume event
|
|
|
|
c = 0; |
|
|
|
} |
|
|
|
// Note: Display turn-on and auto-off timer extension are handled by handleButtonAnyPress
|
|
|
|
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
|
|
|
_next_refresh = 0; // trigger refresh
|
|
|
|
} |
|
|
|
return c; |
|
|
|
} |
|
|
|
|
|
|
|
void UITask::handleButtonDoublePress() { |
|
|
|
MESH_DEBUG_PRINTLN("UITask: double press triggered, sending advert"); |
|
|
|
// ADVERT
|
|
|
|
#ifdef PIN_BUZZER |
|
|
|
soundBuzzer(UIEventType::ack); |
|
|
|
#endif |
|
|
|
if (the_mesh.advert()) { |
|
|
|
MESH_DEBUG_PRINTLN("Advert sent!"); |
|
|
|
sprintf(_alert, "Advert sent!"); |
|
|
|
} else { |
|
|
|
MESH_DEBUG_PRINTLN("Advert failed!"); |
|
|
|
sprintf(_alert, "Advert failed.."); |
|
|
|
char UITask::handleLongPress(char c) { |
|
|
|
if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue
|
|
|
|
the_mesh.enterCLIRescue(); |
|
|
|
c = 0; // consume event
|
|
|
|
} |
|
|
|
_need_refresh = true; |
|
|
|
return c; |
|
|
|
} |
|
|
|
|
|
|
|
/*
|
|
|
|
void UITask::handleButtonTriplePress() { |
|
|
|
MESH_DEBUG_PRINTLN("UITask: triple press triggered"); |
|
|
|
// Toggle buzzer quiet mode
|
|
|
|
@ -390,43 +569,12 @@ void UITask::handleButtonTriplePress() { |
|
|
|
if (buzzer.isQuiet()) { |
|
|
|
buzzer.quiet(false); |
|
|
|
soundBuzzer(UIEventType::ack); |
|
|
|
sprintf(_alert, "Buzzer: ON"); |
|
|
|
showAlert("Buzzer: ON", 600); |
|
|
|
} else { |
|
|
|
buzzer.quiet(true); |
|
|
|
sprintf(_alert, "Buzzer: OFF"); |
|
|
|
showAlert("Buzzer: OFF", 600); |
|
|
|
} |
|
|
|
_need_refresh = true; |
|
|
|
_next_refresh = 0; // trigger refresh
|
|
|
|
#endif |
|
|
|
} |
|
|
|
|
|
|
|
void UITask::handleButtonQuadruplePress() { |
|
|
|
MESH_DEBUG_PRINTLN("UITask: quad press triggered"); |
|
|
|
if (_sensors != NULL) { |
|
|
|
// toggle GPS onn/off
|
|
|
|
int num = _sensors->getNumSettings(); |
|
|
|
for (int i = 0; i < num; i++) { |
|
|
|
if (strcmp(_sensors->getSettingName(i), "gps") == 0) { |
|
|
|
if (strcmp(_sensors->getSettingValue(i), "1") == 0) { |
|
|
|
_sensors->setSettingValue("gps", "0"); |
|
|
|
soundBuzzer(UIEventType::ack); |
|
|
|
sprintf(_alert, "GPS: Disabled"); |
|
|
|
} else { |
|
|
|
_sensors->setSettingValue("gps", "1"); |
|
|
|
soundBuzzer(UIEventType::ack); |
|
|
|
sprintf(_alert, "GPS: Enabled"); |
|
|
|
} |
|
|
|
break; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
_need_refresh = true; |
|
|
|
} |
|
|
|
|
|
|
|
void UITask::handleButtonLongPress() { |
|
|
|
MESH_DEBUG_PRINTLN("UITask: long press triggered"); |
|
|
|
if (millis() - ui_started_at < 8000) { // long press in first 8 seconds since startup -> CLI/rescue
|
|
|
|
the_mesh.enterCLIRescue(); |
|
|
|
} else { |
|
|
|
shutdown(); |
|
|
|
} |
|
|
|
} |
|
|
|
*/ |
|
|
|
|