/**************************************************************************** ** ScStw Libraries ** Copyright (C) 2020 Itsblue development ** ** This program is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** This program is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with this program. If not, see . ****************************************************************************/ #include "scstwclient.h" ScStwClient * pGlobalScStwClient = nullptr; ScStwClient::ScStwClient(QObject * parent, QList signalSubscriptions) : QObject(parent) { this->state = DISCONNECTED; this->nextConnectionId = 1; this->extensions = QVariantMap({}); this->signalSubscriptions = signalSubscriptions; this->socket = new QTcpSocket(this); this->timeoutTimer = new QTimer(this); this->timeoutTimer->setSingleShot(true); connect(this->timeoutTimer, &QTimer::timeout, [=](){this->handleError(QAbstractSocket::ProxyConnectionTimeoutError);}); connect(this->socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(handleError(QAbstractSocket::SocketError))); connect(this->socket, &QAbstractSocket::stateChanged, this, &ScStwClient::handleSocketStateChange); connect(this->socket, &QAbstractSocket::readyRead, this, &ScStwClient::handleReadyRead); pGlobalScStwClient = this; } void ScStwClient::connectToHost() { if(this->state != DISCONNECTED) return; setState(CONNECTING); //connect this->socket->connectToHost(this->ip, this->port); timeoutTimer->start(3000); } void ScStwClient::connectionTimeout() { if(this->state != CONNECTING) return; this->socket->abort(); disconnect(this->timeoutTimer, SIGNAL(timeout()), this, SLOT(connectionTimeout())); } bool ScStwClient::init() { if(this->state != CONNECTING) return false; this->setState(INITIALISING); this->timeoutTimer->stop(); QJsonArray signalSubs; foreach (ScStw::SignalKey key, this->signalSubscriptions) { signalSubs.append(key); } // init remote session QJsonObject sessionParams = {{"apiVersion", this->API_VERSION}, {"signalSubscriptions", signalSubs}, {"init", true}, {"usingTerminationKeys", true}}; QVariantMap initResponse = this->sendCommand(1, sessionParams, 3000, false); if(initResponse["status"] != 200) { this->closeConnection(); return false; } this->apiVersion = initResponse["data"].toMap()["apiVersion"].toString(); qDebug() << "[INFO][CLIENT] base station api version is: " << this->apiVersion; int compareResult = ScStw::firmwareCompare(this->API_VERSION, this->apiVersion); //qDebug() << "compare result is: " << compareResult; if( compareResult == -3 ){ // the client version is out of date!! this->closeConnection(); return false; } else if(compareResult == 3){ // the server version is out of date!! this->closeConnection(); return false; } else if(compareResult == -4){ // the server sent an invalid version this->closeConnection(); return false; } this->firmwareVersion = initResponse["data"].toMap()["firmwareVersion"].toString(); this->timeOffset = initResponse["data"].toMap()["time"].toDouble() - this->date->currentMSecsSinceEpoch(); qDebug() << "[INFO][BaseStation] Init done! firmware: " << this->firmwareVersion << " time offset: " << this->timeOffset; this->setState(CONNECTED); return true; } void ScStwClient::deInit() { if(this->state == DISCONNECTED) return; this->setExtensions(QVariantMap({})); this->setState(DISCONNECTED); } void ScStwClient::closeConnection() { if(this->getState() == DISCONNECTED) return; qDebug() << "closing connection"; switch (socket->state()) { case 0: socket->disconnectFromHost(); break; case 2: socket->abort(); break; default: socket->abort(); } } // ------------------------------------- // --- socket communication handling --- // ------------------------------------- QVariantMap ScStwClient::sendCommand(int header, QJsonValue data, int timeout) { if(this->state != CONNECTED) return {{"status", ScStw::NotConnectedError}, {"data", "not connected"}}; return this->sendCommand(header, data, timeout, true); } QVariantMap ScStwClient::sendCommand(int header, QJsonValue data, int timeout, bool useTerminationKeys) { if(this->state != CONNECTED && this->state != INITIALISING){ return {{"status", ScStw::NotConnectedError}, {"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())); // start the timer before starting to connect timer->start(timeout); //write data if(useTerminationKeys) socket->write(ScStw::SOCKET_MESSAGE_START_KEY + jsonRequest.toUtf8() + ScStw::SOCKET_MESSAGE_END_KEY); else socket->write(jsonRequest.toUtf8()); //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", ScStw::Error}, {"data", ""}}; } if(timer->remainingTime() == -1){ //the time has been triggered -> timeout return {{"status", ScStw::TimeoutError}, {"data", ""}}; } delete timer; return {{"status", reply.value("header").toInt()}, {"data", reply.value("data").toVariant()}}; } void ScStwClient::handleSocketStateChange(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; } } } void ScStwClient::handleError(QAbstractSocket::SocketError err) { if(err == QAbstractSocket::ProxyConnectionClosedError) this->closeConnection(); switch (err) { case QAbstractSocket::ProxyConnectionTimeoutError: if(this->state == CONNECTING) this->closeConnection(); break; default: break; } emit gotError(err); qDebug() << "got socket error: " << err; } void ScStwClient::handleReadyRead() { //qDebug() << "ready to ready " << socket->bytesAvailable() << " bytes" ; QString reply = socket->readAll(); //qWarning() << "socket read: " << reply; processSocketMessage(reply); } void ScStwClient::processSocketMessage(QString message) { //qWarning() << "... processing message now ... : " << message; QString startKey = ScStw::SOCKET_MESSAGE_START_KEY; QString endKey = ScStw::SOCKET_MESSAGE_END_KEY; 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->handleSocketMessage(message); } void ScStwClient::handleSocketMessage(QString reply) { reply.replace(ScStw::SOCKET_MESSAGE_START_KEY, ""); reply.replace(ScStw::SOCKET_MESSAGE_END_KEY, ""); //qDebug() << "got message: " << reply; 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->handleSignal(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; } } } emit gotUnexpectedMessage(reply); } void ScStwClient::handleSignal(QVariantMap data) { // get the signal type if(ScStw::signalKeyFromInt(data["header"].toInt()) == ScStw::InvalidSignal) return; ScStw::SignalKey signalKey = ScStw::signalKeyFromInt(data["header"].toInt()); //qDebug() << "got signal: " << signalKey << " with data: " << data["data"]; switch (signalKey) { case ScStw::ExtensionsChanged: { // the extension connections have changed // -> handle locally this->setExtensions(data["data"].toMap()); return; break; } default: { break; } } // forward to external handlers emit this->gotSignal(signalKey, data["data"]); } // ------------------------ // --- helper functions --- // ------------------------ ScStw::StatusCode ScStwClient::writeRemoteSetting(ScStwSettings::BaseStationSetting key, QVariant value) { QJsonArray requestData; requestData.append(int(key)); requestData.append(QJsonValue::fromVariant(value)); return ScStw::StatusCode(this->sendCommand(3000, requestData)["status"].toInt()); } QVariant ScStwClient::readRemoteSetting(ScStwSettings::BaseStationSetting key) { QVariantMap reply = this->sendCommand(3001, int(key)); if(reply["status"] != 200){ return "false"; } return reply["data"]; } void ScStwClient::setIP(QString newIp){ this->ip = newIp; } QString ScStwClient::getIP() { return this->ip; } ScStwClient::State ScStwClient::getState() { return this->state; } void ScStwClient::setState(ScStwClient::State newState){ if(this->state != newState) { qDebug() << "+--- ScStwClient state changed: " << newState; this->state = newState; emit stateChanged(); } } QVariantMap ScStwClient::getExtensions() { return this->extensions; } int ScStwClient::getTimeOffset() { return this->timeOffset; } QString ScStwClient::getFirmwareVersion() { return this->firmwareVersion; } QString ScStwClient::getApiVersion() { return this->apiVersion; } void ScStwClient::setExtensions(QVariantMap extensions) { qDebug() << "[CLIENT][DEBUG] Extensions changed: " << extensions; if(this->extensions != extensions){ this->extensions = extensions; emit this->gotSignal(ScStw::ExtensionsChanged, this->getExtensions()); emit this->extensionsChanged(); } } void ScStwClient::addSignalSubscription(ScStw::SignalKey key) { if(!this->signalSubscriptions.contains(key)) this->signalSubscriptions.append(key); }