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

    Интро


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

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

    Начало


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

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

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

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


    В заголовочном файле все стандартно и неинтересно:
    #ifndef SIGGENCONTROL_H
    #define SIGGENCONTROL_H
    
    #include <QMainWindow>
    #include <QTcpSocket>
    #include <QString>
    
    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
    


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



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

    port = 5025;
    host = "192.168.1.109";
    

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

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


    Все самое интересное начинается в .cpp файле:
    #include "siggencontrol.h"
    #include "ui_siggencontrol.h"
    #include "qdebug.h"
    #include <QTime> 
    
    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" );
      }
    
    }


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


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

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

    Похожие публикации

    Комментарии 14

      +3
      А нахера тащить сюда эту простынку бесполезного кода?
        –1
        Для тех людей, кому это может потенциально быть интересно :) (не для вас)
          +3
          Но это же какой-то позор, разве нет?
            0
            Обычный код студента. Что в нём позорного?
              +1
              Для студента — ничего. Для ресурса, который позиционируется, как «профессиональный» — в этой статье позорно практически все.
            0
            Возможно стоит отполировать код? Хабр должен учить хорошему. Например хотя бы разделить ui от логики.
            Почему выбрали виджеты а не тот же qml?
          +2
          Ну раз статья фактически состоит из кода, позволю себе его прокомментировать. Я сам начинал трудовую деятельность с предприятия, занимающегося ракетно-космической тематикой, поэтому хорошо представляю, как там любят аббривеатуры и какими предпочтениями обладают местные ветераны-программисты (из тех, кто застал становление компьютерной отрасли в СССР). Они безусловно молодцы, но я очень не рекомендую бездумно повторять за ними. Имена переменных и функций в вашем проекте могут показаться совершенно неочевидными для другого разработчика. Попробуйте не использовать аббривеатуры (кроме общепринятых, известных за пределами вашего предприятия), а так же не сокращать слова. Кроме того, не совсем понятно, зачем некоторые переменные-члены класса sigGenControl сделаны публичными.
            0

            Радиозавод?

              0
              Нет, ведущее предприятие по Глонассу
                0

                Отлично, везде одно и то же.

              +2
              Pre
              Очень не хотел комментировать (ибо карма сливается только в путь даже от положительных комментариев), но все же.


              На втором курсе я устроился на работу в одно КБ, в котором было необходимо осуществить контроль над несколькими приборами разом для полной автоматизации калибровки и поверки (без этого не давали сертификацию). Зоопарк протоколов был такой: USB (SCPI), RS232 (MODBUS), RS232 (BINARY), LAN (SCPI) + наш бинарный через RS485/RS232.
              Заголовок спойлера
              Выглядело примерно вот так
              image

              Писалось все это на Qt за неимением лучшей альтернативы.
              Постараюсь прокомментировать все, что выше:
              >Он довольно прост и интуитивно понятен
              Нет

              1. На интерфейсе два непонятных EDIT'а, неподписанных и вставленных в столбик. Раз вы предполагаете отдать кому-то этот софт — то как должен человек понять что это за два поля? Мы обычно делали так:
              «Адрес: ________:___ (?)» и ни у одного старца проблемы не возникало.

              2. Опять же по интерфейсу. Стильно-модно-молодежная ручка это стильно-модно-молодежно, но судя по 5164803 kHz установить с ее помощью например 20 кHz будет очень трудно. Нужно поле для точного ввода. У нас были АКИП`ы-3402, поэтому приходилось максимально увеличивать сходство — был переключатель Синус\Пила\Прям\Имп\Шум (кстати, где он у вас?), была крутилка и точный ввод.

              3. Все эти host: not set/CONNECT TO DEVICE (кстати, почему капс?) понятны только вам (ну и еще одному англочитающему со словарем в другом корпусе предприятия). Раз софт для русского предприятия, то все сообщения должны быть на русском и максимально понятными.

              Теперь по коду:
              4. :OUTP 0\n и прочее — выносите в константы, если не планируете делать другой протокол. Если планируете — то создайте класс, который по входному сокету будет отправлять нужные данные по команде например setFrequency(value)

              5. ui->radioButton

              6. qDebug.

              7. cmd+ui->freqEdit->text()+fr+"\n" — После выполнения пункта 4 эта небезопасная хреновина исчезнет. НИКОГДА не позволяйте пользователю отправлять что-либо на устройство без защитных проверок. У нас одну девайсину из-за этого пришлось отдать в ремонт. Все, что отправляется в устройство должно контролироваться и строго соответствовать ее протоколам.

              8. У вас вся бизнес-логика в GUI потоке, что значит, что весь интерфейс будет висеть пока данные не отправятся (а в сети возможно всё).

              9. Нет проверки того, что значение установилось. Можно сколько угодно делать write, но до тех пор, пока вы не узнали результат выполнения команды (man SCPI ?) это все без толку. Любой сотрудник рядом с генератором может покрутить ручку и у вас на интерфейсе будет 100, а в реальности 500, что может погубить тестируемое устройство (а оно стоит дороже, чем ваша разработка).

              10. Код должен быть структурирован, у вас это сейчас просто каша из сигналов.
                0
                Минимум с десяток ошибок в коде.
                В QObject:: connect парные аргументы это адреса функций а не их имена.

                Такой код не скомпилируется
                image
                  –1
                  Читайте документацию лучше. Все скомпилируется без ошибок.
                    0
                    Далекий вам будет путь к программисту с таким подходом.
                    Это вызов статических функций а не обращение к адресу.
                    Проверяйте код перед публикацией и после тоже.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое