summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--hvac/hvac.cpp21
-rw-r--r--hvac/hvac.h2
-rw-r--r--meson.build2
-rw-r--r--meson_options.txt1
-rw-r--r--navigation/navigation.cpp194
-rw-r--r--navigation/navigation.h35
-rw-r--r--vehicle-signals/QtKuksaClient.cpp421
-rw-r--r--vehicle-signals/QtKuksaClient.h86
-rw-r--r--vehicle-signals/VehicleSignalsConfig.cpp104
-rw-r--r--vehicle-signals/VehicleSignalsConfig.h43
-rw-r--r--vehicle-signals/meson.build58
-rw-r--r--vehicle-signals/vehiclesignals.cpp481
-rw-r--r--vehicle-signals/vehiclesignals.h87
13 files changed, 990 insertions, 545 deletions
diff --git a/hvac/hvac.cpp b/hvac/hvac.cpp
index 246fa63..63ba5cf 100644
--- a/hvac/hvac.cpp
+++ b/hvac/hvac.cpp
@@ -29,8 +29,6 @@ HVAC::HVAC(VehicleSignals *vs, QObject * parent) :
m_temp_right_zone(21)
{
QObject::connect(m_vs, &VehicleSignals::connected, this, &HVAC::onConnected);
- QObject::connect(m_vs, &VehicleSignals::authorized, this, &HVAC::onAuthorized);
- QObject::connect(m_vs, &VehicleSignals::disconnected, this, &HVAC::onDisconnected);
if (m_vs)
m_vs->connect();
@@ -48,7 +46,7 @@ void HVAC::set_fanspeed(int speed)
// Scale incoming 0-255 speed to 0-100 to match VSS signal
double value = (speed % 256) * 100.0 / 255.0;
- m_vs->set("Vehicle.Cabin.HVAC.Station.Row1.Left.FanSpeed", QString::number((int) (value + 0.5)));
+ m_vs->set("Vehicle.Cabin.HVAC.Station.Row1.Left.FanSpeed", (unsigned int) (value + 0.5), true);
emit fanSpeedChanged(speed);
}
@@ -63,7 +61,7 @@ void HVAC::set_temp_left_zone(int temp)
value = 50;
else if (value < -50)
value = -50;
- m_vs->set("Vehicle.Cabin.HVAC.Station.Row1.Left.Temperature", QString::number(value));
+ m_vs->set("Vehicle.Cabin.HVAC.Station.Row1.Left.Temperature", value, true);
emit leftTemperatureChanged(temp);
}
@@ -78,7 +76,7 @@ void HVAC::set_temp_right_zone(int temp)
value = 50;
else if (value < -50)
value = -50;
- m_vs->set("Vehicle.Cabin.HVAC.Station.Row1.Right.Temperature", QString::number(value));
+ m_vs->set("Vehicle.Cabin.HVAC.Station.Row1.Right.Temperature", value, true);
emit rightTemperatureChanged(temp);
}
@@ -87,21 +85,8 @@ void HVAC::onConnected()
if (!m_vs)
return;
- m_vs->authorize();
-}
-
-void HVAC::onAuthorized()
-{
- if (!m_vs)
- return;
-
// Could subscribe and connect notification signal here to monitor
// external updates...
m_connected = true;
}
-
-void HVAC::onDisconnected()
-{
- m_connected = false;
-}
diff --git a/hvac/hvac.h b/hvac/hvac.h
index 34530fa..12c84fb 100644
--- a/hvac/hvac.h
+++ b/hvac/hvac.h
@@ -45,8 +45,6 @@ signals:
private slots:
void onConnected();
- void onAuthorized();
- void onDisconnected();
private:
VehicleSignals *m_vs;
diff --git a/meson.build b/meson.build
index eb52859..263d3d5 100644
--- a/meson.build
+++ b/meson.build
@@ -19,7 +19,7 @@ project (
['cpp'],
version : '2.0.1',
license : 'Apache-2.0',
- meson_version : '>= 0.46.0',
+ meson_version : '>= 0.49.0',
default_options :
[
'warning_level=1',
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..1fe219e
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1 @@
+option('protos', type : 'string', value : '/usr/include', description : 'Include directory for .proto files')
diff --git a/navigation/navigation.cpp b/navigation/navigation.cpp
index cb9f6cd..a39684f 100644
--- a/navigation/navigation.cpp
+++ b/navigation/navigation.cpp
@@ -1,33 +1,23 @@
/*
- * Copyright (C) 2019-2022 Konsulko Group
+ * Copyright (C) 2019-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 <math.h>
#include "navigation.h"
#include "vehiclesignals.h"
-Navigation::Navigation(VehicleSignals *vs, QObject * parent) :
- QObject(parent),
- m_vs(vs),
- m_connected(false)
+#define ROUND9(x) (round((x) * 1000000000) / 1000000000)
+Navigation::Navigation(VehicleSignals *vs, bool router, QObject * parent) :
+ m_vs(vs),
+ m_router(router),
+ QObject(parent)
{
QObject::connect(m_vs, &VehicleSignals::connected, this, &Navigation::onConnected);
- QObject::connect(m_vs, &VehicleSignals::authorized, this, &Navigation::onAuthorized);
- QObject::connect(m_vs, &VehicleSignals::disconnected, this, &Navigation::onDisconnected);
if (m_vs)
m_vs->connect();
@@ -40,7 +30,7 @@ Navigation::~Navigation()
void Navigation::sendWaypoint(double lat, double lon)
{
- if (!(m_vs && m_connected))
+ if (!(m_vs && m_connected && m_router))
return;
// The original implementation resulted in at least 9 decimal places
@@ -48,18 +38,18 @@ void Navigation::sendWaypoint(double lat, double lon)
// practice going from the QString default 6 to 9 does make a difference
// with respect to smoothness in the position-based map rotations done
// in tbtnavi.
- m_vs->set("Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Latitude", QString::number(lat, 'f', 9));
- m_vs->set("Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Longitude", QString::number(lon, 'f', 9));
+ m_vs->set("Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Latitude", lat);
+ m_vs->set("Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Longitude", lon);
}
void Navigation::broadcastPosition(double lat, double lon, double drc, double dst)
{
- if (!(m_vs && m_connected))
+ if (!(m_vs && m_connected && m_router))
return;
- m_vs->set("Vehicle.CurrentLocation.Latitude", QString::number(lat, 'f', 9));
- m_vs->set("Vehicle.CurrentLocation.Longitude", QString::number(lon, 'f', 9));
- m_vs->set("Vehicle.CurrentLocation.Heading", QString::number(drc, 'f', 9));
+ m_vs->set("Vehicle.CurrentLocation.Latitude", lat);
+ m_vs->set("Vehicle.CurrentLocation.Longitude", lon);
+ m_vs->set("Vehicle.CurrentLocation.Heading", drc);
// NOTES:
// - This signal is an AGL addition, it may make sense to engage with the
@@ -67,24 +57,24 @@ void Navigation::broadcastPosition(double lat, double lon, double drc, double ds
// - The signal makes more sense in kilometers wrt VSS expectations, so
// conversion from meters happens here for now to avoid changing the
// existing clients. This may be worth revisiting down the road.
- m_vs->set("Vehicle.Cabin.Infotainment.Navigation.ElapsedDistance", QString::number(dst / 1000, 'f', 9));
+ m_vs->set("Vehicle.Cabin.Infotainment.Navigation.ElapsedDistance", (float) (dst / 1000));
}
void Navigation::broadcastRouteInfo(double lat, double lon, double route_lat, double route_lon)
{
- if (!(m_vs && m_connected))
+ if (!(m_vs && m_connected && m_router))
return;
- m_vs->set("Vehicle.CurrentLocation.Latitude", QString::number(lat, 'f', 9));
- m_vs->set("Vehicle.CurrentLocation.Longitude", QString::number(lon, 'f', 9));
+ m_vs->set("Vehicle.CurrentLocation.Latitude", lat);
+ m_vs->set("Vehicle.CurrentLocation.Longitude", lon);
- m_vs->set("Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Latitude", QString::number(route_lat, 'f', 9));
- m_vs->set("Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Longitude", QString::number(route_lon, 'f', 9));
+ m_vs->set("Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Latitude", route_lat, true);
+ m_vs->set("Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Longitude", route_lon, true);
}
void Navigation::broadcastStatus(QString state)
{
- if (!(m_vs && m_connected))
+ if (!(m_vs && m_connected && m_router))
return;
// The signal states have been changed to all uppercase to match
@@ -97,58 +87,121 @@ void Navigation::onConnected()
if (!m_vs)
return;
- m_vs->authorize();
-}
-
-void Navigation::onAuthorized()
-{
- if (!m_vs)
- return;
-
m_connected = true;
- QObject::connect(m_vs, &VehicleSignals::signalNotification, this, &Navigation::onSignalNotification);
+ if (m_router) {
+ qInfo() << "Connected as router";
+ QObject::connect(m_vs, &VehicleSignals::signalNotification, this, &Navigation::onSignalNotificationRouter);
+ } else {
+ QObject::connect(m_vs, &VehicleSignals::signalNotification, this, &Navigation::onSignalNotification);
+ }
// NOTE: This signal is another AGL addition where it is possible
// upstream may be open to adding it to VSS.
- m_vs->subscribe("Vehicle.Cabin.Infotainment.Navigation.State");
- m_vs->subscribe("Vehicle.CurrentLocation.Latitude");
- m_vs->subscribe("Vehicle.CurrentLocation.Longitude");
- m_vs->subscribe("Vehicle.CurrentLocation.Heading");
- m_vs->subscribe("Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Latitude");
- m_vs->subscribe("Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Longitude");
- m_vs->subscribe("Vehicle.Cabin.Infotainment.Navigation.ElapsedDistance");
-}
-
-void Navigation::onDisconnected()
-{
- QObject::disconnect(m_vs, &VehicleSignals::signalNotification, this, &Navigation::onSignalNotification);
-
- m_connected = false;
+ QMap<QString, bool> s;
+ s["Vehicle.Cabin.Infotainment.Navigation.State"] = false;
+ if (!m_router) {
+ // Router broadcasts these signals and does not need to listen to them
+ s["Vehicle.CurrentLocation.Latitude"] = false;
+ s["Vehicle.CurrentLocation.Longitude"] = false;
+ s["Vehicle.CurrentLocation.Heading"] = false;
+ s["Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Latitude"] = false;
+ s["Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Longitude"] = false;
+ s["Vehicle.Cabin.Infotainment.Navigation.ElapsedDistance"] = false;
+ } else {
+ // Router acts as actuator for these signals
+ // This is a bit convoluted, but would allow wiring up external destination
+ // setting with e.g. Alexa as has been done in the past.
+ s["Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Latitude"] = true;
+ s["Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Longitude"] = true;
+ }
+ m_vs->subscribe(s);
}
void Navigation::onSignalNotification(QString path, QString value, QString timestamp)
{
- // NOTE: Since all the known AGL users of the VSS signals are users of
- // this API, we know that updates occur in certain sequences and
- // can leverage this to roll up for emitting the existing events.
- // This is the path of least effort with respect to changing
- // the existing clients, but it may make sense down the road to
- // either switch them to using VehicleSignals directly or having
- // a more granular signal scheme that maps more directly onto
- // VSS.
if (path == "Vehicle.Cabin.Infotainment.Navigation.State") {
QVariantMap event;
event["state"] = value;
emit statusEvent(event);
} else if (path == "Vehicle.CurrentLocation.Latitude") {
+ m_position_mutex.lock();
m_latitude = value.toDouble();
+ m_latitude_set = true;
+ handlePositionUpdate();
+ m_position_mutex.unlock();
} else if (path == "Vehicle.CurrentLocation.Longitude") {
+ m_position_mutex.lock();
m_longitude = value.toDouble();
+ m_longitude_set = true;
+ handlePositionUpdate();
+ m_position_mutex.unlock();
} else if (path == "Vehicle.CurrentLocation.Heading") {
+ m_position_mutex.lock();
m_heading = value.toDouble();
+ m_heading_set = true;
+ handlePositionUpdate();
+ m_position_mutex.unlock();
} else if (path == "Vehicle.Cabin.Infotainment.Navigation.ElapsedDistance") {
+ m_position_mutex.lock();
m_distance = value.toDouble();
+ m_distance_set = true;
+ handlePositionUpdate();
+ m_position_mutex.unlock();
+ } else if (path == "Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Latitude") {
+ m_position_mutex.lock();
+ m_dest_latitude = value.toDouble();
+ m_dest_latitude_set = true;
+ handlePositionUpdate();
+ m_position_mutex.unlock();
+ } else if (path == "Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Longitude") {
+ m_position_mutex.lock();
+ m_dest_longitude = value.toDouble();
+ m_dest_longitude_set = true;
+ handlePositionUpdate();
+ m_position_mutex.unlock();
+
+ // NOTE: Potentially could emit a waypointsEvent here, but
+ // nothing in the demo currently requires it, so do
+ // not bother for now. If something like Alexa is
+ // added it or some other replacement / rework will
+ // be required.
+ }
+}
+
+void Navigation::onSignalNotificationRouter(QString path, QString value, QString timestamp)
+{
+ if (path == "Vehicle.Cabin.Infotainment.Navigation.State") {
+ QVariantMap event;
+ event["state"] = value;
+ emit statusEvent(event);
+ } else if (path == "Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Latitude") {
+ m_vs->set(path, value.toDouble());
+ } else if (path == "Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Longitude") {
+ m_vs->set(path, value.toDouble());
+ }
+}
+
+// Private
+
+void Navigation::handlePositionUpdate()
+{
+ // NOTE: Since all the known AGL users of the VSS signals are users of
+ // this API, we know that updates occur in certain sequences and
+ // can leverage this to roll up for emitting the existing events.
+ //
+ // The switch to the gRPC API for KUKSA.val complicates this as
+ // we now have to live with the combination of only getting
+ // notified when a signal changes to a new value on top of
+ // notifications not necessarily happening in order by the time
+ // they get through gRPC client/server/client/Qt stacks. The "set"
+ // flags give us something that mostly works for now, but further
+ // investigation is required. It is possible that either a switch
+ // to a custom gRPC API (i.e. standalone nav service daemon) or
+ // pushing for a rolled up position signal in a struct in upstream
+ // VSS might be needed to do this better.
+
+ if (m_latitude_set && m_longitude_set && m_heading_set && m_distance_set) {
QVariantMap event;
event["position"] = "car";
event["latitude"] = m_latitude;
@@ -156,10 +209,9 @@ void Navigation::onSignalNotification(QString path, QString value, QString times
event["direction"] = m_heading;
event["distance"] = m_distance * 1000;
emit positionEvent(event);
- } else if (path == "Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Latitude") {
- m_dest_latitude = value.toDouble();
- } else if (path == "Vehicle.Cabin.Infotainment.Navigation.DestinationSet.Longitude") {
- m_dest_longitude = value.toDouble();
+ // The distance being updated is our trigger, so clear its flag.
+ m_distance_set = false;
+ } else if (m_latitude_set && m_longitude_set && m_dest_latitude_set && m_dest_longitude_set) {
QVariantMap event;
event["position"] = "route";
event["latitude"] = m_latitude;
@@ -167,11 +219,7 @@ void Navigation::onSignalNotification(QString path, QString value, QString times
event["route_latitude"] = m_dest_latitude;
event["route_longitude"] = m_dest_longitude;
emit positionEvent(event);
-
- // NOTE: Potentially could emit a waypointsEvent here, but
- // nothing in the demo currently requires it, so do
- // not bother for now. If something like Alexa is
- // added it or some other replacement / rework will
- // be required.
+ // The destination values are the triggers, so clear their flags.
+ m_dest_latitude_set = m_dest_longitude_set = false;
}
}
diff --git a/navigation/navigation.h b/navigation/navigation.h
index 2c8a8ff..eb15ed3 100644
--- a/navigation/navigation.h
+++ b/navigation/navigation.h
@@ -1,17 +1,7 @@
/*
- * Copyright (C) 2019-2022 Konsulko Group
+ * Copyright (C) 2019-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
*/
#ifndef NAVIGATION_H
@@ -19,6 +9,7 @@
#include <QObject>
#include <QVariant>
+#include <QMutex>
class VehicleSignals;
@@ -27,7 +18,7 @@ class Navigation : public QObject
Q_OBJECT
public:
- explicit Navigation(VehicleSignals *vs, QObject *parent = Q_NULLPTR);
+ explicit Navigation(VehicleSignals *vs, bool router = false, QObject *parent = Q_NULLPTR);
virtual ~Navigation();
Q_INVOKABLE void broadcastPosition(double lat, double lon, double drc, double dst);
@@ -44,19 +35,29 @@ signals:
private slots:
void onConnected();
- void onAuthorized();
- void onDisconnected();
void onSignalNotification(QString path, QString value, QString timestamp);
+ void onSignalNotificationRouter(QString path, QString value, QString timestamp);
private:
+ void handlePositionUpdate();
+
VehicleSignals *m_vs;
- bool m_connected;
+ bool m_router = false;
+ bool m_connected = false;
+
+ QMutex m_position_mutex;
double m_latitude;
+ bool m_latitude_set = false;
double m_longitude;
+ bool m_longitude_set = false;
double m_heading;
- double m_distance;
+ bool m_heading_set = false;
+ double m_distance = 0.0;
+ bool m_distance_set = false;
double m_dest_latitude;
+ bool m_dest_latitude_set = false;
double m_dest_longitude;
+ bool m_dest_longitude_set = false;
};
#endif // NAVIGATION_H
diff --git a/vehicle-signals/QtKuksaClient.cpp b/vehicle-signals/QtKuksaClient.cpp
new file mode 100644
index 0000000..901459d
--- /dev/null
+++ b/vehicle-signals/QtKuksaClient.cpp
@@ -0,0 +1,421 @@
+/*
+ * Copyright (C) 2023 Konsulko Group
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <QDebug>
+#include <QSettings>
+#include <QUrl>
+#include <QFile>
+#include <QtConcurrent>
+#include <mutex>
+#include <chrono>
+
+#include "QtKuksaClient.h"
+
+using grpc::Channel;
+using grpc::ClientContext;
+using grpc::ClientReader;
+using grpc::Status;
+
+class QtKuksaClient::SubscribeReader : public grpc::ClientReadReactor<SubscribeResponse> {
+public:
+ SubscribeReader(VAL::Stub *stub,
+ QtKuksaClient *client,
+ VehicleSignalsConfig &config,
+ const QMap<QString, bool> &s,
+ const SubscribeRequest *request):
+ client_(client),
+ config_(config),
+ signals_(s) {
+ QString token = config_.authToken();
+ if (!token.isEmpty()) {
+ token.prepend("Bearer ");
+ context_.AddMetadata(std::string("authorization"), token.toStdString());
+ }
+ stub->async()->Subscribe(&context_, request, this);
+ StartRead(&response_);
+ StartCall();
+ }
+ void OnReadDone(bool ok) override {
+ std::unique_lock<std::mutex> lock(mutex_);
+ if (ok) {
+ if (client_)
+ client_->handleSubscribeResponse(&response_);
+ StartRead(&response_);
+ }
+ }
+ void OnDone(const Status& s) override {
+ status_ = s;
+ if (client_) {
+ if (config_.verbose() > 1)
+ qDebug() << "QtKuksaClient::subscribe::Reader done";
+ client_->handleSubscribeDone(signals_, status_);
+ }
+
+ // gRPC engine is done with us, safe to self-delete
+ delete this;
+ }
+
+private:
+ QtKuksaClient *client_;
+ VehicleSignalsConfig config_;
+ QMap<QString, bool> signals_;
+
+ ClientContext context_;
+ SubscribeResponse response_;
+ std::mutex mutex_;
+ Status status_;
+};
+
+QtKuksaClient::QtKuksaClient(const std::shared_ptr< ::grpc::ChannelInterface>& channel,
+ const VehicleSignalsConfig &config,
+ QObject *parent) :
+ QObject(parent),
+ m_channel(channel),
+ m_config(config),
+ m_connected(false)
+{
+ m_stub = VAL::NewStub(channel);
+
+}
+
+void QtKuksaClient::connect()
+{
+ // Check for connection in another thread
+ QFuture<void> future = QtConcurrent::run(this, &QtKuksaClient::waitForConnected);
+}
+
+void QtKuksaClient::get(const QString &path, const bool actuator)
+{
+ m_connected_mutex.lock();
+ if (!m_connected) {
+ m_connected_mutex.unlock();
+ return;
+ }
+ m_connected_mutex.unlock();
+
+ ClientContext *context = new ClientContext();
+ if (!context) {
+ handleCriticalFailure("Could not create ClientContext");
+ return;
+ }
+ QString token = m_config.authToken();
+ if (!token.isEmpty()) {
+ token.prepend("Bearer ");
+ context->AddMetadata(std::string("authorization"), token.toStdString());
+ }
+
+ GetRequest request;
+ auto entry = request.add_entries();
+ entry->set_path(path.toStdString());
+ entry->add_fields(Field::FIELD_PATH);
+ if (actuator)
+ entry->add_fields(Field::FIELD_ACTUATOR_TARGET);
+ else
+ entry->add_fields(Field::FIELD_VALUE);
+
+ GetResponse *response = new GetResponse();
+ if (!response) {
+ handleCriticalFailure("Could not create GetResponse");
+ return;
+ }
+
+ // NOTE: Using ClientUnaryReactor instead of the shortcut method
+ // would allow getting detailed errors.
+ m_stub->async()->Get(context, &request, response,
+ [this, context, response](Status s) {
+ if (s.ok())
+ handleGetResponse(response);
+ delete response;
+ delete context;
+ });
+}
+
+// Since a set request needs a Datapoint with the appropriate type value,
+// checking the signal metadata to get the type would be a requirement for
+// a generic set call that takes a string as argument. For now, assume
+// that set with a string is specifically for a signal of string type.
+
+void QtKuksaClient::set(const QString &path, const QString &value, const bool actuator)
+{
+ Datapoint dp;
+ dp.set_string(value.toStdString());
+ set(path, dp, actuator);
+}
+
+void QtKuksaClient::set(const QString &path, const qint32 value, const bool actuator)
+{
+ Datapoint dp;
+ dp.set_int32(value);
+ set(path, dp, actuator);
+}
+
+void QtKuksaClient::set(const QString &path, const qint64 value, const bool actuator)
+{
+ Datapoint dp;
+ dp.set_int64(value);
+ set(path, dp, actuator);
+}
+
+void QtKuksaClient::set(const QString &path, const quint32 value, const bool actuator)
+{
+ Datapoint dp;
+ dp.set_uint32(value);
+ set(path, dp, actuator);
+}
+
+void QtKuksaClient::set(const QString &path, const quint64 value, const bool actuator)
+{
+ Datapoint dp;
+ dp.set_uint64(value);
+ set(path, dp, actuator);
+}
+
+void QtKuksaClient::set(const QString &path, const float value, const bool actuator)
+{
+ Datapoint dp;
+ dp.set_float_(value);
+ set(path, dp, actuator);
+}
+
+void QtKuksaClient::set(const QString &path, const double value, const bool actuator)
+{
+ Datapoint dp;
+ dp.set_double_(value);
+ set(path, dp, actuator);
+}
+
+void QtKuksaClient::subscribe(const QString &path, const bool actuator)
+{
+ m_connected_mutex.lock();
+ if (!m_connected) {
+ m_connected_mutex.unlock();
+ return;
+ }
+ m_connected_mutex.unlock();
+
+ QMap<QString, bool> s;
+ s[path] = actuator;
+ subscribe(s);
+}
+
+void QtKuksaClient::subscribe(const QMap<QString, bool> &signals_)
+{
+ m_connected_mutex.lock();
+ if (!m_connected) {
+ m_connected_mutex.unlock();
+ return;
+ }
+ m_connected_mutex.unlock();
+
+ SubscribeRequest *request = new SubscribeRequest();
+ if (!request) {
+ handleCriticalFailure("Could not create SubscribeRequest");
+ return;
+ }
+
+ auto it = signals_.constBegin();
+ while (it != signals_.constEnd()) {
+ if (m_config.verbose() > 1)
+ qDebug() << "QtKuksaClient::subscribe: adding " << it.key() << ", actuator " << it.value();
+ auto entry = request->add_entries();
+ entry->set_path(it.key().toStdString());
+ entry->add_fields(Field::FIELD_PATH);
+ if (it.value())
+ entry->add_fields(Field::FIELD_ACTUATOR_TARGET);
+ else
+ entry->add_fields(Field::FIELD_VALUE);
+ ++it;
+ }
+
+ SubscribeReader *reader = new SubscribeReader(m_stub.get(), this, m_config, signals_, request);
+ if (!reader)
+ handleCriticalFailure("Could not create SubscribeReader");
+
+ delete request;
+}
+
+// Private
+
+void QtKuksaClient::waitForConnected()
+{
+ while (!m_channel->WaitForConnected(std::chrono::system_clock::now() +
+ std::chrono::milliseconds(500)));
+
+ m_connected_mutex.lock();
+ m_connected = true;
+ m_connected_mutex.unlock();
+
+ qInfo() << "Databroker gRPC channel ready";
+ emit connected();
+}
+
+void QtKuksaClient::set(const QString &path, const Datapoint &dp, const bool actuator)
+{
+ m_connected_mutex.lock();
+ if (!m_connected) {
+ m_connected_mutex.unlock();
+ return;
+ }
+ m_connected_mutex.unlock();
+
+ ClientContext *context = new ClientContext();
+ if (!context) {
+ handleCriticalFailure("Could not create ClientContext");
+ return;
+ }
+ QString token = m_config.authToken();
+ if (!token.isEmpty()) {
+ token.prepend("Bearer ");
+ context->AddMetadata(std::string("authorization"), token.toStdString());
+ }
+
+ SetRequest request;
+ auto update = request.add_updates();
+ auto entry = update->mutable_entry();
+ entry->set_path(path.toStdString());
+ if (actuator) {
+ auto target = entry->mutable_actuator_target();
+ *target = dp;
+ update->add_fields(Field::FIELD_ACTUATOR_TARGET);
+ } else {
+ auto value = entry->mutable_value();
+ *value = dp;
+ update->add_fields(Field::FIELD_VALUE);
+ }
+
+ SetResponse *response = new SetResponse();
+ if (!response) {
+ handleCriticalFailure("Could not create SetResponse");
+ delete context;
+ return;
+ }
+
+ // NOTE: Using ClientUnaryReactor instead of the shortcut method
+ // would allow getting detailed errors.
+ m_stub->async()->Set(context, &request, response,
+ [this, context, response](Status s) {
+ if (s.ok())
+ handleSetResponse(response);
+ delete response;
+ delete context;
+ });
+}
+
+void QtKuksaClient::handleGetResponse(const GetResponse *response)
+{
+ if (!(response && response->entries_size()))
+ return;
+
+ for (auto it = response->entries().begin(); it != response->entries().end(); ++it) {
+ // We expect paths in the response entries
+ if (!it->path().size())
+ continue;
+ Datapoint dp;
+ if (it->has_actuator_target())
+ dp = it->actuator_target();
+ else
+ dp = it->value();
+
+ QString path = QString::fromStdString(it->path());
+ QString value;
+ QString timestamp;
+ if (convertDatapoint(dp, value, timestamp)) {
+ if (m_config.verbose() > 1)
+ qDebug() << "QtKuksaClient::handleGetResponse: path = " << path << ", value = " << value;
+
+ emit getResponse(path, value, timestamp);
+ }
+ }
+}
+
+void QtKuksaClient::handleSetResponse(const SetResponse *response)
+{
+ if (!(response && response->errors_size()))
+ return;
+
+ for (auto it = response->errors().begin(); it != response->errors().end(); ++it) {
+ QString path = QString::fromStdString(it->path());
+ QString error = QString::fromStdString(it->error().reason());
+ emit setResponse(path, error);
+ }
+
+}
+
+void QtKuksaClient::handleSubscribeResponse(const SubscribeResponse *response)
+{
+ if (!(response && response->updates_size()))
+ return;
+
+ for (auto it = response->updates().begin(); it != response->updates().end(); ++it) {
+ // We expect entries that have paths in the response
+ if (!(it->has_entry() && it->entry().path().size()))
+ continue;
+
+ auto entry = it->entry();
+ QString path = QString::fromStdString(entry.path());
+
+ Datapoint dp;
+ if (entry.has_actuator_target())
+ dp = entry.actuator_target();
+ else
+ dp = entry.value();
+
+ QString value;
+ QString timestamp;
+ if (convertDatapoint(dp, value, timestamp)) {
+ if (m_config.verbose() > 1)
+ qDebug() << "QtKuksaClient::handleSubscribeResponse: path = " << path << ", value = " << value;
+
+ emit subscribeResponse(path, value, timestamp);
+ }
+ }
+}
+
+void QtKuksaClient::handleSubscribeDone(const QMap<QString, bool> &signals_, const Status &status)
+{
+ qInfo() << "QtKuksaClient::handleSubscribeDone: enter";
+ if (m_config.verbose() > 1)
+ qDebug() << "Subscribe status = " << status.error_code() <<
+ " (" << QString::fromStdString(status.error_message()) << ")";
+
+ emit subscribeDone(signals_, status.error_code() == grpc::CANCELLED);
+}
+
+bool QtKuksaClient::convertDatapoint(const Datapoint &dp, QString &value, QString &timestamp)
+{
+ // NOTE: Currently no array type support
+
+ if (dp.has_string())
+ value = QString::fromStdString(dp.string());
+ else if (dp.has_int32())
+ value = QString::number(dp.int32());
+ else if (dp.has_int64())
+ value = QString::number(dp.int64());
+ else if (dp.has_uint32())
+ value = QString::number(dp.uint32());
+ else if (dp.has_uint64())
+ value = QString::number(dp.uint64());
+ else if (dp.has_float_())
+ value = QString::number(dp.float_(), 'f', 6);
+ else if (dp.has_double_())
+ value = QString::number(dp.double_(), 'f', 6);
+ else if (dp.has_bool_())
+ value = dp.bool_() ? "true" : "false";
+ else {
+ if (m_config.verbose())
+ qWarning() << "Malformed response (unsupported value type)";
+ return false;
+ }
+
+ timestamp = QString::number(dp.timestamp().seconds()) + "." + QString::number(dp.timestamp().nanos());
+ return true;
+}
+
+void QtKuksaClient::handleCriticalFailure(const QString &error)
+{
+ if (error.size())
+ qCritical() << error;
+}
diff --git a/vehicle-signals/QtKuksaClient.h b/vehicle-signals/QtKuksaClient.h
new file mode 100644
index 0000000..1f24e09
--- /dev/null
+++ b/vehicle-signals/QtKuksaClient.h
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 Konsulko Group
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef QT_KUKSA_CLIENT_H
+#define QT_KUKSA_CLIENT_H
+
+#include <QObject>
+#include <QMap>
+#include <QMutex>
+#include <grpcpp/grpcpp.h>
+#include "kuksa/val/v1/val.grpc.pb.h"
+
+// Just pull in the whole namespace since Datapoint contains a lot of
+// definitions that may potentially be needed.
+using namespace kuksa::val::v1;
+
+using grpc::Status;
+
+#include "VehicleSignalsConfig.h"
+
+// KUKSA.val databroker gRPC API Qt client class
+
+class QtKuksaClient : public QObject
+{
+ Q_OBJECT
+
+public:
+ explicit QtKuksaClient(const std::shared_ptr< ::grpc::ChannelInterface>& channel,
+ const VehicleSignalsConfig &config,
+ QObject * parent = Q_NULLPTR);
+
+ Q_INVOKABLE void connect();
+
+ Q_INVOKABLE void get(const QString &path, const bool actuator = false);
+
+ Q_INVOKABLE void set(const QString &path, const QString &value, const bool actuator = false);
+ Q_INVOKABLE void set(const QString &path, const qint32 value, const bool actuator = false);
+ Q_INVOKABLE void set(const QString &path, const qint64 value, const bool actuator = false);
+ Q_INVOKABLE void set(const QString &path, const quint32 value, const bool actuator = false);
+ Q_INVOKABLE void set(const QString &path, const quint64 value, const bool actuator = false);
+ Q_INVOKABLE void set(const QString &path, const float value, const bool actuator = false);
+ Q_INVOKABLE void set(const QString &path, const double value, const bool actuator = false);
+
+ Q_INVOKABLE void subscribe(const QString &path, const bool actuator = false);
+ Q_INVOKABLE void subscribe(const QMap<QString, bool> &signals_);
+
+signals:
+ void connected();
+ void getResponse(QString path, QString value, QString timestamp);
+ void setResponse(QString path, QString error);
+ void subscribeResponse(QString path, QString value, QString timestamp);
+ void subscribeDone(const QMap<QString, bool> &signals_, bool canceled);
+
+private:
+ class SubscribeReader;
+
+ std::shared_ptr< ::grpc::ChannelInterface> m_channel;
+ VehicleSignalsConfig m_config;
+ std::shared_ptr<VAL::Stub> m_stub;
+ bool m_connected;
+ QMutex m_connected_mutex;
+
+ void waitForConnected();
+
+ void set(const QString &path, const Datapoint &dp, const bool actuator);
+
+ void handleGetResponse(const GetResponse *response);
+
+ void handleSetResponse(const SetResponse *response);
+
+ void handleSubscribeResponse(const SubscribeResponse *response);
+
+ void handleSubscribeDone(const QMap<QString, bool> &signals_, const Status &status);
+
+ void handleCriticalFailure(const QString &error);
+ void handleCriticalFailure(const char *error) { handleCriticalFailure(QString(error)); };
+
+ void resubscribe(const QMap<QString, bool> &signals_);
+
+ bool convertDatapoint(const Datapoint &dp, QString &value, QString &timestamp);
+};
+
+#endif // QT_KUKSA_CLIENT_H
diff --git a/vehicle-signals/VehicleSignalsConfig.cpp b/vehicle-signals/VehicleSignalsConfig.cpp
new file mode 100644
index 0000000..c72c2cd
--- /dev/null
+++ b/vehicle-signals/VehicleSignalsConfig.cpp
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 Konsulko Group
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <QDebug>
+#include <QSettings>
+#include <QUrl>
+#include <QFile>
+// ?
+//#include <QSslKey>
+//#include <QTimer>
+
+#include "VehicleSignalsConfig.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 &caCert,
+ const QString &tlsServerName,
+ const QString &authToken) :
+ m_hostname(hostname),
+ m_port(port),
+ m_caCert(caCert),
+ m_tlsServerName(tlsServerName),
+ m_authToken(authToken),
+ 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("kuksa-client/server", "localhost").toString();
+ if (m_hostname.isEmpty()) {
+ qCritical() << "Invalid server hostname";
+ return;
+ }
+
+ m_port = pSettings->value("kuksa-client/port", 55555).toInt();
+ if (m_port == 0) {
+ qCritical() << "Invalid server port";
+ return;
+ }
+
+ QString caCertFileName = pSettings->value("kuksa-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;
+ }
+ m_caCert = caCertData;
+
+ m_tlsServerName = pSettings->value("kuksa-client/tls-server-name", "").toString();
+
+ QString authTokenFileName = pSettings->value("kuksa-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("kuksa-client/verbose").toString();
+ if (!verbose.isEmpty()) {
+ if (verbose == "true" || verbose == "1")
+ m_verbose = 1;
+ if (verbose == "2")
+ m_verbose = 2;
+ }
+
+ m_valid = true;
+}
diff --git a/vehicle-signals/VehicleSignalsConfig.h b/vehicle-signals/VehicleSignalsConfig.h
new file mode 100644
index 0000000..c3d52ca
--- /dev/null
+++ b/vehicle-signals/VehicleSignalsConfig.h
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 Konsulko Group
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#ifndef VEHICLE_SIGNALS_CONFIG_H
+#define VEHICLE_SIGNALS_CONFIG_H
+
+#include <QObject>
+
+// Class to read/hold VSS server configuration
+
+class VehicleSignalsConfig
+{
+public:
+ explicit VehicleSignalsConfig(const QString &hostname,
+ const unsigned port,
+ const QByteArray &caCert,
+ const QString &tlsServerName,
+ const QString &authToken);
+ explicit VehicleSignalsConfig(const QString &appname);
+ ~VehicleSignalsConfig() {};
+
+ QString hostname() { return m_hostname; };
+ unsigned port() { return m_port; };
+ QByteArray caCert() { return m_caCert; };
+ QString tlsServerName() { return m_tlsServerName; };
+ QString authToken() { return m_authToken; };
+ bool valid() { return m_valid; };
+ unsigned verbose() { return m_verbose; };
+
+private:
+ QString m_hostname;
+ unsigned m_port;
+ QByteArray m_caCert;
+ QString m_tlsServerName;
+ QString m_authToken;
+ bool m_valid = true;
+ unsigned m_verbose = 0;
+};
+
+#endif // VEHICLE_SIGNALS_CONFIG_H
diff --git a/vehicle-signals/meson.build b/vehicle-signals/meson.build
index e4c6dc0..50383e4 100644
--- a/vehicle-signals/meson.build
+++ b/vehicle-signals/meson.build
@@ -1,25 +1,69 @@
-qt5_dep = dependency('qt5', modules: ['Core', 'WebSockets'])
+cpp = meson.get_compiler('cpp')
+grpcpp_reflection_dep = cpp.find_library('grpc++_reflection')
-moc_files = qt5.compile_moc(headers: 'vehiclesignals.h',
+qt5_dep = dependency('qt5', modules: ['Core', 'Concurrent'])
+
+vs_dep = [
+ qt5_dep,
+ dependency('protobuf'),
+ dependency('grpc'),
+ dependency('grpc++'),
+ grpcpp_reflection_dep,
+]
+
+protoc = find_program('protoc')
+grpc_cpp = find_program('grpc_cpp_plugin')
+
+protos_base_dir = get_option('protos')
+protos_dir = protos_base_dir / 'kuksa/val/v1'
+protoc_gen = generator(protoc, \
+ output : ['@BASENAME@.pb.cc', '@BASENAME@.pb.h'],
+ arguments : ['-I=' + protos_base_dir,
+ '--cpp_out=@BUILD_DIR@',
+ '@INPUT@'])
+generated_protoc_sources = [ \
+ protoc_gen.process(protos_dir / 'types.proto', preserve_path_from : protos_base_dir),
+ protoc_gen.process(protos_dir / 'val.proto', preserve_path_from : protos_base_dir),
+]
+
+grpc_gen = generator(protoc, \
+ output : ['@BASENAME@.grpc.pb.cc', '@BASENAME@.grpc.pb.h'],
+ arguments : ['-I=' + protos_base_dir,
+ '--grpc_out=@BUILD_DIR@',
+ '--plugin=protoc-gen-grpc=' + grpc_cpp.path(),
+ '@INPUT@'])
+generated_grpc_sources = [ \
+ grpc_gen.process(protos_dir / 'val.proto', preserve_path_from : protos_base_dir),
+]
+
+moc_files = qt5.compile_moc(headers: [ 'vehiclesignals.h', 'QtKuksaClient.h' ],
dependencies: qt5_dep)
-src = ['vehiclesignals.cpp', moc_files]
+src = [
+ 'vehiclesignals.cpp',
+ 'VehicleSignalsConfig.cpp',
+ 'QtKuksaClient.cpp',
+ moc_files,
+ generated_protoc_sources,
+ generated_grpc_sources,
+]
+
lib = shared_library('qtappfw-vehicle-signals',
sources: src,
version: '1.0.0',
soversion: '0',
- dependencies: qt5_dep,
+ dependencies: vs_dep,
install: true)
-install_headers('vehiclesignals.h')
+install_headers(['vehiclesignals.h', 'VehicleSignalsConfig.h'])
pkg_mod = import('pkgconfig')
pkg_mod.generate(libraries: lib,
version: '1.0',
name: 'libqtappfw-vehicle-signals',
filebase: 'qtappfw-vehicle-signals',
- requires: ['Qt5Core', 'Qt5WebSockets'],
- description: 'Library wrapping VIS vehicle signaling API in Qt objects')
+ requires: ['Qt5Core'],
+ description: 'Library wrapping VSS API in Qt objects')
qtappfw_vs_dep = declare_dependency(dependencies: qt5_dep,
link_with: lib,
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 &timestamp)
+// 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_);
}
diff --git a/vehicle-signals/vehiclesignals.h b/vehicle-signals/vehiclesignals.h
index fd5fe9c..734cb27 100644
--- a/vehicle-signals/vehiclesignals.h
+++ b/vehicle-signals/vehiclesignals.h
@@ -1,63 +1,19 @@
/*
- * 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
*/
#ifndef VEHICLESIGNALS_H
#define VEHICLESIGNALS_H
#include <QObject>
-#include <QWebSocket>
-// Class to read/hold VIS server configuration
+#include "VehicleSignalsConfig.h"
-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; };
+class QtKuksaClient;
-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
+// VSS signal interface class
class VehicleSignals : public QObject
{
@@ -70,30 +26,39 @@ public:
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);
+ Q_INVOKABLE void get(const QString &path, const bool actuator = false);
+
+ Q_INVOKABLE void set(const QString &path, const QString &value, const bool actuator = false);
+ Q_INVOKABLE void set(const QString &path, const qint32 value, const bool actuator = false);
+ Q_INVOKABLE void set(const QString &path, const qint64 value, const bool actuator = false);
+ Q_INVOKABLE void set(const QString &path, const quint32 value, const bool actuator = false);
+ Q_INVOKABLE void set(const QString &path, const quint64 value, const bool actuator = false);
+ Q_INVOKABLE void set(const QString &path, const float value, const bool actuator = false);
+ Q_INVOKABLE void set(const QString &path, const double value, const bool actuator = false);
+
+ Q_INVOKABLE void subscribe(const QString &path, const bool actuator = false);
+ Q_INVOKABLE void subscribe(const QMap<QString, bool> &signals_);
signals:
void connected();
void authorized();
- void getSuccessResponse(QString path, QString value, QString timestamp);
- void signalNotification(QString path, QString value, QString timestamp);
void disconnected();
+ void getSuccessResponse(QString path, QString value, QString timestamp);
+ void setErrorResponse(QString path, QString error);
+ void signalNotification(QString path, QString value, QString timestamp);
private slots:
void onConnected();
- void onError(QAbstractSocket::SocketError error);
- void reconnect();
- void onDisconnected();
- void onTextMessageReceived(QString message);
+ void onGetResponse(QString path, QString value, QString timestamp);
+ void onSetResponse(QString path, QString error);
+ void onSubscribeResponse(QString path, QString value, QString timestamp);
+ void onSubscribeDone(const QMap<QString, bool> &signals_, bool canceled);
private:
VehicleSignalsConfig m_config;
- QWebSocket m_websocket;
- std::atomic<unsigned int> m_request_id;
+ QtKuksaClient *m_broker;
- bool parseData(const QJsonObject &response, QString &path, QString &value, QString &timestamp);
+ void resubscribe(const QMap<QString, bool> &signals_);
};
#endif // VEHICLESIGNALS_H