File indexing completed on 2024-04-28 15:09:12

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 }