diff options
-rw-r--r-- | CMakeLists.txt | 21 | ||||
-rwxr-xr-x | autobuild/agl/autobuild | 128 | ||||
-rwxr-xr-x | autobuild/linux/autobuild | 128 | ||||
-rw-r--r-- | binding/CMakeLists.txt | 35 | ||||
-rw-r--r-- | binding/hvac-demo-binding.c | 674 | ||||
-rw-r--r-- | conf.d/cmake/config.cmake | 152 | ||||
-rw-r--r-- | conf.d/wgt/config.xml.in | 29 | ||||
-rw-r--r-- | hvac.json | 8 | ||||
-rw-r--r-- | meson.build | 10 | ||||
-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 | ||||
-rw-r--r-- | systemd/agl-service-hvac.service | 11 | ||||
-rw-r--r-- | systemd/meson.build | 3 |
23 files changed, 1304 insertions, 1175 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index b485097..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -########################################################################### -# Copyright 2015, 2016, 2017 IoT.bzh -# -# author: Romain Forlot <romain.forlot@iot.bzh> -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -########################################################################### - -CMAKE_MINIMUM_REQUIRED(VERSION 3.3) - -include(${CMAKE_CURRENT_SOURCE_DIR}/conf.d/cmake/config.cmake) diff --git a/autobuild/agl/autobuild b/autobuild/agl/autobuild deleted file mode 100755 index 16181b8..0000000 --- a/autobuild/agl/autobuild +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/make -f -# Copyright (C) 2015 - 2018 "IoT.bzh" -# Copyright (C) 2020 Konsulko Group -# Author "Romain Forlot" <romain.forlot@iot.bzh> -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -THISFILE := $(lastword $(MAKEFILE_LIST)) -ROOT_DIR := $(abspath $(dir $(THISFILE))/../..) - -# Build directories -# Note that the debug/test/coverage directories are defined in relation -# to the release directory (BUILD_DIR), this needs to be kept in mind -# if over-riding it and building those widget types, the specific widget -# type variable (e.g. BUILD_DIR_DEBUG) may also need to be specified -# to yield the desired output hierarchy. -BUILD_DIR = $(ROOT_DIR)/build -BUILD_DIR_DEBUG = $(abspath $(BUILD_DIR)/../build-debug) -BUILD_DIR_TEST = $(abspath $(BUILD_DIR)/../build-test) -BUILD_DIR_COVERAGE = $(abspath $(BUILD_DIR)/../build-coverage) - -# Output directory variable for use in pattern rules. -# This is intended for internal use only, hence the explicit override -# definition. -override OUTPUT_DIR = $(BUILD_DIR) - -# Final install directory for widgets -DEST = $(OUTPUT_DIR) - -# Default build type for release/test builds -BUILD_TYPE = RELEASE - -.PHONY: all help update install distclean -.PHONY: clean clean-release clean-debug clean-test clean-coverage clean-all -.PHONY: configure configure-release configure-debug configure-test configure-coverage -.PHONY: build build-release build-debug build-test build-coverage build-all -.PHONY: package package-release package-debug package-test package-coverage package-all - -help: - @echo "List of targets available:" - @echo "" - @echo "- all" - @echo "- help" - @echo "- clean" - @echo "- distclean" - @echo "- configure" - @echo "- build: compilation, link and prepare files for package into a widget" - @echo "- package: output a widget file '*.wgt'" - @echo "- install: install in your $(CMAKE_INSTALL_DIR) directory" - @echo "" - @echo "Usage: ./autobuild/agl/autobuild package DEST=${HOME}/opt" - @echo "Don't use your build dir as DEST as wgt file is generated at this location" - -all: package-all - -# Target specific variable over-rides so static pattern rules can be -# used for the various type-specific targets. - -configure-test build-test package-test clean-test: OUTPUT_DIR = $(BUILD_DIR_TEST) - -configure-coverage build-coverage package-coverage clean-coverage: OUTPUT_DIR = $(BUILD_DIR_COVERAGE) -configure-coverage build-coverage package-coverage: BUILD_TYPE = COVERAGE - -configure-debug build-debug package-debug clean-debug: OUTPUT_DIR = $(BUILD_DIR_DEBUG) -configure-debug build-debug package-debug: BUILD_TYPE = DEBUG - -clean-release clean-test clean-debug clean-coverage: - @if [ -d $(OUTPUT_DIR) ]; then \ - $(MAKE) -C $(OUTPUT_DIR) $(CLEAN_ARGS) clean; \ - else \ - echo Nothing to clean; \ - fi - -clean: clean-release - -clean-all: clean-release clean-test clean-debug clean-coverage - -distclean: clean-all - -configure-release configure-test configure-debug configure-coverage: - @mkdir -p $(OUTPUT_DIR) - @if [ ! -f $(OUTPUT_DIR)/Makefile ]; then \ - (cd $(OUTPUT_DIR) && cmake -S $(ROOT_DIR) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) $(CONFIGURE_ARGS)); \ - fi - -configure: configure-release - -build-release build-debug build-coverage: build-%: configure-% - @cmake --build $(OUTPUT_DIR) $(BUILD_ARGS) --target all - -# Kept for consistency, empty to avoid building everything for test widget -build-test: configure-test - -build: build-release - -build-all: build-release build-debug build-test build-coverage - -package-release package-debug package-coverage: package-%: build-% - @cmake --build $(OUTPUT_DIR) $(PACKAGE_ARGS) --target widget - @if [ "$(abspath $(DEST))" != "$(abspath $(OUTPUT_DIR))" ]; then \ - mkdir -p $(DEST) && cp $(OUTPUT_DIR)/*.wgt $(DEST); \ - fi - -package-test: build-test - @cmake --build $(OUTPUT_DIR) $(PACKAGE_ARGS) --target test_widget - @if [ "$(abspath $(DEST))" != "$(abspath $(OUTPUT_DIR))" ]; then \ - mkdir -p $(DEST) && cp $(OUTPUT_DIR)/*.wgt $(DEST); \ - fi - -package: package-release - -package-all: package-release package-test package-coverage package-debug - -update: configure - @cmake --build $(BUILD_DIR) --target autobuild - -install: build - @cmake --build $(BUILD_DIR) $(INSTALL_ARGS) --target install diff --git a/autobuild/linux/autobuild b/autobuild/linux/autobuild deleted file mode 100755 index 16181b8..0000000 --- a/autobuild/linux/autobuild +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/make -f -# Copyright (C) 2015 - 2018 "IoT.bzh" -# Copyright (C) 2020 Konsulko Group -# Author "Romain Forlot" <romain.forlot@iot.bzh> -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -THISFILE := $(lastword $(MAKEFILE_LIST)) -ROOT_DIR := $(abspath $(dir $(THISFILE))/../..) - -# Build directories -# Note that the debug/test/coverage directories are defined in relation -# to the release directory (BUILD_DIR), this needs to be kept in mind -# if over-riding it and building those widget types, the specific widget -# type variable (e.g. BUILD_DIR_DEBUG) may also need to be specified -# to yield the desired output hierarchy. -BUILD_DIR = $(ROOT_DIR)/build -BUILD_DIR_DEBUG = $(abspath $(BUILD_DIR)/../build-debug) -BUILD_DIR_TEST = $(abspath $(BUILD_DIR)/../build-test) -BUILD_DIR_COVERAGE = $(abspath $(BUILD_DIR)/../build-coverage) - -# Output directory variable for use in pattern rules. -# This is intended for internal use only, hence the explicit override -# definition. -override OUTPUT_DIR = $(BUILD_DIR) - -# Final install directory for widgets -DEST = $(OUTPUT_DIR) - -# Default build type for release/test builds -BUILD_TYPE = RELEASE - -.PHONY: all help update install distclean -.PHONY: clean clean-release clean-debug clean-test clean-coverage clean-all -.PHONY: configure configure-release configure-debug configure-test configure-coverage -.PHONY: build build-release build-debug build-test build-coverage build-all -.PHONY: package package-release package-debug package-test package-coverage package-all - -help: - @echo "List of targets available:" - @echo "" - @echo "- all" - @echo "- help" - @echo "- clean" - @echo "- distclean" - @echo "- configure" - @echo "- build: compilation, link and prepare files for package into a widget" - @echo "- package: output a widget file '*.wgt'" - @echo "- install: install in your $(CMAKE_INSTALL_DIR) directory" - @echo "" - @echo "Usage: ./autobuild/agl/autobuild package DEST=${HOME}/opt" - @echo "Don't use your build dir as DEST as wgt file is generated at this location" - -all: package-all - -# Target specific variable over-rides so static pattern rules can be -# used for the various type-specific targets. - -configure-test build-test package-test clean-test: OUTPUT_DIR = $(BUILD_DIR_TEST) - -configure-coverage build-coverage package-coverage clean-coverage: OUTPUT_DIR = $(BUILD_DIR_COVERAGE) -configure-coverage build-coverage package-coverage: BUILD_TYPE = COVERAGE - -configure-debug build-debug package-debug clean-debug: OUTPUT_DIR = $(BUILD_DIR_DEBUG) -configure-debug build-debug package-debug: BUILD_TYPE = DEBUG - -clean-release clean-test clean-debug clean-coverage: - @if [ -d $(OUTPUT_DIR) ]; then \ - $(MAKE) -C $(OUTPUT_DIR) $(CLEAN_ARGS) clean; \ - else \ - echo Nothing to clean; \ - fi - -clean: clean-release - -clean-all: clean-release clean-test clean-debug clean-coverage - -distclean: clean-all - -configure-release configure-test configure-debug configure-coverage: - @mkdir -p $(OUTPUT_DIR) - @if [ ! -f $(OUTPUT_DIR)/Makefile ]; then \ - (cd $(OUTPUT_DIR) && cmake -S $(ROOT_DIR) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE) $(CONFIGURE_ARGS)); \ - fi - -configure: configure-release - -build-release build-debug build-coverage: build-%: configure-% - @cmake --build $(OUTPUT_DIR) $(BUILD_ARGS) --target all - -# Kept for consistency, empty to avoid building everything for test widget -build-test: configure-test - -build: build-release - -build-all: build-release build-debug build-test build-coverage - -package-release package-debug package-coverage: package-%: build-% - @cmake --build $(OUTPUT_DIR) $(PACKAGE_ARGS) --target widget - @if [ "$(abspath $(DEST))" != "$(abspath $(OUTPUT_DIR))" ]; then \ - mkdir -p $(DEST) && cp $(OUTPUT_DIR)/*.wgt $(DEST); \ - fi - -package-test: build-test - @cmake --build $(OUTPUT_DIR) $(PACKAGE_ARGS) --target test_widget - @if [ "$(abspath $(DEST))" != "$(abspath $(OUTPUT_DIR))" ]; then \ - mkdir -p $(DEST) && cp $(OUTPUT_DIR)/*.wgt $(DEST); \ - fi - -package: package-release - -package-all: package-release package-test package-coverage package-debug - -update: configure - @cmake --build $(BUILD_DIR) --target autobuild - -install: build - @cmake --build $(BUILD_DIR) $(INSTALL_ARGS) --target install diff --git a/binding/CMakeLists.txt b/binding/CMakeLists.txt deleted file mode 100644 index d452411..0000000 --- a/binding/CMakeLists.txt +++ /dev/null @@ -1,35 +0,0 @@ -########################################################################### -# Copyright 2015, 2016, 2017 IoT.bzh -# -# author: Fulup Ar Foll <fulup@iot.bzh> -# contrib: Romain Forlot <romain.forlot@iot.bzh> -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -########################################################################### - -# Add target to project dependency list -PROJECT_TARGET_ADD(afm-hvac-binding) - - # Define project Targets - add_library(afm-hvac-binding MODULE hvac-demo-binding.c) - - # Binder exposes a unique public entry point - SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES - PREFIX "lib" - LABELS "BINDING" - LINK_FLAGS ${BINDINGS_LINK_FLAG} - OUTPUT_NAME ${TARGET_NAME} - ) - - # Library dependencies (include updates automatically) - TARGET_LINK_LIBRARIES(${TARGET_NAME} ${link_libraries} m) diff --git a/binding/hvac-demo-binding.c b/binding/hvac-demo-binding.c deleted file mode 100644 index cdc6ea3..0000000 --- a/binding/hvac-demo-binding.c +++ /dev/null @@ -1,674 +0,0 @@ -/* - * Copyright (C) 2015, 2016 "IoT.bzh" - * Copyright (C) 2016, 2020 Konsulko Group - * Author "Romain Forlot" - * Author "Jose Bolo" - * Author "Scott Murray <scott.murray@konsulko.com>" - * Author "Matt Ranostay <matt.ranostay@konsulko.com>" - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -#define _GNU_SOURCE - -#include <stdio.h> -#include <string.h> -#include <stdbool.h> -#include <unistd.h> -#include <math.h> -#include <json-c/json.h> - -#define AFB_BINDING_VERSION 3 -#include <afb/afb-binding.h> - -#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" - -static afb_event_t event; - -/*****************************************************************************************/ -/*****************************************************************************************/ -/** **/ -/** **/ -/** SECTION: HANDLE CAN DEVICE **/ -/** **/ -/** **/ -/*****************************************************************************************/ -/*****************************************************************************************/ - -// Initialize CAN hvac array that will be sent trough the socket -static struct { - const char *name; - uint8_t value; -} hvac_values[] = { - { "LeftTemperature", 21 }, - { "RightTemperature", 21 }, - { "Temperature", 21 }, - { "FanSpeed", 0 }, - { "ACEnabled", 0 }, - { "LeftLed", 15 }, - { "RightLed", 15 } -}; - -// Holds RGB combinations for each temperature -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} } -}; - -struct led_paths { - const char *red; - const char *green; - const char *blue; -}led_paths_values = { - .red = RED, - .green = GREEN, - .blue = BLUE -}; - -// Get original get temperature function from cpp hvacplugin code -static uint8_t to_can_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; -} - -static uint8_t read_temp_left_zone() -{ - return hvac_values[0].value; -} - -static uint8_t read_temp_right_zone() -{ - return hvac_values[1].value; -} - -static uint8_t read_temp_left_led() -{ - return hvac_values[5].value; -} - -static uint8_t read_temp_right_led() -{ - return hvac_values[6].value; -} - -static uint8_t read_temp() -{ - return (uint8_t)(((int)read_temp_left_zone() + (int)read_temp_right_zone()) >> 1); -} - -static uint8_t read_fanspeed() -{ - return hvac_values[3].value; -} - -/* - * @param: None - * - * @brief: Parse JSON configuration file for blinkm path - */ -static int parse_config() -{ - struct json_object *ledtemp = NULL; - struct json_object *jobj = json_object_from_file("/etc/hvac.json"); - - // Check if .json file has been parsed as a json_object - if (!jobj) { - AFB_ERROR("JSON file could not be opened!\n"); - return 1; - } - - // Check if json_object with key "ledtemp" has been found in .json file - if (!json_object_object_get_ex(jobj, "ledtemp", &ledtemp)){ - AFB_ERROR("Key not found!\n"); - return 1; - } - - // Extract directory paths for each LED colour - json_object_object_foreach(ledtemp, key, value) { - if (strcmp(key, "red") == 0) { - led_paths_values.red = json_object_get_string(value); - } else if (strcmp(key, "green") == 0) { - led_paths_values.green = json_object_get_string(value); - } else if (strcmp(key, "blue") == 0) { - led_paths_values.blue = json_object_get_string(value); - } - } - - return 0; -} - -/* - * @brief Writing to LED for both temperature sliders - */ -static int temp_write_led() -{ - - int red_value, green_value, blue_value; - int right_temp; - int left_temp; - - left_temp = read_temp_left_led() - 15; - right_temp = read_temp_right_led() - 15; - - // Calculates average colour value taken from the temperature toggles - red_value = (degree_colours[left_temp].rgb[0] + degree_colours[right_temp].rgb[0]) / 2; - green_value = (degree_colours[left_temp].rgb[1] + degree_colours[right_temp].rgb[1]) / 2; - blue_value = (degree_colours[left_temp].rgb[2] + degree_colours[right_temp].rgb[2]) / 2; - - // default path: /sys/class/leds/blinkm-3-9-red/brightness - FILE* r = fopen(led_paths_values.red, "w"); - if(r){ - fprintf(r, "%d", red_value); - fclose(r); - } else { - AFB_ERROR("Unable to open red LED path!\n"); - return -1; - } - - // default path: /sys/class/leds/blinkm-3-9-green/brightness - FILE* g = fopen(led_paths_values.green, "w"); - if(g){ - fprintf(g, "%d", green_value); - fclose(g); - } else { - AFB_ERROR("Unable to open green LED path!\n"); - return -1; - } - - // default path: /sys/class/leds/blinkm-3-9-blue/brightness - FILE* b = fopen(led_paths_values.blue, "w"); - if(b){ - fprintf(b, "%d", blue_value); - fclose(b); - } else { - AFB_ERROR("Unable to open blue LED path!\n"); - return -1; - } - - return 0; -} - -/* - * @brief Get temperature of left toggle in HVAC system - * - * @param afb_req_t : pointer to a afb request structure - * - */ -static void temp_left_zone_led(afb_req_t request) -{ - int i = 5, rc, x, changed; - double d; - struct json_object *query, *val; - uint8_t values[sizeof hvac_values / sizeof *hvac_values]; - uint8_t saves[sizeof hvac_values / sizeof *hvac_values]; - - AFB_WARNING("In temp_left_zone_led."); - - query = afb_req_json(request); - - /* records initial values */ - AFB_WARNING("Records initial values"); - values[i] = saves[i] = hvac_values[i].value; - - - if (json_object_object_get_ex(query, hvac_values[i].name, &val)) - { - AFB_WARNING("Value of values[i] = %d", values[i]); - AFB_WARNING("We got it. Tests if it is an int or double."); - if (json_object_is_type(val, json_type_int)) { - x = json_object_get_int(val); - AFB_WARNING("We get an int: %d",x); - } - else if (json_object_is_type(val, json_type_double)) { - d = json_object_get_double(val); - x = (int)round(d); - AFB_WARNING("We get a double: %f => %d",d,x); - } - else { - afb_req_fail_f(request, "bad-request", - "argument '%s' isn't integer or double", hvac_values[i].name); - return; - } - if (x < 0 || x > 255) - { - afb_req_fail_f(request, "bad-request", - "argument '%s' is out of bounds", hvac_values[i].name); - return; - } - if (values[i] != x) { - values[i] = (uint8_t)x; - changed = 1; - AFB_WARNING("%s changed to %d", hvac_values[i].name,x); - } - } - else { - AFB_WARNING("%s not found in query!",hvac_values[i].name); - } - - - if (changed) { - hvac_values[i].value = values[i]; // update structure at line 102 - AFB_WARNING("WRITE_LED: value: %d", hvac_values[i].value); - rc = temp_write_led(); - if (rc >= 0) { - afb_req_success(request, NULL, NULL); - return; - } - /* restore initial values */ - hvac_values[i].value = saves[i]; - afb_req_fail(request, "error", "I2C error"); - } -} - -/* - * @brief Get temperature of right toggle in HVAC system - * - * @param afb_req_t : pointer to a afb request structure - * - */ -static void temp_right_zone_led(afb_req_t request) -{ - int i = 6, rc, x, changed; - double d; - struct json_object *query, *val; - uint8_t values[sizeof hvac_values / sizeof *hvac_values]; - uint8_t saves[sizeof hvac_values / sizeof *hvac_values]; - - AFB_WARNING("In temp_right_zone_led."); - - query = afb_req_json(request); - - /* records initial values */ - AFB_WARNING("Records initial values"); - values[i] = saves[i] = hvac_values[i].value; - - - if (json_object_object_get_ex(query, hvac_values[i].name, &val)) - { - AFB_WARNING("Value of values[i] = %d", values[i]); - AFB_WARNING("We got it. Tests if it is an int or double."); - if (json_object_is_type(val, json_type_int)) { - x = json_object_get_int(val); - AFB_WARNING("We get an int: %d",x); - } - else if (json_object_is_type(val, json_type_double)) { - d = json_object_get_double(val); - x = (int)round(d); - AFB_WARNING("We get a double: %f => %d",d,x); - } - else { - afb_req_fail_f(request, "bad-request", - "argument '%s' isn't integer or double", hvac_values[i].name); - return; - } - if (x < 0 || x > 255) - { - afb_req_fail_f(request, "bad-request", - "argument '%s' is out of bounds", hvac_values[i].name); - return; - } - if (values[i] != x) { - values[i] = (uint8_t)x; - changed = 1; - AFB_WARNING("%s changed to %d", hvac_values[i].name,x); - } - } - else { - AFB_WARNING("%s not found in query!", hvac_values[i].name); - } - - - if (changed) { - hvac_values[i].value = values[i]; // update structure at line 102 - AFB_WARNING("WRITE_LED: value: %d", hvac_values[i].value); - - rc = temp_write_led(); - if (rc >= 0) { - afb_req_success(request, NULL, NULL); - return; - } - - /* restore initial values */ - hvac_values[i].value = saves[i]; - afb_req_fail(request, "error", "I2C error"); - } -} - -static int write_can(afb_api_t api) -{ - json_object *jresp = json_object_new_object(); - json_object *jobj = json_object_new_object(); - json_object *jarray = json_object_new_array(); - - json_object_object_add(jresp, "bus_name", json_object_new_string("ls")); - json_object_object_add(jresp, "frame", jobj); - - json_object_object_add(jobj, "can_id", json_object_new_int(0x30)); - json_object_object_add(jobj, "can_dlc", json_object_new_int(8)); - - - json_object_array_add(jarray, - json_object_new_int(to_can_temp(read_temp_left_zone()))); - - json_object_array_add(jarray, - json_object_new_int(to_can_temp(read_temp_right_zone()))); - - json_object_array_add(jarray, - json_object_new_int(to_can_temp(read_temp()))); - - json_object_array_add(jarray, json_object_new_int(0xf0)); - json_object_array_add(jarray, json_object_new_int(read_fanspeed())); - json_object_array_add(jarray, json_object_new_int(1)); - json_object_array_add(jarray, json_object_new_int(0)); - json_object_array_add(jarray, json_object_new_int(0)); - - json_object_object_add(jobj, "can_data", jarray); - - return afb_api_call_sync(api, "low-can", "write", jresp, NULL, NULL, NULL); -} - -/*****************************************************************************************/ -/*****************************************************************************************/ -/** **/ -/** **/ -/** SECTION: BINDING VERBS IMPLEMENTATION **/ -/** **/ -/** **/ -/*****************************************************************************************/ -/*****************************************************************************************/ - -/* - * @brief Get fan speed HVAC system - * - * @param afb_req_t : pointer to a afb request structure - * - */ -static void get_fanspeed(afb_req_t request) -{ - json_object *ret_json; - uint8_t fanspeed = read_fanspeed(); - - ret_json = json_object_new_object(); - json_object_object_add(ret_json, "FanSpeed", json_object_new_int(fanspeed)); - - afb_req_success(request, ret_json, NULL); -} - -/* - * @brief Read Consign right zone temperature for HVAC system - * - * @param afb_req_t : pointer to a afb request structure - * - */ -static void get_temp_right_zone(afb_req_t request) -{ - json_object *ret_json; - uint8_t temp = read_temp_right_zone(); - - ret_json = json_object_new_object(); - json_object_object_add(ret_json, "RightTemperature", json_object_new_int(temp)); - - afb_req_success(request, ret_json, NULL); -} - -/* - * @brief Read Consign left zone temperature for HVAC system - * - * @param afb_req_t : pointer to a afb request structure - * - */ -static void get_temp_left_zone(afb_req_t request) -{ - json_object *ret_json; - uint8_t temp = read_temp_left_zone(); - - ret_json = json_object_new_object(); - json_object_object_add(ret_json, "LeftTemperature", json_object_new_int(temp)); - - afb_req_success(request, ret_json, NULL); -} - -/* - * @brief Read all values - * - * @param afb_req_t : pointer to a afb request structure - * - */ -static void get(afb_req_t request) -{ - AFB_REQ_DEBUG(request, "Getting all values"); - json_object *ret_json; - - ret_json = json_object_new_object(); - json_object_object_add(ret_json, "LeftTemperature", json_object_new_int(read_temp_left_zone())); - json_object_object_add(ret_json, "RightTemperature", json_object_new_int(read_temp_right_zone())); - json_object_object_add(ret_json, "FanSpeed", json_object_new_int(read_fanspeed())); - - afb_req_success(request, ret_json, NULL); -} - -/* - * @brief Set a component value using a json object retrieved from request - * - * @param afb_req_t : pointer to a afb request structure - * - */ -static void set(afb_req_t request) -{ - int i, rc, x, changed; - double d; - struct json_object *query, *val; - uint8_t values[sizeof hvac_values / sizeof *hvac_values]; - uint8_t saves[sizeof hvac_values / sizeof *hvac_values]; - afb_api_t api = afb_req_get_api(request); - - /* records initial values */ - AFB_DEBUG("Records initial values"); - i = (int)(sizeof hvac_values / sizeof *hvac_values); - while (i) { - i--; - values[i] = saves[i] = hvac_values[i].value; - } - - /* Loop getting arguments */ - query = afb_req_json(request); - changed = 0; - i = (int)(sizeof hvac_values / sizeof *hvac_values); - AFB_DEBUG("Looping for args. i: %d", i); - while (i) - { - i--; - AFB_DEBUG("Searching... query: %s, i: %d, comp: %s", json_object_to_json_string(query), i, hvac_values[i].name); - if (json_object_object_get_ex(query, hvac_values[i].name, &val)) - { - AFB_DEBUG("We got it. Tests if it is an int or double."); - if (json_object_is_type(val, json_type_int)) { - x = json_object_get_int(val); - AFB_DEBUG("We get an int: %d",x); - } - else if (json_object_is_type(val, json_type_double)) { - d = json_object_get_double(val); - x = (int)round(d); - AFB_DEBUG("We get a double: %f => %d",d,x); - } - else { - afb_req_fail_f(request, "bad-request", - "argument '%s' isn't integer or double", hvac_values[i].name); - return; - } - if (x < 0 || x > 255) - { - afb_req_fail_f(request, "bad-request", - "argument '%s' is out of bounds", hvac_values[i].name); - return; - } - if (values[i] != x) { - values[i] = (uint8_t)x; - changed = 1; - AFB_DEBUG("%s changed to %d",hvac_values[i].name,x); - } - } - else { - AFB_DEBUG("%s not found in query!",hvac_values[i].name); - } - } - - /* attemps to set new values */ - AFB_DEBUG("Diff: %d", changed); - if (changed) - { - i = (int)(sizeof hvac_values / sizeof *hvac_values); - while (i) { - i--; - hvac_values[i].value = values[i]; - } - rc = write_can(api); - if (rc >= 0) { - afb_req_success(request, NULL, NULL); - return; - } - - /* restore initial values */ - i = (int)(sizeof hvac_values / sizeof *hvac_values); - while (i) { - i--; - hvac_values[i].value = saves[i]; - } - afb_req_fail(request, "error", "CAN error"); - } - else { - afb_req_success(request, NULL, "No changes"); - } -} - -static int bindingServicePreInit(afb_api_t api) -{ - if(parse_config() != 0) - AFB_WARNING("Default values are being used!\n"); - - return 0; -} - -static int bindingServiceInit(afb_api_t api) -{ - event = afb_daemon_make_event("language"); - - if(afb_daemon_require_api("identity", 1)) - return -1; - - if(afb_daemon_require_api("low-can", 1)) - return -1; - - if (afb_api_call_sync(api, "low-can", "auth", NULL, NULL, NULL, NULL)) - return -1; - - return afb_api_call_sync(api, "identity", "subscribe", json_object_new_object(), NULL, NULL, NULL); -} - -static void onEvent(afb_api_t api, const char *event_name, struct json_object *object) -{ - json_object *id_evt_name, *current_identity; - - if (json_object_object_get_ex(object, "eventName", &id_evt_name) && - !strcmp(json_object_get_string(id_evt_name), "login") && - !afb_api_call_sync(api, "identity", "get", json_object_new_object(), ¤t_identity, NULL, NULL)) { - json_object *language = NULL; - json_object *response; - if (! json_object_object_get_ex(current_identity, "response", &response) || ! json_object_object_get_ex(response, "graphPreferredLanguage", &language)) { - language = json_object_new_string("en_US"); - } - afb_event_broadcast(event, language); - return; - } -} - -// TODO: Have to change session management flag to AFB_SESSION_CHECK to use token auth -static const afb_verb_t hvac_verbs[]= { - { - .verb = "get_temp_left_zone", - .callback = get_temp_left_zone, - .info = "Get the left zone temperature", - .session = AFB_SESSION_NONE, - }, - { - .verb = "get_temp_right_zone", - .callback = get_temp_right_zone, - .info = "Get the right zone temperature", - .session = AFB_SESSION_NONE, - }, - { - .verb = "get_fanspeed", - .callback = get_fanspeed, - .info = "Read fan speed", - .session = AFB_SESSION_NONE, - }, - { - .verb = "get", - .callback = get, - .info = "Read all speed", - .session = AFB_SESSION_NONE, - }, - { - .verb = "set", - .callback = set, - .info = "Set a HVAC component value", - .session = AFB_SESSION_NONE, - }, - { - .verb = "temp_left_zone_led", - .callback = temp_left_zone_led, - .info = "Turn on LED on left temperature zone", - .session = AFB_SESSION_NONE, - }, - { - .verb = "temp_right_zone_led", - .callback = temp_right_zone_led, - .info = "Turn on LED on left temperature zone", - .session = AFB_SESSION_NONE, - - }, - { } -}; - -const afb_binding_t afbBindingV3 = { - .api = "hvac", - .info = "HVAC service API", - .verbs = hvac_verbs, - .preinit = bindingServicePreInit, - .init = bindingServiceInit, - .onevent = onEvent, -}; diff --git a/conf.d/cmake/config.cmake b/conf.d/cmake/config.cmake deleted file mode 100644 index bf7a10c..0000000 --- a/conf.d/cmake/config.cmake +++ /dev/null @@ -1,152 +0,0 @@ -########################################################################### -# Copyright 2015, 2016, 2017 IoT.bzh -# -# author: Fulup Ar Foll <fulup@iot.bzh> -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -########################################################################### - -# Project Info -# ------------------ -set(PROJECT_NAME agl-service-hvac) -set(PROJECT_PRETTY_NAME "AFM binding for HVAC interface") -set(PROJECT_DESCRIPTION "Binding for HVAC interface") -set(PROJECT_VERSION "1.0") -set(PROJECT_ICON "icon.png") -set(PROJECT_LICENSE "APL2.0") -set(PROJECT_LANGUAGES,"C") -set(API_NAME "hvac") - -# Where are stored the project configuration files -# relative to the root project directory -set(PROJECT_CMAKE_CONF_DIR "conf.d") - -# Where are stored your external libraries for your project. This is 3rd party library that you don't maintain -# but used and must be built and linked. -# set(PROJECT_LIBDIR "libs") - -# Where are stored data for your application. Pictures, static resources must be placed in that folder. -# set(PROJECT_RESOURCES "data") - -# Which directories inspect to find CMakeLists.txt target files -# set(PROJECT_SRC_DIR_PATTERN "*") - -# Compilation Mode (DEBUG, RELEASE) -# ---------------------------------- -set(BUILD_TYPE "RELEASE") - -# Kernel selection if needed. You can choose between a -# mandatory version to impose a minimal version. -# Or check Kernel minimal version and just print a Warning -# about missing features and define a preprocessor variable -# to be used as preprocessor condition in code to disable -# incompatibles features. Preprocessor define is named -# KERNEL_MINIMAL_VERSION_OK. -# -# NOTE*** FOR NOW IT CHECKS KERNEL Yocto environment and -# Yocto SDK Kernel version. -# ----------------------------------------------- -#set(kernel_mandatory_version 4.8) - -# Compiler selection if needed. Impose a minimal version. -# ----------------------------------------------- -set (gcc_minimal_version 4.9) - -# PKG_CONFIG required packages -# ----------------------------- -set (PKG_REQUIRED_LIST - json-c - libsystemd>=222 - afb-daemon -) - -# Customize link option -# ----------------------------- -list (APPEND link_libraries -pthread) - -# (BUG!!!) as PKG_CONFIG_PATH does not work [should be an env variable] -# --------------------------------------------------------------------- -set(INSTALL_PREFIX $ENV{HOME}/opt) -set(CMAKE_PREFIX_PATH ${CMAKE_INSTALL_PREFIX}/lib64/pkgconfig ${CMAKE_INSTALL_PREFIX}/lib/pkgconfig) -set(LD_LIBRARY_PATH ${CMAKE_INSTALL_PREFIX}/lib64 ${CMAKE_INSTALL_PREFIX}/lib) - -# Optional location for config.xml.in -# ----------------------------------- -set(WIDGET_CONFIG_TEMPLATE ${CMAKE_CURRENT_SOURCE_DIR}/conf.d/wgt/config.xml.in) - -# Mandatory widget Mimetype specification of the main unit -# -------------------------------------------------------------------------- -# Choose between : -#- text/html : HTML application, -# content.src designates the home page of the application -# -#- application/vnd.agl.native : AGL compatible native, -# content.src designates the relative path of the binary. -# -# - application/vnd.agl.service: AGL service, content.src is not used. -# -#- ***application/x-executable***: Native application, -# content.src designates the relative path of the binary. -# For such application, only security setup is made. -# -set(WIDGET_TYPE application/vnd.agl.service) - -# Mandatory Widget entry point file of the main unit -# -------------------------------------------------------------- -# This is the file that will be executed, loaded, -# at launch time by the application framework. -# -set(WIDGET_ENTRY_POINT lib/libafm-hvac-binding.so) - -# Print a helper message when every thing is finished -# ---------------------------------------------------- -set(CLOSING_MESSAGE "Test with: afb-daemon --rootdir=\$\$(pwd)/package --binding=\$\$(pwd)/package/${WIDGET_ENTRY_POINT} --port=1234 --tracereq=common --token=\"1\" --verbose") -set(PACKAGE_MESSAGE "Install widget file using in the target : afm-util install ${PROJECT_NAME}.wgt") - -# Optional dependencies order -# --------------------------- -#set(EXTRA_DEPENDENCIES_ORDER) - -# Optional Extra global include path -# ----------------------------------- -#set(EXTRA_INCLUDE_DIRS) - -# Optional extra libraries -# ------------------------- -#set(EXTRA_LINK_LIBRARIES) - -# Optional force binding installation -# ------------------------------------ -# set(BINDINGS_INSTALL_PREFIX PrefixPath ) - -# Optional force binding Linking flag -# ------------------------------------ -# set(BINDINGS_LINK_FLAG LinkOptions ) - -# Optional force package prefix generation, like widget -# ----------------------------------------------------- -# set(PKG_PREFIX DestinationPath) - -# Optional Application Framework security token -# and port use for remote debugging. -#------------------------------------------------------------ -#set(AFB_TOKEN "" CACHE PATH "Default AFB_TOKEN") -#set(AFB_REMPORT "1234" CACHE PATH "Default AFB_TOKEN") - -# This include is mandatory and MUST happens at the end -# of this file, else you expose you to unexpected behavior -# -# This CMake module could be found at the following url: -# https://gerrit.automotivelinux.org/gerrit/#/admin/projects/src/cmake-apps-module -# ----------------------------------------------------------- -include(CMakeAfbTemplates) diff --git a/conf.d/wgt/config.xml.in b/conf.d/wgt/config.xml.in deleted file mode 100644 index 9f26c8d..0000000 --- a/conf.d/wgt/config.xml.in +++ /dev/null @@ -1,29 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<widget xmlns="http://www.w3.org/ns/widgets" id="@PROJECT_NAME@" version="@PROJECT_VERSION@"> - <name>@PROJECT_NAME@</name> - <icon src="@PROJECT_ICON@"/> - <content src="@WIDGET_ENTRY_POINT@" type="@WIDGET_TYPE@"/> - <description>@PROJECT_DESCRIPTION@</description> - <author>@PROJECT_AUTHOR@ <@PROJECT_AUTHOR_MAIL@></author> - <license>@PROJECT_LICENSE@</license> - - <feature name="urn:AGL:widget:required-permission"> - <param name="urn:AGL:permission::public:hidden" value="required" /> - <param name="urn:AGL:permission::public:no-htdocs" value="required" /> - <param name="urn:AGL:permission::system:run-by-default" value="required" /> - <param name="urn:AGL:permission::platform:can:write" value="required" /> - </feature> - - <feature name="urn:AGL:widget:provided-api"> - <param name="HVAC" value="ws" /> - </feature> - - <feature name="urn:AGL:widget:required-api"> - <param name="identity" value="ws" /> - <param name="low-can" value="ws" /> - </feature> - - <feature name="urn:AGL:widget:required-binding"> - <param name="@WIDGET_ENTRY_POINT@" value="local" /> - </feature> -</widget> diff --git a/hvac.json b/hvac.json deleted file mode 100644 index 67827a7..0000000 --- a/hvac.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "ledtemp": { - "red": "/sys/class/leds/blinkm-3-9-red/brightness", - "green": "/sys/class/leds/blinkm-3-9-green/brightness", - "blue": "/sys/class/leds/blinkm-3-9-blue/brightness" - }, - "can_device": "vcan0" -} diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..83239fa --- /dev/null +++ b/meson.build @@ -0,0 +1,10 @@ +project('agl-service-hvac', + 'cpp', + license : 'Apache-2.0', + default_options : ['c_std=c17', 'cpp_std=c++17']) + +systemd_dep = dependency('systemd') + +subdir('src') +subdir('systemd') + 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 diff --git a/systemd/agl-service-hvac.service b/systemd/agl-service-hvac.service new file mode 100644 index 0000000..3c05206 --- /dev/null +++ b/systemd/agl-service-hvac.service @@ -0,0 +1,11 @@ +[Unit] +Requires=kuksa-val.service +After=kuksa-val.service + +[Service] +Type=simple +ExecStart=/usr/sbin/agl-service-hvac +Restart=on-failure + +[Install] +WantedBy=default.target diff --git a/systemd/meson.build b/systemd/meson.build new file mode 100644 index 0000000..4551805 --- /dev/null +++ b/systemd/meson.build @@ -0,0 +1,3 @@ +systemd_system_unit_dir = systemd_dep.get_pkgconfig_variable('systemdsystemunitdir') + +install_data('agl-service-hvac.service', install_dir : systemd_system_unit_dir) |