TouchPortal-CPP-API  v1.0.0
Touch Portal Plugin API Client for C++ and Qt
TPClientQt.cpp
1/*
2TPClientQt - Touch Poral Plugin API network client for C++/Qt-based plugins.
3Copyright Maxim Paperno; all rights reserved.
4
5Dual licensed under the terms of either the GNU General Public License (GPL)
6or the GNU Lesser General Public License (LGPL), as published by the Free Software
7Foundation, either version 3 of the Licenses, or (at your option) any later version.
8
9This program is distributed in the hope that it will be useful,
10but WITHOUT ANY WARRANTY; without even the implied warranty of
11MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12GNU General Public License for more details.
13
14Copies of the GNU GPL and LGPL are available at <http://www.gnu.org/licenses/>.
15
16This project may also use 3rd-party Open Source software under the terms
17of their respective licenses. The copyright notice above does not apply
18to any 3rd-party components used within.
19*/
20
21#include <QElapsedTimer>
22#include <QMetaEnum>
23#include <QTcpSocket>
24#include <QThread>
25#include <QDebug>
26
27#include "TPClientQt.h"
28
29#ifdef QT_DEBUG
30Q_LOGGING_CATEGORY(lcTPC, "TPClientQt", QtDebugMsg);
31#else
32Q_LOGGING_CATEGORY(lcTPC, "TPClientQt", QtWarningMsg);
33#endif
34
35
36struct TPClientQt::Private
37{
38 Private(TPClientQt *q, const char *pluginId) :
39 q(q),
40 socket(new QTcpSocket(q)),
42 { }
43
44 inline void onSockStateChanged(QAbstractSocket::SocketState s)
45 {
46 qCDebug(lcTPC) << "Socket state changed:" << s;
47 switch (s) {
48 case QAbstractSocket::ConnectedState:
49 socket->setSocketOption(QAbstractSocket::LowDelayOption, 1);
50#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) || !defined(Q_OS_WIN)
51 // On POSIX this needs to be set after connection according to Qt5 docs.
52 socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
53#endif
54 q->send({
55 {"type", "pair"},
56 {"id", pluginId.toUtf8().data()}
57 });
58
59 if (connTimeout > 0) {
60 QThread *waitThread = QThread::create([=] { waitForPaired(); });
61 QObject::connect(waitThread, &QThread::finished, q, [=]() { waitThread->wait(); waitThread->deleteLater(); }, Qt::QueuedConnection);
62 waitThread->start();
63 }
64 break;
65
66 case QAbstractSocket::UnconnectedState:
67 tpInfo.paired = false;
68 qCInfo(lcTPC) << "Closed TP Connection.";
69 break;
70 default:
71 break;
72 }
73 }
74
75 inline void onSocketError(QAbstractSocket::SocketError e)
76 {
77 if (e == QAbstractSocket::TemporaryError || e == QAbstractSocket::UnknownSocketError)
78 return;
79 lastError = socket->errorString();
80 Q_EMIT q->error(e);
81 qCWarning(lcTPC) << "Permanent socket error:" << e << lastError;
82 }
83
84 void waitForPaired()
85 {
87 return;
88
89 QElapsedTimer sw;
90 sw.start();
91 while (!tpInfo.tpVersionCode && socket->state() == QAbstractSocket::ConnectedState && !sw.hasExpired(connTimeout))
92 QThread::yieldCurrentThread();
93
94 if (!tpInfo.tpVersionCode) {
95 qCCritical(lcTPC) << "Could not pair with TP! Disconnecting.";
96 Q_EMIT q->error(QAbstractSocket::SocketTimeoutError);
97 QMetaObject::invokeMethod(q, "disconnect", Qt::QueuedConnection);
98 }
99 }
100
101 void onTpMessage(MessageType type, const QJsonObject &msg)
102 {
103 switch (type) {
104 case MessageType::info: {
105 tpInfo.status = msg.value(QLatin1String("status")).toString();
106 tpInfo.paired = tpInfo.status.toLower() == "paired";
107 tpInfo.sdkVersion = msg.value(QLatin1String("sdkVersion")).toInt(0);
108 tpInfo.tpVersionCode = msg.value(QLatin1String("tpVersionCode")).toInt(0);
109 tpInfo.pluginVersion = msg.value(QLatin1String("pluginVersion")).toInt(0);
110 tpInfo.tpVersionString = msg.value(QLatin1String("tpVersionString")).toString("??");
111 qCInfo(lcTPC).nospace().noquote()
112 << "Connection status '" << tpInfo.status << "' with Touch Portal v" << tpInfo.tpVersionString
113 << " (" << tpInfo.tpVersionCode << "; SDK v" << tpInfo.sdkVersion
114 << ") for Plugin " << pluginId << " v" << tpInfo.pluginVersion << " with TPClientQt v" << TP_CLIENT_VERSION_STR;
115
116 if (Q_UNLIKELY(!tpInfo.paired)) {
117 lastError = "Touch Portal responded with unknown 'status' of: " + tpInfo.status;
118 qCCritical(lcTPC) << lastError;
119 Q_EMIT q->error(QAbstractSocket::ConnectionRefusedError);
120 q->disconnect();
121 return;
122 }
123
124 const QJsonObject settings = arrayToObj(msg.value(QLatin1String("settings")));
125 Q_EMIT q->connected(tpInfo, settings);
126 Q_EMIT q->message(MessageType::info, msg);
127 Q_EMIT q->message(MessageType::settings, settings);
128 return;
129 }
130
132 Q_EMIT q->message(MessageType::settings, arrayToObj(msg.value(QLatin1String("values"))));
133 return;
134
135 default:
136 break;
137 }
138 Q_EMIT q->message(type, msg);
139 }
140
141 QJsonObject arrayToObj(const QJsonValue &arry) const
142 {
143 QJsonObject ret;
144 const QJsonArray a = arry.toArray();
145 for (const QJsonValue &v : a) {
146 if (v.isObject()) {
147 const QJsonObject &vObj = v.toObject();
148 QJsonObject::const_iterator next = vObj.begin(), last = vObj.end();
149 for (; next != last; ++next)
150 ret.insert(next.key(), next.value());
151 }
152 }
153 return ret;
154 }
155
156 TPClientQt * const q;
157 QTcpSocket * const socket;
158 QString lastError;
159 QString pluginId;
160 QString tpHost = QStringLiteral("127.0.0.1");
161 uint16_t tpPort = 12136;
162 int connTimeout = 10000; // ms
164 friend class TPClientQt;
165};
166
167#define d_const const_cast<const Private *>(d)
168
169// ---------------------------------------
170// TPClientQt
171// ---------------------------------------
172
173TPClientQt::TPClientQt(const char *pluginId, QObject *parent) :
174 QObject(parent),
175 d(new Private(this, pluginId))
176{
177 qRegisterMetaType<TPClientQt::MessageType>();
178 qRegisterMetaType<TPClientQt::TPInfo>();
179 qRegisterMetaType<QAbstractSocket::SocketState>();
180 qRegisterMetaType<QAbstractSocket::SocketError>();
181
182 QObject::connect(d->socket, &QTcpSocket::readyRead, this, &TPClientQt::onReadyRead);
183 QObject::connect(d->socket, &QTcpSocket::disconnected, this, &TPClientQt::disconnected);
184 QObject::connect(d->socket, &QTcpSocket::stateChanged, this, [this](QAbstractSocket::SocketState s) { d->onSockStateChanged(s); });
185#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0))
186 QObject::connect(d->socket, qOverload<QAbstractSocket::SocketError>(&QAbstractSocket::error), this, [this](QAbstractSocket::SocketError e) { d->onSocketError(e); });
187#else
188 QObject::connect(d->socket, &QAbstractSocket::errorOccurred, this, [this](QAbstractSocket::SocketError e) { d->onSocketError(e); });
189#endif
190}
191
192TPClientQt::~TPClientQt()
193{
194 disconnect();
195 delete d;
196}
197
198bool TPClientQt::isConnected() const { return d_const->socket->state() == QAbstractSocket::ConnectedState && d_const->tpInfo.paired; }
199QAbstractSocket::SocketState TPClientQt::socketState() const { return d_const->socket->state(); }
200QAbstractSocket::SocketError TPClientQt::socketError() const { return d_const->socket->error(); }
201QString TPClientQt::errorString() const { return d_const->lastError; }
202
203const TPClientQt::TPInfo &TPClientQt::tpInfo() const { return d_const->tpInfo; }
204QString TPClientQt::pluginId() const { return d_const->pluginId; }
205QString TPClientQt::hostName() const { return d_const->tpHost; }
206uint16_t TPClientQt::hostPort() const { return d_const->tpPort; }
207int TPClientQt::connectionTimeout() const { return d_const->connTimeout; }
208
209
211{
212 if (!pluginId || !strlen(pluginId)) {
213 qCCritical(lcTPC()) << "Plugin ID is required!";
214 return false;
215 }
216 if (d_const->socket->state() != QAbstractSocket::UnconnectedState) {
217 qCCritical(lcTPC()) << "Cannot change Plugin ID while connected.";
218 return false;
219 }
220 d->pluginId = pluginId;
221 return true;
222}
223
224void TPClientQt::setHostProperties(const QString &nameOrAddress, uint16_t port)
225{
226 if (!nameOrAddress.isEmpty())
227 d->tpHost = nameOrAddress;
228 if (port > 0)
229 d->tpPort = port;
230}
231
233{
234 d->connTimeout = timeoutMs;
235}
236
238{
239 if (d_const->socket->state() != QAbstractSocket::UnconnectedState) {
240 qCWarning(lcTPC()) << "Cannot connect while socket already connected or pending operation.";
241 return;
242 }
243
244 if (d->pluginId.isEmpty()) {
245 d->lastError = "Plugin ID is required!";
246 qCCritical(lcTPC()) << d->lastError;
247 Q_EMIT error(QAbstractSocket::OperationError);
248 return;
249 }
250
251#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) && defined(Q_OS_WIN)
252 // According to Qt5 docs, on Windows this needs to be set before connection.
253 d->socket->setSocketOption(QAbstractSocket::KeepAliveOption, 1);
254#endif
255
256 d->tpInfo = TPInfo();
257 d->socket->connectToHost(d->tpHost, d->tpPort);
258}
259
261{
262 d_const->socket->flush();
263 d_const->socket->disconnectFromHost();
264}
265
266void TPClientQt::write(const QByteArray &data) const
267{
268 if (!d_const->socket || !d_const->socket->isWritable())
269 return;
270 const int len = data.length();
271 qint64 bw = 0, sbw = 0;
272 do {
273 sbw = d_const->socket->write(data);
274 bw += sbw;
275 }
276 while (bw != len && sbw > -1);
277 if (sbw < 0) {
278 qCCritical(lcTPC()) << "Socket write error: " << d->socket->errorString();
279 disconnect();
280 return;
281 }
282 d_const->socket->write("\n", 1);
283}
284
285// private
286
287void TPClientQt::onReadyRead()
288{
289 QJsonParseError jpe;
290 while (d->socket->canReadLine()) {
291 const QByteArray &bytes = d->socket->readLine();
292 if (bytes.isEmpty())
293 continue;;
294 const QJsonDocument &js = QJsonDocument::fromJson(bytes, &jpe);
295 if (!js.isObject()) {
296 if (jpe.error == QJsonParseError::NoError)
297 qCWarning(lcTPC) << "Got empty or invalid JSON data, with no parsing error.";
298 else
299 qCWarning(lcTPC) << "Got invalid JSON data:" << jpe.errorString() << "; @" << jpe.offset;
300 qCDebug(lcTPC) << bytes;
301 continue;
302 }
303 const QJsonObject &msg = js.object();
304 // qCDebug(lcTPC) << msg;
305 const QJsonValue &jMsgType = msg.value(QLatin1String("type"));
306 if (!jMsgType.isString()) {
307 qCWarning(lcTPC) << "TP message data missing the 'type' property.";
308 qCDebug(lcTPC) << msg;
309 continue;
310 }
311
312 bool ok;
313 MessageType iMsgType = (MessageType)QMetaEnum::fromType<TPClientQt::MessageType>().keyToValue(qPrintable(jMsgType.toString()), &ok);
314 if (!ok) {
315 iMsgType = MessageType::Unknown;
316 qCWarning(lcTPC) << "Unknown TP message 'type' property:" << jMsgType.toString();
317 }
318 d->onTpMessage(iMsgType, msg);
319 }
320}
The TPClientQt class is a simple TCP/IP network client for usage in Touch Portal plugins which wish t...
Definition: TPClientQt.h:85
QString tpVersionString
Touch Portal version number as text.
Definition: TPClientQt.h:114
uint16_t hostPort() const
Returns the currently set Touch Portal host port. This is either the default or one explicitly set wi...
Definition: TPClientQt.cpp:206
uint32_t tpVersionCode
Numeric Touch Portal version.
Definition: TPClientQt.h:112
void connect()
Initiate a connection to Touch Portal. The plugin ID (set in constructor or with setPluginId() must b...
Definition: TPClientQt.cpp:237
const TPClientQt::TPInfo & tpInfo() const
Returns information about the currently connected Touch Portal instance. This data is saved from the ...
Definition: TPClientQt.cpp:203
void disconnect() const
Initiates disconnection from Touch Portal. This flushes and gracefully closes any open network socket...
Definition: TPClientQt.cpp:260
QString errorString() const
Returns the current TCP/IP network error, if any, as a human-readable string.
Definition: TPClientQt.cpp:201
TPClientQt(const char *pluginId, QObject *parent=nullptr)
The constructor creates the instance but does not attempt any connections. The pluginId will be used ...
Definition: TPClientQt.cpp:173
uint16_t sdkVersion
Supported SDK version.
Definition: TPClientQt.h:111
bool paired
true if actively connected to TP; expects that 'status' == 'paired' in initial 'info' message.
Definition: TPClientQt.h:110
QAbstractSocket::SocketError socketError() const
Returns the current TCP/IP network socket error, if any.
Definition: TPClientQt.cpp:200
bool setPluginId(const char *pluginId)
Alternate way to set or change the plugin ID. This cannot be changed when connected to TP....
Definition: TPClientQt.cpp:210
void disconnected()
Emitted upon disconnection from Touch Portal, either from an explicit call to close() or if the conne...
void error(QAbstractSocket::SocketError error)
Emitted in case of error upon initial connection or unexpected termination. This would typically be w...
bool isConnected() const
Returns true if connected to Touch Portal, false otherwise.
Definition: TPClientQt.cpp:198
void setHostProperties(const QString &nameOrAddress=QStringLiteral("127.0.0.1"), uint16_t port=12136)
Set the Touch Portal host name/address and port number for connection. nameOrAddress can be a IPv4 do...
Definition: TPClientQt.cpp:224
uint32_t pluginVersion
Numeric plugin version read from entry.tp file.
Definition: TPClientQt.h:113
QAbstractSocket::SocketState socketState() const
Returns the current state of the TCP/IP network socket used to communicate with Touch Portal.
Definition: TPClientQt.cpp:199
QString pluginId() const
Returns the plugin ID set in constructor or with setPluginId().
Definition: TPClientQt.cpp:204
MessageType
This enumeration is used in the message() signal to indicate message type. The names match the Touch ...
Definition: TPClientQt.h:90
@ settings
Emitted for 'info' and 'settings' message type; value/settings array is flattened to QJsonObject of {...
@ Unknown
An unknown event, perhaps from a newer version of TP which isn't supported yet.
@ info
The initial connection event, sent after pairing with TP.
QString status
The 'status' property from initial 'info' message (typically "paired"); This does not get changed aft...
Definition: TPClientQt.h:115
void write(const QByteArray &data) const
Low-level API: Write UTF-8 bytes directly to Touch Portal. data should contain one TP message in the ...
Definition: TPClientQt.cpp:266
int connectionTimeout() const
Returns the currently set connection timeout value, in milliseconds. This is either the default or on...
Definition: TPClientQt.cpp:207
QString hostName() const
Returns the currently set Touch Portal host name/address string. This is either the default or one ex...
Definition: TPClientQt.cpp:205
void setConnectionTimeout(int timeoutMs=10000)
Sets the timeout value for the initial pairing 'info' message to be received from Touch Portal,...
Definition: TPClientQt.cpp:232
Structure to hold information about current Touch Portal session. Populated from the initial 'info' m...
Definition: TPClientQt.h:109