summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt21
-rwxr-xr-xautobuild/agl/autobuild128
-rwxr-xr-xautobuild/linux/autobuild128
-rw-r--r--binding/CMakeLists.txt35
-rw-r--r--binding/hvac-demo-binding.c674
-rw-r--r--conf.d/cmake/config.cmake152
-rw-r--r--conf.d/wgt/config.xml.in29
-rw-r--r--hvac.json8
-rw-r--r--meson.build10
-rw-r--r--src/hvac-can-helper.cpp176
-rw-r--r--src/hvac-can-helper.hpp53
-rw-r--r--src/hvac-led-helper.cpp194
-rw-r--r--src/hvac-led-helper.hpp34
-rw-r--r--src/hvac-service.cpp85
-rw-r--r--src/hvac-service.hpp33
-rw-r--r--src/main.cpp34
-rw-r--r--src/meson.build19
-rw-r--r--src/vis-config.cpp157
-rw-r--r--src/vis-config.hpp43
-rw-r--r--src/vis-session.cpp374
-rw-r--r--src/vis-session.hpp78
-rw-r--r--systemd/agl-service-hvac.service11
-rw-r--r--systemd/meson.build3
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(), &current_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@ &lt;@PROJECT_AUTHOR_MAIL@&gt;</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 &timestamp)
+{
+ // Placeholder since no gets are performed ATM
+}
+
+void HvacService::handle_notification(std::string &path, std::string &value, std::string &timestamp)
+{
+ if (path == "Vehicle.Cabin.HVAC.Station.Row1.Left.Temperature") {
+ try {
+ int temp = std::stoi(value);
+ if (temp >= 0 && temp < 256)
+ set_left_temperature(temp);
+ }
+ catch (std::exception ex) {
+ // ignore bad value
+ }
+ } else if (path == "Vehicle.Cabin.HVAC.Station.Row1.Right.Temperature") {
+ try {
+ int temp = std::stoi(value);
+ if (temp >= 0 && temp < 256)
+ set_right_temperature(temp);
+ }
+ catch (std::exception ex) {
+ // ignore bad value
+ }
+ } else if (path == "Vehicle.Cabin.HVAC.Station.Row1.Left.FanSpeed") {
+ try {
+ int speed = std::stoi(value);
+ if (speed >= 0 && speed < 256)
+ set_fan_speed(speed);
+ }
+ catch (std::exception ex) {
+ // ignore bad value
+ }
+ } else if (path == "Vehicle.Cabin.HVAC.Station.Row1.Right.FanSpeed") {
+ try {
+ int speed = std::stoi(value);
+ if (speed >= 0 && speed < 256)
+ set_fan_speed(speed);
+ }
+ catch (std::exception ex) {
+ // ignore bad value
+ }
+ }
+ // else ignore
+}
+
+void HvacService::set_left_temperature(uint8_t temp)
+{
+ m_can_helper.set_left_temperature(temp);
+ m_led_helper.set_left_temperature(temp);
+}
+
+void HvacService::set_right_temperature(uint8_t temp)
+{
+ m_can_helper.set_right_temperature(temp);
+ m_led_helper.set_right_temperature(temp);
+}
+
+void HvacService::set_fan_speed(uint8_t speed)
+{
+ m_can_helper.set_fan_speed(speed);
+}
diff --git a/src/hvac-service.hpp b/src/hvac-service.hpp
new file mode 100644
index 0000000..27bfe1a
--- /dev/null
+++ b/src/hvac-service.hpp
@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _HVAC_SERVICE_HPP
+#define _HVAC_SERVICE_HPP
+
+#include "vis-session.hpp"
+#include "hvac-can-helper.hpp"
+#include "hvac-led-helper.hpp"
+
+class HvacService : public VisSession
+{
+public:
+ HvacService(const VisConfig &config, net::io_context& ioc, ssl::context& ctx);
+
+protected:
+ virtual void handle_authorized_response(void) override;
+
+ virtual void handle_get_response(std::string &path, std::string &value, std::string &timestamp) override;
+
+ virtual void handle_notification(std::string &path, std::string &value, std::string &timestamp) override;
+
+private:
+ HvacCanHelper m_can_helper;
+ HvacLedHelper m_led_helper;
+
+ void set_left_temperature(uint8_t temp);
+
+ void set_right_temperature(uint8_t temp);
+
+ void set_fan_speed(uint8_t temp);
+};
+
+#endif // _HVAC_SERVICE_HPP
diff --git a/src/main.cpp b/src/main.cpp
new file mode 100644
index 0000000..6bb165f
--- /dev/null
+++ b/src/main.cpp
@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include <iostream>
+#include <iomanip>
+#include <boost/asio/signal_set.hpp>
+#include <boost/bind.hpp>
+#include "hvac-service.hpp"
+
+using work_guard_type = boost::asio::executor_work_guard<boost::asio::io_context::executor_type>;
+
+int main(int argc, char** argv)
+{
+ // The io_context is required for all I/O
+ net::io_context ioc;
+
+ // Register to stop I/O context on SIGINT and SIGTERM
+ net::signal_set signals(ioc, SIGINT, SIGTERM);
+ signals.async_wait(boost::bind(&net::io_context::stop, &ioc));
+
+ // The SSL context is required, and holds certificates
+ ssl::context ctx{ssl::context::tlsv12_client};
+
+ // Launch the asynchronous operation
+ VisConfig config("agl-service-hvac");
+ std::make_shared<HvacService>(config, ioc, ctx)->run();
+
+ // Ensure I/O context continues running even if there's no work
+ work_guard_type work_guard(ioc.get_executor());
+
+ // Run the I/O context
+ ioc.run();
+
+ return 0;
+}
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..149ec99
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,19 @@
+boost_dep = dependency('boost',
+ version : '>=1.72',
+ modules : [ 'thread', 'filesystem', 'program_options', 'log', 'system' ])
+openssl_dep = dependency('openssl')
+thread_dep = dependency('threads')
+cxx = meson.get_compiler('cpp')
+
+src = [ 'vis-config.cpp',
+ 'vis-session.cpp',
+ 'hvac-service.cpp',
+ 'hvac-can-helper.cpp',
+ 'hvac-led-helper.cpp',
+ 'main.cpp'
+]
+executable('agl-service-hvac',
+ src,
+ dependencies: [boost_dep, openssl_dep, thread_dep, systemd_dep],
+ install: true,
+ install_dir : get_option('sbindir'))
diff --git a/src/vis-config.cpp b/src/vis-config.cpp
new file mode 100644
index 0000000..b8d9266
--- /dev/null
+++ b/src/vis-config.cpp
@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "vis-config.hpp"
+#include <iostream>
+#include <iomanip>
+#include <sstream>
+#include <boost/property_tree/ptree.hpp>
+#include <boost/property_tree/ini_parser.hpp>
+#include <boost/filesystem.hpp>
+
+namespace property_tree = boost::property_tree;
+namespace filesystem = boost::filesystem;
+
+#define DEFAULT_CLIENT_KEY_FILE "/etc/kuksa-val/Client.key"
+#define DEFAULT_CLIENT_CERT_FILE "/etc/kuksa-val/Client.pem"
+#define DEFAULT_CA_CERT_FILE "/etc/kuksa-val/CA.pem"
+
+
+VisConfig::VisConfig(const std::string &hostname,
+ const unsigned port,
+ const std::string &clientKey,
+ const std::string &clientCert,
+ const std::string &caCert,
+ const std::string &authToken,
+ bool verifyPeer) :
+ m_hostname(hostname),
+ m_port(port),
+ m_clientKey(clientKey),
+ m_clientCert(clientCert),
+ m_caCert(caCert),
+ m_authToken(authToken),
+ m_verifyPeer(verifyPeer),
+ m_verbose(0),
+ m_valid(true)
+{
+ // Potentially could do some certificate validation here...
+}
+
+VisConfig::VisConfig(const std::string &appname) :
+ m_valid(false)
+{
+ std::string config("/etc/xdg/AGL/");
+ config += appname;
+ config += ".conf";
+ char *home = getenv("XDG_CONFIG_HOME");
+ if (home) {
+ config = home;
+ config += "/AGL/";
+ config += appname;
+ config += ".conf";
+ }
+
+ std::cout << "Using configuration " << config << std::endl;
+ property_tree::ptree pt;
+ try {
+ property_tree::ini_parser::read_ini(config, pt);
+ }
+ catch (std::exception &ex) {
+ std::cerr << "Could not read " << config << std::endl;
+ return;
+ }
+ const property_tree::ptree &settings =
+ pt.get_child("vis-client", property_tree::ptree());
+
+ m_hostname = settings.get("server", "localhost");
+ std::stringstream ss;
+ ss << m_hostname;
+ ss >> std::quoted(m_hostname);
+ if (m_hostname.empty()) {
+ std::cerr << "Invalid server hostname" << std::endl;
+ return;
+ }
+
+ m_port = settings.get("port", 8090);
+ if (m_port == 0) {
+ std::cerr << "Invalid server port" << std::endl;
+ return;
+ }
+
+ // Default to disabling peer verification for now to be able
+ // to use the default upstream KUKSA.val certificates for
+ // testing. Wrangling server and CA certificate generation
+ // and management to be able to verify will require further
+ // investigation.
+ m_verifyPeer = settings.get("verify-server", false);
+
+ std::string keyFileName = settings.get("key", DEFAULT_CLIENT_KEY_FILE);
+ std::stringstream().swap(ss);
+ ss << keyFileName;
+ ss >> std::quoted(keyFileName);
+ ss.str("");
+ if (keyFileName.empty()) {
+ std::cerr << "Invalid client key filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(keyFileName, m_clientKey);
+ if (m_clientKey.empty()) {
+ std::cerr << "Invalid client key file" << std::endl;
+ return;
+ }
+
+ std::string certFileName = settings.get("certificate", DEFAULT_CLIENT_CERT_FILE);
+ std::stringstream().swap(ss);
+ ss << certFileName;
+ ss >> std::quoted(certFileName);
+ if (certFileName.empty()) {
+ std::cerr << "Invalid client certificate filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(certFileName, m_clientCert);
+ if (m_clientCert.empty()) {
+ std::cerr << "Invalid client certificate file" << std::endl;
+ return;
+ }
+
+ std::string caCertFileName = settings.get("ca-certificate", DEFAULT_CA_CERT_FILE);
+ std::stringstream().swap(ss);
+ ss << caCertFileName;
+ ss >> std::quoted(caCertFileName);
+ if (caCertFileName.empty()) {
+ std::cerr << "Invalid CA certificate filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(caCertFileName, m_caCert);
+ if (m_caCert.empty()) {
+ std::cerr << "Invalid CA certificate file" << std::endl;
+ return;
+ }
+
+ std::string authTokenFileName = settings.get("authorization", "");
+ std::stringstream().swap(ss);
+ ss << authTokenFileName;
+ ss >> std::quoted(authTokenFileName);
+ if (authTokenFileName.empty()) {
+ std::cerr << "Invalid authorization token filename" << std::endl;
+ return;
+ }
+ filesystem::load_string_file(authTokenFileName, m_authToken);
+ if (m_authToken.empty()) {
+ std::cerr << "Invalid authorization token file" << std::endl;
+ return;
+ }
+
+ m_verbose = 0;
+ std::string verbose = settings.get("verbose", "");
+ std::stringstream().swap(ss);
+ ss << verbose;
+ ss >> std::quoted(verbose);
+ if (!verbose.empty()) {
+ if (verbose == "true" || verbose == "1")
+ m_verbose = 1;
+ if (verbose == "2")
+ m_verbose = 2;
+ }
+
+ m_valid = true;
+}
diff --git a/src/vis-config.hpp b/src/vis-config.hpp
new file mode 100644
index 0000000..b0f72f9
--- /dev/null
+++ b/src/vis-config.hpp
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _VIS_CONFIG_HPP
+#define _VIS_CONFIG_HPP
+
+#include <string>
+
+class VisConfig
+{
+public:
+ explicit VisConfig(const std::string &hostname,
+ const unsigned port,
+ const std::string &clientKey,
+ const std::string &clientCert,
+ const std::string &caCert,
+ const std::string &authToken,
+ bool verifyPeer = true);
+ explicit VisConfig(const std::string &appname);
+ ~VisConfig() {};
+
+ std::string hostname() { return m_hostname; };
+ unsigned port() { return m_port; };
+ std::string clientKey() { return m_clientKey; };
+ std::string clientCert() { return m_clientCert; };
+ std::string caCert() { return m_caCert; };
+ std::string authToken() { return m_authToken; };
+ bool verifyPeer() { return m_verifyPeer; };
+ bool valid() { return m_valid; };
+ unsigned verbose() { return m_verbose; };
+
+private:
+ std::string m_hostname;
+ unsigned m_port;
+ std::string m_clientKey;
+ std::string m_clientCert;
+ std::string m_caCert;
+ std::string m_authToken;
+ bool m_verifyPeer;
+ unsigned m_verbose;
+ bool m_valid;
+};
+
+#endif // _VIS_CONFIG_HPP
diff --git a/src/vis-session.cpp b/src/vis-session.cpp
new file mode 100644
index 0000000..880e3ae
--- /dev/null
+++ b/src/vis-session.cpp
@@ -0,0 +1,374 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#include "vis-session.hpp"
+#include <iostream>
+#include <sstream>
+#include <thread>
+
+
+// Logging helper
+static void log_error(beast::error_code error, char const* what)
+{
+ std::cerr << what << " error: " << error.message() << std::endl;
+}
+
+
+// Resolver and socket require an io_context
+VisSession::VisSession(const VisConfig &config, net::io_context& ioc, ssl::context& ctx) :
+ m_config(config),
+ m_resolver(net::make_strand(ioc)),
+ m_ws(net::make_strand(ioc), ctx)
+{
+}
+
+// Start the asynchronous operation
+void VisSession::run()
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ // Start by resolving hostname
+ m_resolver.async_resolve(m_config.hostname(),
+ std::to_string(m_config.port()),
+ beast::bind_front_handler(&VisSession::on_resolve,
+ shared_from_this()));
+}
+
+void VisSession::on_resolve(beast::error_code error,
+ tcp::resolver::results_type results)
+{
+ if(error) {
+ log_error(error, "resolve");
+ return;
+ }
+
+ // Set a timeout on the connect operation
+ beast::get_lowest_layer(m_ws).expires_after(std::chrono::seconds(30));
+
+ // Connect to resolved address
+ if (m_config.verbose())
+ std::cout << "Connecting" << std::endl;
+ m_results = results;
+ connect();
+}
+
+void VisSession::connect()
+{
+ beast::get_lowest_layer(m_ws).async_connect(m_results,
+ beast::bind_front_handler(&VisSession::on_connect,
+ shared_from_this()));
+}
+
+void VisSession::on_connect(beast::error_code error,
+ tcp::resolver::results_type::endpoint_type endpoint)
+{
+ if(error) {
+ // The server can take a while to be ready to accept connections,
+ // so keep retrying until we hit the timeout.
+ if (error == net::error::timed_out) {
+ log_error(error, "connect");
+ return;
+ }
+
+ // Delay 500 ms before retrying
+ std::this_thread::sleep_for(std::chrono::milliseconds(500));
+
+ if (m_config.verbose())
+ std::cout << "Connecting" << std::endl;
+
+ connect();
+ return;
+ }
+
+ if (m_config.verbose())
+ std::cout << "Connected" << std::endl;
+
+ // Set handshake timeout
+ beast::get_lowest_layer(m_ws).expires_after(std::chrono::seconds(30));
+
+ // Set SNI Hostname (many hosts need this to handshake successfully)
+ if(!SSL_set_tlsext_host_name(m_ws.next_layer().native_handle(),
+ m_config.hostname().c_str()))
+ {
+ error = beast::error_code(static_cast<int>(::ERR_get_error()),
+ net::error::get_ssl_category());
+ log_error(error, "connect");
+ return;
+ }
+
+ // Update the hostname. This will provide the value of the
+ // Host HTTP header during the WebSocket handshake.
+ // See https://tools.ietf.org/html/rfc7230#section-5.4
+ m_hostname = m_config.hostname() + ':' + std::to_string(endpoint.port());
+
+ if (m_config.verbose())
+ std::cout << "Negotiating SSL handshake" << std::endl;
+
+ // Perform the SSL handshake
+ m_ws.next_layer().async_handshake(ssl::stream_base::client,
+ beast::bind_front_handler(&VisSession::on_ssl_handshake,
+ shared_from_this()));
+}
+
+void VisSession::on_ssl_handshake(beast::error_code error)
+{
+ if(error) {
+ log_error(error, "SSL handshake");
+ return;
+ }
+
+ // Turn off the timeout on the tcp_stream, because
+ // the websocket stream has its own timeout system.
+ beast::get_lowest_layer(m_ws).expires_never();
+
+ // NOTE: Explicitly not setting websocket stream timeout here,
+ // as the client is long-running.
+
+ if (m_config.verbose())
+ std::cout << "Negotiating WSS handshake" << std::endl;
+
+ // Perform handshake
+ m_ws.async_handshake(m_hostname,
+ "/",
+ beast::bind_front_handler(&VisSession::on_handshake,
+ shared_from_this()));
+}
+
+void VisSession::on_handshake(beast::error_code error)
+{
+ if(error) {
+ log_error(error, "WSS handshake");
+ return;
+ }
+
+ if (m_config.verbose())
+ std::cout << "Authorizing" << std::endl;
+
+ // Authorize
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"]= "authorize";
+ req["tokens"] = m_config.authToken();
+
+ m_ws.async_write(net::buffer(req.dump(4)),
+ beast::bind_front_handler(&VisSession::on_authorize,
+ shared_from_this()));
+}
+
+void VisSession::on_authorize(beast::error_code error, std::size_t bytes_transferred)
+{
+ boost::ignore_unused(bytes_transferred);
+
+ if(error) {
+ log_error(error, "authorize");
+ return;
+ }
+
+ // Read response
+ m_ws.async_read(m_buffer,
+ beast::bind_front_handler(&VisSession::on_read,
+ shared_from_this()));
+}
+
+// NOTE: Placeholder for now
+void VisSession::on_write(beast::error_code error, std::size_t bytes_transferred)
+{
+ boost::ignore_unused(bytes_transferred);
+
+ if(error) {
+ log_error(error, "write");
+ return;
+ }
+
+ // Do nothing...
+}
+
+void VisSession::on_read(beast::error_code error, std::size_t bytes_transferred)
+{
+ boost::ignore_unused(bytes_transferred);
+
+ if(error) {
+ log_error(error, "read");
+ return;
+ }
+
+ // Handle message
+ std::string s = beast::buffers_to_string(m_buffer.data());
+ json response = json::parse(s, nullptr, false);
+ if (!response.is_discarded()) {
+ handle_message(response);
+ } else {
+ std::cerr << "json::parse failed? got " << s << std::endl;
+ }
+ m_buffer.consume(m_buffer.size());
+
+ // Read next message
+ m_ws.async_read(m_buffer,
+ beast::bind_front_handler(&VisSession::on_read,
+ shared_from_this()));
+}
+
+void VisSession::get(const std::string &path)
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"] = "get";
+ req["path"] = path;
+ req["tokens"] = m_config.authToken();
+
+ m_ws.write(net::buffer(req.dump(4)));
+}
+
+void VisSession::set(const std::string &path, const std::string &value)
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"] = "set";
+ req["path"] = path;
+ req["value"] = value;
+ req["tokens"] = m_config.authToken();
+
+ m_ws.write(net::buffer(req.dump(4)));
+}
+
+void VisSession::subscribe(const std::string &path)
+{
+ if (!m_config.valid()) {
+ return;
+ }
+
+ json req;
+ req["requestId"] = std::to_string(m_requestid++);
+ req["action"] = "subscribe";
+ req["path"] = path;
+ req["tokens"] = m_config.authToken();
+
+ m_ws.write(net::buffer(req.dump(4)));
+}
+
+bool VisSession::parseData(const json &message, std::string &path, std::string &value, std::string &timestamp)
+{
+ if (message.contains("error")) {
+ std::string error = message["error"];
+ return false;
+ }
+
+ if (!(message.contains("data") && message["data"].is_object())) {
+ std::cerr << "Malformed message (data missing)" << std::endl;
+ return false;
+ }
+ auto data = message["data"];
+ if (!(data.contains("path") && data["path"].is_string())) {
+ std::cerr << "Malformed message (path missing)" << std::endl;
+ return false;
+ }
+ path = data["path"];
+ // Convert '/' to '.' in paths to ensure consistency for clients
+ std::replace(path.begin(), path.end(), '/', '.');
+
+ if (!(data.contains("dp") && data["dp"].is_object())) {
+ std::cerr << "Malformed message (datapoint missing)" << std::endl;
+ return false;
+ }
+ auto dp = data["dp"];
+ if (!dp.contains("value")) {
+ std::cerr << "Malformed message (value missing)" << std::endl;
+ return false;
+ } else if (dp["value"].is_string()) {
+ value = dp["value"];
+ } else if (dp["value"].is_number_float()) {
+ double num = dp["value"];
+ value = std::to_string(num);
+ } else if (dp["value"].is_boolean()) {
+ value = dp["value"] ? "true" : "false";
+ } else {
+ std::cerr << "Malformed message (unsupported value type)" << std::endl;
+ return false;
+ }
+
+ if (!(dp.contains("ts") && dp["ts"].is_string())) {
+ std::cerr << "Malformed message (timestamp missing)" << std::endl;
+ return false;
+ }
+ timestamp = dp["ts"];
+
+ return true;
+}
+
+void VisSession::handle_message(const json &message)
+{
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: enter, message = " << to_string(message) << std::endl;
+
+ if (!message.contains("action")) {
+ std::cerr << "Received unknown message (no action), discarding" << std::endl;
+ return;
+ }
+
+ std::string action = message["action"];
+ if (action == "authorize") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS authorization failed: " << error << std::endl;
+ } else {
+ if (m_config.verbose() > 1)
+ std::cout << "authorized" << std::endl;
+
+ handle_authorized_response();
+ }
+ } else if (action == "subscribe") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS subscription failed: " << error << std::endl;
+ }
+ } else if (action == "get") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS get failed: " << error << std::endl;
+ } else {
+ std::string path, value, ts;
+ if (parseData(message, path, value, ts)) {
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: got response " << path << " = " << value << std::endl;
+
+ handle_get_response(path, value, ts);
+ }
+ }
+ } else if (action == "set") {
+ if (message.contains("error")) {
+ std::string error = "unknown";
+ if (message["error"].is_object() && message["error"].contains("message"))
+ error = message["error"]["message"];
+ std::cerr << "VIS set failed: " << error;
+ }
+ } else if (action == "subscription") {
+ std::string path, value, ts;
+ if (parseData(message, path, value, ts)) {
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: got notification " << path << " = " << value << std::endl;
+
+ handle_notification(path, value, ts);
+ }
+ } else {
+ std::cerr << "unhandled VIS response of type: " << action;
+ }
+
+ if (m_config.verbose() > 1)
+ std::cout << "VisSession::handle_message: exit" << std::endl;
+}
+
diff --git a/src/vis-session.hpp b/src/vis-session.hpp
new file mode 100644
index 0000000..8c7b0d9
--- /dev/null
+++ b/src/vis-session.hpp
@@ -0,0 +1,78 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _VIS_SESSION_HPP
+#define _VIS_SESSION_HPP
+
+#include "vis-config.hpp"
+#include <atomic>
+#include <string>
+#include <boost/beast/core.hpp>
+#include <boost/beast/ssl.hpp>
+#include <boost/beast/websocket.hpp>
+#include <boost/beast/websocket/ssl.hpp>
+#include <boost/asio/strand.hpp>
+#include <nlohmann/json.hpp>
+
+namespace beast = boost::beast;
+namespace websocket = beast::websocket;
+namespace net = boost::asio;
+namespace ssl = boost::asio::ssl;
+using tcp = boost::asio::ip::tcp;
+using json = nlohmann::json;
+
+
+class VisSession : public std::enable_shared_from_this<VisSession>
+{
+ //net::io_context m_ioc;
+ tcp::resolver m_resolver;
+ tcp::resolver::results_type m_results;
+ std::string m_hostname;
+ websocket::stream<beast::ssl_stream<beast::tcp_stream>> m_ws;
+ beast::flat_buffer m_buffer;
+
+public:
+ // Resolver and socket require an io_context
+ explicit VisSession(const VisConfig &config, net::io_context& ioc, ssl::context& ctx);
+
+ // Start the asynchronous operation
+ void run();
+
+protected:
+ VisConfig m_config;
+ std::atomic_uint m_requestid;
+
+ void on_resolve(beast::error_code error, tcp::resolver::results_type results);
+
+ void connect();
+
+ void on_connect(beast::error_code error, tcp::resolver::results_type::endpoint_type endpoint);
+
+ void on_ssl_handshake(beast::error_code error);
+
+ void on_handshake(beast::error_code error);
+
+ void on_authorize(beast::error_code error, std::size_t bytes_transferred);
+
+ void on_write(beast::error_code error, std::size_t bytes_transferred);
+
+ void on_read(beast::error_code error, std::size_t bytes_transferred);
+
+ void get(const std::string &path);
+
+ void set(const std::string &path, const std::string &value);
+
+ void subscribe(const std::string &path);
+
+ void handle_message(const json &message);
+
+ bool parseData(const json &message, std::string &path, std::string &value, std::string &timestamp);
+
+ virtual void handle_authorized_response(void) = 0;
+
+ virtual void handle_get_response(std::string &path, std::string &value, std::string &timestamp) = 0;
+
+ virtual void handle_notification(std::string &path, std::string &value, std::string &timestamp) = 0;
+
+};
+
+#endif // _VIS_SESSION_HPP
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)