Add RGB LED and haptic notifications for SenseCAP MeshTracker X1

This commit is contained in:
Hacuchino-hash
2026-08-06 07:27:05 -05:00
parent 9b4a591ec4
commit 3abe415cf7
6 changed files with 225 additions and 5 deletions
+2
View File
@@ -28,6 +28,7 @@ public:
uint32_t ble_pin = 0;
uint8_t advert_loc_policy = 0;
uint8_t buzzer_quiet = 0;
uint8_t vibe_quiet = 0;
uint8_t gps_enabled = 0; // GPS enabled flag (0=disabled, 1=enabled)
uint32_t gps_interval = 0; // GPS read interval in seconds
uint8_t autoadd_config = 0; // bitmask for auto-add contacts config
@@ -101,6 +102,7 @@ private:
def("defs_key", (void *) _parent->default_scope_key, sizeof(_parent->default_scope_key));
def("pin", _parent->ble_pin);
def("buzz_q", _parent->buzzer_quiet);
def("vibe_q", _parent->vibe_quiet);
def("auto_add", _parent->autoadd_config); // bitmask for auto-add contacts config
def("man_add", _parent->manual_add_contacts);
def("tel_base", _parent->telemetry_mode_base);
+119 -5
View File
@@ -6,12 +6,19 @@
#define AUTO_OFF_MILLIS 15000 // 15 seconds
#define BOOT_SCREEN_MILLIS 3000 // 3 seconds
#ifdef PIN_STATUS_LED
#if defined(PIN_STATUS_LED) || defined(PIN_STATUS_LED_R)
#define LED_ON_MILLIS 20
#define LED_ON_MSG_MILLIS 200
#define LED_CYCLE_MILLIS 4000
#endif
#if defined(PIN_STATUS_LED_R) && defined(PIN_STATUS_LED_G) && defined(PIN_STATUS_LED_B)
#define STATUS_LED_RGB 1
#ifndef LOW_BATT_MILLIVOLTS
#define LOW_BATT_MILLIVOLTS 3500
#endif
#endif
#ifndef USER_BTN_PRESSED
#define USER_BTN_PRESSED LOW
#endif
@@ -60,6 +67,11 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
buzzer.startup();
#endif
#ifdef HAS_DRV2605
vibration.begin();
vibration.quiet(_node_prefs->vibe_quiet);
#endif
// Initialize digital button if available
#ifdef PIN_USER_BTN
_userButton = new Button(PIN_USER_BTN, USER_BTN_PRESSED);
@@ -130,6 +142,10 @@ void UITask::clearMsgPreview() {
void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, int msgcount) {
_msgcount = msgcount;
#ifdef HAS_DRV2605
vibration.trigger(); // vibrate even while the app is connected (honors quiet + cooldown)
#endif
if (path_len == 0xFF) {
sprintf(_origin, "(F) %s", from_name);
} else {
@@ -260,8 +276,88 @@ void UITask::renderCurrScreen() {
_need_refresh = false;
}
#ifdef STATUS_LED_RGB
static void statusLedWrite(uint8_t r, uint8_t g, uint8_t b) {
analogWrite(PIN_STATUS_LED_R, r);
analogWrite(PIN_STATUS_LED_G, g);
analogWrite(PIN_STATUS_LED_B, b);
}
static void statusLedWheel(uint8_t pos) { // smooth hue sweep, 0..255
if (pos < 85) {
statusLedWrite(255 - pos * 3, pos * 3, 0);
} else if (pos < 170) {
pos -= 85;
statusLedWrite(0, 255 - pos * 3, pos * 3);
} else {
pos -= 170;
statusLedWrite(pos * 3, 0, 255 - pos * 3);
}
}
#endif
void UITask::userLedHandler() {
#ifdef PIN_STATUS_LED
#ifdef STATUS_LED_RGB
static bool booted = false;
static bool flourish_active = false;
static unsigned long flourish_until = 0;
static int prev_msgcount = 0;
static int state = 0;
static unsigned long next_change = 0;
static int last_increment = 0;
static unsigned long next_batt_check = 0;
static bool low_batt = false;
unsigned long cur_time = millis();
if (!booted) { // color sweep on boot
booted = true;
flourish_until = cur_time + 1200;
flourish_active = true;
}
if (_msgcount > prev_msgcount) { // color sweep when a new message arrives
flourish_until = cur_time + 800;
flourish_active = true;
}
prev_msgcount = _msgcount;
if (flourish_active) {
if (cur_time < flourish_until) {
statusLedWheel((cur_time % 1200) * 255 / 1200);
return;
}
statusLedWrite(0, 0, 0);
flourish_active = false;
state = 0;
next_change = cur_time;
}
if (cur_time > next_batt_check) { // battery reads are not free, keep them rare
low_batt = _board->getBattMilliVolts() < LOW_BATT_MILLIVOLTS;
next_batt_check = cur_time + 60000;
}
if (cur_time > next_change) {
if (state == 0) {
state = 1;
last_increment = (_msgcount > 0) ? LED_ON_MSG_MILLIS : LED_ON_MILLIS;
next_change = cur_time + last_increment;
if (low_batt) {
statusLedWrite(255, 0, 0); // red: battery low
} else if (_msgcount > 0) {
statusLedWrite(255, 90, 0); // amber: unread messages
} else if (_connected) {
statusLedWrite(0, 0, 255); // blue: app connected
} else {
statusLedWrite(0, 255, 0); // green: heartbeat
}
} else {
state = 0;
next_change = cur_time + LED_CYCLE_MILLIS - last_increment;
statusLedWrite(0, 0, 0);
}
}
#elif defined(PIN_STATUS_LED)
static int state = 0;
static int next_change = 0;
static int last_increment = 0;
@@ -406,8 +502,26 @@ void UITask::handleButtonDoublePress() {
void UITask::handleButtonTriplePress() {
MESH_DEBUG_PRINTLN("UITask: triple press triggered");
// Toggle buzzer quiet mode
#ifdef PIN_BUZZER
#if defined(PIN_BUZZER) && defined(HAS_DRV2605)
// cycle alert modes: Buzz+Vibe -> Buzz only -> Vibe only -> Silent
int mode = (_node_prefs->buzzer_quiet ? 2 : 0) | (_node_prefs->vibe_quiet ? 1 : 0);
mode = (mode + 1) & 3;
_node_prefs->buzzer_quiet = (mode & 2) ? 1 : 0;
_node_prefs->vibe_quiet = (mode & 1) ? 1 : 0;
buzzer.quiet(_node_prefs->buzzer_quiet);
vibration.quiet(_node_prefs->vibe_quiet);
// audible/tactile confirmation of the new mode (no screen on some boards)
if (!_node_prefs->buzzer_quiet) notify(UIEventType::ack);
if (!_node_prefs->vibe_quiet) vibration.trigger(true);
switch (mode) {
case 0: sprintf(_alert, "Alerts: Buzz+Vibe"); break;
case 1: sprintf(_alert, "Alerts: Buzz only"); break;
case 2: sprintf(_alert, "Alerts: Vibe only"); break;
default: sprintf(_alert, "Alerts: Silent"); break;
}
the_mesh.savePrefs();
_need_refresh = true;
#elif defined(PIN_BUZZER)
if (buzzer.isQuiet()) {
buzzer.quiet(false);
notify(UIEventType::ack);
@@ -419,7 +533,7 @@ void UITask::handleButtonTriplePress() {
_node_prefs->buzzer_quiet = buzzer.isQuiet();
the_mesh.savePrefs();
_need_refresh = true;
#endif
#endif
}
void UITask::handleButtonQuadruplePress() {
@@ -14,11 +14,18 @@
#include "Button.h"
#ifdef HAS_DRV2605
#include <helpers/ui/DRV2605Vibration.h>
#endif
class UITask : public AbstractUITask {
DisplayDriver* _display;
SensorManager* _sensors;
#ifdef PIN_BUZZER
genericBuzzer buzzer;
#endif
#ifdef HAS_DRV2605
DRV2605Vibration vibration;
#endif
unsigned long _next_refresh, _auto_off;
NodePrefs* _node_prefs;
+47
View File
@@ -0,0 +1,47 @@
#ifdef HAS_DRV2605
#include "DRV2605Vibration.h"
#include <Wire.h>
void DRV2605Vibration::begin() {
#ifdef PIN_DRV_EN
pinMode(PIN_DRV_EN, OUTPUT);
digitalWrite(PIN_DRV_EN, HIGH); // power up the haptic driver
delay(10);
#endif
if (!drv.begin(&Wire)) {
return; // no haptic driver found, stay silent
}
#ifdef DRV2605_USE_LRA
// LRA mode: 4x brake factor, medium loop gain, back-EMF gain 2
drv.writeRegister8(DRV2605_REG_FEEDBACK, 0xB6);
#endif
drv.selectLibrary(1);
drv.setMode(DRV2605_MODE_INTTRIG);
_ready = true;
}
void DRV2605Vibration::trigger(bool force) {
if (!_ready || _quiet) return;
unsigned long now = millis();
if (!force && _last_trigger != 0 && now - _last_trigger < VIBRATION_TIMEOUT) return;
_last_trigger = now;
drv.setWaveform(0, DRV2605_EFFECT);
drv.setWaveform(1, 0); // pause
drv.setWaveform(2, DRV2605_EFFECT);
drv.setWaveform(3, 0); // end of sequence
drv.go();
}
void DRV2605Vibration::loop() {
}
bool DRV2605Vibration::isVibrating() {
return false; // effects are short, treat as instantaneous
}
void DRV2605Vibration::stop() {
if (_ready) drv.stop();
}
#endif // ifdef HAS_DRV2605
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#ifdef HAS_DRV2605
#include <Arduino.h>
#include <Adafruit_DRV2605.h>
/*
* Vibration control class for boards where the motor is behind a
* DRV2605 haptic driver on I2C (e.g. Seeed SenseCAP MeshTracker X1).
* Same interface as GenericVibration.
*/
#ifndef VIBRATION_TIMEOUT
#define VIBRATION_TIMEOUT 5000 // cooldown between vibrations
#endif
#ifndef DRV2605_EFFECT
#define DRV2605_EFFECT 16 // "1000 ms alert" from the DRV2605 effect library
#endif
class DRV2605Vibration {
public:
void begin(); // power up and init the DRV2605
void trigger(bool force = false); // trigger vibration; force skips the cooldown
void loop(); // no-op, the DRV2605 plays effects autonomously
bool isVibrating();
void stop(); // stop vibration immediately
void quiet(bool q) { _quiet = q; }
bool isQuiet() const { return _quiet; }
private:
Adafruit_DRV2605 drv;
bool _ready = false;
bool _quiet = false;
unsigned long _last_trigger = 0;
};
#endif // ifdef HAS_DRV2605
+11
View File
@@ -12,6 +12,9 @@ build_flags = ${nrf52_base.build_flags}
-D PIN_USER_BTN=6
-D USER_BTN_PRESSED=HIGH
-D PIN_STATUS_LED=24
-D PIN_STATUS_LED_R=3 ; P0.3 red
-D PIN_STATUS_LED_G=24 ; P0.24 green
-D PIN_STATUS_LED_B=28 ; P0.28 blue
-D RADIO_CLASS=CustomLR2021
-D USE_LR2021
-D WRAPPER_CLASS=CustomLR2021Wrapper
@@ -77,16 +80,20 @@ build_flags = ${MeshTracker_X1.build_flags}
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=NullDisplayDriver
-D PIN_BUZZER=25
-D HAS_DRV2605=1
-D DRV2605_USE_LRA=1
-D ENABLE_USB_INTERFACE
build_src_filter = ${MeshTracker_X1.build_src_filter}
+<helpers/ui/buzzer.cpp>
+<helpers/ui/NullDisplayDriver.cpp>
+<helpers/ui/DRV2605Vibration.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-orig/*.cpp>
lib_deps = ${MeshTracker_X1.lib_deps}
densaugeo/base64 @ ~1.4.0
stevemarple/MicroNMEA @ ^2.0.6
end2endzone/NonBlockingRTTTL@^1.3.0
adafruit/Adafruit DRV2605 Library @ ^1.2.4
[env:MeshTracker_X1_companion_radio_ble]
extends = MeshTracker_X1
@@ -104,14 +111,18 @@ build_flags = ${MeshTracker_X1.build_flags}
-D OFFLINE_QUEUE_SIZE=256
-D DISPLAY_CLASS=NullDisplayDriver
-D PIN_BUZZER=25
-D HAS_DRV2605=1
-D DRV2605_USE_LRA=1
-D ADVERT_NAME='"@@MAC"'
build_src_filter = ${MeshTracker_X1.build_src_filter}
+<helpers/nrf52/SerialBLEInterface.cpp>
+<helpers/ui/buzzer.cpp>
+<helpers/ui/NullDisplayDriver.cpp>
+<helpers/ui/DRV2605Vibration.cpp>
+<../examples/companion_radio/*.cpp>
+<../examples/companion_radio/ui-orig/*.cpp>
lib_deps = ${MeshTracker_X1.lib_deps}
densaugeo/base64 @ ~1.4.0
stevemarple/MicroNMEA @ ^2.0.6
end2endzone/NonBlockingRTTTL@^1.3.0
adafruit/Adafruit DRV2605 Library @ ^1.2.4