465 lines
14 KiB
C++
465 lines
14 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->nextConnectionId = 1;
|
|
this->extensions = QVariantList({});
|
|
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() {
|
|
|
|
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(QVariantList({}));
|
|
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; i<this->waitingRequests.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.: <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: " << 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"].toList());
|
|
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();
|
|
}
|
|
}
|
|
|
|
QVariantList 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(QVariantList 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);
|
|
}
|