File indexing completed on 2024-04-28 03:43:09
0001 /* 0002 SPDX-FileCopyrightText: 2019 Jasem Mutlaq <mutlaqja@ikarustech.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include <QMovie> 0008 #include <QCheckBox> 0009 #include <QJsonDocument> 0010 #include <QJsonArray> 0011 #include <QStandardItem> 0012 #include <QNetworkReply> 0013 #include <QButtonGroup> 0014 #include <QRegularExpression> 0015 #include <QTimer> 0016 #include <basedevice.h> 0017 0018 #include "ksnotification.h" 0019 #include "indi/indiwebmanager.h" 0020 #include "serialportassistant.h" 0021 #include "ekos_debug.h" 0022 #include "kspaths.h" 0023 #include "Options.h" 0024 0025 SerialPortAssistant::SerialPortAssistant(ProfileInfo *profile, QWidget *parent) : QDialog(parent), 0026 m_Profile(profile) 0027 { 0028 setupUi(this); 0029 0030 QPixmap im; 0031 if (im.load(KSPaths::locate(QStandardPaths::AppLocalDataLocation, "wzserialportassistant.png"))) 0032 wizardPix->setPixmap(im); 0033 else if (im.load(QDir(QCoreApplication::applicationDirPath() + "/../Resources/kstars").absolutePath() + 0034 "/wzserialportassistant.png")) 0035 wizardPix->setPixmap(im); 0036 0037 connect(nextB, &QPushButton::clicked, [this]() 0038 { 0039 if (m_CurrentDevice) 0040 gotoDevicePage(m_CurrentDevice); 0041 else if (!m_Devices.empty()) 0042 gotoDevicePage(m_Devices.first()); 0043 }); 0044 0045 loadRules(); 0046 0047 connect(rulesView->selectionModel(), &QItemSelectionModel::selectionChanged, [&](const QItemSelection & selected) 0048 { 0049 clearRuleB->setEnabled(selected.count() > 0); 0050 }); 0051 connect(model.get(), &QStandardItemModel::rowsRemoved, [&]() 0052 { 0053 clearRuleB->setEnabled(model->rowCount() > 0); 0054 }); 0055 connect(clearRuleB, &QPushButton::clicked, this, &SerialPortAssistant::removeActiveRule); 0056 0057 displayOnStartupC->setChecked(Options::autoLoadSerialAssistant()); 0058 connect(displayOnStartupC, &QCheckBox::toggled, [&](bool enabled) 0059 { 0060 Options::setAutoLoadSerialAssistant(enabled); 0061 }); 0062 0063 connect(closeB, &QPushButton::clicked, [&]() 0064 { 0065 gotoDevicePage(nullptr); 0066 close(); 0067 }); 0068 } 0069 0070 void SerialPortAssistant::addDevice(const QSharedPointer<ISD::GenericDevice> &device) 0071 { 0072 qCDebug(KSTARS_EKOS) << "Serial Port Assistant new device" << device->getDeviceName(); 0073 0074 addDevicePage(device); 0075 } 0076 0077 void SerialPortAssistant::addDevicePage(const QSharedPointer<ISD::GenericDevice> &device) 0078 { 0079 m_Devices.append(device); 0080 0081 QWidget *devicePage = new QWidget(this); 0082 devicePage->setObjectName(device->getDeviceName()); 0083 0084 QVBoxLayout *layout = new QVBoxLayout(devicePage); 0085 0086 QLabel *deviceLabel = new QLabel(devicePage); 0087 deviceLabel->setText(QString("<h1>%1</h1>").arg(device->getDeviceName())); 0088 layout->addWidget(deviceLabel); 0089 0090 QLabel *instructionsLabel = new QLabel(devicePage); 0091 instructionsLabel->setText( 0092 i18n("To assign a permanent designation to the device, you need to unplug the device from stellarmate " 0093 "then replug it after 1 second. Click on the <b>Start Scan</b> to begin this procedure.")); 0094 instructionsLabel->setWordWrap(true); 0095 layout->addWidget(instructionsLabel); 0096 0097 QHBoxLayout *actionsLayout = new QHBoxLayout(devicePage); 0098 QPushButton *startButton = new QPushButton(i18n("Start Scan"), devicePage); 0099 startButton->setObjectName("startButton"); 0100 0101 QPushButton *homeButton = new QPushButton(QIcon::fromTheme("go-home"), i18n("Home"), devicePage); 0102 connect(homeButton, &QPushButton::clicked, [&]() 0103 { 0104 gotoDevicePage(nullptr); 0105 }); 0106 0107 QPushButton *skipButton = new QPushButton(i18n("Skip Device"), devicePage); 0108 connect(skipButton, &QPushButton::clicked, [this]() 0109 { 0110 // If we have more devices, go to them one by one 0111 if (m_CurrentDevice) 0112 { 0113 // Check if next index is available 0114 int nextIndex = m_Devices.indexOf(m_CurrentDevice) + 1; 0115 if (nextIndex < m_Devices.count()) 0116 { 0117 gotoDevicePage(m_Devices[nextIndex]); 0118 return; 0119 } 0120 } 0121 0122 gotoDevicePage(nullptr); 0123 }); 0124 QCheckBox *hardwareSlotC = new QCheckBox(i18n("Physical Port Mapping"), devicePage); 0125 hardwareSlotC->setObjectName("hardwareSlot"); 0126 hardwareSlotC->setToolTip( 0127 i18n("Assign the permanent name based on which physical port the device is plugged to in StellarMate. " 0128 "This is useful to distinguish between two identical USB adapters. The device must <b>always</b> be " 0129 "plugged into the same port for this to work.")); 0130 actionsLayout->addItem(new QSpacerItem(10, 10, QSizePolicy::Preferred)); 0131 actionsLayout->addWidget(startButton); 0132 actionsLayout->addWidget(skipButton); 0133 actionsLayout->addWidget(hardwareSlotC); 0134 actionsLayout->addItem(new QSpacerItem(10, 10, QSizePolicy::Preferred)); 0135 actionsLayout->addWidget(homeButton); 0136 layout->addLayout(actionsLayout); 0137 0138 QHBoxLayout *animationLayout = new QHBoxLayout(devicePage); 0139 QLabel *smAnimation = new QLabel(devicePage); 0140 smAnimation->setFixedSize(QSize(360, 203)); 0141 QMovie *smGIF = new QMovie(":/videos/sm_animation.gif"); 0142 smAnimation->setMovie(smGIF); 0143 smAnimation->setObjectName("animation"); 0144 0145 animationLayout->addItem(new QSpacerItem(10, 10, QSizePolicy::Preferred)); 0146 animationLayout->addWidget(smAnimation); 0147 animationLayout->addItem(new QSpacerItem(10, 10, QSizePolicy::Preferred)); 0148 0149 QButtonGroup *actionGroup = new QButtonGroup(devicePage); 0150 actionGroup->setObjectName("actionGroup"); 0151 actionGroup->setExclusive(false); 0152 actionGroup->addButton(startButton); 0153 actionGroup->addButton(skipButton); 0154 actionGroup->addButton(hardwareSlotC); 0155 actionGroup->addButton(homeButton); 0156 0157 layout->addLayout(animationLayout); 0158 //smGIF->start(); 0159 //smAnimation->hide(); 0160 0161 serialPortWizard->insertWidget(serialPortWizard->count() - 1, devicePage); 0162 0163 connect(startButton, &QPushButton::clicked, [ = ]() 0164 { 0165 startButton->setText(i18n("Standby, Scanning...")); 0166 for (auto b : actionGroup->buttons()) 0167 b->setEnabled(false); 0168 smGIF->start(); 0169 scanDevices(); 0170 }); 0171 } 0172 0173 void SerialPortAssistant::gotoDevicePage(const QSharedPointer<ISD::GenericDevice> &device) 0174 { 0175 int index = m_Devices.indexOf(device); 0176 0177 // reset to home page 0178 if (index < 0) 0179 { 0180 m_CurrentDevice = nullptr; 0181 serialPortWizard->setCurrentIndex(0); 0182 return; 0183 } 0184 0185 m_CurrentDevice = device; 0186 serialPortWizard->setCurrentIndex(index + 1); 0187 } 0188 0189 bool SerialPortAssistant::loadRules() 0190 { 0191 QUrl url(QString("http://%1:%2/api/udev/rules").arg(m_Profile->host).arg(m_Profile->INDIWebManagerPort)); 0192 QJsonDocument json; 0193 0194 if (INDI::WebManager::getWebManagerResponse(QNetworkAccessManager::GetOperation, url, &json)) 0195 { 0196 QJsonArray array = json.array(); 0197 0198 if (array.isEmpty()) 0199 return false; 0200 0201 model.reset(new QStandardItemModel(0, 5, this)); 0202 0203 model->setHeaderData(0, Qt::Horizontal, i18nc("Vendor ID", "VID")); 0204 model->setHeaderData(1, Qt::Horizontal, i18nc("Product ID", "PID")); 0205 model->setHeaderData(2, Qt::Horizontal, i18n("Link")); 0206 model->setHeaderData(3, Qt::Horizontal, i18n("Serial #")); 0207 model->setHeaderData(4, Qt::Horizontal, i18n("Hardware Port?")); 0208 0209 0210 // Get all the drivers running remotely 0211 for (auto value : array) 0212 { 0213 QJsonObject rule = value.toObject(); 0214 QList<QStandardItem*> items; 0215 QStandardItem *vid = new QStandardItem(rule["vid"].toString()); 0216 QStandardItem *pid = new QStandardItem(rule["pid"].toString()); 0217 QStandardItem *link = new QStandardItem(rule["symlink"].toString()); 0218 QStandardItem *serial = new QStandardItem(rule["serial"].toString()); 0219 QStandardItem *hardware = new QStandardItem(rule["port"].toString()); 0220 items << vid << pid << link << serial << hardware; 0221 model->appendRow(items); 0222 } 0223 0224 rulesView->setModel(model.get()); 0225 return true; 0226 } 0227 0228 return false; 0229 } 0230 0231 bool SerialPortAssistant::removeActiveRule() 0232 { 0233 QUrl url(QString("http://%1:%2/api/udev/remove_rule").arg(m_Profile->host).arg(m_Profile->INDIWebManagerPort)); 0234 0235 QModelIndex index = rulesView->currentIndex(); 0236 if (index.isValid() == false) 0237 return false; 0238 0239 QStandardItem *symlink = model->item(index.row(), 2); 0240 if (symlink == nullptr) 0241 return false; 0242 0243 QJsonObject rule = { {"symlink", symlink->text()} }; 0244 QByteArray data = QJsonDocument(rule).toJson(QJsonDocument::Compact); 0245 0246 if (INDI::WebManager::getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr, &data)) 0247 { 0248 model->removeRow(index.row()); 0249 return true; 0250 } 0251 0252 return false; 0253 } 0254 0255 void SerialPortAssistant::resetCurrentPage() 0256 { 0257 // Reset all buttons 0258 QButtonGroup *actionGroup = serialPortWizard->currentWidget()->findChild<QButtonGroup*>("actionGroup"); 0259 for (auto b : actionGroup->buttons()) 0260 b->setEnabled(true); 0261 0262 // Set start button to start scanning 0263 QPushButton *startButton = serialPortWizard->currentWidget()->findChild<QPushButton*>("startButton"); 0264 startButton->setText(i18n("Start Scanning")); 0265 0266 // Clear animation 0267 QLabel *animation = serialPortWizard->currentWidget()->findChild<QLabel*>("animation"); 0268 animation->movie()->stop(); 0269 animation->clear(); 0270 } 0271 0272 void SerialPortAssistant::scanDevices() 0273 { 0274 QUrl url(QString("http://%1:%2/api/udev/watch").arg(m_Profile->host).arg(m_Profile->INDIWebManagerPort)); 0275 0276 QNetworkReply *response = manager.get(QNetworkRequest(url)); 0277 0278 // We need to disconnect the device first 0279 m_CurrentDevice->Disconnect(); 0280 0281 connect(response, &QNetworkReply::finished, this, &SerialPortAssistant::parseDevices); 0282 } 0283 0284 void SerialPortAssistant::parseDevices() 0285 { 0286 QNetworkReply *response = qobject_cast<QNetworkReply*>(sender()); 0287 response->deleteLater(); 0288 if (response->error() != QNetworkReply::NoError) 0289 { 0290 qCCritical(KSTARS_EKOS) << response->errorString(); 0291 KSNotification::error(i18n("Failed to scan devices.")); 0292 resetCurrentPage(); 0293 return; 0294 } 0295 0296 QJsonDocument jsonDoc = QJsonDocument::fromJson(response->readAll()); 0297 if (jsonDoc.isObject() == false) 0298 { 0299 KSNotification::error( 0300 i18n("Failed to detect any devices. Please make sure device is powered and connected to StellarMate via USB.")); 0301 resetCurrentPage(); 0302 return; 0303 } 0304 0305 QJsonObject rule = jsonDoc.object(); 0306 0307 // Make sure we have valid vendor ID 0308 if (rule.contains("ID_VENDOR_ID") == false || rule["ID_VENDOR_ID"].toString().count() != 4) 0309 { 0310 KSNotification::error( 0311 i18n("Failed to detect any devices. Please make sure device is powered and connected to StellarMate via USB.")); 0312 resetCurrentPage(); 0313 return; 0314 } 0315 0316 QString serial = "--"; 0317 0318 QRegularExpression re("^[0-9a-zA-Z-]+$"); 0319 QRegularExpressionMatch match = re.match(rule["ID_SERIAL"].toString()); 0320 if (match.hasMatch()) 0321 serial = rule["ID_SERIAL"].toString(); 0322 0323 // Remove any spaces from the device name 0324 QString symlink = serialPortWizard->currentWidget()->objectName().toLower().remove(" "); 0325 0326 QJsonObject newRule = 0327 { 0328 {"vid", rule["ID_VENDOR_ID"].toString() }, 0329 {"pid", rule["ID_MODEL_ID"].toString() }, 0330 {"serial", serial }, 0331 {"symlink", symlink }, 0332 }; 0333 0334 QCheckBox *hardwareSlot = serialPortWizard->currentWidget()->findChild<QCheckBox*>("hardwareSlot"); 0335 if (hardwareSlot->isChecked()) 0336 { 0337 QString devPath = rule["DEVPATH"].toString(); 0338 int index = devPath.lastIndexOf("/"); 0339 if (index > 0) 0340 { 0341 newRule.insert("port", devPath.mid(index + 1)); 0342 } 0343 } 0344 else if (model) 0345 { 0346 bool vidMatch = !(model->findItems(newRule["vid"].toString(), Qt::MatchExactly, 0).empty()); 0347 bool pidMatch = !(model->findItems(newRule["pid"].toString(), Qt::MatchExactly, 1).empty()); 0348 if (vidMatch && pidMatch) 0349 { 0350 KSNotification::error(i18n("Duplicate devices detected. You must remove one mapping or enable hardware slot mapping.")); 0351 resetCurrentPage(); 0352 return; 0353 } 0354 } 0355 0356 0357 addRule(newRule); 0358 // Remove current device page since it is no longer required. 0359 serialPortWizard->removeWidget(serialPortWizard->currentWidget()); 0360 gotoDevicePage(nullptr); 0361 } 0362 0363 bool SerialPortAssistant::addRule(const QJsonObject &rule) 0364 { 0365 QUrl url(QString("http://%1:%2/api/udev/add_rule").arg(m_Profile->host).arg(m_Profile->INDIWebManagerPort)); 0366 QByteArray data = QJsonDocument(rule).toJson(QJsonDocument::Compact); 0367 if (INDI::WebManager::getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr, &data)) 0368 { 0369 KSNotification::event(QLatin1String("IndiServerMessage"), i18n("Mapping is successful.")); 0370 auto devicePort = m_CurrentDevice->getBaseDevice().getText("DEVICE_PORT"); 0371 if (devicePort) 0372 { 0373 // Set port in device and then save config 0374 devicePort->at(0)->setText(QString("/dev/%1").arg(rule["symlink"].toString()).toLatin1().constData()); 0375 m_CurrentDevice->sendNewProperty(devicePort); 0376 m_CurrentDevice->setConfig(SAVE_CONFIG); 0377 m_CurrentDevice->Connect(); 0378 } 0379 0380 loadRules(); 0381 return true; 0382 } 0383 0384 KSNotification::sorry(i18n("Failed to add a new rule.")); 0385 resetCurrentPage(); 0386 return false; 0387 }