| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579 |
- #include "UITask.h"
- #include <helpers/TxtDataHelpers.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
- }
- */
|