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
581 lines
20 KiB
C++
581 lines
20 KiB
C++
#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);
|
|
}
|