st-4-esp32/src/ST4Pulse.cpp
Ryan Malloy 35ec6dfdb5 Harden safety-critical paths from Hamilton review
- 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
2026-02-17 20:18:14 -07:00

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_);
}