#include "UITask.h" #include #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), 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 } */