Instrument Neutral Distributed Interface INDI 2.2.5
avalon_upas.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Avalon Instruments Universal Polar Alignment System (UPAS)
3
4 Copyright (C) 2026 Jasem Mutlaq ([email protected])
5
6 GRBL-based serial protocol.
7
8 This library is free software; you can redistribute it and/or
9 modify it under the terms of the GNU Lesser General Public
10 License as published by the Free Software Foundation; either
11 version 2.1 of the License, or (at your option) any later version.
12
13 This library is distributed in the hope that it will be useful,
14 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 Lesser General Public License for more details.
17
18 You should have received a copy of the GNU Lesser General Public
19 License along with this library; if not, write to the Free Software
20 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21*******************************************************************************/
22
23#include "avalon_upas.h"
24
25#include "indicom.h"
26#include "indilogger.h"
28
29#include <cmath>
30#include <cstring>
31#include <memory>
32#include <termios.h>
33#include <regex>
34
35static std::unique_ptr<AvalonUPAS> upas(new AvalonUPAS());
36
43
45{
46 return "Avalon UPAS";
47}
48
50{
52
54
55 // Speed: map directly to GRBL feed rate (mm/min), range 100–1000 step 100 default 500.
56 SpeedNP[0].setMin(100);
57 SpeedNP[0].setMax(1000);
58 SpeedNP[0].setStep(100);
59 SpeedNP[0].setValue(500);
60
61 // Gear ratio: mm of motor travel per degree of polar-axis correction.
62 // Default 1.0 mm/deg; user should calibrate for their specific hardware.
63 // Avalon Default is 1.0 mm/arcmin
64 GearRatioNP[GEAR_AZ].fill("GEAR_AZ", "Azimuth (mm/arcmin)", "%.4f", 0.001, 1000, 0.1, 1.0);
65 GearRatioNP[GEAR_ALT].fill("GEAR_ALT", "Altitude (mm/arcmin)", "%.4f", 0.001, 1000, 0.1, 1.0);
66 GearRatioNP.fill(getDeviceName(), "PAC_GEAR_RATIO", "Gear Ratio", MAIN_CONTROL_TAB, IP_RW, 60, IPS_IDLE);
67
68 // Firmware (read-only, populated in Handshake)
69 FirmwareTP[0].fill("VERSION", "Version", "Unknown");
70 FirmwareTP.fill(getDeviceName(), "FIRMWARE_INFO", "Firmware", INFO_TAB, IP_RO, 60, IPS_IDLE);
71
73
74 // Serial connection
75 serialConnection = new Connection::Serial(this);
76 serialConnection->registerHandshake([&]()
77 {
78 return Handshake();
79 });
81 registerConnection(serialConnection);
82
84 return true;
85}
86
88{
90
91 if (isConnected())
92 {
93 defineProperty(GearRatioNP);
94 defineProperty(FirmwareTP);
95 }
96 else
97 {
98 deleteProperty(GearRatioNP);
99 deleteProperty(FirmwareTP);
100 }
101
103 return true;
104}
105
107{
109 GearRatioNP.save(fp);
111 return true;
112}
113
115{
116 PortFD = serialConnection->getPortFD();
117
118 // Flush any GRBL boot message
119 tcflush(PortFD, TCIOFLUSH);
120
121 // Send soft-reset so GRBL is in a known idle state, then discard response.
122 const char softReset[2] = {'\x18', '\n'};
123 int nbytes = 0;
124 tty_write(PortFD, softReset, 1, &nbytes); // 0x18 is single byte, no \n needed
125 usleep(500000); // wait 500 ms for GRBL to boot/reset
126 tcflush(PortFD, TCIOFLUSH);
127
128 // Query status to confirm communication.
129 char res[DRIVER_LEN] = {0};
130 if (!sendCommand("?", res))
131 return false;
132
133 if (strstr(res, "MPos") == nullptr && strstr(res, "WPos") == nullptr)
134 {
135 LOGF_ERROR("Unexpected handshake response: %s", res);
136 return false;
137 }
138
139 // Parse position and state from the response.
140 parseStatus(res);
141
142 // Try to read GRBL version via $I command.
143 char ver[DRIVER_LEN] = {0};
144 if (sendCommand("$I", ver))
145 {
146 // GRBL version is typically on a line like "[VER:1.1f.20170801:]"
147 const char *tag = strstr(ver, "[VER:");
148 if (tag)
149 {
150 // Extract up to the closing bracket
151 char version[64] = {0};
152 sscanf(tag, "[VER:%63[^]]]", version);
153 FirmwareTP[0].setText(version);
154 }
155 else
156 {
157 FirmwareTP[0].setText(ver);
158 }
159 }
160
161 LOG_INFO("UPAS connected and ready.");
162 return true;
163}
164
166{
167 if (!isConnected())
168 return;
169
170 char res[DRIVER_LEN] = {0};
171 if (sendCommand("?", res))
172 parseStatus(res);
173
175}
176
177bool AvalonUPAS::parseStatus(const char *response)
178{
179 // GRBL status format: <State|MPos:x.xxx,y.xxx|...>
180 // State can be: Idle, Jog, Run, Hold, Alarm, ...
181 const char *mposTag = strstr(response, "MPos:");
182 if (mposTag == nullptr)
183 return false;
184
185 double azMM = 0.0, altMM = 0.0;
186 if (sscanf(mposTag + 5, "%lf,%lf", &azMM, &altMM) < 2)
187 return false;
188
189 const double gearAZ = GearRatioNP[GEAR_AZ].getValue();
190 const double gearALT = GearRatioNP[GEAR_ALT].getValue();
191
192 PositionNP[POSITION_AZ].setValue(gearAZ > 0 ? azMM / (60 * gearAZ) : 0);
193 PositionNP[POSITION_ALT].setValue(gearALT > 0 ? altMM / (60 * gearALT) : 0);
194
195 // Determine if the device is still jogging.
196 bool wasMoving = m_IsMoving;
197 m_IsMoving = (strstr(response, "Jog") != nullptr || strstr(response, "Run") != nullptr);
198
199 PositionNP.setState(m_IsMoving ? IPS_BUSY : IPS_OK);
201
202 // Motion just completed?
203 if (wasMoving && !m_IsMoving && ManualAdjustmentNP.getState() == IPS_BUSY)
204 {
207 LOG_INFO("UPAS motion complete.");
208 }
209
210 return true;
211}
212
214{
215 // incluce conversion from arcminutes to degrees in the gear ratio, so users can calibrate in more intuitive mm/arcmin instead of mm/degree.
216 const double mm = degrees * 60 * GearRatioNP[GEAR_AZ].getValue();
217 const double feedRate = SpeedNP[0].getValue();
218
219 char cmd[DRIVER_LEN] = {0};
220 snprintf(cmd, DRIVER_LEN, "$J=G91G21X%.4f F%.0f", mm, feedRate);
221
222 char res[DRIVER_LEN] = {0};
223 if (!sendCommand(cmd, res))
224 return IPS_ALERT;
225
226 // GRBL acknowledges jog commands with "ok"
227 if (strncmp(res, "ok", 2) != 0)
228 {
229 LOGF_ERROR("Unexpected AZ jog response: %s", res);
230 return IPS_ALERT;
231 }
232
233 m_IsMoving = true;
234 LOGF_INFO("AZ jog: %.4f deg → %.4f mm @ %.0f mm/min.", degrees, mm, feedRate);
235 return IPS_BUSY;
236}
237
239{
240 // incluce conversion from arcminutes to degrees in the gear ratio, so users can calibrate in more intuitive mm/arcmin instead of mm/degree.
241 const double mm = degrees * 60 *GearRatioNP[GEAR_ALT].getValue();
242 const double feedRate = SpeedNP[0].getValue();
243
244 char cmd[DRIVER_LEN] = {0};
245 snprintf(cmd, DRIVER_LEN, "$J=G91G21Y%.4f F%.0f", mm, feedRate);
246
247 char res[DRIVER_LEN] = {0};
248 if (!sendCommand(cmd, res))
249 return IPS_ALERT;
250
251 if (strncmp(res, "ok", 2) != 0)
252 {
253 LOGF_ERROR("Unexpected ALT jog response: %s", res);
254 return IPS_ALERT;
255 }
256
257 m_IsMoving = true;
258 LOGF_INFO("ALT jog: %.4f deg → %.4f mm @ %.0f mm/min.", degrees, mm, feedRate);
259 return IPS_BUSY;
260}
261
262IPState AvalonUPAS::MoveBoth(double azDegrees, double altDegrees)
263{
264 // GRBL supports multi-axis jog in a single command: X (AZ) and Y (ALT) together.
265 // This moves both axes simultaneously, which is more efficient and accurate than
266 // two separate jog commands.
267 const double mmAZ = azDegrees * 60 * GearRatioNP[GEAR_AZ].getValue();
268 const double mmALT = altDegrees * 60 * GearRatioNP[GEAR_ALT].getValue();
269 const double feedRate = SpeedNP[0].getValue();
270
271 char cmd[DRIVER_LEN] = {0};
272 snprintf(cmd, DRIVER_LEN, "$J=G91G21X%.4fY%.4f F%.0f", mmAZ, mmALT, feedRate);
273
274 char res[DRIVER_LEN] = {0};
275 if (!sendCommand(cmd, res))
276 return IPS_ALERT;
277
278 if (strncmp(res, "ok", 2) != 0)
279 {
280 LOGF_ERROR("MoveBoth GRBL response: %s", res);
281 return IPS_ALERT;
282 }
283
284 m_IsMoving = true;
285 LOGF_INFO("MoveBoth: AZ %.4f° (%.4f mm), ALT %.4f° (%.4f mm) @ %.0f mm/min.",
286 azDegrees, mmAZ, altDegrees, mmALT, feedRate);
287 return IPS_BUSY;
288}
289
291{
292 // GRBL real-time jog cancel command (single byte 0x85, no newline needed).
293 const char cancel = '\x85';
294 int nbytes = 0;
295 int rc = tty_write(PortFD, &cancel, 1, &nbytes);
296 if (rc != TTY_OK)
297 {
298 char errstr[MAXRBUF] = {0};
299 tty_error_msg(rc, errstr, MAXRBUF);
300 LOGF_ERROR("AbortMotion serial write error: %s.", errstr);
301 return false;
302 }
303
304 m_IsMoving = false;
305 LOG_INFO("UPAS motion aborted.");
306 return true;
307}
308
309bool AvalonUPAS::SetPACSpeed(uint16_t speed)
310{
311 // Speed is stored in SpeedNP and used in the next jog command; nothing is
312 // sent to the hardware here since GRBL feed rate is per-command.
313 LOGF_INFO("UPAS speed set to %u mm/min.", speed);
314 return true;
315}
316
317bool AvalonUPAS::ReverseAZ(bool enabled)
318{
319 if (enabled)
320 m_ReverseFlags |= 0x01;
321 else
322 m_ReverseFlags &= ~0x01;
323
324 char cmd[DRIVER_LEN] = {0};
325 snprintf(cmd, DRIVER_LEN, "$3=%d", m_ReverseFlags);
326
327 char res[DRIVER_LEN] = {0};
328 if (!sendCommand(cmd, res))
329 return false;
330
331 LOGF_INFO("AZ reverse %s (flags=0x%02X).", enabled ? "enabled" : "disabled", m_ReverseFlags);
332 return (strncmp(res, "ok", 2) == 0);
333}
334
335bool AvalonUPAS::ReverseALT(bool enabled)
336{
337 if (enabled)
338 m_ReverseFlags |= 0x02;
339 else
340 m_ReverseFlags &= ~0x02;
341
342 char cmd[DRIVER_LEN] = {0};
343 snprintf(cmd, DRIVER_LEN, "$3=%d", m_ReverseFlags);
344
345 char res[DRIVER_LEN] = {0};
346 if (!sendCommand(cmd, res))
347 return false;
348
349 LOGF_INFO("ALT reverse %s (flags=0x%02X).", enabled ? "enabled" : "disabled", m_ReverseFlags);
350 return (strncmp(res, "ok", 2) == 0);
351}
352
353bool AvalonUPAS::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
354{
355 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
356 {
357 if (GearRatioNP.isNameMatch(name))
358 {
359 GearRatioNP.update(values, names, n);
360 GearRatioNP.setState(IPS_OK);
361 GearRatioNP.apply();
362 return true;
363 }
364
365 if (PACI::processNumber(dev, name, values, names, n))
366 return true;
367 }
368
369 return INDI::DefaultDevice::ISNewNumber(dev, name, values, names, n);
370}
371
372bool AvalonUPAS::ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
373{
374 if (dev != nullptr && strcmp(dev, getDeviceName()) == 0)
375 {
376 if (PACI::processSwitch(dev, name, states, names, n))
377 return true;
378 }
379
380 return INDI::DefaultDevice::ISNewSwitch(dev, name, states, names, n);
381}
382
386bool AvalonUPAS::sendCommand(const char *cmd, char *res, int cmd_len, int res_len)
387{
388 int nbytes_written = 0, nbytes_read = 0, rc = -1;
389
390 tcflush(PortFD, TCIOFLUSH);
391
392 if (cmd_len > 0)
393 {
394 // Raw binary write
395 char hex_cmd[DRIVER_LEN * 3] = {0};
396 hexDump(hex_cmd, cmd, cmd_len);
397 LOGF_DEBUG("CMD <%s>", hex_cmd);
398 rc = tty_write(PortFD, cmd, cmd_len, &nbytes_written);
399 }
400 else
401 {
402 // String command: append newline
403 LOGF_DEBUG("CMD <%s>", cmd);
404 char formatted[DRIVER_LEN] = {0};
405 snprintf(formatted, DRIVER_LEN, "%s\n", cmd);
406 rc = tty_write_string(PortFD, formatted, &nbytes_written);
407 }
408
409 if (rc != TTY_OK)
410 {
411 char errstr[MAXRBUF] = {0};
412 tty_error_msg(rc, errstr, MAXRBUF);
413 LOGF_ERROR("Serial write error: %s.", errstr);
414 return false;
415 }
416
417 if (res == nullptr)
418 return true;
419
420 if (res_len > 0)
421 rc = tty_read(PortFD, res, res_len, DRIVER_TIMEOUT, &nbytes_read);
422 else
423 rc = tty_nread_section(PortFD, res, DRIVER_LEN, DRIVER_STOP_CHAR, DRIVER_TIMEOUT, &nbytes_read);
424
425 if (rc != TTY_OK)
426 {
427 char errstr[MAXRBUF] = {0};
428 tty_error_msg(rc, errstr, MAXRBUF);
429 LOGF_ERROR("Serial read error: %s.", errstr);
430 return false;
431 }
432
433 // Strip trailing newline/carriage-return
434 if (nbytes_read > 0 && (res[nbytes_read - 1] == '\n' || res[nbytes_read - 1] == '\r'))
435 res[nbytes_read - 1] = '\0';
436 if (nbytes_read > 1 && (res[nbytes_read - 2] == '\r'))
437 res[nbytes_read - 2] = '\0';
438
439 LOGF_DEBUG("RES <%s>", res);
440
441 tcflush(PortFD, TCIOFLUSH);
442 return true;
443}
444
445void AvalonUPAS::hexDump(char *buf, const char *data, int size)
446{
447 for (int i = 0; i < size; i++)
448 sprintf(buf + 3 * i, "%02X ", static_cast<uint8_t>(data[i]));
449 if (size > 0)
450 buf[3 * size - 1] = '\0';
451}
The AvalonUPAS class drives the Avalon Instruments Universal Polar Alignment System (UPAS) via its GR...
Definition avalon_upas.h:43
bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
bool saveConfigItems(FILE *fp) override
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
bool Handshake()
bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n) override
Process the client newSwitch command.
bool AbortMotion() override
AbortMotion Abort all axis motion immediately.
IPState MoveAZ(double degrees) override
Move the azimuth axis by the given number of degrees.
IPState MoveBoth(double azDegrees, double altDegrees) override
MoveBoth Move both axes in a single coordinated operation.
bool ReverseAZ(bool enabled) override
ReverseAZ Reverse the direction of azimuth axis movement.
bool ReverseALT(bool enabled) override
ReverseALT Reverse the direction of altitude axis movement.
const char * getDefaultName() override
bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
void TimerHit() override
Callback function to be called once SetTimer duration elapses.
IPState MoveALT(double degrees) override
MoveALT Move the altitude axis by the given number of degrees.
bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
bool SetPACSpeed(uint16_t speed) override
SetPACSpeed Set the motor speed used for axis movements.
void registerHandshake(std::function< bool()> callback)
registerHandshake Register a handshake function to be called once the initial connection to the devic...
The Serial class manages connection with serial devices including Bluetooth. Serial communication is ...
void setDefaultBaudRate(BaudRate newRate)
setDefaultBaudRate Set default baud rate. The default baud rate is 9600 unless otherwise changed by t...
bool isConnected() const
const char * getDeviceName() const
virtual bool updateProperties()
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
virtual bool ISNewSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
Process the client newSwitch command.
void registerConnection(Connection::Interface *newConnection)
registerConnection Add new connection plugin to the existing connection pool. The connection type sha...
void setDriverInterface(uint32_t value)
setInterface Set driver interface. By default the driver interface is set to GENERAL_DEVICE....
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)
virtual bool saveConfigItems(FILE *fp)
saveConfigItems Save specific properties in the provide config file handler. Child class usually over...
uint32_t getCurrentPollingPeriod() const
getCurrentPollingPeriod Return the current polling period.
virtual bool initProperties()
Initialize properties initial state and value. The child class must implement this function.
void addAuxControls()
Add Debug, Simulation, and Configuration options to the driver.
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
Process the client newNumber command.
int SetTimer(uint32_t ms)
Set a timer to call the function TimerHit after ms milliseconds.
bool saveConfigItems(FILE *fp)
saveConfigItems Save PAC interface properties to config file. Call this from your driver's saveConfig...
bool processNumber(const char *dev, const char *name, double values[], char *names[], int n)
Process PAC number properties.
INDI::PropertyNumber SpeedNP
void initProperties(const char *group)
initProperties Initialize PAC properties. Call this from your driver's initProperties().
void SetCapability(uint32_t cap)
SetCapability sets the PAC device capabilities. Call this in the driver constructor.
INDI::PropertyNumber PositionNP
INDI::PropertyNumber ManualAdjustmentNP
bool processSwitch(const char *dev, const char *name, ISState *states, char *names[], int n)
Process PAC switch properties.
bool updateProperties()
updateProperties Define or delete properties based on connection state. Call this from your driver's ...
@ POSITION_ALT
Current altitude position in degrees.
@ POSITION_AZ
Current azimuth position in degrees.
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)
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
Provides interface to implement automated polar alignment correction (PAC).
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.
ISState
Switch state.
Definition indiapi.h:148
@ 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
int tty_write(int fd, const char *buf, int nbytes, int *nbytes_written)
Writes a buffer to fd.
Definition indicom.c:418
int tty_read(int fd, char *buf, int nbytes, int timeout, int *nbytes_read)
read buffer from terminal
Definition indicom.c:548
int tty_write_string(int fd, const char *buf, int *nbytes_written)
Writes a null terminated string to fd.
Definition indicom.c:473
void tty_error_msg(int err_code, char *err_msg, int err_msg_len)
Retrieve the tty error message.
Definition indicom.c:1243
int tty_nread_section(int fd, char *buf, int nsize, char stop_char, int timeout, int *nbytes_read)
read buffer from terminal with a delimiter
Definition indicom.c:759
Implementations for common driver routines.
@ TTY_OK
Definition indicom.h:54
#define MAXRBUF
Definition indidriver.c:52
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define LOGF_DEBUG(fmt,...)
Definition indilogger.h:83
#define LOGF_ERROR(fmt,...)
Definition indilogger.h:80
#define LOG_INFO(txt)
Definition indilogger.h:74
uint8_t cmd[4]
Definition pwc-ioctl.h:2