shared-libraries/ScStwLibraries/sources/client/scstwclient.cpp

489 lines
15 KiB
C++

/****************************************************************************
** 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 <http://www.gnu.org/licenses/>.
****************************************************************************/
#include "scstwclient.h"
ScStwClient * pGlobalScStwClient = nullptr;
ScStwClient::ScStwClient(QObject * parent, QList<ScStw::SignalKey> signalSubscriptions) : QObject(parent)
{
this->state = DISCONNECTED;
this->currentRequestId = 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) {
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->currentRequestId = 1;
this->firmwareVersion = "";
this->apiVersion = "";
this->setExtensions(QVariantMap({}));
for(WaitingRequest waitingRequest : this->waitingRequests.values())
if(waitingRequest.loop != nullptr)
waitingRequest.loop->exit(ScStw::NotConnectedError);
this->waitingRequests.clear();
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"}};
}
// pick a new request id that is not already active
do {
if(this->currentRequestId >= 99)
this->currentRequestId = 0;
else
this->currentRequestId ++;
} while(this->waitingRequests.contains(this->currentRequestId));
int thisId = currentRequestId;
//qDebug() << "sending command: " << header << " with data: " << data << " and id: " << thisId;
QEventLoop *loop = new QEventLoop(this);
QTimer *timer = new QTimer(this);
QJsonObject reply;
this->waitingRequests.insert(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
connect(timer, &QTimer::timeout, [=]{loop->exit(ScStw::TimeoutError);});
// 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)
ScStw::StatusCode statusCode = ScStw::StatusCode(loop->exec());
// delete the timer
timer->deleteLater();
// find reply and delete the request from waiting list
if(this->waitingRequests.contains(thisId)) {
// request was found
// delete event loop
if(this->waitingRequests[thisId].loop != nullptr) {
this->waitingRequests[thisId].loop->deleteLater();
}
// store reply
reply = this->waitingRequests[thisId].reply;
// remove reply from waiting list
this->waitingRequests.remove(thisId);
}
else {
// some internal error occured
return {{"status", ScStw::InternalError}, {"data", ""}};
}
if(statusCode == ScStw::TimeoutError){
//the time has been triggered -> timeout
return {{"status", ScStw::TimeoutError}, {"data", ""}};
}
else if(statusCode == ScStw::NotConnectedError) {
// connection was closed during request
return {{"status", ScStw::NotConnectedError}, {"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.: <message>123456789</message>
}
else if(!message.contains(endKey) && (!this->readBuffer.isEmpty() || message.startsWith(startKey))){
// begin of a split message ( e.g.: <message>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</message> )
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.: <message>123456789</message><message>987654321</message> )
// or multiple message fragments in one message ( e.g.: 56789</message><message>987654321</message> or 56789</message><message>98765 )
//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: " << qPrintable(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!
if(this->waitingRequests.contains(id)){
this->waitingRequests[id].reply = replyObj;
if(this->waitingRequests[id].loop != nullptr){
this->waitingRequests[id].loop->exit(ScStw::Success);
}
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, ScStw::StatusCode* status) {
QVariantMap reply = this->sendCommand(3001, int(key), 10000);
qDebug() << "Setting read status is: " << reply["status"];
if(status != nullptr)
*status = ScStw::StatusCode(reply["status"].toInt());
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);
}