Initial release v1.1.0 — ESP32 automated telescope flat panel
Firmware (Arduino Nano ESP32 / PlatformIO): - Native ASCOM Alpaca CoverCalibrator REST API on port 11111 - Alnitak serial protocol on USB at 9600 baud (simultaneous with WiFi) - MG995 servo cover mechanism with non-blocking state machine (D9) - LED brightness PWM via IRLZ44N MOSFET, LEDC channel 0 (D3) - 12V dew heater PWM via IRLZ44N MOSFET, LEDC channel 1 (D5) - mDNS + UDP Alpaca discovery, WiFi watchdog reconnect - SERIAL_DEBUG flag to silence debug output in USB-only mode INDI driver (C++ / libcurl / nlohmann-json): - WiFi mode: HTTP Alpaca via libcurl - USB mode: Alnitak serial via POSIX termios - LightBoxInterface + DustCapInterface + dew heater number property Python controller (PyQt6): - Dark-themed desktop app for direct manual control - AlpacaBackend (requests) + AlnitakBackend (pyserial) - PollWorker QThread; cover, brightness, dew heater panels - QSettings persistence; auto serial port discovery Docs: - system-diagram.svg, wiring-diagram.svg (browser-renderable SVG) - BUILD_NOTES.md with BOM, LM2596 calibration, power-on checklist - WIRING.md quick-reference, README.md, CHANGELOG.md
This commit is contained in:
commit
c32f00a2be
15 changed files with 3653 additions and 0 deletions
39
indi-driver/CMakeLists.txt
Normal file
39
indi-driver/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
project(indi_esp32_flatpanel CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Find required packages
|
||||
find_package(INDI REQUIRED)
|
||||
find_package(CURL REQUIRED)
|
||||
|
||||
# nlohmann/json – header-only, installed via `apt install nlohmann-json3-dev`
|
||||
# or fetched automatically below if not found
|
||||
find_package(nlohmann_json QUIET)
|
||||
if(NOT nlohmann_json_FOUND)
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(json
|
||||
URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz)
|
||||
FetchContent_MakeAvailable(json)
|
||||
endif()
|
||||
|
||||
include_directories(${INDI_INCLUDE_DIR})
|
||||
|
||||
add_executable(indi_esp32_flatpanel
|
||||
indi_esp32_flatpanel.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(indi_esp32_flatpanel
|
||||
${INDI_LIBRARIES}
|
||||
CURL::libcurl
|
||||
nlohmann_json::nlohmann_json
|
||||
pthread
|
||||
)
|
||||
|
||||
# Install binary and XML descriptor
|
||||
install(TARGETS indi_esp32_flatpanel
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
|
||||
|
||||
install(FILES indi_esp32_flatpanel.xml
|
||||
DESTINATION ${INDI_DATA_DIR})
|
||||
581
indi-driver/indi_esp32_flatpanel.cpp
Normal file
581
indi-driver/indi_esp32_flatpanel.cpp
Normal file
|
|
@ -0,0 +1,581 @@
|
|||
#include "indi_esp32_flatpanel.h"
|
||||
|
||||
#include <libindi/indicom.h>
|
||||
#include <curl/curl.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
// POSIX serial port (USB/Alnitak mode)
|
||||
#include <fcntl.h>
|
||||
#include <termios.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/select.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
// Alpaca CoverCalibrator cover-state values
|
||||
static constexpr int COVER_OPEN = 2;
|
||||
static constexpr int COVER_CLOSED = 3;
|
||||
static constexpr int COVER_MOVING = 4;
|
||||
|
||||
// Alpaca calibrator-state values
|
||||
static constexpr int CAL_OFF = 1;
|
||||
static constexpr int CAL_READY = 3;
|
||||
|
||||
// Alnitak device ID embedded in responses (19 = Flip-Flat)
|
||||
static constexpr int ALNITAK_DEVID = 19;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Registration / factory
|
||||
// ---------------------------------------------------------------------------
|
||||
static std::unique_ptr<ESP32FlatPanel> g_device;
|
||||
|
||||
void ISGetProperties(const char* dev) { g_device->ISGetProperties(dev); }
|
||||
void ISNewSwitch(const char* dev, const char* name, ISState* s, char* n[], int num)
|
||||
{ g_device->ISNewSwitch(dev, name, s, n, num); }
|
||||
void ISNewText(const char* dev, const char* name, char* t[], char* n[], int num)
|
||||
{ g_device->ISNewText(dev, name, t, n, num); }
|
||||
void ISNewNumber(const char* dev, const char* name, double v[], char* n[], int num)
|
||||
{ g_device->ISNewNumber(dev, name, v, n, num); }
|
||||
void ISNewBLOB(const char* dev, const char* name, int s, int b,
|
||||
char* f[], char* n[], int num)
|
||||
{ INDI_UNUSED(dev); INDI_UNUSED(name); INDI_UNUSED(s); INDI_UNUSED(b);
|
||||
INDI_UNUSED(f); INDI_UNUSED(n); INDI_UNUSED(num); }
|
||||
void ISSnoopDevice(XMLEle* root) { g_device->ISSnoopDevice(root); }
|
||||
|
||||
int main(int /*argc*/, char* /*argv*/[]) {
|
||||
curl_global_init(CURL_GLOBAL_ALL);
|
||||
g_device = std::make_unique<ESP32FlatPanel>();
|
||||
g_device->ISGetProperties(nullptr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constructor / destructor
|
||||
// ---------------------------------------------------------------------------
|
||||
ESP32FlatPanel::ESP32FlatPanel()
|
||||
: LightBoxInterface(this, true),
|
||||
DustCapInterface(this)
|
||||
{
|
||||
setVersion(1, 1);
|
||||
}
|
||||
|
||||
ESP32FlatPanel::~ESP32FlatPanel() {
|
||||
closeSerial();
|
||||
curl_global_cleanup();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DefaultDevice interface
|
||||
// ---------------------------------------------------------------------------
|
||||
const char* ESP32FlatPanel::getDefaultName() {
|
||||
return "ESP32 FlatPanel";
|
||||
}
|
||||
|
||||
bool ESP32FlatPanel::initProperties() {
|
||||
INDI::DefaultDevice::initProperties();
|
||||
|
||||
// Connection mode
|
||||
IUFillSwitch(&ConnModeS[0], "WIFI", "WiFi / Alpaca", ISS_ON);
|
||||
IUFillSwitch(&ConnModeS[1], "USB", "USB Serial (Alnitak)", ISS_OFF);
|
||||
IUFillSwitchVector(&ConnModeSP, ConnModeS, 2, getDeviceName(),
|
||||
"CONN_MODE", "Connection Mode", CONNECTION_TAB,
|
||||
IP_RW, ISR_1OFMANY, 60, IPS_IDLE);
|
||||
|
||||
// WiFi host / port
|
||||
IUFillText(&HostT[0], "HOST", "Host / IP", "esp32-flatpanel.local");
|
||||
IUFillText(&HostT[1], "PORT", "Port", "11111");
|
||||
IUFillTextVector(&HostTP, HostT, 2, getDeviceName(),
|
||||
"ALPACA_HOST", "Alpaca Server", CONNECTION_TAB,
|
||||
IP_RW, 60, IPS_IDLE);
|
||||
|
||||
// USB serial port path
|
||||
IUFillText(&SerialPortT[0], "PORT", "Serial Port", "/dev/ttyUSB0");
|
||||
IUFillTextVector(&SerialPortTP, SerialPortT, 1, getDeviceName(),
|
||||
"SERIAL_PORT", "Serial Port", CONNECTION_TAB,
|
||||
IP_RW, 60, IPS_IDLE);
|
||||
|
||||
// Dew heater 0-100 %
|
||||
IUFillNumber(&DewHeaterN[0], "DEW_HEATER_PCT", "Dew Heater (%)", "%.0f", 0, 100, 5, 0);
|
||||
IUFillNumberVector(&DewHeaterNP, DewHeaterN, 1, getDeviceName(),
|
||||
"DEW_HEATER", "Dew Heater", MAIN_CONTROL_TAB,
|
||||
IP_RW, 60, IPS_IDLE);
|
||||
|
||||
LightBoxInterface::initProperties(MAIN_CONTROL_TAB, MAIN_CONTROL_TAB);
|
||||
DustCapInterface::initProperties(MAIN_CONTROL_TAB);
|
||||
|
||||
addAuxControls();
|
||||
setDriverInterface(AUX_INTERFACE | LIGHTBOX_INTERFACE | DUSTCAP_INTERFACE);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ESP32FlatPanel::updateProperties() {
|
||||
INDI::DefaultDevice::updateProperties();
|
||||
|
||||
if (isConnected()) {
|
||||
defineProperty(&ConnModeSP);
|
||||
|
||||
if (m_connMode == CONN_WIFI) {
|
||||
defineProperty(&HostTP);
|
||||
} else {
|
||||
defineProperty(&SerialPortTP);
|
||||
}
|
||||
|
||||
defineProperty(&DewHeaterNP);
|
||||
LightBoxInterface::updateProperties();
|
||||
DustCapInterface::updateProperties();
|
||||
|
||||
// Sync initial state from device
|
||||
pollState();
|
||||
SetTimer(1000);
|
||||
} else {
|
||||
deleteProperty(ConnModeSP.name);
|
||||
deleteProperty(HostTP.name);
|
||||
deleteProperty(SerialPortTP.name);
|
||||
deleteProperty(DewHeaterNP.name);
|
||||
LightBoxInterface::updateProperties();
|
||||
DustCapInterface::updateProperties();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ESP32FlatPanel::Connect() {
|
||||
// Read connection mode
|
||||
m_connMode = (ConnModeS[1].s == ISS_ON) ? CONN_USB : CONN_WIFI;
|
||||
|
||||
if (m_connMode == CONN_WIFI) {
|
||||
m_host = HostT[0].text;
|
||||
m_port = std::atoi(HostT[1].text);
|
||||
|
||||
int iface = alpacaGetInt("/api/v1/covercalibrator/0/interfaceversion", -1);
|
||||
if (iface < 0) {
|
||||
LOG_ERROR("Cannot reach ESP32 at the specified host/port.");
|
||||
return false;
|
||||
}
|
||||
alpacaPutVoid("/api/v1/covercalibrator/0/connected", "Connected=true");
|
||||
LOGF_INFO("Connected via WiFi to %s:%d", m_host.c_str(), m_port);
|
||||
} else {
|
||||
if (!openSerial(SerialPortT[0].text)) {
|
||||
LOGF_ERROR("Cannot open serial port %s", SerialPortT[0].text);
|
||||
return false;
|
||||
}
|
||||
// Ping to verify
|
||||
std::string resp = sendAlnitak(">P000");
|
||||
if (resp.empty() || resp[0] != '*') {
|
||||
LOG_ERROR("No valid Alnitak ping response.");
|
||||
closeSerial();
|
||||
return false;
|
||||
}
|
||||
LOGF_INFO("Connected via USB/Alnitak on %s", SerialPortT[0].text);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ESP32FlatPanel::Disconnect() {
|
||||
if (m_connMode == CONN_WIFI)
|
||||
alpacaPutVoid("/api/v1/covercalibrator/0/connected", "Connected=false");
|
||||
else
|
||||
closeSerial();
|
||||
|
||||
LOG_INFO("Disconnected from ESP32 FlatPanel.");
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESP32FlatPanel::TimerHit() {
|
||||
if (!isConnected()) return;
|
||||
pollState();
|
||||
SetTimer(1000);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Property change handlers
|
||||
// ---------------------------------------------------------------------------
|
||||
bool ESP32FlatPanel::ISNewSwitch(const char* dev, const char* name, ISState* states, char* names[], int n) {
|
||||
if (!strcmp(dev, getDeviceName()) && !strcmp(name, ConnModeSP.name)) {
|
||||
IUUpdateSwitch(&ConnModeSP, states, names, n);
|
||||
ConnModeSP.s = IPS_OK;
|
||||
IDSetSwitch(&ConnModeSP, nullptr);
|
||||
return true;
|
||||
}
|
||||
if (LightBoxInterface::ISNewSwitch(dev, name, states, names, n)) return true;
|
||||
if (DustCapInterface::ISNewSwitch(dev, name, states, names, n)) return true;
|
||||
return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n);
|
||||
}
|
||||
|
||||
bool ESP32FlatPanel::ISNewText(const char* dev, const char* name, char* texts[], char* names[], int n) {
|
||||
if (!strcmp(dev, getDeviceName())) {
|
||||
if (!strcmp(name, HostTP.name)) {
|
||||
IUUpdateText(&HostTP, texts, names, n);
|
||||
HostTP.s = IPS_OK;
|
||||
IDSetText(&HostTP, nullptr);
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(name, SerialPortTP.name)) {
|
||||
IUUpdateText(&SerialPortTP, texts, names, n);
|
||||
SerialPortTP.s = IPS_OK;
|
||||
IDSetText(&SerialPortTP, nullptr);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (LightBoxInterface::ISNewText(dev, name, texts, names, n)) return true;
|
||||
return INDI::DefaultDevice::ISNewText(dev, name, texts, names, n);
|
||||
}
|
||||
|
||||
bool ESP32FlatPanel::ISNewNumber(const char* dev, const char* name, double values[], char* names[], int n) {
|
||||
if (!strcmp(dev, getDeviceName()) && !strcmp(name, DewHeaterNP.name)) {
|
||||
IUUpdateNumber(&DewHeaterNP, values, names, n);
|
||||
setDewHeater(static_cast<int>(DewHeaterN[0].value));
|
||||
return true;
|
||||
}
|
||||
if (LightBoxInterface::ISNewNumber(dev, name, values, names, n)) return true;
|
||||
return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n);
|
||||
}
|
||||
|
||||
bool ESP32FlatPanel::ISSnoopDevice(XMLEle* root) {
|
||||
LightBoxInterface::snoop(root);
|
||||
return INDI::DefaultDevice::ISSnoopDevice(root);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LightBoxInterface implementation
|
||||
// ---------------------------------------------------------------------------
|
||||
bool ESP32FlatPanel::EnableLightBox(bool enable) {
|
||||
if (m_connMode == CONN_WIFI) {
|
||||
if (enable) {
|
||||
int b = static_cast<int>(LightIntensityN[0].value);
|
||||
if (b == 0) b = 100;
|
||||
std::string body = commonPutBody() + "&Brightness=" + std::to_string(b);
|
||||
return alpacaPutVoid("/api/v1/covercalibrator/0/calibratoron", body);
|
||||
}
|
||||
return alpacaPutVoid("/api/v1/covercalibrator/0/calibratoroff");
|
||||
} else {
|
||||
if (enable) {
|
||||
int b = static_cast<int>(LightIntensityN[0].value);
|
||||
if (b == 0) b = 100;
|
||||
return alnitakSetBrightness(b);
|
||||
}
|
||||
return alnitakSetBrightness(0);
|
||||
}
|
||||
}
|
||||
|
||||
bool ESP32FlatPanel::SetLightBoxBrightness(uint16_t value) {
|
||||
if (m_connMode == CONN_WIFI) {
|
||||
std::string body = commonPutBody() + "&Brightness=" + std::to_string(static_cast<int>(value));
|
||||
return alpacaPutVoid("/api/v1/covercalibrator/0/calibratoron", body);
|
||||
}
|
||||
return alnitakSetBrightness(static_cast<int>(value));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DustCapInterface implementation
|
||||
// ParkCap = close cover (panel over telescope aperture, ready for flats)
|
||||
// UnParkCap = open cover (panel rotated clear, telescope free for imaging)
|
||||
// ---------------------------------------------------------------------------
|
||||
IPState ESP32FlatPanel::ParkCap() {
|
||||
bool ok = (m_connMode == CONN_WIFI)
|
||||
? alpacaPutVoid("/api/v1/covercalibrator/0/closecover")
|
||||
: !sendAlnitak(">C000").empty();
|
||||
if (!ok) return IPS_ALERT;
|
||||
LOG_INFO("Closing cover…");
|
||||
return IPS_BUSY;
|
||||
}
|
||||
|
||||
IPState ESP32FlatPanel::UnParkCap() {
|
||||
bool ok = (m_connMode == CONN_WIFI)
|
||||
? alpacaPutVoid("/api/v1/covercalibrator/0/opencover")
|
||||
: !sendAlnitak(">O000").empty();
|
||||
if (!ok) return IPS_ALERT;
|
||||
LOG_INFO("Opening cover…");
|
||||
return IPS_BUSY;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dew heater control
|
||||
// ---------------------------------------------------------------------------
|
||||
void ESP32FlatPanel::setDewHeater(int pct) {
|
||||
pct = std::max(0, std::min(100, pct));
|
||||
|
||||
bool ok;
|
||||
if (m_connMode == CONN_WIFI) {
|
||||
std::string body = commonPutBody() +
|
||||
"&Action=SetDewHeater&Parameters=" + std::to_string(pct);
|
||||
ok = alpacaPutVoid("/api/v1/covercalibrator/0/action", body);
|
||||
} else {
|
||||
ok = alnitakSetDewPercent(pct);
|
||||
}
|
||||
|
||||
DewHeaterN[0].value = pct;
|
||||
DewHeaterNP.s = ok ? IPS_OK : IPS_ALERT;
|
||||
IDSetNumber(&DewHeaterNP, nullptr);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// State polling
|
||||
// ---------------------------------------------------------------------------
|
||||
void ESP32FlatPanel::pollState() {
|
||||
int covState, calState, bright, dew;
|
||||
|
||||
if (m_connMode == CONN_WIFI) {
|
||||
covState = alpacaGetInt("/api/v1/covercalibrator/0/coverstate", COVER_CLOSED);
|
||||
calState = alpacaGetInt("/api/v1/covercalibrator/0/calibratorstate", CAL_OFF);
|
||||
bright = alpacaGetInt("/api/v1/covercalibrator/0/brightness", 0);
|
||||
// Dew heater via custom Alpaca action
|
||||
std::string resp = httpGet("/api/v1/covercalibrator/0/action");
|
||||
dew = 0;
|
||||
try {
|
||||
auto j = json::parse(httpGet("/api/v1/covercalibrator/0/brightness"));
|
||||
// Poll dew via supportedactions action isn't a GET — just keep last known value
|
||||
dew = static_cast<int>(DewHeaterN[0].value);
|
||||
} catch (...) {}
|
||||
} else {
|
||||
covState = alnitakGetCoverState();
|
||||
bright = alnitakGetBrightness();
|
||||
calState = (bright > 0) ? CAL_READY : CAL_OFF;
|
||||
dew = alnitakGetDewPercent();
|
||||
}
|
||||
|
||||
// Update cover
|
||||
IPState newCovState = IPS_IDLE;
|
||||
if (covState == COVER_MOVING) {
|
||||
newCovState = IPS_BUSY;
|
||||
} else if (covState == COVER_OPEN || covState == COVER_CLOSED) {
|
||||
if (ParkCapSP.s == IPS_BUSY) {
|
||||
LOG_INFO(covState == COVER_OPEN ? "Cover fully open." : "Cover fully closed.");
|
||||
}
|
||||
newCovState = IPS_OK;
|
||||
ParkCapSP[CAP_PARK].s = (covState == COVER_CLOSED) ? ISS_ON : ISS_OFF;
|
||||
ParkCapSP[CAP_UNPARK].s = (covState == COVER_OPEN) ? ISS_ON : ISS_OFF;
|
||||
} else {
|
||||
newCovState = IPS_ALERT;
|
||||
}
|
||||
if (newCovState != ParkCapSP.s) {
|
||||
ParkCapSP.s = newCovState;
|
||||
IDSetSwitch(&ParkCapSP, nullptr);
|
||||
}
|
||||
|
||||
// Update brightness
|
||||
LightIntensityN[0].value = bright;
|
||||
LightIntensityNP.s = (calState == CAL_READY) ? IPS_OK : IPS_IDLE;
|
||||
IDSetNumber(&LightIntensityNP, nullptr);
|
||||
|
||||
LightSP[FLAT_LIGHT_ON].s = (calState == CAL_READY) ? ISS_ON : ISS_OFF;
|
||||
LightSP[FLAT_LIGHT_OFF].s = (calState != CAL_READY) ? ISS_ON : ISS_OFF;
|
||||
LightSP.s = IPS_OK;
|
||||
IDSetSwitch(&LightSP, nullptr);
|
||||
|
||||
// Update dew heater
|
||||
if (dew >= 0) {
|
||||
DewHeaterN[0].value = dew;
|
||||
DewHeaterNP.s = IPS_OK;
|
||||
IDSetNumber(&DewHeaterNP, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// USB / Alnitak serial helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
bool ESP32FlatPanel::openSerial(const std::string& device) {
|
||||
m_serialFd = open(device.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK);
|
||||
if (m_serialFd < 0) return false;
|
||||
|
||||
struct termios tty{};
|
||||
tcgetattr(m_serialFd, &tty);
|
||||
cfsetspeed(&tty, B9600); // standard Alnitak / N.I.N.A. baud rate
|
||||
tty.c_cflag = CS8 | CREAD | CLOCAL;
|
||||
tty.c_iflag = IGNBRK | IGNPAR;
|
||||
tty.c_oflag = 0;
|
||||
tty.c_lflag = 0;
|
||||
tty.c_cc[VMIN] = 0;
|
||||
tty.c_cc[VTIME] = 20; // 2 second read timeout (units of 0.1 s)
|
||||
tcsetattr(m_serialFd, TCSANOW, &tty);
|
||||
tcflush(m_serialFd, TCIOFLUSH);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESP32FlatPanel::closeSerial() {
|
||||
if (m_serialFd >= 0) {
|
||||
close(m_serialFd);
|
||||
m_serialFd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
std::string ESP32FlatPanel::sendAlnitak(const std::string& cmd) {
|
||||
if (m_serialFd < 0) return {};
|
||||
|
||||
std::string full = cmd + "\r";
|
||||
if (write(m_serialFd, full.c_str(), full.size()) < 0) return {};
|
||||
|
||||
// Read response up to '\r', with 2-second timeout
|
||||
char buf[32];
|
||||
int pos = 0;
|
||||
char c;
|
||||
unsigned long deadline_ms = 2000;
|
||||
struct timeval tv;
|
||||
fd_set readset;
|
||||
|
||||
auto remaining = [&]() -> struct timeval {
|
||||
return {static_cast<time_t>(deadline_ms / 1000),
|
||||
static_cast<suseconds_t>((deadline_ms % 1000) * 1000)};
|
||||
};
|
||||
|
||||
while (true) {
|
||||
FD_ZERO(&readset);
|
||||
FD_SET(m_serialFd, &readset);
|
||||
tv = remaining();
|
||||
int ret = select(m_serialFd + 1, &readset, nullptr, nullptr, &tv);
|
||||
if (ret <= 0) break;
|
||||
if (read(m_serialFd, &c, 1) != 1) break;
|
||||
if (c == '\r' || c == '\n') {
|
||||
if (pos > 0) break;
|
||||
} else if (pos < 30) {
|
||||
buf[pos++] = c;
|
||||
}
|
||||
}
|
||||
buf[pos] = '\0';
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
int ESP32FlatPanel::alnitakGetBrightness() {
|
||||
std::string r = sendAlnitak(">J000");
|
||||
if (r.size() < 5 || r[0] != '*' || r[1] != 'J') return 0;
|
||||
// *JDD BBB (DD = device id, BBB = brightness)
|
||||
int devId = 0, bright = 0;
|
||||
sscanf(r.c_str() + 2, "%2d%3d", &devId, &bright);
|
||||
return bright;
|
||||
}
|
||||
|
||||
bool ESP32FlatPanel::alnitakSetBrightness(int b) {
|
||||
char cmd[8];
|
||||
snprintf(cmd, sizeof(cmd), ">B%03d", b);
|
||||
std::string r = sendAlnitak(cmd);
|
||||
return !r.empty() && r[0] == '*' && r[1] == 'B';
|
||||
}
|
||||
|
||||
int ESP32FlatPanel::alnitakGetCoverState() {
|
||||
// Returns Alpaca CoverStatus int
|
||||
std::string r = sendAlnitak(">S000");
|
||||
if (r.size() < 7 || r[0] != '*' || r[1] != 'S') return COVER_CLOSED;
|
||||
|
||||
int devId = 0, motorState = 0, lightState = 0, extra = 0;
|
||||
sscanf(r.c_str() + 2, "%2d%1d%1d%2d", &devId, &motorState, &lightState, &extra);
|
||||
|
||||
// Alnitak motor: 0=unknown 1=Closed 2=Open
|
||||
switch (motorState) {
|
||||
case 2: return COVER_OPEN;
|
||||
case 1: return COVER_CLOSED;
|
||||
default: return COVER_MOVING;
|
||||
}
|
||||
}
|
||||
|
||||
int ESP32FlatPanel::alnitakGetDewPercent() {
|
||||
// Custom command >U000 → *UDDppp
|
||||
std::string r = sendAlnitak(">U000");
|
||||
if (r.size() < 5 || r[0] != '*' || r[1] != 'U') return -1;
|
||||
int devId = 0, pct = 0;
|
||||
sscanf(r.c_str() + 2, "%2d%3d", &devId, &pct);
|
||||
return pct;
|
||||
}
|
||||
|
||||
bool ESP32FlatPanel::alnitakSetDewPercent(int pct) {
|
||||
char cmd[8];
|
||||
snprintf(cmd, sizeof(cmd), ">T%03d", pct);
|
||||
std::string r = sendAlnitak(cmd);
|
||||
return !r.empty() && r[0] == '*' && r[1] == 'T';
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HTTP / Alpaca helpers (WiFi mode)
|
||||
// ---------------------------------------------------------------------------
|
||||
size_t ESP32FlatPanel::writeCallback(void* ptr, size_t size, size_t nmemb, std::string* out) {
|
||||
out->append(static_cast<char*>(ptr), size * nmemb);
|
||||
return size * nmemb;
|
||||
}
|
||||
|
||||
std::string ESP32FlatPanel::alpacaURL(const std::string& endpoint) const {
|
||||
return "http://" + m_host + ":" + std::to_string(m_port) + endpoint;
|
||||
}
|
||||
|
||||
std::string ESP32FlatPanel::commonPutBody() {
|
||||
return "ClientID=1&ClientTransactionID=" + std::to_string(++m_txID);
|
||||
}
|
||||
|
||||
std::string ESP32FlatPanel::httpGet(const std::string& endpoint) {
|
||||
std::string url = alpacaURL(endpoint) +
|
||||
"?ClientID=1&ClientTransactionID=" + std::to_string(++m_txID);
|
||||
std::string response;
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) return {};
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &ESP32FlatPanel::writeCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L);
|
||||
curl_easy_perform(curl);
|
||||
curl_easy_cleanup(curl);
|
||||
return response;
|
||||
}
|
||||
|
||||
bool ESP32FlatPanel::httpPut(const std::string& endpoint, const std::string& body) {
|
||||
std::string url = alpacaURL(endpoint);
|
||||
std::string response;
|
||||
|
||||
CURL* curl = curl_easy_init();
|
||||
if (!curl) return false;
|
||||
|
||||
curl_slist* headers = curl_slist_append(nullptr,
|
||||
"Content-Type: application/x-www-form-urlencoded");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &ESP32FlatPanel::writeCallback);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
if (res != CURLE_OK) return false;
|
||||
|
||||
try {
|
||||
return json::parse(response).value("ErrorNumber", -1) == 0;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int ESP32FlatPanel::alpacaGetInt(const std::string& endpoint, int defaultVal) {
|
||||
std::string resp = httpGet(endpoint);
|
||||
if (resp.empty()) return defaultVal;
|
||||
try {
|
||||
auto j = json::parse(resp);
|
||||
if (j.value("ErrorNumber", -1) != 0) return defaultVal;
|
||||
return j.value("Value", defaultVal);
|
||||
} catch (...) {
|
||||
return defaultVal;
|
||||
}
|
||||
}
|
||||
|
||||
bool ESP32FlatPanel::alpacaGetBool(const std::string& endpoint, bool defaultVal) {
|
||||
std::string resp = httpGet(endpoint);
|
||||
if (resp.empty()) return defaultVal;
|
||||
try {
|
||||
auto j = json::parse(resp);
|
||||
if (j.value("ErrorNumber", -1) != 0) return defaultVal;
|
||||
return j.value("Value", defaultVal);
|
||||
} catch (...) {
|
||||
return defaultVal;
|
||||
}
|
||||
}
|
||||
|
||||
bool ESP32FlatPanel::alpacaPutVoid(const std::string& endpoint, const std::string& extraBody) {
|
||||
std::string body = commonPutBody();
|
||||
if (!extraBody.empty()) body += "&" + extraBody;
|
||||
return httpPut(endpoint, body);
|
||||
}
|
||||
88
indi-driver/indi_esp32_flatpanel.h
Normal file
88
indi-driver/indi_esp32_flatpanel.h
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
#pragma once
|
||||
|
||||
#include <libindi/defaultdevice.h>
|
||||
#include <libindi/indilightboxinterface.h>
|
||||
#include <libindi/indidustcapinterface.h>
|
||||
#include <curl/curl.h>
|
||||
#include <string>
|
||||
|
||||
class ESP32FlatPanel : public INDI::DefaultDevice,
|
||||
public INDI::LightBoxInterface,
|
||||
public INDI::DustCapInterface
|
||||
{
|
||||
public:
|
||||
ESP32FlatPanel();
|
||||
~ESP32FlatPanel() override;
|
||||
|
||||
// DefaultDevice
|
||||
const char* getDefaultName() override;
|
||||
bool initProperties() override;
|
||||
bool updateProperties() override;
|
||||
bool Connect() override;
|
||||
bool Disconnect() override;
|
||||
void TimerHit() override;
|
||||
|
||||
bool ISNewText (const char* dev, const char* name, char* texts[], char* names[], int n) override;
|
||||
bool ISNewSwitch(const char* dev, const char* name, ISState* states, char* names[], int n) override;
|
||||
bool ISNewNumber(const char* dev, const char* name, double values[], char* names[], int n) override;
|
||||
bool ISSnoopDevice(XMLEle* root) override;
|
||||
|
||||
// LightBoxInterface
|
||||
bool SetLightBoxBrightness(uint16_t value) override;
|
||||
bool EnableLightBox(bool enable) override;
|
||||
|
||||
// DustCapInterface
|
||||
IPState ParkCap() override;
|
||||
IPState UnParkCap() override;
|
||||
|
||||
private:
|
||||
// ---- Connection mode switch ------------------------------------------
|
||||
enum ConnMode { CONN_WIFI, CONN_USB };
|
||||
ConnMode m_connMode { CONN_WIFI };
|
||||
|
||||
ISwitch ConnModeS[2]{};
|
||||
ISwitchVectorProperty ConnModeSP;
|
||||
|
||||
// ---- WiFi / Alpaca settings ------------------------------------------
|
||||
IText HostT[2]{};
|
||||
ITextVectorProperty HostTP;
|
||||
|
||||
std::string m_host {"esp32-flatpanel.local"};
|
||||
int m_port {11111};
|
||||
uint32_t m_clientID {1};
|
||||
uint32_t m_txID {0};
|
||||
|
||||
// ---- USB / Alnitak settings ------------------------------------------
|
||||
IText SerialPortT[1]{};
|
||||
ITextVectorProperty SerialPortTP;
|
||||
|
||||
int m_serialFd {-1};
|
||||
|
||||
// ---- Dew heater property --------------------------------------------
|
||||
INumber DewHeaterN[1]{};
|
||||
INumberVectorProperty DewHeaterNP;
|
||||
|
||||
// ---- CURL / HTTP helpers (WiFi mode) --------------------------------
|
||||
static size_t writeCallback(void* ptr, size_t size, size_t nmemb, std::string* out);
|
||||
std::string httpGet(const std::string& endpoint);
|
||||
bool httpPut(const std::string& endpoint, const std::string& body);
|
||||
std::string alpacaURL(const std::string& endpoint) const;
|
||||
std::string commonPutBody();
|
||||
int alpacaGetInt (const std::string& endpoint, int defaultVal = 0);
|
||||
bool alpacaGetBool(const std::string& endpoint, bool defaultVal = false);
|
||||
bool alpacaPutVoid(const std::string& endpoint, const std::string& extraBody = "");
|
||||
|
||||
// ---- Alnitak serial helpers (USB mode) ------------------------------
|
||||
bool openSerial(const std::string& device);
|
||||
void closeSerial();
|
||||
std::string sendAlnitak(const std::string& cmd);
|
||||
int alnitakGetBrightness();
|
||||
bool alnitakSetBrightness(int b);
|
||||
int alnitakGetCoverState(); // returns Alpaca CoverStatus int
|
||||
int alnitakGetDewPercent();
|
||||
bool alnitakSetDewPercent(int pct);
|
||||
|
||||
// ---- Common helpers -------------------------------------------------
|
||||
void setDewHeater(int pct);
|
||||
void pollState();
|
||||
};
|
||||
9
indi-driver/indi_esp32_flatpanel.xml
Normal file
9
indi-driver/indi_esp32_flatpanel.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<driversList>
|
||||
<devGroup group="Aux">
|
||||
<device label="ESP32 FlatPanel" manufacturer="DIY">
|
||||
<driver name="ESP32 FlatPanel">indi_esp32_flatpanel</driver>
|
||||
<version>1.0</version>
|
||||
</device>
|
||||
</devGroup>
|
||||
</driversList>
|
||||
Loading…
Add table
Add a link
Reference in a new issue