1 /*
2 * Stellarium TelescopeControl Plug-in
3 *
4 * Copyright (C) 2009-2011 Bogdan Marinov (this file)
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program 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
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
19 */
20
21 #include "TelescopeConfigurationDialog.hpp"
22 #include "Dialog.hpp"
23 #include "StelApp.hpp"
24 #include "StelCore.hpp"
25 #include "StelFileMgr.hpp"
26 #include "StelLocaleMgr.hpp"
27 #include "StelModuleMgr.hpp"
28 #include "StelStyle.hpp"
29 #include "StelTranslator.hpp"
30 #include "TelescopeControl.hpp"
31 #include "ui_telescopeConfigurationDialog.h"
32
33 #ifdef Q_OS_WIN
34 #include "../common/ASCOMSupport.hpp"
35 #endif
36
37 #include <QCompleter>
38 #include <QDebug>
39 #include <QDir>
40 #include <QFile>
41 #include <QFrame>
42 #include <QTimer>
43 #include <QRegularExpression>
44 #include <QtSerialPort/QSerialPortInfo>
45
TelescopeConfigurationDialog()46 TelescopeConfigurationDialog::TelescopeConfigurationDialog()
47 : StelDialog("TelescopeControlConfiguration"), configuredSlot(0)
48 {
49 ui = new Ui_telescopeConfigurationDialog();
50
51 telescopeManager = GETSTELMODULE(TelescopeControl);
52
53 telescopeNameValidator = new QRegularExpressionValidator(QRegularExpression("[^:\"]+"), this); // Test the update for JSON
54 hostNameValidator =
55 new QRegularExpressionValidator(QRegularExpression("[a-zA-Z0-9\\-\\.]+"), this); // TODO: Write a proper host/IP regexp?
56 circleListValidator = new QRegularExpressionValidator(QRegularExpression("[0-9,\\.\\s]+"), this);
57 #ifdef Q_OS_WIN
58 serialPortValidator = new QRegularExpressionValidator(QRegularExpression("COM[0-9]+"), this);
59 #else
60 serialPortValidator = new QRegularExpressionValidator(QRegularExpression("/.*"), this);
61 #endif
62 }
63
~TelescopeConfigurationDialog()64 TelescopeConfigurationDialog::~TelescopeConfigurationDialog()
65 {
66 delete ui;
67 delete telescopeNameValidator;
68 delete hostNameValidator;
69 delete circleListValidator;
70 delete serialPortValidator;
71 }
72
listSerialPorts()73 QStringList* TelescopeConfigurationDialog::listSerialPorts()
74 {
75 // list real serial ports
76 QStringList* plist = new QStringList();
77 for (const auto& serialPortInfo : QSerialPortInfo::availablePorts())
78 {
79 #ifdef Q_OS_WIN
80 plist->append(serialPortInfo.portName()); // Use COM1 in the GUI instead \\.\COM1 naming
81 #else
82 plist->append(serialPortInfo.systemLocation());
83 #endif
84 qDebug() << "[TelescopeControl] port name:" << serialPortInfo.portName()
85 << "; vendor identifier:" << serialPortInfo.vendorIdentifier()
86 << "; product identifier:" << serialPortInfo.productIdentifier();
87 }
88
89 // on linux find some virtual ports
90 #ifdef Q_OS_LINUX
91 QStringList filters;
92 filters << "ttyNET*"
93 << "ttynet*"
94 << "Telescope*";
95 // look in /dev/*
96 QDir dev("/dev");
97 dev.setFilter(QDir::System);
98 dev.setSorting(QDir::Reversed);
99 dev.setNameFilters(filters);
100 QFileInfoList list = dev.entryInfoList();
101 for (int i = 0; i < list.size(); i++)
102 {
103 QFileInfo fileInfo = list.at(i);
104 plist->append(fileInfo.absoluteFilePath());
105 }
106 // look in /tmp/* for non-root virtual ports (append ttyS8 and ttyUSB*)
107 filters << "ttyS*"
108 << "ttyUSB*";
109 QDir tmp("/tmp");
110 tmp.setFilter(QDir::System);
111 tmp.setSorting(QDir::Reversed);
112 tmp.setNameFilters(filters);
113 list = tmp.entryInfoList();
114 for (int i = 0; i < list.size(); i++)
115 {
116 QFileInfo fileInfo = list.at(i);
117 plist->append(fileInfo.absoluteFilePath());
118 }
119 #endif
120
121 return plist;
122 }
123
retranslate()124 void TelescopeConfigurationDialog::retranslate()
125 {
126 if (dialog)
127 {
128 ui->retranslateUi(dialog);
129 populateToolTips();
130 }
131 }
132
133 // Initialize the dialog widgets and connect the signals/slots
createDialogContent()134 void TelescopeConfigurationDialog::createDialogContent()
135 {
136 ui->setupUi(dialog);
137
138 // ASCOM Telescope client widget needs to be dynamically added in order to make use of preprocessors to exclude for non-windows
139 #ifdef Q_OS_WIN
140 ascomWidget = new TelescopeClientASCOMWidget(ui->scrollAreaWidgetContents);
141 ui->ASCOMLayout->addWidget(ascomWidget);
142
143 if (!ASCOMSupport::isASCOMSupported())
144 {
145 ui->radioButtonTelescopeASCOM->hide();
146 }
147 #endif
148
149 // Inherited connect
150 connect(&StelApp::getInstance(), SIGNAL(languageChanged()), this, SLOT(retranslate()));
151 connect(ui->closeStelWindow, SIGNAL(clicked()), this, SLOT(buttonDiscardPressed()));
152 connect(ui->TitleBar, SIGNAL(movedTo(QPoint)), this, SLOT(handleMovedTo(QPoint)));
153 connect(dialog, SIGNAL(rejected()), this, SLOT(buttonDiscardPressed()));
154
155 // Connect: sender, signal, receiver, member
156 connect(ui->radioButtonTelescopeLocal, SIGNAL(toggled(bool)), this, SLOT(toggleTypeLocal(bool)));
157 connect(ui->radioButtonTelescopeConnection, SIGNAL(toggled(bool)), this, SLOT(toggleTypeConnection(bool)));
158 connect(ui->radioButtonTelescopeVirtual, SIGNAL(toggled(bool)), this, SLOT(toggleTypeVirtual(bool)));
159 connect(ui->radioButtonTelescopeRTS2, SIGNAL(toggled(bool)), this, SLOT(toggleTypeRTS2(bool)));
160 connect(ui->radioButtonTelescopeINDI, SIGNAL(toggled(bool)), this, SLOT(toggleTypeINDI(bool)));
161 #ifdef Q_OS_WIN
162 connect(ui->radioButtonTelescopeASCOM, SIGNAL(toggled(bool)), this, SLOT(toggleTypeASCOM(bool)));
163 #else
164 ui->radioButtonTelescopeASCOM->hide();
165 #endif
166
167 connect(ui->pushButtonSave, SIGNAL(clicked()), this, SLOT(buttonSavePressed()));
168 connect(ui->pushButtonDiscard, SIGNAL(clicked()), this, SLOT(buttonDiscardPressed()));
169
170 connect(ui->comboBoxDeviceModel, SIGNAL(currentIndexChanged(const QString&)), this,
171 SLOT(deviceModelSelected(const QString&)));
172
173 // Setting validators
174 ui->lineEditTelescopeName->setValidator(telescopeNameValidator);
175 ui->lineEditHostName->setValidator(hostNameValidator);
176 ui->lineEditCircleList->setValidator(circleListValidator);
177 ui->comboSerialPort->setValidator(serialPortValidator);
178
179 populateToolTips();
180 }
181
populateToolTips()182 void TelescopeConfigurationDialog::populateToolTips()
183 {
184 ui->doubleSpinBoxTelescopeDelay->setToolTip(
185 QString("<p>%1</p>")
186 .arg(q_("The approximate time it takes for the signals from the telescope to reach Stellarium. "
187 "Increase this value if the reticle is skipping.")));
188 ui->doubleSpinBoxRTS2Refresh->setToolTip(
189 QString("<p>%1</p>")
190 .arg(q_("Refresh rate of the RTS2 telescope. Delay before sending next telescope status request. The "
191 "default value of 0.5 second works fine with most setups.")));
192 }
193
194 // Set the configuration panel in a predictable state
initConfigurationDialog()195 void TelescopeConfigurationDialog::initConfigurationDialog()
196 {
197 ui->groupBoxConnectionSettings->hide();
198 ui->groupBoxDeviceSettings->hide();
199 ui->groupBoxRTS2Settings->hide();
200 ui->INDIProperties->hide();
201 #ifdef Q_OS_WIN
202 ascomWidget->hide();
203 #endif
204
205 // Reusing code used in both methods that call this one
206 deviceModelNames = telescopeManager->getDeviceModels().keys();
207
208 // Name
209 ui->lineEditTelescopeName->clear();
210
211 // Equinox
212 ui->radioButtonJ2000->setChecked(true);
213
214 // Connect at startup
215 ui->checkBoxConnectAtStartup->setChecked(false);
216
217 // Serial port
218 QStringList* plist = listSerialPorts();
219 ui->comboSerialPort->clear();
220 ui->comboSerialPort->addItems(*plist);
221 ui->comboSerialPort->activated(plist->value(0));
222 ui->comboSerialPort->setEditText(plist->value(0));
223 delete (plist);
224
225 // Populating the list of available devices
226 ui->comboBoxDeviceModel->clear();
227 if (!deviceModelNames.isEmpty())
228 {
229 deviceModelNames.sort();
230 ui->comboBoxDeviceModel->addItems(deviceModelNames);
231 }
232 ui->comboBoxDeviceModel->setCurrentIndex(0);
233
234 // FOV circles
235 ui->checkBoxCircles->setChecked(false);
236 ui->lineEditCircleList->clear();
237
238 // It is very unlikely that this situation will happen any more due to the
239 // embedded telescope servers.
240 if (deviceModelNames.isEmpty())
241 {
242 ui->radioButtonTelescopeLocal->setEnabled(false);
243 ui->radioButtonTelescopeConnection->setChecked(true);
244 toggleTypeConnection(true); // Not called if the button is already checked
245 }
246 else
247 {
248 ui->radioButtonTelescopeLocal->setEnabled(true);
249 ui->radioButtonTelescopeLocal->setChecked(true);
250 toggleTypeLocal(true); // Not called if the button is already checked
251 }
252 }
253
initNewTelescopeConfiguration(int slot)254 void TelescopeConfigurationDialog::initNewTelescopeConfiguration(int slot)
255 {
256 configuredSlot = slot;
257 initConfigurationDialog();
258 ui->stelWindowTitle->setText(q_("Add New Telescope"));
259 ui->lineEditTelescopeName->setText(QString("New Telescope %1").arg(QString::number(configuredSlot)));
260
261 ui->doubleSpinBoxTelescopeDelay->setValue(SECONDS_FROM_MICROSECONDS(DEFAULT_DELAY));
262 }
263
initExistingTelescopeConfiguration(int slot)264 void TelescopeConfigurationDialog::initExistingTelescopeConfiguration(int slot)
265 {
266 configuredSlot = slot;
267 initConfigurationDialog();
268 ui->stelWindowTitle->setText(q_("Configure Telescope"));
269
270 // Read the telescope properties
271 QString name;
272 ConnectionType connectionType;
273 QString equinox;
274 QString host;
275 int portTCP;
276 int delay;
277 bool connectAtStartup;
278 QList<double> circles;
279 QString deviceModelName;
280 QString serialPortName;
281 QString rts2Url;
282 QString rts2Username;
283 QString rts2Password;
284 int rts2Refresh;
285 QString ascomDeviceId;
286 bool ascomUseDeviceEqCoordType;
287
288 if (!telescopeManager->getTelescopeAtSlot(slot, connectionType, name, equinox, host, portTCP, delay,
289 connectAtStartup, circles, deviceModelName, serialPortName, rts2Url, rts2Username, rts2Password,
290 rts2Refresh, ascomDeviceId, ascomUseDeviceEqCoordType))
291 {
292 // TODO: Add debug
293 return;
294 }
295 ui->lineEditTelescopeName->setText(name);
296
297 if (connectionType == ConnectionInternal && !deviceModelName.isEmpty())
298 {
299 ui->radioButtonTelescopeLocal->setChecked(true);
300 ui->lineEditHostName->setText("localhost"); // TODO: Remove magic word!
301
302 // Make the current device model selected in the list
303 int index = ui->comboBoxDeviceModel->findText(deviceModelName);
304 if (index < 0)
305 {
306 qDebug() << "TelescopeConfigurationDialog: Current device model is not in the list?";
307 emit changesDiscarded();
308 return;
309 }
310 else
311 ui->comboBoxDeviceModel->setCurrentIndex(index);
312
313 // Initialize the serial port value
314 ui->comboSerialPort->activated(serialPortName);
315 ui->comboSerialPort->setEditText(serialPortName);
316 }
317 else if (connectionType == ConnectionRemote)
318 {
319 ui->radioButtonTelescopeConnection->setChecked(true); // Calls toggleTypeConnection(true)
320 ui->lineEditHostName->setText(host);
321 }
322 else if (connectionType == ConnectionLocal)
323 {
324 ui->radioButtonTelescopeConnection->setChecked(true);
325 ui->lineEditHostName->setText("localhost");
326 }
327 else if (connectionType == ConnectionVirtual)
328 {
329 ui->radioButtonTelescopeVirtual->setChecked(true);
330 }
331 else if (connectionType == ConnectionRTS2)
332 {
333 ui->radioButtonTelescopeRTS2->setChecked(true);
334 ui->lineEditRTS2Url->setText(rts2Url);
335 ui->lineEditRTS2Username->setText(rts2Username);
336 ui->lineEditRTS2Password->setText(rts2Password);
337 ui->doubleSpinBoxRTS2Refresh->setValue(SECONDS_FROM_MICROSECONDS(rts2Refresh));
338 }
339 else if (connectionType == ConnectionINDI)
340 {
341 ui->radioButtonTelescopeINDI->setChecked(true);
342 ui->INDIProperties->setHost(host);
343 ui->INDIProperties->setPort(portTCP);
344 ui->INDIProperties->setSelectedDevice(deviceModelName);
345 }
346 #ifdef Q_OS_WIN
347 else if (connectionType == ConnectionASCOM)
348 {
349 ui->radioButtonTelescopeASCOM->setChecked(true);
350 ascomWidget->setSelectedDevice(ascomDeviceId);
351 ascomWidget->setUseDeviceEqCoordType(ascomUseDeviceEqCoordType);
352 }
353 #endif
354
355 // Equinox
356 if (equinox == "JNow")
357 ui->radioButtonJNow->setChecked(true);
358 else
359 ui->radioButtonJ2000->setChecked(true);
360
361 // Circles
362 if (!circles.isEmpty())
363 {
364 ui->checkBoxCircles->setChecked(true);
365
366 QStringList circleList;
367 for (int i = 0; i < circles.size(); i++)
368 circleList.append(QString::number(circles[i]));
369 ui->lineEditCircleList->setText(circleList.join(", "));
370 }
371
372 // TCP port
373 ui->spinBoxTCPPort->setValue(portTCP);
374
375 // Delay
376 ui->doubleSpinBoxTelescopeDelay->setValue(SECONDS_FROM_MICROSECONDS(delay)); // Microseconds to seconds
377
378 // Connect at startup
379 ui->checkBoxConnectAtStartup->setChecked(connectAtStartup);
380 }
381
toggleTypeLocal(bool isChecked)382 void TelescopeConfigurationDialog::toggleTypeLocal(bool isChecked)
383 {
384 if (isChecked)
385 {
386 // Re-initialize values that may have been changed
387 ui->comboBoxDeviceModel->setCurrentIndex(0);
388 QStringList* plist = listSerialPorts();
389 ui->comboSerialPort->activated(plist->value(0));
390 ui->comboSerialPort->setEditText(plist->value(0));
391 delete (plist);
392 ui->lineEditHostName->setText("localhost");
393 ui->spinBoxTCPPort->setValue(DEFAULT_TCP_PORT_FOR_SLOT(configuredSlot));
394
395 ui->groupBoxDeviceSettings->show();
396
397 ui->scrollArea->ensureWidgetVisible(ui->groupBoxTelescopeProperties);
398 }
399 else
400 {
401 ui->groupBoxDeviceSettings->hide();
402 }
403 }
404
toggleTypeConnection(bool isChecked)405 void TelescopeConfigurationDialog::toggleTypeConnection(bool isChecked)
406 {
407 if (isChecked)
408 {
409 // Re-initialize values that may have been changed
410 ui->lineEditHostName->setText("localhost");
411 ui->spinBoxTCPPort->setValue(DEFAULT_TCP_PORT_FOR_SLOT(configuredSlot));
412
413 ui->groupBoxConnectionSettings->show();
414
415 ui->scrollArea->ensureWidgetVisible(ui->groupBoxTelescopeProperties);
416 }
417 else
418 {
419 ui->groupBoxConnectionSettings->hide();
420 }
421 }
422
toggleTypeVirtual(bool isChecked)423 void TelescopeConfigurationDialog::toggleTypeVirtual(bool isChecked)
424 {
425 Q_UNUSED(isChecked);
426 ui->scrollArea->ensureWidgetVisible(ui->groupBoxTelescopeProperties);
427 }
428
toggleTypeRTS2(bool isChecked)429 void TelescopeConfigurationDialog::toggleTypeRTS2(bool isChecked)
430 {
431 if (isChecked)
432 {
433 // Re-initialize values that may have been changed
434 ui->lineEditRTS2Url->setText("localhost:8889");
435
436 ui->groupBoxRTS2Settings->show();
437
438 ui->scrollArea->ensureWidgetVisible(ui->groupBoxRTS2Settings);
439 }
440 else
441 {
442 ui->groupBoxRTS2Settings->hide();
443 }
444 }
445
toggleTypeINDI(bool enabled)446 void TelescopeConfigurationDialog::toggleTypeINDI(bool enabled)
447 {
448 ui->INDIProperties->setVisible(enabled);
449 }
450
451 #ifdef Q_OS_WIN
toggleTypeASCOM(bool enabled)452 void TelescopeConfigurationDialog::toggleTypeASCOM(bool enabled)
453 {
454 ascomWidget->setVisible(enabled);
455 }
456 #endif
457
buttonSavePressed()458 void TelescopeConfigurationDialog::buttonSavePressed()
459 {
460 // Main telescope properties
461 QString name = ui->lineEditTelescopeName->text().trimmed();
462
463 if (name.isEmpty()) return;
464
465 QString host = ui->lineEditHostName->text();
466
467 if (host.isEmpty()) // Remove validation of hostname
468 return;
469
470 int delay = qRound(MICROSECONDS_FROM_SECONDS(ui->doubleSpinBoxTelescopeDelay->value()));
471 int portTCP = ui->spinBoxTCPPort->value();
472 bool connectAtStartup = ui->checkBoxConnectAtStartup->isChecked();
473
474 // Circles
475 // TODO: This will change if there is a validator for that field
476 QList<double> circles;
477 QString rawCircles = ui->lineEditCircleList->text().trimmed();
478 QStringList circleStrings;
479 if (ui->checkBoxCircles->isChecked() && !(rawCircles.isEmpty()))
480 {
481 #if (QT_VERSION>=QT_VERSION_CHECK(5, 14, 0))
482 circleStrings = rawCircles.simplified().remove(' ').split(',', Qt::SkipEmptyParts);
483 #else
484 circleStrings = rawCircles.simplified().remove(' ').split(',', QString::SkipEmptyParts);
485 #endif
486 circleStrings.removeDuplicates();
487 circleStrings.sort();
488
489 for (int i = 0; i < circleStrings.size(); i++)
490 {
491 if (i >= MAX_CIRCLE_COUNT) break;
492 double circle = circleStrings.at(i).toDouble();
493 if (circle > 0.0) circles.append(circle);
494 }
495 }
496
497 QString equinox("J2000");
498 if (ui->radioButtonJNow->isChecked()) equinox = "JNow";
499
500 // Type and server properties
501 // TODO: When adding, check for success!
502 ConnectionType type = ConnectionNA;
503 if (ui->radioButtonTelescopeLocal->isChecked())
504 {
505 // Read the serial port
506 QString serialPortName = ui->comboSerialPort->currentText();
507 type = ConnectionInternal;
508 telescopeManager->addTelescopeAtSlot(configuredSlot, type, name, equinox, host, portTCP, delay,
509 connectAtStartup, circles, ui->comboBoxDeviceModel->currentText(), serialPortName);
510 }
511 else if (ui->radioButtonTelescopeConnection->isChecked())
512 {
513 if (host == "localhost")
514 type = ConnectionLocal;
515 else
516 type = ConnectionRemote;
517 telescopeManager->addTelescopeAtSlot(
518 configuredSlot, type, name, equinox, host, portTCP, delay, connectAtStartup, circles);
519 }
520 else if (ui->radioButtonTelescopeVirtual->isChecked())
521 {
522 type = ConnectionVirtual;
523 telescopeManager->addTelescopeAtSlot(
524 configuredSlot, type, name, equinox, QString(), portTCP, delay, connectAtStartup, circles);
525 }
526 else if (ui->radioButtonTelescopeRTS2->isChecked())
527 {
528 type = ConnectionRTS2;
529 telescopeManager->addTelescopeAtSlot(configuredSlot, type, name, equinox, host, portTCP, delay,
530 connectAtStartup, circles, QString(), QString(), ui->lineEditRTS2Url->text(),
531 ui->lineEditRTS2Username->text(), ui->lineEditRTS2Password->text(),
532 qRound(MICROSECONDS_FROM_SECONDS(ui->doubleSpinBoxRTS2Refresh->value())));
533 }
534 else if (ui->radioButtonTelescopeINDI->isChecked())
535 {
536 type = ConnectionINDI;
537 telescopeManager->addTelescopeAtSlot(configuredSlot, type, name, equinox, ui->INDIProperties->host(),
538 ui->INDIProperties->port(), delay, connectAtStartup, circles, ui->INDIProperties->selectedDevice());
539 }
540 #ifdef Q_OS_WIN
541 else if (ui->radioButtonTelescopeASCOM->isChecked())
542 {
543 type = ConnectionASCOM;
544 telescopeManager->addTelescopeAtSlot(configuredSlot, type, name, equinox, host, portTCP, delay,
545 connectAtStartup, circles, QString(), QString(), QString(), QString(), QString(), -1,
546 ascomWidget->selectedDevice(), ascomWidget->useDeviceEqCoordType());
547 }
548 #endif
549
550 emit changesSaved(name, type);
551 }
552
buttonDiscardPressed()553 void TelescopeConfigurationDialog::buttonDiscardPressed()
554 {
555 emit changesDiscarded();
556 }
557
deviceModelSelected(const QString & deviceModelName)558 void TelescopeConfigurationDialog::deviceModelSelected(const QString& deviceModelName)
559 {
560 ui->labelDeviceModelDescription->setText(
561 q_(telescopeManager->getDeviceModels().value(deviceModelName).description));
562 ui->doubleSpinBoxTelescopeDelay->setValue(
563 SECONDS_FROM_MICROSECONDS(telescopeManager->getDeviceModels().value(deviceModelName).defaultDelay));
564 }
565