From a15d02f2170428907826fb02e6218c22c9ade831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Sun, 11 Apr 2021 00:10:22 +0200 Subject: [PATCH] Implementation of some SCPI commands via TCP --- .../CustomWidgets/jsonpickerdialog.cpp | 6 + .../PC_Application/Generator/generator.cpp | 52 ++++++ Software/PC_Application/Generator/generator.h | 4 +- .../Generator/signalgenwidget.cpp | 25 ++- .../Generator/signalgenwidget.h | 1 + Software/PC_Application/LibreVNA-GUI.pro | 6 +- Software/PC_Application/VNA/vna.cpp | 121 +++++++++++++ Software/PC_Application/VNA/vna.h | 4 +- Software/PC_Application/appwindow.cpp | 112 +++++++++++- Software/PC_Application/appwindow.h | 9 +- Software/PC_Application/main.cpp | 20 ++- Software/PC_Application/scpi.cpp | 160 ++++++++++++++++++ Software/PC_Application/scpi.h | 63 +++++++ Software/PC_Application/tcpserver.cpp | 40 +++++ Software/PC_Application/tcpserver.h | 23 +++ 15 files changed, 627 insertions(+), 19 deletions(-) create mode 100644 Software/PC_Application/scpi.cpp create mode 100644 Software/PC_Application/scpi.h create mode 100644 Software/PC_Application/tcpserver.cpp create mode 100644 Software/PC_Application/tcpserver.h diff --git a/Software/PC_Application/CustomWidgets/jsonpickerdialog.cpp b/Software/PC_Application/CustomWidgets/jsonpickerdialog.cpp index 627a73b..4a83f31 100644 --- a/Software/PC_Application/CustomWidgets/jsonpickerdialog.cpp +++ b/Software/PC_Application/CustomWidgets/jsonpickerdialog.cpp @@ -22,6 +22,7 @@ JSONPickerDialog::~JSONPickerDialog() JSONModel::JSONModel(const nlohmann::json &json, QObject *parent) : json(json) { + Q_UNUSED(parent) setupJsonInfo(json); } @@ -97,6 +98,7 @@ int JSONModel::rowCount(const QModelIndex &parent) const int JSONModel::columnCount(const QModelIndex &parent) const { + Q_UNUSED(parent) return 2; } @@ -133,11 +135,15 @@ QVariant JSONModel::data(const QModelIndex &index, int role) const QVariant JSONModel::headerData(int section, Qt::Orientation orientation, int role) const { + Q_UNUSED(section) + Q_UNUSED(orientation) + Q_UNUSED(role) return QVariant(); } bool JSONModel::setData(const QModelIndex &index, const QVariant &value, int role) { + Q_UNUSED(value) nlohmann::json *item = static_cast(index.internalPointer()); auto info = jsonInfo.at(item); if(role == Qt::CheckStateRole) diff --git a/Software/PC_Application/Generator/generator.cpp b/Software/PC_Application/Generator/generator.cpp index 0f66e4a..007a7b6 100644 --- a/Software/PC_Application/Generator/generator.cpp +++ b/Software/PC_Application/Generator/generator.cpp @@ -3,6 +3,7 @@ Generator::Generator(AppWindow *window) : Mode(window, "Signal Generator") + , SCPINode("GENerator") { central = new SignalgeneratorWidget(window); @@ -18,6 +19,8 @@ Generator::Generator(AppWindow *window) central->setLevel(pref.Startup.Generator.level); } + setupSCPI(); + finalize(central); connect(central, &SignalgeneratorWidget::SettingsChanged, this, &Generator::updateDevice); } @@ -48,3 +51,52 @@ void Generator::updateDevice() p.generator = central->getDeviceStatus(); window->getDevice()->SendPacket(p); } + +void Generator::setupSCPI() +{ + add(new SCPICommand("FREQuency", [=](QStringList params) -> QString { + bool ok; + if(params.size() != 1) { + return "ERROR"; + } + auto newval = params[0].toUInt(&ok); + if(!ok) { + return "ERROR"; + } else { + central->setFrequency(newval); + return ""; + } + }, [=]() -> QString { + return QString::number(central->getDeviceStatus().frequency); + })); + add(new SCPICommand("LVL", [=](QStringList params) -> QString { + bool ok; + if(params.size() != 1) { + return "ERROR"; + } + auto newval = params[0].toDouble(&ok); + if(!ok) { + return "ERROR"; + } else { + central->setLevel(newval); + return ""; + } + }, [=]() -> QString { + return QString::number(central->getDeviceStatus().cdbm_level / 100.0); + })); + add(new SCPICommand("PORT", [=](QStringList params) -> QString { + bool ok; + if(params.size() != 1) { + return "ERROR"; + } + auto newval = params[0].toUInt(&ok); + if(!ok || newval > 2) { + return "ERROR"; + } else { + central->setPort(newval); + return ""; + } + }, [=]() -> QString { + return QString::number(central->getDeviceStatus().activePort); + })); +} diff --git a/Software/PC_Application/Generator/generator.h b/Software/PC_Application/Generator/generator.h index 8e5f999..3418a1b 100644 --- a/Software/PC_Application/Generator/generator.h +++ b/Software/PC_Application/Generator/generator.h @@ -3,8 +3,9 @@ #include "mode.h" #include "signalgenwidget.h" +#include "scpi.h" -class Generator : public Mode +class Generator : public Mode, public SCPINode { public: Generator(AppWindow *window); @@ -19,6 +20,7 @@ private slots: void updateDevice(); private: + void setupSCPI(); SignalgeneratorWidget *central; }; diff --git a/Software/PC_Application/Generator/signalgenwidget.cpp b/Software/PC_Application/Generator/signalgenwidget.cpp index 57623cc..01b0218 100644 --- a/Software/PC_Application/Generator/signalgenwidget.cpp +++ b/Software/PC_Application/Generator/signalgenwidget.cpp @@ -92,13 +92,13 @@ SignalgeneratorWidget::SignalgeneratorWidget(QWidget *parent) : connect(ui->levelSlider, &QSlider::valueChanged, [=](int value) { setLevel((double) value / 100.0); }); - connect(ui->EnablePort1, &QCheckBox::clicked, [=](){ + connect(ui->EnablePort1, &QCheckBox::toggled, [=](){ if(ui->EnablePort1->isChecked() && ui->EnablePort2->isChecked()) { ui->EnablePort2->setCheckState(Qt::CheckState::Unchecked); } emit SettingsChanged(); }); - connect(ui->EnablePort2, &QCheckBox::clicked, [=](){ + connect(ui->EnablePort2, &QCheckBox::toggled, [=](){ if(ui->EnablePort1->isChecked() && ui->EnablePort2->isChecked()) { ui->EnablePort1->setCheckState(Qt::CheckState::Unchecked); } @@ -174,3 +174,24 @@ void SignalgeneratorWidget::setFrequency(double frequency) ui->frequency->setValue(frequency); } +void SignalgeneratorWidget::setPort(int port) +{ + if(port < 0 || port > 2) { + return; + } + switch(port) { + case 0: + ui->EnablePort1->setChecked(false); + ui->EnablePort2->setChecked(false); + break; + case 1: + ui->EnablePort1->setChecked(true); + ui->EnablePort2->setChecked(false); + break; + case 2: + ui->EnablePort1->setChecked(false); + ui->EnablePort2->setChecked(true); + break; + } +} + diff --git a/Software/PC_Application/Generator/signalgenwidget.h b/Software/PC_Application/Generator/signalgenwidget.h index a059c0d..90b72bf 100644 --- a/Software/PC_Application/Generator/signalgenwidget.h +++ b/Software/PC_Application/Generator/signalgenwidget.h @@ -24,6 +24,7 @@ signals: public slots: void setLevel(double level); void setFrequency(double frequency); + void setPort(int port); protected: void timerEvent(QTimerEvent *) override; diff --git a/Software/PC_Application/LibreVNA-GUI.pro b/Software/PC_Application/LibreVNA-GUI.pro index 4c4c85f..de8fd0c 100644 --- a/Software/PC_Application/LibreVNA-GUI.pro +++ b/Software/PC_Application/LibreVNA-GUI.pro @@ -115,6 +115,8 @@ HEADERS += \ mode.h \ preferences.h \ savable.h \ + scpi.h \ + tcpserver.h \ touchstone.h \ unit.h @@ -221,6 +223,8 @@ SOURCES += \ main.cpp \ mode.cpp \ preferences.cpp \ + scpi.cpp \ + tcpserver.cpp \ touchstone.cpp \ unit.cpp @@ -230,7 +234,7 @@ win32:LIBS += -L"$$_PRO_FILE_PWD_" # Github actions placed libusb here osx:INCPATH += /usr/local/include osx:LIBS += $(shell pkg-config --libs libusb-1.0) -QT += widgets +QT += widgets network FORMS += \ Calibration/addamplitudepointsdialog.ui \ diff --git a/Software/PC_Application/VNA/vna.cpp b/Software/PC_Application/VNA/vna.cpp index 2ab60cb..995a806 100644 --- a/Software/PC_Application/VNA/vna.cpp +++ b/Software/PC_Application/VNA/vna.cpp @@ -49,6 +49,7 @@ VNA::VNA(AppWindow *window) : Mode(window, "Vector Network Analyzer"), + SCPINode("VNA"), deembedding(traceModel), central(new TileWidget(traceModel)) { @@ -58,6 +59,8 @@ VNA::VNA(AppWindow *window) calDialog.reset(); calEdited = false; + SetupSCPI(); + // Create default traces auto tS11 = new Trace("S11", Qt::yellow); tS11->fromLivedata(Trace::LivedataType::Overwrite, Trace::LiveParameter::S11); @@ -875,6 +878,124 @@ void VNA::StartCalibrationMeasurement(Calibration::Measurement m) calEdited = true; } +void VNA::SetupSCPI() +{ + auto scpi_freq = new SCPINode("FREQuency"); + SCPINode::add(scpi_freq); + auto toULong = [](QStringList params) { + bool ok; + if(params.size() != 1) { + return std::numeric_limits::max(); + } + auto newval = params[0].toULong(&ok); + if(!ok) { + return std::numeric_limits::max(); + } else { + return newval; + } + }; + scpi_freq->add(new SCPICommand("SPAN", [=](QStringList params) -> QString { + auto newval = toULong(params); + if(newval == std::numeric_limits::max()) { + return "ERROR"; + } else { + SetSpan(newval); + return ""; + } + }, [=]() -> QString { + return QString::number(settings.f_stop - settings.f_start); + })); + scpi_freq->add(new SCPICommand("START", [=](QStringList params) -> QString { + auto newval = toULong(params); + if(newval == std::numeric_limits::max()) { + return "ERROR"; + } else { + SetStartFreq(newval); + return ""; + } + }, [=]() -> QString { + return QString::number(settings.f_start); + })); + scpi_freq->add(new SCPICommand("CENTER", [=](QStringList params) -> QString { + auto newval = toULong(params); + if(newval == std::numeric_limits::max()) { + return "ERROR"; + } else { + SetCenterFreq(newval); + return ""; + } + }, [=]() -> QString { + return QString::number((settings.f_start + settings.f_stop)/2); + })); + scpi_freq->add(new SCPICommand("STOP", [=](QStringList params) -> QString { + auto newval = toULong(params); + if(newval == std::numeric_limits::max()) { + return "ERROR"; + } else { + SetStopFreq(newval); + return ""; + } + }, [=]() -> QString { + return QString::number(settings.f_stop); + })); + scpi_freq->add(new SCPICommand("FULL", [=](QStringList params) -> QString { + SetFullSpan(); + return ""; + }, nullptr)); + auto scpi_acq = new SCPINode("ACQuisition"); + SCPINode::add(scpi_acq); + scpi_acq->add(new SCPICommand("IFBW", [=](QStringList params) -> QString { + auto newval = toULong(params); + if(newval == std::numeric_limits::max()) { + return "ERROR"; + } else { + SetIFBandwidth(newval); + return ""; + } + }, [=]() -> QString { + return QString::number(settings.if_bandwidth); + })); + scpi_acq->add(new SCPICommand("POINTS", [=](QStringList params) -> QString { + auto newval = toULong(params); + if(newval == std::numeric_limits::max()) { + return "ERROR"; + } else { + SetPoints(newval); + return ""; + } + }, [=]() -> QString { + return QString::number(settings.points); + })); + scpi_acq->add(new SCPICommand("AVG", [=](QStringList params) -> QString { + auto newval = toULong(params); + if(newval == std::numeric_limits::max()) { + return "ERROR"; + } else { + SetAveraging(newval); + return ""; + } + }, [=]() -> QString { + return QString::number(averages); + })); + auto scpi_stim = new SCPINode("STIMulus"); + SCPINode::add(scpi_stim); + scpi_stim->add(new SCPICommand("LVL", [=](QStringList params) -> QString { + bool ok; + if(params.size() != 1) { + return "ERROR"; + } + auto newval = params[0].toDouble(&ok); + if(!ok) { + return "ERROR"; + } else { + SetSourceLevel(newval); + return ""; + } + }, [=]() -> QString { + return QString::number(settings.cdbm_excitation / 100.0); + })); +} + void VNA::ConstrainAndUpdateFrequencies() { auto pref = Preferences::getInstance(); diff --git a/Software/PC_Application/VNA/vna.h b/Software/PC_Application/VNA/vna.h index 6238cd3..811cd52 100644 --- a/Software/PC_Application/VNA/vna.h +++ b/Software/PC_Application/VNA/vna.h @@ -9,8 +9,9 @@ #include "Device/device.h" #include #include "Deembedding/deembedding.h" +#include "scpi.h" -class VNA : public Mode +class VNA : public Mode, public SCPINode { Q_OBJECT public: @@ -50,6 +51,7 @@ signals: void CalibrationMeasurementComplete(Calibration::Measurement m); private: + void SetupSCPI(); void UpdateAverageCount(); void SettingsChanged(std::function cb = nullptr); void ConstrainAndUpdateFrequencies(); diff --git a/Software/PC_Application/appwindow.cpp b/Software/PC_Application/appwindow.cpp index 08cb391..df41ad6 100644 --- a/Software/PC_Application/appwindow.cpp +++ b/Software/PC_Application/appwindow.cpp @@ -48,23 +48,106 @@ #include "Calibration/receivercaldialog.h" #include #include "CustomWidgets/jsonpickerdialog.h" +#include + using namespace std; AppWindow::AppWindow(QWidget *parent) : QMainWindow(parent) , deviceActionGroup(new QActionGroup(this)) , ui(new Ui::MainWindow) + , server(nullptr) { QCoreApplication::setOrganizationName("LibreVNA"); QCoreApplication::setApplicationName("LibreVNA-GUI"); + auto commit = QString(GITHASH); + commit.truncate(7); + QCoreApplication::setApplicationVersion(QString::number(FW_MAJOR) + "." + QString::number(FW_MINOR) + + "." + QString::number(FW_PATCH) + FW_SUFFIX + " ("+ commit+")"); qSetMessagePattern("%{time process}: [%{type}] %{message}"); +// qDebug().setVerbosity(0); qDebug() << "Application start"; + parser.setApplicationDescription("LibreVNA-GUI"); + parser.addHelpOption(); + parser.addVersionOption(); + parser.addOption(QCommandLineOption({"p","port"}, "Specify port to listen for SCPY commands", "port")); + parser.addOption(QCommandLineOption({"d","device"}, "Only allow connections to the specified device", "device")); + parser.addOption(QCommandLineOption("no-gui", "Disables the graphical interface")); + + parser.process(QCoreApplication::arguments()); + Preferences::getInstance().load(); device = nullptr; + if(parser.isSet("port")) { + bool OK; + auto port = parser.value("port").toUInt(&OK); + if(!OK) { + // set default port + port = 19542; + } + server = new TCPServer(port); + connect(server, &TCPServer::received, &scpi, &SCPI::input); + connect(&scpi, &SCPI::output, server, &TCPServer::send); + } + + scpi.add(new SCPICommand("*IDN", nullptr, [=](){ + return "LibreVNA-GUI"; + })); + auto scpi_dev = new SCPINode("DEVice"); + scpi.add(scpi_dev); + scpi_dev->add(new SCPICommand("DISConnect", [=](QStringList params) -> QString { + Q_UNUSED(params) + DisconnectDevice(); + return ""; + }, nullptr)); + scpi_dev->add(new SCPICommand("CONNect", [=](QStringList params) -> QString { + QString serial; + if(params.size() > 0) { + serial = params[0]; + } + if(!ConnectToDevice(serial)) { + return "Device not found"; + } else { + return ""; + } + }, [=]() -> QString { + if(device) { + return device->serial(); + } else { + return "Not connected"; + } + })); + scpi.add(new SCPICommand("MODE", [=](QStringList params) -> QString { + if (params.size() != 1) { + return "ERROR"; + } + if (params[0] == "VNA") { + vna->activate(); + } else if(params[0] == "GEN") { + generator->activate(); + } else if(params[0] == "SA") { + spectrumAnalyzer->activate(); + } else { + return "INVALID MDOE"; + } + return ""; + }, [=]() -> QString { + auto active = Mode::getActiveMode(); + if(active == vna) { + return "VNA"; + } else if(active == generator) { + return "GEN"; + } else if(active == spectrumAnalyzer) { + return "SA"; + } else { + return "ERROR"; + } + })); + ui->setupUi(this); ui->statusbar->addWidget(&lConnectionStatus); auto div1 = new QFrame; @@ -112,6 +195,9 @@ AppWindow::AppWindow(QWidget *parent) generator = new Generator(this); spectrumAnalyzer = new SpectrumAnalyzer(this); + scpi.add(vna); + scpi.add(generator); + // UI connections connect(ui->actionUpdate_Device_List, &QAction::triggered, this, &AppWindow::UpdateDeviceList); connect(ui->actionDisconnect, &QAction::triggered, this, &AppWindow::DisconnectDevice); @@ -158,11 +244,8 @@ AppWindow::AppWindow(QWidget *parent) // TraceXYPlot::updateGraphColors(); }); connect(ui->actionAbout, &QAction::triggered, [=](){ - auto commit = QString(GITHASH); - commit.truncate(7); QMessageBox::about(this, "About", "More information: github.com/jankae/LibreVNA\n" - "\nVersion: " + QString::number(FW_MAJOR) + "." + QString::number(FW_MINOR) - + "." + QString::number(FW_PATCH) + FW_SUFFIX + " ("+ commit+")"); + "\nVersion: " + QCoreApplication::applicationVersion()); }); setWindowTitle("LibreVNA-GUI"); @@ -190,11 +273,16 @@ AppWindow::AppWindow(QWidget *parent) // at least one device available ConnectToDevice(); } + if(!parser.isSet("no-gui")) { + resize(1280, 800); + show(); + } } AppWindow::~AppWindow() { delete ui; + delete server; } void AppWindow::closeEvent(QCloseEvent *event) @@ -213,7 +301,7 @@ void AppWindow::closeEvent(QCloseEvent *event) QMainWindow::closeEvent(event); } -void AppWindow::ConnectToDevice(QString serial) +bool AppWindow::ConnectToDevice(QString serial) { if(serial.isEmpty()) { qDebug() << "Trying to connect to any device"; @@ -256,10 +344,12 @@ void AppWindow::ConnectToDevice(QString serial) break; } } + return true; } catch (const runtime_error &e) { qWarning() << "Failed to connect:" << e.what(); DisconnectDevice(); UpdateDeviceList(); + return false; } } @@ -322,8 +412,13 @@ int AppWindow::UpdateDeviceList() if(device) { devices.insert(device->serial()); } + int available = 0; if(devices.size()) { for(auto d : devices) { + if(!parser.value("device").isEmpty() && parser.value("device") != d) { + // specified device does not match, ignore + continue; + } auto connectAction = ui->menuConnect_to->addAction(d); connectAction->setCheckable(true); connectAction->setActionGroup(deviceActionGroup); @@ -333,14 +428,15 @@ int AppWindow::UpdateDeviceList() connect(connectAction, &QAction::triggered, [this, d]() { ConnectToDevice(d); }); + ui->menuConnect_to->setEnabled(true); + available++; } - ui->menuConnect_to->setEnabled(true); } else { // no devices available, disable connection option ui->menuConnect_to->setEnabled(false); } - qDebug() << "Updated device list, found" << devices.size(); - return devices.size(); + qDebug() << "Updated device list, found" << available; + return available; } void AppWindow::StartManualControl() diff --git a/Software/PC_Application/appwindow.h b/Software/PC_Application/appwindow.h index 425a97b..0674153 100644 --- a/Software/PC_Application/appwindow.h +++ b/Software/PC_Application/appwindow.h @@ -18,6 +18,9 @@ #include #include #include +#include +#include "scpi.h" +#include "tcpserver.h" namespace Ui { class MainWindow; @@ -41,7 +44,7 @@ public: protected: void closeEvent(QCloseEvent *event) override; private slots: - void ConnectToDevice(QString serial = QString()); + bool ConnectToDevice(QString serial = QString()); void DisconnectDevice(); int UpdateDeviceList(); void StartManualControl(); @@ -84,6 +87,10 @@ private: QLabel lUnlock; Ui::MainWindow *ui; + QCommandLineParser parser; + + SCPI scpi; + TCPServer *server; }; #endif // VNA_H diff --git a/Software/PC_Application/main.cpp b/Software/PC_Application/main.cpp index 53e8549..2e47473 100644 --- a/Software/PC_Application/main.cpp +++ b/Software/PC_Application/main.cpp @@ -7,12 +7,22 @@ #include "Calibration/calkit.h" #include "touchstone.h" +#include + #include + +static QApplication *app; +static AppWindow *window; + +void sig_handler(int s) { + Q_UNUSED(s) + window->close(); +} + int main(int argc, char *argv[]) { - QApplication a(argc, argv); - AppWindow vna; - vna.resize(1280, 800); - vna.show(); - a.exec(); + app = new QApplication(argc, argv); + window = new AppWindow; + signal(SIGINT, sig_handler); + app->exec(); return 0; } diff --git a/Software/PC_Application/scpi.cpp b/Software/PC_Application/scpi.cpp new file mode 100644 index 0000000..150837f --- /dev/null +++ b/Software/PC_Application/scpi.cpp @@ -0,0 +1,160 @@ +#include "scpi.h" +#include + +SCPI::SCPI() : + SCPINode("") +{ + lastNode = this; + add(new SCPICommand("*LST", nullptr, [=](){ + QString list; + createCommandList("", list); + return list.trimmed(); + })); +} + +bool SCPI::match(QString s1, QString s2) +{ + if (s1.compare(s2, Qt::CaseInsensitive) == 0 + || s1.compare(alternateName(s2), Qt::CaseInsensitive) == 0 + || alternateName(s1).compare(alternateName(s2), Qt::CaseInsensitive) == 0 + || alternateName(s1).compare(alternateName(s2), Qt::CaseInsensitive) == 0) { + return true; + } else { + return false; + } +} + +QString SCPI::alternateName(QString name) +{ + while(name[name.size()-1].isLower()) { + name.chop(1); + } + return name; +} + +void SCPI::input(QString line) +{ + auto cmds = line.split(";"); + for(auto cmd : cmds) { + if(cmd[0] == ':' || cmd[0] == '*') { + // reset to root node + lastNode = this; + } + if(cmd[0] == ':') { + cmd.remove(0, 1); + } + cmd = cmd.toUpper(); + auto response = lastNode->parse(cmd, lastNode); + if(!response.isEmpty()) { + emit output(response); + } + } +} + +bool SCPINode::add(SCPINode *node) +{ + if(nameCollision(node->name)) { + qWarning() << "Unable to add SCPI node, name collision: " << node->name; + return false; + } + subnodes.push_back(node); + return true; +} + +bool SCPINode::add(SCPICommand *cmd) +{ + if(nameCollision(cmd->name())) { + qWarning() << "Unable to add SCPI node, name collision: " << cmd->name(); + return false; + } + commands.push_back(cmd); + return true; +} + +bool SCPINode::nameCollision(QString name) +{ + for(auto n : subnodes) { + if(SCPI::match(n->name, name)) { + return true; + } + } + for(auto c : commands) { + if(SCPI::match(c->name(), name)) { + return true; + } + } + return false; +} + +void SCPINode::createCommandList(QString prefix, QString &list) +{ + for(auto c : commands) { + if(c->queryable()) { + list += prefix + c->name() + "?\n"; + } + if(c->executable()) { + list += prefix + c->name() + '\n'; + } + } + for(auto n : subnodes) { + n->createCommandList(prefix + n->name + ":", list); + } +} + +QString SCPINode::parse(QString cmd, SCPINode* &lastNode) +{ + auto splitPos = cmd.indexOf(':'); + if(splitPos > 0) { + // have not reached a leaf, find next subnode + auto subnode = cmd.left(splitPos); + for(auto n : subnodes) { + if(SCPI::match(n->name, subnode)) { + // pass on to next level + return n->parse(cmd.right(cmd.size() - splitPos - 1), lastNode); + } + } + // unable to find subnode + return "ERROR"; + } else { + // no more levels, search for command + auto params = cmd.split(" "); + auto cmd = params.front(); + params.pop_front(); + bool isQuery = false; + if (cmd[cmd.size()-1]=='?') { + isQuery = true; + cmd.chop(1); + } + for(auto c : commands) { + if(SCPI::match(c->name(), cmd)) { + // save current node in case of non-root for the next command + lastNode = this; + if(isQuery) { + return c->query(); + } else { + return c->execute(params); + } + } + } + // couldn't find command + return "ERROR"; + } +} + +QString SCPICommand::execute(QStringList params) +{ + if(fn_cmd == nullptr) { + return "ERROR"; + } else { + return fn_cmd(params); + } +} + +QString SCPICommand::query() +{ + if(fn_query == nullptr) { + return "ERROR"; + } else { + return fn_query(); + } +} diff --git a/Software/PC_Application/scpi.h b/Software/PC_Application/scpi.h new file mode 100644 index 0000000..c32ed9f --- /dev/null +++ b/Software/PC_Application/scpi.h @@ -0,0 +1,63 @@ +#ifndef SCPI_H +#define SCPI_H + +#include +#include +#include +#include + +class SCPICommand { +public: + SCPICommand(QString name, std::function cmd, std::function query) : + _name(name), + fn_cmd(cmd), + fn_query(query){} + + QString execute(QStringList params); + QString query(); + QString name() {return _name;} + bool queryable() { return fn_query != nullptr;}; + bool executable() { return fn_cmd != nullptr;}; +private: + const QString _name; + std::function fn_cmd; + std::function fn_query; +}; + +class SCPINode { + friend class SCPI; +public: + SCPINode(QString name) : + name(name){} + + bool add(SCPINode *node); + bool add(SCPICommand *cmd); + +private: + QString parse(QString cmd, SCPINode* &lastNode); + bool nameCollision(QString name); + void createCommandList(QString prefix, QString &list); + const QString name; + std::vector subnodes; + std::vector commands; +}; + +class SCPI : public QObject, public SCPINode +{ + Q_OBJECT +public: + SCPI(); + + static bool match(QString s1, QString s2); + static QString alternateName(QString name); + +public slots: + void input(QString line); +signals: + void output(QString line); + +private: + SCPINode *lastNode; +}; + +#endif // SCPI_H diff --git a/Software/PC_Application/tcpserver.cpp b/Software/PC_Application/tcpserver.cpp new file mode 100644 index 0000000..c0655af --- /dev/null +++ b/Software/PC_Application/tcpserver.cpp @@ -0,0 +1,40 @@ +#include "tcpserver.h" +#include + +TCPServer::TCPServer(int port) +{ + qInfo() << "Listening on port" << port; + socket = nullptr; + server.listen(QHostAddress::Any, port); + connect(&server, &QTcpServer::newConnection, [&](){ + // only one connection at a time + delete socket; + socket = server.nextPendingConnection(); + connect(socket, &QTcpSocket::readyRead, [=](){ + if(socket->canReadLine()) { + auto available = socket->bytesAvailable(); + char data[available+1]; + socket->readLine(data, sizeof(data)); + auto line = QString(data); + emit received(line.trimmed()); + } + }); + connect(socket, &QTcpSocket::stateChanged, [&](QAbstractSocket::SocketState state){ + if (state == QAbstractSocket::UnconnectedState) + { + socket->deleteLater(); + socket = nullptr; + } + }); + }); +} + +bool TCPServer::send(QString line) +{ + if (socket) { + socket->write(QByteArray::fromStdString(line.toStdString()+'\n')); + return true; + } else { + return false; + } +} diff --git a/Software/PC_Application/tcpserver.h b/Software/PC_Application/tcpserver.h new file mode 100644 index 0000000..e888901 --- /dev/null +++ b/Software/PC_Application/tcpserver.h @@ -0,0 +1,23 @@ +#ifndef TCPSERVER_H +#define TCPSERVER_H + +#include +#include +#include + +class TCPServer : public QObject +{ + Q_OBJECT +public: + TCPServer(int port); + +public slots: + bool send(QString line); +signals: + void received(QString line); +private: + QTcpServer server; + QTcpSocket *socket; +}; + +#endif // TCPSERVER_H