diff --git a/app/LabTool.pro b/app/LabTool.pro index 9d3247d..a202ff8 100644 --- a/app/LabTool.pro +++ b/app/LabTool.pro @@ -1,4 +1,6 @@ SOURCES += \ + capture/streamworker.cpp \ + capture/uicapturestreamer.cpp \ main.cpp \ generator/i2cgenerator.cpp \ device/devicemanager.cpp \ @@ -75,6 +77,8 @@ SOURCES += \ device/reconfigurelistener.cpp HEADERS += \ + capture/streamworker.h \ + capture/uicapturestreamer.h \ generator/i2cgenerator.h \ libusbx/include/libusbx-1.0/libusb.h \ device/devicemanager.h \ @@ -164,7 +168,7 @@ else:unix:!symbian: LIBS += -L$$PWD/libusbx/Linux/ -lusb-1.0 -ludev INCLUDEPATH += $$PWD/libusbx/MS32/dll DEPENDPATH += $$PWD/libusbx/MS32/dll -QT += widgets +QT += widgets network mac { ICON = resources/oscilloscope.icns diff --git a/app/capture/captureapp.cpp b/app/capture/captureapp.cpp index bc255ed..17db23c 100644 --- a/app/capture/captureapp.cpp +++ b/app/capture/captureapp.cpp @@ -55,6 +55,7 @@ CaptureApp::CaptureApp(QWidget* uiContext, QObject *parent) : mUiContext = uiContext; mCaptureActive = false; + mContinuous = false; // Deallocation: uiContext is set as parent mArea = new UiCaptureArea(mSignalManager, uiContext); @@ -75,6 +76,10 @@ CaptureApp::CaptureApp(QWidget* uiContext, QObject *parent) : } } + + mStreamingActive = false; + captureStreamer = new UiCaptureStreamer(DeviceManager::instance().activeDevice()->captureDevice(), mUiContext); + } /*! @@ -275,6 +280,14 @@ void CaptureApp::saveProject(QSettings &project) */ void CaptureApp::handleDeviceChanged(Device* activeDevice) { + // recreate captureStreamer (even if not running) + delete captureStreamer; + captureStreamer = new UiCaptureStreamer(activeDevice->captureDevice(), mUiContext); + // update UI to follow up + // this will make it end up in the "stopped" UI state + mStreamingActive = true; + streamData(); + setupRates(activeDevice->captureDevice()); mSignalManager->reloadSignalsFromDevice(); mArea->updateAnalogGroup(); @@ -454,6 +467,18 @@ void CaptureApp::createMenu() connect(action, SIGNAL(triggered()), this, SLOT(exportData())); mMenu->addAction(action); + + // + // Set Up Streaming via Network + // + + mStreamAction = new QAction(tr("Stream Data to Socket"), this); + mStreamAction->setData("Stream Data to Socket"); + mStreamAction->setToolTip("Open a socket and send the currently captured data there"); + connect(mStreamAction, SIGNAL(triggered()), this, SLOT(streamData())); + mMenu->addAction(mStreamAction); + + } /*! @@ -747,6 +772,48 @@ void CaptureApp::exportData() } +/*! + Called when the user selects to stream data to socket, adapted from exportData +*/ +void CaptureApp::streamData() +{ + if(mStreamingActive) { + // currently streaming, so stop now + emit captureStreamer->stopWorker(); + mStreamingActive = false; + mStreamAction->setText(tr("Stream Data to Socket")); + mStreamAction->setData("Stream Data to Socket"); + + } else { + // currently not streaming, so (try to) start now + + // checks before streaming + CaptureDevice* device = DeviceManager::instance().activeDevice() + ->captureDevice(); + if (device == NULL) { + return; + } + // check if there is data to stream + if(device->digitalSignals().empty() && device->analogSignals().empty()) { + QMessageBox::warning(mUiContext, + "No signal found", + "Please add at least one signal!"); + return; + } + + + if(captureStreamer->exec() != QDialog::Accepted) { + // not accepted, abort + return; + } + + mStreamingActive = true; + mStreamAction->setText(tr("Stop Streaming")); + mStreamAction->setData("Stop Streaming"); + } + +} + /*! Called when the sample rate has changed. */ diff --git a/app/capture/captureapp.h b/app/capture/captureapp.h index 65aa1ff..e81dbf5 100644 --- a/app/capture/captureapp.h +++ b/app/capture/captureapp.h @@ -24,6 +24,7 @@ #include #include "uicapturearea.h" +#include "uicapturestreamer.h" #include "device/device.h" class CaptureApp : public QObject @@ -65,11 +66,15 @@ public slots: QAction* mTbStartAction; QAction* mTbContinuousAction; QAction* mTbStopAction; + QAction* mStreamAction; QComboBox* mRateBox; bool mCaptureActive; + bool mStreamingActive; + UiCaptureStreamer* captureStreamer; + void createToolBar(); void createMenu(); void changeCaptureActions(bool captureActive); @@ -87,6 +92,7 @@ private slots: void calibrationSettings(); void selectSignalsToAdd(); void exportData(); + void streamData(); void sampleRateChanged(int rateIndex); diff --git a/app/capture/streamworker.cpp b/app/capture/streamworker.cpp new file mode 100644 index 0000000..780d400 --- /dev/null +++ b/app/capture/streamworker.cpp @@ -0,0 +1,164 @@ +#include "streamworker.h" + +StreamWorker::StreamWorker(CaptureDevice* device) : QObject(nullptr) { + this->device = device; +// server = new QTcpServer(); +// connect(server, &QTcpServer::newConnection, this, &StreamWorker::handleNewConnection); +} + +StreamWorker::~StreamWorker() +{ + stop(); + delete server; +} + +/*! +Start streaming (start listening) +*/ +void StreamWorker::start(int port) { + if(server == nullptr) { + // the server does not like to be dragged across threads, so create it only when we need it (and already are in the right thread) + server = new QTcpServer(); + connect(server, &QTcpServer::newConnection, this, &StreamWorker::handleNewConnection); + } + if(port < 1 || port > 65535) { + // bad port value + setState(StreamingState::ERROR); + return; + } + + if(getState() != StreamingState::STOPPED) { + // we are not in the right state, stop and indicate an error + stop(); + setState(StreamingState::ERROR); + return; + } + + if(!server->listen(QHostAddress::Any, port)) { + // error with the port + setState(StreamingState::ERROR); + server->close(); + return; + } + + qInfo() << "StreamWorker: Starting"; + setState(StreamingState::RUNNING); +} + +/*! + Stop streaming + */ +void StreamWorker::stop() { + qInfo() << "StreamWorker: Stopping"; + if(getState() == StreamingState::RUNNING) { + server->close(); + foreach (auto socket, sockets) { + socket->deleteLater(); + } + sockets.clear(); + } + setState(StreamingState::STOPPED); +} + +/*! +Sets the state, synchronizes with a mutex, and emits the appropriate signal + */ +void StreamWorker::setState(StreamingState newState) { + QMutexLocker stateChangingMutexLocker {&stateChangingMutex}; + state = newState; + switch (state) { + case STOPPED: + emit stopped(); + break; + case RUNNING: + emit running(); + break; + case ERROR: + emit error(); + break; + }; +} + +/*! + Handle a new connection by adding it to the list and remove it when disconnected +*/ +void StreamWorker::handleNewConnection() { + QTcpSocket* socket = server->nextPendingConnection(); + if(socket == nullptr) { + // I have no idea why this would happen, but it happens + return; + } + sockets.append(socket); + qInfo() << "StreamWorker: Got new connection: " << (long)socket; + connect(socket, &QTcpSocket::disconnected, [this, socket](){ + this->sockets.removeOne(socket); + qDebug() << "StreamWorker: Removed a connection: " << (long)socket; + socket->deleteLater(); + } ); +} + +/*! + Send the captured data to all active sockets +*/ +void StreamWorker::handleCaptureFinished(bool successful, QString msg) { + // no need to further synchronize this, since all of this class is run via the event loop of its thread + if(getState() == StreamingState::RUNNING && successful) { + qDebug() << "StreamWorker: Got new data, sending to " << sockets.length() << " clients"; + QJsonObject json; + this->writeToJson(json); + // write compact json, so one message -> one line + // this simplifies reading in clients with tcp streaming + // so the delimiter between messages will be '\n' + QByteArray jsonBytes = QJsonDocument(json).toJson(QJsonDocument::Compact); + foreach(QTcpSocket* socket, sockets) { + socket->write(jsonBytes); + socket->write("\n"); + } + } +} + +/*! + Writes the current state of the CaptureDevice to JSON +*/ +void StreamWorker::writeToJson(QJsonObject &json) { + QJsonArray jsonSignals; + + QList digitalSignals = device->digitalSignals(); + QList analogSignals = device->analogSignals(); + + json.insert("sampleRate", device->usedSampleRate()); + + foreach(DigitalSignal* s, digitalSignals) { + QVector* data = device->digitalData(s->id()); + if (data == NULL) continue; + + QJsonObject jsonSignal; + jsonSignal.insert("id", s->id()); + jsonSignal.insert("name", s->name()); + jsonSignal.insert("type", "digital"); + QJsonArray jsonData; + foreach(int dataPoint, *data) { + jsonData.append(dataPoint); + } + jsonSignal.insert("data", jsonData); + jsonSignals.append(jsonSignal); + } + foreach(AnalogSignal* s, analogSignals) { + QVector* data = device->analogData(s->id()); + if (data == NULL) continue; + + QJsonObject jsonSignal; + jsonSignal.insert("id", s->id()); + jsonSignal.insert("name", s->name()); + jsonSignal.insert("type", "analog"); + QJsonArray jsonData; + foreach(double dataPoint, *data) { + jsonData.append(dataPoint); + } + jsonSignal.insert("data", jsonData); + jsonSignals.append(jsonSignal); + } + + json.insert("signals", jsonSignals); + +} diff --git a/app/capture/streamworker.h b/app/capture/streamworker.h new file mode 100644 index 0000000..90214b6 --- /dev/null +++ b/app/capture/streamworker.h @@ -0,0 +1,66 @@ +#ifndef STREAMWORKER_H +#define STREAMWORKER_H + +#include +#include +#include +#include +#include +#include +#include + +#include "device/capturedevice.h" + +/*! + \class StreamWorker + \brief Worker class/thread managed by \a UiCaptureStreamer + + \ingroup Capture +*/ +class StreamWorker : public QObject { + Q_OBJECT + +public: + StreamWorker(CaptureDevice* device); + ~StreamWorker(); + + enum StreamingState { + STOPPED, // not running, no current error + RUNNING, // successfully running + ERROR // not running because of an error + }; + + /// makes sure that this synchronized by a mutex + /// probably a bit paranoid, but doesn't hurt + StreamingState getState() { QMutexLocker stateChangingMutexLocker {&stateChangingMutex}; return state; }; + +private: + QMutex stateChangingMutex; // indicate that a (possible) state change is happening + QTcpServer *server = nullptr; + StreamingState state = StreamingState::STOPPED; + CaptureDevice* device = nullptr; + + QList sockets; + + void writeToJson(QJsonObject &json); + + /// makes sure that this synchronized by a mutex and also emits the signal every time + void setState(StreamingState newState); + +private slots: + void handleNewConnection(); + +public slots: + void start(int port); + void stop(); + void handleCaptureFinished(bool successful, QString msg); + +signals: + void running(); + void stopped(); + void error(); + void deleted(); + +}; + +#endif // STREAMWORKER_H diff --git a/app/capture/uicapturestreamer.cpp b/app/capture/uicapturestreamer.cpp new file mode 100644 index 0000000..96c71e4 --- /dev/null +++ b/app/capture/uicapturestreamer.cpp @@ -0,0 +1,121 @@ +#include "uicapturestreamer.h" + +#include +#include +#include +#include + + +/*! + Dialog Window +*/ +UiCaptureStreamer::UiCaptureStreamer(CaptureDevice* device, QWidget *parent) : + QDialog(parent) +{ + setWindowTitle(tr("Stream")); + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + + mCaptureDevice = device; + + // Deallocation: Ownership changed when calling setLayout. + mMainLayout = new QVBoxLayout(); + + QFormLayout* formLayout = new QFormLayout(); + + mMainLayout->addWidget(new QLabel(tr("Stream data over TCP as JSON"), this)); + mMainLayout->addWidget(new QLabel(tr("Every message is in a new line"), this)); + + // Deallocation: "Qt Object trees" (See UiMainWindow) + mPortSpinBox = new QSpinBox(this); + mPortSpinBox->setMinimum(1); + mPortSpinBox->setMaximum(65535); + mPortSpinBox->setValue(18080); // random port + formLayout->addRow(tr("Port: "), mPortSpinBox); + + mMainLayout->addLayout(formLayout); + + // Deallocation: "Qt Object trees" (See UiMainWindow) + QPushButton* streamBtn = new QPushButton("Stream", this); + connect(streamBtn, SIGNAL(clicked()), this, SLOT(handleStreamBtnPressed())); + + QPushButton* cancelBtn = new QPushButton("Cancel", this); + connect(cancelBtn, SIGNAL(clicked()), this, SLOT(reject())); + + // Deallocation: Re-parented when calling mMainLayout->addLayout. + QHBoxLayout* hLayout = new QHBoxLayout(); + hLayout->addWidget(streamBtn); + hLayout->addWidget(cancelBtn); + hLayout->addStretch(); + mMainLayout->addLayout(hLayout); + + mMainLayout->addStretch(); + + setLayout(mMainLayout); + + // set up and connect StreamWorker + streamWorker = new StreamWorker(device); + workerThread = new QThread(); + streamWorker->moveToThread(workerThread); + connect(this, &UiCaptureStreamer::startWorker, streamWorker, &StreamWorker::start); + connect(this, &UiCaptureStreamer::stopWorker, streamWorker, &StreamWorker::stop); + connect(streamWorker, &StreamWorker::running, this, &UiCaptureStreamer::handleStreamRunning); + connect(streamWorker, &StreamWorker::error, this, &UiCaptureStreamer::handleStreamError); + connect(device, &CaptureDevice::captureFinished, streamWorker, &StreamWorker::handleCaptureFinished); + // make sure to properly set up deletion across threads + connect(this, &UiCaptureStreamer::destroyWorker, streamWorker, &StreamWorker::deleteLater); + connect(streamWorker, &StreamWorker::deleted, workerThread, &QThread::deleteLater); + +} + +/*! +Destructor that takes care of stopping the thread etc. +*/ +UiCaptureStreamer::~UiCaptureStreamer() +{ + // this stops the worker and then frees its resources + // then the worker will detele its thread via signals + emit destroyWorker(); + +} + +/*! +Called when the user clicks the Stream button. +*/ +void UiCaptureStreamer::handleStreamBtnPressed() +{ + // check if the worker is in the right state + if(streamWorker->getState() != StreamWorker::STOPPED) { + reject(); + return; + } + + // start thread if necessary + if(!workerThread->isRunning()) { + workerThread->start(); + } + + emit startWorker(mPortSpinBox->value()); + + // the answer is handled by handleStreamWorkerRunning/Error +} + +/*! +This will get called after the start request and a successful start +*/ +void UiCaptureStreamer::handleStreamRunning() { + // just accept the dialog, everything went well + accept(); +} + +/*! +This will get called after the start request and an error +*/ +void UiCaptureStreamer::handleStreamError() { + QMessageBox::warning(this, + "Stream Error", + "Failed to set up server, check port!"); + // this sets the state back to stopped + emit stopWorker(); + reject(); +} + diff --git a/app/capture/uicapturestreamer.h b/app/capture/uicapturestreamer.h new file mode 100644 index 0000000..4feabb3 --- /dev/null +++ b/app/capture/uicapturestreamer.h @@ -0,0 +1,53 @@ +#ifndef UICAPTURESTREAMER_H +#define UICAPTURESTREAMER_H + +#include +#include +#include +#include +#include + +#include "streamworker.h" + +#include "device/capturedevice.h" + + +/*! + \class UiCaptureStreamer + \brief This class is responsible for setting up and managing the data streaming. + Largely an adapted form of \a UiCaptureExporter + + \ingroup Capture +*/ +class UiCaptureStreamer : public QDialog +{ + Q_OBJECT +public: + explicit UiCaptureStreamer(CaptureDevice* device, QWidget *parent = 0); + ~UiCaptureStreamer(); + void stopStreaming(); + +private: + + QThread* workerThread; + StreamWorker* streamWorker; + + CaptureDevice* mCaptureDevice; + QVBoxLayout* mMainLayout; + + QSpinBox* mPortSpinBox; + +private slots: + void handleStreamBtnPressed(); + void handleStreamRunning(); + void handleStreamError(); + +signals: + void startWorker(int port); + void stopWorker(); + void destroyWorker(); + void destroyThread(); + +}; + +#endif // UICAPTURESTREAMER_H diff --git a/app/generator/uianalogshape.cpp b/app/generator/uianalogshape.cpp index 64c1d5b..d927182 100644 --- a/app/generator/uianalogshape.cpp +++ b/app/generator/uianalogshape.cpp @@ -17,6 +17,7 @@ #include #include +#include /*! \class UiAnalogShape