From a1c0d23a2ac9c74bf9f688299d7816641f7a72a7 Mon Sep 17 00:00:00 2001 From: mark9064 <30447455+mark9064@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:35:30 +0100 Subject: [PATCH] Improve MotorController flexibility --- src/components/motor/MotorController.cpp | 139 +++++++++++++++--- src/components/motor/MotorController.h | 35 ++++- src/displayapp/DisplayApp.cpp | 4 +- src/displayapp/screens/Alarm.cpp | 2 +- src/displayapp/screens/Dice.cpp | 2 +- src/displayapp/screens/InfiniPaint.cpp | 2 +- src/displayapp/screens/Metronome.cpp | 4 +- src/displayapp/screens/Notifications.cpp | 4 +- src/displayapp/screens/Timer.cpp | 2 +- .../screens/settings/QuickSettings.cpp | 2 +- src/main.cpp | 2 +- 11 files changed, 161 insertions(+), 37 deletions(-) diff --git a/src/components/motor/MotorController.cpp b/src/components/motor/MotorController.cpp index d3bd2cf3a9..04ea2c8c70 100644 --- a/src/components/motor/MotorController.cpp +++ b/src/components/motor/MotorController.cpp @@ -1,43 +1,146 @@ #include "components/motor/MotorController.h" +#include #include -#include "systemtask/SystemTask.h" +#include "components/motion/MotionController.h" #include "drivers/PinMap.h" using namespace Pinetime::Controllers; +MotorController::MotorController(Pinetime::Controllers::MotionController& motionController) : motionController {motionController} { +} + void MotorController::Init() { nrf_gpio_cfg_output(PinMap::Motor); nrf_gpio_pin_set(PinMap::Motor); - shortVib = xTimerCreate("shortVib", 1, pdFALSE, nullptr, StopMotor); - longVib = xTimerCreate("longVib", pdMS_TO_TICKS(1000), pdTRUE, this, Ring); + nextEvt = xTimerCreate("vib", 1, pdFALSE, this, NextEvent); +} + +void MotorController::NextEvent(TimerHandle_t timer) { + auto* motorController = static_cast(pvTimerGetTimerID(timer)); + + TickType_t deviation = xTaskGetTickCount() - motorController->nextEvtTime; + + TickType_t duration; + + switch (motorController->state) { + case State::Buzz: + SetMotorRunning(false); + motorController->state = State::Idle; + return; + case State::RingOn: + SetMotorRunning(false); + if (motorController->remainingBuzzes == 1) { + motorController->state = State::Idle; + return; + } + if (motorController->remainingBuzzes > 1) { + motorController->remainingBuzzes--; + } + motorController->state = State::RingOff; + duration = motorController->ringPeriod - motorController->ringOnTicks; + break; + case State::RingOff: + SetMotorRunning(true); + motorController->state = State::RingOn; + duration = motorController->ringOnTicks; + break; + case State::Idle: + // should never hit the idle state in the timer callback + assert(false); + __builtin_trap(); + } + motorController->nextEvtTime += duration; + if (duration > deviation) { + xTimerChangePeriod(timer, duration - deviation, 0); + } else { + xTimerChangePeriod(timer, 1, 0); + } } -void MotorController::Ring(TimerHandle_t xTimer) { - auto* motorController = static_cast(pvTimerGetTimerID(xTimer)); - motorController->RunForDuration(50); +uint16_t MotorController::CalculateLength(Intensity intensity) { + // don't think shake speed really has units + // 200 seems sensible, then clamp the multiplier to range 1-3 + // a fancy curve would probably feel better, but this seems to work well + float durationMultiplier = motionController.CurrentShakeSpeed() / 200.f; + durationMultiplier = std::max(std::min(durationMultiplier, 3.f), 1.f); + + float length; + + switch (intensity) { + case Intensity::Light: + length = 30.f; + break; + case Intensity::Medium: + length = 50.f; + break; + case Intensity::Strong: + length = 90.f; + break; + } + return length * durationMultiplier; } -void MotorController::RunForDuration(uint8_t motorDuration) { - if (motorDuration > 0 && xTimerChangePeriod(shortVib, pdMS_TO_TICKS(motorDuration), 0) == pdPASS && xTimerStart(shortVib, 0) == pdPASS) { +void MotorController::RunEvent(void* motorControllerPtr, uint32_t requestedStateInt) { + auto* motorController = static_cast(motorControllerPtr); + auto requestedState = static_cast(requestedStateInt); + + // these aren't always set, but if they're not set they're never used + // so it's safe to copy unconditionally + motorController->ringOnTicks = motorController->nextOperation.ringOnTicks; + motorController->ringPeriod = motorController->nextOperation.ringPeriod; + motorController->remainingBuzzes = motorController->nextOperation.buzzCount; + + if (requestedState == RequestEvt::RequestRingOff) { + SetMotorRunning(false); + motorController->state = State::Idle; + xTimerStop(motorController->nextEvt, 0); + } else { + SetMotorRunning(true); + motorController->nextEvtTime = xTaskGetTickCount() + motorController->ringOnTicks; + if (requestedState == RequestEvt::RequestBuzz) { + motorController->state = State::Buzz; + } else { + motorController->state = State::RingOn; + } + // changing the period starts the timer + xTimerChangePeriod(motorController->nextEvt, motorController->ringOnTicks, 0); + } +} + +void MotorController::PushEvent(RequestEvt state) { + // since the timer callback modifies its own state, + // we need to run timer state changes in the timer task to avoid races + xTimerPendFunctionCall(RunEvent, this, static_cast(state), portMAX_DELAY); +} + +void MotorController::Buzz(Intensity intensity) { + nextOperation.ringOnTicks = CalculateLength(intensity); + + PushEvent(RequestEvt::RequestBuzz); +} + +void MotorController::SetMotorRunning(bool active) { + if (active) { nrf_gpio_pin_clear(PinMap::Motor); + } else { + nrf_gpio_pin_set(PinMap::Motor); } } -void MotorController::StartRinging() { - RunForDuration(50); - xTimerStart(longVib, 0); +// numBuzzes = 0 rings forever +void MotorController::Ring(Intensity intensity, TickType_t ringPeriod, uint16_t numBuzzes) { + nextOperation.ringOnTicks = CalculateLength(intensity); + nextOperation.ringPeriod = ringPeriod; + nextOperation.buzzCount = numBuzzes; + + PushEvent(RequestEvt::RequestRingOn); } void MotorController::StopRinging() { - xTimerStop(longVib, 0); - nrf_gpio_pin_set(PinMap::Motor); + PushEvent(RequestEvt::RequestRingOff); } bool MotorController::IsRinging() { - return (xTimerIsTimerActive(longVib) == pdTRUE); -} - -void MotorController::StopMotor(TimerHandle_t /*xTimer*/) { - nrf_gpio_pin_set(PinMap::Motor); + return state == State::RingOn || state == State::RingOff; } diff --git a/src/components/motor/MotorController.h b/src/components/motor/MotorController.h index ab8c956e41..0fefa33ca7 100644 --- a/src/components/motor/MotorController.h +++ b/src/components/motor/MotorController.h @@ -6,22 +6,43 @@ namespace Pinetime { namespace Controllers { + class MotionController; class MotorController { public: - MotorController() = default; + explicit MotorController(Pinetime::Controllers::MotionController& motionController); + + enum class Intensity : uint8_t { Light, Medium, Strong }; void Init(); - void RunForDuration(uint8_t motorDuration); - void StartRinging(); + void Buzz(Intensity intensity); + void Ring(Intensity intensity, TickType_t ringPeriod, uint16_t numBuzzes = 0); void StopRinging(); bool IsRinging(); private: - static void Ring(TimerHandle_t xTimer); - static void StopMotor(TimerHandle_t xTimer); - TimerHandle_t shortVib; - TimerHandle_t longVib; + enum class State : uint8_t { Idle, Buzz, RingOn, RingOff }; + enum class RequestEvt : uint8_t { RequestBuzz, RequestRingOn, RequestRingOff }; + + struct Request { + TickType_t ringOnTicks; + TickType_t ringPeriod; + uint16_t buzzCount; + }; + + Request nextOperation; + State state; + uint16_t ringOnTicks; + TickType_t ringPeriod; + TickType_t nextEvtTime; + TimerHandle_t nextEvt; + uint16_t remainingBuzzes; + MotionController& motionController; + uint16_t CalculateLength(Intensity intensity); + static void NextEvent(TimerHandle_t timer); + static void RunEvent(void* motorControllerPtr, uint32_t requestedStateInt); + static void SetMotorRunning(bool active); + void PushEvent(RequestEvt state); }; } } diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index 84fa603622..b029de743b 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -394,7 +394,7 @@ void DisplayApp::Refresh() { break; case Messages::ShowPairingKey: LoadNewScreen(Apps::PassKey, DisplayApp::FullRefreshDirections::Up); - motorController.RunForDuration(35); + motorController.Buzz(Controllers::MotorController::Intensity::Light); break; case Messages::TouchEvent: { if (state != States::Running) { @@ -483,7 +483,7 @@ void DisplayApp::Refresh() { break; case Messages::Chime: LoadNewScreen(Apps::Clock, DisplayApp::FullRefreshDirections::None); - motorController.RunForDuration(35); + motorController.Ring(Controllers::MotorController::Intensity::Light, pdMS_TO_TICKS(200), 2); break; } } diff --git a/src/displayapp/screens/Alarm.cpp b/src/displayapp/screens/Alarm.cpp index 4cf4392157..c30464753a 100644 --- a/src/displayapp/screens/Alarm.cpp +++ b/src/displayapp/screens/Alarm.cpp @@ -209,7 +209,7 @@ void Alarm::SetAlerting() { minuteCounter.HideControls(); lv_obj_set_hidden(btnStop, false); taskStopAlarm = lv_task_create(StopAlarmTaskCallback, pdMS_TO_TICKS(60 * 1000), LV_TASK_PRIO_MID, this); - motorController.StartRinging(); + motorController.Ring(Controllers::MotorController::Intensity::Strong, pdMS_TO_TICKS(1000)); wakeLock.Lock(); } diff --git a/src/displayapp/screens/Dice.cpp b/src/displayapp/screens/Dice.cpp index 302c5f3fb2..67b4d50a11 100644 --- a/src/displayapp/screens/Dice.cpp +++ b/src/displayapp/screens/Dice.cpp @@ -186,7 +186,7 @@ void Dice::Roll() { lv_label_set_text_fmt(resultTotalLabel, "%d", resultTotal); if (openingRoll == false) { - motorController.RunForDuration(30); + motorController.Buzz(Controllers::MotorController::Intensity::Light); NextColor(); currentRollHysteresis = rollHysteresis; } diff --git a/src/displayapp/screens/InfiniPaint.cpp b/src/displayapp/screens/InfiniPaint.cpp index 958d7939fd..27584a5523 100644 --- a/src/displayapp/screens/InfiniPaint.cpp +++ b/src/displayapp/screens/InfiniPaint.cpp @@ -52,7 +52,7 @@ bool InfiniPaint::OnTouchEvent(Pinetime::Applications::TouchEvents event) { } std::fill(b, b + bufferSize, selectColor); - motor.RunForDuration(35); + motor.Buzz(Controllers::MotorController::Intensity::Light); return true; default: return true; diff --git a/src/displayapp/screens/Metronome.cpp b/src/displayapp/screens/Metronome.cpp index 6b758470a4..58d562668c 100644 --- a/src/displayapp/screens/Metronome.cpp +++ b/src/displayapp/screens/Metronome.cpp @@ -82,9 +82,9 @@ void Metronome::Refresh() { counter--; if (counter == 0) { counter = bpb; - motorController.RunForDuration(90); + motorController.Buzz(Controllers::MotorController::Intensity::Strong); } else { - motorController.RunForDuration(30); + motorController.Buzz(Controllers::MotorController::Intensity::Light); } } } diff --git a/src/displayapp/screens/Notifications.cpp b/src/displayapp/screens/Notifications.cpp index 837c4683aa..b7a992f290 100644 --- a/src/displayapp/screens/Notifications.cpp +++ b/src/displayapp/screens/Notifications.cpp @@ -42,9 +42,9 @@ Notifications::Notifications(DisplayApp* app, if (mode == Modes::Preview) { wakeLock.Lock(); if (notification.category == Controllers::NotificationManager::Categories::IncomingCall) { - motorController.StartRinging(); + motorController.Ring(Controllers::MotorController::Intensity::Medium, pdMS_TO_TICKS(1000)); } else { - motorController.RunForDuration(35); + motorController.Buzz(Controllers::MotorController::Intensity::Light); } timeoutLine = lv_line_create(lv_scr_act(), nullptr); diff --git a/src/displayapp/screens/Timer.cpp b/src/displayapp/screens/Timer.cpp index 749d985933..9da7f1704b 100644 --- a/src/displayapp/screens/Timer.cpp +++ b/src/displayapp/screens/Timer.cpp @@ -170,7 +170,7 @@ void Timer::SetTimerStopped() { } void Timer::SetTimerRinging() { - motorController.StartRinging(); + motorController.Ring(Controllers::MotorController::Intensity::Medium, pdMS_TO_TICKS(1000)); wakeLock.Lock(); minuteCounter.HideControls(); secondCounter.HideControls(); diff --git a/src/displayapp/screens/settings/QuickSettings.cpp b/src/displayapp/screens/settings/QuickSettings.cpp index c5c3071aef..1dfc6a6012 100644 --- a/src/displayapp/screens/settings/QuickSettings.cpp +++ b/src/displayapp/screens/settings/QuickSettings.cpp @@ -159,7 +159,7 @@ void QuickSettings::OnButtonEvent(lv_obj_t* object) { settingsController.SetNotificationStatus(Controllers::Settings::Notification::On); lv_label_set_text_static(btn3_lvl, Symbols::notificationsOn); lv_obj_set_state(btn3, static_cast(ButtonState::NotificationsOn)); - motorController.RunForDuration(35); + motorController.Buzz(Controllers::MotorController::Intensity::Light); } } else if (object == btn4) { diff --git a/src/main.cpp b/src/main.cpp index d0ab3e4887..30731706c9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -96,7 +96,6 @@ Pinetime::Controllers::Ble bleController; Pinetime::Controllers::FS fs {spiNorFlash}; Pinetime::Controllers::Settings settingsController {fs}; -Pinetime::Controllers::MotorController motorController {}; Pinetime::Controllers::HeartRateController heartRateController; Pinetime::Applications::HeartRateTask heartRateApp(heartRateSensor, heartRateController, settingsController); @@ -105,6 +104,7 @@ Pinetime::Controllers::DateTime dateTimeController {settingsController}; Pinetime::Drivers::Watchdog watchdog; Pinetime::Controllers::NotificationManager notificationManager; Pinetime::Controllers::MotionController motionController; +Pinetime::Controllers::MotorController motorController {motionController}; Pinetime::Controllers::StopWatchController stopWatchController; Pinetime::Controllers::AlarmController alarmController {dateTimeController, fs}; Pinetime::Controllers::TouchHandler touchHandler;