diff --git a/kstars/ekos/auxiliary/serialportassistant.cpp b/kstars/ekos/auxiliary/serialportassistant.cpp index e7f822955..15470d8fa 100644 --- a/kstars/ekos/auxiliary/serialportassistant.cpp +++ b/kstars/ekos/auxiliary/serialportassistant.cpp @@ -1,179 +1,317 @@ /* Ekos Serial Port Assistant tool Copyright (C) 2019 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include #include #include #include #include +#include +#include +#include +#include +#include "ksnotification.h" #include "indi/indiwebmanager.h" #include "serialportassistant.h" +#include "indi/clientmanager.h" +#include "indi/driverinfo.h" #include "ekos_debug.h" #include "kspaths.h" SerialPortAssistant::SerialPortAssistant(ProfileInfo *profile, QWidget *parent) : QDialog(parent), m_Profile(profile) { setupUi(this); QPixmap im; if (im.load(KSPaths::locate(QStandardPaths::GenericDataLocation, "wzserialportassistant.png"))) wizardPix->setPixmap(im); else if (im.load(QDir(QCoreApplication::applicationDirPath() + "/../Resources/data").absolutePath() + "/wzserialportassistant.png")) wizardPix->setPixmap(im); connect(nextB, &QPushButton::clicked, [&]() { serialPortWizard->setCurrentIndex(serialPortWizard->currentIndex()+1); }); loadRules(); connect(rulesView->selectionModel(), &QItemSelectionModel::selectionChanged, [&](const QItemSelection &selected) { clearRuleB->setEnabled(selected.count() > 0); }); connect(model.get(), &QStandardItemModel::rowsRemoved, [&]() { clearRuleB->setEnabled(model->rowCount() > 0); }); connect(clearRuleB, &QPushButton::clicked, this, &SerialPortAssistant::removeActiveRule); } void SerialPortAssistant::addDevice(ISD::GDInterface *device) { qCDebug(KSTARS_EKOS) << "Serial Port Assistant new device" << device->getDeviceName(); addPage(device); } void SerialPortAssistant::addPage(ISD::GDInterface *device) { - QWidget *devicePage = new QWidget(this); - devices.append(device); + QWidget *devicePage = new QWidget(this); + devicePage->setObjectName(device->getDeviceName()); + QVBoxLayout *layout = new QVBoxLayout(devicePage); QLabel *deviceLabel = new QLabel(devicePage); deviceLabel->setText(QString("

%1

").arg(device->getDeviceName())); layout->addWidget(deviceLabel); QLabel *instructionsLabel = new QLabel(devicePage); instructionsLabel->setText(i18n("To assign a permanent designation to the device, you need to unplug the device from stellarmate " "then replug it after 1 second. Click on the Start Scan to begin this procedure.")); instructionsLabel->setWordWrap(true); layout->addWidget(instructionsLabel); QHBoxLayout *actionsLayout = new QHBoxLayout(devicePage); QPushButton *startButton = new QPushButton(i18n("Start Scan"), devicePage); + startButton->setObjectName("startButton"); QPushButton *skipButton = new QPushButton(i18n("Skip Device"), devicePage); QCheckBox *hardwareSlotC = new QCheckBox(i18n("Physical Port Mapping"), devicePage); + hardwareSlotC->setObjectName("hardwareSlot"); hardwareSlotC->setToolTip(i18n("Assign the permanent name based on which physical port the device is plugged to in StellarMate. " "This is useful to distinguish between two identical USB adapters. The device must always be " "plugged into the same port for this to work.")); actionsLayout->addItem(new QSpacerItem(10,10, QSizePolicy::Preferred)); actionsLayout->addWidget(startButton); actionsLayout->addWidget(skipButton); actionsLayout->addWidget(hardwareSlotC); actionsLayout->addItem(new QSpacerItem(10,10, QSizePolicy::Preferred)); layout->addLayout(actionsLayout); QHBoxLayout *animationLayout = new QHBoxLayout(devicePage); QLabel *smAnimation = new QLabel(devicePage); - //smAnimation->setFixedSize(QSize(360,203)); + smAnimation->setFixedSize(QSize(360,203)); QMovie *smGIF = new QMovie(":/videos/sm_animation.gif"); smAnimation->setMovie(smGIF); + smAnimation->setObjectName("animation"); animationLayout->addItem(new QSpacerItem(10,10, QSizePolicy::Preferred)); animationLayout->addWidget(smAnimation); animationLayout->addItem(new QSpacerItem(10,10, QSizePolicy::Preferred)); + QButtonGroup *actionGroup = new QButtonGroup(devicePage); + actionGroup->setObjectName("actionGroup"); + actionGroup->setExclusive(false); + actionGroup->addButton(startButton); + actionGroup->addButton(skipButton); + actionGroup->addButton(hardwareSlotC); + layout->addLayout(animationLayout); - smGIF->start(); + //smGIF->start(); //smAnimation->hide(); serialPortWizard->insertWidget(serialPortWizard->count()-1, devicePage); + + connect(startButton, &QPushButton::clicked, [=]() { + startButton->setText(i18n("Standby, Scanning...")); + for (auto b : actionGroup->buttons()) + b->setEnabled(false); + smGIF->start(); + scanDevices(); + }); } void SerialPortAssistant::gotoPage(ISD::GDInterface *device) { int index = devices.indexOf(device); if (index < 0) return; currentDevice = device; serialPortWizard->setCurrentIndex( (1 + index) * 2); } bool SerialPortAssistant::loadRules() { QUrl url(QString("http://%1:%2/api/udev/rules").arg(m_Profile->host).arg(m_Profile->INDIWebManagerPort)); QJsonDocument json; if (INDI::WebManager::getWebManagerResponse(QNetworkAccessManager::GetOperation, url, &json)) { QJsonArray array = json.array(); if (array.isEmpty()) return false; model.reset(new QStandardItemModel(0, 5, this)); - model->setHeaderData(0, Qt::Horizontal, i18nc("Product ID", "PID")); - model->setHeaderData(1, Qt::Horizontal, i18nc("Vendor ID", "VID")); + model->setHeaderData(0, Qt::Horizontal, i18nc("Vendor ID", "VID")); + model->setHeaderData(1, Qt::Horizontal, i18nc("Product ID", "PID")); model->setHeaderData(2, Qt::Horizontal, i18n("Link")); model->setHeaderData(3, Qt::Horizontal, i18n("Serial #")); model->setHeaderData(4, Qt::Horizontal, i18n("Hardware Port?")); // Get all the drivers running remotely for (auto value : array) { QJsonObject rule = value.toObject(); QList items; - QStandardItem *pid = new QStandardItem(rule["pid"].toString()); QStandardItem *vid = new QStandardItem(rule["vid"].toString()); + QStandardItem *pid = new QStandardItem(rule["pid"].toString()); QStandardItem *link = new QStandardItem(rule["symlink"].toString()); QStandardItem *serial = new QStandardItem(rule["serial"].toString()); QStandardItem *hardware = new QStandardItem(rule["port"].toString()); - items << pid << vid << link << serial << hardware; + items << vid << pid << link << serial << hardware; model->appendRow(items); } rulesView->setModel(model.get()); return true; } return false; } bool SerialPortAssistant::removeActiveRule() { QUrl url(QString("http://%1:%2/api/udev/remove_rule").arg(m_Profile->host).arg(m_Profile->INDIWebManagerPort)); QModelIndex index = rulesView->currentIndex(); if (index.isValid() == false) return false; QStandardItem *symlink = model->item(index.row(), 2); if (symlink == nullptr) return false; QJsonObject rule = { {"symlink", symlink->text()} }; QByteArray data = QJsonDocument(rule).toJson(QJsonDocument::Compact); if (INDI::WebManager::getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr, &data)) { model->removeRow(index.row()); return true; } return false; } +void SerialPortAssistant::resetPage(int index) +{ + QButtonGroup *actionGroup = serialPortWizard->widget(index)->findChild("actionGroup"); + for (auto b : actionGroup->buttons()) + b->setEnabled(true); + QPushButton *startButton = serialPortWizard->widget(index)->findChild("startButton"); + startButton->setText(i18n("Start Scanning")); + QLabel *animation = serialPortWizard->widget(index)->findChild("animation"); + animation->movie()->stop(); + animation->clear(); + serialPortWizard->setCurrentIndex(index); +} + +void SerialPortAssistant::scanDevices() +{ + QUrl url(QString("http://%1:%2/api/udev/watch").arg(m_Profile->host).arg(m_Profile->INDIWebManagerPort)); + + QNetworkReply *response = manager.get(QNetworkRequest(url)); + + // We need to disconnect the device first + devices[serialPortWizard->currentIndex()-1]->Disconnect(); + + connect(response, &QNetworkReply::finished, this, &SerialPortAssistant::parseDevices); +} + +void SerialPortAssistant::parseDevices() +{ + QNetworkReply *response = qobject_cast(sender()); + response->deleteLater(); + if (response->error() != QNetworkReply::NoError) + { + qCCritical(KSTARS_EKOS) << response->errorString(); + KSNotification::error(i18n("Failed to scan devices.")); + resetPage(serialPortWizard->currentIndex()); + return; + } + + QJsonDocument jsonDoc = QJsonDocument::fromJson(response->readAll()); + if (jsonDoc.isObject() == false) + { + KSNotification::error(i18n("Failed to detect any devices. Please make sure device is powered and connected to StellarMate via USB.")); + resetPage(serialPortWizard->currentIndex()); + return; + } + + QString serial = "--"; + QJsonObject rule = jsonDoc.object(); + QRegularExpression re("^[0-9a-zA-Z-]+$"); + QRegularExpressionMatch match = re.match(rule["ID_SERIAL"].toString()); + if (match.hasMatch()) + serial = rule["ID_SERIAL"].toString(); + + // Remove any spaces from the device name + QString symlink = serialPortWizard->currentWidget()->objectName().toLower().remove(" "); + + QJsonObject newRule = { + {"vid", rule["ID_VENDOR_ID"].toString() }, + {"pid", rule["ID_MODEL_ID"].toString() }, + {"serial", serial }, + {"symlink", symlink }, + }; + + QCheckBox *hardwareSlot = serialPortWizard->currentWidget()->findChild("hardwareSlot"); + if (hardwareSlot->isChecked()) + { + QString devPath = rule["DEVPATH"].toString(); + int index = devPath.lastIndexOf("/"); + if (index > 0) + { + newRule.insert("port", devPath.mid(index+1)); + } + } + else + { + QList items = model->findItems(newRule["vid"].toString(), Qt::MatchExactly, 0); + bool vidMatch = !(model->findItems(newRule["vid"].toString(), Qt::MatchExactly, 0).empty()); + bool pidMatch = !(model->findItems(newRule["pid"].toString(), Qt::MatchExactly, 1).empty()); + if (vidMatch && pidMatch) + { + KSNotification::error(i18n("Duplicate devices detected. You must remove one mapping or enable hardware slot mapping.")); + resetPage(serialPortWizard->currentIndex()); + return; + } + } + + addRule(newRule); +} + +bool SerialPortAssistant::addRule(const QJsonObject &rule) +{ + QUrl url(QString("http://%1:%2/api/udev/add_rule").arg(m_Profile->host).arg(m_Profile->INDIWebManagerPort)); + QByteArray data = QJsonDocument(rule).toJson(QJsonDocument::Compact); + if (INDI::WebManager::getWebManagerResponse(QNetworkAccessManager::PostOperation, url, nullptr, &data)) + { + KSNotification::info(i18n("Mapping is succesful. Please unplug and replug your device now.")); + ITextVectorProperty *devicePort = devices[serialPortWizard->currentIndex()-1]->getBaseDevice()->getText("DEVICE_PORT"); + if (devicePort) + { + // Set port in device and then save config + IUSaveText(&devicePort->tp[0], QString("/dev/%1").arg(rule["symlink"].toString()).toLatin1().constData()); + devices[serialPortWizard->currentIndex()-1]->getDriverInfo()->getClientManager()->sendNewText(devicePort); + devices[serialPortWizard->currentIndex()-1]->setConfig(SAVE_CONFIG); + devices[serialPortWizard->currentIndex()-1]->Connect(); + serialPortWizard->setCurrentIndex(serialPortWizard->currentIndex()+1); + } + return true; + } + + KSNotification::sorry(i18n("Failed to add a new rule.")); + resetPage(serialPortWizard->currentIndex()); + return false; +} diff --git a/kstars/ekos/auxiliary/serialportassistant.h b/kstars/ekos/auxiliary/serialportassistant.h index 9e7021c43..97b4e7f6b 100644 --- a/kstars/ekos/auxiliary/serialportassistant.h +++ b/kstars/ekos/auxiliary/serialportassistant.h @@ -1,42 +1,53 @@ /* Ekos Serial Port Assistant tool Copyright (C) 2019 Jasem Mutlaq This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include #include +#include +#include #include #include "indi/indistd.h" #include "profileinfo.h" #include "ui_serialportassistant.h" class SerialPortAssistant : public QDialog, public Ui::SerialPortAssistant { public: SerialPortAssistant(ProfileInfo *profile, QWidget *parent = nullptr); void addDevice(ISD::GDInterface *device); private: bool loadRules(); bool removeActiveRule(); + bool addRule(const QJsonObject &rule); void addPage(ISD::GDInterface *device); void gotoPage(ISD::GDInterface *device); + void resetPage(int index); + + void scanDevices(); + void parseDevices(); + + void discoverDevice(); QList devices; std::unique_ptr model; ISD::GDInterface *currentDevice { nullptr }; const ProfileInfo *m_Profile; + + QNetworkAccessManager manager; }; diff --git a/kstars/ekos/auxiliary/serialportassistant.ui b/kstars/ekos/auxiliary/serialportassistant.ui index e6c426005..49800d27e 100644 --- a/kstars/ekos/auxiliary/serialportassistant.ui +++ b/kstars/ekos/auxiliary/serialportassistant.ui @@ -1,252 +1,278 @@ SerialPortAssistant 0 0 541 325 Serial Port Assistant 3 3 3 3 3 0 0 120 0 true + + 3 + 0 + + 3 + + + 3 + + + 3 + + + 3 + + + 3 + <html><head/><body><p>Welcome to StellarMate <span style=" font-weight:600;">Serial Port Assistant</span> tool.</p></body></html> <html><head/><body><p>This tool shall assign <span style=" font-style:italic;">permanent</span> names to your <span style=" font-weight:600;">Serial to USB</span> devices so that they are easier to connect to in the future.</p><p><br/></p><p>Click <span style=" font-weight:600;">Next</span> to continue.</p></body></html> true Qt::Vertical 20 40 Existing Mapping Qt::Horizontal 40 20 false Remove rule - + + .. QAbstractItemView::NoEditTriggers QAbstractItemView::SelectRows true + + + + 3 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + Display on detecting unmapped ports + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 56 + 11 + + + + + + + + &Next + + + true + + + true + + + + + Qt::Vertical 20 99 All devices are successfully mapped. You can now connect to your equipment. Qt::Vertical 20 96 Close - - - - 6 - - - 10 - - - 10 - - - 10 - - - 10 - - - - - Qt::Horizontal - - - QSizePolicy::Expanding - - - - 56 - 11 - - - - - - - - &Next - - - true - - - true - - - - -