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