ufcolemanlab / QT_USBDAQ_Project

GUI to interact with the 1208-FS USBDAQ
0 stars 0 forks source link

Taking apart the code...(NeuroGuiUpdated) #1

Closed ufcolemanlab closed 9 years ago

ufcolemanlab commented 9 years ago

So I started with the main.cpp file. This file creates samplingThread and MainWindow. MainWindow is then shown and the samplingThread is started. After I show the main.cpp code, I'll go over the sampling thread first... (here's the main.cpp code):

    SamplingThread samplingThread;
    QApplication a(argc, argv);
    MainWindow w;    <---- Notice Main Window is created before samplingThread is started.
    w.show();
    samplingThread.start();

Sampling thread is defined in the qwt package: http://qwt.sourceforge.net/class_qwt_sampling_thread.html

We've added d_frequency and d_amplitude in the code as it exists now to generate fake data for testing the plot feature.

Sampling thread needs a defined interval before it is started which is handled when MainWindow w is created. (not correct, I've added the corrected code in the comment below this)

when the code samplingThread.start() is called the timer begins and every tic executes the following code:

    if ( d_frequency > 0.0 )
    {
        const QPointF s( elapsed, value( elapsed ) );
        SignalData::instance().append( s );
    }

elapsed is the amount of time between samples and value() is a function that generates data using a sin wave equation.

Essentially the constQPointF s() returns a data value. This is where we will need to put our loop for collecting real data instead of generating fake data.

Regardless, after the data is defined, SignalData::instance() is called which returns the memory address of pending values to be plotted... The logic is: New Data -> Plot Buffer (called pendingValues) -> lock thread -> write pending values to value vector -> unlock thread.

To achieve this logic, the append function is called which has the following code:

    d_data->mutex.lock();
    d_data->pendingValues += sample;
    const bool isLocked = d_data->lock.tryLockForWrite();
    if ( isLocked )
    {
        const int numValues = d_data->pendingValues.size();
        const QPointF *pendingValues = d_data->pendingValues.data();
        for ( int i = 0; i < numValues; i++ )
            d_data->append( pendingValues[i] );
        d_data->pendingValues.clear();
        d_data->lock.unlock();
    }
    d_data->mutex.unlock();

**\ notice that the code uses d_data->append( pendingValues[i] ) which is defined within SignalData as an inline function (google inline functions if you want to learn what that is) with the following code:

    inline void append( const QPointF &sample )
    {
        values.append( sample );
        // adjust the bounding rectangle
        if ( boundingRect.width() < 0 || boundingRect.height() < 0 )
        {
            boundingRect.setRect( sample.x(), sample.y(), 0.0, 0.0 );
        }
        else
        {
            boundingRect.setRight( sample.x() );
            if ( sample.y() > boundingRect.bottom() )
                boundingRect.setBottom( sample.y() );
            if ( sample.y() < boundingRect.top() )
                boundingRect.setTop( sample.y() );
        }
    }

I'm not going to get into the boundingRect yet, but values.append(sample) adds the data point to our stored data values. values is defined in the code as:

QVector<QPointF> values;

which is defined in the qt package... http://qt-project.org/doc/qt-4.8/qvector.html

So up to this point, we've gone over how the sampling thread is created and collects data. Each time data is collected, the data mutex is locked by the thread: http://www.cplusplus.com/reference/mutex/mutex/lock/ and pending values are added. If d_data can be locked for writing http://qt-project.org/doc/qt-4.8/qreadwritelock.html#tryLockForWrite then the pending data values are stored in the plot values (data->values)

I'm still going through the code so we can customize it to our needs, but hopefully this will save a little time trying to go through what the code is doing.

ufcolemanlab commented 9 years ago

Correction: Sampling thread interval was never defined. I've modified main.cpp with:

    SamplingThread samplingThread;
    samplingThread.setInterval( 1 );

(1000Hz) which now correctly shows the sinwave data.

ufcolemanlab commented 9 years ago

overview

We'll make the GUI pretty once we get it working.

ufcolemanlab commented 9 years ago

MainWindow handles all the USB DAQ stuff when the start button is pressed so I'll go over that later.

For now, I'll focus on the plot itself which is handled by the following code when mainwindow is first generated:

    d_plot = new Plot(this);
    d_plot->setIntervalLength(10.0);
    d_plot->start();

Plot is defined in plot.cpp as a QwtPlotCanvas http://qwt.sourceforge.net/class_qwt_plot_canvas.html

When Plot is first generated, d_paintedPoints = 0 d_interval = 0 to 10 d_timerID = -1 Here's the initial code:

    d_directPainter = new QwtPlotDirectPainter();
    setAutoReplot( false );
    setCanvas( new Canvas() );
    plotLayout()->setAlignCanvasToScales( true );
    setAxisTitle( QwtPlot::xBottom, "Time [s]" );
    setAxisScale( QwtPlot::xBottom, d_interval.minValue(), d_interval.maxValue() );
    setAxisScale( QwtPlot::yLeft, -200.0, 200.0 );
    QwtPlotGrid *grid = new QwtPlotGrid();
    grid->setPen( Qt::gray, 0.0, Qt::DotLine );
    grid->enableX( true );
    grid->enableXMin( true );
    grid->enableY( true );
    grid->enableYMin( false );
    grid->attach( this );
    d_origin = new QwtPlotMarker();
    d_origin->setLineStyle( QwtPlotMarker::Cross );
    d_origin->setValue( d_interval.minValue() + d_interval.width() / 2.0, 0.0 );
    d_origin->setLinePen( Qt::gray, 0.0, Qt::DashLine );
    d_origin->attach( this );
    d_curve = new QwtPlotCurve();
    d_curve->setStyle( QwtPlotCurve::Lines );
    d_curve->setPen( canvas()->palette().color( QPalette::WindowText ) );
    d_curve->setRenderHint( QwtPlotItem::RenderAntialiased, true );
    d_curve->setPaintAttribute( QwtPlotCurve::ClipPolygons, false );
    d_curve->setData( new CurveData() );
    d_curve->attach( this );

To go over this code we really have a few things we're dealing with: The initial options, canvas, grid, origin, and curve.

The initial options consist of:

    d_directPainter = new QwtPlotDirectPainter();
    setAutoReplot( false );

AutoReplot is turned off (this is faster) We will handle the replot ourselves. http://qwt.sourceforge.net/class_qwt_plot.html#ab1cbce6d43ff9772735a9df9104f882f

d_directPainter is an object that is used to plot incrementally. It's very useful if you want to plot new data without removing your old data (which is exactly what we want). http://qwt.sourceforge.net/class_qwt_plot_direct_painter.html#af9e6e2056afd4db4c081e4b04d5c9a85

The canvas is just a qwidget where the plot stuff will be displayed: http://qwt.sourceforge.net/class_qwt_plot_canvas.html

The grid is useful for a rough visual estimation of amplitudes http://qwt.sourceforge.net/class_qwt_plot_grid.html

The origin is dashed white lines you see in the image above (just another visual aid) http://qwt.sourceforge.net/class_qwt_plot_marker.html

Finally, we have QwtPlotCurve which is where our collected data curve is located: http://qwt.sourceforge.net/class_qwt_plot_curve.html The important code is:

d_curve->setData( new CurveData() );

Here we are setting the data of the curve using CurveData when it is first initialized. Below is the link to an overview of the CurveData class: http://qwt.sourcearchive.com/documentation/6.0.0-1/classCurveData.html

Up to this point we've gone over the code found in the mainwindow file:

d_plot = new Plot(this);
d_plot->setIntervalLength(10.0);

The next piece of code:

d_plot->start();

starts a timer in plot that has the following code:

    d_clock.start();
    d_timerId = startTimer( 10 );

d_clock is previously defined in the header file of plot.h as: QwtSystemClock d_clock; the info on QwtSystemClock can be found here: http://qwt.sourceforge.net/class_qwt_system_clock.html Basically, timer functions in stock Qt are not good enough for data recording so QwtSystemClock offers a more precise option.

info on startTimer can be found here: http://qt-project.org/doc/qt-4.8/qobject.html#startTimer

Essentailly, the code above starts a precise clock on which it then starts a timer and gives it a name. From the information provided in the link above about startTimer, we know that whenever a timer executes it calls timerEvent( timername ) The code in plot.cpp that handles this is:

void Plot::timerEvent( QTimerEvent *event )
    if ( event->timerId() == d_timerId )
    {
        updateCurve();
        const double elapsed = d_clock.elapsed() / 1000.0;
        if ( elapsed > d_interval.maxValue() )
            incrementInterval();
        return;
    }
    QwtPlot::timerEvent( event );

Essentially, whenever the plot timer executes it says.. is this timer equal to the plot timer id? if it is then run the function updateCurve()... Once the curve is updated, check to see if the elapsed time is greater than the d_interval specified by the user. If it's greater then run the function incrementInterval()

So now we have two functions... updateCurve and incrementInterval we need to learn about.

ufcolemanlab commented 9 years ago

The code for updateCurve() is:

void Plot::updateCurve()
{
    CurveData *data = static_cast<CurveData *>( d_curve->data() );
    data->values().lock();
    const int numPoints = data->size();
    if ( numPoints > d_paintedPoints )
    {
        const bool doClip = !canvas()->testAttribute( Qt::WA_PaintOnScreen );
        if ( doClip )
        {
            /*
                Depending on the platform setting a clip might be an important
                performance issue. F.e. for Qt Embedded this reduces the
                part of the backing store that has to be copied out - maybe
                to an unaccelerated frame buffer device.
            */
            const QwtScaleMap xMap = canvasMap( d_curve->xAxis() );
            const QwtScaleMap yMap = canvasMap( d_curve->yAxis() );

            QRectF br = qwtBoundingRect( *data,
                d_paintedPoints - 1, numPoints - 1 );

            const QRect clipRect = QwtScaleMap::transform( xMap, yMap, br ).toRect();
            d_directPainter->setClipRegion( clipRect );
        }
        d_directPainter->drawSeries( d_curve,
            d_paintedPoints - 1, numPoints - 1 );
        d_paintedPoints = numPoints;
    }
    data->values().unlock();
}

In the beginning, we have:

CurveData *data = static_cast<CurveData *>( d_curve->data() );

is basically making a pointer to the currently stored plot data.

Next, we have

data->values().lock();

which attempts to lock the values so no other threads can access them while writing.

Next we examine the size of the current plot data with:

const int numPoints = data->size();

and check to see if this is greater than the number of data points that have already been plotted with:

if ( numPoints > d_paintedPoints )

Basically, it's saying... if there is data that has not been plotted we need to plot it. We then have some code that handles the canvas and the scaling with:

        const bool doClip = !canvas()->testAttribute( Qt::WA_PaintOnScreen );
        if ( doClip )
        {
            const QwtScaleMap xMap = canvasMap( d_curve->xAxis() );
            const QwtScaleMap yMap = canvasMap( d_curve->yAxis() );
            QRectF br = qwtBoundingRect( *data, d_paintedPoints - 1, numPoints - 1 );
            const QRect clipRect = QwtScaleMap::transform( xMap, yMap, br ).toRect();
            d_directPainter->setClipRegion( clipRect );
        }

Basically the above chunk of code is saying... if the plot has gone past the defined area we need to adjust the canvas bounding box.

We then get to:

        d_directPainter->drawSeries( d_curve, d_paintedPoints - 1, numPoints - 1 );
        d_paintedPoints = numPoints;

which plots the data points that have not been plotted and updates d_paintedPoints to equal numPoints since at this point all data has been plotted.

Since the plot has been updated the next line of code unlocks the data for other threads to access:

data->values().unlock();
ufcolemanlab commented 9 years ago

Above we saw in the timerEvent code that incrementInterval is only called if the elapsed time is greater than the d_interval.maxValue:

        if ( elapsed > d_interval.maxValue() )
            incrementInterval();

Basically, if the curve goes outside of the plot area, the following code adjusts the axis, grid, etc. and sets the curve to start plotting from the left to the right again. The code for incrementInterval is:

void Plot::incrementInterval()
{
    d_interval = QwtInterval( d_interval.maxValue(), d_interval.maxValue() + d_interval.width() );
    CurveData *data = static_cast<CurveData *>( d_curve->data() );
    data->values().clearStaleValues( d_interval.minValue() );
    // To avoid, that the grid is jumping, we disable
    // the autocalculation of the ticks and shift them
    // manually instead.
    QwtScaleDiv scaleDiv = axisScaleDiv( QwtPlot::xBottom );
    scaleDiv.setInterval( d_interval );
    for ( int i = 0; i < QwtScaleDiv::NTickTypes; i++ )
    {
        QList<double> ticks = scaleDiv.ticks( i );
        for ( int j = 0; j < ticks.size(); j++ )
            ticks[j] += d_interval.width();
        scaleDiv.setTicks( i, ticks );
    }
    setAxisScaleDiv( QwtPlot::xBottom, scaleDiv );
    d_origin->setValue( d_interval.minValue() + d_interval.width() / 2.0, 0.0 );
    d_paintedPoints = 0;
    replot();
}

First, the interval is determined using QwtInterval:

d_interval = QwtInterval( d_interval.maxValue(), d_interval.maxValue() + d_interval.width() );

http://qwt.sourceforge.net/class_qwt_interval.html#acd8699b69f46bcea31fa225d1425add3

the pointer is then created to the curve data (we did this in updateCurve() also)

CurveData *data = static_cast<CurveData *>( d_curve->data() );

From here, a new function, clearStaleValues(), is called. The code for this function is actually located in signaldata.cpp and NOT in plot.cpp. The code for this function is:

void SignalData::clearStaleValues( double limit )
{
    d_data->lock.lockForWrite();
    d_data->boundingRect = QRectF( 1.0, 1.0, -2.0, -2.0 ); // invalid
    const QVector<QPointF> values = d_data->values;
    d_data->values.clear();
    d_data->values.reserve( values.size() );
    int index;
    for ( index = values.size() - 1; index >= 0; index-- )
    {
        if ( values[index].x() < limit )
            break;
    }
    if ( index > 0 )
        d_data->append( values[index++] );
    while ( index < values.size() - 1 )
        d_data->append( values[index++] );
    d_data->lock.unlock();
}

This code block locks the data for writing and sets a default bounding box (we covered this earlier). a new variable is created called values which is a copy of d_data->values. d_data->values are the data that are being plotted. After this copy has been made, the data is deleted and the memory freed with:

d_data->values.clear();

http://qt-project.org/doc/qt-4.8/qvector.html#clear now that the original data has been deleted, we reserve some memory based on the size of the copy we made:

d_data->values.reserve( values.size() );

At this point it's important to remember the data that was given to the function originally in the incrementInterval function:

data->values().clearStaleValues( d_interval.minValue() );

this input is called "limit" in the clearStaleValues function:

void SignalData::clearStaleValues( double limit )

Now that we've reminded ourselves what limit is, the clearStaleValues function has the following code:

    for ( index = values.size() - 1; index >= 0; index-- )
    {
        if ( values[index].x() < limit )
            break;
    }

Which is going through our copied data to find the x-position that equals the new canvas's start value. Once it finds the location, it exits the loop and the value of the location is stored in the variable index. The next chunk of code:

    if ( index > 0 )
        d_data->append( values[index++] );
    while ( index < values.size() - 1 )
        d_data->append( values[index++] );

is saying if there is any plot data that should be included in the new canvas, be sure to include it in the new plot instead of deleting it.

Finally, we get to

    d_data->lock.unlock();

which unlocks the plot data and we return to the incrementInterval function:

<...>
    CurveData *data = static_cast<CurveData *>( d_curve->data() );
    data->values().clearStaleValues( d_interval.minValue() ); <-------- HERE
<...>

The next chunk of code in incrementInterval manually updates the grid and ticks so I will not go over this in detail since it's not that important.

 QwtScaleDiv scaleDiv = axisScaleDiv( QwtPlot::xBottom );
    scaleDiv.setInterval( d_interval );
    for ( int i = 0; i < QwtScaleDiv::NTickTypes; i++ )
    {
        QList<double> ticks = scaleDiv.ticks( i );
        for ( int j = 0; j < ticks.size(); j++ )
            ticks[j] += d_interval.width();
        scaleDiv.setTicks( i, ticks );
    }
    setAxisScaleDiv( QwtPlot::xBottom, scaleDiv );
    d_origin->setValue( d_interval.minValue() + d_interval.width() / 2.0, 0.0 );

Finally the data and bounding box have been updated, so we set d_paintedPoints=0 since nothing has been plotted on this new interval and the plot is updated by calling the replot() function. The code for the replot function is:

    CurveData *data = static_cast<CurveData *>( d_curve->data() );
    data->values().lock();

    QwtPlot::replot();
    d_paintedPoints = data->size();

    data->values().unlock();

By now we should be pretty familiar with what the code is doing... getting the pointer to the plot data, locking the thread, and calling the replot function that is handled by qwt: http://qwt.sourceforge.net/class_qwt_plot.html#a7b094e29b8e92b00e36517d0d7633c4b

finally we end by setting the d_paintedPoints to equal the size of the data since it's all been plotted, and unlocking the data for new threads.

At this point the code has finished executing so now we just wait for another timerevent and go through the process again.