From fd5c1dc122813ed8d69d9a476c56774023ccacc5 Mon Sep 17 00:00:00 2001 From: Dorian Zedler Date: Sun, 5 Apr 2020 14:11:52 +0200 Subject: [PATCH] added first class --- {ScStwClient => ScStwLibraries}/.gitignore | 0 ScStwLibraries/ScStwLibraries.pri | 15 + .../ScStwLibraries.pro | 12 +- ScStwLibraries/ScStwLibraries_global.h | 21 + ScStwLibraries/scstwclient.cpp | 450 ++++++++++++++++++ ScStwLibraries/scstwclient.h | 173 +++++++ 6 files changed, 669 insertions(+), 2 deletions(-) rename {ScStwClient => ScStwLibraries}/.gitignore (100%) create mode 100644 ScStwLibraries/ScStwLibraries.pri rename ScStwClient/ScStwClient.pro => ScStwLibraries/ScStwLibraries.pro (80%) create mode 100644 ScStwLibraries/ScStwLibraries_global.h create mode 100644 ScStwLibraries/scstwclient.cpp create mode 100644 ScStwLibraries/scstwclient.h diff --git a/ScStwClient/.gitignore b/ScStwLibraries/.gitignore similarity index 100% rename from ScStwClient/.gitignore rename to ScStwLibraries/.gitignore diff --git a/ScStwLibraries/ScStwLibraries.pri b/ScStwLibraries/ScStwLibraries.pri new file mode 100644 index 0000000..9f20d3e --- /dev/null +++ b/ScStwLibraries/ScStwLibraries.pri @@ -0,0 +1,15 @@ +!isEmpty(SCSTWLIBRARIES_LIB):error("ScStwLibraries.pri already included") +SCSTWLIBRARIES_LIB = 1 + +#DEPENDS +CONFIG(release, debug|release): { + SCSTWLIBRARIES_LIB_OUTPUT_DIR="$$PWD/build/release" +} else { + SCSTWLIBRARIES_LIB_OUTPUT_DIR="$$PWD/build/debug" +} + +unix:LIBS += -L$$SCSTWLIBRARIES_LIB_OUTPUT_DIR -lScStwLibraries + +win32:LIBS += -L$$SCSTWLIBRARIES_LIB_OUTPUT_DIR -lScStwLibraries1 + +INCLUDEPATH += "$$PWD" diff --git a/ScStwClient/ScStwClient.pro b/ScStwLibraries/ScStwLibraries.pro similarity index 80% rename from ScStwClient/ScStwClient.pro rename to ScStwLibraries/ScStwLibraries.pro index ad06929..de12b61 100644 --- a/ScStwClient/ScStwClient.pro +++ b/ScStwLibraries/ScStwLibraries.pro @@ -1,7 +1,8 @@ QT -= gui +QT += network TEMPLATE = lib -DEFINES += SCSTWCLIENT_LIBRARY +DEFINES += SCSTWLIBRARIES_LIBRARY CONFIG += c++11 @@ -20,9 +21,16 @@ SOURCES += \ scstwclient.cpp HEADERS += \ - ScStwClient_global.h \ + ScStwLibraries_global.h \ scstwclient.h +#DEPENDS +CONFIG(release, debug|release): { + DESTDIR="$$PWD/build/release" +} else { + DESTDIR="$$PWD/build/debug" +} + # Default rules for deployment. unix { target.path = /usr/lib diff --git a/ScStwLibraries/ScStwLibraries_global.h b/ScStwLibraries/ScStwLibraries_global.h new file mode 100644 index 0000000..2bb7052 --- /dev/null +++ b/ScStwLibraries/ScStwLibraries_global.h @@ -0,0 +1,21 @@ +#ifndef SCSTWLIBRARIES_GLOBAL_H +#define SCSTWLIBRARIES_GLOBAL_H + +#include + +#if defined(SCSTWLIBRARIES_LIBRARY) +# define SCSTWLIBRARIES_EXPORT Q_DECL_EXPORT +#else +# define SCSTWLIBRARIES_EXPORT Q_DECL_IMPORT +#endif + +/** + * Some global enums + * + */ + +enum raceState { IDLE, STARTING, WAITING, RUNNING, STOPPED }; +enum signalKey { RaceStateChanged, TimersChanged, ExtensionsChanged, NextStartActionChanged /*, ProfilesChanged*/ }; +enum nextStartAction { AtYourMarks, Ready, Start, None }; + +#endif // SCSTWCLIENT_GLOBAL_H diff --git a/ScStwLibraries/scstwclient.cpp b/ScStwLibraries/scstwclient.cpp new file mode 100644 index 0000000..e4b9bdb --- /dev/null +++ b/ScStwLibraries/scstwclient.cpp @@ -0,0 +1,450 @@ +#include "scstwclient.h" + +ScStwClient * pGlobalScStwClient = nullptr; + +ScStwClient::ScStwClient(QObject *parent) : QObject(parent) +{ + pGlobalScStwClient = this; + + socket = new QTcpSocket(this); + + this->timeoutTimer = new QTimer(this); + this->timeoutTimer->setSingleShot(true); + + this->state = "disconnected"; + + connect(this->socket, SIGNAL(error(QAbstractSocket::SocketError)), + this, SLOT(gotError(QAbstractSocket::SocketError))); + + connect(this->socket, &QAbstractSocket::stateChanged, this, &ScStwClient::socketStateChanged); + + this->nextConnectionId = 1; + this->connections = QVariantList({}); +} + +void ScStwClient::connectToHost() { + qDebug() << "connecting"; + setState("connecting"); + + connect(this->timeoutTimer, SIGNAL(timeout()), this, SLOT(connectionTimeout())); + + //connect + this->socket->connectToHost(this->ip, this->port); + + timeoutTimer->start(3000); +} + +void ScStwClient::connectionTimeout() { + this->socket->abort(); + disconnect(this->timeoutTimer, SIGNAL(timeout()), this, SLOT(connectionTimeout())); +} + +bool ScStwClient::init() { + disconnect(this->timeoutTimer, SIGNAL(timeout()), this, SLOT(connectionTimeout())); + this->timeoutTimer->stop(); + + connect(this->socket, &QTcpSocket::readyRead, this, &ScStwClient::readyRead); + this->setState("connected"); + + // init remote session + QJsonArray updateSubs = {"onRaceStateChanged", "onTimersChanged", "onExtensionConnectionsChanged", "onNextStartActionChanged"}; + QJsonObject sessionParams = {{"updateSubs", updateSubs}, {"init", true}, {"usingTerminationKeys", true}}; + + QVariantMap initResponse = this->sendCommand(1, sessionParams, false); + + if(initResponse["status"] != 200) { + return false; + } + + this->firmwareVersion = initResponse["data"].toMap()["version"].toString(); + this->timeOffset = initResponse["data"].toMap()["time"].toDouble() - this->date->currentMSecsSinceEpoch(); + this->firmwareUpToDate = this->isFirmwareUpToDate(); + + emit this->propertiesChanged(); + + qDebug() << "[INFO][BaseStation] Init done! firmware: version: " << this->firmwareVersion << " up-to-date: " << this->firmwareUpToDate << " time offset: " << this->timeOffset; + + return true; +} + +void ScStwClient::deInit() { + this->connections.clear(); + emit this->connectionsChanged(); + this->setState("disconnected"); +} + +void ScStwClient::closeConnection() +{ + this->connections = QVariantList({}); + emit this->connectionsChanged(); + + qDebug() << "closing connection"; + switch (socket->state()) + { + case 0: + socket->disconnectFromHost(); + break; + case 2: + socket->abort(); + break; + default: + socket->abort(); + } + + setState("disconnected"); +} + +void ScStwClient::gotError(QAbstractSocket::SocketError err) +{ + //qDebug() << "got error"; + QString strError = "unknown"; + switch (err) + { + case 0: + strError = "Connection was refused"; + break; + case 1: + strError = "Remote host closed the connection"; + this->closeConnection(); + break; + case 2: + strError = "Host address was not found"; + break; + case 5: + strError = "Connection timed out"; + break; + default: + strError = "Unknown error"; + } + + emit gotError(strError); + qDebug() << "got socket error: " << strError; +} + +// ------------------------------------- +// --- socket communication handling --- +// ------------------------------------- + +void ScStwClient::socketStateChanged(QAbstractSocket::SocketState socketState) { + switch (socketState) { + case QAbstractSocket::UnconnectedState: + { + this->deInit(); + break; + } + case QAbstractSocket::ConnectedState: + { + if(!this->init()) { + this->closeConnection(); + } + + break; + } + default: + { + //qDebug() << "+ --- UNKNOWN SOCKET STATE: " << socketState; + break; + } + } +} + +QVariantMap ScStwClient::sendCommand(int header, QJsonValue data, bool useTerminationKeys, int timeout) { + if(this->state != "connected"){ + return {{"status", 910}, {"data", "not connected"}}; + } + + // generate id and witing requests entry + int thisId = nextConnectionId; + //qDebug() << "sending command: " << header << " with data: " << data << " and id: " << thisId; + nextConnectionId ++; + + QEventLoop *loop = new QEventLoop(this); + QTimer *timer = new QTimer(this); + QJsonObject reply; + + this->waitingRequests.append({thisId, loop, reply}); + + QJsonObject requestObj; + requestObj.insert("id", thisId); + requestObj.insert("header", header); + requestObj.insert("data", data); + + QString jsonRequest = QJsonDocument(requestObj).toJson(); + + timer->setSingleShot(true); + // quit the loop when the timer times out + loop->connect(timer, SIGNAL(timeout()), loop, SLOT(quit())); + // quit the loop when the connection was established + // loop.connect(this, &ScStwClient::gotReply, &loop, &QEventLoop::quit); + // start the timer before starting to connect + timer->start(timeout); + + //write data + if(useTerminationKeys) { + socket->write("" + jsonRequest.toLatin1() + ""); + } + else { + socket->write(jsonRequest.toLatin1()); + } + + //wait for an answer to finish (programm gets stuck in here) + loop->exec(); + + bool replyFound = false; + + // find reply and delete the request from waiting list + for(int i = 0; iwaitingRequests.length(); i++){ + if(this->waitingRequests[i].id == thisId){ + // request was found + replyFound = true; + // delete event loop + if(this->waitingRequests[i].loop != nullptr) { + delete this->waitingRequests[i].loop; + } + // store reply + reply = this->waitingRequests[i].reply; + // remove reply from waiting list + this->waitingRequests.removeAt(i); + } + } + + if(!replyFound) { + // some internal error occured + return {{"status", 900}, {"data", ""}}; + } + + if(timer->remainingTime() == -1){ + //the time has been triggered -> timeout + return {{"status", 911}, {"data", ""}}; + } + + delete timer; + return {{"status", reply.value("header").toInt()}, {"data", reply.value("data").toVariant()}}; + +} + +void ScStwClient::readyRead() { + + //qDebug() << "ready to ready " << socket->bytesAvailable() << " bytes" ; + QString reply = socket->readAll(); + + //qWarning() << "socket read: " << reply; + + processSocketMessage(reply); +} + +void ScStwClient::processSocketMessage(QString message) { + QString startKey = ""; + QString endKey = ""; + + //qWarning() << "... processing message now ... : " << message; + + if(message == ""){ + return; + } + + if((message.startsWith(startKey) && message.endsWith(endKey)) && (message.count(startKey) == 1 && message.count(endKey) == 1)){ + // non-split message ( e.g.: 123456789 + } + else if(!message.contains(endKey) && (!this->readBuffer.isEmpty() || message.startsWith(startKey))){ + // begin of a split message ( e.g.: 123 ) + // or middle of a split message ( e.g.: 456 ) + //qWarning() << "this is a begin or middle of split a message"; + this->readBuffer += message; + return; + } + else if(!message.contains(startKey) && message.endsWith(endKey)) { + // end of a split message ( e.g.: 789 ) + + if(!this->readBuffer.isEmpty()){ + message = readBuffer + message; + readBuffer.clear(); + } + } + else if((message.count(startKey) > 1 || message.count(endKey) > 1) || (message.contains(endKey) && !message.endsWith(endKey) && message.contains(startKey) && !message.startsWith(startKey))) { + // multiple messages in one packet ( e.g.: 123456789987654321 ) + // or multiple message fragments in one message ( e.g.: 56789987654321 or 5678998765 ) + //qDebug() << "detected multiple messages"; + + int startOfSecondMessage = message.lastIndexOf(startKey); + // process first part of message + QString firstMessage = message.left(startOfSecondMessage); + this->processSocketMessage(firstMessage); + // process second part of message + QString secondMessage = message.right(message.length() - startOfSecondMessage); + this->processSocketMessage(secondMessage); + + return; + } + else { + // invalid message + return; + } + + //qWarning() << "... done processing, message: " << message; + this->socketReplyRecieved(message); +} + +void ScStwClient::socketReplyRecieved(QString reply) { + reply.replace("", ""); + reply.replace("", ""); + + int id = 0; + + QJsonDocument jsonReply = QJsonDocument::fromJson(reply.toUtf8()); + QJsonObject replyObj = jsonReply.object(); + + if(!replyObj.isEmpty()){ + id = replyObj.value("id").toInt(); + + if(id == -1) { + // this message is an update!! + emit this->gotUpdate(replyObj.toVariantMap()); + return; + } + + // this message is the reply to a command! + for(int i = 0; i < this->waitingRequests.length(); i++){ + if(this->waitingRequests[i].id == id){ + this->waitingRequests[i].reply = replyObj; + if(this->waitingRequests[i].loop != nullptr){ + this->waitingRequests[i].loop->quit(); + } + return; + } + } + } + + latestReadReply = reply; + emit gotUnexpectedReply(reply); +} + +// ------------------------- +// --- updater functions --- +// ------------------------- + +bool ScStwClient::updateTime() { + if(abs(this->timeOffset) < 10000) { + // the time is already up-to-date + return true; + } + + QVariantMap ret = this->sendCommand(5001, this->date->currentSecsSinceEpoch()); + qDebug() << ret; + + return ret["status"].toInt() == 200; +} + +bool ScStwClient::updateFirmware() { + QString file = ":/ScStwBasestation.sb64"; + QFile f(file); + if (!f.open(QFile::ReadOnly)) return false; + QString fileContents = f.readAll(); + + if(this->firmwareUpToDate) { + return true; + } + + QVariantMap ret = this->sendCommand(5000, fileContents, true, 15000); + + return ret["status"].toInt() == 200; +} + +bool ScStwClient::isFirmwareUpToDate() { + QString file = ":/ScStwBasestation.sb64"; + QFile f(file); + if (!f.open(QFile::ReadOnly)) return false; + QString fileContents = f.readAll(); + + QString newFirmwareVersion = fileContents.split("")[1].split("")[0]; + int newFirmwareVersionMajor = newFirmwareVersion.split(".")[0].toInt(); + int newFirmwareVersionMinor = newFirmwareVersion.split(".")[1].toInt(); + int newFirmwareVersionPatch = newFirmwareVersion.split(".")[2].toInt(); + + qDebug() << "App firmware version is: " << newFirmwareVersion; + + QString currentFirmwareVersion = this->firmwareVersion; + int currentFirmwareVersionMajor = currentFirmwareVersion.split(".")[0].toInt(); + int currentFirmwareVersionMinor = currentFirmwareVersion.split(".")[1].toInt(); + int currentFirmwareVersionPatch = currentFirmwareVersion.split(".")[2].toInt(); + + return newFirmwareVersionMajor < currentFirmwareVersionMajor || newFirmwareVersionMinor < currentFirmwareVersionMinor || newFirmwareVersionPatch <= currentFirmwareVersionPatch; +} + +// ------------------------ +// --- helper functions --- +// ------------------------ + +int ScStwClient::writeRemoteSetting(QString key, QString value) { + QJsonArray requestData; + requestData.append(key); + requestData.append(value); + return this->sendCommand(3000, requestData)["status"].toInt(); +} + +void ScStwClient::setIP(QString ipAdress){ + this->ip = ipAdress; +} + +QString ScStwClient::getIP() +{ + return this->ip; +} + +QString ScStwClient::getState() +{ + return this->state; +} + +void ScStwClient::setState(QString newState){ + if(this->state != newState) { + qDebug() << "+--- ScStwClient state changed: " << newState; + this->state = newState; + emit stateChanged(); + if(this->state == "disconnected") { + this->deInit(); + } + } +} + +bool ScStwClient::refreshConnections() { + /* + "id": "id of the extention (int)", + "type": "type of the extention (can be: 'STARTPAD', 'TOPPAD')", + "name": "name of the extention", + "ip": "ip-adress of he extention (string)", + "state": "state of the extention (can be: 'disconnected', 'connecting', 'connected')" + */ + QVariantMap reply = this->sendCommand(2006); + + if(reply["status"] != 200){ + //handle Error!! + if(reply["status"] == 910){ + this->connections = QVariantList({}); + return true; + } + qDebug() << "+ --- error refreshing connections: " << reply["status"]; + return false; + } + + QVariantList tmpConnections = reply["data"].toList(); + + if(this->connections != reply["data"].toList()){ + this->connections = reply["data"].toList(); + emit this->connectionsChanged(); + } + + return true; + +} + +QVariant ScStwClient::getConnections() { + return connections; +} + +void ScStwClient::setConnections(QVariantList connections) { + if(this->connections != connections){ + this->connections = connections; + emit this->connectionsChanged(); + } +} diff --git a/ScStwLibraries/scstwclient.h b/ScStwLibraries/scstwclient.h new file mode 100644 index 0000000..e4975b6 --- /dev/null +++ b/ScStwLibraries/scstwclient.h @@ -0,0 +1,173 @@ +#ifndef SCSTWCLIENT_H +#define SCSTWCLIENT_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * This class is used to connect and talk to the ScStw basestation. + * + * @brief The ScStwClient class + * @author Dorian Zedler + */ +class ScStwClient : public QObject +{ + Q_OBJECT + +public: + /** + * The constructor + * + * @brief ScStwClient + */ + explicit ScStwClient(); + +private: + // values for the socket connection + QString ip; + ushort port = 3563; + int errors; + int errors_until_disconnect = 4; + + QVariant connections; + + QString latestReadReply; + + //---general status values---// + + // some meta data of the base + QString firmwareVersion; + bool firmwareUpToDate; + double timeOffset; + + + // the current state + QString state; + // can be: + // - 'disconnected' + // - 'connecting' + // - 'connected' + + QDateTime *date; + //to get the current time + + QTcpSocket *socket; + //socket for communication with the extention + + QTimer *timeoutTimer; + + QString readBuffer; + + int nextConnectionId; + + struct waitingRequest { + int id; + QEventLoop * loop; + QJsonObject reply; + }; + + QList waitingRequests; + +signals: + /** + * Is emitted, when the connection state changes + * + * @brief stateChanged + */ + void stateChanged(); + + /** + * Is emitted, whenever a reply is recieved which does not match any requests + * + * @brief gotUnexpectedReply + * @param reply contains the reply + */ + void gotUnexpectedReply(QString reply); + + /** + * Is emitted, when an update signal from the basestation is recieved + * + * @brief gotUpdate + * @param data + */ + void gotUpdate(QVariant data); + + void connectionsChanged(); + + void connectionSlotReleased(); + + void nextRemoteActionChanged(); + + void nextRemoteActionDelayProgChanged(); + + void gotError(QString error); + + void propertiesChanged(); + +public slots: + + void connectToHost(); + //function to connect to the base station + + void connectionTimeout(); + + void closeConnection(); + + void gotError(QAbstractSocket::SocketError err); + + // --- socket communication handling --- + + QVariantMap sendCommand(int header, QJsonValue data = "", bool useTerminationKeys = true, int timeout = 3000); + + // --- updater functions --- + + bool updateTime(); + bool updateFirmware(); + bool isFirmwareUpToDate(); + + // --- helper functions --- + + int writeRemoteSetting(QString key, QString value); + + bool refreshConnections(); + + // functions for the qml adapter + QString getIP(); + void setIP(QString ipAdress); + + QString getState(); + void setState(QString newState); + + int getProgress(); + + QVariant getConnections(); + +private slots: + + bool init(); + + void deInit(); + + void readyRead(); + + void processSocketMessage(QString message); + + void socketReplyRecieved(QString reply); + + void socketStateChanged(QAbstractSocket::SocketState socketState); + + void setConnections(QVariantList connections); +}; +extern ScStwClient * pGlobalScStwClient; + +#endif // SCSTWCLIENT_H