diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 8077627f8..c6645844b 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -63,7 +63,13 @@ class SplashScreen : public UIScreen { display.drawTextCentered(display.width()/2, 22, _version_info); display.setTextSize(1); +#ifdef OLED_RU + char filtered_date[sizeof(FIRMWARE_BUILD_DATE)]; + display.translateUTF8ToBlocks(filtered_date, FIRMWARE_BUILD_DATE, sizeof(filtered_date)); + display.drawTextCentered(display.width()/2, 42, filtered_date); +#else display.drawTextCentered(display.width()/2, 42, FIRMWARE_BUILD_DATE); +#endif return 1000; } @@ -100,31 +106,51 @@ class HomeScreen : public UIScreen { bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; - 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 minMilliVolts = 3000; + #ifdef AUTO_SHUTDOWN_MILLIVOLTS + minMilliVolts = AUTO_SHUTDOWN_MILLIVOLTS; + #endif + + const int maxMilliVolts = 4200; 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); + batteryPercentage = constrain(batteryPercentage, 0, 100); + + #ifdef TEXT_BATTERY + // ===== TEXT BATTERY ===== + int battBackWidth = 24; + int battBackHeight = 10; + int battBackStartPosY = 0; + int battBackStartPosX = display.width() - battBackWidth - 5; + + String batteryPercText = String(batteryPercentage) + "%"; + int battTextStartPosX = display.width() - 5; - // battery outline - display.drawRect(iconX, iconY, iconWidth, iconHeight); + display.setColor(DisplayDriver::DARK); + display.fillRect(battBackStartPosX, battBackStartPosY, battBackWidth, battBackHeight); + display.setTextSize(1); + display.setColor(DisplayDriver::GREEN); + display.drawTextRightAlign(battTextStartPosX, 1, batteryPercText.c_str()); - // battery "cap" - display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 3, iconHeight / 2); + #else + // ===== ICON BATTERY ===== + int iconWidth = 24; + int iconHeight = 10; + int iconX = display.width() - iconWidth - 5; + int iconY = 0; + + display.setColor(DisplayDriver::GREEN); - // fill the battery based on the percentage - int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; - display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); + // outline + display.drawRect(iconX, iconY, iconWidth, iconHeight); + + // cap + display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 3, iconHeight / 2); + + // fill + int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; + display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); + #endif } CayenneLPP sensors_lpp; @@ -542,6 +568,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no #if defined(PIN_USER_BTN) user_btn.begin(); #endif +#if defined(HAS_ENCODER) + encoder.begin(); +#endif #if defined(PIN_USER_BTN_ANA) analog_btn.begin(); #endif @@ -631,14 +660,24 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i setCurrScreen(msg_preview); if (_display != NULL) { - if (!_display->isOn() && !hasConnection()) { - _display->turnOn(); - } - if (_display->isOn()) { - _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer - _next_refresh = 100; // trigger refresh + + #ifdef DISPLAY_TOGGLE + if (_displayWakeOnMsg) { + #endif + + if (!_display->isOn() && !hasConnection()) { + _display->turnOn(); + } + + #ifdef DISPLAY_TOGGLE + } + #endif + + if (_display->isOn()) { + _auto_off = millis() + AUTO_OFF_MILLIS; + _next_refresh = 100; + } } - } } void UITask::userLedHandler() { @@ -739,6 +778,21 @@ void UITask::loop() { c = handleTripleClick(KEY_SELECT); } #endif +#if defined(HAS_ENCODER) + int enc_ev = encoder.check(); + if (enc_ev == ENC_EVENT_CW) { + c = checkDisplayOn(KEY_RIGHT); + } else if (enc_ev == ENC_EVENT_CCW) { + c = checkDisplayOn(KEY_LEFT); + } else if (enc_ev == ENC_EVENT_BUTTON) { + toggleDisplayWakeupOnMsg(); + c = 0; + } else if (enc_ev == ENC_EVENT_LONG_PRESS) { + turnOnDisplayWakeupOnMsg(); + c = 0; + } +#endif + #if defined(PIN_USER_BTN_ANA) if (abs(millis() - _analogue_pin_read_millis) > 10) { ev = analog_btn.check(); @@ -766,6 +820,33 @@ void UITask::loop() { } #endif +#if defined(DISPLAY_TOGGLE) + bool disp_state = (digitalRead(DISPLAY_TOGGLE) == LOW); // ACTIVE LOW + + // edge: press + if (disp_state && !_dispTglPrevState) { + _dispTglPressStart = millis(); + _dispTglLongHandled = false; + } + + // hold → long press + if (disp_state && !_dispTglLongHandled) { + if (millis() - _dispTglPressStart >= LONG_PRESS_MILLIS) { + turnOnDisplayWakeupOnMsg(); + _dispTglLongHandled = true; + } + } + + // release → click + if (!disp_state && _dispTglPrevState) { + if (!_dispTglLongHandled) { + toggleDisplayWakeupOnMsg(); + } + } + + _dispTglPrevState = disp_state; +#endif + if (c != 0 && curr) { curr->handleInput(c); _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer @@ -921,3 +1002,40 @@ void UITask::toggleBuzzer() { _next_refresh = 0; // trigger refresh #endif } + +void UITask::toggleDisplayWakeupOnMsg() { + if (_display && !_display->isOn()) { + // Display is turned off, only wakeup on the first press + _display->turnOn(); + _auto_off = millis() + AUTO_OFF_MILLIS; + _next_refresh = 0; + return; + } + + // Display is on wake, toggle the flag + _displayWakeOnMsg = !_displayWakeOnMsg; + + showAlert( + _displayWakeOnMsg ? "Msg wake: ON" : "Msg wake: OFF", + 800 + ); + notify(UIEventType::ack); + + _auto_off = millis() + AUTO_OFF_MILLIS; + _next_refresh = 0; +} + +void UITask::turnOnDisplayWakeupOnMsg() { + // Принудительно включаем wake-on-msg + _displayWakeOnMsg = true; + + if (_display && !_display->isOn()) { + _display->turnOn(); + } + + showAlert("Msg wake: ON", 800); + notify(UIEventType::ack); + + _auto_off = millis() + AUTO_OFF_MILLIS; + _next_refresh = 0; +} diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 02c3cafbd..e5a9c8665 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -48,6 +48,15 @@ class UITask : public AbstractUITask { unsigned long _analogue_pin_read_millis = millis(); #endif +bool _displayWakeOnMsg = true; + +#if defined(DISPLAY_TOGGLE) + bool _dispTglPrevState = false; + uint32_t _dispTglPressStart = 0; + bool _dispTglLongHandled = false; +#endif + + UIScreen* splash; UIScreen* home; UIScreen* msg_preview; @@ -56,6 +65,8 @@ class UITask : public AbstractUITask { void userLedHandler(); // Button action handlers + void turnOnDisplayWakeupOnMsg(); + void toggleDisplayWakeupOnMsg(); char checkDisplayOn(char c); char handleLongPress(char c); char handleDoubleClick(char c); diff --git a/src/helpers/ui/EncoderAndButton.cpp b/src/helpers/ui/EncoderAndButton.cpp new file mode 100644 index 000000000..5d1db69e8 --- /dev/null +++ b/src/helpers/ui/EncoderAndButton.cpp @@ -0,0 +1,99 @@ +#include "EncoderAndButton.h" + +#define ENC_DEBOUNCE_US 800 +#define BTN_DEBOUNCE_MS 25 + +// Valid quadrature transitions table +static const int8_t enc_table[16] = { + 0, -1, 1, 0, + 1, 0, 0, -1, + -1, 0, 0, 1, + 0, 1, -1, 0 +}; + +EncoderAndButton::EncoderAndButton( + int8_t pinA, + int8_t pinB, + int8_t btnPin, + uint16_t longPressMs, + bool pullups +) : + _pinA(pinA), + _pinB(pinB), + _btnPin(btnPin), + _longPressMs(longPressMs) +{ + _state = 0; + _delta = 0; + _btnState = false; + _btnLast = false; + _lastEncTime = 0; + _btnDownAt = 0; + _lastBtnChange = 0; +} + +void EncoderAndButton::begin() { + pinMode(_pinA, INPUT_PULLUP); + pinMode(_pinB, INPUT_PULLUP); + pinMode(_btnPin, INPUT_PULLUP); + + _state = (digitalRead(_pinA) << 1) | digitalRead(_pinB); +} + +bool EncoderAndButton::buttonPressed() const { + return !_btnState; +} + +void EncoderAndButton::readEncoder() { + unsigned long now = micros(); + if (now - _lastEncTime < ENC_DEBOUNCE_US) return; + + _lastEncTime = now; + + _state = ((_state << 2) | + (digitalRead(_pinA) << 1) | + digitalRead(_pinB)) & 0x0F; + + _delta += enc_table[_state]; +} + +int EncoderAndButton::check() { + int event = ENC_EVENT_NONE; + + // --- Encoder --- + readEncoder(); + if (_delta >= 4) { + _delta = 0; + event = ENC_EVENT_CW; + } else if (_delta <= -4) { + _delta = 0; + event = ENC_EVENT_CCW; + } + + // --- Button --- + bool raw = digitalRead(_btnPin); + unsigned long now = millis(); + + if (raw != _btnLast && (now - _lastBtnChange) > BTN_DEBOUNCE_MS) { + _lastBtnChange = now; + _btnLast = raw; + + if (!raw) { + _btnDownAt = now; + } else { + if (_btnDownAt && + (now - _btnDownAt) < _longPressMs) { + event = ENC_EVENT_BUTTON; + } + _btnDownAt = 0; + } + } + + if (_btnDownAt && + (now - _btnDownAt) >= _longPressMs) { + event = ENC_EVENT_LONG_PRESS; + _btnDownAt = 0; + } + + return event; +} diff --git a/src/helpers/ui/EncoderAndButton.h b/src/helpers/ui/EncoderAndButton.h new file mode 100644 index 000000000..ca6fec0a3 --- /dev/null +++ b/src/helpers/ui/EncoderAndButton.h @@ -0,0 +1,40 @@ +#pragma once +#include + +#define ENC_EVENT_NONE 0 +#define ENC_EVENT_CW 1 +#define ENC_EVENT_CCW 2 +#define ENC_EVENT_BUTTON 3 +#define ENC_EVENT_LONG_PRESS 4 + +class EncoderAndButton { +public: + EncoderAndButton( + int8_t pinA, + int8_t pinB, + int8_t btnPin, + uint16_t longPressMs = 1000, + bool pullups = true + ); + + void begin(); + int check(); // returns ENC_EVENT_* + bool buttonPressed() const; + +private: + // encoder + int8_t _pinA, _pinB; + uint8_t _state; + int8_t _delta; + unsigned long _lastEncTime; + + // button + int8_t _btnPin; + bool _btnState; + bool _btnLast; + unsigned long _btnDownAt; + uint16_t _longPressMs; + unsigned long _lastBtnChange; + + void readEncoder(); +}; diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 78ea5fa1e..3e9721c90 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -22,11 +22,14 @@ build_flags = ${nrf52_base.build_flags} -D ENV_INCLUDE_BMP280=1 -D ENV_INCLUDE_INA3221=1 -D ENV_INCLUDE_INA219=1 + -D OLED_RU=1 + -D AUTO_SHUTDOWN_MILLIVOLTS=3100 + -D TEXT_BATTERY build_src_filter = ${nrf52_base.build_src_filter} + +<../variants/promicro> lib_deps= ${nrf52_base.lib_deps} - adafruit/Adafruit SSD1306 @ ^2.5.13 + adafruit/Adafruit SSD1306 @ ^2.5.13 adafruit/Adafruit INA3221 Library @ ^1.0.1 adafruit/Adafruit INA219 @ ^1.2.3 adafruit/Adafruit AHTX0 @ ^2.0.5 @@ -116,12 +119,18 @@ build_flags = ${Promicro.build_flags} -D BLE_DEBUG_LOGGING=1 -D OFFLINE_QUEUE_SIZE=256 -D DISPLAY_CLASS=SSD1306Display -; -D MESH_PACKET_LOGGING=1 - -D MESH_DEBUG=1 + -D HAS_ENCODER + -D PIN_ENC_A=0 + -D PIN_ENC_B=1 + ; -D DISPLAY_TOGGLE=5 + -D PIN_ENCODER_BTN=5 + ;-D MESH_PACKET_LOGGING=1 + ;-D MESH_DEBUG=1 build_src_filter = ${Promicro.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> +<../examples/companion_radio/ui-new/*.cpp> lib_deps = ${Promicro.lib_deps} diff --git a/variants/promicro/target.cpp b/variants/promicro/target.cpp index b26320e47..dadf4f25f 100644 --- a/variants/promicro/target.cpp +++ b/variants/promicro/target.cpp @@ -21,6 +21,11 @@ AutoDiscoverRTCClock rtc_clock(fallback_clock); #ifdef DISPLAY_CLASS DISPLAY_CLASS display; MomentaryButton user_btn(PIN_USER_BTN, 1000, true, true); + #define UI_HAS_DISPLAY 1 +#endif + +#if defined(UI_HAS_DISPLAY) && defined(HAS_ENCODER) + EncoderAndButton encoder(PIN_ENC_A, PIN_ENC_B, PIN_ENCODER_BTN, 1200); #endif bool radio_init() { diff --git a/variants/promicro/target.h b/variants/promicro/target.h index 38c4b4e88..633dc537b 100644 --- a/variants/promicro/target.h +++ b/variants/promicro/target.h @@ -9,6 +9,12 @@ #ifdef DISPLAY_CLASS #include #include + #define UI_HAS_DISPLAY 1 +#endif + +#if defined(UI_HAS_DISPLAY) && defined(HAS_ENCODER) + #include + extern EncoderAndButton encoder; #endif #include diff --git a/variants/promicro/variant.cpp b/variants/promicro/variant.cpp index 0a4c3aac5..94d2fc2b6 100644 --- a/variants/promicro/variant.cpp +++ b/variants/promicro/variant.cpp @@ -11,5 +11,6 @@ const uint32_t g_ADigitalPinMap[] = { void initVariant() { + pinMode(DISPLAY_TOGGLE, INPUT_PULLUP); } diff --git a/variants/promicro/variant.h b/variants/promicro/variant.h index 98489da19..afd3b6a9d 100644 --- a/variants/promicro/variant.h +++ b/variants/promicro/variant.h @@ -78,5 +78,6 @@ #define PIN_BUTTON1 (6) #define BUTTON_PIN PIN_BUTTON1 +#define DISPLAY_TOGGLE (-1)