diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 3626dbc6..3650b903 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -227,7 +227,7 @@ static int battMvToPercent(int mv, int low_mv) { // digits fill the narrow width. On wide panels (OLED, landscape e-ink) the // classic single-line "HH:MM" at size 2 is kept. static int drawClockTime(DisplayDriver& d, int top_y, const struct tm* ti, - bool h12, bool show_sec) { + bool h12, bool show_sec, bool alignCenter = false) { const bool tall = d.height() > d.width(); // true only on portrait e-ink if (tall) { @@ -248,13 +248,27 @@ static int drawClockTime(DisplayDriver& d, int top_y, const struct tm* ti, // Centre on the visible width (minus that trailing column) instead. const int trail = d.getCharWidth() / 6; // one built-in column at this size auto drawBig = [&](const char* s, int yy) { - int w = (int)d.getTextWidth(s) - trail; - d.setCursor(cx - w / 2, yy); - d.print(s); + if (alignCenter) { + int w = (int)d.getTextWidth(s) - trail; + d.setCursor(cx - w / 2, yy); + d.print(s); + } else { + d.setCursor(0, yy); + d.print(s); + } }; drawBig(hbuf, y); y += lhb + 2; drawBig(mbuf, y); y += lhb + 2; - if (ap) { d.setTextSize(2); d.drawTextCentered(cx, y, ap); y += d.getLineHeight() + 1; } + if (ap) { + d.setTextSize(2); + if (alignCenter) { + d.drawTextCentered(cx, y, ap); + } else { + d.setCursor(0, y); + d.print(ap); + } + y += d.getLineHeight() + 1; + } d.setTextSize(1); return y; } @@ -272,12 +286,21 @@ static int drawClockTime(DisplayDriver& d, int top_y, const struct tm* ti, if (show_sec) snprintf(buf, sizeof(buf), "%02d:%02d:%02d", ti->tm_hour, ti->tm_min, ti->tm_sec); else snprintf(buf, sizeof(buf), "%02d:%02d", ti->tm_hour, ti->tm_min); } - d.drawTextCentered(d.width() / 2, top_y, buf); + if (alignCenter) { + d.drawTextCentered(d.width() / 2, top_y, buf); + } else { + d.setCursor(0, top_y); + d.print(buf); + } d.setTextSize(1); return top_y + lh2 + 2; } // ── HomeScreen ──────────────────────────────────────────────────────────────── +// Forward declaration to be able to call formatDashVal from HomeScreen::render() +static void formatDashVal(uint8_t field, char* val, int val_len, uint16_t batt_mv, + uint16_t low_batt_mv, int unread, CayenneLPP* lpp = nullptr); + class HomeScreen : public UIScreen { enum HomePage { CLOCK, @@ -297,7 +320,8 @@ class HomeScreen : public UIScreen { TOOLS, QUICK_MSG, SHUTDOWN, - Count // keep as last + Count, // keep before LOCK — navigable-page count + LOCK // lock screen: full clock + dashboard view, not a navigable page }; // Selected slot on the Favourites page (0..FAVOURITES_COUNT - 1). @@ -322,6 +346,7 @@ class HomeScreen : public UIScreen { SensorManager* _sensors; NodePrefs* _node_prefs; uint8_t _page; + uint8_t _prev_page; // home page restored when the device unlocks bool _shutdown_init; int pageBit(int page) const { @@ -530,7 +555,17 @@ class HomeScreen : public UIScreen { 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), sensors_lpp(200) { } + _prev_page(0), _shutdown_init(false), sensors_lpp(200) { } + + // Flips home screen between LOCK page and the page that was showing before locking + void setLocked(bool locked) { + if (locked) { + if (_page != LOCK) _prev_page = _page; + _page = LOCK; + } else { + _page = _prev_page; + } + } void poll() override { if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released @@ -709,25 +744,30 @@ public: const int dots_y = lh + pg_half + 1; // icon-row centre, below the header const int content_y = dots_y + pg_half + 3; // first content row, below the icons - // node name + battery — hidden on CLOCK page (full screen used for dashboard) + // Title bar displaying node name (except on lock screen), status icons and battery. + // Hidden on fullscreen pages (CLOCK). if (_page != CLOCK) { display.setColor(DisplayDriver::LIGHT); - char filtered_name[sizeof(_node_prefs->node_name)]; - display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name)); int rightEdge = renderBatteryIndicator(display, _task->getBattMilliVolts()); display.setColor(DisplayDriver::LIGHT); - // Only show the live-power readout when APC is actually controlling power — - // not merely when the pref is set. While repeating APC is suppressed and - // power is pinned to the ceiling, so apcActive() is false and the name bar - // drops the readout (matching the "--" lock in Settings). - if (the_mesh.apcActive()) { - char pwr_buf[8]; - snprintf(pwr_buf, sizeof(pwr_buf), "%ddB", (int)radio_driver.getTxPower()); - int pwr_w = display.getTextWidth(pwr_buf); - display.drawTextEllipsized(0, 0, rightEdge - 2 - pwr_w - 2, filtered_name); - display.drawTextRightAlign(rightEdge - 2, 0, pwr_buf); - } else { - display.drawTextEllipsized(0, 0, rightEdge - 2, filtered_name); + + if (_page != LOCK) { + char filtered_name[sizeof(_node_prefs->node_name)]; + display.translateUTF8ToBlocks(filtered_name, _node_prefs->node_name, sizeof(filtered_name)); + + // Only show the live-power readout when APC is actually controlling power — + // not merely when the pref is set. While repeating APC is suppressed and + // power is pinned to the ceiling, so apcActive() is false and the name bar + // drops the readout (matching the "--" lock in Settings). + if (the_mesh.apcActive()) { + char pwr_buf[8]; + snprintf(pwr_buf, sizeof(pwr_buf), "%ddB", (int)radio_driver.getTxPower()); + int pwr_w = display.getTextWidth(pwr_buf); + display.drawTextEllipsized(0, 0, rightEdge - 2 - pwr_w - 2, filtered_name); + display.drawTextRightAlign(rightEdge - 2, 0, pwr_buf); + } else { + display.drawTextEllipsized(0, 0, rightEdge - 2, filtered_name); + } } } @@ -735,8 +775,9 @@ public: if (!isPageVisible(_page)) _page = navPage(_page, +1); // curr page indicator — a row of small page icons, one per visible page, with - // the current page underlined. Hidden on CLOCK (full screen used for dashboard). - if (_page != CLOCK) { + // the current page underlined. Hidden on CLOCK and LOCK (full screen used for + // the clock/dashboard). + if (_page != CLOCK && _page != LOCK) { int order[(int)Count]; int n = buildVisibleOrder(order); int curr_vis = 0; for (int i = 0; i < n; i++) if (order[i] == _page) { curr_vis = i; break; } @@ -776,7 +817,7 @@ public: display.setColor(DisplayDriver::LIGHT); bool show_sec = !Features::IS_EINK && (!_node_prefs || !_node_prefs->clock_hide_seconds); bool h12 = _node_prefs && _node_prefs->clock_12h; - int date_y = drawClockTime(display, 0, ti, h12, show_sec); + int date_y = drawClockTime(display, 0, ti, h12, show_sec, true); display.setTextSize(1); static const char* wd[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; @@ -883,6 +924,79 @@ public: } } } + } else if (_page == HomePage::LOCK) { + // Lock screen: clock + two dashboard spots + unlock-hint popup + uint32_t unix_ts = _rtc->getCurrentTime(); + display.setColor(DisplayDriver::LIGHT); + display.setTextSize(1); + if (unix_ts < 1000000000UL) { + display.drawTextCentered(display.width() / 2, display.height() / 2 - step, "No time sync"); + } else { + int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0; + unix_ts += (int32_t)tz * 3600; + time_t t = (time_t)unix_ts; + struct tm* ti = gmtime(&t); + char buf[12]; + bool h12 = _node_prefs && _node_prefs->clock_12h; + int date_y = drawClockTime(display, 0, ti, h12, /*show_sec*/false); + display.setTextSize(1); + static const char* wd[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; + static const char* mo[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"}; + snprintf(buf, sizeof(buf),"%s %d %s", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon]); + display.setCursor(0, date_y); + display.print(buf); + + // Two sensor values side by side (dashboard_fields[0] and [1]) + if (_node_prefs) { + char v0[20] = "", v1[20] = ""; + CayenneLPP* lpp_ptr = nullptr; + uint8_t f0 = _node_prefs->dashboard_fields[0], f1 = _node_prefs->dashboard_fields[1]; + auto isLPP = [](uint8_t f) { + return f==DASH_TEMP||f==DASH_HUM||f==DASH_PRES||f==DASH_ALT||f==DASH_LUX||f==DASH_CO2; + }; + if (isLPP(f0) || isLPP(f1)) { + sensors_lpp.reset(); sensors.querySensors(0xFF, sensors_lpp); lpp_ptr = &sensors_lpp; + } + int unread = (f0 == DASH_MSGS || f1 == DASH_MSGS) + ? _task->getDMUnreadTotal() + _task->getChannelUnreadCount() + _task->getRoomUnreadCount() : 0; + uint16_t batt_mv = _task->getBattMilliVolts(); + formatDashVal(f0, v0, sizeof(v0), batt_mv, _node_prefs->low_batt_mv, unread, lpp_ptr); + formatDashVal(f1, v1, sizeof(v1), batt_mv, _node_prefs->low_batt_mv, unread, lpp_ptr); + if (v0[0] || v1[0]) { + int sv_y = date_y + step; + display.setColor(DisplayDriver::LIGHT); + if (v0[0] && v1[0]) { + display.setCursor(0, sv_y); + display.print(v0); + int vw = display.getTextWidth(v1); + display.setCursor(display.width() - vw, sv_y); + display.print(v1); + } else { + const char* sv = v0[0] ? v0 : v1; + display.drawTextCentered(display.width() / 2, sv_y, sv); + } + } + } + } + // Unlock-hint popup at the bottom (like alert style) + display.setTextSize(1); + const int lk_lh = display.getLineHeight(); +#if defined(CARDKB_I2C) + const char* hint = _task->lockSeqCount() == 0 ? (_task->hasCardKB() ? "Back+3xEnter/Fn+Esc" : "Hold Back + 3xEnter") : + _task->lockSeqCount() == 1 ? "Enter x2 more..." : "Enter x1 more..."; +#else + const char* hint = _task->lockSeqCount() == 0 ? "Hold Back + 3xEnter" : + _task->lockSeqCount() == 1 ? "Enter x2 more..." : "Enter x1 more..."; +#endif + const int p = 3; + const int hy = display.height() - lk_lh - p * 2; + const int hw = display.getTextWidth(hint); + const int hx = (display.width() - hw) / 2; + display.setColor(DisplayDriver::LIGHT); + display.fillRect(hx - p, hy - p, hw + p*2, lk_lh + p*2); + display.setColor(DisplayDriver::DARK); + display.setCursor(hx, hy); + display.print(hint); } else if (_page == HomePage::RADIO) { display.setColor(DisplayDriver::LIGHT); // freq / sf @@ -1416,7 +1530,12 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no pinMode(PIN_HALL_SENSOR, HALL_ACTIVE_HIGH ? INPUT_PULLDOWN : INPUT_PULLUP); _hall_magnet_present = HALL_ACTIVE_HIGH ? (digitalRead(PIN_HALL_SENSOR) == HIGH) : (digitalRead(PIN_HALL_SENSOR) == LOW); - if (_hall_magnet_present) _locked = true; // booting with the cover already closed + + // Handle booting with the cover already closed + if (_hall_magnet_present) { + _locked = true; + syncLockToHome(); + } #endif #if defined(PIN_USER_BTN) @@ -1472,7 +1591,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no if (f) { _waypoints.readFrom(f); f.close(); } } } - + // Initialize ping state _ping_active = false; _ping_tag = 0; @@ -1483,6 +1602,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no splash = new SplashScreen(this); home = new HomeScreen(this, &rtc_clock, sensors, node_prefs); + syncLockToHome(); // booted locked (e.g. cover closed) → home starts on the LOCK page settings = new SettingsScreen(this, &_kb); messages_screen = new MessagesScreen(this, &_kb); tools_screen = new ToolsScreen(this); @@ -2001,6 +2121,11 @@ void UITask::setCurrScreen(UIScreen* c) { _next_refresh = 100; } +void UITask::syncLockToHome() { + // Lock/unlock switches the home screen's page to LOCK (or back) + if (home) static_cast(home)->setLocked(_locked); +} + bool UITask::savePrefsIfDirty(bool& dirty) { if (!dirty) return false; the_mesh.savePrefs(); @@ -2088,7 +2213,7 @@ bool UITask::isButtonPressed() const { } static void formatDashVal(uint8_t field, char* val, int val_len, uint16_t batt_mv, - uint16_t low_batt_mv, int unread, CayenneLPP* lpp = nullptr) { + uint16_t low_batt_mv, int unread, CayenneLPP* lpp) { val[0] = '\0'; switch (field) { case DASH_NONE: return; @@ -2338,6 +2463,7 @@ void UITask::pollCardKB() { uint32_t aoff = autoOffMillis(); if (aoff > 0) _auto_off = millis() + aoff; } + syncLockToHome(); _next_refresh = 0; return; } else if (raw >= 0x80 && raw <= 0xAF) { // Fn+ -- open its accent popup @@ -2393,6 +2519,7 @@ void UITask::pollHallSensor() { if (present) { // cover closed _locked = true; + syncLockToHome(); _lock_wake_until = 0; if (_display) _display->turnOff(); #ifdef PIN_LED @@ -2400,6 +2527,7 @@ void UITask::pollHallSensor() { #endif } else { // cover opened _locked = false; + syncLockToHome(); if (_display && !_display->isOn()) _display->turnOn(); uint32_t aoff = autoOffMillis(); if (aoff > 0) _auto_off = millis() + aoff; @@ -2438,6 +2566,7 @@ void UITask::loop() { uint32_t aoff = autoOffMillis(); if (aoff > 0) _auto_off = millis() + aoff; } + syncLockToHome(); } // eat the Enter — don't pass to curr } else { @@ -2669,80 +2798,9 @@ void UITask::loop() { if (_display != NULL && _display->isOn()) { if (_locked && (int32_t)(millis() - _lock_wake_until) >= 0) { _display->turnOff(); - } else if (_locked && millis() >= _next_refresh) { + } else if (_locked && millis() >= _next_refresh && home) { _display->startFrame(); - // Lock screen: clock + unlock hint popup - uint32_t unix_ts = rtc_clock.getCurrentTime(); - _display->setColor(DisplayDriver::LIGHT); - _display->setTextSize(1); - const int lk_lh = _display->getLineHeight(); - const int lk_step = _display->lineStep(); - if (unix_ts < 1000000000UL) { - _display->drawTextCentered(_display->width() / 2, _display->height() / 2 - lk_step, "No time sync"); - } else { - int8_t tz = _node_prefs ? _node_prefs->tz_offset_hours : 0; - unix_ts += (int32_t)tz * 3600; - time_t t = (time_t)unix_ts; - struct tm* ti = gmtime(&t); - char buf[12]; - const int clk_y = 2; - bool h12 = _node_prefs && _node_prefs->clock_12h; - int date_y = drawClockTime(*_display, clk_y, ti, h12, /*show_sec*/false); - _display->setTextSize(1); - static const char* wd[] = {"Sun","Mon","Tue","Wed","Thu","Fri","Sat"}; - static const char* mo[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"}; - snprintf(buf, sizeof(buf),"%s %d %s", wd[ti->tm_wday], ti->tm_mday, mo[ti->tm_mon]); - _display->drawTextCentered(_display->width() / 2, date_y, buf); - - // Two sensor values side by side (dashboard_fields[0] and [1]) - if (_node_prefs) { - char v0[20] = "", v1[20] = ""; - CayenneLPP* lpp_ptr = nullptr; - uint8_t f0 = _node_prefs->dashboard_fields[0], f1 = _node_prefs->dashboard_fields[1]; - auto isLPP = [](uint8_t f) { - return f==DASH_TEMP||f==DASH_HUM||f==DASH_PRES||f==DASH_ALT||f==DASH_LUX||f==DASH_CO2; - }; - if (isLPP(f0) || isLPP(f1)) { - _dash_lpp.reset(); sensors.querySensors(0xFF, _dash_lpp); lpp_ptr = &_dash_lpp; - } - int unread = (f0 == DASH_MSGS || f1 == DASH_MSGS) - ? getDMUnreadTotal() + getChannelUnreadCount() + getRoomUnreadCount() : 0; - formatDashVal(f0, v0, sizeof(v0), _batt_mv, _node_prefs->low_batt_mv, unread, lpp_ptr); - formatDashVal(f1, v1, sizeof(v1), _batt_mv, _node_prefs->low_batt_mv, unread, lpp_ptr); - if (v0[0] || v1[0]) { - int sv_y = date_y + lk_step; - _display->setColor(DisplayDriver::LIGHT); - if (v0[0] && v1[0]) { - _display->setCursor(0, sv_y); - _display->print(v0); - int vw = _display->getTextWidth(v1); - _display->setCursor(_display->width() - vw, sv_y); - _display->print(v1); - } else { - const char* sv = v0[0] ? v0 : v1; - _display->drawTextCentered(_display->width() / 2, sv_y, sv); - } - } - } - } - // Hint popup at bottom (like alert style) - _display->setTextSize(1); -#if defined(CARDKB_I2C) - const char* hint = _lock_seq_count == 0 ? (_has_cardkb ? "Back+3xEnter/Fn+Esc" : "Hold Back + 3xEnter") : - _lock_seq_count == 1 ? "Enter x2 more..." : "Enter x1 more..."; -#else - const char* hint = _lock_seq_count == 0 ? "Hold Back + 3xEnter" : - _lock_seq_count == 1 ? "Enter x2 more..." : "Enter x1 more..."; -#endif - int p = 3; - int hy = _display->height() - lk_lh - p * 2; - int hw = _display->getTextWidth(hint); - int hx = (_display->width() - hw) / 2; - _display->setColor(DisplayDriver::LIGHT); - _display->fillRect(hx - p, hy - p, hw + p*2, lk_lh + p*2); - _display->setColor(DisplayDriver::DARK); - _display->setCursor(hx, hy); - _display->print(hint); + home->render(*_display); // Alert overlay on top — without this a ringing alarm on a locked device // played its melody against a screen that never said what was ringing. if (millis() < _alert_expiry) renderAlertOverlay(); @@ -2789,6 +2847,7 @@ void UITask::loop() { if (_node_prefs && _node_prefs->auto_lock) { _locked = true; _lock_wake_until = 0; + syncLockToHome(); } } #endif diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 2957bba4..aa9d5971 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -114,7 +114,6 @@ class UITask : public AbstractUITask { UIScreen* gpio_screen = nullptr; #endif UIScreen* curr = nullptr; - CayenneLPP _dash_lpp; TrailStore _trail; WaypointStore _waypoints; LiveTrackStore _livetrack; @@ -280,6 +279,11 @@ private: void setCurrScreen(UIScreen* c); + // Mirror _locked into HomeScreen's LOCK page so the lock screen renders via + // the normal home render() path (top-right status bar included) instead of a + // dedicated lock-screen code path in loop(). + void syncLockToHome(); + // Centred alert overlay (the showAlert() box). Wraps long text to up to // three lines inside the box instead of letting it overflow the border. // Shared by the normal render path and the lock screen (so a ringing @@ -288,7 +292,7 @@ private: public: - UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL), _node_prefs(NULL), _dash_lpp(200) { + UITask(mesh::MainBoard* board, BaseSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL), _node_prefs(NULL) { next_batt_chck = _next_refresh = 0; ui_started_at = 0; _batt_mv = 0; @@ -524,6 +528,17 @@ public: } bool isButtonPressed() const; + // Lock-screen support: the HomeScreen LOCK page draws the unlock hint from + // these (the platform combo is either "Back+3xEnter" or the CardKB nybble). + int lockSeqCount() const { return _lock_seq_count; } + bool hasCardKB() const { +#if defined(CARDKB_I2C) + return _has_cardkb; +#else + return false; +#endif + } + bool isBuzzerQuiet() { #ifdef PIN_BUZZER return buzzer.isQuiet();