Instrument Neutral Distributed Interface INDI 2.2.5
rasa_filtercube.cpp
Go to the documentation of this file.
1/*******************************************************************************
2 Copyright(c) 2026 WandererAstro. All rights reserved.
3
4 RASA FilterCube Filter Wheel Driver
5
6 This library is free software; you can redistribute it and/or
7 modify it under the terms of the GNU Library General Public
8 License version 2 as published by the Free Software Foundation.
9
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Library General Public License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to
17 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 Boston, MA 02110-1301, USA.
19*******************************************************************************/
20
21#include "rasa_filtercube.h"
22
23#include "indicom.h"
24
26#include <cstring>
27#include <memory>
28#include <strings.h>
29#include <termios.h>
30
31#define FILTERCUBE_TIMEOUT 5
32#define FILTERCUBE_STATUS_TIMEOUT 2
33#define FILTERCUBE_MIN_FIRMWARE 20260312.0
34#define FILTERCUBE_MODEL "WFC50"
35
36static std::unique_ptr<RasaFilterCube> rasa_filtercube(new RasaFilterCube());
37
43
45{
46
47 return "Wanderer FilterCube";
48}
49
51{
53
54 if (serialConnection != nullptr)
56
57 FilterSlotNP[0].setMin(1);
58 FilterSlotNP[0].setMax(FILTERCUBE_MAX_FILTERS);
59
60 IUFillNumber(&DeviceIDN[0], "DEVICE_ID", "ID", "%0.0f", 0, 10, 1, 0);
61 IUFillNumberVector(&DeviceIDNP, DeviceIDN, 1, getDeviceName(),
62 "DEVICE_ID", "Device ID", OPTIONS_TAB, IP_RW, 60, IPS_IDLE);
63
64 IUFillText(&DeviceInfoT[0], "MODEL", "Model", "");
65 IUFillText(&DeviceInfoT[1], "FIRMWARE", "Firmware", "");
66 IUFillTextVector(&DeviceInfoTP, DeviceInfoT, 2, getDeviceName(),
67 "DEVICE_INFO", "Device Info", OPTIONS_TAB, IP_RO, 60, IPS_IDLE);
68
70
71 return true;
72}
73
75{
77
78 if (isConnected())
79 {
80 defineProperty(&DeviceIDNP);
81 DeviceIDN[0].value = mDeviceID;
82 DeviceIDNP.s = IPS_OK;
83 IDSetNumber(&DeviceIDNP, nullptr);
84 defineProperty(&DeviceInfoTP);
85 }
86 else
87 {
88 deleteProperty(DeviceIDNP.name);
89 deleteProperty(DeviceInfoTP.name);
90 }
91
92 return true;
93}
94
96{
98 for (int i = 0; i < FILTERCUBE_MAX_FILTERS; i++)
99 {
100 char filterName[32];
101 char filterLabel[16];
102 char filterText[4];
103 snprintf(filterName, sizeof(filterName), "FILTER_SLOT_NAME_%d", i + 1);
104 snprintf(filterLabel, sizeof(filterLabel), "Filter %d", i + 1);
105 snprintf(filterText, sizeof(filterText), "%c", mFilterLetters[i]);
106 INDI::WidgetText oneWidget;
107
108 oneWidget.fill(filterName, filterLabel, filterText);
109 FilterNameTP.push(std::move(oneWidget));
110 }
111 FilterNameTP.fill(getDeviceName(), "FILTER_NAME", "Filter",
114 FilterSlotNP[0].setMax(FilterNameTP.count());
115 return true;
116}
117
119{
120 if (isSimulation())
121 return true;
122
123 bool allOk = true;
124 for (int i = 0; i < FILTERCUBE_MAX_FILTERS; i++)
125 {
126 const char *txt = FilterNameTP[i].getText();
127 if (txt == nullptr)
128 continue;
129
130 const char *p = txt;
131 while (*p == ' ' || *p == '\t')
132 p++;
133 char c = *p;
134 if (c >= 'a' && c <= 'z')
135 c -= 32;
136 char after = p[1];
137 bool singleLetter = (c >= 'B' && c <= 'Z') &&
138 (after == '\0' || after == ' ' || after == '\t' || after == '\r' || after == '\n');
139
140 if (!singleLetter)
141 {
142
143 LOGF_ERROR("Filter %d name '%s' invalid — must be a single letter (B-Z).", i + 1, txt);
144
145 char orig[2] = { mFilterLetters[i] ? mFilterLetters[i] : 'X', '\0' };
146 FilterNameTP[i].setText(orig);
147 allOk = false;
148 continue;
149 }
150
151 char norm[2] = { c, '\0' };
152 FilterNameTP[i].setText(norm);
153
154 int code = (161 + i) * 10000 + (c - 'A' + 1);
155 char cmd[16] = {0};
156 snprintf(cmd, sizeof(cmd), "%d", code);
157
158 if (!sendCommand(cmd, nullptr, 0, FILTERCUBE_TIMEOUT))
159 {
160 LOGF_ERROR("Failed to set filter %d name to %c.", i + 1, c);
161 allOk = false;
162 continue;
163 }
164
165 mFilterLetters[i] = c;
166 LOGF_INFO("Filter %d name set to %c.", i + 1, c);
167 }
168
169 if (!allOk)
170 {
172 FilterNameTP.apply("Invalid filter name(s): must be a single letter (B-Z). Invalid entries reverted.");
173 }
174 return true;
175}
176
178{
179 if (isSimulation())
180 return true;
181
182 tcflush(PortFD, TCIOFLUSH);
183
184 int current = 0;
185 char field[64] = {0};
186 int rc = 0, nbytes_read = 0;
187
188 constexpr int maxResync = 32;
189 bool foundModel = false;
190
191 for (int skip = 0; skip < maxResync; ++skip)
192 {
193 if ((rc = tty_read_section(PortFD, field, 'A', FILTERCUBE_STATUS_TIMEOUT, &nbytes_read)) != TTY_OK)
194 {
195 LOG_ERROR("Failed to read from RASA FilterCube during handshake.");
196 return false;
197 }
198
199 while (nbytes_read > 0 && (field[nbytes_read - 1] == 'A' || field[nbytes_read - 1] == '\r' || field[nbytes_read - 1] == '\n'))
200 {
201 nbytes_read--;
202 field[nbytes_read] = '\0';
203 }
204
205 char *p = field;
206 while (*p == ' ' || *p == '\t')
207 p++;
208 if (p != field)
209 std::memmove(field, p, std::strlen(p) + 1);
210
211 if (strncasecmp(field, FILTERCUBE_MODEL, std::strlen(FILTERCUBE_MODEL)) == 0)
212 {
213 snprintf(mModel, sizeof(mModel), "%s", field);
214 foundModel = true;
215 break;
216 }
217 }
218
219 if (!foundModel)
220 {
221 LOG_ERROR("RASA FilterCube model identifier (WFC50) not found.");
222 return false;
223 }
224
225 if ((rc = tty_read_section(PortFD, field, 'A', FILTERCUBE_STATUS_TIMEOUT, &nbytes_read)) != TTY_OK)
226 {
227 LOG_ERROR("Failed to read firmware version from RASA FilterCube.");
228 return false;
229 }
230 while (nbytes_read > 0 && (field[nbytes_read - 1] == 'A' || field[nbytes_read - 1] == '\r' || field[nbytes_read - 1] == '\n'))
231 {
232 nbytes_read--;
233 field[nbytes_read] = '\0';
234 }
235 snprintf(mFirmware, sizeof(mFirmware), "%s", field);
236
237 double fwVersion = std::atof(mFirmware);
238 if (fwVersion < FILTERCUBE_MIN_FIRMWARE)
239 {
240 LOGF_ERROR("RASA FilterCube firmware %.0f is too old (minimum: %d). "
241 "Please update the firmware in WandererEmpire.",
242 fwVersion, (int)FILTERCUBE_MIN_FIRMWARE);
243 return false;
244 }
245 LOGF_INFO("RASA FilterCube firmware: %s", mFirmware);
246
247 if ((rc = tty_read_section(PortFD, field, 'A', FILTERCUBE_STATUS_TIMEOUT, &nbytes_read)) != TTY_OK)
248 {
249 LOG_ERROR("Failed to read current filter position from RASA FilterCube.");
250 return false;
251 }
252 while (nbytes_read > 0 && (field[nbytes_read - 1] == 'A' || field[nbytes_read - 1] == '\r' || field[nbytes_read - 1] == '\n'))
253 {
254 nbytes_read--;
255 field[nbytes_read] = '\0';
256 }
257 current = std::atoi(field);
258 if (current >= 1 && current <= FILTERCUBE_MAX_FILTERS)
259 {
260 CurrentFilter = current;
261 FilterSlotNP[0].setValue(CurrentFilter);
262 }
263
264 char namesField[16] = {0};
265 if ((rc = tty_read_section(PortFD, namesField, 'A', FILTERCUBE_STATUS_TIMEOUT, &nbytes_read)) != TTY_OK)
266 {
267 LOG_WARN("Failed to read filter names from RASA FilterCube; using defaults.");
268 }
269 else
270 {
271 while (nbytes_read > 0 && (namesField[nbytes_read - 1] == 'A' || namesField[nbytes_read - 1] == '\r' || namesField[nbytes_read - 1] == '\n'))
272 {
273 nbytes_read--;
274 namesField[nbytes_read] = '\0';
275 }
276 if (nbytes_read >= FILTERCUBE_MAX_FILTERS)
277 {
278 for (int i = 0; i < FILTERCUBE_MAX_FILTERS; i++)
279 mFilterLetters[i] = namesField[i];
280 LOGF_DEBUG("Filter names: %c %c %c %c %c",
281 mFilterLetters[0], mFilterLetters[1], mFilterLetters[2],
282 mFilterLetters[3], mFilterLetters[4]);
283 }
284 }
285
286 for (int i = 0; i < FILTERCUBE_MAX_FILTERS; i++)
287 {
288 if ((rc = tty_read_section(PortFD, field, 'A', FILTERCUBE_STATUS_TIMEOUT, &nbytes_read)) != TTY_OK)
289 {
290 LOGF_WARN("Failed to read offset %d from RASA FilterCube.", i + 1);
291 break;
292 }
293 }
294
295 if ((rc = tty_read_section(PortFD, field, 'A', FILTERCUBE_STATUS_TIMEOUT, &nbytes_read)) != TTY_OK)
296 {
297 LOG_WARN("Failed to read device ID from RASA FilterCube.");
298 }
299 else
300 {
301 while (nbytes_read > 0 && (field[nbytes_read - 1] == 'A' || field[nbytes_read - 1] == '\r' || field[nbytes_read - 1] == '\n'))
302 {
303 nbytes_read--;
304 field[nbytes_read] = '\0';
305 }
306 mDeviceID = std::atoi(field);
307 }
308
309 LOGF_INFO("RASA FilterCube connected - Firmware: %s, ID: %d, Position: %d",
310 mFirmware, mDeviceID, CurrentFilter);
311
312 IUSaveText(&DeviceInfoT[0], mModel);
313 IUSaveText(&DeviceInfoT[1], mFirmware);
314 DeviceInfoTP.s = IPS_OK;
315 IDSetText(&DeviceInfoTP, nullptr);
316
317 return true;
318}
319
321{
322
323 return CurrentFilter;
324}
325
327{
328 if (position == CurrentFilter)
329 {
331 return true;
332 }
333
334 if (position < 1 || position > FILTERCUBE_MAX_FILTERS)
335 {
336 LOGF_ERROR("Requested filter position %d is out of range (1-%d).", position, FILTERCUBE_MAX_FILTERS);
337 return false;
338 }
339
340 TargetFilter = position;
341
342 if (isSimulation())
343 {
345 FilterSlotNP[0].setValue(CurrentFilter);
347 FilterSlotNP.apply("Selected filter position reached (simulation).");
349 return true;
350 }
351
352 char cmd[16] = {0};
353 snprintf(cmd, sizeof(cmd), "%d", 2000 + TargetFilter);
354
355 if (!sendCommand(cmd, nullptr, 0, FILTERCUBE_TIMEOUT))
356 {
357 LOG_ERROR("Failed to send move command to RASA FilterCube.");
358 return false;
359 }
360
361 tcflush(PortFD, TCIFLUSH);
362
363 int attempts = 0;
364 constexpr int maxAttempts = 150;
365
366 while (attempts++ < maxAttempts)
367 {
368 int current = 0;
369 if (readCurrentFilterFromStatus(current))
370 {
371 LOGF_DEBUG("SelectFilter: current=%d target=%d attempt %d", current, TargetFilter, attempts);
372 if (current == TargetFilter)
373 {
374 CurrentFilter = current;
375 FilterSlotNP[0].setValue(CurrentFilter);
377 FilterSlotNP.apply("Selected filter position reached");
379 return true;
380 }
381 }
382 usleep(200 * 1000);
383 }
384
385 LOG_ERROR("Timed out waiting for RASA FilterCube to reach target position.");
386 return false;
387}
388
389bool RasaFilterCube::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
390{
391 if (dev != nullptr && std::strcmp(dev, getDeviceName()) == 0)
392 {
393
394 if (std::strcmp(DeviceIDNP.name, name) == 0)
395 {
396 IUUpdateNumber(&DeviceIDNP, values, names, n);
397
398 int id = static_cast<int>(DeviceIDN[0].value);
399 if (id < 0 || id > 10)
400 {
401 LOG_ERROR("Device ID must be 0-10.");
402 DeviceIDNP.s = IPS_ALERT;
403 IDSetNumber(&DeviceIDNP, nullptr);
404 return true;
405 }
406
407 char cmd[16] = {0};
408 snprintf(cmd, sizeof(cmd), "%d", 1900000 + id);
409
410 if (!sendCommand(cmd, nullptr, 0, FILTERCUBE_TIMEOUT))
411 {
412 LOG_ERROR("Failed to set device ID.");
413 DeviceIDNP.s = IPS_ALERT;
414 IDSetNumber(&DeviceIDNP, nullptr);
415 return true;
416 }
417
418 mDeviceID = id;
419 DeviceIDNP.s = IPS_OK;
420 IDSetNumber(&DeviceIDNP, nullptr);
421 LOGF_INFO("Device ID set to %d.", id);
422 return true;
423 }
424 }
425
426 return INDI::FilterWheel::ISNewNumber(dev, name, values, names, n);
427}
428
429bool RasaFilterCube::sendCommand(const char *command, char *response, int responseLen, int timeoutSeconds)
430{
431 int nbytes_written = 0;
432 int nbytes_read = 0;
433 int rc;
434
435 if (PortFD < 0)
436 return false;
437
438 char line[48];
439
440 const int linelen = std::snprintf(line, sizeof(line), "%s\n", command);
441 if (linelen <= 0 || linelen >= static_cast<int>(sizeof(line)))
442 {
443 LOG_ERROR("RASA FilterCube command too long for transmit buffer.");
444 return false;
445 }
446
447 if (response != nullptr && responseLen > 1)
448 tcflush(PortFD, TCIFLUSH);
449
450 if ((rc = tty_write(PortFD, line, linelen, &nbytes_written)) != TTY_OK)
451 {
452 char errstr[MAXRBUF] = {0};
453 tty_error_msg(rc, errstr, MAXRBUF);
454 LOGF_ERROR("Error sending command to RASA FilterCube: %s", errstr);
455 return false;
456 }
457
458 if (response == nullptr || responseLen <= 1)
459 return true;
460
461 rc = tty_read_section(PortFD, response, '\r', timeoutSeconds, &nbytes_read);
462 if (rc != TTY_OK)
463 rc = tty_read_section(PortFD, response, '\n', timeoutSeconds, &nbytes_read);
464 if (rc != TTY_OK)
465 {
466 response[0] = '\0';
467 return true;
468 }
469
470 while (nbytes_read > 0 && (response[nbytes_read - 1] == '\r' || response[nbytes_read - 1] == '\n'))
471 {
472 nbytes_read--;
473 response[nbytes_read] = '\0';
474 }
475 if (nbytes_read >= responseLen)
476 nbytes_read = responseLen - 1;
477 response[nbytes_read] = '\0';
478
479 return true;
480}
481
482namespace
483{
484void filtercubeStripField(char *field, int &nbytes_read)
485{
486 while (nbytes_read > 0)
487 {
488 char c = field[nbytes_read - 1];
489 if (c == 'A' || c == '\r' || c == '\n')
490 {
491 nbytes_read--;
492 field[nbytes_read] = '\0';
493 }
494 else
495 break;
496 }
497 field[nbytes_read] = '\0';
498}
499
500void filtercubeTrimLeft(char *s)
501{
502 char *p = s;
503 while (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n')
504 p++;
505 if (p != s)
506 std::memmove(s, p, std::strlen(p) + 1);
507}
508}
509
510bool RasaFilterCube::readCurrentFilterFromStatus(int &position)
511{
512 int rc = -1;
513 int nbytes_read = 0;
514 char field[64] = {0};
515
516 if (PortFD < 0)
517 return false;
518
519 constexpr int maxResync = 32;
520
521 for (int skip = 0; skip < maxResync; ++skip)
522 {
523 if ((rc = tty_read_section(PortFD, field, 'A', FILTERCUBE_STATUS_TIMEOUT, &nbytes_read)) != TTY_OK)
524 return false;
525 filtercubeStripField(field, nbytes_read);
526
527 filtercubeTrimLeft(field);
528 if (strncasecmp(field, FILTERCUBE_MODEL, std::strlen(FILTERCUBE_MODEL)) == 0)
529 {
530
531 if ((rc = tty_read_section(PortFD, field, 'A', FILTERCUBE_STATUS_TIMEOUT, &nbytes_read)) != TTY_OK)
532 return false;
533
534 if ((rc = tty_read_section(PortFD, field, 'A', FILTERCUBE_STATUS_TIMEOUT, &nbytes_read)) != TTY_OK)
535 return false;
536 filtercubeStripField(field, nbytes_read);
537
538 int current = std::atoi(field);
539 LOGF_DEBUG("status: position field='%s' -> %d", field, current);
540 if (current >= 1 && current <= FILTERCUBE_MAX_FILTERS)
541 {
542 position = current;
543 return true;
544 }
545 return false;
546 }
547 }
548
549 return false;
550}
551
void setDefaultBaudRate(BaudRate newRate)
setDefaultBaudRate Set default baud rate. The default baud rate is 9600 unless otherwise changed by t...
bool isConnected() 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)
bool isSimulation() const
void addAuxControls()
Add Debug, Simulation, and Configuration options to the driver.
INDI::PropertyNumber FilterSlotNP
INDI::PropertyText FilterNameTP
void SelectFilterDone(int newpos)
The child class calls this function when the hardware successfully finished selecting a new filter wh...
virtual bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
Connection::Serial * serialConnection
virtual bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
void setFilterConnection(const uint8_t &value)
setFilterConnection Set Filter connection mode. Child class should call this in the constructor befor...
virtual bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
int PortFD
For Serial & TCP connections.
void push(WidgetView< T > &&item)
void setState(IPState state)
void apply(const char *format,...) const ATTRIBUTE_FORMAT_PRINTF(2
void resize(size_t size)
const char * getGroupName() const
void fill(const char *device, const char *name, const char *label, const char *group, IPerm permission, double timeout, IPState state)
INDI::PropertyViewText * getText() const
bool ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n) override
Process the client newNumber command.
bool Handshake() override
perform handshake with device to check communication
bool initProperties() override
Initialize properties initial state and value. The child class must implement this function.
bool GetFilterNames() override
Obtains a list of filter names from the hardware and initializes the FilterNameTP property....
bool updateProperties() override
updateProperties is called whenever there is a change in the CONNECTION status of the driver....
bool SelectFilter(int position) override
Select a new filter position.
bool SetFilterNames() override
Set filter names as defined by the client for each filter position. The desired filter names are stor...
const char * getDefaultName() override
int QueryFilter() override
Return current filter position.
const char * OPTIONS_TAB
OPTIONS_TAB Where all the driver's options are located. Those may include auxiliary controls,...
@ IP_RW
Definition indiapi.h:184
@ IP_RO
Definition indiapi.h:182
@ IPS_ALERT
Definition indiapi.h:162
@ IPS_IDLE
Definition indiapi.h:159
@ IPS_OK
Definition indiapi.h:160
int tty_read_section(int fd, char *buf, char stop_char, int timeout, int *nbytes_read)
read buffer from terminal with a delimiter
Definition indicom.c:653
int tty_write(int fd, const char *buf, int nbytes, int *nbytes_written)
Writes a buffer to fd.
Definition indicom.c:418
void tty_error_msg(int err_code, char *err_msg, int err_msg_len)
Retrieve the tty error message.
Definition indicom.c:1243
Implementations for common driver routines.
@ TTY_OK
Definition indicom.h:54
void IUFillNumberVector(INumberVectorProperty *nvp, INumber *np, int nnp, const char *dev, const char *name, const char *label, const char *group, IPerm p, double timeout, IPState s)
Assign attributes for a number vector property. The vector's auxiliary elements will be set to NULL.
Definition indidevapi.c:490
void IUFillTextVector(ITextVectorProperty *tvp, IText *tp, int ntp, const char *dev, const char *name, const char *label, const char *group, IPerm p, double timeout, IPState s)
Assign attributes for a text vector property. The vector's auxiliary elements will be set to NULL.
Definition indidevapi.c:509
void IUSaveText(IText *tp, const char *newtext)
Function to reliably save new text in a IText.
Definition indidevapi.c:44
void IUFillText(IText *tp, const char *name, const char *label, const char *initialText)
Assign attributes for a text property. The text's auxiliary elements will be set to NULL.
Definition indidevapi.c:416
void IUFillNumber(INumber *np, const char *name, const char *label, const char *format, double min, double max, double step, double value)
Assign attributes for a number property. The number's auxiliary elements will be set to NULL.
Definition indidevapi.c:398
void IDSetNumber(const INumberVectorProperty *nvp, const char *fmt,...)
#define MAXRBUF
Definition indidriver.c:52
int IUUpdateNumber(INumberVectorProperty *nvp, double values[], char *names[], int n)
Update all numbers in a number vector property.
void IDSetText(const ITextVectorProperty *tvp, const char *fmt,...)
#define LOGF_INFO(fmt,...)
Definition indilogger.h:82
#define LOGF_WARN(fmt,...)
Definition indilogger.h:81
#define LOG_WARN(txt)
Definition indilogger.h:73
#define LOGF_DEBUG(fmt,...)
Definition indilogger.h:83
#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 getDeviceName
uint8_t cmd[4]
Definition pwc-ioctl.h:2
#define FILTERCUBE_TIMEOUT
#define FILTERCUBE_MIN_FIRMWARE
#define FILTERCUBE_MODEL
#define FILTERCUBE_STATUS_TIMEOUT
void fill(const char *name, const char *label, const char *initialText)
char name[MAXINDINAME]
Definition indiapi.h:321
char name[MAXINDINAME]
Definition indiapi.h:248