mirror of
https://github.com/meshcore-dev/MeshCore.git
synced 2026-04-20 22:13:47 +00:00
580 lines
16 KiB
C++
580 lines
16 KiB
C++
#include "UITask.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
|
|
|
|
#ifdef PIN_STATUS_LED
|
|
#define LED_ON_MILLIS 20
|
|
#define LED_ON_MSG_MILLIS 200
|
|
#define LED_CYCLE_MILLIS 4000
|
|
#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
|
|
}
|
|
|
|
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) - 2, 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;
|
|
|
|
#if defined(PIN_USER_BTN)
|
|
user_btn.begin();
|
|
#endif
|
|
|
|
_node_prefs = node_prefs;
|
|
if (_display != NULL) {
|
|
_display->turnOn();
|
|
}
|
|
|
|
#ifdef PIN_BUZZER
|
|
buzzer.begin();
|
|
#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) {
|
|
#if defined(PIN_BUZZER)
|
|
switch(bet){
|
|
case UIEventType::contactMessage:
|
|
// gemini's pick
|
|
buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7");
|
|
break;
|
|
case UIEventType::channelMessage:
|
|
buzzer.play("kerplop:d=16,o=6,b=120:32g#,32c#");
|
|
break;
|
|
case UIEventType::ack:
|
|
buzzer.play("ack:d=32,o=8,b=120:c");
|
|
break;
|
|
case UIEventType::roomMessage:
|
|
case UIEventType::newContactMessage:
|
|
case UIEventType::none:
|
|
default:
|
|
break;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void UITask::msgRead(int msgcount) {
|
|
_msgcount = msgcount;
|
|
if (msgcount == 0) {
|
|
gotoHomeScreen();
|
|
}
|
|
}
|
|
|
|
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
|
|
_msgcount = msgcount;
|
|
|
|
((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
|
|
_next_refresh = 0; // trigger refresh
|
|
}
|
|
}
|
|
|
|
void UITask::userLedHandler() {
|
|
#ifdef PIN_STATUS_LED
|
|
static int state = 0;
|
|
static int next_change = 0;
|
|
static int last_increment = 0;
|
|
|
|
int cur_time = millis();
|
|
if (cur_time > next_change) {
|
|
if (state == 0) {
|
|
state = 1;
|
|
if (_msgcount > 0) {
|
|
last_increment = LED_ON_MSG_MILLIS;
|
|
} else {
|
|
last_increment = LED_ON_MILLIS;
|
|
}
|
|
next_change = cur_time + last_increment;
|
|
} else {
|
|
state = 0;
|
|
next_change = cur_time + LED_CYCLE_MILLIS - last_increment;
|
|
}
|
|
digitalWrite(PIN_STATUS_LED, state);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void UITask::setCurrScreen(UIScreen* c) {
|
|
curr = c;
|
|
_next_refresh = 0;
|
|
}
|
|
|
|
/*
|
|
hardware-agnostic pre-shutdown activity should be done here
|
|
*/
|
|
void UITask::shutdown(bool restart){
|
|
|
|
#ifdef PIN_BUZZER
|
|
/* note: we have a choice here -
|
|
we can do a blocking buzzer.loop() with non-deterministic consequences
|
|
or we can set a flag and delay the shutdown for a couple of seconds
|
|
while a non-blocking buzzer.loop() plays out in UITask::loop()
|
|
*/
|
|
buzzer.shutdown();
|
|
uint32_t buzzer_timer = millis(); // fail-safe shutdown
|
|
while (buzzer.isPlaying() && (millis() - 2500) < buzzer_timer)
|
|
buzzer.loop();
|
|
|
|
#endif // PIN_BUZZER
|
|
|
|
if (restart) {
|
|
_board->reboot();
|
|
} else {
|
|
_display->turnOff();
|
|
_board->powerOff();
|
|
}
|
|
}
|
|
|
|
bool UITask::isButtonPressed() const {
|
|
#ifdef PIN_USER_BTN
|
|
return user_btn.isPressed();
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void UITask::loop() {
|
|
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()) {
|
|
if (millis() >= _next_refresh && curr) {
|
|
_display->startFrame();
|
|
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();
|
|
}
|
|
if (millis() > _auto_off) {
|
|
_display->turnOff();
|
|
}
|
|
}
|
|
|
|
#ifdef AUTO_SHUTDOWN_MILLIVOLTS
|
|
if (millis() > next_batt_chck) {
|
|
uint16_t milliVolts = getBattMilliVolts();
|
|
if (milliVolts > 0 && milliVolts < AUTO_SHUTDOWN_MILLIVOLTS) {
|
|
shutdown();
|
|
}
|
|
next_batt_chck = millis() + 8000;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
char UITask::checkDisplayOn(char c) {
|
|
if (_display != NULL) {
|
|
if (!_display->isOn()) {
|
|
_display->turnOn(); // turn display on and consume event
|
|
c = 0;
|
|
}
|
|
_auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer
|
|
_next_refresh = 0; // trigger refresh
|
|
}
|
|
return c;
|
|
}
|
|
|
|
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
|
|
}
|
|
return c;
|
|
}
|
|
|
|
/*
|
|
void UITask::handleButtonTriplePress() {
|
|
MESH_DEBUG_PRINTLN("UITask: triple press triggered");
|
|
// Toggle buzzer quiet mode
|
|
#ifdef PIN_BUZZER
|
|
if (buzzer.isQuiet()) {
|
|
buzzer.quiet(false);
|
|
soundBuzzer(UIEventType::ack);
|
|
showAlert("Buzzer: ON", 600);
|
|
} else {
|
|
buzzer.quiet(true);
|
|
showAlert("Buzzer: OFF", 600);
|
|
}
|
|
_next_refresh = 0; // trigger refresh
|
|
#endif
|
|
}
|
|
*/
|