- Fix lock hierarchy: stopAll() cancels pulse before touching axes - Add configASSERT bounds checks on axis index in move/pulseGuide - Enforce ST4Pulse singleton with configASSERT - Check esp_timer_start_once return, rollback hardware on failure - Validate SYNC coordinates (reject garbage → silent 0.0) - Discard truncated serial commands on buffer overflow - Guard WiFi update()/broadcastState() against null ws_ pointer - Report connection errors to WebSocket clients on move/pulse - Remove redundant Serial.begin() from pulse_guide example
151 lines
4.5 KiB
C++
151 lines
4.5 KiB
C++
// SPDX-License-Identifier: LGPL-3.0-or-later
|
|
// Hardware timer pulse guiding with deferred ISR-safe stop
|
|
//
|
|
// Pattern: esp_timer callback -> semaphore -> high-priority task -> mutex -> axis.stop()
|
|
// The mutex protects the multi-field state (activeAxis_, activeTracker_, active_)
|
|
// from concurrent pulse()/cancel() calls across ESP32 cores.
|
|
#include "ST4Pulse.h"
|
|
|
|
ST4Pulse* ST4Pulse::instance_ = nullptr;
|
|
|
|
static const int PULSE_TASK_PRIORITY = configMAX_PRIORITIES - 2;
|
|
static const int PULSE_TASK_STACK = 2048;
|
|
|
|
ST4Pulse::ST4Pulse()
|
|
: timer_(nullptr), pulseDoneSem_(nullptr), mutex_(nullptr),
|
|
pulseTaskHandle_(nullptr),
|
|
activeAxis_(nullptr), activeTracker_(nullptr),
|
|
active_(false), shutdown_(false) {}
|
|
|
|
ST4Pulse::~ST4Pulse() {
|
|
// 1. Stop and delete timer (prevents new semaphore gives)
|
|
if (timer_) {
|
|
esp_timer_stop(timer_);
|
|
esp_timer_delete(timer_);
|
|
timer_ = nullptr;
|
|
}
|
|
// 2. Signal task to exit and unblock it
|
|
shutdown_ = true;
|
|
if (pulseDoneSem_) xSemaphoreGive(pulseDoneSem_);
|
|
// 3. Wait for task to see shutdown flag
|
|
if (pulseTaskHandle_) {
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
vTaskDelete(pulseTaskHandle_);
|
|
pulseTaskHandle_ = nullptr;
|
|
}
|
|
// 4. Delete synchronization primitives last
|
|
if (pulseDoneSem_) vSemaphoreDelete(pulseDoneSem_);
|
|
if (mutex_) vSemaphoreDelete(mutex_);
|
|
instance_ = nullptr;
|
|
}
|
|
|
|
void ST4Pulse::timerCallback(void* arg) {
|
|
ST4Pulse* self = static_cast<ST4Pulse*>(arg);
|
|
// Give semaphore to wake the pulse-stop task
|
|
// Safe for ESP_TIMER_TASK dispatch (the default)
|
|
xSemaphoreGive(self->pulseDoneSem_);
|
|
}
|
|
|
|
void ST4Pulse::pulseTaskFunc(void* arg) {
|
|
ST4Pulse* self = static_cast<ST4Pulse*>(arg);
|
|
for (;;) {
|
|
if (xSemaphoreTake(self->pulseDoneSem_, portMAX_DELAY) == pdTRUE) {
|
|
if (self->shutdown_) break;
|
|
|
|
xSemaphoreTake(self->mutex_, portMAX_DELAY);
|
|
if (self->active_) {
|
|
if (self->activeAxis_) self->activeAxis_->stop();
|
|
if (self->activeTracker_) self->activeTracker_->stop();
|
|
self->active_ = false;
|
|
}
|
|
xSemaphoreGive(self->mutex_);
|
|
}
|
|
}
|
|
// Task deletes itself on shutdown
|
|
vTaskDelete(nullptr);
|
|
}
|
|
|
|
void ST4Pulse::begin() {
|
|
configASSERT(instance_ == nullptr);
|
|
instance_ = this;
|
|
|
|
mutex_ = xSemaphoreCreateMutex();
|
|
configASSERT(mutex_);
|
|
|
|
pulseDoneSem_ = xSemaphoreCreateBinary();
|
|
configASSERT(pulseDoneSem_);
|
|
|
|
esp_timer_create_args_t timerArgs = {};
|
|
timerArgs.callback = timerCallback;
|
|
timerArgs.arg = this;
|
|
timerArgs.name = "st4_pulse";
|
|
esp_err_t err = esp_timer_create(&timerArgs, &timer_);
|
|
configASSERT(err == ESP_OK);
|
|
|
|
BaseType_t created = xTaskCreatePinnedToCore(
|
|
pulseTaskFunc, "st4_pulse", PULSE_TASK_STACK,
|
|
this, PULSE_TASK_PRIORITY, &pulseTaskHandle_, 1
|
|
);
|
|
configASSERT(created == pdPASS);
|
|
}
|
|
|
|
void ST4Pulse::cancelLocked() {
|
|
// Caller must hold mutex_
|
|
esp_timer_stop(timer_);
|
|
// Drain any pending semaphore from a just-fired timer
|
|
xSemaphoreTake(pulseDoneSem_, 0);
|
|
|
|
if (activeAxis_) activeAxis_->stop();
|
|
if (activeTracker_) activeTracker_->stop();
|
|
active_ = false;
|
|
activeAxis_ = nullptr;
|
|
activeTracker_ = nullptr;
|
|
}
|
|
|
|
bool ST4Pulse::pulse(ST4Axis& axis, ST4Tracker& tracker,
|
|
ST4Direction dir, double slewRate, uint32_t ms) {
|
|
xSemaphoreTake(mutex_, portMAX_DELAY);
|
|
|
|
if (active_) cancelLocked();
|
|
|
|
if (dir == ST4Direction::STOP || ms == 0) {
|
|
xSemaphoreGive(mutex_);
|
|
return false;
|
|
}
|
|
|
|
if (ms > MAX_PULSE_MS) ms = MAX_PULSE_MS;
|
|
|
|
activeAxis_ = &axis;
|
|
activeTracker_ = &tracker;
|
|
active_ = true;
|
|
|
|
axis.move(dir);
|
|
tracker.start(slewRate);
|
|
|
|
// Start one-shot timer (microseconds)
|
|
esp_err_t timerErr = esp_timer_start_once(timer_, static_cast<uint64_t>(ms) * 1000);
|
|
if (timerErr != ESP_OK) {
|
|
// Timer failed -- rollback hardware activation
|
|
axis.stop();
|
|
tracker.stop();
|
|
active_ = false;
|
|
activeAxis_ = nullptr;
|
|
activeTracker_ = nullptr;
|
|
xSemaphoreGive(mutex_);
|
|
return false;
|
|
}
|
|
|
|
xSemaphoreGive(mutex_);
|
|
return true;
|
|
}
|
|
|
|
bool ST4Pulse::isActive() const {
|
|
return active_;
|
|
}
|
|
|
|
void ST4Pulse::cancel() {
|
|
xSemaphoreTake(mutex_, portMAX_DELAY);
|
|
cancelLocked();
|
|
xSemaphoreGive(mutex_);
|
|
}
|