summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt3
-rw-r--r--vehicle-signals/CMakeLists.txt19
-rw-r--r--vehicle-signals/qtappfw-vehicle-signals.pc.in12
-rw-r--r--vehicle-signals/vehiclesignals.cpp427
-rw-r--r--vehicle-signals/vehiclesignals.h99
5 files changed, 560 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a766d3f..a4def83 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -7,6 +7,7 @@ set(CMAKE_AUTOMOC ON)
set(OE_QMAKE_PATH_EXTERNAL_HOST_BINS $ENV{OE_QMAKE_PATH_HOST_BINS})
find_package(Qt5Qml REQUIRED)
+find_package(Qt5WebSockets REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(glib REQUIRED IMPORTED_TARGET glib-2.0)
@@ -26,6 +27,7 @@ set(PRIVATE_LIBS "${PRIVATE_LIBS} -lqtappfw-bt
-lqtappfw-pbap
-lqtappfw-radio
-lqtappfw-telephony
+ -lqtappfw-vehicle-signals
-lqtappfw-weather")
set (SUBDIRS
@@ -39,6 +41,7 @@ set (SUBDIRS
pbap
radio
telephony
+ vehicle-signals
weather)
foreach(subdir ${SUBDIRS})
diff --git a/vehicle-signals/CMakeLists.txt b/vehicle-signals/CMakeLists.txt
new file mode 100644
index 0000000..f7cbf71
--- /dev/null
+++ b/vehicle-signals/CMakeLists.txt
@@ -0,0 +1,19 @@
+
+CONFIGURE_FILE("qtappfw-vehicle-signals.pc.in" "qtappfw-vehicle-signals.pc" @ONLY)
+install(FILES ${CMAKE_CURRENT_BINARY_DIR}/qtappfw-vehicle-signals.pc
+ DESTINATION ${CMAKE_INSTALL_PREFIX}/lib/pkgconfig)
+
+add_library(qtappfw-vehicle-signals SHARED vehiclesignals.cpp)
+
+target_include_directories(qtappfw-vehicle-signals PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}")
+target_include_directories(qtappfw-vehicle-signals PUBLIC "${CMAKE_INSTALL_INCLUDEDIR}")
+
+target_link_libraries(qtappfw-vehicle-signals Qt5::Core Qt5::WebSockets)
+set_target_properties(qtappfw-vehicle-signals PROPERTIES
+ VERSION ${PROJECT_VERSION}
+ SOVERSION 1
+ PUBLIC_HEADER vehiclesignals.h)
+
+install(TARGETS qtappfw-vehicle-signals
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
+ PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/qtappfw-vehicle-signals)
diff --git a/vehicle-signals/qtappfw-vehicle-signals.pc.in b/vehicle-signals/qtappfw-vehicle-signals.pc.in
new file mode 100644
index 0000000..5afac39
--- /dev/null
+++ b/vehicle-signals/qtappfw-vehicle-signals.pc.in
@@ -0,0 +1,12 @@
+prefix=@DEST_DIR@
+exec_prefix=${prefix}
+libdir=${prefix}/lib
+includedir=${prefix}/include
+
+Name: qtappfw-vehicle-signals
+Description: Library providing VIS vehicle signal updates for Qt objects
+Version: 1.0.0
+
+Requires: Qt5Core Qt5WebSockets
+Libs: -L${libdir} -lqtappfw-vehicle-signals
+Cflags: -I${includedir}/qtappfw-vehicle-signals
diff --git a/vehicle-signals/vehiclesignals.cpp b/vehicle-signals/vehiclesignals.cpp
new file mode 100644
index 0000000..f3db521
--- /dev/null
+++ b/vehicle-signals/vehiclesignals.cpp
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2022 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <QDebug>
+#include <QSettings>
+#include <QUrl>
+#include <QFile>
+#include <QSslKey>
+#include <QTimer>
+#include <QVariantMap>
+#include <QJsonDocument>
+#include <QJsonObject>
+
+#include "vehiclesignals.h"
+
+#define DEFAULT_CLIENT_KEY_FILE "/etc/kuksa-val/Client.key"
+#define DEFAULT_CLIENT_CERT_FILE "/etc/kuksa-val/Client.pem"
+#define DEFAULT_CA_CERT_FILE "/etc/kuksa-val/CA.pem"
+
+VehicleSignalsConfig::VehicleSignalsConfig(const QString &hostname,
+ const unsigned port,
+ const QByteArray &clientKey,
+ const QByteArray &clientCert,
+ const QByteArray &caCert,
+ const QString &authToken,
+ bool verifyPeer) :
+ m_hostname(hostname),
+ m_port(port),
+ m_clientKey(clientKey),
+ m_clientCert(clientCert),
+ m_caCert(caCert),
+ m_authToken(authToken),
+ m_verifyPeer(verifyPeer),
+ m_verbose(0),
+ m_valid(true)
+{
+ // Potentially could do some certificate validation here...
+}
+
+VehicleSignalsConfig::VehicleSignalsConfig(const QString &appname)
+{
+ m_valid = false;
+
+ QSettings *pSettings = new QSettings("AGL", appname);
+ if (!pSettings)
+ return;
+
+ m_hostname = pSettings->value("vis-client/server", "localhost").toString();
+ if (m_hostname.isEmpty()) {
+ qCritical() << "Invalid server hostname";
+ return;
+ }
+
+ m_port = pSettings->value("vis-client/port", 8090).toInt();
+ if (m_port == 0) {
+ qCritical() << "Invalid server port";
+ return;
+ }
+
+ // Default to disabling peer verification for now to be able
+ // to use the default upstream KUKSA.val certificates for
+ // testing. Wrangling server and CA certificate generation
+ // and management to be able to verify will require further
+ // investigation.
+ m_verifyPeer = pSettings->value("vis-client/verify-server", false).toBool();
+
+ QString keyFileName = pSettings->value("vis-client/key", DEFAULT_CLIENT_KEY_FILE).toString();
+ if (keyFileName.isEmpty()) {
+ qCritical() << "Invalid client key filename";
+ return;
+ }
+ QFile keyFile(keyFileName);
+ if (!keyFile.open(QIODevice::ReadOnly)) {
+ qCritical() << "Could not open client key file";
+ return;
+ }
+ QByteArray keyData = keyFile.readAll();
+ if (keyData.isEmpty()) {
+ qCritical() << "Invalid client key file";
+ return;
+ }
+ m_clientKey = keyData;
+
+ QString certFileName = pSettings->value("vis-client/certificate", DEFAULT_CLIENT_CERT_FILE).toString();
+ if (certFileName.isEmpty()) {
+ qCritical() << "Invalid client certificate filename";
+ return;
+ }
+ QFile certFile(certFileName);
+ if (!certFile.open(QIODevice::ReadOnly)) {
+ qCritical() << "Could not open client certificate file";
+ return;
+ }
+ QByteArray certData = certFile.readAll();
+ if (certData.isEmpty()) {
+ qCritical() << "Invalid client certificate file";
+ return;
+ }
+ m_clientCert = certData;
+
+ QString caCertFileName = pSettings->value("vis-client/ca-certificate", DEFAULT_CA_CERT_FILE).toString();
+ if (caCertFileName.isEmpty()) {
+ qCritical() << "Invalid CA certificate filename";
+ return;
+ }
+ QFile caCertFile(caCertFileName);
+ if (!caCertFile.open(QIODevice::ReadOnly)) {
+ qCritical() << "Could not open CA certificate file";
+ return;
+ }
+ QByteArray caCertData = caCertFile.readAll();
+ if (caCertData.isEmpty()) {
+ qCritical() << "Invalid CA certificate file";
+ return;
+ }
+ // Pre-check CA certificate
+ QList<QSslCertificate> newSslCaCerts = QSslCertificate::fromData(caCertData);
+ if (newSslCaCerts.isEmpty()) {
+ qCritical() << "Invalid CA certificate";
+ return;
+ }
+ m_caCert = caCertData;
+
+ QString authTokenFileName = pSettings->value("vis-client/authorization").toString();
+ if (authTokenFileName.isEmpty()) {
+ qCritical() << "Invalid authorization token filename";
+ return;
+ }
+ QFile authTokenFile(authTokenFileName);
+ if (!authTokenFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ qCritical() << "Could not open authorization token file";
+ return;
+ }
+ QTextStream in(&authTokenFile);
+ QString authToken = in.readLine();
+ if (authToken.isEmpty()) {
+ qCritical() << "Invalid authorization token file";
+ return;
+ }
+ m_authToken = authToken;
+
+ m_verbose = 0;
+ QString verbose = pSettings->value("vis-client/verbose").toString();
+ if (!verbose.isEmpty()) {
+ if (verbose == "true" || verbose == "1")
+ m_verbose = 1;
+ if (verbose == "2")
+ m_verbose = 2;
+ }
+
+ m_valid = true;
+}
+
+VehicleSignals::VehicleSignals(const VehicleSignalsConfig &config, QObject *parent) :
+ QObject(parent),
+ m_config(config),
+ m_request_id(0)
+{
+ QObject::connect(&m_websocket, &QWebSocket::connected, this, &VehicleSignals::onConnected);
+ QObject::connect(&m_websocket, QOverload<QAbstractSocket::SocketError>::of(&QWebSocket::error),
+ this, &VehicleSignals::onError);
+ QObject::connect(&m_websocket, &QWebSocket::disconnected, this, &VehicleSignals::onDisconnected);
+}
+
+VehicleSignals::~VehicleSignals()
+{
+ m_websocket.close();
+}
+
+void VehicleSignals::connect()
+{
+ if (!m_config.valid()) {
+ qCritical() << "Invalid VIS server configuration";
+ return;
+ }
+
+ QUrl visUrl;
+ visUrl.setScheme(QStringLiteral("wss"));
+ visUrl.setHost(m_config.hostname());
+ visUrl.setPort(m_config.port());
+
+ QSslConfiguration sslConfig = QSslConfiguration::defaultConfiguration();
+
+ // Add client private key
+ // i.e. kuksa_certificates/Client.key in source tree
+ QSslKey sslKey(m_config.clientKey(), QSsl::Rsa);
+ sslConfig.setPrivateKey(sslKey);
+
+ // Add local client certificate
+ // i.e. kuksa_certificates/Client.pem in source tree
+ QList<QSslCertificate> sslCerts = QSslCertificate::fromData(m_config.clientCert());
+ if (sslCerts.empty()) {
+ qCritical() << "Invalid client certificate";
+ return;
+ }
+ sslConfig.setLocalCertificate(sslCerts.first());
+
+ // Add CA certificate
+ // i.e. kuksa_certificates/CA.pem in source tree
+ // Note the following can be simplified with QSslConfiguration::addCaCertificate with Qt 5.15
+ QList<QSslCertificate> sslCaCerts = sslConfig.caCertificates();
+ QList<QSslCertificate> newSslCaCerts = QSslCertificate::fromData(m_config.caCert());
+ if (newSslCaCerts.empty()) {
+ qCritical() << "Invalid CA certificate";
+ return;
+ }
+ sslCaCerts.append(newSslCaCerts.first());
+ sslConfig.setCaCertificates(sslCaCerts);
+
+ sslConfig.setPeerVerifyMode(m_config.verifyPeer() ? QSslSocket::VerifyPeer : QSslSocket::VerifyNone);
+
+ m_websocket.setSslConfiguration(sslConfig);
+
+ if (m_config.verbose())
+ qInfo() << "Opening VIS websocket";
+ m_websocket.open(visUrl);
+}
+
+void VehicleSignals::onConnected()
+{
+ if (m_config.verbose() > 1)
+ qDebug() << "VehicleSignals::onConnected: enter";
+ QObject::connect(&m_websocket, &QWebSocket::textMessageReceived, this, &VehicleSignals::onTextMessageReceived);
+ emit connected();
+}
+
+void VehicleSignals::onError(QAbstractSocket::SocketError error)
+{
+ if (m_config.verbose() > 1)
+ qDebug() << "VehicleSignals::onError: enter";
+ QTimer::singleShot(1000, this, &VehicleSignals::reconnect);
+}
+
+void VehicleSignals::reconnect()
+{
+ if (m_config.verbose() > 1)
+ qDebug() << "VehicleSignals::reconnect: enter";
+ connect();
+}
+
+void VehicleSignals::onDisconnected()
+{
+ if (m_config.verbose() > 1)
+ qDebug() << "VehicleSignals::onDisconnected: enter";
+ QObject::disconnect(&m_websocket, &QWebSocket::textMessageReceived, this, &VehicleSignals::onTextMessageReceived);
+ emit disconnected();
+
+ // Try to reconnect
+ QTimer::singleShot(1000, this, &VehicleSignals::reconnect);
+}
+
+void VehicleSignals::authorize()
+{
+ QVariantMap map;
+ map["action"] = QString("authorize");
+ map["tokens"] = m_config.authToken();
+ map["requestId"] = QString::number(m_request_id++);
+ QJsonDocument doc = QJsonDocument::fromVariant(map);
+ m_websocket.sendTextMessage(doc.toJson(QJsonDocument::Compact).data());
+}
+
+void VehicleSignals::get(const QString &path)
+{
+ QVariantMap map;
+ map["action"] = QString("get");
+ map["tokens"] = m_config.authToken();
+ map["path"] = path;
+ map["requestId"] = QString::number(m_request_id++);
+ QJsonDocument doc = QJsonDocument::fromVariant(map);
+ m_websocket.sendTextMessage(doc.toJson(QJsonDocument::Compact).data());
+}
+
+void VehicleSignals::set(const QString &path, const QString &value)
+{
+ QVariantMap map;
+ map["action"] = QString("set");
+ map["tokens"] = m_config.authToken();
+ map["path"] = path;
+ map["value"] = value;
+ map["requestId"] = QString::number(m_request_id++);
+ QJsonDocument doc = QJsonDocument::fromVariant(map);
+ m_websocket.sendTextMessage(doc.toJson(QJsonDocument::Compact).data());
+}
+
+void VehicleSignals::subscribe(const QString &path)
+{
+ QVariantMap map;
+ map["action"] = QString("subscribe");
+ map["tokens"] = m_config.authToken();
+ map["path"] = path;
+ map["requestId"] = QString::number(m_request_id++);
+ QJsonDocument doc = QJsonDocument::fromVariant(map);
+ m_websocket.sendTextMessage(doc.toJson(QJsonDocument::Compact).data());
+}
+
+bool VehicleSignals::parseData(const QJsonObject &response, QString &path, QString &value, QString &timestamp)
+{
+ if (response.contains("error")) {
+ QString error = response.value("error").toString();
+ return false;
+ }
+
+ if (!(response.contains("data") && response["data"].isObject())) {
+ qWarning() << "Malformed response (data missing)";
+ return false;
+ }
+ QJsonObject data = response["data"].toObject();
+ if (!(data.contains("path") && data["path"].isString())) {
+ qWarning() << "Malformed response (path missing)";
+ return false;
+ }
+ path = data["path"].toString();
+ // Convert '/' to '.' in paths to ensure consistency for clients
+ path.replace(QString("/"), QString("."));
+
+ if (!(data.contains("dp") && data["dp"].isObject())) {
+ qWarning() << "Malformed response (datapoint missing)";
+ return false;
+ }
+ QJsonObject dp = data["dp"].toObject();
+ if (!dp.contains("value")) {
+ qWarning() << "Malformed response (value missing)";
+ return false;
+ } else if (dp["value"].isString()) {
+ value = dp["value"].toString();
+ } else if (dp["value"].isDouble()) {
+ value.setNum(dp["value"].toDouble());
+ } else if (dp["value"].isBool()) {
+ value = dp["value"].toBool() ? "true" : "false";
+ } else {
+ qWarning() << "Malformed response (unsupported value type)";
+ return false;
+ }
+
+ if (!(dp.contains("ts") && dp["ts"].isString())) {
+ qWarning() << "Malformed response (timestamp missing)";
+ return false;
+ }
+ timestamp = dp["ts"].toString();
+
+ return true;
+}
+
+//
+// NOTE:
+//
+// Ideally request ids would be used to provide some form of mapping
+// to callers of get/set for responses/errors. At present the demo
+// usecases are simple enough that it does not seem worth implementing
+// just yet.
+//
+void VehicleSignals::onTextMessageReceived(QString msg)
+{
+ msg = msg.simplified();
+ QJsonDocument doc(QJsonDocument::fromJson(msg.toUtf8()));
+ if (doc.isEmpty()) {
+ qWarning() << "Received invalid JSON: empty VIS message";
+ return;
+ }
+
+ if (!doc.isObject()) {
+ qWarning() << "Received invalid JSON: malformed VIS message";
+ return;
+ }
+ QJsonObject obj = doc.object();
+
+ if (!obj.contains("action")) {
+ qWarning() << "Received unknown message (no action), discarding";
+ return;
+ }
+
+ QString action = obj.value("action").toString();
+ if (action == "authorize") {
+ if (obj.contains("error")) {
+ QString error = obj.value("error").toString();
+ qWarning() << "VIS authorization failed: " << error;
+ } else {
+ if (m_config.verbose() > 1)
+ qDebug() << "authorized";
+ emit authorized();
+ }
+ } else if (action == "subscribe") {
+ if (obj.contains("error")) {
+ QString error = obj.value("error").toString();
+ qWarning() << "VIS subscription failed: " << error;
+ }
+ } else if (action == "get") {
+ if (obj.contains("error")) {
+ QString error = obj.value("error").toString();
+ qWarning() << "VIS get failed: " << error;
+ } else {
+ QString path, value, ts;
+ if (parseData(obj, path, value, ts)) {
+ if (m_config.verbose() > 1)
+ qDebug() << "VehicleSignals::onTextMessageReceived: emitting response" << path << " = " << value;
+ emit getSuccessResponse(path, value, ts);
+ }
+ }
+ } else if (action == "set") {
+ if (obj.contains("error")) {
+ QString error = obj.value("error").toString();
+ qWarning() << "VIS set failed: " << error;
+ }
+ } else if (action == "subscription") {
+ QString path, value, ts;
+ if (parseData(obj, path, value, ts)) {
+ if (m_config.verbose() > 1)
+ qDebug() << "VehicleSignals::onTextMessageReceived: emitting notification" << path << " = " << value;
+ emit signalNotification(path, value, ts);
+ }
+ } else {
+ qWarning() << "unhandled VIS response of type: " << action;
+ }
+}
diff --git a/vehicle-signals/vehiclesignals.h b/vehicle-signals/vehiclesignals.h
new file mode 100644
index 0000000..fd5fe9c
--- /dev/null
+++ b/vehicle-signals/vehiclesignals.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2022 Konsulko Group
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef VEHICLESIGNALS_H
+#define VEHICLESIGNALS_H
+
+#include <QObject>
+#include <QWebSocket>
+
+// Class to read/hold VIS server configuration
+
+class VehicleSignalsConfig
+{
+public:
+ explicit VehicleSignalsConfig(const QString &hostname,
+ const unsigned port,
+ const QByteArray &clientKey,
+ const QByteArray &clientCert,
+ const QByteArray &caCert,
+ const QString &authToken,
+ bool verifyPeer = true);
+ explicit VehicleSignalsConfig(const QString &appname);
+ ~VehicleSignalsConfig() {};
+
+ QString hostname() { return m_hostname; };
+ unsigned port() { return m_port; };
+ QByteArray clientKey() { return m_clientKey; };
+ QByteArray clientCert() { return m_clientCert; };
+ QByteArray caCert() { return m_caCert; };
+ QString authToken() { return m_authToken; };
+ bool verifyPeer() { return m_verifyPeer; };
+ bool valid() { return m_valid; };
+ unsigned verbose() { return m_verbose; };
+
+private:
+ QString m_hostname;
+ unsigned m_port;
+ QByteArray m_clientKey;
+ QByteArray m_clientCert;
+ QByteArray m_caCert;
+ QString m_authToken;
+ bool m_verifyPeer;
+ bool m_valid;
+ unsigned m_verbose;
+};
+
+// VIS signaling interface class
+
+class VehicleSignals : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit VehicleSignals(const VehicleSignalsConfig &config, QObject * parent = Q_NULLPTR);
+ virtual ~VehicleSignals();
+
+ Q_INVOKABLE void connect();
+ Q_INVOKABLE void authorize();
+
+ Q_INVOKABLE void get(const QString &path);
+ Q_INVOKABLE void set(const QString &path, const QString &value);
+ Q_INVOKABLE void subscribe(const QString &path);
+
+signals:
+ void connected();
+ void authorized();
+ void getSuccessResponse(QString path, QString value, QString timestamp);
+ void signalNotification(QString path, QString value, QString timestamp);
+ void disconnected();
+
+private slots:
+ void onConnected();
+ void onError(QAbstractSocket::SocketError error);
+ void reconnect();
+ void onDisconnected();
+ void onTextMessageReceived(QString message);
+
+private:
+ VehicleSignalsConfig m_config;
+ QWebSocket m_websocket;
+ std::atomic<unsigned int> m_request_id;
+
+ bool parseData(const QJsonObject &response, QString &path, QString &value, QString &timestamp);
+};
+
+#endif // VEHICLESIGNALS_H