summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
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