Instrument Neutral Distributed Interface INDI 2.2.5
planewave_rotator.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2026 Jasem Mutlaq. All rights reserved.
3
4 PlaneWave Rotator Driver
5 API Communication via PWI4 HTTP Interface (port 8220)
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_rotator.h"
23
24#include "indicom.h"
26
27#include <cmath>
28#include <cstring>
29#include <memory>
30#include <sstream>
31#include <iomanip>
32
33// Single global instance
34static std::unique_ptr<PlaneWaveRotator> sRotator(new PlaneWaveRotator());
35
40{
41 setVersion(1, 0);
42
43 // Capabilities: can abort and can reverse direction (software)
45
46 // We only want TCP — disable serial
48}
49
50// Must be defined here so unique_ptr<httplib::Client> is destroyed
51// after the complete type is known.
53
58{
59 return "PlaneWave Rotator";
60}
61
66{
68
69 // ── TCP connection defaults ────────────────────────────────────────────
70 tcpConnection->setDefaultHost("localhost");
71 tcpConnection->setDefaultPort(DEFAULT_PORT);
72
73 // ── PWI4 version ────────────────────────────────────────────────────────
74 VersionTP[0].fill("VERSION", "PWI4 Version", "");
75 VersionTP.fill(getDeviceName(), "FIRMWARE_INFO", "PWI4",
77
78 // ── Motor enable / disable ─────────────────────────────────────────────
79 MotorStateSP[MOTOR_ENABLE].fill("MOTOR_ENABLE", "Enable", ISS_OFF);
80 MotorStateSP[MOTOR_DISABLE].fill("MOTOR_DISABLE", "Disable", ISS_OFF);
81 MotorStateSP.fill(getDeviceName(), "ROTATOR_MOTOR", "Motor",
83
84 // ── Mechanical position (read-write) ───────────────────────────────────
85 MechPositionNP[0].fill("MECH_POSITION", "Mechanical (°)", "%.2f", 0.0, 360.0, 0.1, 0.0);
86 MechPositionNP.fill(getDeviceName(), "ROTATOR_MECH_POSITION", "Mech. Position",
88
89 // ── Status lights ──────────────────────────────────────────────────────
90 RotatorStatusLP[STATUS_CONNECTED].fill("STATUS_CONNECTED", "HW Connected", IPS_IDLE);
91 RotatorStatusLP[STATUS_ENABLED].fill("STATUS_ENABLED", "Motor Enabled", IPS_IDLE);
92 RotatorStatusLP.fill(getDeviceName(), "ROTATOR_STATUS", "Status",
94
95 // ── Rotator interface properties ───────────────────────────────────────
96 // These are already filled in by INDI::RotatorInterface::initProperties(),
97 // called inside INDI::Rotator::initProperties() above.
98 // GotoRotatorNP represents the field angle (0-360°).
99
102
103 return true;
104}
105
110{
112
113 if (isConnected())
114 {
115 defineProperty(VersionTP);
116 defineProperty(MotorStateSP);
117 defineProperty(MechPositionNP);
118 defineProperty(RotatorStatusLP);
119 }
120 else
121 {
122 deleteProperty(VersionTP);
123 deleteProperty(MotorStateSP);
124 deleteProperty(MechPositionNP);
125 deleteProperty(RotatorStatusLP);
126 }
127
128 return true;
129}
130
135{
136 // Create (or recreate on reconnect) the persistent HTTP client
137 m_HttpClient = std::make_unique<httplib::Client>(tcpConnection->host(),
139 m_HttpClient->set_connection_timeout(5, 0);
140 m_HttpClient->set_read_timeout(10, 0);
141
142 // Fetch initial status to verify PWI4 is reachable
143 if (!getStatus())
144 {
145 LOG_ERROR("Cannot communicate with PWI4. Is PWI4 running and reachable?");
146 return false;
147 }
148
149 // Check that PWI4 reports the rotator as connected to hardware
150 bool rotatorConnected = m_Status["rotator.is_connected"].as<bool>();
151 if (!rotatorConnected)
152 {
153 LOG_ERROR("PWI4 reports rotator is not connected to hardware. "
154 "Please connect the rotator in PWI4 first.");
155 return false;
156 }
157
158 // Auto-enable motor if it is currently disabled
159 bool motorEnabled = m_Status["rotator.is_enabled"].as<bool>();
160 if (!motorEnabled)
161 {
162 LOG_INFO("Motor is disabled. Sending /rotator/enable …");
163 dispatch("/rotator/enable");
164 // Re-read status to confirm
165 getStatus();
166 motorEnabled = m_Status["rotator.is_enabled"].as<bool>();
167 }
168
169 // Populate version info
170 std::string version = m_Status["pwi4.version"].as<std::string>();
171 VersionTP[0].setText(version.c_str());
172 VersionTP.setState(IPS_OK);
173
174 // Populate motor state switch
175 MotorStateSP[MOTOR_ENABLE].setState(motorEnabled ? ISS_ON : ISS_OFF);
176 MotorStateSP[MOTOR_DISABLE].setState(motorEnabled ? ISS_OFF : ISS_ON);
177 MotorStateSP.setState(IPS_OK);
178
179 // Populate status lights
180 RotatorStatusLP[STATUS_CONNECTED].setState(IPS_OK);
181 RotatorStatusLP[STATUS_ENABLED].setState(motorEnabled ? IPS_OK : IPS_ALERT);
182 RotatorStatusLP.setState(IPS_OK);
183
184 // Seed the position fields
185 double fieldAngle = m_Status["rotator.field_angle_degs"].as<double>();
186 double mechAngle = m_Status["rotator.mech_position_degs"].as<double>();
187
188 GotoRotatorNP[0].setValue(fieldAngle);
190
191 MechPositionNP[0].setValue(mechAngle);
192 MechPositionNP.setState(IPS_OK);
193
194 m_LastFieldAngle = fieldAngle;
195 m_LastMechPosition = mechAngle;
196
197 LOGF_INFO("Connected to PWI4 %s. Field angle: %.2f°, Mech position: %.2f°",
198 version.c_str(), fieldAngle, mechAngle);
199
200 return true;
201}
202
206bool PlaneWaveRotator::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
207{
208 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
209 {
210 // ── Motor enable / disable ────────────────────────────────────────
211 if (MotorStateSP.isNameMatch(name))
212 {
213 MotorStateSP.update(states, names, n);
214 bool enableRequested = (MotorStateSP[MOTOR_ENABLE].getState() == ISS_ON);
215
216 bool ok = dispatch(enableRequested ? "/rotator/enable" : "/rotator/disable");
217 if (ok)
218 {
219 LOGF_INFO("Rotator motor %s.", enableRequested ? "enabled" : "disabled");
220 MotorStateSP.setState(IPS_OK);
221 RotatorStatusLP[STATUS_ENABLED].setState(enableRequested ? IPS_OK : IPS_ALERT);
222 RotatorStatusLP.apply();
223 }
224 else
225 {
226 LOG_ERROR("Failed to change motor state.");
227 MotorStateSP.setState(IPS_ALERT);
228 }
229 MotorStateSP.apply();
230 return true;
231 }
232
233 // ── RotatorInterface switches (Abort, Reverse, etc.) ──────────────
234 if (INDI::RotatorInterface::processSwitch(dev, name, states, names, n))
235 return true;
236 }
237
238 return INDI::Rotator::ISNewSwitch(dev, name, states, names, n);
239}
240
244bool PlaneWaveRotator::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
245{
246 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
247 {
248 // ── Mechanical position goto ─────────────────────────────────────
249 if (MechPositionNP.isNameMatch(name))
250 {
251 double target = values[0];
252
253 std::ostringstream req;
254 req << std::fixed << std::setprecision(4) << "/rotator/goto_mech?degs=" << target;
255
256 if (dispatch(req.str()))
257 {
258 MechPositionNP[0].setValue(target);
259 MechPositionNP.setState(IPS_BUSY);
260 // Field angle goto will also update GotoRotatorNP via TimerHit
263 LOGF_INFO("Moving rotator to mechanical position %.2f°.", target);
264 }
265 else
266 {
267 LOG_ERROR("Failed to send /rotator/goto_mech command.");
268 MechPositionNP.setState(IPS_ALERT);
269 }
270 MechPositionNP.apply();
271 return true;
272 }
273
274 // ── RotatorInterface numbers (GotoRotatorNP handled by MoveRotator) ─
275 if (INDI::RotatorInterface::processNumber(dev, name, values, names, n))
276 return true;
277 }
278
279 return INDI::Rotator::ISNewNumber(dev, name, values, names, n);
280}
281
287{
288 // Apply direction reversal in software
289 double target = angle;
290 if (ReverseRotatorSP[INDI_ENABLED].getState() == ISS_ON)
291 target = range360(360.0 - angle);
292
293 std::ostringstream req;
294 req << std::fixed << std::setprecision(4) << "/rotator/goto_field?degs=" << target;
295
296 if (!dispatch(req.str()))
297 {
298 LOG_ERROR("Failed to send /rotator/goto_field command.");
299 return IPS_ALERT;
300 }
301
302 LOGF_INFO("Moving rotator to field angle %.2f°%s.", target,
303 (ReverseRotatorSP[INDI_ENABLED].getState() == ISS_ON) ? " (reversed)" : "");
304
305 return IPS_BUSY;
306}
307
312{
313 bool ok = dispatch("/rotator/stop");
314 if (ok)
315 LOG_INFO("Rotator motion aborted.");
316 else
317 LOG_ERROR("Failed to abort rotator motion.");
318 return ok;
319}
320
326{
327 LOGF_INFO("Rotator direction %s.", enabled ? "reversed" : "normal");
328 return true;
329}
330
335{
336 if (!isConnected())
337 {
339 return;
340 }
341
342 if (!getStatus())
343 {
344 LOG_WARN("Failed to get status from PWI4.");
346 return;
347 }
348
349 // ── Read live values from status ──────────────────────────────────────
350 bool hwConnected = m_Status["rotator.is_connected"].as<bool>();
351 bool motorEnabled = m_Status["rotator.is_enabled"].as<bool>();
352 bool isMoving = m_Status["rotator.is_moving"].as<bool>();
353 double fieldAngle = m_Status["rotator.field_angle_degs"].as<double>();
354 double mechAngle = m_Status["rotator.mech_position_degs"].as<double>();
355
356 // ── Status lights ─────────────────────────────────────────────────────
357 IPState connLight = hwConnected ? IPS_OK : IPS_ALERT;
358 IPState enabledLight = motorEnabled ? IPS_OK : IPS_ALERT;
359 if (RotatorStatusLP[STATUS_CONNECTED].getState() != connLight ||
360 RotatorStatusLP[STATUS_ENABLED].getState() != enabledLight)
361 {
362 RotatorStatusLP[STATUS_CONNECTED].setState(connLight);
363 RotatorStatusLP[STATUS_ENABLED].setState(enabledLight);
364 RotatorStatusLP.apply();
365 }
366
367 // ── Motor state switch ────────────────────────────────────────────────
368 ISState onState = motorEnabled ? ISS_ON : ISS_OFF;
369 ISState offState = motorEnabled ? ISS_OFF : ISS_ON;
370 if (MotorStateSP[MOTOR_ENABLE].getState() != onState)
371 {
372 MotorStateSP[MOTOR_ENABLE].setState(onState);
373 MotorStateSP[MOTOR_DISABLE].setState(offState);
374 MotorStateSP.apply();
375 }
376
377 // ── Field angle (GotoRotatorNP) ───────────────────────────────────────
378 bool fieldChanged = std::fabs(fieldAngle - m_LastFieldAngle) > POSITION_THRESHOLD;
379
381 {
382 if (!isMoving)
383 {
384 // Motion completed
385 GotoRotatorNP[0].setValue(fieldAngle);
388 LOG_INFO("Rotator reached target field angle.");
389 fieldChanged = false;
390 }
391 else if (fieldChanged)
392 {
393 // Still moving — update position readout
394 GotoRotatorNP[0].setValue(fieldAngle);
396 }
397 }
398 else if (fieldChanged)
399 {
400 GotoRotatorNP[0].setValue(fieldAngle);
402 }
403
404 if (fieldChanged)
405 m_LastFieldAngle = fieldAngle;
406
407 // ── Mechanical position (MechPositionNP) ─────────────────────────────
408 bool mechChanged = std::fabs(mechAngle - m_LastMechPosition) > POSITION_THRESHOLD;
409
410 if (MechPositionNP.getState() == IPS_BUSY)
411 {
412 if (!isMoving)
413 {
414 MechPositionNP[0].setValue(mechAngle);
415 MechPositionNP.setState(IPS_OK);
416 MechPositionNP.apply();
417 mechChanged = false;
418 }
419 else if (mechChanged)
420 {
421 MechPositionNP[0].setValue(mechAngle);
422 MechPositionNP.apply();
423 }
424 }
425 else if (mechChanged)
426 {
427 MechPositionNP[0].setValue(mechAngle);
428 MechPositionNP.apply();
429 }
430
431 if (mechChanged)
432 m_LastMechPosition = mechAngle;
433
435}
436
441{
443 // ReverseRotatorSP is already saved by RotatorInterface::saveConfigItems
444 return true;
445}
446
450bool PlaneWaveRotator::getStatus()
451{
452 return dispatch("/status");
453}
454
462bool PlaneWaveRotator::dispatch(const std::string &request)
463{
464 if (!m_HttpClient)
465 {
466 LOG_ERROR("HTTP client not initialized. Is the rotator connected?");
467 return false;
468 }
469
470 auto res = m_HttpClient->Get(request);
471
472 if (!res)
473 {
474 LOGF_ERROR("HTTP GET %s failed: %s",
475 request.c_str(),
476 httplib::to_string(res.error()).c_str());
477 return false;
478 }
479
480 if (res->status != 200)
481 {
482 LOGF_ERROR("HTTP GET %s returned HTTP %d: %s",
483 request.c_str(), res->status,
484 res->body.empty() ? "(no body)" : res->body.c_str());
485 return false;
486 }
487
488 if (res->body.empty())
489 {
490 LOGF_ERROR("HTTP GET %s returned an empty body.", request.c_str());
491 return false;
492 }
493
494 try
495 {
496 // PWI4 returns a flat key=value format. Wrap it in a dummy [status]
497 // section so that inicpp can parse it as a standard INI file.
498 ini::IniFile inif;
499 inif.decode("[status]\n" + res->body);
500
501 if (inif["status"].size() == 0)
502 {
503 LOGF_ERROR("Parsed status for %s is empty.", request.c_str());
504 return false;
505 }
506
507 m_Status = inif["status"];
508 return true;
509 }
510 catch (const std::exception &e)
511 {
512 LOGF_ERROR("Failed to parse PWI4 response for %s: %s",
513 request.c_str(), e.what());
514 return false;
515 }
516}
void setDefaultHost(const char *addressHost)
void setDefaultPort(uint32_t addressPort)
virtual const char * host() const
virtual uint32_t port() const
bool isConnected() const
void setDefaultPollingPeriod(uint32_t msec)
setDefaultPollingPeriod Change the default polling period to call TimerHit() function in the driver.
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)
uint32_t getCurrentPollingPeriod() const
getCurrentPollingPeriod Return the current polling period.
void addAuxControls()
Add Debug, Simulation, and Configuration options to the driver.
int SetTimer(uint32_t ms)
Set a timer to call the function TimerHit after ms milliseconds.
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
bool isNameMatch(const char *otherName) const
void fill(const char *device, const char *name, const char *label, const char *group, IPState state)
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)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
bool processSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
Process Rotator switch properties.
void SetCapability(uint32_t cap)
SetRotatorCapability sets the Rotator capabilities. All capabilities must be initialized.
bool processNumber(const char *dev, const char *name, double values[], char *names[], int n)
Process Rotator number properties.
INDI::PropertySwitch ReverseRotatorSP
INDI::PropertyNumber GotoRotatorNP
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
virtual bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
void setRotatorConnection(const uint8_t &value)
setRotatorConnection Set Rotator connection mode. Child class should call this in the constructor bef...
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems Saves the reverse direction property in the configuration file
Connection::TCP * tcpConnection
Definition indirotator.h:96
The PlaneWaveRotator class provides an INDI driver for the PlaneWave Series 5 rotator,...
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
ISNewNumber.
virtual bool AbortRotator() override
AbortRotator — RotatorInterface virtual.
virtual bool updateProperties() override
updateProperties
PlaneWaveRotator()
Constructor / Destructor.
virtual bool Handshake() override
Handshake — build the persistent HTTP client and verify connectivity.
virtual bool ReverseRotator(bool enabled) override
virtual void TimerHit() override
TimerHit — polling loop.
virtual bool saveConfigItems(FILE *fp) override
saveConfigItems
virtual ~PlaneWaveRotator()
virtual const char * getDefaultName() override
getDefaultName
virtual IPState MoveRotator(double angle) override
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
ISNewSwitch.
virtual bool initProperties() override
initProperties
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.
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
IPState
Property state.
Definition indiapi.h:158
@ 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
double range360(double r)
range360 Limits an angle to be between 0-360 degrees.
Definition indicom.c:1321
Implementations for common driver routines.
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define LOG_WARN(txt)
Definition indilogger.h:73
#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