Kstars

serialportassistant.cpp
1/*
2 SPDX-FileCopyrightText: 2019 Jasem Mutlaq <mutlaqja@ikarustech.com>
3
4 SPDX-License-Identifier: GPL-2.0-or-later
5*/
6
7#include <QMovie>
8#include <QCheckBox>
9#include <QJsonDocument>
10#include <QJsonArray>
11#include <QStandardItem>
12#include <QNetworkReply>
13#include <QButtonGroup>
14#include <QRegularExpression>
15#include <QTimer>
16#include <basedevice.h>
17
18#include "ksnotification.h"
19#include "indi/indiwebmanager.h"
20#include "serialportassistant.h"
21#include "ekos_debug.h"
22#include "kspaths.h"
23#include "Options.h"
24
25SerialPortAssistant::SerialPortAssistant(ProfileInfo *profile, QWidget *parent) : QDialog(parent),
26 m_Profile(profile)
27{
28 setupUi(this);
29
30 QPixmap im;
31 if (im.load(KSPaths::locate(QStandardPaths::AppLocalDataLocation, "wzserialportassistant.png")))
32 wizardPix->setPixmap(im);
33 else if (im.load(QDir(QCoreApplication::applicationDirPath() + "/../Resources/kstars").absolutePath() +
34 "/wzserialportassistant.png"))
35 wizardPix->setPixmap(im);
36
37 connect(nextB, &QPushButton::clicked, [this]()
38 {
39 if (m_CurrentDevice)
40 gotoDevicePage(m_CurrentDevice);
41 else if (!m_Devices.empty())
42 gotoDevicePage(m_Devices.first());
43 });
44
45 loadRules();
46
47 connect(rulesView->selectionModel(), &QItemSelectionModel::selectionChanged, [&](const QItemSelection & selected)
48 {
49 clearRuleB->setEnabled(selected.count() > 0);
50 });
51 connect(model.get(), &QStandardItemModel::rowsRemoved, [&]()
52 {
53 clearRuleB->setEnabled(model->rowCount() > 0);
54 });
55 connect(clearRuleB, &QPushButton::clicked, this, &SerialPortAssistant::removeActiveRule);
56
57 displayOnStartupC->setChecked(Options::autoLoadSerialAssistant());
58 connect(displayOnStartupC, &QCheckBox::toggled, [&](bool enabled)
59 {
60 Options::setAutoLoadSerialAssistant(enabled);
61 });
62
63 connect(closeB, &QPushButton::clicked, [&]()
64 {
65 gotoDevicePage(nullptr);
66 close();
67 });
68}
69
70void SerialPortAssistant::addDevice(const QSharedPointer<ISD::GenericDevice> &device)
71{
72 qCDebug(KSTARS_EKOS) << "Serial Port Assistant new device" << device->getDeviceName();
73
74 addDevicePage(device);
75}
76
77void SerialPortAssistant::addDevicePage(const QSharedPointer<ISD::GenericDevice> &device)
78{
79 m_Devices.append(device);
80
81 QWidget *devicePage = new QWidget(this);
82 devicePage->setObjectName(device->getDeviceName());
83
84 QVBoxLayout *layout = new QVBoxLayout(devicePage);
85
86 QLabel *deviceLabel = new QLabel(devicePage);
87 deviceLabel->setText(QString("<h1>%1</h1>").arg(device->getDeviceName()));
88 layout->addWidget(deviceLabel);
89
90 QLabel *instructionsLabel = new QLabel(devicePage);
91 instructionsLabel->setText(
92 i18n("To assign a permanent designation to the device, you need to unplug the device from stellarmate "
93 "then replug it after 1 second. Click on the <b>Start Scan</b> to begin this procedure."));
94 instructionsLabel->setWordWrap(true);
95 layout->addWidget(instructionsLabel);
96
97 QHBoxLayout *actionsLayout = new QHBoxLayout(devicePage);
98 QPushButton *startButton = new QPushButton(i18n("Start Scan"), devicePage);
99 startButton->setObjectName("startButton");
100
101 QPushButton *homeButton = new QPushButton(QIcon::fromTheme("go-home"), i18n("Home"), devicePage);
102 connect(homeButton, &QPushButton::clicked, [&]()
103 {
104 gotoDevicePage(nullptr);
105 });
106
107 QPushButton *skipButton = new QPushButton(i18n("Skip Device"), devicePage);
108 connect(skipButton, &QPushButton::clicked, [this]()
109 {
110 // If we have more devices, go to them one by one
111 if (m_CurrentDevice)
112 {
113 // Check if next index is available
114 int nextIndex = m_Devices.indexOf(m_CurrentDevice) + 1;
115 if (nextIndex < m_Devices.count())
116 {
117 gotoDevicePage(m_Devices[nextIndex]);
118 return;
119 }
120 }
121
122 gotoDevicePage(nullptr);
123 });
124 QCheckBox *hardwareSlotC = new QCheckBox(i18n("Physical Port Mapping"), devicePage);
125 hardwareSlotC->setObjectName("hardwareSlot");
126 hardwareSlotC->setToolTip(
127 i18n("Assign the permanent name based on which physical port the device is plugged to in StellarMate. "
128 "This is useful to distinguish between two identical USB adapters. The device must <b>always</b> be "
129 "plugged into the same port for this to work."));
130 actionsLayout->addItem(new QSpacerItem(10, 10, QSizePolicy::Preferred));
131 actionsLayout->addWidget(startButton);
132 actionsLayout->addWidget(skipButton);
133 actionsLayout->addWidget(hardwareSlotC);
134 actionsLayout->addItem(new QSpacerItem(10, 10, QSizePolicy::Preferred));
135 actionsLayout->addWidget(homeButton);
136 layout->addLayout(actionsLayout);
137
138 QHBoxLayout *animationLayout = new QHBoxLayout(devicePage);
139 QLabel *smAnimation = new QLabel(devicePage);
140 smAnimation->setFixedSize(QSize(360, 203));
141 QMovie *smGIF = new QMovie(":/videos/sm_animation.gif");
142 smAnimation->setMovie(smGIF);
143 smAnimation->setObjectName("animation");
144
145 animationLayout->addItem(new QSpacerItem(10, 10, QSizePolicy::Preferred));
146 animationLayout->addWidget(smAnimation);
147 animationLayout->addItem(new QSpacerItem(10, 10, QSizePolicy::Preferred));
148
149 QButtonGroup *actionGroup = new QButtonGroup(devicePage);
150 actionGroup->setObjectName("actionGroup");
151 actionGroup->setExclusive(false);
152 actionGroup->addButton(startButton);
153 actionGroup->addButton(skipButton);
154 actionGroup->addButton(hardwareSlotC);
155 actionGroup->addButton(homeButton);
156
157 layout->addLayout(animationLayout);
158 //smGIF->start();
159 //smAnimation->hide();
160
161 serialPortWizard->insertWidget(serialPortWizard->count() - 1, devicePage);
162
163 connect(startButton, &QPushButton::clicked, [ = ]()
164 {
165 startButton->setText(i18n("Standby, Scanning..."));
166 for (auto b : actionGroup->buttons())
167 b->setEnabled(false);
168 smGIF->start();
169 scanDevices();
170 });
171}
172
173void SerialPortAssistant::gotoDevicePage(const QSharedPointer<ISD::GenericDevice> &device)
174{
175 int index = m_Devices.indexOf(device);
176
177 // reset to home page
178 if (index < 0)
179 {
180 m_CurrentDevice = nullptr;
181 serialPortWizard->setCurrentIndex(0);
182 return;
183 }
184
185 m_CurrentDevice = device;
186 serialPortWizard->setCurrentIndex(index + 1);
187}
188
189bool SerialPortAssistant::loadRules()
190{
191 QUrl url(QString("http://%1:%2/api/udev/rules").arg(m_Profile->host).arg(m_Profile->INDIWebManagerPort));
192 QJsonDocument json;
193
194 if (INDI::WebManager::getWebManagerResponse(QNetworkAccessManager::GetOperation, url, &json))
195 {
196 QJsonArray array = json.array();
197
198 if (array.isEmpty())
199 return false;
200
201 model.reset(new QStandardItemModel(0, 5, this));
202
203 model->setHeaderData(0, Qt::Horizontal, i18nc("Vendor ID", "VID"));
204 model->setHeaderData(1, Qt::Horizontal, i18nc("Product ID", "PID"));
205 model->setHeaderData(2, Qt::Horizontal, i18n("Link"));
206 model->setHeaderData(3, Qt::Horizontal, i18n("Serial #"));
207 model->setHeaderData(4, Qt::Horizontal, i18n("Hardware Port?"));
208
209
210 // Get all the drivers running remotely
211 for (auto value : array)
212 {
213 QJsonObject rule = value.toObject();
215 QStandardItem *vid = new QStandardItem(rule["vid"].toString());
216 QStandardItem *pid = new QStandardItem(rule["pid"].toString());
217 QStandardItem *link = new QStandardItem(rule["symlink"].toString());
218 QStandardItem *serial = new QStandardItem(rule["serial"].toString());
219 QStandardItem *hardware = new QStandardItem(rule["port"].toString());
220 items << vid << pid << link << serial << hardware;
221 model->appendRow(items);
222 }
223
224 rulesView->setModel(model.get());
225 return true;
226 }
227
228 return false;
229}
230
231bool SerialPortAssistant::removeActiveRule()
232{
233 QUrl url(QString("http://%1:%2/api/udev/remove_rule").arg(m_Profile->host).arg(m_Profile->INDIWebManagerPort));
234
235 QModelIndex index = rulesView->currentIndex();
236 if (index.isValid() == false)
237 return false;
238
239 QStandardItem *symlink = model->item(index.row(), 2);
240 if (symlink == nullptr)
241 return false;
242
243 QJsonObject rule = { {"symlink", symlink->text()} };
245
246 if (INDI::WebManager::getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr, &data))
247 {
248 model->removeRow(index.row());
249 return true;
250 }
251
252 return false;
253}
254
255void SerialPortAssistant::resetCurrentPage()
256{
257 // Reset all buttons
258 QButtonGroup *actionGroup = serialPortWizard->currentWidget()->findChild<QButtonGroup*>("actionGroup");
259 for (auto b : actionGroup->buttons())
260 b->setEnabled(true);
261
262 // Set start button to start scanning
263 QPushButton *startButton = serialPortWizard->currentWidget()->findChild<QPushButton*>("startButton");
264 startButton->setText(i18n("Start Scanning"));
265
266 // Clear animation
267 QLabel *animation = serialPortWizard->currentWidget()->findChild<QLabel*>("animation");
268 animation->movie()->stop();
269 animation->clear();
270}
271
272void SerialPortAssistant::scanDevices()
273{
274 QUrl url(QString("http://%1:%2/api/udev/watch").arg(m_Profile->host).arg(m_Profile->INDIWebManagerPort));
275
276 QNetworkReply *response = manager.get(QNetworkRequest(url));
277
278 // We need to disconnect the device first
279 m_CurrentDevice->Disconnect();
280
281 connect(response, &QNetworkReply::finished, this, &SerialPortAssistant::parseDevices);
282}
283
284void SerialPortAssistant::parseDevices()
285{
287 response->deleteLater();
288 if (response->error() != QNetworkReply::NoError)
289 {
290 qCCritical(KSTARS_EKOS) << response->errorString();
291 KSNotification::error(i18n("Failed to scan devices."));
292 resetCurrentPage();
293 return;
294 }
295
296 QJsonDocument jsonDoc = QJsonDocument::fromJson(response->readAll());
297 if (jsonDoc.isObject() == false)
298 {
299 KSNotification::error(
300 i18n("Failed to detect any devices. Please make sure device is powered and connected to StellarMate via USB."));
301 resetCurrentPage();
302 return;
303 }
304
305 QJsonObject rule = jsonDoc.object();
306
307 // Make sure we have valid vendor ID
308 if (rule.contains("ID_VENDOR_ID") == false || rule["ID_VENDOR_ID"].toString().count() != 4)
309 {
310 KSNotification::error(
311 i18n("Failed to detect any devices. Please make sure device is powered and connected to StellarMate via USB."));
312 resetCurrentPage();
313 return;
314 }
315
316 QString serial = "--";
317
318 QRegularExpression re("^[0-9a-zA-Z-]+$");
319 QRegularExpressionMatch match = re.match(rule["ID_SERIAL"].toString());
320 if (match.hasMatch())
321 serial = rule["ID_SERIAL"].toString();
322
323 // Remove any spaces from the device name
324 QString symlink = serialPortWizard->currentWidget()->objectName().toLower().remove(" ");
325
326 QJsonObject newRule =
327 {
328 {"vid", rule["ID_VENDOR_ID"].toString() },
329 {"pid", rule["ID_MODEL_ID"].toString() },
330 {"serial", serial },
331 {"symlink", symlink },
332 };
333
334 QCheckBox *hardwareSlot = serialPortWizard->currentWidget()->findChild<QCheckBox*>("hardwareSlot");
335 if (hardwareSlot->isChecked())
336 {
337 QString devPath = rule["DEVPATH"].toString();
338 int index = devPath.lastIndexOf("/");
339 if (index > 0)
340 {
341 newRule.insert("port", devPath.mid(index + 1));
342 }
343 }
344 else if (model)
345 {
346 bool vidMatch = !(model->findItems(newRule["vid"].toString(), Qt::MatchExactly, 0).empty());
347 bool pidMatch = !(model->findItems(newRule["pid"].toString(), Qt::MatchExactly, 1).empty());
348 if (vidMatch && pidMatch)
349 {
350 KSNotification::error(i18n("Duplicate devices detected. You must remove one mapping or enable hardware slot mapping."));
351 resetCurrentPage();
352 return;
353 }
354 }
355
356
357 addRule(newRule);
358 // Remove current device page since it is no longer required.
359 serialPortWizard->removeWidget(serialPortWizard->currentWidget());
360 gotoDevicePage(nullptr);
361}
362
363bool SerialPortAssistant::addRule(const QJsonObject &rule)
364{
365 QUrl url(QString("http://%1:%2/api/udev/add_rule").arg(m_Profile->host).arg(m_Profile->INDIWebManagerPort));
367 if (INDI::WebManager::getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr, &data))
368 {
369 KSNotification::event(QLatin1String("IndiServerMessage"), i18n("Mapping is successful."));
370 auto devicePort = m_CurrentDevice->getBaseDevice().getText("DEVICE_PORT");
371 if (devicePort)
372 {
373 // Set port in device and then save config
374 devicePort->at(0)->setText(QString("/dev/%1").arg(rule["symlink"].toString()).toLatin1().constData());
375 m_CurrentDevice->sendNewProperty(devicePort);
376 m_CurrentDevice->setConfig(SAVE_CONFIG);
377 m_CurrentDevice->Connect();
378 }
379
380 loadRules();
381 return true;
382 }
383
384 KSNotification::sorry(i18n("Failed to add a new rule."));
385 resetCurrentPage();
386 return false;
387}
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
char * toString(const EngineQuery &query)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
KIOCORE_EXPORT CopyJob * link(const QList< QUrl > &src, const QUrl &destDir, JobFlags flags=DefaultFlags)
KIOCORE_EXPORT SimpleJob * symlink(const QString &target, const QUrl &dest, JobFlags flags=DefaultFlags)
const QList< QKeySequence > & close()
bool isChecked() const const
void clicked(bool checked)
void setText(const QString &text)
void toggled(bool checked)
void rowsRemoved(const QModelIndex &parent, int first, int last)
virtual void addItem(QLayoutItem *item) override
void addWidget(QWidget *widget, int stretch, Qt::Alignment alignment)
void addButton(QAbstractButton *button, int id)
QList< QAbstractButton * > buttons() const const
void setExclusive(bool)
QString applicationDirPath()
QIcon fromTheme(const QString &name)
QString errorString() const const
QByteArray readAll()
void selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
bool isEmpty() const const
QJsonArray array() const const
QJsonDocument fromJson(const QByteArray &json, QJsonParseError *error)
bool isObject() const const
QJsonObject object() const const
QByteArray toJson(JsonFormat format) const const
bool contains(QLatin1StringView key) const const
iterator insert(QLatin1StringView key, const QJsonValue &value)
void clear()
QMovie * movie() const const
void setMovie(QMovie *movie)
void setText(const QString &)
void setWordWrap(bool on)
void addWidget(QWidget *w)
void append(QList< T > &&value)
qsizetype count() const const
qsizetype indexOf(const AT &value, qsizetype from) const const
bool isValid() const const
int row() const const
void start()
void stop()
QNetworkReply * get(const QNetworkRequest &request)
NetworkError error() const const
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void deleteLater()
T findChild(const QString &name, Qt::FindChildOptions options) const const
T qobject_cast(QObject *object)
QObject * sender() const const
void setObjectName(QAnyStringView name)
bool load(const QString &fileName, const char *format, Qt::ImageConversionFlags flags)
QString arg(Args &&... args) const const
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
QString mid(qsizetype position, qsizetype n) const const
MatchExactly
Horizontal
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QWidget(QWidget *parent, Qt::WindowFlags f)
QLayout * layout() const const
void setFixedSize(const QSize &s)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Dec 6 2024 12:09:51 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.