Kstars

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

KDE's Doxygen guidelines are available online.