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
14
firmware/platformio.ini
Normal file
14
firmware/platformio.ini
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[env:arduino_nano_esp32]
|
||||
platform = espressif32
|
||||
board = arduino_nano_esp32
|
||||
framework = arduino
|
||||
|
||||
upload_speed = 921600
|
||||
monitor_speed = 115200
|
||||
|
||||
lib_deps =
|
||||
; More actively maintained forks of AsyncTCP / AsyncWebServer
|
||||
esphome/AsyncTCP-esphome @ ^2.1.4
|
||||
esphome/ESPAsyncWebServer-esphome @ ^3.2.2
|
||||
bblanchon/ArduinoJson @ >=6.21.0 <7.0.0
|
||||
madhephaestus/ESP32Servo @ ^0.13.0
|
||||
58
firmware/src/config.h
Normal file
58
firmware/src/config.h
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#pragma once
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WiFi
|
||||
// ---------------------------------------------------------------------------
|
||||
#define WIFI_SSID "your_wifi_ssid"
|
||||
#define WIFI_PASSWORD "your_wifi_password"
|
||||
#define HOSTNAME "esp32-flatpanel" // accessible as esp32-flatpanel.local
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pin assignments (Arduino Nano ESP32 D-pin numbers)
|
||||
// ---------------------------------------------------------------------------
|
||||
#define SERVO_PIN 9 // MG995 signal wire → D9
|
||||
#define LED_PWM_PIN 3 // MOSFET gate → D3
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Servo angles (degrees) – tune to your mechanical stop positions
|
||||
// ---------------------------------------------------------------------------
|
||||
#define SERVO_CLOSED 0 // panel in image path (cover closed / panel active)
|
||||
#define SERVO_OPEN 90 // panel rotated clear of telescope
|
||||
|
||||
// Time (ms) to wait for the MG995 to reach its target before marking done.
|
||||
// MG995 spec: ~0.2 s / 60° no-load; 1 500 ms covers 90° + margin.
|
||||
#define COVER_MOVE_TIMEOUT_MS 1500
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LED PWM (LEDC channel 0, 8-bit → 0-255)
|
||||
// ---------------------------------------------------------------------------
|
||||
#define LED_PWM_CHANNEL 0
|
||||
#define LED_PWM_FREQ 5000 // 5 kHz – above visible flicker
|
||||
#define LED_PWM_RESOLUTION 8
|
||||
#define MAX_BRIGHTNESS 255
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dew heater (12 V via N-channel MOSFET, same circuit as LED)
|
||||
// ---------------------------------------------------------------------------
|
||||
#define DEW_HEATER_PIN 5 // MOSFET gate → D5
|
||||
#define DEW_PWM_CHANNEL 1 // LEDC channel 1 (channel 0 used for LED)
|
||||
#define DEW_PWM_FREQ 1000 // 1 kHz – fine for resistive heating element
|
||||
#define DEW_PWM_RESOLUTION 8 // 8-bit → 0-255 maps to 0-100 %
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// USB / Alnitak serial protocol
|
||||
// Set SERIAL_DEBUG to false when using USB mode so the Alnitak driver
|
||||
// receives only properly-formatted responses. Debug output is harmless in
|
||||
// WiFi-only mode because Alnitak parsers ignore lines not starting with '*'.
|
||||
// ---------------------------------------------------------------------------
|
||||
#define SERIAL_DEBUG true
|
||||
// Device type 19 = Flip-Flat (cover + calibrator). Recognised natively by
|
||||
// N.I.N.A., SGPro, APT, and the INDI indi_flipflat driver.
|
||||
#define ALNITAK_DEVICE_ID 19
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ASCOM Alpaca
|
||||
// ---------------------------------------------------------------------------
|
||||
#define ALPACA_PORT 11111
|
||||
// Generate a fresh UUID at https://www.uuidgenerator.net/ and paste it here.
|
||||
#define DEVICE_UUID "a2ba5c7e-4b3f-4f98-a7d0-1c5b8e2f9d3a"
|
||||
515
firmware/src/main.cpp
Normal file
515
firmware/src/main.cpp
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <ESPmDNS.h>
|
||||
#include <AsyncUDP.h>
|
||||
#include <AsyncTCP.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <ESP32Servo.h>
|
||||
#include "config.h"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Conditional debug output – disabled in USB/Alnitak mode to keep the serial
|
||||
// line clean for the Alnitak protocol parser.
|
||||
// ---------------------------------------------------------------------------
|
||||
#if SERIAL_DEBUG
|
||||
#define DBG(...) Serial.print(__VA_ARGS__)
|
||||
#define DBGF(...) Serial.printf(__VA_ARGS__)
|
||||
#define DBGLN(...) Serial.println(__VA_ARGS__)
|
||||
#else
|
||||
#define DBG(...)
|
||||
#define DBGF(...)
|
||||
#define DBGLN(...)
|
||||
#endif
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ASCOM Alpaca state enumerations
|
||||
// ---------------------------------------------------------------------------
|
||||
enum CalibratorStatus : int {
|
||||
CalNotPresent = 0,
|
||||
CalOff = 1,
|
||||
CalNotReady = 2,
|
||||
CalReady = 3,
|
||||
CalUnknown = 4,
|
||||
CalError = 5
|
||||
};
|
||||
|
||||
enum CoverStatus : int {
|
||||
CovNotPresent = 0,
|
||||
CovUnknown = 1,
|
||||
CovOpen = 2,
|
||||
CovClosed = 3,
|
||||
CovMoving = 4,
|
||||
CovError = 5
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Global device state
|
||||
// ---------------------------------------------------------------------------
|
||||
static int g_brightness = 0;
|
||||
static int g_dewPercent = 0;
|
||||
static CalibratorStatus g_calState = CalOff;
|
||||
static CoverStatus g_covState = CovClosed;
|
||||
static bool g_connected = false;
|
||||
static uint32_t g_txID = 0;
|
||||
|
||||
// Cover-movement state machine
|
||||
static CoverStatus g_moveTarget = CovClosed;
|
||||
static unsigned long g_moveStartMs = 0;
|
||||
static bool g_moving = false;
|
||||
|
||||
AsyncWebServer server(ALPACA_PORT);
|
||||
AsyncUDP udp;
|
||||
Servo coverServo;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hardware helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
static void applyBrightness(int b) {
|
||||
b = constrain(b, 0, MAX_BRIGHTNESS);
|
||||
g_brightness = b;
|
||||
ledcWrite(LED_PWM_CHANNEL, b);
|
||||
g_calState = (b > 0) ? CalReady : CalOff;
|
||||
}
|
||||
|
||||
static void applyDewHeater(int pct) {
|
||||
g_dewPercent = constrain(pct, 0, 100);
|
||||
// map 0-100 % to 0-255 PWM, with full off at 0 % and full on at 100 %
|
||||
ledcWrite(DEW_PWM_CHANNEL, map(g_dewPercent, 0, 100, 0, 255));
|
||||
}
|
||||
|
||||
static void startCoverMove(CoverStatus target) {
|
||||
coverServo.write(target == CovOpen ? SERVO_OPEN : SERVO_CLOSED);
|
||||
g_covState = CovMoving;
|
||||
g_moveTarget = target;
|
||||
g_moveStartMs = millis();
|
||||
g_moving = true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Alnitak serial protocol (USB mode)
|
||||
//
|
||||
// Format: command >CXXX\r (C = command char, XXX = 3-digit value)
|
||||
// response *CDDVVV\r (DD = 2-digit device type, VVV = 3-digit value)
|
||||
//
|
||||
// Standard commands understood by N.I.N.A., SGPro, APT, indi_flipflat:
|
||||
// P – Ping B – Set brightness J – Get brightness
|
||||
// L – Light on D – Light off
|
||||
// O – Open C – Close H – Halt
|
||||
// S – State (motorStatus + lightStatus)
|
||||
//
|
||||
// Custom extensions (for our INDI/ASCOM driver when using USB mode):
|
||||
// T – Set dew heater % U – Get dew heater %
|
||||
// ---------------------------------------------------------------------------
|
||||
static int alnitakMotorState() {
|
||||
switch (g_covState) {
|
||||
case CovOpen: return 2; // Open
|
||||
case CovClosed: return 1; // Closed
|
||||
default: return 0; // NotOpenOrClosed / moving / unknown
|
||||
}
|
||||
}
|
||||
|
||||
static void processAlnitak(const char* cmd) {
|
||||
if (cmd[0] != '>') return;
|
||||
|
||||
char ch = cmd[1];
|
||||
int val = (strlen(cmd) > 2) ? atoi(cmd + 2) : 0;
|
||||
char resp[20];
|
||||
|
||||
switch (ch) {
|
||||
case 'P': // Ping
|
||||
snprintf(resp, sizeof(resp), "*P%02d000\r", ALNITAK_DEVICE_ID);
|
||||
break;
|
||||
|
||||
case 'S': // State: *SDD<motor><light>00
|
||||
snprintf(resp, sizeof(resp), "*S%02d%d%d00\r",
|
||||
ALNITAK_DEVICE_ID,
|
||||
alnitakMotorState(),
|
||||
(g_calState == CalReady) ? 1 : 0);
|
||||
break;
|
||||
|
||||
case 'B': // Set brightness 0-255
|
||||
applyBrightness(constrain(val, 0, MAX_BRIGHTNESS));
|
||||
snprintf(resp, sizeof(resp), "*B%02d%03d\r", ALNITAK_DEVICE_ID, g_brightness);
|
||||
break;
|
||||
|
||||
case 'J': // Get brightness
|
||||
snprintf(resp, sizeof(resp), "*J%02d%03d\r", ALNITAK_DEVICE_ID, g_brightness);
|
||||
break;
|
||||
|
||||
case 'L': // Light on (keep existing brightness; default 100 if zero)
|
||||
if (g_brightness == 0) applyBrightness(100);
|
||||
else applyBrightness(g_brightness);
|
||||
snprintf(resp, sizeof(resp), "*L%02d000\r", ALNITAK_DEVICE_ID);
|
||||
break;
|
||||
|
||||
case 'D': // Light off
|
||||
applyBrightness(0);
|
||||
snprintf(resp, sizeof(resp), "*D%02d000\r", ALNITAK_DEVICE_ID);
|
||||
break;
|
||||
|
||||
case 'O': // Open cover
|
||||
if (!g_moving) startCoverMove(CovOpen);
|
||||
snprintf(resp, sizeof(resp), "*O%02d000\r", ALNITAK_DEVICE_ID);
|
||||
break;
|
||||
|
||||
case 'C': // Close cover
|
||||
if (!g_moving) startCoverMove(CovClosed);
|
||||
snprintf(resp, sizeof(resp), "*C%02d000\r", ALNITAK_DEVICE_ID);
|
||||
break;
|
||||
|
||||
case 'H': // Halt
|
||||
if (g_moving) {
|
||||
coverServo.write(coverServo.read());
|
||||
g_covState = CovUnknown;
|
||||
g_moving = false;
|
||||
}
|
||||
snprintf(resp, sizeof(resp), "*H%02d000\r", ALNITAK_DEVICE_ID);
|
||||
break;
|
||||
|
||||
case 'T': // (custom) Set dew heater percent 0-100
|
||||
applyDewHeater(constrain(val, 0, 100));
|
||||
snprintf(resp, sizeof(resp), "*T%02d%03d\r", ALNITAK_DEVICE_ID, g_dewPercent);
|
||||
break;
|
||||
|
||||
case 'U': // (custom) Get dew heater percent
|
||||
snprintf(resp, sizeof(resp), "*U%02d%03d\r", ALNITAK_DEVICE_ID, g_dewPercent);
|
||||
break;
|
||||
|
||||
default:
|
||||
return; // unknown command – send nothing
|
||||
}
|
||||
|
||||
Serial.print(resp);
|
||||
}
|
||||
|
||||
static void handleAlnitak() {
|
||||
static char buf[20];
|
||||
static uint8_t pos = 0;
|
||||
|
||||
while (Serial.available()) {
|
||||
char c = static_cast<char>(Serial.read());
|
||||
if (c == '\r' || c == '\n') {
|
||||
if (pos > 0) {
|
||||
buf[pos] = '\0';
|
||||
processAlnitak(buf);
|
||||
pos = 0;
|
||||
}
|
||||
} else if (pos < sizeof(buf) - 1) {
|
||||
buf[pos++] = c;
|
||||
} else {
|
||||
pos = 0; // buffer overflow guard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Alpaca response helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
static int clientTxID(AsyncWebServerRequest* req) {
|
||||
const AsyncWebParameter* p = req->getParam("ClientTransactionID");
|
||||
if (!p) p = req->getParam("ClientTransactionID", true);
|
||||
return p ? p->value().toInt() : 0;
|
||||
}
|
||||
|
||||
static void fillCommon(StaticJsonDocument<512>& doc, AsyncWebServerRequest* req,
|
||||
int errNum = 0, const char* errMsg = "") {
|
||||
doc["ClientTransactionID"] = clientTxID(req);
|
||||
doc["ServerTransactionID"] = ++g_txID;
|
||||
doc["ErrorNumber"] = errNum;
|
||||
doc["ErrorMessage"] = errMsg;
|
||||
}
|
||||
|
||||
static void sendInt(AsyncWebServerRequest* req, int value) {
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["Value"] = value;
|
||||
fillCommon(doc, req);
|
||||
String out; serializeJson(doc, out);
|
||||
req->send(200, "application/json", out);
|
||||
}
|
||||
|
||||
static void sendBool(AsyncWebServerRequest* req, bool value) {
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["Value"] = value;
|
||||
fillCommon(doc, req);
|
||||
String out; serializeJson(doc, out);
|
||||
req->send(200, "application/json", out);
|
||||
}
|
||||
|
||||
static void sendStr(AsyncWebServerRequest* req, const char* value) {
|
||||
StaticJsonDocument<384> doc;
|
||||
doc["Value"] = value;
|
||||
fillCommon(doc, req);
|
||||
String out; serializeJson(doc, out);
|
||||
req->send(200, "application/json", out);
|
||||
}
|
||||
|
||||
static void sendOK(AsyncWebServerRequest* req) {
|
||||
StaticJsonDocument<192> doc;
|
||||
fillCommon(doc, req);
|
||||
String out; serializeJson(doc, out);
|
||||
req->send(200, "application/json", out);
|
||||
}
|
||||
|
||||
static void sendErr(AsyncWebServerRequest* req, int errNum, const char* errMsg) {
|
||||
StaticJsonDocument<256> doc;
|
||||
fillCommon(doc, req, errNum, errMsg);
|
||||
String out; serializeJson(doc, out);
|
||||
req->send(200, "application/json", out);
|
||||
}
|
||||
|
||||
static void sendIntArray(AsyncWebServerRequest* req, std::initializer_list<int> values) {
|
||||
StaticJsonDocument<256> doc;
|
||||
JsonArray arr = doc.createNestedArray("Value");
|
||||
for (int v : values) arr.add(v);
|
||||
fillCommon(doc, req);
|
||||
String out; serializeJson(doc, out);
|
||||
req->send(200, "application/json", out);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Route setup
|
||||
// ---------------------------------------------------------------------------
|
||||
static void setupRoutes() {
|
||||
|
||||
// ---- Alpaca Management API -----------------------------------------------
|
||||
|
||||
server.on("/management/apiversions", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
sendIntArray(req, {1});
|
||||
});
|
||||
|
||||
server.on("/management/v1/description", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
StaticJsonDocument<512> doc;
|
||||
JsonObject val = doc.createNestedObject("Value");
|
||||
val["ServerName"] = "ESP32 FlatPanel";
|
||||
val["Manufacturer"] = "DIY";
|
||||
val["ManufacturerVersion"] = "1.0.0";
|
||||
val["Location"] = "Telescope";
|
||||
fillCommon(doc, req);
|
||||
String out; serializeJson(doc, out);
|
||||
req->send(200, "application/json", out);
|
||||
});
|
||||
|
||||
server.on("/management/v1/configureddevices", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
StaticJsonDocument<512> doc;
|
||||
JsonArray arr = doc.createNestedArray("Value");
|
||||
JsonObject dev = arr.createNestedObject();
|
||||
dev["DeviceName"] = "ESP32 FlatPanel";
|
||||
dev["DeviceType"] = "CoverCalibrator";
|
||||
dev["DeviceNumber"] = 0;
|
||||
dev["UniqueID"] = DEVICE_UUID;
|
||||
fillCommon(doc, req);
|
||||
String out; serializeJson(doc, out);
|
||||
req->send(200, "application/json", out);
|
||||
});
|
||||
|
||||
// ---- CoverCalibrator – common device properties -------------------------
|
||||
|
||||
server.on("/api/v1/covercalibrator/0/name", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
sendStr(req, "ESP32 FlatPanel");
|
||||
});
|
||||
server.on("/api/v1/covercalibrator/0/description", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
sendStr(req, "Automated telescope flat panel with motorised cover and dew heater");
|
||||
});
|
||||
server.on("/api/v1/covercalibrator/0/driverinfo", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
sendStr(req, "ESP32 FlatPanel Alpaca driver v1.1");
|
||||
});
|
||||
server.on("/api/v1/covercalibrator/0/driverversion", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
sendStr(req, "1.1.0");
|
||||
});
|
||||
server.on("/api/v1/covercalibrator/0/interfaceversion", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
sendInt(req, 1);
|
||||
});
|
||||
|
||||
// SetDewHeater / GetDewHeater exposed as Alpaca Actions
|
||||
server.on("/api/v1/covercalibrator/0/supportedactions", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
StaticJsonDocument<256> doc;
|
||||
JsonArray arr = doc.createNestedArray("Value");
|
||||
arr.add("SetDewHeater");
|
||||
arr.add("GetDewHeater");
|
||||
fillCommon(doc, req);
|
||||
String out; serializeJson(doc, out);
|
||||
req->send(200, "application/json", out);
|
||||
});
|
||||
|
||||
server.on("/api/v1/covercalibrator/0/action", HTTP_PUT, [](AsyncWebServerRequest* req) {
|
||||
const AsyncWebParameter* aP = req->getParam("Action", true);
|
||||
const AsyncWebParameter* pP = req->getParam("Parameters", true);
|
||||
if (!aP) { sendErr(req, 0x400, "Action parameter required"); return; }
|
||||
|
||||
String action = aP->value();
|
||||
String params = pP ? pP->value() : "";
|
||||
action.toLowerCase();
|
||||
|
||||
if (action == "setdewheater") {
|
||||
applyDewHeater(constrain(params.toInt(), 0, 100));
|
||||
sendStr(req, "");
|
||||
} else if (action == "getdewheater") {
|
||||
sendStr(req, String(g_dewPercent).c_str());
|
||||
} else {
|
||||
sendErr(req, 0x40C, "Action not implemented");
|
||||
}
|
||||
});
|
||||
|
||||
// ---- CoverCalibrator – state properties (GET) ----------------------------
|
||||
|
||||
server.on("/api/v1/covercalibrator/0/connected", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
sendBool(req, g_connected);
|
||||
});
|
||||
server.on("/api/v1/covercalibrator/0/brightness", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
sendInt(req, g_brightness);
|
||||
});
|
||||
server.on("/api/v1/covercalibrator/0/maxbrightness", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
sendInt(req, MAX_BRIGHTNESS);
|
||||
});
|
||||
server.on("/api/v1/covercalibrator/0/calibratorstate", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
sendInt(req, static_cast<int>(g_calState));
|
||||
});
|
||||
server.on("/api/v1/covercalibrator/0/coverstate", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
sendInt(req, static_cast<int>(g_covState));
|
||||
});
|
||||
|
||||
// Non-standard but polled by our Python controller and INDI driver
|
||||
server.on("/api/v1/covercalibrator/0/dewheater", HTTP_GET, [](AsyncWebServerRequest* req) {
|
||||
sendInt(req, g_dewPercent);
|
||||
});
|
||||
server.on("/api/v1/covercalibrator/0/dewheater", HTTP_PUT, [](AsyncWebServerRequest* req) {
|
||||
const AsyncWebParameter* p = req->getParam("Percentage", true);
|
||||
if (!p) { sendErr(req, 0x400, "Percentage parameter required"); return; }
|
||||
applyDewHeater(constrain(p->value().toInt(), 0, 100));
|
||||
sendOK(req);
|
||||
});
|
||||
|
||||
// ---- CoverCalibrator – commands (PUT) ------------------------------------
|
||||
|
||||
server.on("/api/v1/covercalibrator/0/connected", HTTP_PUT, [](AsyncWebServerRequest* req) {
|
||||
const AsyncWebParameter* p = req->getParam("Connected", true);
|
||||
if (p) {
|
||||
String v = p->value(); v.toLowerCase();
|
||||
g_connected = (v == "true");
|
||||
}
|
||||
sendOK(req);
|
||||
});
|
||||
|
||||
server.on("/api/v1/covercalibrator/0/calibratoron", HTTP_PUT, [](AsyncWebServerRequest* req) {
|
||||
const AsyncWebParameter* p = req->getParam("Brightness", true);
|
||||
if (!p) { sendErr(req, 0x400, "Brightness parameter required"); return; }
|
||||
int b = p->value().toInt();
|
||||
if (b < 0 || b > MAX_BRIGHTNESS) { sendErr(req, 0x401, "Brightness out of range 0-255"); return; }
|
||||
applyBrightness(b);
|
||||
sendOK(req);
|
||||
});
|
||||
|
||||
server.on("/api/v1/covercalibrator/0/calibratoroff", HTTP_PUT, [](AsyncWebServerRequest* req) {
|
||||
applyBrightness(0);
|
||||
sendOK(req);
|
||||
});
|
||||
|
||||
server.on("/api/v1/covercalibrator/0/opencover", HTTP_PUT, [](AsyncWebServerRequest* req) {
|
||||
if (g_covState != CovOpen && !g_moving) startCoverMove(CovOpen);
|
||||
sendOK(req);
|
||||
});
|
||||
|
||||
server.on("/api/v1/covercalibrator/0/closecover", HTTP_PUT, [](AsyncWebServerRequest* req) {
|
||||
if (g_covState != CovClosed && !g_moving) startCoverMove(CovClosed);
|
||||
sendOK(req);
|
||||
});
|
||||
|
||||
server.on("/api/v1/covercalibrator/0/haltcover", HTTP_PUT, [](AsyncWebServerRequest* req) {
|
||||
if (g_moving) {
|
||||
coverServo.write(coverServo.read());
|
||||
g_covState = CovUnknown;
|
||||
g_moving = false;
|
||||
}
|
||||
sendOK(req);
|
||||
});
|
||||
|
||||
server.onNotFound([](AsyncWebServerRequest* req) {
|
||||
req->send(404, "text/plain", "Not found");
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Alpaca UDP discovery (port 32227)
|
||||
// Respond to "alpacadiscovery1" broadcasts with {"AlpacaPort":11111}
|
||||
// ---------------------------------------------------------------------------
|
||||
static void setupDiscovery() {
|
||||
if (udp.listen(32227)) {
|
||||
udp.onPacket([](AsyncUDPPacket pkt) {
|
||||
if (pkt.length() == 16 &&
|
||||
memcmp(pkt.data(), "alpacadiscovery1", 16) == 0) {
|
||||
String resp = "{\"AlpacaPort\":" + String(ALPACA_PORT) + "}";
|
||||
pkt.print(resp);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Arduino entry points
|
||||
// ---------------------------------------------------------------------------
|
||||
void setup() {
|
||||
Serial.begin(9600); // 9600 matches indi_flipflat / N.I.N.A. default for Alnitak
|
||||
DBGLN("\nESP32 FlatPanel booting…");
|
||||
|
||||
// Servo – start in closed position
|
||||
coverServo.attach(SERVO_PIN);
|
||||
coverServo.write(SERVO_CLOSED);
|
||||
delay(500);
|
||||
|
||||
// LED PWM (LEDC ch 0)
|
||||
ledcSetup(LED_PWM_CHANNEL, LED_PWM_FREQ, LED_PWM_RESOLUTION);
|
||||
ledcAttachPin(LED_PWM_PIN, LED_PWM_CHANNEL);
|
||||
ledcWrite(LED_PWM_CHANNEL, 0);
|
||||
|
||||
// Dew heater PWM (LEDC ch 1, 12 V via MOSFET – off at boot)
|
||||
ledcSetup(DEW_PWM_CHANNEL, DEW_PWM_FREQ, DEW_PWM_RESOLUTION);
|
||||
ledcAttachPin(DEW_HEATER_PIN, DEW_PWM_CHANNEL);
|
||||
ledcWrite(DEW_PWM_CHANNEL, 0);
|
||||
|
||||
// WiFi
|
||||
WiFi.setHostname(HOSTNAME);
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
DBGF("Connecting to WiFi");
|
||||
unsigned long t0 = millis();
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
if (millis() - t0 > 30000) { DBGLN("\nWiFi timeout – restarting"); ESP.restart(); }
|
||||
delay(500);
|
||||
DBG('.');
|
||||
}
|
||||
DBGF("\nIP: %s\n", WiFi.localIP().toString().c_str());
|
||||
|
||||
// mDNS (_alpaca._tcp)
|
||||
if (MDNS.begin(HOSTNAME)) {
|
||||
MDNS.addService("alpaca", "tcp", ALPACA_PORT);
|
||||
MDNS.addServiceTxt("alpaca", "tcp", "txtvers", "1");
|
||||
DBGF("mDNS: http://%s.local:%d\n", HOSTNAME, ALPACA_PORT);
|
||||
}
|
||||
|
||||
setupRoutes();
|
||||
setupDiscovery();
|
||||
server.begin();
|
||||
DBGLN("Alpaca server started. USB/Alnitak also active on Serial.");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// USB Alnitak protocol – always active regardless of SERIAL_DEBUG
|
||||
handleAlnitak();
|
||||
|
||||
// Cover movement state machine
|
||||
if (g_moving && (millis() - g_moveStartMs >= COVER_MOVE_TIMEOUT_MS)) {
|
||||
g_covState = g_moveTarget;
|
||||
g_moving = false;
|
||||
DBGF("Cover reached: %s\n", g_covState == CovOpen ? "OPEN" : "CLOSED");
|
||||
}
|
||||
|
||||
// WiFi watchdog
|
||||
static unsigned long lastWifiCheck = 0;
|
||||
if (millis() - lastWifiCheck > 10000) {
|
||||
lastWifiCheck = millis();
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
DBGLN("WiFi lost – reconnecting");
|
||||
WiFi.reconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue