diff options
Diffstat (limited to 'vehicle-signals/vehiclesignals.cpp')
-rw-r--r-- | vehicle-signals/vehiclesignals.cpp | 481 |
1 files changed, 115 insertions, 366 deletions
diff --git a/vehicle-signals/vehiclesignals.cpp b/vehicle-signals/vehiclesignals.cpp index d6cf8eb..adf371a 100644 --- a/vehicle-signals/vehiclesignals.cpp +++ b/vehicle-signals/vehiclesignals.cpp @@ -1,427 +1,176 @@ /* - * Copyright (C) 2022 Konsulko Group + * Copyright (C) 2022,2023 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. + * SPDX-License-Identifier: Apache-2.0 */ #include <QDebug> -#include <QSettings> -#include <QUrl> -#include <QFile> -#include <QSslKey> -#include <QTimer> -#include <QVariantMap> -#include <QJsonDocument> -#include <QJsonObject> +#include <QtConcurrent> #include "vehiclesignals.h" +#include "QtKuksaClient.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) +VehicleSignals::VehicleSignals(const VehicleSignalsConfig &config, QObject *parent) : + QObject(parent), + m_config(config) { - 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; + // Create gRPC channel + // NOTE: channel creation and waiting for connected state could be put into + // a thread that is spawned here. + + QString host = m_config.hostname(); + host += ":"; + host += QString::number(m_config.port()); + + std::shared_ptr<grpc::Channel> channel; + if (!m_config.caCert().isEmpty()) { + qInfo() << "Using TLS"; + grpc::SslCredentialsOptions options; + options.pem_root_certs = m_config.caCert().toStdString(); + if (!m_config.tlsServerName().isEmpty()) { + grpc::ChannelArguments args; + auto target = m_config.tlsServerName(); + qInfo() << "Overriding TLS target name with " << target; + args.SetString(GRPC_SSL_TARGET_NAME_OVERRIDE_ARG, target.toStdString()); + channel = grpc::CreateCustomChannel(host.toStdString(), grpc::SslCredentials(options), args); + } else { + channel = grpc::CreateChannel(host.toStdString(), grpc::SslCredentials(options)); + } + } else { + channel = grpc::CreateChannel(host.toStdString(), grpc::InsecureChannelCredentials()); } - 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_broker = new QtKuksaClient(channel, config); + if (!m_broker) + qCritical() << "gRPC client initialization failed"; - 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); + QObject::connect(m_broker, &QtKuksaClient::connected, this, &VehicleSignals::onConnected); } VehicleSignals::~VehicleSignals() { - m_websocket.close(); + delete m_broker; } 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); + // QtKuksaClient will call our onConnected slot when the channel + // is connected, and then we pass that along via our connected + // signal. + if (m_broker) + m_broker->connect(); +} - m_websocket.setSslConfiguration(sslConfig); +void VehicleSignals::authorize() +{ + // Databroker has no separate authorize call, so this is a no-op + emit authorized(); +} - if (m_config.verbose()) - qInfo() << "Opening VIS websocket"; - m_websocket.open(visUrl); +void VehicleSignals::get(const QString &path, const bool actuator) +{ + if (m_broker) + m_broker->get(path, actuator); } -void VehicleSignals::onConnected() +void VehicleSignals::set(const QString &path, const QString &value, const bool actuator) { - if (m_config.verbose() > 1) - qDebug() << "VehicleSignals::onConnected: enter"; - QObject::connect(&m_websocket, &QWebSocket::textMessageReceived, this, &VehicleSignals::onTextMessageReceived); - emit connected(); + if (m_broker) + m_broker->set(path, value, actuator); } -void VehicleSignals::onError(QAbstractSocket::SocketError error) +void VehicleSignals::set(const QString &path, const qint32 value, const bool actuator) { - if (m_config.verbose() > 1) - qDebug() << "VehicleSignals::onError: enter"; - QTimer::singleShot(1000, this, &VehicleSignals::reconnect); + if (m_broker) + m_broker->set(path, value, actuator); } -void VehicleSignals::reconnect() +void VehicleSignals::set(const QString &path, const qint64 value, const bool actuator) { - if (m_config.verbose() > 1) - qDebug() << "VehicleSignals::reconnect: enter"; - connect(); + if (m_broker) + m_broker->set(path, value, actuator); } -void VehicleSignals::onDisconnected() +void VehicleSignals::set(const QString &path, const quint32 value, const bool actuator) { - if (m_config.verbose() > 1) - qDebug() << "VehicleSignals::onDisconnected: enter"; - QObject::disconnect(&m_websocket, &QWebSocket::textMessageReceived, this, &VehicleSignals::onTextMessageReceived); - emit disconnected(); + if (m_broker) + m_broker->set(path, value, actuator); +} - // Try to reconnect - QTimer::singleShot(1000, this, &VehicleSignals::reconnect); +void VehicleSignals::set(const QString &path, const quint64 value, const bool actuator) +{ + if (m_broker) + m_broker->set(path, value, actuator); } -void VehicleSignals::authorize() +void VehicleSignals::set(const QString &path, const float value, const bool actuator) { - 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()); + if (m_broker) + m_broker->set(path, value, actuator); } -void VehicleSignals::get(const QString &path) +void VehicleSignals::set(const QString &path, const double value, const bool actuator) { - 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()); + if (m_broker) + m_broker->set(path, value, actuator); } -void VehicleSignals::set(const QString &path, const QString &value) +void VehicleSignals::subscribe(const QString &path, bool actuator) { - 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()); + if (m_broker) + m_broker->subscribe(path, actuator); } -void VehicleSignals::subscribe(const QString &path) +void VehicleSignals::subscribe(const QMap<QString, bool> &signals_) { - 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()); + if (m_broker) + m_broker->subscribe(signals_); } -bool VehicleSignals::parseData(const QJsonObject &response, QString &path, QString &value, QString ×tamp) +// Slots + +void VehicleSignals::onConnected() { - if (response.contains("error")) { - QString error = response.value("error").toString(); - return false; - } + QObject::connect(m_broker, &QtKuksaClient::getResponse, this, &VehicleSignals::onGetResponse); + QObject::connect(m_broker, &QtKuksaClient::setResponse, this, &VehicleSignals::onSetResponse); + QObject::connect(m_broker, &QtKuksaClient::subscribeResponse, this, &VehicleSignals::onSubscribeResponse); + //QObject::connect(m_broker, &QtKuksaClient::subscribeDone, this, &VehicleSignals::onSubscribeDone); - 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(".")); + emit connected(); +} - 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 = QString::number(dp["value"].toDouble(), 'f', 9); - } else if (dp["value"].isBool()) { - value = dp["value"].toBool() ? "true" : "false"; - } else { - qWarning() << "Malformed response (unsupported value type)"; - return false; - } +void VehicleSignals::onGetResponse(QString path, QString value, QString timestamp) +{ + emit getSuccessResponse(path, value, timestamp); +} - if (!(dp.contains("ts") && dp["ts"].isString())) { - qWarning() << "Malformed response (timestamp missing)"; - return false; - } - timestamp = dp["ts"].toString(); +void VehicleSignals::onSetResponse(QString path, QString error) +{ + emit setErrorResponse(path, error); +} - return true; +void VehicleSignals::onSubscribeResponse(QString path, QString value, QString timestamp) +{ + if (m_config.verbose() > 1) + qDebug() << "VehicleSignals::onSubscribeResponse: got " << path << " = " << value; + emit signalNotification(path, value, timestamp); } -// -// 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) +void VehicleSignals::onSubscribeDone(const QMap<QString, bool> &signals_, bool canceled) { - msg = msg.simplified(); - QJsonDocument doc(QJsonDocument::fromJson(msg.toUtf8())); - if (doc.isEmpty()) { - qWarning() << "Received invalid JSON: empty VIS message"; - return; + if (!canceled) { + // queue up a resubscribe attempt + QFuture<void> future = QtConcurrent::run(this, &VehicleSignals::resubscribe, signals_); } +} - if (!doc.isObject()) { - qWarning() << "Received invalid JSON: malformed VIS message"; - return; - } - QJsonObject obj = doc.object(); +// Private - 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; - } +void VehicleSignals::resubscribe(const QMap<QString, bool> &signals_) +{ + // Delay 100 milliseconds between subscribe attempts + QThread::msleep(100); + + if (m_broker) + m_broker->subscribe(signals_); } |