From 2da50882c006930678b93cfee6025225ef2971da Mon Sep 17 00:00:00 2001 From: csrutil Date: Sun, 7 Sep 2025 15:16:15 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20vibration=20feedb?= =?UTF-8?q?ack=20support=20for=20UI=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add genericVibration class with 5-second cooldown and 1-second pulse - Integrate vibration triggers for new messages and contact discoveries - Add conditional compilation support with PIN_VIBRATION guard - Implement abstract interface for vibration in UITask system --- examples/companion_radio/AbstractUITask.h | 3 ++ examples/companion_radio/MyMesh.cpp | 5 +++ examples/companion_radio/ui-new/UITask.cpp | 18 +++++++++ examples/companion_radio/ui-new/UITask.h | 9 +++++ src/helpers/ui/vibration.cpp | 43 ++++++++++++++++++++++ src/helpers/ui/vibration.h | 34 +++++++++++++++++ 6 files changed, 112 insertions(+) create mode 100644 src/helpers/ui/vibration.cpp create mode 100644 src/helpers/ui/vibration.h diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index 1277bba9..fa0146a1 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -42,5 +42,8 @@ public: virtual void msgRead(int msgcount) = 0; virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0; virtual void soundBuzzer(UIEventType bet = UIEventType::none) = 0; +#ifdef PIN_VIBRATION + virtual void triggerVibration() = 0; +#endif virtual void loop() = 0; }; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 04d5577e..ea54e051 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -244,6 +244,11 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } else { #ifdef DISPLAY_CLASS if (_ui) _ui->soundBuzzer(UIEventType::newContactMessage); + if (_ui) { +#ifdef PIN_VIBRATION + if (is_new) _ui->triggerVibration(); +#endif + } #endif } diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 1e03f086..0e906e41 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -483,6 +483,10 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no buzzer.begin(); #endif +#ifdef PIN_VIBRATION + vibration.begin(); +#endif + ui_started_at = millis(); _alert_expiry = 0; @@ -519,6 +523,12 @@ switch(bet){ #endif } +#ifdef PIN_VIBRATION +void UITask::triggerVibration() { + vibration.trigger(); +} +#endif + void UITask::msgRead(int msgcount) { _msgcount = msgcount; if (msgcount == 0) { @@ -532,6 +542,10 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text); setCurrScreen(msg_preview); +#ifdef PIN_VIBRATION + triggerVibration(); +#endif + if (_display != NULL) { if (!_display->isOn()) _display->turnOn(); _auto_off = millis() + AUTO_OFF_MILLIS; // extend the auto-off timer @@ -687,6 +701,10 @@ void UITask::loop() { #endif } +#ifdef PIN_VIBRATION + vibration.loop(); +#endif + #ifdef AUTO_SHUTDOWN_MILLIVOLTS if (millis() > next_batt_chck) { uint16_t milliVolts = getBattMilliVolts(); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 769b2c64..7aaeabc7 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -11,6 +11,9 @@ #ifdef PIN_BUZZER #include #endif +#ifdef PIN_VIBRATION + #include +#endif #include "../AbstractUITask.h" #include "../NodePrefs.h" @@ -20,6 +23,9 @@ class UITask : public AbstractUITask { SensorManager* _sensors; #ifdef PIN_BUZZER genericBuzzer buzzer; +#endif +#ifdef PIN_VIBRATION + genericVibration vibration; #endif unsigned long _next_refresh, _auto_off; NodePrefs* _node_prefs; @@ -72,6 +78,9 @@ public: void msgRead(int msgcount) override; void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; void soundBuzzer(UIEventType bet = UIEventType::none) override; +#ifdef PIN_VIBRATION + void triggerVibration() override; +#endif void loop() override; void shutdown(bool restart = false); diff --git a/src/helpers/ui/vibration.cpp b/src/helpers/ui/vibration.cpp new file mode 100644 index 00000000..37db54ed --- /dev/null +++ b/src/helpers/ui/vibration.cpp @@ -0,0 +1,43 @@ +#ifdef PIN_VIBRATION +#include "vibration.h" + +void genericVibration::begin() +{ + pinMode(PIN_VIBRATION, OUTPUT); + digitalWrite(PIN_VIBRATION, LOW); + duration = 0; +} + +void genericVibration::trigger() +{ + duration = millis(); + digitalWrite(PIN_VIBRATION, HIGH); +} + +void genericVibration::loop() +{ + if (isVibrating()) { + if ((millis() / 1000) % 2 == 0) { + digitalWrite(PIN_VIBRATION, LOW); + } else { + digitalWrite(PIN_VIBRATION, HIGH); + } + + if (millis() - duration > VIBRATION_TIMEOUT) { + stop(); + } + } +} + +bool genericVibration::isVibrating() +{ + return duration > 0; +} + +void genericVibration::stop() +{ + duration = 0; + digitalWrite(PIN_VIBRATION, LOW); +} + +#endif // ifdef PIN_VIBRATION diff --git a/src/helpers/ui/vibration.h b/src/helpers/ui/vibration.h new file mode 100644 index 00000000..42cd0f67 --- /dev/null +++ b/src/helpers/ui/vibration.h @@ -0,0 +1,34 @@ +#pragma once + +#ifdef PIN_VIBRATION + +#include + +/* + * Vibration motor control class + * + * Provides vibration feedback for events like new messages and new contacts + * Features: + * - 1-second vibration pulse + * - 5-second nag timeout (cooldown between vibrations) + * - Non-blocking operation + */ + +#ifndef VIBRATION_TIMEOUT +#define VIBRATION_TIMEOUT 5000 // 5 seconds default +#endif + +class genericVibration +{ + public: + void begin(); // set up vibration pin + void trigger(); // trigger vibration if cooldown has passed + void loop(); // non-blocking timer handling + bool isVibrating(); // returns true if currently vibrating + void stop(); // stop vibration immediately + + private: + unsigned long duration; +}; + +#endif // ifdef PIN_VIBRATION From 043f37a08ec0b2defc51b2ce05efe259e4e78eb4 Mon Sep 17 00:00:00 2001 From: csrutil Date: Wed, 17 Sep 2025 08:53:50 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor:=20unify=20UI?= =?UTF-8?q?=20notification=20methods=20into=20single=20notify()=20function?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidates soundBuzzer() and triggerVibration() into a unified notify() method that handles both audio and haptic feedback based on UIEventType. --- examples/companion_radio/AbstractUITask.h | 5 +--- examples/companion_radio/MyMesh.cpp | 11 +++------ examples/companion_radio/ui-new/UITask.cpp | 27 +++++++++------------ examples/companion_radio/ui-new/UITask.h | 5 +--- examples/companion_radio/ui-orig/UITask.cpp | 16 ++++++------ examples/companion_radio/ui-orig/UITask.h | 2 +- 6 files changed, 26 insertions(+), 40 deletions(-) diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index fa0146a1..0eee45ae 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -41,9 +41,6 @@ public: void disableSerial() { _serial->disable(); } virtual void msgRead(int msgcount) = 0; virtual void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) = 0; - virtual void soundBuzzer(UIEventType bet = UIEventType::none) = 0; -#ifdef PIN_VIBRATION - virtual void triggerVibration() = 0; -#endif + virtual void notify(UIEventType t = UIEventType::none) = 0; virtual void loop() = 0; }; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index ea54e051..a2c2f8f8 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -243,12 +243,7 @@ void MyMesh::onDiscoveredContact(ContactInfo &contact, bool is_new, uint8_t path } } else { #ifdef DISPLAY_CLASS - if (_ui) _ui->soundBuzzer(UIEventType::newContactMessage); - if (_ui) { -#ifdef PIN_VIBRATION - if (is_new) _ui->triggerVibration(); -#endif - } + if (_ui) _ui->notify(UIEventType::newContactMessage); #endif } @@ -358,7 +353,7 @@ void MyMesh::queueMessage(const ContactInfo &from, uint8_t txt_type, mesh::Packe if (should_display && _ui) { _ui->newMsg(path_len, from.name, text, offline_queue_len); if (!_serial->isConnected()) { - _ui->soundBuzzer(UIEventType::contactMessage); + _ui->notify(UIEventType::contactMessage); } } #endif @@ -417,7 +412,7 @@ void MyMesh::onChannelMessageRecv(const mesh::GroupChannel &channel, mesh::Packe _serial->writeFrame(frame, 1); } else { #ifdef DISPLAY_CLASS - if (_ui) _ui->soundBuzzer(UIEventType::channelMessage); + if (_ui) _ui->notify(UIEventType::channelMessage); #endif } #ifdef DISPLAY_CLASS diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 0e906e41..d117a7fb 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -348,9 +348,7 @@ public: return true; } if (c == KEY_ENTER && _page == HomePage::ADVERT) { - #ifdef PIN_BUZZER - _task->soundBuzzer(UIEventType::ack); - #endif + _task->notify(UIEventType::ack); if (the_mesh.advert()) { _task->showAlert("Advert sent!", 1000); } else { @@ -501,9 +499,9 @@ void UITask::showAlert(const char* text, int duration_millis) { _alert_expiry = millis() + duration_millis; } -void UITask::soundBuzzer(UIEventType bet) { +void UITask::notify(UIEventType t) { #if defined(PIN_BUZZER) -switch(bet){ +switch(t){ case UIEventType::contactMessage: // gemini's pick buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7"); @@ -521,13 +519,15 @@ switch(bet){ break; } #endif -} #ifdef PIN_VIBRATION -void UITask::triggerVibration() { - vibration.trigger(); -} + // Trigger vibration for all UI events except none + if (t != UIEventType::none) { + vibration.trigger(); + } #endif +} + void UITask::msgRead(int msgcount) { _msgcount = msgcount; @@ -542,9 +542,6 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text); setCurrScreen(msg_preview); -#ifdef PIN_VIBRATION - triggerVibration(); -#endif if (_display != NULL) { if (!_display->isOn()) _display->turnOn(); @@ -773,11 +770,11 @@ void UITask::toggleGPS() { if (strcmp(_sensors->getSettingName(i), "gps") == 0) { if (strcmp(_sensors->getSettingValue(i), "1") == 0) { _sensors->setSettingValue("gps", "0"); - soundBuzzer(UIEventType::ack); + notify(UIEventType::ack); showAlert("GPS: Disabled", 800); } else { _sensors->setSettingValue("gps", "1"); - soundBuzzer(UIEventType::ack); + notify(UIEventType::ack); showAlert("GPS: Enabled", 800); } _next_refresh = 0; @@ -792,7 +789,7 @@ void UITask::toggleBuzzer() { #ifdef PIN_BUZZER if (buzzer.isQuiet()) { buzzer.quiet(false); - soundBuzzer(UIEventType::ack); + notify(UIEventType::ack); showAlert("Buzzer: ON", 800); } else { buzzer.quiet(true); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 7aaeabc7..bc12c679 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -77,10 +77,7 @@ public: // from AbstractUITask void msgRead(int msgcount) override; void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; - void soundBuzzer(UIEventType bet = UIEventType::none) override; -#ifdef PIN_VIBRATION - void triggerVibration() override; -#endif + void notify(UIEventType t = UIEventType::none) override; void loop() override; void shutdown(bool restart = false); diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 29d995a7..045c955d 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -88,9 +88,9 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no ui_started_at = millis(); } -void UITask::soundBuzzer(UIEventType bet) { +void UITask::notify(UIEventType t) { #if defined(PIN_BUZZER) -switch(bet){ +switch(t){ case UIEventType::contactMessage: // gemini's pick buzzer.play("MsgRcv3:d=4,o=6,b=200:32e,32g,32b,16c7"); @@ -108,8 +108,8 @@ switch(bet){ break; } #endif -// Serial.print("DBG: Buzzzzzz -> "); -// Serial.println((int) bet); +// Serial.print("DBG: Alert user -> "); +// Serial.println((int) t); } void UITask::msgRead(int msgcount) { @@ -370,7 +370,7 @@ void UITask::handleButtonDoublePress() { MESH_DEBUG_PRINTLN("UITask: double press triggered, sending advert"); // ADVERT #ifdef PIN_BUZZER - soundBuzzer(UIEventType::ack); + notify(UIEventType::ack); #endif if (the_mesh.advert()) { MESH_DEBUG_PRINTLN("Advert sent!"); @@ -388,7 +388,7 @@ void UITask::handleButtonTriplePress() { #ifdef PIN_BUZZER if (buzzer.isQuiet()) { buzzer.quiet(false); - soundBuzzer(UIEventType::ack); + notify(UIEventType::ack); sprintf(_alert, "Buzzer: ON"); } else { buzzer.quiet(true); @@ -407,11 +407,11 @@ void UITask::handleButtonQuadruplePress() { if (strcmp(_sensors->getSettingName(i), "gps") == 0) { if (strcmp(_sensors->getSettingValue(i), "1") == 0) { _sensors->setSettingValue("gps", "0"); - soundBuzzer(UIEventType::ack); + notify(UIEventType::ack); sprintf(_alert, "GPS: Disabled"); } else { _sensors->setSettingValue("gps", "1"); - soundBuzzer(UIEventType::ack); + notify(UIEventType::ack); sprintf(_alert, "GPS: Enabled"); } break; diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index a59ddc41..60cd0d04 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -66,7 +66,7 @@ public: // from AbstractUITask void msgRead(int msgcount) override; void newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) override; - void soundBuzzer(UIEventType bet = UIEventType::none) override; + void notify(UIEventType t = UIEventType::none) override; void loop() override; void shutdown(bool restart = false); From 6f8ce425d8f63d73e326db85a283ca34e8c5a184 Mon Sep 17 00:00:00 2001 From: csrutil Date: Wed, 17 Sep 2025 09:19:18 +0800 Subject: [PATCH 3/3] remove the unnecessary blank line --- examples/companion_radio/ui-new/UITask.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index d117a7fb..e15a1d67 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -542,7 +542,6 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i ((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