diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 8077627f8..f4777ee2a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -102,29 +102,50 @@ class HomeScreen : public UIScreen { 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; - // battery outline - display.drawRect(iconX, iconY, iconWidth, iconHeight); + String batteryPercText = String(batteryPercentage) + "%"; + int battTextStartPosX = display.width() - 5; + + display.setColor(DisplayDriver::DARK); + display.fillRect(battBackStartPosX, battBackStartPosY, battBackWidth, battBackHeight); + display.setTextSize(1); + display.setColor(DisplayDriver::GREEN); + display.drawTextRightAlign(battTextStartPosX, 1, batteryPercText.c_str()); + + #else + // ===== ICON BATTERY ===== + int iconWidth = 24; + int iconHeight = 10; + int iconX = display.width() - iconWidth - 5; + int iconY = 0; + + display.setColor(DisplayDriver::GREEN); - // battery "cap" - display.fillRect(iconX + iconWidth, iconY + (iconHeight / 4), 3, iconHeight / 2); + // outline + display.drawRect(iconX, iconY, iconWidth, iconHeight); - // fill the battery based on the percentage - int fillWidth = (batteryPercentage * (iconWidth - 4)) / 100; - display.fillRect(iconX + 2, iconY + 2, fillWidth, iconHeight - 4); + // 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 +563,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 @@ -739,6 +763,18 @@ 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){ + c = checkDisplayOn(KEY_SELECT); + } else if (enc_ev == ENC_EVENT_LONG_PRESS){ + c = handleLongPress(KEY_ENTER); + } +#endif #if defined(PIN_USER_BTN_ANA) if (abs(millis() - _analogue_pin_read_millis) > 10) { ev = analog_btn.check(); @@ -892,13 +928,14 @@ void UITask::toggleGPS() { _sensors->setSettingValue("gps", "0"); _node_prefs->gps_enabled = 0; notify(UIEventType::ack); + showAlert("GPS: Disabled", 800); } else { _sensors->setSettingValue("gps", "1"); _node_prefs->gps_enabled = 1; notify(UIEventType::ack); + showAlert("GPS: Enabled", 800); } the_mesh.savePrefs(); - showAlert(_node_prefs->gps_enabled ? "GPS: Enabled" : "GPS: Disabled", 800); _next_refresh = 0; break; } @@ -912,12 +949,13 @@ void UITask::toggleBuzzer() { if (buzzer.isQuiet()) { buzzer.quiet(false); notify(UIEventType::ack); + showAlert("Buzzer: ON", 800); } else { buzzer.quiet(true); + showAlert("Buzzer: OFF", 800); } _node_prefs->buzzer_quiet = buzzer.isQuiet(); the_mesh.savePrefs(); - showAlert(buzzer.isQuiet() ? "Buzzer: OFF" : "Buzzer: ON", 800); _next_refresh = 0; // trigger refresh #endif -} +} \ No newline at end of file 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..395bef7d4 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -116,12 +116,17 @@ 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 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