diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/hvac-can-helper.cpp | 176 | ||||
-rw-r--r-- | src/hvac-can-helper.hpp | 53 | ||||
-rw-r--r-- | src/hvac-led-helper.cpp | 194 | ||||
-rw-r--r-- | src/hvac-led-helper.hpp | 34 | ||||
-rw-r--r-- | src/hvac-service.cpp | 85 | ||||
-rw-r--r-- | src/hvac-service.hpp | 33 | ||||
-rw-r--r-- | src/main.cpp | 34 | ||||
-rw-r--r-- | src/meson.build | 19 | ||||
-rw-r--r-- | src/vis-config.cpp | 157 | ||||
-rw-r--r-- | src/vis-config.hpp | 43 | ||||
-rw-r--r-- | src/vis-session.cpp | 374 | ||||
-rw-r--r-- | src/vis-session.hpp | 78 |
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 ×tamp) +{ + // Placeholder since no gets are performed ATM +} + +void HvacService::handle_notification(std::string &path, std::string &value, std::string ×tamp) +{ + 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 ×tamp) override; + + virtual void handle_notification(std::string &path, std::string &value, std::string ×tamp) 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 ×tamp) +{ + 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 ×tamp); + + virtual void handle_authorized_response(void) = 0; + + virtual void handle_get_response(std::string &path, std::string &value, std::string ×tamp) = 0; + + virtual void handle_notification(std::string &path, std::string &value, std::string ×tamp) = 0; + +}; + +#endif // _VIS_SESSION_HPP |