ESP32 ST-4 autoguider library with thread-safe pulse guiding
Port of arduino-st4 (Kevin Ferrare) to ESP32/PlatformIO with: - FreeRTOS mutex protection at every layer (Pin, Axis, Pulse, Controller) - Hardware timer pulse guiding with ISR-safe deferred stop pattern - Backward-compatible serial protocol (57600 baud, #-terminated) - Extended commands: PULSE, POS?, SYNC, STATUS?, VERSION? - Optional WiFi/WebSocket control (gated by ST4_WIFI_ENABLED) - Dead-reckoning position tracker using esp_timer microsecond precision All 4 examples build clean against esp32dev target.
This commit is contained in:
commit
4c91fd4811
165
LICENSE
Normal file
165
LICENSE
Normal file
@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
24
Makefile
Normal file
24
Makefile
Normal file
@ -0,0 +1,24 @@
|
||||
.PHONY: all basic serial pulse wifi clean
|
||||
|
||||
BOARD = esp32dev
|
||||
LIB = .
|
||||
|
||||
all: basic serial pulse wifi
|
||||
@echo "All examples built successfully"
|
||||
|
||||
basic:
|
||||
pio ci examples/basic_gpio/basic_gpio.ino --lib="$(LIB)" --board=$(BOARD)
|
||||
|
||||
serial:
|
||||
pio ci examples/serial_compatible/serial_compatible.ino --lib="$(LIB)" --board=$(BOARD)
|
||||
|
||||
pulse:
|
||||
pio ci examples/pulse_guide/pulse_guide.ino --lib="$(LIB)" --board=$(BOARD)
|
||||
|
||||
wifi:
|
||||
pio ci examples/wifi_control/wifi_control.ino --lib="$(LIB)" --board=$(BOARD) \
|
||||
--project-option="build_flags=-DST4_WIFI_ENABLED" \
|
||||
--project-option="lib_deps=bblanchon/ArduinoJson@^7.0.0, mathieucarbou/ESPAsyncWebServer@^3.6.0"
|
||||
|
||||
clean:
|
||||
rm -rf .pio
|
||||
52
examples/basic_gpio/basic_gpio.ino
Normal file
52
examples/basic_gpio/basic_gpio.ino
Normal file
@ -0,0 +1,52 @@
|
||||
// ST4-ESP32 Basic GPIO Example
|
||||
// Cycles through all axis directions to verify wiring
|
||||
// Use multimeter or logic analyzer on TLP521-4 outputs to verify
|
||||
|
||||
#include <ST4.h>
|
||||
|
||||
ST4Axis ra;
|
||||
ST4Axis dec;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Serial.println("ST4 Basic GPIO Test");
|
||||
|
||||
ra.begin(ST4_PIN_RA_PLUS, ST4_PIN_RA_MINUS);
|
||||
dec.begin(ST4_PIN_DEC_PLUS, ST4_PIN_DEC_MINUS);
|
||||
|
||||
Serial.println("Pins initialized. Cycling directions...");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
Serial.println("RA+");
|
||||
ra.plus();
|
||||
delay(2000);
|
||||
|
||||
Serial.println("RA stop");
|
||||
ra.stop();
|
||||
delay(1000);
|
||||
|
||||
Serial.println("RA-");
|
||||
ra.minus();
|
||||
delay(2000);
|
||||
|
||||
Serial.println("RA stop");
|
||||
ra.stop();
|
||||
delay(1000);
|
||||
|
||||
Serial.println("DEC+");
|
||||
dec.plus();
|
||||
delay(2000);
|
||||
|
||||
Serial.println("DEC stop");
|
||||
dec.stop();
|
||||
delay(1000);
|
||||
|
||||
Serial.println("DEC-");
|
||||
dec.minus();
|
||||
delay(2000);
|
||||
|
||||
Serial.println("All stop");
|
||||
dec.stop();
|
||||
delay(3000);
|
||||
}
|
||||
48
examples/pulse_guide/pulse_guide.ino
Normal file
48
examples/pulse_guide/pulse_guide.ino
Normal file
@ -0,0 +1,48 @@
|
||||
// ST4-ESP32 Pulse Guide Example
|
||||
// Demonstrates hardware-timer pulse guiding with position tracking
|
||||
//
|
||||
// Send via serial (57600 baud):
|
||||
// CONNECT# - connect to mount
|
||||
// PULSE RA+ 500# - pulse RA+ for 500ms
|
||||
// PULSE DEC- 1000# - pulse DEC- for 1 second
|
||||
// POS?# - read current position
|
||||
// STATUS?# - full status report
|
||||
|
||||
#include <ST4.h>
|
||||
|
||||
ST4Controller controller;
|
||||
ST4Serial st4Serial;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Serial.println("ST4 Pulse Guide Example");
|
||||
|
||||
controller.begin(
|
||||
ST4_PIN_RA_PLUS, ST4_PIN_RA_MINUS,
|
||||
ST4_PIN_DEC_PLUS, ST4_PIN_DEC_MINUS,
|
||||
ST4_PIN_LED
|
||||
);
|
||||
|
||||
st4Serial.begin(controller, Serial);
|
||||
controller.connect();
|
||||
|
||||
Serial.println("Ready. Try: PULSE RA+ 500#");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
st4Serial.update();
|
||||
|
||||
// Print position every 5 seconds while moving
|
||||
static uint32_t lastPrint = 0;
|
||||
if (millis() - lastPrint > 5000) {
|
||||
lastPrint = millis();
|
||||
if (controller.axisActive(ST4AxisId::RA) ||
|
||||
controller.axisActive(ST4AxisId::DECLINATION) ||
|
||||
controller.isPulseActive()) {
|
||||
Serial.print("RA: ");
|
||||
Serial.print(controller.position(ST4AxisId::RA), 6);
|
||||
Serial.print(" DEC: ");
|
||||
Serial.println(controller.position(ST4AxisId::DECLINATION), 6);
|
||||
}
|
||||
}
|
||||
}
|
||||
27
examples/serial_compatible/serial_compatible.ino
Normal file
27
examples/serial_compatible/serial_compatible.ino
Normal file
@ -0,0 +1,27 @@
|
||||
// ST4-ESP32 Serial Compatible Example
|
||||
// Drop-in replacement for original ArduinoCode.ino
|
||||
// Works with the ASCOM ArduinoST4 driver at 57600 baud
|
||||
//
|
||||
// Original commands: CONNECT# DISCONNECT# RA+# RA-# RA0# DEC+# DEC-# DEC0#
|
||||
// Extended commands: PULSE RA+ 500# POS?# SYNC 12.345 45.678# STATUS?# VERSION?#
|
||||
|
||||
#include <ST4.h>
|
||||
|
||||
ST4Controller controller;
|
||||
ST4Serial st4Serial;
|
||||
|
||||
void setup() {
|
||||
controller.begin(
|
||||
ST4_PIN_RA_PLUS, ST4_PIN_RA_MINUS,
|
||||
ST4_PIN_DEC_PLUS, ST4_PIN_DEC_MINUS,
|
||||
ST4_PIN_LED
|
||||
);
|
||||
|
||||
// Extended mode adds PULSE, POS?, SYNC, STATUS?, VERSION?
|
||||
// Set to false for strict original protocol compatibility
|
||||
st4Serial.begin(controller, Serial, true);
|
||||
}
|
||||
|
||||
void loop() {
|
||||
st4Serial.update();
|
||||
}
|
||||
59
examples/wifi_control/wifi_control.ino
Normal file
59
examples/wifi_control/wifi_control.ino
Normal file
@ -0,0 +1,59 @@
|
||||
// ST4-ESP32 WiFi Control Example
|
||||
// Creates a WiFi AP with WebSocket server for wireless autoguiding
|
||||
//
|
||||
// Connect to WiFi AP, then open WebSocket at ws://<IP>/ws
|
||||
// JSON commands:
|
||||
// {"cmd":"move","axis":"ra","dir":"+"}
|
||||
// {"cmd":"pulse","axis":"dec","dir":"-","ms":500}
|
||||
// {"cmd":"stop"}
|
||||
// {"cmd":"sync","ra":12.345,"dec":45.678}
|
||||
// {"cmd":"status"}
|
||||
//
|
||||
// State broadcasts are sent automatically on direction changes
|
||||
// and periodically during active slew
|
||||
|
||||
#ifndef ST4_WIFI_ENABLED
|
||||
#define ST4_WIFI_ENABLED
|
||||
#endif
|
||||
#include <ST4.h>
|
||||
|
||||
ST4Controller controller;
|
||||
ST4Serial st4Serial;
|
||||
ST4WiFi st4WiFi;
|
||||
|
||||
const char* WIFI_SSID = "ST4-Guider";
|
||||
const char* WIFI_PASS = "st4guide!";
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Serial.println("ST4 WiFi Control");
|
||||
|
||||
controller.begin(
|
||||
ST4_PIN_RA_PLUS, ST4_PIN_RA_MINUS,
|
||||
ST4_PIN_DEC_PLUS, ST4_PIN_DEC_MINUS,
|
||||
ST4_PIN_LED
|
||||
);
|
||||
|
||||
st4Serial.begin(controller, Serial);
|
||||
|
||||
ST4WiFiConfig wifiConfig = {
|
||||
.ssid = WIFI_SSID,
|
||||
.password = WIFI_PASS,
|
||||
.apMode = true,
|
||||
.httpPort = 80,
|
||||
.broadcastIntervalMs = 250
|
||||
};
|
||||
st4WiFi.begin(controller, wifiConfig);
|
||||
|
||||
controller.connect();
|
||||
|
||||
Serial.println("WiFi AP: " + String(WIFI_SSID));
|
||||
Serial.print("WebSocket: ws://");
|
||||
Serial.print(WiFi.softAPIP());
|
||||
Serial.println("/ws");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
st4Serial.update();
|
||||
st4WiFi.update();
|
||||
}
|
||||
18
include/ST4.h
Normal file
18
include/ST4.h
Normal file
@ -0,0 +1,18 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// ST4-ESP32: ESP32 ST-4 autoguider port controller
|
||||
// Facade header - includes all library components
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ST4Types.h"
|
||||
#include "ST4Config.h"
|
||||
#include "ST4Pin.h"
|
||||
#include "ST4Axis.h"
|
||||
#include "ST4Tracker.h"
|
||||
#include "ST4Pulse.h"
|
||||
#include "ST4Controller.h"
|
||||
#include "ST4Serial.h"
|
||||
|
||||
#ifdef ST4_WIFI_ENABLED
|
||||
#include "ST4WiFi.h"
|
||||
#endif
|
||||
31
include/ST4Axis.h
Normal file
31
include/ST4Axis.h
Normal file
@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// ST4-ESP32: Dual-pin axis with mutual exclusion
|
||||
// Deactivates opposing pin before activating (matches original safety pattern)
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include "ST4Pin.h"
|
||||
#include "ST4Types.h"
|
||||
|
||||
class ST4Axis {
|
||||
ST4Pin plusPin_;
|
||||
ST4Pin minusPin_;
|
||||
volatile ST4Direction direction_;
|
||||
SemaphoreHandle_t mutex_;
|
||||
|
||||
public:
|
||||
ST4Axis();
|
||||
~ST4Axis();
|
||||
|
||||
void begin(int plusPin, int minusPin,
|
||||
ST4PinLogic logic = ST4PinLogic::ACTIVE_HIGH);
|
||||
void plus();
|
||||
void minus();
|
||||
void stop();
|
||||
void move(ST4Direction dir);
|
||||
|
||||
ST4Direction direction() const;
|
||||
bool isActive() const;
|
||||
};
|
||||
28
include/ST4Config.h
Normal file
28
include/ST4Config.h
Normal file
@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// ST4-ESP32: Default pin configuration for ESP32
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Default GPIO assignments for ESP32 with TLP521-4 optocoupler
|
||||
// Override by defining before including ST4.h
|
||||
#ifndef ST4_PIN_RA_PLUS
|
||||
#define ST4_PIN_RA_PLUS 16
|
||||
#endif
|
||||
|
||||
#ifndef ST4_PIN_RA_MINUS
|
||||
#define ST4_PIN_RA_MINUS 17
|
||||
#endif
|
||||
|
||||
#ifndef ST4_PIN_DEC_PLUS
|
||||
#define ST4_PIN_DEC_PLUS 18
|
||||
#endif
|
||||
|
||||
#ifndef ST4_PIN_DEC_MINUS
|
||||
#define ST4_PIN_DEC_MINUS 19
|
||||
#endif
|
||||
|
||||
#ifndef ST4_PIN_LED
|
||||
#define ST4_PIN_LED 2
|
||||
#endif
|
||||
63
include/ST4Controller.h
Normal file
63
include/ST4Controller.h
Normal file
@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// ST4-ESP32: High-level mount controller
|
||||
// Owns axes, trackers, and pulse engine with configurable sidereal rates
|
||||
// Controller-level mutex ensures composite operations are atomic across cores
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include "ST4Types.h"
|
||||
#include "ST4Config.h"
|
||||
#include "ST4Axis.h"
|
||||
#include "ST4Tracker.h"
|
||||
#include "ST4Pulse.h"
|
||||
|
||||
class ST4Controller {
|
||||
ST4Axis axes_[2];
|
||||
ST4Tracker trackers_[2];
|
||||
ST4Pulse pulse_;
|
||||
|
||||
ST4RateConfig rateConfig_;
|
||||
bool connected_;
|
||||
int ledPin_;
|
||||
mutable SemaphoreHandle_t mutex_;
|
||||
|
||||
double calculateSlewRate(ST4AxisId axis, ST4Direction dir) const;
|
||||
|
||||
public:
|
||||
ST4Controller();
|
||||
~ST4Controller();
|
||||
|
||||
void begin(int raPlusPin = ST4_PIN_RA_PLUS,
|
||||
int raMinusPin = ST4_PIN_RA_MINUS,
|
||||
int decPlusPin = ST4_PIN_DEC_PLUS,
|
||||
int decMinusPin = ST4_PIN_DEC_MINUS,
|
||||
int ledPin = ST4_PIN_LED,
|
||||
ST4PinLogic logic = ST4PinLogic::ACTIVE_HIGH);
|
||||
|
||||
void setRates(const ST4RateConfig& config);
|
||||
|
||||
void connect();
|
||||
void disconnect();
|
||||
bool isConnected() const;
|
||||
|
||||
void move(ST4AxisId axis, ST4Direction dir);
|
||||
void stopAxis(ST4AxisId axis);
|
||||
void stopAll();
|
||||
|
||||
bool pulseGuide(ST4AxisId axis, ST4Direction dir, uint32_t ms);
|
||||
bool isPulseActive() const;
|
||||
|
||||
double position(ST4AxisId axis) const;
|
||||
void setPosition(ST4AxisId axis, double pos);
|
||||
void sync(double ra, double dec);
|
||||
|
||||
ST4Direction axisDirection(ST4AxisId axis) const;
|
||||
bool axisActive(ST4AxisId axis) const;
|
||||
|
||||
ST4State state() const;
|
||||
|
||||
ST4Axis& axis(ST4AxisId id);
|
||||
ST4Tracker& tracker(ST4AxisId id);
|
||||
};
|
||||
22
include/ST4Pin.h
Normal file
22
include/ST4Pin.h
Normal file
@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// ST4-ESP32: Single GPIO pin abstraction with configurable active logic
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "ST4Types.h"
|
||||
|
||||
class ST4Pin {
|
||||
int pin_;
|
||||
ST4PinLogic logic_;
|
||||
bool active_;
|
||||
|
||||
public:
|
||||
ST4Pin();
|
||||
|
||||
void begin(int pin, ST4PinLogic logic = ST4PinLogic::ACTIVE_HIGH);
|
||||
void activate();
|
||||
void deactivate();
|
||||
bool isActive() const;
|
||||
int pin() const;
|
||||
};
|
||||
43
include/ST4Pulse.h
Normal file
43
include/ST4Pulse.h
Normal file
@ -0,0 +1,43 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// ST4-ESP32: Hardware timer pulse guiding (non-blocking)
|
||||
// Uses esp_timer one-shot with deferred stop via FreeRTOS task
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <esp_timer.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
#include "ST4Axis.h"
|
||||
#include "ST4Tracker.h"
|
||||
|
||||
class ST4Pulse {
|
||||
// Static instance for timer callback (standard embedded ISR pattern)
|
||||
static ST4Pulse* instance_;
|
||||
|
||||
esp_timer_handle_t timer_;
|
||||
SemaphoreHandle_t pulseDoneSem_;
|
||||
SemaphoreHandle_t mutex_;
|
||||
TaskHandle_t pulseTaskHandle_;
|
||||
|
||||
ST4Axis* activeAxis_;
|
||||
ST4Tracker* activeTracker_;
|
||||
volatile bool active_;
|
||||
volatile bool shutdown_;
|
||||
|
||||
static constexpr uint32_t MAX_PULSE_MS = 10000;
|
||||
|
||||
void cancelLocked();
|
||||
static void timerCallback(void* arg);
|
||||
static void pulseTaskFunc(void* arg);
|
||||
|
||||
public:
|
||||
ST4Pulse();
|
||||
~ST4Pulse();
|
||||
|
||||
void begin();
|
||||
bool pulse(ST4Axis& axis, ST4Tracker& tracker,
|
||||
ST4Direction dir, double slewRate, uint32_t ms);
|
||||
bool isActive() const;
|
||||
void cancel();
|
||||
};
|
||||
28
include/ST4Serial.h
Normal file
28
include/ST4Serial.h
Normal file
@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// ST4-ESP32: Serial protocol handler (ASCOM/INDI compatible)
|
||||
// Drop-in replacement for original ArduinoCode.ino serial protocol
|
||||
// Extended mode adds PULSE, POS?, SYNC, STATUS?, VERSION?
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "ST4Controller.h"
|
||||
|
||||
class ST4Serial {
|
||||
ST4Controller* controller_;
|
||||
HardwareSerial* serial_;
|
||||
String buffer_;
|
||||
bool extendedMode_;
|
||||
|
||||
void processCommand(const String& cmd);
|
||||
void processExtendedCommand(const String& cmd);
|
||||
String directionStr(ST4Direction dir) const;
|
||||
|
||||
public:
|
||||
ST4Serial();
|
||||
|
||||
void begin(ST4Controller& controller,
|
||||
HardwareSerial& serial = Serial,
|
||||
bool extendedMode = true);
|
||||
void update();
|
||||
};
|
||||
31
include/ST4Tracker.h
Normal file
31
include/ST4Tracker.h
Normal file
@ -0,0 +1,31 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// ST4-ESP32: Dead-reckoning position tracker
|
||||
// Port of ASCOM AxisMovementTracker.cs using esp_timer microsecond precision
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include "ST4Types.h"
|
||||
|
||||
class ST4Tracker {
|
||||
double position_;
|
||||
double slewRate_;
|
||||
int64_t startTime_;
|
||||
mutable SemaphoreHandle_t mutex_;
|
||||
|
||||
double calculateDelta() const;
|
||||
|
||||
public:
|
||||
ST4Tracker();
|
||||
~ST4Tracker();
|
||||
|
||||
void begin();
|
||||
void start(double slewRate);
|
||||
void stop();
|
||||
|
||||
double position() const;
|
||||
void setPosition(double pos);
|
||||
double slewRate() const;
|
||||
bool isMoving() const;
|
||||
};
|
||||
63
include/ST4Types.h
Normal file
63
include/ST4Types.h
Normal file
@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// ST4-ESP32: ESP32 ST-4 autoguider port controller
|
||||
// Based on arduino-st4 by Kevin Ferrare
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
enum class ST4Direction : uint8_t {
|
||||
PLUS,
|
||||
MINUS,
|
||||
STOP
|
||||
};
|
||||
|
||||
enum class ST4AxisId : uint8_t {
|
||||
RA = 0,
|
||||
DECLINATION = 1
|
||||
};
|
||||
|
||||
enum class ST4PinLogic : uint8_t {
|
||||
ACTIVE_HIGH,
|
||||
ACTIVE_LOW
|
||||
};
|
||||
|
||||
// Sidereal rate constants (from ASCOM ArduinoST4 Constants.cs)
|
||||
namespace ST4Constants {
|
||||
constexpr double DEGREES_PER_SECOND = 360.0 / (24.0 * 3600.0);
|
||||
constexpr double RA_PER_SECOND = 1.0 / 3600.0;
|
||||
|
||||
// Default sidereal rate multipliers (from ASCOM Driver.cs)
|
||||
// RA+: 8x slew + 1x earth rotation = 9x
|
||||
// RA-: 8x slew - 1x earth rotation = 7x
|
||||
// DEC: symmetric 8x
|
||||
constexpr double DEFAULT_RA_RATE_PLUS = 9.0;
|
||||
constexpr double DEFAULT_RA_RATE_MINUS = 7.0;
|
||||
constexpr double DEFAULT_DEC_RATE_PLUS = 8.0;
|
||||
constexpr double DEFAULT_DEC_RATE_MINUS = 8.0;
|
||||
|
||||
constexpr uint32_t DEFAULT_BAUD_RATE = 57600;
|
||||
constexpr uint16_t DEFAULT_WS_PORT = 81;
|
||||
constexpr uint16_t DEFAULT_HTTP_PORT = 80;
|
||||
|
||||
constexpr char VERSION[] = "2026.02.17";
|
||||
}
|
||||
|
||||
struct ST4AxisState {
|
||||
bool active;
|
||||
ST4Direction direction;
|
||||
double position;
|
||||
};
|
||||
|
||||
struct ST4State {
|
||||
bool connected;
|
||||
ST4AxisState ra;
|
||||
ST4AxisState dec;
|
||||
};
|
||||
|
||||
struct ST4RateConfig {
|
||||
double raPlusMultiplier;
|
||||
double raMinusMultiplier;
|
||||
double decPlusMultiplier;
|
||||
double decMinusMultiplier;
|
||||
};
|
||||
49
include/ST4WiFi.h
Normal file
49
include/ST4WiFi.h
Normal file
@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// ST4-ESP32: WiFi + WebSocket server (optional)
|
||||
// Enable by defining ST4_WIFI_ENABLED before including ST4.h
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifdef ST4_WIFI_ENABLED
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "ST4Controller.h"
|
||||
|
||||
struct ST4WiFiConfig {
|
||||
const char* ssid;
|
||||
const char* password;
|
||||
bool apMode;
|
||||
uint16_t httpPort;
|
||||
uint32_t broadcastIntervalMs;
|
||||
};
|
||||
|
||||
class ST4WiFi {
|
||||
ST4Controller* controller_;
|
||||
AsyncWebServer* server_;
|
||||
AsyncWebSocket* ws_;
|
||||
ST4WiFiConfig config_;
|
||||
|
||||
uint32_t lastBroadcast_;
|
||||
ST4Direction lastRaDir_;
|
||||
ST4Direction lastDecDir_;
|
||||
|
||||
void handleWebSocketEvent(AsyncWebSocket* server,
|
||||
AsyncWebSocketClient* client,
|
||||
AwsEventType type, void* arg,
|
||||
uint8_t* data, size_t len);
|
||||
void processCommand(AsyncWebSocketClient* client,
|
||||
uint8_t* data, size_t len);
|
||||
void broadcastState();
|
||||
String stateJson() const;
|
||||
|
||||
public:
|
||||
ST4WiFi();
|
||||
~ST4WiFi();
|
||||
|
||||
void begin(ST4Controller& controller, const ST4WiFiConfig& config);
|
||||
void update();
|
||||
};
|
||||
|
||||
#endif // ST4_WIFI_ENABLED
|
||||
43
library.json
Normal file
43
library.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "ST4-ESP32",
|
||||
"version": "2026.02.17",
|
||||
"description": "ESP32 ST-4 autoguider port controller with WiFi/WebSocket support, hardware-timer pulse guiding, and FreeRTOS thread safety",
|
||||
"keywords": [
|
||||
"telescope",
|
||||
"autoguider",
|
||||
"st-4",
|
||||
"mount-control",
|
||||
"pulse-guiding",
|
||||
"satellite-tracking",
|
||||
"ham-radio",
|
||||
"esp32"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://git.supported.systems/rpm/st-4-esp32"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ryan Malloy",
|
||||
"email": "ryan@supported.systems"
|
||||
}
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"frameworks": "arduino",
|
||||
"platforms": "espressif32",
|
||||
"headers": "ST4.h",
|
||||
"dependencies": {
|
||||
"bblanchon/ArduinoJson": "^7.0.0"
|
||||
},
|
||||
"build": {
|
||||
"flags": [
|
||||
"-I include"
|
||||
]
|
||||
},
|
||||
"examples": [
|
||||
"examples/basic_gpio/basic_gpio.ino",
|
||||
"examples/serial_compatible/serial_compatible.ino",
|
||||
"examples/pulse_guide/pulse_guide.ino",
|
||||
"examples/wifi_control/wifi_control.ino"
|
||||
]
|
||||
}
|
||||
30
platformio.ini
Normal file
30
platformio.ini
Normal file
@ -0,0 +1,30 @@
|
||||
; ST4-ESP32 PlatformIO library
|
||||
; For building examples: use 'make' targets (uses pio ci)
|
||||
; For development/upload: pio run -e <env> (uses symlink lib_deps)
|
||||
|
||||
[platformio]
|
||||
default_envs = serial_compatible
|
||||
|
||||
[env]
|
||||
platform = espressif32
|
||||
board = esp32dev
|
||||
framework = arduino
|
||||
monitor_speed = 115200
|
||||
lib_deps = symlink://.
|
||||
|
||||
[env:basic_gpio]
|
||||
build_src_filter = +<../examples/basic_gpio/>
|
||||
|
||||
[env:serial_compatible]
|
||||
build_src_filter = +<../examples/serial_compatible/>
|
||||
|
||||
[env:pulse_guide]
|
||||
build_src_filter = +<../examples/pulse_guide/>
|
||||
|
||||
[env:wifi_control]
|
||||
build_flags = -DST4_WIFI_ENABLED
|
||||
lib_deps =
|
||||
symlink://.
|
||||
bblanchon/ArduinoJson@^7.0.0
|
||||
mathieucarbou/ESPAsyncWebServer@^3.6.0
|
||||
build_src_filter = +<../examples/wifi_control/>
|
||||
59
src/ST4Axis.cpp
Normal file
59
src/ST4Axis.cpp
Normal file
@ -0,0 +1,59 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
#include "ST4Axis.h"
|
||||
|
||||
ST4Axis::ST4Axis()
|
||||
: direction_(ST4Direction::STOP), mutex_(nullptr) {}
|
||||
|
||||
ST4Axis::~ST4Axis() {
|
||||
if (mutex_) vSemaphoreDelete(mutex_);
|
||||
}
|
||||
|
||||
void ST4Axis::begin(int plusPin, int minusPin, ST4PinLogic logic) {
|
||||
mutex_ = xSemaphoreCreateMutex();
|
||||
configASSERT(mutex_);
|
||||
plusPin_.begin(plusPin, logic);
|
||||
minusPin_.begin(minusPin, logic);
|
||||
stop();
|
||||
}
|
||||
|
||||
void ST4Axis::plus() {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
// Safety: deactivate opposing direction first (prevents optocoupler short)
|
||||
minusPin_.deactivate();
|
||||
plusPin_.activate();
|
||||
direction_ = ST4Direction::PLUS;
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
void ST4Axis::minus() {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
plusPin_.deactivate();
|
||||
minusPin_.activate();
|
||||
direction_ = ST4Direction::MINUS;
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
void ST4Axis::stop() {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
plusPin_.deactivate();
|
||||
minusPin_.deactivate();
|
||||
direction_ = ST4Direction::STOP;
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
void ST4Axis::move(ST4Direction dir) {
|
||||
switch (dir) {
|
||||
case ST4Direction::PLUS: plus(); break;
|
||||
case ST4Direction::MINUS: minus(); break;
|
||||
case ST4Direction::STOP: stop(); break;
|
||||
}
|
||||
}
|
||||
|
||||
ST4Direction ST4Axis::direction() const {
|
||||
// Atomic read on ESP32 (uint8_t enum)
|
||||
return direction_;
|
||||
}
|
||||
|
||||
bool ST4Axis::isActive() const {
|
||||
return direction_ != ST4Direction::STOP;
|
||||
}
|
||||
180
src/ST4Controller.cpp
Normal file
180
src/ST4Controller.cpp
Normal file
@ -0,0 +1,180 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
#include "ST4Controller.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
ST4Controller::ST4Controller()
|
||||
: connected_(false), ledPin_(-1), mutex_(nullptr) {
|
||||
rateConfig_ = {
|
||||
ST4Constants::DEFAULT_RA_RATE_PLUS,
|
||||
ST4Constants::DEFAULT_RA_RATE_MINUS,
|
||||
ST4Constants::DEFAULT_DEC_RATE_PLUS,
|
||||
ST4Constants::DEFAULT_DEC_RATE_MINUS
|
||||
};
|
||||
}
|
||||
|
||||
ST4Controller::~ST4Controller() {
|
||||
if (mutex_) vSemaphoreDelete(mutex_);
|
||||
}
|
||||
|
||||
void ST4Controller::begin(int raPlusPin, int raMinusPin,
|
||||
int decPlusPin, int decMinusPin,
|
||||
int ledPin, ST4PinLogic logic) {
|
||||
mutex_ = xSemaphoreCreateMutex();
|
||||
configASSERT(mutex_);
|
||||
|
||||
ledPin_ = ledPin;
|
||||
if (ledPin_ >= 0) {
|
||||
pinMode(ledPin_, OUTPUT);
|
||||
digitalWrite(ledPin_, LOW);
|
||||
}
|
||||
|
||||
axes_[static_cast<int>(ST4AxisId::RA)].begin(raPlusPin, raMinusPin, logic);
|
||||
axes_[static_cast<int>(ST4AxisId::DECLINATION)].begin(decPlusPin, decMinusPin, logic);
|
||||
trackers_[0].begin();
|
||||
trackers_[1].begin();
|
||||
pulse_.begin();
|
||||
}
|
||||
|
||||
void ST4Controller::setRates(const ST4RateConfig& config) {
|
||||
configASSERT(config.raPlusMultiplier > 0);
|
||||
configASSERT(config.raMinusMultiplier > 0);
|
||||
configASSERT(config.decPlusMultiplier > 0);
|
||||
configASSERT(config.decMinusMultiplier > 0);
|
||||
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
rateConfig_ = config;
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
double ST4Controller::calculateSlewRate(ST4AxisId axis, ST4Direction dir) const {
|
||||
if (dir == ST4Direction::STOP) return 0;
|
||||
|
||||
double baseRate = (axis == ST4AxisId::RA)
|
||||
? ST4Constants::RA_PER_SECOND
|
||||
: ST4Constants::DEGREES_PER_SECOND;
|
||||
|
||||
double multiplier;
|
||||
if (axis == ST4AxisId::RA) {
|
||||
multiplier = (dir == ST4Direction::PLUS)
|
||||
? rateConfig_.raPlusMultiplier
|
||||
: rateConfig_.raMinusMultiplier;
|
||||
} else {
|
||||
multiplier = (dir == ST4Direction::PLUS)
|
||||
? rateConfig_.decPlusMultiplier
|
||||
: rateConfig_.decMinusMultiplier;
|
||||
}
|
||||
|
||||
double sign = (dir == ST4Direction::MINUS) ? -1.0 : 1.0;
|
||||
return sign * baseRate * multiplier;
|
||||
}
|
||||
|
||||
void ST4Controller::connect() {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
connected_ = true;
|
||||
if (ledPin_ >= 0) digitalWrite(ledPin_, HIGH);
|
||||
xSemaphoreGive(mutex_);
|
||||
stopAll();
|
||||
}
|
||||
|
||||
void ST4Controller::disconnect() {
|
||||
stopAll();
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
connected_ = false;
|
||||
if (ledPin_ >= 0) digitalWrite(ledPin_, LOW);
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
bool ST4Controller::isConnected() const {
|
||||
return connected_;
|
||||
}
|
||||
|
||||
void ST4Controller::move(ST4AxisId axis, ST4Direction dir) {
|
||||
if (!connected_) return;
|
||||
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
int idx = static_cast<int>(axis);
|
||||
if (dir == ST4Direction::STOP) {
|
||||
axes_[idx].stop();
|
||||
trackers_[idx].stop();
|
||||
} else {
|
||||
axes_[idx].move(dir);
|
||||
trackers_[idx].start(calculateSlewRate(axis, dir));
|
||||
}
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
void ST4Controller::stopAxis(ST4AxisId axis) {
|
||||
move(axis, ST4Direction::STOP);
|
||||
}
|
||||
|
||||
void ST4Controller::stopAll() {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
int raIdx = static_cast<int>(ST4AxisId::RA);
|
||||
int decIdx = static_cast<int>(ST4AxisId::DECLINATION);
|
||||
axes_[raIdx].stop();
|
||||
trackers_[raIdx].stop();
|
||||
axes_[decIdx].stop();
|
||||
trackers_[decIdx].stop();
|
||||
xSemaphoreGive(mutex_);
|
||||
if (pulse_.isActive()) pulse_.cancel();
|
||||
}
|
||||
|
||||
bool ST4Controller::pulseGuide(ST4AxisId axis, ST4Direction dir, uint32_t ms) {
|
||||
if (!connected_) return false;
|
||||
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
int idx = static_cast<int>(axis);
|
||||
double rate = calculateSlewRate(axis, dir);
|
||||
bool result = pulse_.pulse(axes_[idx], trackers_[idx], dir, rate, ms);
|
||||
xSemaphoreGive(mutex_);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ST4Controller::isPulseActive() const {
|
||||
return pulse_.isActive();
|
||||
}
|
||||
|
||||
double ST4Controller::position(ST4AxisId axis) const {
|
||||
return trackers_[static_cast<int>(axis)].position();
|
||||
}
|
||||
|
||||
void ST4Controller::setPosition(ST4AxisId axis, double pos) {
|
||||
trackers_[static_cast<int>(axis)].setPosition(pos);
|
||||
}
|
||||
|
||||
void ST4Controller::sync(double ra, double dec) {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
setPosition(ST4AxisId::RA, ra);
|
||||
setPosition(ST4AxisId::DECLINATION, dec);
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
ST4Direction ST4Controller::axisDirection(ST4AxisId axis) const {
|
||||
return axes_[static_cast<int>(axis)].direction();
|
||||
}
|
||||
|
||||
bool ST4Controller::axisActive(ST4AxisId axis) const {
|
||||
return axes_[static_cast<int>(axis)].isActive();
|
||||
}
|
||||
|
||||
ST4State ST4Controller::state() const {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
ST4State s;
|
||||
s.connected = connected_;
|
||||
s.ra.active = axes_[0].isActive();
|
||||
s.ra.direction = axes_[0].direction();
|
||||
s.ra.position = trackers_[0].position();
|
||||
s.dec.active = axes_[1].isActive();
|
||||
s.dec.direction = axes_[1].direction();
|
||||
s.dec.position = trackers_[1].position();
|
||||
xSemaphoreGive(mutex_);
|
||||
return s;
|
||||
}
|
||||
|
||||
ST4Axis& ST4Controller::axis(ST4AxisId id) {
|
||||
return axes_[static_cast<int>(id)];
|
||||
}
|
||||
|
||||
ST4Tracker& ST4Controller::tracker(ST4AxisId id) {
|
||||
return trackers_[static_cast<int>(id)];
|
||||
}
|
||||
33
src/ST4Pin.cpp
Normal file
33
src/ST4Pin.cpp
Normal file
@ -0,0 +1,33 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
#include "ST4Pin.h"
|
||||
|
||||
ST4Pin::ST4Pin()
|
||||
: pin_(-1), logic_(ST4PinLogic::ACTIVE_HIGH), active_(false) {}
|
||||
|
||||
void ST4Pin::begin(int pin, ST4PinLogic logic) {
|
||||
pin_ = pin;
|
||||
logic_ = logic;
|
||||
active_ = false;
|
||||
pinMode(pin_, OUTPUT);
|
||||
deactivate();
|
||||
}
|
||||
|
||||
void ST4Pin::activate() {
|
||||
if (pin_ < 0) return;
|
||||
active_ = true;
|
||||
digitalWrite(pin_, (logic_ == ST4PinLogic::ACTIVE_HIGH) ? HIGH : LOW);
|
||||
}
|
||||
|
||||
void ST4Pin::deactivate() {
|
||||
if (pin_ < 0) return;
|
||||
active_ = false;
|
||||
digitalWrite(pin_, (logic_ == ST4PinLogic::ACTIVE_HIGH) ? LOW : HIGH);
|
||||
}
|
||||
|
||||
bool ST4Pin::isActive() const {
|
||||
return active_;
|
||||
}
|
||||
|
||||
int ST4Pin::pin() const {
|
||||
return pin_;
|
||||
}
|
||||
139
src/ST4Pulse.cpp
Normal file
139
src/ST4Pulse.cpp
Normal file
@ -0,0 +1,139 @@
|
||||
// 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() {
|
||||
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_timer_start_once(timer_, static_cast<uint64_t>(ms) * 1000);
|
||||
|
||||
xSemaphoreGive(mutex_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ST4Pulse::isActive() const {
|
||||
return active_;
|
||||
}
|
||||
|
||||
void ST4Pulse::cancel() {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
cancelLocked();
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
151
src/ST4Serial.cpp
Normal file
151
src/ST4Serial.cpp
Normal file
@ -0,0 +1,151 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Serial protocol: backward compatible with original ArduinoCode.ino
|
||||
// Original commands: CONNECT#, DISCONNECT#, RA+#, RA-#, RA0#, DEC+#, DEC-#, DEC0#
|
||||
// Extended commands: PULSE RA+ 500#, POS?#, SYNC 12.345 45.678#, STATUS?#, VERSION?#
|
||||
#include "ST4Serial.h"
|
||||
|
||||
ST4Serial::ST4Serial()
|
||||
: controller_(nullptr), serial_(nullptr), extendedMode_(true) {}
|
||||
|
||||
void ST4Serial::begin(ST4Controller& controller, HardwareSerial& serial,
|
||||
bool extendedMode) {
|
||||
controller_ = &controller;
|
||||
serial_ = &serial;
|
||||
extendedMode_ = extendedMode;
|
||||
buffer_.reserve(64);
|
||||
serial_->begin(ST4Constants::DEFAULT_BAUD_RATE, SERIAL_8N1);
|
||||
serial_->println("INITIALIZED#");
|
||||
}
|
||||
|
||||
String ST4Serial::directionStr(ST4Direction dir) const {
|
||||
switch (dir) {
|
||||
case ST4Direction::PLUS: return "+";
|
||||
case ST4Direction::MINUS: return "-";
|
||||
default: return "0";
|
||||
}
|
||||
}
|
||||
|
||||
void ST4Serial::processCommand(const String& cmd) {
|
||||
bool valid = true;
|
||||
|
||||
if (cmd == "CONNECT") {
|
||||
controller_->connect();
|
||||
} else if (cmd == "DISCONNECT") {
|
||||
controller_->disconnect();
|
||||
} else if (cmd == "RA0") {
|
||||
controller_->stopAxis(ST4AxisId::RA);
|
||||
} else if (cmd == "RA+") {
|
||||
controller_->move(ST4AxisId::RA, ST4Direction::PLUS);
|
||||
} else if (cmd == "RA-") {
|
||||
controller_->move(ST4AxisId::RA, ST4Direction::MINUS);
|
||||
} else if (cmd == "DEC0") {
|
||||
controller_->stopAxis(ST4AxisId::DECLINATION);
|
||||
} else if (cmd == "DEC+") {
|
||||
controller_->move(ST4AxisId::DECLINATION, ST4Direction::PLUS);
|
||||
} else if (cmd == "DEC-") {
|
||||
controller_->move(ST4AxisId::DECLINATION, ST4Direction::MINUS);
|
||||
} else if (extendedMode_) {
|
||||
processExtendedCommand(cmd);
|
||||
return;
|
||||
} else {
|
||||
valid = false;
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
serial_->println("OK#");
|
||||
}
|
||||
}
|
||||
|
||||
void ST4Serial::processExtendedCommand(const String& cmd) {
|
||||
if (cmd.startsWith("PULSE ")) {
|
||||
// Format: PULSE RA+ 500 or PULSE DEC- 1000
|
||||
String rest = cmd.substring(6);
|
||||
rest.trim();
|
||||
|
||||
ST4AxisId axis;
|
||||
if (rest.startsWith("RA")) {
|
||||
axis = ST4AxisId::RA;
|
||||
rest = rest.substring(2);
|
||||
} else if (rest.startsWith("DEC")) {
|
||||
axis = ST4AxisId::DECLINATION;
|
||||
rest = rest.substring(3);
|
||||
} else {
|
||||
serial_->println("ERR:INVALID_AXIS#");
|
||||
return;
|
||||
}
|
||||
|
||||
ST4Direction dir;
|
||||
if (rest.startsWith("+")) {
|
||||
dir = ST4Direction::PLUS;
|
||||
rest = rest.substring(1);
|
||||
} else if (rest.startsWith("-")) {
|
||||
dir = ST4Direction::MINUS;
|
||||
rest = rest.substring(1);
|
||||
} else {
|
||||
serial_->println("ERR:INVALID_DIR#");
|
||||
return;
|
||||
}
|
||||
|
||||
rest.trim();
|
||||
uint32_t ms = rest.toInt();
|
||||
if (ms > 0) {
|
||||
controller_->pulseGuide(axis, dir, ms);
|
||||
serial_->println("OK#");
|
||||
} else {
|
||||
serial_->println("ERR:INVALID_DURATION#");
|
||||
}
|
||||
} else if (cmd == "POS?") {
|
||||
serial_->print("POS ");
|
||||
serial_->print(controller_->position(ST4AxisId::RA), 6);
|
||||
serial_->print(" ");
|
||||
serial_->print(controller_->position(ST4AxisId::DECLINATION), 6);
|
||||
serial_->println("#");
|
||||
} else if (cmd.startsWith("SYNC ")) {
|
||||
// Format: SYNC 12.345 45.678
|
||||
String rest = cmd.substring(5);
|
||||
int spaceIdx = rest.indexOf(' ');
|
||||
if (spaceIdx > 0) {
|
||||
double ra = rest.substring(0, spaceIdx).toDouble();
|
||||
double dec = rest.substring(spaceIdx + 1).toDouble();
|
||||
controller_->sync(ra, dec);
|
||||
serial_->println("OK#");
|
||||
} else {
|
||||
serial_->println("ERR:INVALID_COORDS#");
|
||||
}
|
||||
} else if (cmd == "STATUS?") {
|
||||
ST4State s = controller_->state();
|
||||
serial_->print("STATUS ");
|
||||
serial_->print(s.connected ? "CONNECTED" : "DISCONNECTED");
|
||||
serial_->print(" RA:");
|
||||
serial_->print(directionStr(s.ra.direction));
|
||||
serial_->print(":");
|
||||
serial_->print(s.ra.position, 6);
|
||||
serial_->print(" DEC:");
|
||||
serial_->print(directionStr(s.dec.direction));
|
||||
serial_->print(":");
|
||||
serial_->print(s.dec.position, 6);
|
||||
serial_->println("#");
|
||||
} else if (cmd == "VERSION?") {
|
||||
serial_->print("VERSION ");
|
||||
serial_->print(ST4Constants::VERSION);
|
||||
serial_->println("#");
|
||||
}
|
||||
// Unknown extended commands are silently ignored (matches original behavior)
|
||||
}
|
||||
|
||||
void ST4Serial::update() {
|
||||
while (serial_->available()) {
|
||||
char c = serial_->read();
|
||||
if (c == '#') {
|
||||
buffer_.trim();
|
||||
if (buffer_.length() > 0) {
|
||||
processCommand(buffer_);
|
||||
}
|
||||
buffer_ = "";
|
||||
} else if (c != '\r' && c != '\n') {
|
||||
if (buffer_.length() < 64) {
|
||||
buffer_ += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/ST4Tracker.cpp
Normal file
70
src/ST4Tracker.cpp
Normal file
@ -0,0 +1,70 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Port of ASCOM AxisMovementTracker.cs
|
||||
// Uses esp_timer_get_time() for microsecond precision (vs DateTime.Now ~15ms)
|
||||
#include "ST4Tracker.h"
|
||||
#include <esp_timer.h>
|
||||
|
||||
ST4Tracker::ST4Tracker()
|
||||
: position_(0), slewRate_(0), startTime_(0), mutex_(nullptr) {}
|
||||
|
||||
ST4Tracker::~ST4Tracker() {
|
||||
if (mutex_) vSemaphoreDelete(mutex_);
|
||||
}
|
||||
|
||||
void ST4Tracker::begin() {
|
||||
mutex_ = xSemaphoreCreateMutex();
|
||||
configASSERT(mutex_);
|
||||
}
|
||||
|
||||
double ST4Tracker::calculateDelta() const {
|
||||
// Equivalent to ASCOM's: slewRate * (DateTime.Now - slewStartTime).TotalSeconds
|
||||
if (slewRate_ == 0) return 0;
|
||||
int64_t elapsed_us = esp_timer_get_time() - startTime_;
|
||||
return slewRate_ * (elapsed_us / 1000000.0);
|
||||
}
|
||||
|
||||
void ST4Tracker::start(double slewRate) {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
// Accumulate pending delta before changing rate (matches ASCOM Stop+Start pattern)
|
||||
if (slewRate_ != 0) {
|
||||
position_ += calculateDelta();
|
||||
}
|
||||
slewRate_ = slewRate;
|
||||
startTime_ = esp_timer_get_time();
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
void ST4Tracker::stop() {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
position_ += calculateDelta();
|
||||
slewRate_ = 0;
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
double ST4Tracker::position() const {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
double pos = position_ + calculateDelta();
|
||||
xSemaphoreGive(mutex_);
|
||||
return pos;
|
||||
}
|
||||
|
||||
void ST4Tracker::setPosition(double pos) {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
position_ = pos;
|
||||
slewRate_ = 0;
|
||||
xSemaphoreGive(mutex_);
|
||||
}
|
||||
|
||||
double ST4Tracker::slewRate() const {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
double rate = slewRate_;
|
||||
xSemaphoreGive(mutex_);
|
||||
return rate;
|
||||
}
|
||||
|
||||
bool ST4Tracker::isMoving() const {
|
||||
xSemaphoreTake(mutex_, portMAX_DELAY);
|
||||
bool moving = slewRate_ != 0;
|
||||
xSemaphoreGive(mutex_);
|
||||
return moving;
|
||||
}
|
||||
182
src/ST4WiFi.cpp
Normal file
182
src/ST4WiFi.cpp
Normal file
@ -0,0 +1,182 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
#ifdef ST4_WIFI_ENABLED
|
||||
|
||||
#include "ST4WiFi.h"
|
||||
|
||||
ST4WiFi::ST4WiFi()
|
||||
: controller_(nullptr), server_(nullptr), ws_(nullptr),
|
||||
lastBroadcast_(0),
|
||||
lastRaDir_(ST4Direction::STOP), lastDecDir_(ST4Direction::STOP) {}
|
||||
|
||||
ST4WiFi::~ST4WiFi() {
|
||||
delete ws_;
|
||||
delete server_;
|
||||
}
|
||||
|
||||
void ST4WiFi::begin(ST4Controller& controller, const ST4WiFiConfig& config) {
|
||||
controller_ = &controller;
|
||||
config_ = config;
|
||||
|
||||
if (config_.apMode) {
|
||||
WiFi.softAP(config_.ssid, config_.password);
|
||||
} else {
|
||||
WiFi.begin(config_.ssid, config_.password);
|
||||
uint32_t startMs = millis();
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
if (millis() - startMs > 15000) {
|
||||
log_w("WiFi connection timeout -- running serial-only");
|
||||
return;
|
||||
}
|
||||
delay(500);
|
||||
}
|
||||
}
|
||||
|
||||
server_ = new AsyncWebServer(config_.httpPort);
|
||||
ws_ = new AsyncWebSocket("/ws");
|
||||
|
||||
ws_->onEvent([this](AsyncWebSocket* server, AsyncWebSocketClient* client,
|
||||
AwsEventType type, void* arg,
|
||||
uint8_t* data, size_t len) {
|
||||
handleWebSocketEvent(server, client, type, arg, data, len);
|
||||
});
|
||||
|
||||
server_->addHandler(ws_);
|
||||
server_->begin();
|
||||
}
|
||||
|
||||
void ST4WiFi::handleWebSocketEvent(AsyncWebSocket* server,
|
||||
AsyncWebSocketClient* client,
|
||||
AwsEventType type, void* arg,
|
||||
uint8_t* data, size_t len) {
|
||||
switch (type) {
|
||||
case WS_EVT_CONNECT:
|
||||
// Send current state to newly connected client
|
||||
client->text(stateJson());
|
||||
break;
|
||||
case WS_EVT_DATA: {
|
||||
AwsFrameInfo* info = static_cast<AwsFrameInfo*>(arg);
|
||||
if (info->final && info->index == 0 &&
|
||||
info->len == len && info->opcode == WS_TEXT) {
|
||||
processCommand(client, data, len);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ST4WiFi::processCommand(AsyncWebSocketClient* client,
|
||||
uint8_t* data, size_t len) {
|
||||
JsonDocument doc;
|
||||
DeserializationError err = deserializeJson(doc, data, len);
|
||||
if (err) return;
|
||||
|
||||
const char* cmd = doc["cmd"];
|
||||
if (!cmd) return;
|
||||
|
||||
if (strcmp(cmd, "move") == 0) {
|
||||
const char* axisStr = doc["axis"];
|
||||
const char* dirStr = doc["dir"];
|
||||
if (!axisStr || !dirStr) return;
|
||||
|
||||
ST4AxisId axis;
|
||||
if (strcmp(axisStr, "ra") == 0) axis = ST4AxisId::RA;
|
||||
else if (strcmp(axisStr, "dec") == 0) axis = ST4AxisId::DECLINATION;
|
||||
else return;
|
||||
|
||||
ST4Direction dir;
|
||||
if (strcmp(dirStr, "+") == 0) dir = ST4Direction::PLUS;
|
||||
else if (strcmp(dirStr, "-") == 0) dir = ST4Direction::MINUS;
|
||||
else dir = ST4Direction::STOP;
|
||||
|
||||
controller_->move(axis, dir);
|
||||
broadcastState();
|
||||
|
||||
} else if (strcmp(cmd, "pulse") == 0) {
|
||||
const char* axisStr = doc["axis"];
|
||||
const char* dirStr = doc["dir"];
|
||||
uint32_t ms = doc["ms"] | 0;
|
||||
if (!axisStr || !dirStr || ms == 0) return;
|
||||
|
||||
ST4AxisId axis;
|
||||
if (strcmp(axisStr, "ra") == 0) axis = ST4AxisId::RA;
|
||||
else if (strcmp(axisStr, "dec") == 0) axis = ST4AxisId::DECLINATION;
|
||||
else return;
|
||||
|
||||
ST4Direction dir;
|
||||
if (strcmp(dirStr, "+") == 0) dir = ST4Direction::PLUS;
|
||||
else if (strcmp(dirStr, "-") == 0) dir = ST4Direction::MINUS;
|
||||
else return;
|
||||
|
||||
controller_->pulseGuide(axis, dir, ms);
|
||||
broadcastState();
|
||||
|
||||
} else if (strcmp(cmd, "stop") == 0) {
|
||||
controller_->stopAll();
|
||||
broadcastState();
|
||||
|
||||
} else if (strcmp(cmd, "sync") == 0) {
|
||||
double ra = doc["ra"] | 0.0;
|
||||
double dec = doc["dec"] | 0.0;
|
||||
controller_->sync(ra, dec);
|
||||
broadcastState();
|
||||
|
||||
} else if (strcmp(cmd, "status") == 0) {
|
||||
client->text(stateJson());
|
||||
}
|
||||
}
|
||||
|
||||
void ST4WiFi::broadcastState() {
|
||||
if (ws_->count() > 0) {
|
||||
ws_->textAll(stateJson());
|
||||
}
|
||||
lastBroadcast_ = millis();
|
||||
}
|
||||
|
||||
String ST4WiFi::stateJson() const {
|
||||
ST4State s = controller_->state();
|
||||
|
||||
JsonDocument doc;
|
||||
doc["type"] = "state";
|
||||
doc["connected"] = s.connected;
|
||||
|
||||
auto ra = doc["ra"].to<JsonObject>();
|
||||
ra["active"] = s.ra.active;
|
||||
ra["dir"] = (s.ra.direction == ST4Direction::PLUS) ? "+"
|
||||
: (s.ra.direction == ST4Direction::MINUS) ? "-" : "0";
|
||||
ra["pos"] = s.ra.position;
|
||||
|
||||
auto dec = doc["dec"].to<JsonObject>();
|
||||
dec["active"] = s.dec.active;
|
||||
dec["dir"] = (s.dec.direction == ST4Direction::PLUS) ? "+"
|
||||
: (s.dec.direction == ST4Direction::MINUS) ? "-" : "0";
|
||||
dec["pos"] = s.dec.position;
|
||||
|
||||
String output;
|
||||
serializeJson(doc, output);
|
||||
return output;
|
||||
}
|
||||
|
||||
void ST4WiFi::update() {
|
||||
ws_->cleanupClients();
|
||||
|
||||
ST4Direction currentRa = controller_->axisDirection(ST4AxisId::RA);
|
||||
ST4Direction currentDec = controller_->axisDirection(ST4AxisId::DECLINATION);
|
||||
|
||||
bool changed = (currentRa != lastRaDir_) || (currentDec != lastDecDir_);
|
||||
lastRaDir_ = currentRa;
|
||||
lastDecDir_ = currentDec;
|
||||
|
||||
// Broadcast on direction change or periodically during active slew
|
||||
bool anyActive = controller_->axisActive(ST4AxisId::RA) ||
|
||||
controller_->axisActive(ST4AxisId::DECLINATION);
|
||||
bool periodicUpdate = anyActive &&
|
||||
(millis() - lastBroadcast_ >= config_.broadcastIntervalMs);
|
||||
|
||||
if (changed || periodicUpdate) {
|
||||
broadcastState();
|
||||
}
|
||||
}
|
||||
|
||||
#endif // ST4_WIFI_ENABLED
|
||||
Loading…
x
Reference in New Issue
Block a user