summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorScott Murray <scott.murray@konsulko.com>2022-06-16 00:28:36 -0400
committerScott Murray <scott.murray@konsulko.com>2022-06-17 17:44:47 -0400
commit096908375ecbfc6388d0aec69a35b2a8ffc53d47 (patch)
tree4f346d772fcdcbfc112d5aba4d379d1db175979a /src
parent7f647062d889b299a4dd521148a4970bf6c8e75a (diff)
Repurpose repository for a spiritual successor of the previous binding. The replacement is a daemon that demonstrates servicing HVAC actuators from the VSS schema via VIS signals from KUKSA.val. Currently the connection to KUKSA.val is websocket based using the boost::asio framework, but the plan is to migrate to grpc as that becomes more robust in KUKSA.val. Notable changes: - New code is completely C++, partly to leverage using Boost, but also to futureproof future work with grpc. - Switch from CMake to meson for ease of development and some degree of futureproofing. - Use with systemd is assumed; behavior follows the systemd daemon guidelines barring the use of journald logging prefixes, which may be addressed with future work. A systemd unit is also installed as part of the build. - SPDX license headers using SPDX "short identifiers" are used in source files rather than the full copyright headers used in the previous codebase. This follows the direction that projects such as the Linux kernel are going in. - The JSON configuration file for the LED control files for the demo platform has been migrated to a INI format configuration file matching what has been done for the VIS client configuration in other recent work. Bug-AGL: SPEC-4409 Signed-off-by: Scott Murray <scott.murray@konsulko.com> Change-Id: Ic2061bca9670b1e461d6f1e6591471e257fff5b9
Diffstat (limited to 'src')
-rw-r--r--src/hvac-can-helper.cpp176
-rw-r--r--src/hvac-can-helper.hpp53
-rw-r--r--src/hvac-led-helper.cpp194
-rw-r--r--src/hvac-led-helper.hpp34
-rw-r--r--src/hvac-service.cpp85
-rw-r--r--src/hvac-service.hpp33
-rw-r--r--src/main.cpp34
-rw-r--r--src/meson.build19
-rw-r--r--src/vis-config.cpp157
-rw-r--r--src/vis-config.hpp43
-rw-r--r--src/vis-session.cpp374
-rw-r--r--src/vis-session.hpp78
12 files changed, 1280 insertions, 0 deletions
diff --git a/src/hvac-can-helper.cpp b/src/hvac-can-helper.cpp
new file mode 100644
index 0000000..634f4b0
--- /dev/null
+++ b/src/hvac-can-helper.cpp
@@ -0,0 +1,176 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "hvac-can-helper.hpp"
+#include <iostream>
+#include <iomanip>
+#include <sstream>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <net/if.h>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/ini_parser.hpp>
+#include <boost/property_tree/ini_parser.hpp>
+
+namespace property_tree = boost::property_tree;
+
+HvacCanHelper::HvacCanHelper() :
+ m_temp_left(21),
+ m_temp_right(21),
+ m_fan_speed(0),
+ m_port("can0"),
+ m_config_valid(false),
+ m_active(false),
+ m_verbose(0)
+{
+ read_config();
+
+ can_open();
+}
+
+HvacCanHelper::~HvacCanHelper()
+{
+ can_close();
+}
+
+void HvacCanHelper::read_config()
+{
+ // Using a separate configuration file now, it may make sense
+ // to revisit this if a workable scheme to handle overriding
+ // values for the full demo setup can be come up with.
+ std::string config("/etc/xdg/AGL/agl-service-hvac-can.conf");
+ char *home = getenv("XDG_CONFIG_HOME");
+ if (home) {
+ config = home;
+ config += "/AGL/agl-service-hvac.conf";
+ }
+
+ std::cout << "Using configuration " << config << std::endl;
+ property_tree::ptree pt;
+ try {
+ property_tree::ini_parser::read_ini(config, pt);
+ }
+ catch (std::exception &ex) {
+ // Continue with defaults if file missing/broken
+ std::cerr << "Could not read " << config << std::endl;
+ m_config_valid = true;
+ return;
+ }
+ const property_tree::ptree &settings =
+ pt.get_child("can", property_tree::ptree());
+
+ m_port = settings.get("port", "can0");
+ std::stringstream ss;
+ ss << m_port;
+ ss >> std::quoted(m_port);
+ if (m_port.empty()) {
+ std::cerr << "Invalid CAN port path" << std::endl;
+ return;
+ }
+
+ m_verbose = 0;
+ std::string verbose = settings.get("verbose", "");
+ std::stringstream().swap(ss);
+ ss << verbose;
+ ss >> std::quoted(verbose);
+ if (!verbose.empty()) {
+ if (verbose == "true" || verbose == "1")
+ m_verbose = 1;
+ if (verbose == "2")
+ m_verbose = 2;
+ }
+
+ m_config_valid = true;
+}
+
+void HvacCanHelper::can_open()
+{
+ if (!m_config_valid)
+ return;
+
+ if (m_verbose > 1)
+ std::cout << "HvacCanHelper::HvacCanHelper: using port " << m_port << std::endl;
+
+ // Open raw CAN socket
+ m_can_socket = socket(PF_CAN, SOCK_RAW, CAN_RAW);
+ if (m_can_socket < 0) {
+ return;
+ }
+
+ // Look up port address
+ struct ifreq ifr;
+ strcpy(ifr.ifr_name, m_port.c_str());
+ if (ioctl(m_can_socket, SIOCGIFINDEX, &ifr) < 0) {
+ close(m_can_socket);
+ return;
+ }
+
+ m_can_addr.can_family = AF_CAN;
+ m_can_addr.can_ifindex = ifr.ifr_ifindex;
+ if (bind(m_can_socket, (struct sockaddr*) &m_can_addr, sizeof(m_can_addr)) < 0) {
+ close(m_can_socket);
+ return;
+ }
+
+ m_active = true;
+ if (m_verbose > 1)
+ std::cout << "HvacCanHelper::HvacCanHelper: opened " << m_port << std::endl;
+}
+
+void HvacCanHelper::can_close()
+{
+ if (m_active)
+ close(m_can_socket);
+}
+
+void HvacCanHelper::set_left_temperature(uint8_t temp)
+{
+ m_temp_left = temp;
+ can_update();
+}
+
+void HvacCanHelper::set_right_temperature(uint8_t temp)
+{
+ m_temp_right = temp;
+ can_update();
+}
+
+void HvacCanHelper::set_fan_speed(uint8_t speed)
+{
+ // Scale incoming 0-100 VSS signal to 0-255 to match hardware expectations
+ double value = speed * 255.0 / 100.0;
+ m_fan_speed = (uint8_t) (value + 0.5);
+ can_update();
+}
+
+void HvacCanHelper::can_update()
+{
+ if (!m_active)
+ return;
+
+ struct can_frame frame;
+ frame.can_id = 0x30;
+ frame.can_dlc = 8;
+ frame.data[0] = convert_temp(m_temp_left);
+ frame.data[1] = convert_temp(m_temp_right);
+ frame.data[2] = convert_temp((uint8_t) (((int) m_temp_left + (int) m_temp_right) >> 1));
+ frame.data[3] = 0xF0;
+ frame.data[4] = m_fan_speed;
+ frame.data[5] = 1;
+ frame.data[6] = 0;
+ frame.data[7] = 0;
+
+ auto written = sendto(m_can_socket,
+ &frame,
+ sizeof(struct can_frame),
+ 0,
+ (struct sockaddr*) &m_can_addr,
+ sizeof(m_can_addr));
+ if (written < 0) {
+ std::cerr << "Write to " << m_port << " failed!" << std::endl;
+ close(m_can_socket);
+ m_active = false;
+ }
+}
+
+
diff --git a/src/hvac-can-helper.hpp b/src/hvac-can-helper.hpp
new file mode 100644
index 0000000..3005d6b
--- /dev/null
+++ b/src/hvac-can-helper.hpp
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _HVAC_CAN_HELPER_HPP
+#define _HVAC_CAN_HELPER_HPP
+
+#include <string>
+#include <linux/can.h>
+
+class HvacCanHelper
+{
+public:
+ HvacCanHelper();
+
+ ~HvacCanHelper();
+
+ void set_left_temperature(uint8_t temp);
+
+ void set_right_temperature(uint8_t temp);
+
+ void set_fan_speed(uint8_t temp);
+
+private:
+ uint8_t convert_temp(uint8_t value) {
+ int result = ((0xF0 - 0x10) / 15) * (value - 15) + 0x10;
+ if (result < 0x10)
+ result = 0x10;
+ if (result > 0xF0)
+ result = 0xF0;
+
+ return (uint8_t) result;
+ }
+
+ void read_config();
+
+ void can_open();
+
+ void can_close();
+
+ void can_update();
+
+ std::string m_port;
+ unsigned m_verbose;
+ bool m_config_valid;
+ bool m_active;
+ int m_can_socket;
+ struct sockaddr_can m_can_addr;
+
+ uint8_t m_temp_left;
+ uint8_t m_temp_right;
+ uint8_t m_fan_speed;
+};
+
+#endif // _HVAC_CAN_HELPER_HPP
diff --git a/src/hvac-led-helper.cpp b/src/hvac-led-helper.cpp
new file mode 100644
index 0000000..3c86c81
--- /dev/null
+++ b/src/hvac-led-helper.cpp
@@ -0,0 +1,194 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "hvac-led-helper.hpp"
+#include <iostream>
+#include <iomanip>
+#include <sstream>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/ini_parser.hpp>
+
+namespace property_tree = boost::property_tree;
+
+#define RED "/sys/class/leds/blinkm-3-9-red/brightness"
+#define GREEN "/sys/class/leds/blinkm-3-9-green/brightness"
+#define BLUE "/sys/class/leds/blinkm-3-9-blue/brightness"
+
+// RGB temperature mapping
+static struct {
+ const int temperature;
+ const int rgb[3];
+} degree_colours[] = {
+ {15, {0, 0, 229} },
+ {16, {22, 0, 204} },
+ {17, {34, 0, 189} },
+ {18, {46, 0, 175} },
+ {19, {58, 0, 186} },
+ {20, {70, 0, 146} },
+ {21, {82, 0, 131} },
+ {22, {104, 0, 116} },
+ {23, {116, 0, 102} },
+ {24, {128, 0, 87} },
+ {25, {140, 0, 73} },
+ {26, {152, 0, 58} },
+ {27, {164, 0, 43} },
+ {28, {176, 0, 29} },
+ {29, {188, 0, 14} },
+ {30, {201, 0, 5} }
+};
+
+
+HvacLedHelper::HvacLedHelper() :
+ m_temp_left(21),
+ m_temp_right(21),
+ m_config_valid(false),
+ m_verbose(0)
+{
+ read_config();
+}
+
+void HvacLedHelper::read_config()
+{
+ // Using a separate configuration file now, it may make sense
+ // to revisit this if a workable scheme to handle overriding
+ // values for the full demo setup can be come up with.
+ std::string config("/etc/xdg/AGL/agl-service-hvac-leds.conf");
+ char *home = getenv("XDG_CONFIG_HOME");
+ if (home) {
+ config = home;
+ config += "/AGL/agl-service-hvac.conf";
+ }
+
+ std::cout << "Using configuration " << config << std::endl;
+ property_tree::ptree pt;
+ try {
+ property_tree::ini_parser::read_ini(config, pt);
+ }
+ catch (std::exception &ex) {
+ // Continue with defaults if file missing/broken
+ std::cerr << "Could not read " << config << std::endl;
+ m_config_valid = true;
+ return;
+ }
+ const property_tree::ptree &settings =
+ pt.get_child("leds", property_tree::ptree());
+
+ m_led_path_red = settings.get("red", RED);
+ std::stringstream ss;
+ ss << m_led_path_red;
+ ss >> std::quoted(m_led_path_red);
+ if (m_led_path_red.empty()) {
+ std::cerr << "Invalid red LED path" << std::endl;
+ return;
+ }
+ // stat file here?
+ std::cout << "Using red LED path " << m_led_path_red << std::endl;
+
+ m_led_path_green = settings.get("green", GREEN);
+ std::stringstream().swap(ss);
+ ss << m_led_path_green;
+ ss >> std::quoted(m_led_path_green);
+ if (m_led_path_green.empty()) {
+ std::cerr << "Invalid green LED path" << std::endl;
+ return;
+ }
+ // stat file here?
+ std::cout << "Using green LED path " << m_led_path_red << std::endl;
+
+ m_led_path_blue = settings.get("blue", BLUE);
+ std::stringstream().swap(ss);
+ ss << m_led_path_blue;
+ ss >> std::quoted(m_led_path_blue);
+ if (m_led_path_blue.empty()) {
+ std::cerr << "Invalid blue LED path" << std::endl;
+ return;
+ }
+ // stat file here?
+ std::cout << "Using blue LED path " << m_led_path_red << std::endl;
+
+ m_verbose = 0;
+ std::string verbose = settings.get("verbose", "");
+ std::stringstream().swap(ss);
+ ss << verbose;
+ ss >> std::quoted(verbose);
+ if (!verbose.empty()) {
+ if (verbose == "true" || verbose == "1")
+ m_verbose = 1;
+ if (verbose == "2")
+ m_verbose = 2;
+ }
+
+ m_config_valid = true;
+}
+
+void HvacLedHelper::set_left_temperature(uint8_t temp)
+{
+ m_temp_left = temp;
+ led_update();
+}
+
+void HvacLedHelper::set_right_temperature(uint8_t temp)
+{
+ m_temp_right = temp;
+ led_update();
+}
+
+void HvacLedHelper::led_update()
+{
+ if (!m_config_valid)
+ return;
+
+ // Calculates average colour value taken from the temperature toggles,
+ // limiting to our 15 degree range
+ int temp_left = m_temp_left - 15;
+ if (temp_left < 0)
+ temp_left = 0;
+ else if (temp_left > 15)
+ temp_left = 15;
+
+ int temp_right = m_temp_right - 15;
+ if (temp_right < 0)
+ temp_right = 0;
+ else if (temp_right > 15)
+ temp_right = 15;
+
+ int red_value = (degree_colours[temp_left].rgb[0] + degree_colours[temp_right].rgb[0]) / 2;
+ int green_value = (degree_colours[temp_left].rgb[1] + degree_colours[temp_right].rgb[1]) / 2;
+ int blue_value = (degree_colours[temp_left].rgb[2] + degree_colours[temp_right].rgb[2]) / 2;
+
+ //
+ // Push colour mapping out
+ //
+
+ std::ofstream led_red;
+ led_red.open(m_led_path_red);
+ if (led_red.is_open()) {
+ led_red << std::to_string(red_value);
+ led_red.close();
+ } else {
+ std::cerr << "Could not write red LED path " << m_led_path_red << std::endl;
+ m_config_valid = false;
+ return;
+ }
+
+ std::ofstream led_green;
+ led_green.open(m_led_path_green);
+ if (led_green.is_open()) {
+ led_green << std::to_string(green_value);
+ led_green.close();
+ } else {
+ std::cerr << "Could not write green LED path " << m_led_path_green << std::endl;
+ m_config_valid = false;
+ return;
+ }
+
+ std::ofstream led_blue;
+ led_blue.open(m_led_path_blue);
+ if (led_blue.is_open()) {
+ led_blue << std::to_string(blue_value);
+ led_blue.close();
+ } else {
+ std::cerr << "Could not write blue LED path " << m_led_path_blue << std::endl;
+ m_config_valid = false;
+ return;
+ }
+}
diff --git a/src/hvac-led-helper.hpp b/src/hvac-led-helper.hpp
new file mode 100644
index 0000000..8fe41f7
--- /dev/null
+++ b/src/hvac-led-helper.hpp
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _HVAC_LED_HELPER_HPP
+#define _HVAC_LED_HELPER_HPP
+
+#include <string>
+#include <iostream>
+#include <fstream>
+
+class HvacLedHelper
+{
+public:
+ HvacLedHelper();
+
+ void set_left_temperature(uint8_t temp);
+
+ void set_right_temperature(uint8_t temp);
+
+private:
+ void read_config();
+
+ void led_update();
+
+ std::string m_led_path_red;
+ std::string m_led_path_green;
+ std::string m_led_path_blue;
+ unsigned m_verbose;
+ bool m_config_valid;
+
+ uint8_t m_temp_left;
+ uint8_t m_temp_right;
+};
+
+#endif // _HVAC_LED_HELPER_HPP
diff --git a/src/hvac-service.cpp b/src/hvac-service.cpp
new file mode 100644
index 0000000..ee05806
--- /dev/null
+++ b/src/hvac-service.cpp
@@ -0,0 +1,85 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "hvac-service.hpp"
+#include <iostream>
+#include <algorithm>
+
+
+HvacService::HvacService(const VisConfig &config, net::io_context& ioc, ssl::context& ctx) :
+ VisSession(config, ioc, ctx),
+ m_can_helper(),
+ m_led_helper()
+{
+}
+
+void HvacService::handle_authorized_response(void)
+{
+ subscribe("Vehicle.Cabin.HVAC.Station.Row1.Left.Temperature");
+ subscribe("Vehicle.Cabin.HVAC.Station.Row1.Left.FanSpeed");
+ subscribe("Vehicle.Cabin.HVAC.Station.Row1.Right.Temperature");
+ subscribe("Vehicle.Cabin.HVAC.Station.Row1.Right.FanSpeed");
+}
+
+void HvacService::handle_get_response(std::string &path, std::string &value, std::string &timestamp)
+{
+ // Placeholder since no gets are performed ATM
+}
+
+void HvacService::handle_notification(std::string &path, std::string &value, std::string &timestamp)
+{
+ if (path == "Vehicle.Cabin.HVAC.Station.Row1.Left.Temperature") {
+ try {
+ int temp = std::stoi(value);
+ if (temp >= 0 && temp < 256)
+ set_left_temperature(temp);
+ }
+ catch (std::exception ex) {
+ // ignore bad value
+ }
+ } else if (path == "Vehicle.Cabin.HVAC.Station.Row1.Right.Temperature") {
+ try {
+ int temp = std::stoi(value);
+ if (temp >= 0 && temp < 256)
+ set_right_temperature(temp);
+ }
+ catch (std::exception ex) {
+ // ignore bad value
+ }
+ } else if (path == "Vehicle.Cabin.HVAC.Station.Row1.Left.FanSpeed") {
+ try {
+ int speed = std::stoi(value);
+ if (speed >= 0 && speed < 256)
+ set_fan_speed(speed);
+ }
+ catch (std::exception ex) {
+ // ignore bad value
+ }
+ } else if (path == "Vehicle.Cabin.HVAC.Station.Row1.Right.FanSpeed") {
+ try {
+ int speed = std::stoi(value);
+ if (speed >= 0 && speed < 256)
+ set_fan_speed(speed);
+ }
+ catch (std::exception ex) {
+ // ignore bad value
+ }
+ }
+ // else ignore
+}
+
+void HvacService::set_left_temperature(uint8_t temp)
+{
+ m_can_helper.set_left_temperature(temp);
+ m_led_helper.set_left_temperature(temp);
+}
+
+void HvacService::set_right_temperature(uint8_t temp)
+{
+ m_can_helper.set_right_temperature(temp);
+ m_led_helper.set_right_temperature(temp);
+}
+
+void HvacService::set_fan_speed(uint8_t speed)
+{
+ m_can_helper.set_fan_speed(speed);
+}
diff --git a/src/hvac-service.hpp b/src/hvac-service.hpp
new file mode 100644
index 0000000..27bfe1a
--- /dev/null
+++ b/src/hvac-service.hpp
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _HVAC_SERVICE_HPP
+#define _HVAC_SERVICE_HPP
+
+#include "vis-session.hpp"
+#include "hvac-can-helper.hpp"
+#include "hvac-led-helper.hpp"
+
+class HvacService : public VisSession
+{
+public:
+ HvacService(const VisConfig &config, net::io_context& ioc, ssl::context& ctx);
+
+protected:
+ virtual void handle_authorized_response(void) override;
+
+ virtual void handle_get_response(std::string &path, std::string &value, std::string &timestamp) override;
+
+ virtual void handle_notification(std::string &path, std::string &value, std::string &timestamp) override;
+
+private:
+ HvacCanHelper m_can_helper;
+ HvacLedHelper m_led_helper;
+
+ void set_left_temperature(uint8_t temp);
+
+ void set_right_temperature(uint8_t temp);
+
+ void set_fan_speed(uint8_t temp);
+};
+
+#endif // _HVAC_SERVICE_HPP
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..6bb165f
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include <iostream>
+#include <iomanip>
+#include <boost/asio/signal_set.hpp>
+#include <boost/bind.hpp>
+#include "hvac-service.hpp"
+
+using work_guard_type = boost::asio::executor_work_guard<boost::asio::io_context::executor_type>;
+
+int main(int argc, char** argv)
+{
+ // The io_context is required for all I/O
+ net::io_context ioc;
+
+ // Register to stop I/O context on SIGINT and SIGTERM
+ net::signal_set signals(ioc, SIGINT, SIGTERM);
+ signals.async_wait(boost::bind(&net::io_context::stop, &ioc));
+
+ // The SSL context is required, and holds certificates
+ ssl::context ctx{ssl::context::tlsv12_client};
+
+ // Launch the asynchronous operation
+ VisConfig config("agl-service-hvac");
+ std::make_shared<HvacService>(config, ioc, ctx)->run();
+
+ // Ensure I/O context continues running even if there's no work
+ work_guard_type work_guard(ioc.get_executor());
+
+ // Run the I/O context
+ ioc.run();
+
+ return 0;
+}
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..149ec99
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,19 @@
+boost_dep = dependency('boost',
+ version : '>=1.72',
+ modules : [ 'thread', 'filesystem', 'program_options', 'log', 'system' ])
+openssl_dep = dependency('openssl')
+thread_dep = dependency('threads')
+cxx = meson.get_compiler('cpp')
+
+src = [ 'vis-config.cpp',
+ 'vis-session.cpp',
+ 'hvac-service.cpp',
+ 'hvac-can-helper.cpp',
+ 'hvac-led-helper.cpp',
+ 'main.cpp'
+]
+executable('agl-service-hvac',
+ src,
+ dependencies: [boost_dep, openssl_dep, thread_dep, systemd_dep],
+ install: true,
+ install_dir : get_option('sbindir'))
diff --git a/src/vis-config.cpp b/src/vis-config.cpp
new file mode 100644
index 0000000..b8d9266
--- /dev/null
+++ b/src/vis-config.cpp
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "vis-config.hpp"
+#include <iostream>
+#include <iomanip>
+#include <sstream>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/ini_parser.hpp>
+#include <boost/filesystem.hpp>
+
+namespace property_tree = boost::property_tree;
+namespace filesystem = boost::filesystem;
+
+#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"
+
+
+VisConfig::VisConfig(const std::string &hostname,
+ const unsigned port,
+ const std::string &clientKey,
+ const std::string &clientCert,
+ const std::string &caCert,
+ const std::string &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...
+}
+
+VisConfig::VisConfig(const std::string &appname) :
+ m_valid(false)
+{
+ std::string config("/etc/xdg/AGL/");
+ config += appname;
+ config += ".conf";
+ char *home = getenv("XDG_CONFIG_HOME");
+ if (home) {
+ config = home;
+ config += "/AGL/";
+ config += appname;
+ config += ".conf";
+ }
+
+ std::cout << "Using configuration " << config << std::endl;
+ property_tree::ptree pt;
+ try {
+ property_tree::ini_parser::read_ini(config, pt);
+ }
+ catch (std::exception &ex) {
+ std::cerr << "Could not read " << config << std::endl;
+ return;
+ }
+ const property_tree::ptree &settings =
+ pt.get_child("vis-client", property_tree::ptree());
+
+ m_hostname = settings.get("server", "localhost");
+ std::stringstream ss;
+ ss << m_hostname;
+ ss >> std::quoted(m_hostname);
+ if (m_hostname.empty()) {
+ std::cerr << "Invalid server hostname" << std::endl;
+ return;
+ }
+
+ m_port = settings.get("port", 8090);
+ if (m_port == 0) {
+ std::cerr << "Invalid server port" << std::endl;
+ 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 = settings.get("verify-server", false);
+
+ std::string keyFileName = settings.get("key", DEFAULT_CLIENT_KEY_FILE);
+ std::stringstream().swap(ss);
+ ss << keyFileName;
+ ss >> std::quoted(keyFileName);
+ ss.str("");
+ if (keyFileName.empty()) {
+ std::cerr << "Invalid client key filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(keyFileName, m_clientKey);
+ if (m_clientKey.empty()) {
+ std::cerr << "Invalid client key file" << std::endl;
+ return;
+ }
+
+ std::string certFileName = settings.get("certificate", DEFAULT_CLIENT_CERT_FILE);
+ std::stringstream().swap(ss);
+ ss << certFileName;
+ ss >> std::quoted(certFileName);
+ if (certFileName.empty()) {
+ std::cerr << "Invalid client certificate filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(certFileName, m_clientCert);
+ if (m_clientCert.empty()) {
+ std::cerr << "Invalid client certificate file" << std::endl;
+ return;
+ }
+
+ std::string caCertFileName = settings.get("ca-certificate", DEFAULT_CA_CERT_FILE);
+ std::stringstream().swap(ss);
+ ss << caCertFileName;
+ ss >> std::quoted(caCertFileName);
+ if (caCertFileName.empty()) {
+ std::cerr << "Invalid CA certificate filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(caCertFileName, m_caCert);
+ if (m_caCert.empty()) {
+ std::cerr << "Invalid CA certificate file" << std::endl;
+ return;
+ }
+
+ std::string authTokenFileName = settings.get("authorization", "");
+ std::stringstream().swap(ss);
+ ss << authTokenFileName;
+ ss >> std::quoted(authTokenFileName);
+ if (authTokenFileName.empty()) {
+ std::cerr << "Invalid authorization token filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(authTokenFileName, m_authToken);
+ if (m_authToken.empty()) {
+ std::cerr << "Invalid authorization token file" << std::endl;
+ return;
+ }
+
+ m_verbose = 0;
+ std::string verbose = settings.get("verbose", "");
+ std::stringstream().swap(ss);
+ ss << verbose;
+ ss >> std::quoted(verbose);
+ if (!verbose.empty()) {
+ if (verbose == "true" || verbose == "1")
+ m_verbose = 1;
+ if (verbose == "2")
+ m_verbose = 2;
+ }
+
+ m_valid = true;
+}
diff --git a/src/vis-config.hpp b/src/vis-config.hpp
new file mode 100644
index 0000000..b0f72f9
--- /dev/null
+++ b/src/vis-config.hpp
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _VIS_CONFIG_HPP
+#define _VIS_CONFIG_HPP
+
+#include <string>
+
+class VisConfig
+{
+public:
+ explicit VisConfig(const std::string &hostname,
+ const unsigned port,
+ const std::string &clientKey,
+ const std::string &clientCert,
+ const std::string &caCert,
+ const std::string &authToken,
+ bool verifyPeer = true);
+ explicit VisConfig(const std::string &appname);
+ ~VisConfig() {};
+
+ std::string hostname() { return m_hostname; };
+ unsigned port() { return m_port; };
+ std::string clientKey() { return m_clientKey; };
+ std::string clientCert() { return m_clientCert; };
+ std::string caCert() { return m_caCert; };
+ std::string authToken() { return m_authToken; };
+ bool verifyPeer() { return m_verifyPeer; };
+ bool valid() { return m_valid; };
+ unsigned verbose() { return m_verbose; };
+
+private:
+ std::string m_hostname;
+ unsigned m_port;
+ std::string m_clientKey;
+ std::string m_clientCert;
+ std::string m_caCert;
+ std::string m_authToken;
+ bool m_verifyPeer;
+ unsigned m_verbose;
+ bool m_valid;
+};
+
+#endif // _VIS_CONFIG_HPP
diff --git a/src/vis-session.cpp b/src/vis-session.cpp
new file mode 100644
index 0000000..880e3ae
--- /dev/null
+++ b/src/vis-session.cpp
@@ -0,0 +1,374 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "vis-session.hpp"
+#include <iostream>
+#include <sstream>
+#include <thread>
+
+
+// Logging helper
+static void log_error(beast::error_code error, char const* what)
+{
+ std::cerr << what << " error: " << error.message() << std::endl;
+}
+
+
+// Resolver and socket require an io_context
+VisSession::VisSession(const VisConfig &config, net::io_context& ioc, ssl::context& ctx) :
+ m_config(config),
+ m_resolver(net::make_strand(ioc)),
+ m_ws(net::make_strand(ioc), ctx)
+{
+}
+
+// Start the asynchronous operation
+void VisSession::run()
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ // Start by resolving hostname
+ m_resolver.async_resolve(m_config.hostname(),
+ std::to_string(m_config.port()),
+ beast::bind_front_handler(&VisSession::on_resolve,
+ shared_from_this()));
+}
+
+void VisSession::on_resolve(beast::error_code error,
+ tcp::resolver::results_type results)
+{
+ if(error) {
+ log_error(error, "resolve");
+ return;
+ }
+
+ // Set a timeout on the connect operation
+ beast::get_lowest_layer(m_ws).expires_after(std::chrono::seconds(30));
+
+ // Connect to resolved address
+ if (m_config.verbose())
+ std::cout << "Connecting" << std::endl;
+ m_results = results;
+ connect();
+}
+
+void VisSession::connect()
+{
+ beast::get_lowest_layer(m_ws).async_connect(m_results,
+ beast::bind_front_handler(&VisSession::on_connect,
+ shared_from_this()));
+}
+
+void VisSession::on_connect(beast::error_code error,
+ tcp::resolver::results_type::endpoint_type endpoint)
+{
+ if(error) {
+ // The server can take a while to be ready to accept connections,
+ // so keep retrying until we hit the timeout.
+ if (error == net::error::timed_out) {
+ log_error(error, "connect");
+ return;
+ }
+
+ // Delay 500 ms before retrying
+ std::this_thread::sleep_for(std::chrono::milliseconds(500));
+
+ if (m_config.verbose())
+ std::cout << "Connecting" << std::endl;
+
+ connect();
+ return;
+ }
+
+ if (m_config.verbose())
+ std::cout << "Connected" << std::endl;
+
+ // Set handshake timeout
+ beast::get_lowest_layer(m_ws).expires_after(std::chrono::seconds(30));
+
+ // Set SNI Hostname (many hosts need this to handshake successfully)
+ if(!SSL_set_tlsext_host_name(m_ws.next_layer().native_handle(),
+ m_config.hostname().c_str()))
+ {
+ error = beast::error_code(static_cast<int>(::ERR_get_error()),
+ net::error::get_ssl_category());
+ log_error(error, "connect");
+ return;
+ }
+
+ // Update the hostname. This will provide the value of the
+ // Host HTTP header during the WebSocket handshake.
+ // See https://tools.ietf.org/html/rfc7230#section-5.4
+ m_hostname = m_config.hostname() + ':' + std::to_string(endpoint.port());
+
+ if (m_config.verbose())
+ std::cout << "Negotiating SSL handshake" << std::endl;
+
+ // Perform the SSL handshake
+ m_ws.next_layer().async_handshake(ssl::stream_base::client,
+ beast::bind_front_handler(&VisSession::on_ssl_handshake,
+ shared_from_this()));
+}
+
+void VisSession::on_ssl_handshake(beast::error_code error)
+{
+ if(error) {
+ log_error(error, "SSL handshake");
+ return;
+ }
+
+ // Turn off the timeout on the tcp_stream, because
+ // the websocket stream has its own timeout system.
+ beast::get_lowest_layer(m_ws).expires_never();
+
+ // NOTE: Explicitly not setting websocket stream timeout here,
+ // as the client is long-running.
+
+ if (m_config.verbose())
+ std::cout << "Negotiating WSS handshake" << std::endl;
+
+ // Perform handshake
+ m_ws.async_handshake(m_hostname,
+ "/",
+ beast::bind_front_handler(&VisSession::on_handshake,
+ shared_from_this()));
+}
+
+void VisSession::on_handshake(beast::error_code error)
+{
+ if(error) {
+ log_error(error, "WSS handshake");
+ return;
+ }
+
+ if (m_config.verbose())
+ std::cout << "Authorizing" << std::endl;
+
+ // Authorize
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"]= "authorize";
+ req["tokens"] = m_config.authToken();
+
+ m_ws.async_write(net::buffer(req.dump(4)),
+ beast::bind_front_handler(&VisSession::on_authorize,
+ shared_from_this()));
+}
+
+void VisSession::on_authorize(beast::error_code error, std::size_t bytes_transferred)
+{
+ boost::ignore_unused(bytes_transferred);
+
+ if(error) {
+ log_error(error, "authorize");
+ return;
+ }
+
+ // Read response
+ m_ws.async_read(m_buffer,
+ beast::bind_front_handler(&VisSession::on_read,
+ shared_from_this()));
+}
+
+// NOTE: Placeholder for now
+void VisSession::on_write(beast::error_code error, std::size_t bytes_transferred)
+{
+ boost::ignore_unused(bytes_transferred);
+
+ if(error) {
+ log_error(error, "write");
+ return;
+ }
+
+ // Do nothing...
+}
+
+void VisSession::on_read(beast::error_code error, std::size_t bytes_transferred)
+{
+ boost::ignore_unused(bytes_transferred);
+
+ if(error) {
+ log_error(error, "read");
+ return;
+ }
+
+ // Handle message
+ std::string s = beast::buffers_to_string(m_buffer.data());
+ json response = json::parse(s, nullptr, false);
+ if (!response.is_discarded()) {
+ handle_message(response);
+ } else {
+ std::cerr << "json::parse failed? got " << s << std::endl;
+ }
+ m_buffer.consume(m_buffer.size());
+
+ // Read next message
+ m_ws.async_read(m_buffer,
+ beast::bind_front_handler(&VisSession::on_read,
+ shared_from_this()));
+}
+
+void VisSession::get(const std::string &path)
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"] = "get";
+ req["path"] = path;
+ req["tokens"] = m_config.authToken();
+
+ m_ws.write(net::buffer(req.dump(4)));
+}
+
+void VisSession::set(const std::string &path, const std::string &value)
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"] = "set";
+ req["path"] = path;
+ req["value"] = value;
+ req["tokens"] = m_config.authToken();
+
+ m_ws.write(net::buffer(req.dump(4)));
+}
+
+void VisSession::subscribe(const std::string &path)
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"] = "subscribe";
+ req["path"] = path;
+ req["tokens"] = m_config.authToken();
+
+ m_ws.write(net::buffer(req.dump(4)));
+}
+
+bool VisSession::parseData(const json &message, std::string &path, std::string &value, std::string &timestamp)
+{
+ if (message.contains("error")) {
+ std::string error = message["error"];
+ return false;
+ }
+
+ if (!(message.contains("data") && message["data"].is_object())) {
+ std::cerr << "Malformed message (data missing)" << std::endl;
+ return false;
+ }
+ auto data = message["data"];
+ if (!(data.contains("path") && data["path"].is_string())) {
+ std::cerr << "Malformed message (path missing)" << std::endl;
+ return false;
+ }
+ path = data["path"];
+ // Convert '/' to '.' in paths to ensure consistency for clients
+ std::replace(path.begin(), path.end(), '/', '.');
+
+ if (!(data.contains("dp") && data["dp"].is_object())) {
+ std::cerr << "Malformed message (datapoint missing)" << std::endl;
+ return false;
+ }
+ auto dp = data["dp"];
+ if (!dp.contains("value")) {
+ std::cerr << "Malformed message (value missing)" << std::endl;
+ return false;
+ } else if (dp["value"].is_string()) {
+ value = dp["value"];
+ } else if (dp["value"].is_number_float()) {
+ double num = dp["value"];
+ value = std::to_string(num);
+ } else if (dp["value"].is_boolean()) {
+ value = dp["value"] ? "true" : "false";
+ } else {
+ std::cerr << "Malformed message (unsupported value type)" << std::endl;
+ return false;
+ }
+
+ if (!(dp.contains("ts") && dp["ts"].is_string())) {
+ std::cerr << "Malformed message (timestamp missing)" << std::endl;
+ return false;
+ }
+ timestamp = dp["ts"];
+
+ return true;
+}
+
+void VisSession::handle_message(const json &message)
+{
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: enter, message = " << to_string(message) << std::endl;
+
+ if (!message.contains("action")) {
+ std::cerr << "Received unknown message (no action), discarding" << std::endl;
+ return;
+ }
+
+ std::string action = message["action"];
+ if (action == "authorize") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS authorization failed: " << error << std::endl;
+ } else {
+ if (m_config.verbose() > 1)
+ std::cout << "authorized" << std::endl;
+
+ handle_authorized_response();
+ }
+ } else if (action == "subscribe") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS subscription failed: " << error << std::endl;
+ }
+ } else if (action == "get") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS get failed: " << error << std::endl;
+ } else {
+ std::string path, value, ts;
+ if (parseData(message, path, value, ts)) {
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: got response " << path << " = " << value << std::endl;
+
+ handle_get_response(path, value, ts);
+ }
+ }
+ } else if (action == "set") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS set failed: " << error;
+ }
+ } else if (action == "subscription") {
+ std::string path, value, ts;
+ if (parseData(message, path, value, ts)) {
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: got notification " << path << " = " << value << std::endl;
+
+ handle_notification(path, value, ts);
+ }
+ } else {
+ std::cerr << "unhandled VIS response of type: " << action;
+ }
+
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: exit" << std::endl;
+}
+
diff --git a/src/vis-session.hpp b/src/vis-session.hpp
new file mode 100644
index 0000000..8c7b0d9
--- /dev/null
+++ b/src/vis-session.hpp
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _VIS_SESSION_HPP
+#define _VIS_SESSION_HPP
+
+#include "vis-config.hpp"
+#include <atomic>
+#include <string>
+#include <boost/beast/core.hpp>
+#include <boost/beast/ssl.hpp>
+#include <boost/beast/websocket.hpp>
+#include <boost/beast/websocket/ssl.hpp>
+#include <boost/asio/strand.hpp>
+#include <nlohmann/json.hpp>
+
+namespace beast = boost::beast;
+namespace websocket = beast::websocket;
+namespace net = boost::asio;
+namespace ssl = boost::asio::ssl;
+using tcp = boost::asio::ip::tcp;
+using json = nlohmann::json;
+
+
+class VisSession : public std::enable_shared_from_this<VisSession>
+{
+ //net::io_context m_ioc;
+ tcp::resolver m_resolver;
+ tcp::resolver::results_type m_results;
+ std::string m_hostname;
+ websocket::stream<beast::ssl_stream<beast::tcp_stream>> m_ws;
+ beast::flat_buffer m_buffer;
+
+public:
+ // Resolver and socket require an io_context
+ explicit VisSession(const VisConfig &config, net::io_context& ioc, ssl::context& ctx);
+
+ // Start the asynchronous operation
+ void run();
+
+protected:
+ VisConfig m_config;
+ std::atomic_uint m_requestid;
+
+ void on_resolve(beast::error_code error, tcp::resolver::results_type results);
+
+ void connect();
+
+ void on_connect(beast::error_code error, tcp::resolver::results_type::endpoint_type endpoint);
+
+ void on_ssl_handshake(beast::error_code error);
+
+ void on_handshake(beast::error_code error);
+
+ void on_authorize(beast::error_code error, std::size_t bytes_transferred);
+
+ void on_write(beast::error_code error, std::size_t bytes_transferred);
+
+ void on_read(beast::error_code error, std::size_t bytes_transferred);
+
+ void get(const std::string &path);
+
+ void set(const std::string &path, const std::string &value);
+
+ void subscribe(const std::string &path);
+
+ void handle_message(const json &message);
+
+ bool parseData(const json &message, std::string &path, std::string &value, std::string &timestamp);
+
+ virtual void handle_authorized_response(void) = 0;
+
+ virtual void handle_get_response(std::string &path, std::string &value, std::string &timestamp) = 0;
+
+ virtual void handle_notification(std::string &path, std::string &value, std::string &timestamp) = 0;
+
+};
+
+#endif // _VIS_SESSION_HPP