EEVblog Electronics Community Forum

Products => Computers => Programming => Topic started by: No.15 on August 22, 2020, 01:59:48 am

Title: C++ question
Post by: No.15 on August 22, 2020, 01:59:48 am
I am trying to adapt a c++ program to work with my DC Load
in the program I have 2 measure lines
port.write("MEAS:CURR?/r");
port.write("MEAS:VOLT?/r");

Is there a way to append an "A" to the current output when being read that is easy?  (I am a total programming noob)

The reason why is that later in the program it's looking for this "A".  The load it was originally used for gave its measurements with the unit (V, A), but my load just returns numbers for each.

Code: [Select]
 double value = 0;
    bool isCurrent = false;

    QString tmp = port.readAll();

    tmp = tmp.left(tmp.length() - 1);

    if (tmp.at(tmp.length() - 1) == 'A')
        isCurrent = true;
Title: Re: C++ question
Post by: ledtester on August 22, 2020, 02:46:15 am
Are you asking how to write a string that has the form:  "MEAS:CURR xxx A\r" where "xxx" is a number that comes from a variable?

If so, look at the sprintf() function, e.g. http://cplusplus.com/reference/cstdio/sprintf/ (http://cplusplus.com/reference/cstdio/sprintf/)
Title: Re: C++ question
Post by: AlfBaz on August 22, 2020, 02:53:22 am
I'm assuming you can not change the code in the device returning the current value (amps).
Based on what you have shown us you will have to do a read between GPIB calls

I'm not familiar with the QString class but a quick search on the net shows it has an overloaded append member function which you could us to add the A to the returned value.
You could then append the return value of the voltage measurement to the same string since the append function handles single chars and strings
Title: Re: C++ question
Post by: AlfBaz on August 22, 2020, 03:02:10 am
Scratch the append function, you would have to use the insert at length-1 otherwise you would have a '/r' between the number and the A
Just check what the device is actually sending back in case its returning /r/n or maybe a space between the number and the '/r'
Title: Re: C++ question
Post by: langwadt on August 22, 2020, 03:09:30 am
just skip the part that checks for 'A'?
Title: Re: C++ question
Post by: No.15 on August 22, 2020, 03:07:24 pm
Thank you everyone who replied.
ledtester I will look at sprintf and see what it does.  Part of this process is to force me to learn and c++ might not be the best first choice but this program does exactly what I want but for another similar device.

AlfBaz
I was looking at QString obviously because it's already in there.  the /r and /n don't get returned in the output they are jus CR and newline
The output from those commands both look like this
Code: [Select]
OK
XX.XXXX

With the X representing a numeral

langwadt
I have to have some wqay to differentiate the amerage and the voltage.  If V = 0 or V= (X) a variable I set the program terminates the test and graphs the results.

When the test starts it reads it can't tell V from A , of course A = 0 at the start of the test so it terminates.

I guess I should show the code, I got it on github



Code: [Select]
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <string>

void MainWindow::update()
{
    //qDebug() << "On timer";
    port.write("MEAS:CURR?/r");
    port.write("MEAS:VOLT?/r");

    time.start();
}

void MainWindow::readyRead()
{
    int addms = time.elapsed();

    double value = 0;
    bool isCurrent = false;

    QString tmp = port.readAll();

    tmp = tmp.left(tmp.length() - 1);

    if (tmp.at(tmp.length() - 1) == 'A')
        isCurrent = true;

    value = tmp.left(tmp.length() - 1).toDouble();

    if (isCurrent)
    {
        lastCurrent = value;

        if (lastCurrent > highestCurrent)
                highestCurrent = lastCurrent;

        return;
    } else
        if (value <= ui->sbCutVolts->value())
        {
            on_btnStop_clicked();

            QMessageBox msgBox;
            msgBox.setText("Measurement finished.");
            msgBox.setInformativeText(QString("Voltage %1 reached cut value %2. Result is: %3mAh").arg(value).arg(ui->sbCutVolts->value()).arg(ah));
            msgBox.setStandardButtons(QMessageBox::Ok);
            msgBox.setDefaultButton(QMessageBox::Ok);
            msgBox.exec();

            return;
        }

    lastVoltage = value;

    if (value > highestVolt)
            highestVolt = value;

    ah += ((lastCurrent * (1 + addms / 1000)) / 60 / 60) * 1000;

    seriesVolts->append(ah, lastVoltage);
    seriesCurrent->append(ah, lastCurrent);

    axisX->setRange(-2, ah + 2);
    axisY->setRange(ui->sbCutVolts->value(), highestVolt + 0.5);
    axisY2->setRange(0, highestCurrent + 0.5);

    ui->lblResult->setText(QString("%1 mAh").arg(ah));

    timer->start();
}

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    ah = 0;
    highestVolt = 0;
    highestCurrent = 0;
    lastCurrent = 0;
    lastVoltage = 0;

    for (int i = 0; i < QSerialPortInfo::availablePorts().count(); i++)
        ui->cbPort->addItem(QSerialPortInfo::availablePorts().at(i).portName());

    timer = new QTimer(this);
    timer->setInterval(1000);
    timer->setSingleShot(true);

    connect(timer, SIGNAL(timeout()), this, SLOT(update()));
    connect(&port, SIGNAL(readyRead()), this, SLOT(readyRead()));

    QChart *chart = new QChart();

    seriesVolts = new QLineSeries();
    seriesVolts->setName("Battery voltage");

    seriesCurrent = new QLineSeries();
    seriesCurrent->setName("Battery current");

    chart->addSeries(seriesVolts);
    chart->addSeries(seriesCurrent);

    chart->setTitle("Discharge chart");

    axisX = new QValueAxis;
    axisX->setTitleText("Capacity (mAh)");
    chart->addAxis(axisX, Qt::AlignBottom);
    seriesVolts->attachAxis(axisX);
    seriesCurrent->attachAxis(axisX);

    axisY = new QValueAxis;
    axisY->setLabelFormat("%.2f");
    axisY->setTitleText("Voltage (V)");
    chart->addAxis(axisY, Qt::AlignLeft);
    seriesVolts->attachAxis(axisY);

    axisY2 = new QValueAxis;
    axisY2->setLabelFormat("%.2f");
    axisY2->setTitleText("Current (A)");
    chart->addAxis(axisY2, Qt::AlignRight);
    seriesCurrent->attachAxis(axisY2);

    chartView = new QChartView(chart);
    ui->verticalLayout->insertWidget(1, chartView);
}

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

void MainWindow::on_btnStart_clicked()
{
    ah = 0;
    highestVolt = 0;
    lastCurrent = 0;
    lastVoltage = 0;

    ui->lblResult->setText("0");

    seriesVolts->clear();
    seriesCurrent->clear();

    port.write(":INPUT OFF\n");
    port.write(":CURR 0\n");
    port.write(":MODE CC\n");
    port.write(QString(":CURR %1\n").arg(double(ui->sbCurrent->value()) / 1000).toStdString().c_str());
    port.write(":INPUT ON\n");

    timer->start();
    ui->btnStop->setEnabled(true);
    ui->btnStart->setEnabled(false);
}

void MainWindow::on_btnOpen_clicked()
{
    port.setPortName(ui->cbPort->currentText());
    port.setBaudRate(QSerialPort::Baud115200);
    port.setDataBits(QSerialPort::Data8);
    port.setParity(QSerialPort::NoParity);
    port.setStopBits(QSerialPort::OneStop);
    port.setFlowControl(QSerialPort::NoFlowControl);

    port.open(QSerialPort::ReadWrite);

    if (port.isOpen())
    {
        ui->btnOpen->setEnabled(false);
        ui->btnClose->setEnabled(true);
        ui->btnStart->setEnabled(true);
    }
}

void MainWindow::on_btnClose_clicked()
{
    on_btnStop_clicked();

    Sleep(1000);

    port.close();

    if (port.isOpen() == false)
    {
        ui->btnOpen->setEnabled(true);
        ui->btnClose->setEnabled(false);
        ui->btnStart->setEnabled(false);
    }
}

void MainWindow::on_btnStop_clicked()
{
    timer->stop();
    port.write(":INPUT OFF\n");
    port.write(":CURR 0\n");
    port.write(":MODE CC\n");
    port.write(":INPUT OFF\n");

    ui->btnStart->setEnabled(true);
    ui->btnStop->setEnabled(false);
}

void MainWindow::on_btnRequest_clicked()
{

}


Example output from a terminal

Title: Re: C++ question
Post by: MarkF on August 22, 2020, 03:32:38 pm
It looks like your Github project was developed with Qt.
That is a heavy lift for a programmer NOOB.
Qt is a cross-platform development library which also includes GUI creation tools.
See:  https://www.qt.io/ (https://www.qt.io/)

There use to be a 'free' version you could download but I can't find it.
The commercial licenses are very expensive.
Title: Re: C++ question
Post by: No.15 on August 22, 2020, 03:44:29 pm
It looks like your Github project was developed with Qt.
That is a heavy lift for a programmer NOOB.
Qt is a cross-platform development library which also includes GUI creation tools.
See:  https://www.qt.io/ (https://www.qt.io/)

There use to be a 'free' version you could download but I can't find it.
The commercial licenses are very expensive.

They have a community version that is free.  I downloaded it and I am using it but thankfully all the heavy lifting was done by the original coder.  I am just trying to modify it to work with my DC Load.  The program runs, but errors because of the condition I explained above.  I agree that I am mostly lost here :)
Title: Re: C++ question
Post by: MarkF on August 22, 2020, 04:11:30 pm
It would be helpful if you gave us a link to the Github project.
There is too much code missing to be able to answer your question.
Title: Re: C++ question
Post by: No.15 on August 22, 2020, 04:16:33 pm
It would be helpful if you gave us a link to the Github project.
There is too much code missing to be able to answer your question.

Yes sorry, good idea  https://github.com/r1ka/qDischarge
Title: Re: C++ question
Post by: MarkF on August 22, 2020, 06:57:06 pm
I was mainly interested to see how port was declared.
    line 58    QSerialPort port;

The QString tmp = port.readAll(); reads directly from your DC Load.
It appears the DC Load just returns the value without any indication of what was requested???

The update() requests both current and voltage values and then waits for a response from the DC Load of both values.

Code: [Select]
void MainWindow::update()
{
    port.write(":MEAS:CURR?\r\n");
    port.write(":MEAS:VOLT?\r\n");
}

You really need to separate these two requests and keep track of each request. 
Request the current and wait for response.  Then, request the voltage and wait for that response.
Probably the best way would be to setup a flag of what the request was in the update() and then clear that flag in the readyRead().
(i.e. The update() function would alternate its requests and insuring a response.)

Code: [Select]
void MainWindow::readyRead()
{
    if (tmp.at(tmp.length() - 1) == 'A')
        isCurrent = true;

    if (isCurrent)
    {
    } else
        if (value <= ui->sbCutVolts->value())
        {
        }
}

Replace isCurrent with an enumeration flag
(i.e. flag is 0=none, 1=current, 2=current_received, 3=voltage, 4=voltage_received).

Code: [Select]
void MainWindow::update()
{
    if (flag==0 || flag==4) {
        port.write(":MEAS:CURR?\r\n");
        flag = 1;
    }
    else if (flag==2) {
        port.write(":MEAS:VOLT?\r\n");
        flag = 3;
    }
}

void MainWindow::readyRead()
{
   QString tmp = port.readAll();
    switch (flag)
    {
        case 1:  // Current
            flag = 2;
            break;
        case 3:  // Voltage
            flag = 4;
            break;
    }
    timer->start();
}
Title: Re: C++ question
Post by: No.15 on August 23, 2020, 02:11:28 pm
MarkF
This was very helpful, thank you.  My program is still not working correctly but I am learning a lot.

Most of the issues now stem from the fact that when I query the load it always answers with an "OK", sometimes twice before it returns the value I want.

Probably should try to redo this in Python, but even as a noob the whitespace thing drives me crazy :) 

I am going to contact Amrel and see if there is a way to suppress the OK message as it is really unnecessary.  There is no way to do it listed in documentation.
Title: Re: C++ question
Post by: Nominal Animal on August 23, 2020, 03:36:14 pm
There use to be a 'free' version you could download but I can't find it.
The LGPL licensed sources are here (https://www.qt.io/download-open-source).

In essence, if you just use Qt without modifying it, you can rely on the LGPL license even in commercial projects.  If you do modify Qt, you need to provide the sources to the modified version of Qt to your customers.  In no case do you need to provide the sources of your own commercial project, because LGPL expressly allows incorporation into a proprietary product, as long as any modifications to the LGPL-licensed part are also provided per the LGPL license.

What I would recommend here, is to use a separate thread (QThread (https://doc.qt.io/qt-5/qthread.html)) that communicates with the device in a simple loop.  During each iteration, it would first query the current, ignore any superfluous OK responses, until it gets an answer; then query the voltage, ignore any superfluous OK responses, until it gets an answer.

In C++, I would use a pair of global QAtomicInt (https://doc.qt.io/qt-5/qatomicint.html)s to represent the current and voltage, scaled by a suitable power of ten.  (For example, you could have them represent the current in milliamps and voltage in millivolts, for three decimal digits when displaying in volts or amps.)
That way you need no synchronization at all between the device communicating thread and the main thread, and the main thread can simply display the value whenever.

In Python, I would use a Queue.  (Well, in fact, I have used: a year or so ago I mentioned on this forum that I was thinking of writing a tutorial on how to use Python, Qt, and threads in Linux or Macs to create an user interface to control a microcontroller project using a graphical user interface.  In Linux or Macs, because Python termios module can be used to talk to the device (if it has an USB serial interface) without any extra libraries.  I don't particularly like libusb, as there really are two different versions, and one of them may actually just be a translation shim to the other version, making it actually quite fragile in real world use: if it works for you, it just works; but if there is an issue, it is hellishly annoying to find out which part causes it, due to libusb version differences.)

I have not checked out the project, but if the UI can be used to send commands to the device, then you need a QMutex (https://doc.qt.io/qt-5/qmutex.html) and two QList (https://doc.qt.io/qt-5/qlist.html)s between the main thread and the device thread (that are accessed only when holding the QMutex).  Instead of looping forever, the device thread checks if the command list has any commands in it.  If it does, it sends that command to the device, and reads the response, and appends the response (or just a "command X sent") to the result list.  You can also just append the voltage and current changes to the result list without using QAtomicInts; I would personally check if one of the last two values is a previous voltage (if appending voltage) or current (if appending current), and replace that value.
The main thread update function must consume all results per invocation.

It would be best if both command and response objects had an identifier, say a nonzero unsigned long that the main thread increments whenever it sends a new command; and perhaps an int or enum to describe the type of the result.  Then, "idle" voltage and current readings would have a zero identifier.

When the user pushes a button or something causing a command to be sent, the UI can show that the command is being processed, by comparing the last sent command ID to the response ID last received.  As long as the response ID (is nonzero but) does not match, the device is busy processing commands.  When the last nonzero ID matches the command ID last sent, the device has responded to all commands, and is now idle.  So, in the Qt idle/update function, you just check the last sent command ID to the last received nonzero IDs (that the main thread must maintain itself), and if they do not match, it shows "busy" in the UI; otherwise "idle".  This way the user has reliable feedback on whether the device has responded to a command yet or not.

Instead of working all this out in a large existing project, you can work out the details by creating a test program that does this; it does not even have to have a window per se.  Have each thread print what it is doing to standard output, but lock a dedicated QMutex before and unlock after each print; that way you ensure the output from the two threads stays nicely separate.  The test program does not need to talk to any device, you can just fake the responses.  You'll see that this is not at all scary, and actually a nicely modular way to do it.

Really, the "tricky" bit is to decide what kind of objects to use for the commands and the responses.  That depends highly on the device being talked to, but you'll want to use information objects – that is, describing the intent – rather than blindly following the command/response structure the device has.  For example, I would use numbers/enums and not strings for the commands.  This way, by replacing the device communication thread implementation, you can use the same UI for other similar devices also.
Title: Re: C++ question
Post by: No.15 on August 23, 2020, 04:45:34 pm
That is a ot for me to digest :)   Thank you for your thoughts
Title: Re: C++ question
Post by: MarkF on August 23, 2020, 07:16:39 pm
MarkF
This was very helpful, thank you.  My program is still not working correctly but I am learning a lot.

Most of the issues now stem from the fact that when I query the load it always answers with an "OK", sometimes twice before it returns the value I want.

Probably should try to redo this in Python, but even as a noob the whitespace thing drives me crazy :) 

I am going to contact Amrel and see if there is a way to suppress the OK message as it is really unnecessary.  There is no way to do it listed in documentation.

Just ignore the 'OK' response and continue waiting.
Or check the response for 'isNumber()'.  (This may not be the exact function name.  It's been a long time since I've used Qt.)

Code: [Select]

void MainWindow::readyRead()
{
    QString tmp = port.readAll();

    if (tmp != "OK") {                // you might need to fix this syntax here

       switch (flag)
       {
           case 1:  // Current
               flag = 2;
               break;
           case 3:  // Voltage
               flag = 4;
               break;
       }

    }
    timer->start();
}
Title: Re: C++ question
Post by: Nominal Animal on August 24, 2020, 07:22:59 pm
I found the KEL103 command summary online, and I must agree with MarkF: This is such a simple protocol, there is no reason to add the complexity of a worker thread, just use the ReadyRead signal and the canReadLine() test to get all input lines already received by Qt.