diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 901e3db5..75828d5e 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -267,6 +267,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } memcpy(p->pubkey_prefix, contact.id.pub_key, sizeof(p->pubkey_prefix)); + strcpy(p->name, contact.name); p->recv_timestamp = getRTCClock()->getCurrentTime(); p->path_len = path_len; memcpy(p->path, path, p->path_len); @@ -275,6 +276,20 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path dirty_contacts_expiry = futureMillis(LAZY_CONTACTS_WRITE_DELAY); } +static int sort_by_recent(const void *a, const void *b) { + return ((AdvertPath *) b)->recv_timestamp - ((AdvertPath *) a)->recv_timestamp; +} + +int MyMesh::getRecentlyHeard(AdvertPath dest[], int max_num) { + if (max_num > ADVERT_PATH_TABLE_SIZE) max_num = ADVERT_PATH_TABLE_SIZE; + qsort(advert_paths, ADVERT_PATH_TABLE_SIZE, sizeof(advert_paths[0]), sort_by_recent); + + for (int i = 0; i < max_num; i++) { + dest[i] = advert_paths[i]; + } + return max_num; +} + void MyMesh::onContactPathUpdated(const ContactInfo &contact) { out_frame[0] = PUSH_CODE_PATH_UPDATED; memcpy(&out_frame[1], contact.id.pub_key, PUB_KEY_SIZE); diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 01889223..42819813 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -77,6 +77,14 @@ #define REQ_TYPE_KEEP_ALIVE 0x02 #define REQ_TYPE_GET_TELEMETRY_DATA 0x03 +struct AdvertPath { + uint8_t pubkey_prefix[7]; + uint8_t path_len; + char name[32]; + uint32_t recv_timestamp; + uint8_t path[MAX_PATH_SIZE]; +}; + class MyMesh : public BaseChatMesh, public DataStoreHost { public: MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store); @@ -93,6 +101,8 @@ public: bool advert(); void enterCLIRescue(); + int getRecentlyHeard(AdvertPath dest[], int max_num); + protected: float getAirtimeBudgetFactor() const override; int getInterferenceThreshold() const override; @@ -201,12 +211,6 @@ private: AckTableEntry expected_ack_table[EXPECTED_ACK_TABLE_SIZE]; // circular table int next_ack_idx; - struct AdvertPath { - uint8_t pubkey_prefix[7]; - uint8_t path_len; - uint32_t recv_timestamp; - uint8_t path[MAX_PATH_SIZE]; - }; #define ADVERT_PATH_TABLE_SIZE 16 AdvertPath advert_paths[ADVERT_PATH_TABLE_SIZE]; // circular table }; diff --git a/examples/companion_radio/UITask.cpp b/examples/companion_radio/UITask.cpp index a7f03a26..9f5fe5b1 100644 --- a/examples/companion_radio/UITask.cpp +++ b/examples/companion_radio/UITask.cpp @@ -1,8 +1,8 @@ #include "UITask.h" -#include #include #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 -// '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, +#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; - 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 (_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) { - _display->startFrame(); - renderCurrScreen(); - _display->endFrame(); + if (curr) curr->poll(); - _next_refresh = millis() + 1000; // refresh every second + 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 } -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 +char UITask::checkDisplayOn(char c) { if (_display != NULL) { - _displayWasOn = _display->isOn(); // Track display state before any action - if (!_displayWasOn) { - _display->turnOn(); + 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; } -void UITask::handleButtonShortPress() { - MESH_DEBUG_PRINTLN("UITask: short press triggered"); - 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 - } - // Note: Display turn-on and auto-off timer extension are handled by handleButtonAnyPress +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::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.."); - } - _need_refresh = true; -} - +/* 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(); - } -} \ No newline at end of file +*/ diff --git a/examples/companion_radio/UITask.h b/examples/companion_radio/UITask.h index 77ef875f..818779e5 100644 --- a/examples/companion_radio/UITask.h +++ b/examples/companion_radio/UITask.h @@ -2,18 +2,18 @@ #include #include +#include #include -#include +#include +#include #ifdef PIN_BUZZER #include #endif #include "NodePrefs.h" -#include "Button.h" - enum class UIEventType -{ +enum class UIEventType { none, contactMessage, channelMessage, @@ -22,9 +22,12 @@ ack }; +#define MAX_TOP_LEVEL 8 + class UITask { DisplayDriver* _display; mesh::MainBoard* _board; + BaseSerialInterface* _serial; SensorManager* _sensors; #ifdef PIN_BUZZER genericBuzzer buzzer; @@ -32,48 +35,45 @@ class UITask { unsigned long _next_refresh, _auto_off; bool _connected; NodePrefs* _node_prefs; - char _version_info[32]; - char _origin[62]; - char _msg[80]; char _alert[80]; + unsigned long _alert_expiry; int _msgcount; - bool _need_refresh = true; - bool _displayWasOn = false; // Track display state before button press - unsigned long ui_started_at; + unsigned long ui_started_at, next_batt_chck; - // Button handlers -#ifdef PIN_USER_BTN - Button* _userButton = nullptr; -#endif -#ifdef PIN_USER_BTN_ANA - Button* _userButtonAnalog = nullptr; -#endif + UIScreen* splash; + UIScreen* home; + UIScreen* msg_preview; + UIScreen* curr; - void renderCurrScreen(); void userLedHandler(); - void renderBatteryIndicator(uint16_t batteryMilliVolts); // Button action handlers - void handleButtonAnyPress(); - void handleButtonShortPress(); - void handleButtonDoublePress(); - void handleButtonTriplePress(); - void handleButtonQuadruplePress(); - void handleButtonLongPress(); + char checkDisplayOn(char c); + char handleLongPress(char c); + + void setCurrScreen(UIScreen* c); - public: - UITask(mesh::MainBoard* board) : _board(board), _display(NULL), _sensors(NULL) { - _next_refresh = 0; - ui_started_at = 0; - _connected = false; + UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : _board(board), _serial(serial), _display(NULL), _sensors(NULL) { + next_batt_chck = _next_refresh = 0; + ui_started_at = 0; + _connected = false; + curr = NULL; } void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs); + void gotoHomeScreen() { setCurrScreen(home); } + void showAlert(const char* text, int duration_millis); void setHasConnection(bool connected) { _connected = connected; } + bool hasConnection() const { return _connected; } + uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); } + bool isSerialEnabled() const { return _serial->isEnabled(); } + void enableSerial() { _serial->enable(); } + void disableSerial() { _serial->disable(); } + int getMsgCount() const { return _msgcount; } bool hasDisplay() const { return _display != NULL; } - void clearMsgPreview(); + bool isButtonPressed() const; void msgRead(int msgcount); void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount); void soundBuzzer(UIEventType bet = UIEventType::none); diff --git a/examples/companion_radio/icons.h b/examples/companion_radio/icons.h new file mode 100644 index 00000000..5220f409 --- /dev/null +++ b/examples/companion_radio/icons.h @@ -0,0 +1,118 @@ +#pragma once + +#include + +// 'meshcore', 128x13px +static const uint8_t meshcore_logo [] = { + 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, +}; + +static const uint8_t bluetooth_on[] = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x30, 0x00, 0x00, + 0x00, 0x3C, 0x00, 0x00, + 0x00, 0x3E, 0x00, 0x00, + 0x00, 0x3F, 0x80, 0x00, + 0x00, 0x3F, 0xC0, 0x00, + 0x00, 0x3B, 0xE0, 0x00, + 0x30, 0x38, 0xF8, 0x00, + 0x3C, 0x38, 0x7C, 0x00, + 0x3E, 0x38, 0x7C, 0x00, + 0x1F, 0xB8, 0xF8, 0x70, + 0x07, 0xF9, 0xF0, 0x78, + 0x03, 0xFF, 0xC0, 0x78, + 0x00, 0xFF, 0x80, 0x3C, + 0x00, 0x7F, 0x07, 0x1C, + 0x00, 0x7E, 0x07, 0x1C, + 0x03, 0xFF, 0x82, 0x1C, + 0x03, 0xFF, 0xC0, 0x78, + 0x07, 0xFB, 0xE0, 0x78, + 0x0F, 0xB8, 0xF8, 0x70, + 0x3E, 0x38, 0x7C, 0x00, + 0x3C, 0x38, 0x7C, 0x00, + 0x38, 0x38, 0xF8, 0x00, + 0x00, 0x39, 0xF0, 0x00, + 0x00, 0x3F, 0xC0, 0x00, + 0x00, 0x3F, 0x80, 0x00, + 0x00, 0x3E, 0x00, 0x00, + 0x00, 0x3C, 0x00, 0x00, + 0x00, 0x38, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; + +static const uint8_t bluetooth_off[] = { + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x03, 0x80, 0x00, + 0x00, 0x03, 0xC0, 0x00, + 0x00, 0x03, 0xE0, 0x00, + 0x38, 0x03, 0xF8, 0x00, + 0x3C, 0x03, 0xFC, 0x00, + 0x3E, 0x03, 0xBF, 0x00, + 0x0F, 0x83, 0x8F, 0x80, + 0x07, 0xC3, 0x87, 0xC0, + 0x03, 0xF0, 0x03, 0xC0, + 0x00, 0xF8, 0x0F, 0x80, + 0x00, 0x7C, 0x0F, 0x00, + 0x00, 0x1F, 0x0E, 0x00, + 0x00, 0x0F, 0x80, 0x00, + 0x00, 0x07, 0xE0, 0x00, + 0x00, 0x07, 0xF0, 0x00, + 0x00, 0x0F, 0xF8, 0x00, + 0x00, 0x3F, 0xBE, 0x00, + 0x00, 0x7F, 0x9F, 0x00, + 0x00, 0xFB, 0x8F, 0xC0, + 0x03, 0xE3, 0x83, 0xE0, + 0x03, 0xC3, 0x87, 0xF0, + 0x03, 0x83, 0x8F, 0xFC, + 0x00, 0x03, 0xBF, 0x3C, + 0x00, 0x03, 0xFC, 0x1C, + 0x00, 0x03, 0xF8, 0x00, + 0x00, 0x03, 0xE0, 0x00, + 0x00, 0x03, 0xC0, 0x00, + 0x00, 0x03, 0x80, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +}; + +static const uint8_t power_icon[] = { + 0x00, 0x01, 0x80, 0x00, 0x00, 0x03, 0xC0, 0x00, 0x00, 0x03, 0xC0, 0x00, + 0x00, 0x33, 0xCC, 0x00, 0x00, 0xF3, 0xCF, 0x00, 0x01, 0xF3, 0xCF, 0x80, + 0x03, 0xF3, 0xCF, 0xC0, 0x07, 0xF3, 0xCF, 0xE0, 0x0F, 0xE3, 0xC7, 0xF0, + 0x1F, 0xC3, 0xC3, 0xF8, 0x1F, 0x83, 0xC1, 0xF8, 0x3F, 0x03, 0xC0, 0xFC, + 0x3E, 0x03, 0xC0, 0x7C, 0x3E, 0x03, 0xC0, 0x7C, 0x7E, 0x01, 0x80, 0x7E, + 0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E, + 0x7C, 0x00, 0x00, 0x3E, 0x7C, 0x00, 0x00, 0x3E, 0x3E, 0x00, 0x00, 0x7C, + 0x3E, 0x00, 0x00, 0x7C, 0x3F, 0x00, 0x00, 0xFC, 0x1F, 0x80, 0x01, 0xF8, + 0x1F, 0xC0, 0x03, 0xF8, 0x0F, 0xE0, 0x07, 0xF0, 0x0F, 0xF8, 0x1F, 0xF0, + 0x07, 0xFF, 0xFF, 0xE0, 0x03, 0xFF, 0xFF, 0xC0, 0x00, 0xFF, 0xFF, 0x00, + 0x00, 0x3F, 0xFC, 0x00, 0x00, 0x0F, 0xF0, 0x00, +}; + +static const uint8_t advert_icon[] = { +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x30, +0x1C, 0x00, 0x00, 0x38, 0x18, 0x00, 0x00, 0x18, 0x30, 0x00, 0x00, 0x0C, +0x30, 0x60, 0x06, 0x0C, 0x60, 0xE0, 0x07, 0x06, 0x61, 0xC0, 0x03, 0x86, +0xE1, 0x81, 0x81, 0x87, 0xC3, 0x07, 0xE0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3, +0xC3, 0x0F, 0xF0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3, 0xC3, 0x0F, 0xF0, 0xC3, +0xC3, 0x07, 0xE0, 0xC3, 0xC1, 0x83, 0xC1, 0x83, 0x61, 0x80, 0x01, 0x86, +0x60, 0xC0, 0x03, 0x06, 0x70, 0xE0, 0x07, 0x0E, 0x30, 0x40, 0x02, 0x0C, +0x38, 0x00, 0x00, 0x1C, 0x18, 0x00, 0x00, 0x18, 0x0C, 0x00, 0x00, 0x30, +0x04, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; \ No newline at end of file diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 34c30498..d9b3f68b 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -81,7 +81,7 @@ MyMesh the_mesh(radio_driver, fast_rng, rtc_clock, tables, store); #ifdef DISPLAY_CLASS #include "UITask.h" - UITask ui_task(&board); + UITask ui_task(&board, &serial_interface); #endif /* END GLOBAL OBJECTS */ @@ -99,7 +99,10 @@ void setup() { if (display.begin()) { disp = &display; disp->startFrame(); - disp->print("Please wait..."); + #ifdef ST7789 + disp->setTextSize(2); + #endif + disp->drawTextCentered(disp->width() / 2, 28, "Loading..."); disp->endFrame(); } #endif diff --git a/src/helpers/esp32/SerialBLEInterface.cpp b/src/helpers/esp32/SerialBLEInterface.cpp index 8a8710a7..1be703a8 100644 --- a/src/helpers/esp32/SerialBLEInterface.cpp +++ b/src/helpers/esp32/SerialBLEInterface.cpp @@ -83,6 +83,7 @@ void SerialBLEInterface::onConnect(BLEServer* pServer) { void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) { BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", param->connect.conn_id, pServer->getPeerMTU(param->connect.conn_id)); + last_conn_id = param->connect.conn_id; } void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) { @@ -143,6 +144,7 @@ void SerialBLEInterface::disable() { BLE_DEBUG_PRINTLN("SerialBLEInterface::disable"); pServer->getAdvertising()->stop(); + pServer->disconnect(last_conn_id); pService->stop(); oldDeviceConnected = deviceConnected = false; adv_restart_time = 0; diff --git a/src/helpers/esp32/SerialBLEInterface.h b/src/helpers/esp32/SerialBLEInterface.h index bf1eee09..29ad897a 100644 --- a/src/helpers/esp32/SerialBLEInterface.h +++ b/src/helpers/esp32/SerialBLEInterface.h @@ -13,6 +13,7 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE bool deviceConnected; bool oldDeviceConnected; bool _isEnabled; + uint16_t last_conn_id; uint32_t _pin_code; unsigned long _last_write; unsigned long adv_restart_time; @@ -56,6 +57,7 @@ public: adv_restart_time = 0; _isEnabled = false; _last_write = 0; + last_conn_id = 0; send_queue_len = recv_queue_len = 0; } diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index a8c11d97..8049f5c0 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -115,6 +115,16 @@ void SerialBLEInterface::enable() { void SerialBLEInterface::disable() { _isEnabled = false; BLE_DEBUG_PRINTLN("SerialBLEInterface::disable"); + + uint16_t conn_id; + if (Bluefruit.getConnectedHandles(&conn_id, 1) > 0) { + Bluefruit.disconnect(conn_id); + } + + Bluefruit.Advertising.restartOnDisconnect(false); + Bluefruit.Advertising.stop(); + Bluefruit.Advertising.clearData(); + stopAdv(); } diff --git a/src/helpers/nrf52/T114Board.h b/src/helpers/nrf52/T114Board.h index 154ccb22..cd58134d 100644 --- a/src/helpers/nrf52/T114Board.h +++ b/src/helpers/nrf52/T114Board.h @@ -60,5 +60,9 @@ public: NVIC_SystemReset(); } + void powerOff() override { + sd_power_system_off(); + } + bool startOTAUpdate(const char* id, char reply[]) override; }; diff --git a/src/helpers/nrf52/ThinkNodeM1Board.h b/src/helpers/nrf52/ThinkNodeM1Board.h index 97334bd3..c1ffcbbf 100644 --- a/src/helpers/nrf52/ThinkNodeM1Board.h +++ b/src/helpers/nrf52/ThinkNodeM1Board.h @@ -55,4 +55,8 @@ public: void reboot() override { NVIC_SystemReset(); } + + void powerOff() override { + sd_power_system_off(); + } }; diff --git a/src/helpers/ui/DisplayDriver.h b/src/helpers/ui/DisplayDriver.h index 2d8b69c1..d81d99fb 100644 --- a/src/helpers/ui/DisplayDriver.h +++ b/src/helpers/ui/DisplayDriver.h @@ -21,9 +21,15 @@ public: virtual void setColor(Color c) = 0; virtual void setCursor(int x, int y) = 0; virtual void print(const char* str) = 0; + virtual void printWordWrap(const char* str, int max_width) { print(str); } // fallback to basic print() if no override virtual void fillRect(int x, int y, int w, int h) = 0; virtual void drawRect(int x, int y, int w, int h) = 0; virtual void drawXbm(int x, int y, const uint8_t* bits, int w, int h) = 0; virtual uint16_t getTextWidth(const char* str) = 0; + virtual void drawTextCentered(int mid_x, int y, const char* str) { // helper method (override to optimise) + int w = getTextWidth(str); + setCursor(mid_x - w/2, y); + print(str); + } virtual void endFrame() = 0; }; diff --git a/src/helpers/ui/GxEPDDisplay.cpp b/src/helpers/ui/GxEPDDisplay.cpp index 875e29ac..50df9d10 100644 --- a/src/helpers/ui/GxEPDDisplay.cpp +++ b/src/helpers/ui/GxEPDDisplay.cpp @@ -47,6 +47,7 @@ void GxEPDDisplay::clear() { void GxEPDDisplay::startFrame(Color bkg) { display.fillScreen(GxEPD_WHITE); + display.setTextColor(_curr_color = GxEPD_BLACK); } void GxEPDDisplay::setTextSize(int sz) { @@ -67,7 +68,11 @@ void GxEPDDisplay::setTextSize(int sz) { } void GxEPDDisplay::setColor(Color c) { - display.setTextColor(GxEPD_BLACK); + if (c == DARK) { + display.setTextColor(_curr_color = GxEPD_BLACK); + } else { + display.setTextColor(_curr_color = GxEPD_WHITE); + } } void GxEPDDisplay::setCursor(int x, int y) { @@ -79,11 +84,11 @@ void GxEPDDisplay::print(const char* str) { } void GxEPDDisplay::fillRect(int x, int y, int w, int h) { - display.fillRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, GxEPD_BLACK); + display.fillRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, _curr_color); } void GxEPDDisplay::drawRect(int x, int y, int w, int h) { - display.drawRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, GxEPD_BLACK); + display.drawRect(x*SCALE_X, y*SCALE_Y, w*SCALE_X, h*SCALE_Y, _curr_color); } void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { @@ -116,7 +121,7 @@ void GxEPDDisplay::drawXbm(int x, int y, const uint8_t* bits, int w, int h) { // If the bit is set, draw a block of pixels if (bitSet) { // Draw the block as a filled rectangle - display.fillRect(x1, y1, block_w, block_h, GxEPD_BLACK); + display.fillRect(x1, y1, block_w, block_h, _curr_color); } } } diff --git a/src/helpers/ui/GxEPDDisplay.h b/src/helpers/ui/GxEPDDisplay.h index ec2bcec0..49746dee 100644 --- a/src/helpers/ui/GxEPDDisplay.h +++ b/src/helpers/ui/GxEPDDisplay.h @@ -28,6 +28,7 @@ class GxEPDDisplay : public DisplayDriver { GxEPD2_BW display; bool _init = false; bool _isOn = false; + uint16_t _curr_color; public: // there is a margin in y... diff --git a/src/helpers/ui/MomentaryButton.cpp b/src/helpers/ui/MomentaryButton.cpp new file mode 100644 index 00000000..783f7ba7 --- /dev/null +++ b/src/helpers/ui/MomentaryButton.cpp @@ -0,0 +1,74 @@ +#include "MomentaryButton.h" + +MomentaryButton::MomentaryButton(int8_t pin, int long_press_millis, bool reverse) { + _pin = pin; + _reverse = reverse; + down_at = 0; + prev = _reverse ? HIGH : LOW; + cancel = 0; + _long_millis = long_press_millis; +} + +void MomentaryButton::begin(bool pulldownup) { + if (_pin >= 0) { + pinMode(_pin, pulldownup ? (_reverse ? INPUT_PULLUP : INPUT_PULLDOWN) : INPUT); + } +} + +bool MomentaryButton::isPressed() const { + return isPressed(digitalRead(_pin)); +} + +void MomentaryButton::cancelClick() { + cancel = 1; +} + +bool MomentaryButton::isPressed(int level) const { + if (_reverse) { + return level == LOW; + } else { + return level != LOW; + } +} + +int MomentaryButton::check(bool repeat_click) { + if (_pin < 0) return BUTTON_EVENT_NONE; + + int event = BUTTON_EVENT_NONE; + int btn = digitalRead(_pin); + if (btn != prev) { + if (isPressed(btn)) { + down_at = millis(); + } else { + // button UP + if (_long_millis > 0) { + if (down_at > 0 && (unsigned long)(millis() - down_at) < _long_millis) { // only a CLICK if still within the long_press millis + event = BUTTON_EVENT_CLICK; + } + } else { + event = BUTTON_EVENT_CLICK; // any UP results in CLICK event when NOT using long_press feature + } + if (event == BUTTON_EVENT_CLICK && cancel) { + event = BUTTON_EVENT_NONE; + } + down_at = 0; + } + prev = btn; + } + if (!isPressed(btn) && cancel) { // always clear the pending 'cancel' once button is back in UP state + cancel = 0; + } + + if (_long_millis > 0 && down_at > 0 && (unsigned long)(millis() - down_at) >= _long_millis) { + event = BUTTON_EVENT_LONG_PRESS; + down_at = 0; + } + if (down_at > 0 && repeat_click) { + unsigned long diff = (unsigned long)(millis() - down_at); + if (diff >= 700) { + event = BUTTON_EVENT_CLICK; // wait 700 millis before repeating the click events + } + } + + return event; +} \ No newline at end of file diff --git a/src/helpers/ui/MomentaryButton.h b/src/helpers/ui/MomentaryButton.h new file mode 100644 index 00000000..c46561e3 --- /dev/null +++ b/src/helpers/ui/MomentaryButton.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +#define BUTTON_EVENT_NONE 0 +#define BUTTON_EVENT_CLICK 1 +#define BUTTON_EVENT_LONG_PRESS 2 + +class MomentaryButton { + int8_t _pin; + int8_t prev, cancel; + bool _reverse; + int _long_millis; + unsigned long down_at; + + bool isPressed(int level) const; + +public: + MomentaryButton(int8_t pin, int long_press_mills=0, bool reverse=false); + void begin(bool pulldownup=false); + int check(bool repeat_click=false); // returns one of BUTTON_EVENT_* + void cancelClick(); // suppress next BUTTON_EVENT_CLICK (if already in DOWN state) + uint8_t getPin() { return _pin; } + bool isPressed() const; +}; diff --git a/src/helpers/ui/ST7789Display.cpp b/src/helpers/ui/ST7789Display.cpp index 9e71e6bd..185ecc0e 100644 --- a/src/helpers/ui/ST7789Display.cpp +++ b/src/helpers/ui/ST7789Display.cpp @@ -62,6 +62,9 @@ void ST7789Display::clear() { void ST7789Display::startFrame(Color bkg) { display.clear(); + _color = ST77XX_WHITE; + display.setRGB(_color); + display.setFont(ArialMT_Plain_16); } void ST7789Display::setTextSize(int sz) { @@ -81,7 +84,9 @@ void ST7789Display::setColor(Color c) { switch (c) { case DisplayDriver::DARK : _color = ST77XX_BLACK; + display.setColor(OLEDDISPLAY_COLOR::BLACK); break; +#if 0 case DisplayDriver::LIGHT : _color = ST77XX_WHITE; break; @@ -100,8 +105,10 @@ void ST7789Display::setColor(Color c) { case DisplayDriver::ORANGE : _color = ST77XX_ORANGE; break; +#endif default: _color = ST77XX_WHITE; + display.setColor(OLEDDISPLAY_COLOR::WHITE); break; } display.setRGB(_color); @@ -116,6 +123,10 @@ void ST7789Display::print(const char* str) { display.drawString(_x, _y, str); } +void ST7789Display::printWordWrap(const char* str, int max_width) { + display.drawStringMaxWidth(_x, _y, max_width*SCALE_X, str); +} + void ST7789Display::fillRect(int x, int y, int w, int h) { display.fillRect(x*SCALE_X + X_OFFSET, y*SCALE_Y + Y_OFFSET, w*SCALE_X, h*SCALE_Y); } diff --git a/src/helpers/ui/ST7789Display.h b/src/helpers/ui/ST7789Display.h index b267a2cb..8056de81 100644 --- a/src/helpers/ui/ST7789Display.h +++ b/src/helpers/ui/ST7789Display.h @@ -27,6 +27,7 @@ public: void setColor(Color c) override; void setCursor(int x, int y) override; void print(const char* str) override; + void printWordWrap(const char* str, int max_width) override; void fillRect(int x, int y, int w, int h) override; void drawRect(int x, int y, int w, int h) override; void drawXbm(int x, int y, const uint8_t* bits, int w, int h) override; diff --git a/src/helpers/ui/UIScreen.h b/src/helpers/ui/UIScreen.h new file mode 100644 index 00000000..6faa591a --- /dev/null +++ b/src/helpers/ui/UIScreen.h @@ -0,0 +1,21 @@ +#pragma once + +#include "DisplayDriver.h" + +#define KEY_LEFT 0xB4 +#define KEY_UP 0xB5 +#define KEY_DOWN 0xB6 +#define KEY_RIGHT 0xB7 +#define KEY_SELECT 10 +#define KEY_ENTER 13 +#define KEY_BACK 27 // Esc + +class UIScreen { +protected: + UIScreen() { } +public: + virtual int render(DisplayDriver& display) =0; // return value is number of millis until next render + virtual bool handleInput(char c) { return false; } + virtual void poll() { } +}; + diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 08eb500e..ec0956d8 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -6,6 +6,7 @@ build_flags = ${sensor_base.build_flags} -I variants/heltec_v3 -D HELTEC_LORA_V3 + -D ESP32_CPU_FREQ=80 -D RADIO_CLASS=CustomSX1262 -D WRAPPER_CLASS=CustomSX1262Wrapper -D LORA_TX_POWER=22 @@ -46,6 +47,7 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter} lib_deps = ${Heltec_lora32_v3.lib_deps} ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 [env:Heltec_v3_room_server] extends = Heltec_lora32_v3 @@ -91,6 +93,7 @@ build_flags = ; NOTE: DO NOT ENABLE --> -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + +<../examples/companion_radio> lib_deps = ${Heltec_lora32_v3.lib_deps} @@ -100,16 +103,18 @@ lib_deps = extends = Heltec_lora32_v3 build_flags = ${Heltec_lora32_v3.build_flags} - -D MAX_CONTACTS=100 + -D MAX_CONTACTS=160 -D MAX_GROUP_CHANNELS=8 -D DISPLAY_CLASS=SSD1306Display -D BLE_PIN_CODE=123456 ; dynamic, random PIN + -D AUTO_SHUTDOWN_MILLIVOLTS=3400 -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + + +<../examples/companion_radio> lib_deps = @@ -130,6 +135,7 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + + +<../examples/companion_radio> lib_deps = @@ -172,6 +178,7 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter} lib_deps = ${Heltec_lora32_v3.lib_deps} ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 [env:Heltec_WSL3_room_server] extends = Heltec_lora32_v3 @@ -237,3 +244,49 @@ build_src_filter = ${Heltec_lora32_v3.build_src_filter} lib_deps = ${Heltec_lora32_v3.lib_deps} ${esp32_ota.lib_deps} + +[env:Heltec_WSL3_espnow_bridge] +extends = Heltec_lora32_v3 +build_flags = + ${Heltec_lora32_v3.build_flags} +; -D LORA_FREQ=915.8 + -D MESH_PACKET_LOGGING=1 + -D ENV_INCLUDE_AHTX0=0 + -D ENV_INCLUDE_BME280=0 + -D ENV_INCLUDE_BMP280=0 + -D ENV_INCLUDE_INA3221=0 + -D ENV_INCLUDE_INA219=0 + -D ENV_INCLUDE_MLX90614=0 + -D ENV_INCLUDE_VL53L0X=0 + -D ENV_INCLUDE_GPS=0 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + +<../examples/simple_bridge/main.cpp> + + +lib_deps = + ${Heltec_lora32_v3.lib_deps} + bakercp/CRC32 @ ^2.0.0 + +[env:Heltec_WSL3_serial_bridge] +extends = Heltec_lora32_v3 +build_flags = + ${Heltec_lora32_v3.build_flags} +; -D LORA_FREQ=915.8 + -D MESH_PACKET_LOGGING=1 + -D SERIAL_BRIDGE_RX=47 + -D SERIAL_BRIDGE_TX=48 + -D ENV_INCLUDE_AHTX0=0 + -D ENV_INCLUDE_BME280=0 + -D ENV_INCLUDE_BMP280=0 + -D ENV_INCLUDE_INA3221=0 + -D ENV_INCLUDE_INA219=0 + -D ENV_INCLUDE_MLX90614=0 + -D ENV_INCLUDE_VL53L0X=0 + -D ENV_INCLUDE_GPS=0 +; -D MESH_DEBUG=1 +build_src_filter = ${Heltec_lora32_v3.build_src_filter} + +<../examples/simple_bridge/main.cpp> + +<../examples/simple_bridge/SerialBridgeRadio.cpp> +lib_deps = + ${Heltec_lora32_v3.lib_deps} + bakercp/CRC32 @ ^2.0.0 diff --git a/variants/heltec_v3/target.cpp b/variants/heltec_v3/target.cpp index 4cbc78fb..78b88197 100644 --- a/variants/heltec_v3/target.cpp +++ b/variants/heltec_v3/target.cpp @@ -25,6 +25,7 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #ifdef DISPLAY_CLASS DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); #endif bool radio_init() { diff --git a/variants/heltec_v3/target.h b/variants/heltec_v3/target.h index 992a3d2c..b2125664 100644 --- a/variants/heltec_v3/target.h +++ b/variants/heltec_v3/target.h @@ -10,6 +10,7 @@ #include #ifdef DISPLAY_CLASS #include + #include #endif extern HeltecV3Board board; @@ -19,6 +20,7 @@ extern EnvironmentSensorManager sensors; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; #endif bool radio_init(); diff --git a/variants/t114/platformio.ini b/variants/t114/platformio.ini index dac12da9..815ef173 100644 --- a/variants/t114/platformio.ini +++ b/variants/t114/platformio.ini @@ -30,6 +30,7 @@ build_src_filter = ${nrf52840_t114.build_src_filter} + +<../variants/t114> + + + + + lib_deps = diff --git a/variants/t114/target.cpp b/variants/t114/target.cpp index d97c03f6..d2fa6c4c 100644 --- a/variants/t114/target.cpp +++ b/variants/t114/target.cpp @@ -16,6 +16,7 @@ T114SensorManager sensors = T114SensorManager(nmea); #ifdef DISPLAY_CLASS DISPLAY_CLASS display; + MomentaryButton user_btn(PIN_USER_BTN, 1000, true); #endif bool radio_init() { diff --git a/variants/t114/target.h b/variants/t114/target.h index 8831d9f7..35e86f60 100644 --- a/variants/t114/target.h +++ b/variants/t114/target.h @@ -10,6 +10,7 @@ #include #ifdef DISPLAY_CLASS #include + #include #endif class T114SensorManager : public SensorManager { @@ -37,6 +38,7 @@ extern T114SensorManager sensors; #ifdef DISPLAY_CLASS extern DISPLAY_CLASS display; + extern MomentaryButton user_btn; #endif bool radio_init(); diff --git a/variants/xiao_c3/platformio.ini b/variants/xiao_c3/platformio.ini index 3e4bfdb4..59f198e0 100644 --- a/variants/xiao_c3/platformio.ini +++ b/variants/xiao_c3/platformio.ini @@ -61,6 +61,7 @@ build_flags = lib_deps = ${Xiao_esp32_C3.lib_deps} ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 [env:Xiao_C3_companion_radio_ble] extends = Xiao_esp32_C3 @@ -127,6 +128,7 @@ build_flags = lib_deps = ${Xiao_esp32_C3_custom.lib_deps} ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0 [env:Xiao_C3_Repeater_sx1268_custom] extends = Xiao_esp32_C3_custom @@ -146,4 +148,5 @@ build_flags = ; -D MESH_DEBUG=1 lib_deps = ${Xiao_esp32_C3_custom.lib_deps} - ${esp32_ota.lib_deps} \ No newline at end of file + ${esp32_ota.lib_deps} + bakercp/CRC32 @ ^2.0.0