Instrument Neutral Distributed Interface INDI 2.2.5
planewave_mount.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2023-2027 Jasem Mutlaq. All rights reserved.
3
4 Planewave Mount
5 API Communication via PWI4 HTTP Interface
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Library General Public
9 License version 2 as published by the Free Software Foundation.
10 .
11 This library is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 Library General Public License for more details.
15 .
16 You should have received a copy of the GNU Library General Public License
17 along with this library; see the file COPYING.LIB. If not, write to
18 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
20*******************************************************************************/
21
22#include "planewave_mount.h"
23
24#include "indicom.h"
26#include <libnova/sidereal_time.h>
27#include <libnova/transform.h>
28
29#include <cmath>
30#include <cstring>
31#include <memory>
32#include <sstream>
33#include <iomanip>
34
35std::unique_ptr<PlaneWave> PlaneWave_mount(new PlaneWave());
36
57
58// Must be defined here in the .cpp so that unique_ptr<httplib::Client>
59// is destroyed after the complete type is known.
60PlaneWave::~PlaneWave() = default;
61
66{
67 return "PlaneWave";
68}
69
74{
76
77 // ── Track Modes ─────────────────────────────────────────────────────────
78 AddTrackMode("TRACK_SIDEREAL", "Sidereal", true);
79 AddTrackMode("TRACK_SOLAR", "Solar");
80 AddTrackMode("TRACK_LUNAR", "Lunar");
81 AddTrackMode("TRACK_CUSTOM", "Custom");
82
85
86 tcpConnection->setDefaultHost("192.168.1.1");
88
89 // ── Info tab ─────────────────────────────────────────────────────────────
90
91 FirmwareTP[0].fill("VERSION", "PWI4 Version", "");
92 FirmwareTP.fill(getDeviceName(), "FIRMWARE_INFO", "Firmware",
93 INFO_TAB, IP_RO, 60, IPS_IDLE);
94
95 MountGeometryTP[0].fill("GEOMETRY", "Mount Type", "");
96 MountGeometryTP.fill(getDeviceName(), "MOUNT_GEOMETRY", "Geometry",
97 INFO_TAB, IP_RO, 60, IPS_IDLE);
98
99 // Az / Alt — derived from status on every poll
100 AltAzNP[AZ_VALUE].fill("AZ", "Azimuth (°)", "%.4f", 0.0, 360.0, 0.0, 0.0);
101 AltAzNP[ALT_VALUE].fill("ALT", "Altitude (°)", "%.4f", -90.0, 90.0, 0.0, 0.0);
102 AltAzNP.fill(getDeviceName(), "MOUNT_ALTAZ", "Az/Alt",
104
105 // Axis 0 (AZ / RA) servo telemetry
106 Axis0TelemetryNP[TELEMETRY_RMS_ERROR].fill(
107 "AXIS0_RMS", "RMS Error (\")", "%.3f", 0, 1000, 0, 0);
108 Axis0TelemetryNP[TELEMETRY_DIST_TO_TARGET].fill(
109 "AXIS0_DIST", "Dist to Target (\")", "%.3f", -1e6, 1e6, 0, 0);
110 Axis0TelemetryNP[TELEMETRY_SERVO_ERROR].fill(
111 "AXIS0_SERVO", "Servo Error (\")", "%.3f", -1e6, 1e6, 0, 0);
112 Axis0TelemetryNP[TELEMETRY_MOTOR_CURRENT].fill(
113 "AXIS0_CURRENT", "Motor Current (A)", "%.3f", 0, 100, 0, 0);
114 Axis0TelemetryNP.fill(getDeviceName(), "AXIS0_TELEMETRY", "Axis 0 (AZ/RA) Telemetry",
115 INFO_TAB, IP_RO, 0, IPS_IDLE);
116
117 // Axis 1 (ALT / Dec) servo telemetry
118 Axis1TelemetryNP[TELEMETRY_RMS_ERROR].fill(
119 "AXIS1_RMS", "RMS Error (\")", "%.3f", 0, 1000, 0, 0);
120 Axis1TelemetryNP[TELEMETRY_DIST_TO_TARGET].fill(
121 "AXIS1_DIST", "Dist to Target (\")", "%.3f", -1e6, 1e6, 0, 0);
122 Axis1TelemetryNP[TELEMETRY_SERVO_ERROR].fill(
123 "AXIS1_SERVO", "Servo Error (\")", "%.3f", -1e6, 1e6, 0, 0);
124 Axis1TelemetryNP[TELEMETRY_MOTOR_CURRENT].fill(
125 "AXIS1_CURRENT", "Motor Current (A)", "%.3f", 0, 100, 0, 0);
126 Axis1TelemetryNP.fill(getDeviceName(), "AXIS1_TELEMETRY", "Axis 1 (ALT/Dec) Telemetry",
127 INFO_TAB, IP_RO, 0, IPS_IDLE);
128
129 // Angular separation from current pointing to the Sun (safety indicator)
130 DistanceToSunNP[0].fill("DIST_TO_SUN", "Distance (°)", "%.2f", 0, 180, 0, 0);
131 DistanceToSunNP.fill(getDeviceName(), "DISTANCE_TO_SUN", "Sun Distance",
133
134 // ── Main Control tab ─────────────────────────────────────────────────────
135
136 // Motor enable / disable — axis 0
137 Axis0MotorSP[INDI_ENABLED].fill("AXIS0_ENABLE", "Enable", ISS_OFF);
138 Axis0MotorSP[INDI_DISABLED].fill("AXIS0_DISABLE", "Disable", ISS_OFF);
139 Axis0MotorSP.fill(getDeviceName(), "AXIS0_MOTOR", "Axis 0 (AZ/RA) Motor",
141
142 // Motor enable / disable — axis 1
143 Axis1MotorSP[INDI_ENABLED].fill("AXIS1_ENABLE", "Enable", ISS_OFF);
144 Axis1MotorSP[INDI_DISABLED].fill("AXIS1_DISABLE", "Disable", ISS_OFF);
145 Axis1MotorSP.fill(getDeviceName(), "AXIS1_MOTOR", "Axis 1 (ALT/Dec) Motor",
147
148 // Find Home — relevant for L-series mounts with incremental encoders
149 FindHomeSP[0].fill("FIND_HOME", "Find Home", ISS_OFF);
150 FindHomeSP.fill(getDeviceName(), "MOUNT_HOME", "Homing",
152
153 // ── Pointing Model tab ────────────────────────────────────────────────────
154
155 // Active model filename (read-only, pulled from status)
156 ModelFilenameTP[0].fill("MODEL_FILE", "Active Model", "");
157 ModelFilenameTP.fill(getDeviceName(), "MODEL_FILENAME", "Model File",
158 MODEL_TAB, IP_RO, 60, IPS_IDLE);
159
160 // Model quality statistics (read-only, pulled from status)
161 ModelInfoNP[MODEL_POINTS_TOTAL].fill("MODEL_TOTAL", "Total Points", "%.0f", 0, 10000, 1, 0);
162 ModelInfoNP[MODEL_POINTS_ENABLED].fill("MODEL_ENABLED", "Enabled Points", "%.0f", 0, 10000, 1, 0);
163 ModelInfoNP[MODEL_RMS_ERROR].fill("MODEL_RMS", "RMS Error (\")", "%.3f", 0, 10000, 0, 0);
164 ModelInfoNP.fill(getDeviceName(), "MODEL_INFO", "Model Statistics",
165 MODEL_TAB, IP_RO, 0, IPS_IDLE);
166
167 // One-click model operations
168 ModelOperationSP[MODEL_SAVE_AS_DEFAULT].fill("MODEL_SAVE_DEFAULT", "Save as Default", ISS_OFF);
169 ModelOperationSP[MODEL_CLEAR_POINTS].fill("MODEL_CLEAR", "Clear All Points", ISS_OFF);
170 ModelOperationSP.fill(getDeviceName(), "MODEL_OPERATION", "Model Operations",
171 MODEL_TAB, IP_RW, ISR_ATMOST1, 0, IPS_IDLE);
172
173 // Load model: user types filename → Apply → driver sends /mount/model/load
174 ModelLoadFilenameTP[0].fill("MODEL_LOAD_FILE", "Filename", "DefaultModel.pxp");
175 ModelLoadFilenameTP.fill(getDeviceName(), "MODEL_LOAD", "Load Model",
176 MODEL_TAB, IP_RW, 60, IPS_IDLE);
177
178 // Save model: user types filename → Apply → driver sends /mount/model/save
179 ModelSaveFilenameTP[0].fill("MODEL_SAVE_FILE", "Filename", "MyModel.pxp");
180 ModelSaveFilenameTP.fill(getDeviceName(), "MODEL_SAVE", "Save Model",
181 MODEL_TAB, IP_RW, 60, IPS_IDLE);
182
183 // ── Options tab ───────────────────────────────────────────────────────────
184
185 SlewTimeConstantNP[0].fill("SLEW_TIME_CONSTANT", "Time Constant (s)",
186 "%.2f", 0.1, 5.0, 0.1, 0.5);
187 SlewTimeConstantNP.fill(getDeviceName(), "MOUNT_SLEW_CONSTANT", "Slew Aggressiveness",
189
191 return true;
192}
193
198{
200
201 if (isConnected())
202 {
203 // Info tab
204 defineProperty(FirmwareTP);
205 defineProperty(MountGeometryTP);
206 defineProperty(AltAzNP);
207 defineProperty(Axis0TelemetryNP);
208 defineProperty(Axis1TelemetryNP);
209 defineProperty(DistanceToSunNP);
210
211 // Main Control tab
212 defineProperty(Axis0MotorSP);
213 defineProperty(Axis1MotorSP);
214 defineProperty(FindHomeSP);
215
216 // Pointing Model tab
217 defineProperty(ModelFilenameTP);
218 defineProperty(ModelInfoNP);
219 defineProperty(ModelOperationSP);
220 defineProperty(ModelLoadFilenameTP);
221 defineProperty(ModelSaveFilenameTP);
222
223 // Options tab
224 defineProperty(SlewTimeConstantNP);
225
226 // Parking data
227 if (InitPark())
228 {
231 }
232 else
233 {
234 SetAxis1Park(0);
235 SetAxis2Park(0);
238 }
239 }
240 else
241 {
242 deleteProperty(FirmwareTP);
243 deleteProperty(MountGeometryTP);
244 deleteProperty(AltAzNP);
245 deleteProperty(Axis0TelemetryNP);
246 deleteProperty(Axis1TelemetryNP);
247 deleteProperty(DistanceToSunNP);
248
249 deleteProperty(Axis0MotorSP);
250 deleteProperty(Axis1MotorSP);
251 deleteProperty(FindHomeSP);
252
253 deleteProperty(ModelFilenameTP);
254 deleteProperty(ModelInfoNP);
255 deleteProperty(ModelOperationSP);
256 deleteProperty(ModelLoadFilenameTP);
257 deleteProperty(ModelSaveFilenameTP);
258
259 deleteProperty(SlewTimeConstantNP);
260 }
261
262 return true;
263}
264
269{
270 // Build (or rebuild on reconnect) the persistent client
271 m_HttpClient = std::make_unique<httplib::Client>(tcpConnection->host(),
273 m_HttpClient->set_connection_timeout(5, 0);
274 m_HttpClient->set_read_timeout(10, 0);
275
276 // Fetch initial status
277 if (!getStatus())
278 {
279 LOG_ERROR("Cannot communicate with PWI4. Is PWI4 running and reachable?");
280 return false;
281 }
282
283 // If PWI4 is not connected to the mount hardware, try to connect it
284 bool mountConnected = m_Status["mount.is_connected"].as<bool>();
285 if (!mountConnected)
286 {
287 LOG_INFO("Mount not connected to PWI4. Sending /mount/connect …");
288 if (!dispatch("/mount/connect"))
289 {
290 LOG_ERROR("Failed to connect mount via PWI4.");
291 return false;
292 }
293 mountConnected = m_Status["mount.is_connected"].as<bool>();
294 if (!mountConnected)
295 {
296 LOG_ERROR("Mount still not connected after /mount/connect.");
297 return false;
298 }
299 }
300
301 // ── Populate read-only info properties from the first status fetch ────────
302
303 // PWI4 version
304 std::string version = m_Status["pwi4.version"].as<std::string>();
305 FirmwareTP[0].setText(version.c_str());
306 FirmwareTP.setState(IPS_OK);
307
308 // Mount geometry
309 int geometry = m_Status["mount.geometry"].as<int>();
310 const char *geomStr = "Unknown";
311 switch (geometry)
312 {
313 case 0:
314 geomStr = "Alt-Az";
315 break;
316 case 1:
317 geomStr = "Equatorial Fork";
318 break;
319 case 2:
320 geomStr = "German Equatorial";
321 break;
322 }
323 MountGeometryTP[0].setText(geomStr);
324 MountGeometryTP.setState(IPS_OK);
325
326 // Slew time constant
327 SlewTimeConstantNP[0].setValue(m_Status["mount.slew_time_constant"].as<double>());
328 SlewTimeConstantNP.setState(IPS_OK);
329
330 LOGF_INFO("Connected to PWI4 %s — mount geometry: %s", version.c_str(), geomStr);
331 return true;
332}
333
337bool PlaneWave::getStatus()
338{
339 return dispatch("/status");
340}
341
346{
347 if (!getStatus())
348 return false;
349
350 // ── Coordinates ────────────────────────────────────────────────────────
351 double ra = m_Status["mount.ra_apparent_hours"].as<double>();
352 double de = m_Status["mount.dec_apparent_degs"].as<double>();
353
354 // ── Flags ──────────────────────────────────────────────────────────────
355 bool isSlewing = m_Status["mount.is_slewing"].as<bool>();
356 bool isTracking = m_Status["mount.is_tracking"].as<bool>();
357
358 // ── State machine ──────────────────────────────────────────────────────
359 switch (TrackState)
360 {
361 case SCOPE_PARKING:
362 // Park is complete when the mount has stopped slewing AND is no
363 // longer tracking (it will be in the "stopped" state).
364 if (!isSlewing && !isTracking)
365 SetParked(true);
366 break;
367
368 case SCOPE_SLEWING:
369 // Slewing is complete once mount.is_slewing drops to false;
370 // it then either begins tracking or goes idle.
371 if (!isSlewing)
372 {
373 if (isTracking)
374 {
376 SetTrackEnabled(true);
377 }
378 else
379 {
381 }
382 }
383 break;
384
385 default:
386 // If the mount starts tracking externally (e.g. via PWI4 GUI),
387 // reflect that in the INDI state.
388 if (isTracking && TrackState == SCOPE_IDLE)
390 break;
391 }
392
393 NewRaDec(ra, de);
394
395 // ── Az / Alt ───────────────────────────────────────────────────────────
396 double az = m_Status["mount.azimuth_degs"].as<double>();
397 double alt = m_Status["mount.altitude_degs"].as<double>();
398 AltAzNP[AZ_VALUE].setValue(az);
399 AltAzNP[ALT_VALUE].setValue(alt);
400 AltAzNP.setState(IPS_OK);
401 AltAzNP.apply();
402
403 // ── Pier side (GEM only) ───────────────────────────────────────────────
404 // On a German Equatorial, the HA decides which side the scope is on.
405 // PWI4 does not expose pier side directly; we infer it from axis0 position.
406 int geometry = m_Status["mount.geometry"].as<int>();
407 if (geometry == 2) // GEM
408 {
409 double axis0Pos = m_Status["mount.axis0.position_degs"].as<double>();
410 // Conventional: axis0 > 180° → pointing West → scope on East (pier west)
411 setPierSide((axis0Pos > 180.0) ? PIER_WEST : PIER_EAST);
412 }
413
414 // ── Axis 0 telemetry ───────────────────────────────────────────────────
415 Axis0TelemetryNP[TELEMETRY_RMS_ERROR].setValue(
416 m_Status["mount.axis0.rms_error_arcsec"].as<double>());
417 Axis0TelemetryNP[TELEMETRY_DIST_TO_TARGET].setValue(
418 m_Status["mount.axis0.dist_to_target_arcsec"].as<double>());
419 Axis0TelemetryNP[TELEMETRY_SERVO_ERROR].setValue(
420 m_Status["mount.axis0.servo_error_arcsec"].as<double>());
421 Axis0TelemetryNP[TELEMETRY_MOTOR_CURRENT].setValue(
422 m_Status["mount.axis0.measured_current_amps"].as<double>());
423 Axis0TelemetryNP.setState(IPS_OK);
424 Axis0TelemetryNP.apply();
425
426 // ── Axis 1 telemetry ───────────────────────────────────────────────────
427 Axis1TelemetryNP[TELEMETRY_RMS_ERROR].setValue(
428 m_Status["mount.axis1.rms_error_arcsec"].as<double>());
429 Axis1TelemetryNP[TELEMETRY_DIST_TO_TARGET].setValue(
430 m_Status["mount.axis1.dist_to_target_arcsec"].as<double>());
431 Axis1TelemetryNP[TELEMETRY_SERVO_ERROR].setValue(
432 m_Status["mount.axis1.servo_error_arcsec"].as<double>());
433 Axis1TelemetryNP[TELEMETRY_MOTOR_CURRENT].setValue(
434 m_Status["mount.axis1.measured_current_amps"].as<double>());
435 Axis1TelemetryNP.setState(IPS_OK);
436 Axis1TelemetryNP.apply();
437
438 // ── Motor enabled state ────────────────────────────────────────────────
439 bool axis0Enabled = m_Status["mount.axis0.is_enabled"].as<bool>();
440 bool axis1Enabled = m_Status["mount.axis1.is_enabled"].as<bool>();
441
442 Axis0MotorSP[INDI_ENABLED].setState(axis0Enabled ? ISS_ON : ISS_OFF);
443 Axis0MotorSP[INDI_DISABLED].setState(axis0Enabled ? ISS_OFF : ISS_ON);
444 Axis0MotorSP.setState(IPS_OK);
445 Axis0MotorSP.apply();
446
447 Axis1MotorSP[INDI_ENABLED].setState(axis1Enabled ? ISS_ON : ISS_OFF);
448 Axis1MotorSP[INDI_DISABLED].setState(axis1Enabled ? ISS_OFF : ISS_ON);
449 Axis1MotorSP.setState(IPS_OK);
450 Axis1MotorSP.apply();
451
452 // ── Distance to Sun ────────────────────────────────────────────────────
453 double sunDist = m_Status["mount.distance_to_sun_degs"].as<double>();
454 DistanceToSunNP[0].setValue(sunDist);
455 // Warn when pointing within 30° of the Sun
456 DistanceToSunNP.setState(sunDist < 30.0 ? IPS_ALERT : IPS_OK);
457 DistanceToSunNP.apply();
458
459 // ── Pointing model info ────────────────────────────────────────────────
460 std::string modelFile = m_Status["mount.model.filename"].as<std::string>();
461 if (modelFile != std::string(ModelFilenameTP[0].getText()))
462 {
463 ModelFilenameTP[0].setText(modelFile.c_str());
464 ModelFilenameTP.setState(modelFile.empty() ? IPS_IDLE : IPS_OK);
465 ModelFilenameTP.apply();
466 }
467
468 ModelInfoNP[MODEL_POINTS_TOTAL].setValue(
469 m_Status["mount.model.num_points_total"].as<double>());
470 ModelInfoNP[MODEL_POINTS_ENABLED].setValue(
471 m_Status["mount.model.num_points_enabled"].as<double>());
472 ModelInfoNP[MODEL_RMS_ERROR].setValue(
473 m_Status["mount.model.rms_error_arcsec"].as<double>());
474 ModelInfoNP.setState(IPS_OK);
475 ModelInfoNP.apply();
476
477 // ── Site location readback ─────────────────────────────────────────────
478 // PWI4 is the authority on site location; push its values into INDI so
479 // they are always consistent with what the pointing model uses.
480 double pwi4Lat = m_Status["site.latitude_degs"].as<double>();
481 double pwi4Lon = m_Status["site.longitude_degs"].as<double>();
482 double pwi4Elv = m_Status["site.height_meters"].as<double>();
483 if (std::fabs(pwi4Lat - LocationNP[LOCATION_LATITUDE].getValue()) > 0.0001 ||
484 std::fabs(pwi4Lon - LocationNP[LOCATION_LONGITUDE].getValue()) > 0.0001 ||
485 std::fabs(pwi4Elv - LocationNP[LOCATION_ELEVATION].getValue()) > 1.0)
486 {
487 LocationNP[LOCATION_LATITUDE].setValue(pwi4Lat);
488 LocationNP[LOCATION_LONGITUDE].setValue(pwi4Lon);
489 LocationNP[LOCATION_ELEVATION].setValue(pwi4Elv);
491 }
492
493 return true;
494}
495
499bool PlaneWave::Goto(double ra, double dec)
500{
501 std::ostringstream req;
502 req << std::fixed << std::setprecision(9)
503 << "/mount/goto_ra_dec_j2000?ra_hours=" << ra
504 << "&dec_degs=" << dec;
505
506 if (!dispatch(req.str()))
507 return false;
508
510 return true;
511}
512
516bool PlaneWave::Sync(double ra, double dec)
517{
518 // PWI4 does not have a direct "sync" command. The closest equivalent is
519 // /mount/model/add_point, which maps the current encoder position to the
520 // provided J2000 coordinates and immediately updates the pointing model.
521 std::ostringstream req;
522 req << std::fixed << std::setprecision(9)
523 << "/mount/model/add_point?ra_j2000_hours=" << ra
524 << "&dec_j2000_degs=" << dec;
525
526 if (!dispatch(req.str()))
527 return false;
528
529 LOG_INFO("Sync: added a pointing-model calibration point at the current position.");
530 return true;
531}
532
537{
538 if (!dispatch("/mount/park"))
539 return false;
540
542 return true;
543}
544
549{
550 // Enable axis 0 (AZ / RA)
551 if (!enableAxis(0, true))
552 {
553 LOG_ERROR("Failed to enable axis 0 motor.");
554 return false;
555 }
556 // Enable axis 1 (ALT / Dec)
557 if (!enableAxis(1, true))
558 {
559 LOG_ERROR("Failed to enable axis 1 motor.");
560 return false;
561 }
562
563 SetParked(false);
564 return true;
565}
566
571{
572 if (!dispatch("/mount/set_park_here"))
573 return false;
574
575 LOG_INFO("Park position set to current position.");
576 return true;
577}
578
583{
584 SetAxis1Park(0);
585 SetAxis2Park(0);
586 return true;
587}
588
593{
594 return dispatch("/mount/stop");
595}
596
601{
603 {
604 LOG_ERROR("Please unpark the mount before issuing any motion commands.");
605 return false;
606 }
607
608 double rate = getSlewRateArcsecPerSec();
609 if (dir == DIRECTION_SOUTH)
610 rate = -rate; // South → negative Dec offset
611
612 std::ostringstream req;
613 req << std::fixed << std::setprecision(4);
614
615 if (command == MOTION_START)
616 req << "/mount/offset?dec_set_rate_arcsec_per_sec=" << rate;
617 else
618 req << "/mount/offset?dec_set_rate_arcsec_per_sec=0";
619
620 return dispatch(req.str());
621}
622
627{
629 {
630 LOG_ERROR("Please unpark the mount before issuing any motion commands.");
631 return false;
632 }
633
634 double rate = getSlewRateArcsecPerSec();
635 if (dir == DIRECTION_EAST)
636 rate = -rate; // East → negative RA offset (RA increases westward)
637
638 std::ostringstream req;
639 req << std::fixed << std::setprecision(4);
640
641 if (command == MOTION_START)
642 req << "/mount/offset?ra_set_rate_arcsec_per_sec=" << rate;
643 else
644 req << "/mount/offset?ra_set_rate_arcsec_per_sec=0";
645
646 return dispatch(req.str());
647}
648
653bool PlaneWave::updateLocation(double latitude, double longitude, double elevation)
654{
655 INDI_UNUSED(latitude);
656 INDI_UNUSED(longitude);
657 INDI_UNUSED(elevation);
658
659 // PWI4 manages its own site configuration. The driver keeps INDI's
660 // location in sync by reading back site.* values in ReadScopeStatus().
661 LOG_INFO("Note: site location is managed by PWI4. "
662 "INDI displays the location configured in PWI4.");
663 return true;
664}
665
669bool PlaneWave::updateTime(ln_date *utc, double utc_offset)
670{
671 INDI_UNUSED(utc);
672 INDI_UNUSED(utc_offset);
673 return true;
674}
675
679bool PlaneWave::SetTrackMode(uint8_t mode)
680{
681 // First ensure tracking is on
682 if (!dispatch("/mount/tracking_on"))
683 return false;
684
685 // For custom mode, apply the offsets that simulate non-sidereal rates.
686 // The rates stored in TrackRateNP are in arcsec/sec relative to sidereal.
687 if (mode == TRACK_CUSTOM)
688 {
689 double dRA = TrackRateNP[AXIS_RA].getValue();
690 double dDE = TrackRateNP[AXIS_DE].getValue();
691
692 std::ostringstream req;
693 req << std::fixed << std::setprecision(6)
694 << "/mount/offset?ra_set_rate_arcsec_per_sec=" << dRA
695 << "&dec_set_rate_arcsec_per_sec=" << dDE;
696
697 return dispatch(req.str());
698 }
699
700 // For Sidereal / Solar / Lunar the offset rate is zero; standard tracking
701 // on already handles those. Reset any previous custom offsets.
702 return dispatch("/mount/offset?ra_reset=0&dec_reset=0");
703}
704
708bool PlaneWave::SetTrackRate(double raRate, double deRate)
709{
710 std::ostringstream req;
711 req << std::fixed << std::setprecision(6)
712 << "/mount/offset?ra_set_rate_arcsec_per_sec=" << raRate
713 << "&dec_set_rate_arcsec_per_sec=" << deRate;
714
715 return dispatch(req.str());
716}
717
722{
723 if (enabled)
725
726 // Disable tracking and clear any active offset rates
727 if (!dispatch("/mount/tracking_off"))
728 return false;
729
730 dispatch("/mount/offset?ra_reset=0&dec_reset=0");
731 return true;
732}
733
738{
740 SlewTimeConstantNP.save(fp);
741 return true;
742}
743
747bool PlaneWave::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
748{
749 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
750 {
751 // ── Axis 0 motor control ──────────────────────────────────────────
752 if (Axis0MotorSP.isNameMatch(name))
753 {
754 Axis0MotorSP.update(states, names, n);
755 bool enable = (Axis0MotorSP[INDI_ENABLED].getState() == ISS_ON);
756 if (enableAxis(0, enable))
757 {
758 LOGF_INFO("Axis 0 motor %s.", enable ? "enabled" : "disabled");
759 Axis0MotorSP.setState(IPS_OK);
760 }
761 else
762 {
763 LOG_ERROR("Failed to change axis 0 motor state.");
764 Axis0MotorSP.setState(IPS_ALERT);
765 }
766 Axis0MotorSP.apply();
767 return true;
768 }
769
770 // ── Axis 1 motor control ──────────────────────────────────────────
771 if (Axis1MotorSP.isNameMatch(name))
772 {
773 Axis1MotorSP.update(states, names, n);
774 bool enable = (Axis1MotorSP[INDI_ENABLED].getState() == ISS_ON);
775 if (enableAxis(1, enable))
776 {
777 LOGF_INFO("Axis 1 motor %s.", enable ? "enabled" : "disabled");
778 Axis1MotorSP.setState(IPS_OK);
779 }
780 else
781 {
782 LOG_ERROR("Failed to change axis 1 motor state.");
783 Axis1MotorSP.setState(IPS_ALERT);
784 }
785 Axis1MotorSP.apply();
786 return true;
787 }
788
789 // ── Find Home ──────────────────────────────────────────────────────
790 if (FindHomeSP.isNameMatch(name))
791 {
792 if (dispatch("/mount/find_home"))
793 {
794 LOG_INFO("Home finding sequence started.");
795 FindHomeSP.setState(IPS_BUSY);
796 }
797 else
798 {
799 LOG_ERROR("Failed to start home finding.");
800 FindHomeSP.setState(IPS_ALERT);
801 }
802 // Reset the button to Off (momentary action)
803 FindHomeSP[0].setState(ISS_OFF);
804 FindHomeSP.apply();
805 return true;
806 }
807
808 // ── Pointing model operations ──────────────────────────────────────
809 if (ModelOperationSP.isNameMatch(name))
810 {
811 ModelOperationSP.update(states, names, n);
812 bool ok = false;
813
814 if (ModelOperationSP[MODEL_SAVE_AS_DEFAULT].getState() == ISS_ON)
815 {
816 ok = dispatch("/mount/model/save_as_default");
817 if (ok)
818 LOG_INFO("Pointing model saved as default.");
819 else
820 LOG_ERROR("Failed to save pointing model as default.");
821 }
822 else if (ModelOperationSP[MODEL_CLEAR_POINTS].getState() == ISS_ON)
823 {
824 ok = dispatch("/mount/model/clear_points");
825 if (ok)
826 LOG_INFO("All pointing model calibration points cleared.");
827 else
828 LOG_ERROR("Failed to clear pointing model points.");
829 }
830
831 // Reset buttons (momentary)
832 ModelOperationSP[MODEL_SAVE_AS_DEFAULT].setState(ISS_OFF);
833 ModelOperationSP[MODEL_CLEAR_POINTS].setState(ISS_OFF);
834 ModelOperationSP.setState(ok ? IPS_OK : IPS_ALERT);
835 ModelOperationSP.apply();
836 return true;
837 }
838 }
839
840 return INDI::Telescope::ISNewSwitch(dev, name, states, names, n);
841}
842
846bool PlaneWave::ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n)
847{
848 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
849 {
850 // ── Load model by filename ─────────────────────────────────────────
851 if (ModelLoadFilenameTP.isNameMatch(name))
852 {
853 ModelLoadFilenameTP.update(texts, names, n);
854 const char *filename = ModelLoadFilenameTP[0].getText();
855 std::string req = "/mount/model/load?filename=";
856 req += filename;
857
858 if (dispatch(req))
859 {
860 LOGF_INFO("Pointing model '%s' loaded.", filename);
861 ModelLoadFilenameTP.setState(IPS_OK);
862 }
863 else
864 {
865 LOGF_ERROR("Failed to load pointing model '%s'.", filename);
866 ModelLoadFilenameTP.setState(IPS_ALERT);
867 }
868 ModelLoadFilenameTP.apply();
869 return true;
870 }
871
872 // ── Save model with filename ───────────────────────────────────────
873 if (ModelSaveFilenameTP.isNameMatch(name))
874 {
875 ModelSaveFilenameTP.update(texts, names, n);
876 const char *filename = ModelSaveFilenameTP[0].getText();
877 std::string req = "/mount/model/save?filename=";
878 req += filename;
879
880 if (dispatch(req))
881 {
882 LOGF_INFO("Pointing model saved as '%s'.", filename);
883 ModelSaveFilenameTP.setState(IPS_OK);
884 }
885 else
886 {
887 LOGF_ERROR("Failed to save pointing model as '%s'.", filename);
888 ModelSaveFilenameTP.setState(IPS_ALERT);
889 }
890 ModelSaveFilenameTP.apply();
891 return true;
892 }
893 }
894
895 return INDI::Telescope::ISNewText(dev, name, texts, names, n);
896}
897
901bool PlaneWave::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
902{
903 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
904 {
905 // ── Slew time constant ─────────────────────────────────────────────
906 if (SlewTimeConstantNP.isNameMatch(name))
907 {
908 SlewTimeConstantNP.update(values, names, n);
909 double tc = SlewTimeConstantNP[0].getValue();
910
911 std::ostringstream req;
912 req << std::fixed << std::setprecision(3)
913 << "/mount/set_slew_time_constant?value=" << tc;
914
915 if (dispatch(req.str()))
916 {
917 LOGF_INFO("Slew time constant set to %.2f s.", tc);
918 SlewTimeConstantNP.setState(IPS_OK);
919 }
920 else
921 {
922 LOG_ERROR("Failed to set slew time constant.");
923 SlewTimeConstantNP.setState(IPS_ALERT);
924 }
925 SlewTimeConstantNP.apply();
926 return true;
927 }
928 }
929
930 return INDI::Telescope::ISNewNumber(dev, name, values, names, n);
931}
932
936bool PlaneWave::enableAxis(int axis, bool enable)
937{
938 std::ostringstream req;
939 req << (enable ? "/mount/enable?axis=" : "/mount/disable?axis=") << axis;
940 return dispatch(req.str());
941}
942
946double PlaneWave::getSlewRateArcsecPerSec() const
947{
948 int idx = SlewRateSP.findOnSwitchIndex();
949 if (idx >= 0 && idx < 4)
950 return SLEW_RATES[idx];
951 return SLEW_RATES[3]; // default to fast if unknown
952}
953
961bool PlaneWave::dispatch(const std::string &request)
962{
963 if (!m_HttpClient)
964 {
965 LOG_ERROR("HTTP client not initialized. Is the mount connected?");
966 return false;
967 }
968
969 auto res = m_HttpClient->Get(request);
970
971 if (!res)
972 {
973 LOGF_ERROR("HTTP GET %s failed: %s",
974 request.c_str(),
975 httplib::to_string(res.error()).c_str());
976 return false;
977 }
978
979 if (res->status != 200)
980 {
981 LOGF_ERROR("HTTP GET %s returned HTTP %d: %s",
982 request.c_str(), res->status,
983 res->body.empty() ? "(no body)" : res->body.c_str());
984 return false;
985 }
986
987 if (res->body.empty())
988 {
989 LOGF_ERROR("HTTP GET %s returned an empty body.", request.c_str());
990 return false;
991 }
992
993 try
994 {
995 // PWI4 returns a flat key=value format. Wrap it in a dummy [status]
996 // section so that inicpp can parse it as a standard INI file.
997 ini::IniFile inif;
998 inif.decode("[status]\n" + res->body);
999
1000 if (inif["status"].size() == 0)
1001 {
1002 LOGF_ERROR("Parsed status for %s is empty.", request.c_str());
1003 return false;
1004 }
1005
1006 m_Status = inif["status"];
1007 return true;
1008 }
1009 catch (const std::exception &e)
1010 {
1011 LOGF_ERROR("Failed to parse PWI4 response for %s: %s",
1012 request.c_str(), e.what());
1013 return false;
1014 }
1015}
void setDefaultHost(const char *addressHost)
void setDefaultPort(uint32_t addressPort)
virtual const char * host() const
virtual uint32_t port() const
bool isConnected() const
INDI::PropertyText getText(const char *name) const
void setVersion(uint16_t vMajor, uint16_t vMinor)
Set driver version information to be defined in DRIVER_INFO property as vMajor.vMinor.
virtual bool deleteProperty(const char *propertyName)
Delete a property and unregister it. It will also be deleted from all clients.
void defineProperty(INumberVectorProperty *property)
void addAuxControls()
Add Debug, Simulation, and Configuration options to the driver.
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
void save(FILE *f) const
save Save property to configuration file.
bool isNameMatch(const char *otherName) const
bool update(const double values[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
bool update(const ISState states[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, ISRule rule, double timeout, IPState state)
bool update(const char *const texts[], const char *const names[], int n)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
INDI::PropertyViewText * getText() const
TelescopeStatus TrackState
void SetAxis1Park(double value)
SetRAPark Set current RA/AZ parking position. The data park file (stored in ~/.indi/ParkData....
void SetAxis1ParkDefault(double steps)
SetRAPark Set default RA/AZ parking position.
void SetTelescopeCapability(uint32_t cap, uint8_t slewRateCount)
SetTelescopeCapability sets the Telescope capabilities. All capabilities must be initialized.
virtual bool initProperties() override
Called to initialize basic properties required all the time.
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
Process the client newSwitch command.
void setTelescopeConnection(const uint8_t &value)
setTelescopeConnection Set telescope connection mode. Child class should call this in the constructor...
virtual int AddTrackMode(const char *name, const char *label, bool isDefault=false)
AddTrackMode.
virtual bool updateProperties() override
Called when connected state changes, to add/remove properties.
virtual void SetParked(bool isparked)
SetParked Change the mount parking status. The data park file (stored in ~/.indi/ParkData....
Connection::TCP * tcpConnection
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
void NewRaDec(double ra, double dec)
The child class calls this function when it has updates.
void setPierSide(TelescopePierSide side)
bool InitPark()
InitPark Loads parking data (stored in ~/.indi/ParkData.xml) that contains parking status and parking...
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
void SetAxis2Park(double steps)
SetDEPark Set current DEC/ALT parking position. The data park file (stored in ~/.indi/ParkData....
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
void SetParkDataType(TelescopeParkData type)
setParkDataType Sets the type of parking data stored in the park data file and presented to the user.
void SetAxis2ParkDefault(double steps)
SetDEParkDefault Set default DEC/ALT parking position.
virtual bool UnPark() override
UnPark — re-enable both motors before clearing the parked state.
virtual bool Abort() override
Abort.
virtual bool updateLocation(double latitude, double longitude, double elevation) override
virtual ~PlaneWave()
virtual bool Sync(double ra, double dec) override
Sync — adds a pointing-model calibration point at the current position.
virtual bool Goto(double ra, double dec) override
Goto — J2000 RA/Dec (INDI convention)
virtual bool SetTrackRate(double raRate, double deRate) override
SetTrackRate — called when the user changes TrackRateNP.
virtual bool Park() override
Park.
virtual bool updateProperties() override
updateProperties
virtual bool SetDefaultPark() override
SetDefaultPark.
virtual bool Handshake() override
Handshake — build the persistent client and verify the mount is live.
virtual bool initProperties() override
initProperties
virtual const char * getDefaultName() override
virtual bool MoveNS(INDI_DIR_NS dir, TelescopeMotionCommand command) override
MoveNS — jog in Dec using a continuous offset rate.
virtual bool SetTrackEnabled(bool enabled) override
SetTrackEnabled.
virtual bool ISNewText(const char *dev, const char *name, char *texts[], char *names[], int n) override
ISNewText.
virtual bool updateTime(ln_date *utc, double utc_offset) override
updateTime — PWI4 uses the host PC clock; nothing to push.
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
ISNewNumber.
PlaneWave()
Constructor / Destructor.
virtual bool MoveWE(INDI_DIR_WE dir, TelescopeMotionCommand command) override
MoveWE — jog in RA using a continuous offset rate.
virtual bool SetTrackMode(uint8_t mode) override
SetTrackMode.
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems
virtual bool ReadScopeStatus() override
ReadScopeStatus — called every polling interval.
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
ISNewSwitch.
virtual bool SetCurrentPark() override
SetCurrentPark — tell PWI4 to store the current position as the park point.
void decode(std::istream &is)
Definition inicpp.h:586
const char * MAIN_CONTROL_TAB
MAIN_CONTROL_TAB Where all the primary controls for the device are located.
const char * INFO_TAB
INFO_TAB Where all the properties for general information are located.
const char * OPTIONS_TAB
OPTIONS_TAB Where all the driver's options are located. Those may include auxiliary controls,...
INDI::PropertyNumber TrackRateNP
INDI::PropertySwitch SlewRateSP
INDI::PropertySwitch TrackModeSP
INDI::PropertyNumber LocationNP
double ra
double dec
ISState
Switch state.
Definition indiapi.h:148
@ ISS_OFF
Definition indiapi.h:149
@ ISS_ON
Definition indiapi.h:150
@ IP_RW
Definition indiapi.h:184
@ IP_RO
Definition indiapi.h:182
@ IPS_BUSY
Definition indiapi.h:161
@ IPS_ALERT
Definition indiapi.h:162
@ IPS_IDLE
Definition indiapi.h:159
@ IPS_OK
Definition indiapi.h:160
@ ISR_1OFMANY
Definition indiapi.h:171
@ ISR_ATMOST1
Definition indiapi.h:172
@ AXIS_DE
@ AXIS_RA
INDI_DIR_WE
@ DIRECTION_EAST
INDI_DIR_NS
@ DIRECTION_SOUTH
Implementations for common driver routines.
#define INDI_UNUSED(x)
Definition indidevapi.h:132
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define LOG_ERROR(txt)
Shorter logging macros. In order to use these macros, the function (or method) "getDeviceName()" must...
Definition indilogger.h:72
#define LOGF_ERROR(fmt,...)
Definition indilogger.h:80
#define LOG_INFO(txt)
Definition indilogger.h:74
#define getDeviceName
std::string to_string(Error error)
Definition httplib.h:9275
std::unique_ptr< PlaneWave > PlaneWave_mount(new PlaneWave())