Об опыте общения с генератором сигнала через QTcpSocket и SCPI

Интро


На третьем курсе я пришел на практику в одно из предприятий отечественной ракетно-космической отрасли. Стажером я был амбициозным и довольно активным пока не повидал рутинной работы на российском госпредприятии и по своей просьбе был распределен в отдел, занимающийся разработкой контрольно-проверочной аппаратуры(далее КПА). К слову, большой частью разработки КПА является написание программной части для оборудования. В том числе, для проверки разрабатываемой аппаратной части в отделе имеются различные контрольно-измерительные устройства, все они подключены к общей сети. В итоге, в качестве одного из первых моих заданий стало написание приложения для управления генератором сигнала.

Об этом в статье и пойдет речь.

Начало


Все приложения в отделе разрабатываются средствами C++ и библиотеки Qt. Опыт работы с данным фреймворком у меня был, поэтому с этой стороны никаких трудностей не возникло. К тому же у Qt есть обширная документация, а еще всегда можно копипастнуть код со StackOverflow проконсультироваться у куратора.

Поскольку все устройства подключены к одной сети, то вопрос, как к ним подключаться тоже решился очень быстро — используем сетевую часть Qt в виде QTcpSocket.

Самый интересный вопрос возник, когда пришлось решить, как именно общаться с данными устройствами, как активировать ту или иную функцию, передать то или иное значение. Тогда же оказалось, что все довольно тривиально: существует протокол стандартных команд для программируемых инструментов — SCPI. Он позволяет с помощью стандартный команд управлять любыми устройствами, поддерживающими данный стандарт.

Начинаем кодить


В заголовочном файле все стандартно и неинтересно:
#ifndef SIGGENCONTROL_H
#define SIGGENCONTROL_H

#include 
#include 
#include 

namespace Ui {
class sigGenControl;
}

class sigGenControl : public QMainWindow
{
    Q_OBJECT

public:
    explicit sigGenControl(QWidget *parent = 0);
    ~sigGenControl();
//изначально хост и порт были заданы статически, но теперь задаются через интерфейс
    QString host; //= "192.168.1.109"; ip нашего устройства
    int port;// = 5025; порт устройства
    void clearErr(); //процедура очистки ошибок
    bool rfOn; //включен ли вч выход
    bool pset = false; 
    bool hset = false;

private:
    Ui::sigGenControl *ui;
    QTcpSocket* socket;

private slots:
//слоты, подробно будут описаны в .cpp файле
    void connectToHostSlot();
    void sendToHostSlot();
    void readyReadSlot();
    void setFreq();
    void setPow();
    void activateRF();
    void checkErrSlot();
    void setPort();
    void setHost();
    void setDefault();
    void dialValChangedSlot();
};

#endif // SIGGENCONTROL_H



Интерфейс решено было сделать таким:

tb43harz2kczpsdsfjhc0t-mvci.png

Он довольно прост и интуитивно понятен. В двух лайнэдитах вверху задаются хост и порт устройства. Так же имеется возможность выбрать стандартные значения, тогда они примут следующий вид:

port = 5025;
host = "192.168.1.109";


Далее идет текстовое поле лога, ответа от устройства (туда же будут приходить ошибки, если они есть). Чуть ниже находятся кнопки соединения с устройством, отправки команды, проверки ошибок. В трех последних лайнэдитах можно либо задать свою команду и отправить ее на устройство, либо отдельно задать частоту и амплитуду. Радиокнопка справа включает/выключает ВЧ выход. Крутилка регулирует частоту, когда чекбокс снят и амплитуду, когда активирован.

Продолжаем и заканчиваем


Все самое интересное начинается в .cpp файле:
#include "siggencontrol.h"
#include "ui_siggencontrol.h"
#include "qdebug.h"
#include  

sigGenControl::sigGenControl(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::sigGenControl)
{
    ui->setupUi(this);
    ui->history->setReadOnly(true);
    ui->history->setText("host : not set\nport : not set");
    ui->history->append(QTime::currentTime().toString() + " : No connection");
    socket = new QTcpSocket(this); //создаем новый экземпляр класса сокета
//через него будем осуществлять все операции по передаче-приему данных
//соединим нужные сигналы со слотами, большая часть не нуждается в комментариях
//в прицнипе ни один из коннектов не нуждается в подробном описании
    connect(ui->connPb,QPushButton::clicked,this,sigGenControl::connectToHostSlot);
    connect(ui->sendToHostPb,QPushButton::clicked,this,sigGenControl::sendToHostSlot);
    connect(ui->input,QLineEdit::returnPressed,this,sigGenControl::sendToHostSlot);
    connect(socket,QTcpSocket::readyRead,this,sigGenControl::readyReadSlot);
    connect(ui->freqEdit,QLineEdit::returnPressed,this,sigGenControl::setFreq);
    connect(ui->amptdEdit,QLineEdit::returnPressed,this,sigGenControl::setPow);
    connect(ui->radioButton,QRadioButton::clicked,this,sigGenControl::activateRF);
    connect(ui->errPb,QPushButton::clicked,this,sigGenControl::clearErr);
    connect(ui->hostEdit,QLineEdit::returnPressed,this,sigGenControl::setHost);
    connect(ui->portEdit,QLineEdit::returnPressed,this,sigGenControl::setPort);
    connect(ui->checkBox,QCheckBox::clicked,this,sigGenControl::setDefault);
    connect(ui->dial, QDial::valueChanged,this,sigGenControl::dialValChangedSlot);


    ui->hist->setReadOnly(true);
    QString deactRF = ":OUTP 0\n"; //та самая SCPI команда, в данном случае на выкл ВЧ выхода
    socket->write(deactRF.toLocal8Bit()); //записываем команду в сокет и отправляем
    this->rfOn = false; //снимаем флаг активности

    ui->input->setReadOnly(true);
    ui->freqEdit->setReadOnly(true);
    ui->amptdEdit->setReadOnly(true);
    ui->radioButton->setEnabled(false);
    ui->sendToHostPb->setEnabled(false);
    ui->errPb->setEnabled(false);
    ui->connPb->setDisabled(true);

}

sigGenControl::~sigGenControl()
{
    delete ui;
}

void sigGenControl::connectToHostSlot(){
  socket->connectToHost(host,port); //соединяемся с хостом
//ждем соединения и выводим в qDebug() в случае успеха
  if (socket->waitForConnected(1000))
        qDebug("Connected!");
  ui->history->append(QTime::currentTime().toString() + " : Connected"); //добавляем в лог

  ui->input->setReadOnly(false);
  ui->freqEdit->setReadOnly(false);
  ui->amptdEdit->setReadOnly(false);
  ui->radioButton->setEnabled(true);
  ui->sendToHostPb->setEnabled(true);
  ui->errPb->setEnabled(true);
  ui->input->setText("*IDN?"); //запрос на идентификацию устройства
}

void sigGenControl::sendToHostSlot(){
//слот отправляет содержимое лайнэдита хосту
    socket->write(ui->input->text().toLocal8Bit()+"\n");
}

void sigGenControl::readyReadSlot(){
//если есть готовность к считыванию
    qDebug("ready read!");
    QByteArray dataArr;
//считываем все с сокета
    dataArr = socket->readAll();
//добавляем в лог
    ui->hist->append(QTime::currentTime().toString() + " " + dataArr);
}

void sigGenControl::clearErr(){
//считываем ошибки, находящиеся в очереди, тем самым, очищая ее
    QString errTxt = ":SYST:ERR?\n";
    socket->write(errTxt.toLocal8Bit());
}

void sigGenControl::setFreq(){
    QString fr = " kHz";
    QString cmd = ":FREQ "; //команда для установки частоты
    QString command = cmd+ui->freqEdit->text()+fr+"\n"; //формируем запрос
    qDebug() << command;
    socket->write(command.toLocal8Bit()); //записываем в сокет
}

void sigGenControl::setPow(){
    QString amp = " dBm";
    QString cmd = ":POW "; //команда для установки амплитуды
    socket->write(cmd.toLocal8Bit() + ui->amptdEdit->text().toLocal8Bit() + amp.toLocal8Bit() + "\n"); //формируем запрос
}

void sigGenControl::activateRF(){
//команда для установки выхода в положение ВКЛ
//включает ВЧ выход
    QString actRF = ":OUTP 1\n"; //команда на ВКЛ
    QString deactRF = ":OUTP 0\n"; //команда на ВЫКЛ
    if(this->rfOn == false){
    socket->write(actRF.toLocal8Bit());
    rfOn = true;
    }else{
        socket->write(deactRF.toLocal8Bit());
        rfOn = false;
    }
}

void sigGenControl::checkErrSlot(){
    clearErr();
}

void sigGenControl::setHost(){
//устанавливаем значение хоста
   this->host = ui->hostEdit->text();
    ui->history->append("host: " + host);
    ui->checkBox->setEnabled(true);
    hset = true; //ставим флаг 
    if((pset && hset) == true){
        ui->connPb->setEnabled(true);
    }
}

void sigGenControl::setPort(){
//устанавливаем значение порта
   this->port = ui->portEdit->text().toInt();
    ui->history->append("port: "  + QString::number(port));
    ui->checkBox->setEnabled(true);
    pset = true; //ставим флаг 
    if((pset && hset) == true){
        ui->connPb->setEnabled(true);
    }
}

void sigGenControl::setDefault(){
//установка дефолтного хоста и порта
    port = 5025;
    host = "192.168.1.109";
     ui->history->append(QTime::currentTime().toString() + " " + "host: " + host);
     ui->history->append(QTime::currentTime().toString() + " " + "port: "  + QString::number(port));
     ui->checkBox->setDisabled(true);
     ui->connPb->setEnabled(true);
     ui->hostEdit->setText(host);
     ui->portEdit->setText(QString::number(port));

}

void sigGenControl::dialValChangedSlot(){
//нужно только для отладки, выдает значение дайала при его изменении
   qDebug()<< "value : " << ui->dial->value();

  if(ui->amplCheckBox->isChecked() == false){
//если это частота, установим пределы значений для крутилки
      ui->dial->setMinimum(100); 
      ui->dial->setMaximum(20000000);
      QString fr = " kHz";
      QString cmd = ":FREQ "; //команда на изменение частоты
      QString command = cmd+QString::number(ui->dial->value())+fr+"\n";
      qDebug() << command;
      socket->write(command.toLocal8Bit());
      ui->label->setText("FREQUENCY :" + QString::number(ui->dial->value()) + " kHz" );

  }else if(ui->amplCheckBox->isChecked() == true){
//если это амплитуда, установим другие пределы значений для крутилки
      ui->dial->setMinimum(-130);
      ui->dial->setMaximum(15);
      QString pw = " dBm";
      QString cmd = ":POW "; //команда на изменение амплитуды
      QString command = cmd+QString::number(ui->dial->value())+pw+"\n";
      qDebug() << command;
      socket->write(command.toLocal8Bit());
      ui->label->setText("AMPLITUDE :" + QString::number(ui->dial->value()) + " dBm" );
  }

}


После прочтения


Понимаю, что статья может показаться оторванной от жизни и реальности для большого количества читателей, но удаленное управление контрольно-измерительной аппаратурой в сфере инженерии — достаточно распространенная тема, которая приносит много пользы и удобства (например, вам не нужно бегать к приборам и нажимать кнопочки).

В остальном это статья носит информационно-развлекательный характер и нацеленна, скорее, на энтузиастов и на людей, занимающихся разработкой и тестированием плат и другого железа. Остальным просто хотел бы рассказать о своем небольшом опыте разработки ПО для таких специфических целей.

© Habrahabr.ru