Implementation of some SCPI commands via TCP

This commit is contained in:
Jan Käberich 2021-04-11 00:10:22 +02:00
parent f1d52f159b
commit a15d02f217
15 changed files with 627 additions and 19 deletions

View File

@ -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<nlohmann::json*>(index.internalPointer());
auto info = jsonInfo.at(item);
if(role == Qt::CheckStateRole)

View File

@ -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);
}));
}

View File

@ -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;
};

View File

@ -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;
}
}

View File

@ -24,6 +24,7 @@ signals:
public slots:
void setLevel(double level);
void setFrequency(double frequency);
void setPort(int port);
protected:
void timerEvent(QTimerEvent *) override;

View File

@ -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 \

View File

@ -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<unsigned long>::max();
}
auto newval = params[0].toULong(&ok);
if(!ok) {
return std::numeric_limits<unsigned long>::max();
} else {
return newval;
}
};
scpi_freq->add(new SCPICommand("SPAN", [=](QStringList params) -> QString {
auto newval = toULong(params);
if(newval == std::numeric_limits<unsigned long>::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<unsigned long>::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<unsigned long>::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<unsigned long>::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<unsigned long>::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<unsigned long>::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<unsigned long>::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();

View File

@ -9,8 +9,9 @@
#include "Device/device.h"
#include <functional>
#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<void (Device::TransmissionResult)> cb = nullptr);
void ConstrainAndUpdateFrequencies();

View File

@ -48,23 +48,106 @@
#include "Calibration/receivercaldialog.h"
#include <QDebug>
#include "CustomWidgets/jsonpickerdialog.h"
#include <QCommandLineParser>
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++;
}
} 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()

View File

@ -18,6 +18,9 @@
#include <QButtonGroup>
#include <QCheckBox>
#include <QLabel>
#include <QCommandLineParser>
#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

View File

@ -7,12 +7,22 @@
#include "Calibration/calkit.h"
#include "touchstone.h"
#include <signal.h>
#include <complex>
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;
}

View File

@ -0,0 +1,160 @@
#include "scpi.h"
#include <QDebug>
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();
}
}

View File

@ -0,0 +1,63 @@
#ifndef SCPI_H
#define SCPI_H
#include <QString>
#include <QObject>
#include <vector>
#include <functional>
class SCPICommand {
public:
SCPICommand(QString name, std::function<QString(QStringList)> cmd, std::function<QString()> 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<QString(QStringList)> fn_cmd;
std::function<QString()> 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<SCPINode*> subnodes;
std::vector<SCPICommand*> 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

View File

@ -0,0 +1,40 @@
#include "tcpserver.h"
#include <QDebug>
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;
}
}

View File

@ -0,0 +1,23 @@
#ifndef TCPSERVER_H
#define TCPSERVER_H
#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>
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