diff options
26 files changed, 914 insertions, 1042 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index f757721..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,3 +0,0 @@ -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 c199ad2..0000000 --- a/binding/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -PROJECT_TARGET_ADD(audiomixer-binding) - - add_definitions(-DAFB_BINDING_VERSION=3) - - set(audiomixer_SOURCES - audiomixer-binding.c - audiomixer.c - ) - - add_library(${TARGET_NAME} MODULE ${audiomixer_SOURCES}) - - SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES - PREFIX "libafm-" - LABELS "BINDING" - LINK_FLAGS ${BINDINGS_LINK_FLAG} - OUTPUT_NAME ${TARGET_NAME} - ) - - TARGET_LINK_LIBRARIES(${TARGET_NAME} ${link_libraries}) diff --git a/binding/audiomixer-binding.c b/binding/audiomixer-binding.c deleted file mode 100644 index 2c8981a..0000000 --- a/binding/audiomixer-binding.c +++ /dev/null @@ -1,402 +0,0 @@ -/* - * Copyright © 2019 Collabora Ltd. - * Copyright © 2019 Konsulko Group - * @author George Kiagiadakis <george.kiagiadakis@collabora.com> - * - * SPDX-License-Identifier: MIT - */ - -#include <string.h> -#include <json-c/json.h> -#include <afb/afb-binding.h> -#include <systemd/sd-event.h> -#include "audiomixer.h" - -static struct audiomixer *audiomixer; -static afb_event_t controls_changed; -static afb_event_t volume_changed; -static afb_event_t mute_changed; - -static const char *signalcomposer_events[] = { - "event.volume.up", - "event.volume.down", - "event.volume.mute", - NULL, -}; - -static void -audiomixer_controls_changed_deferred(int signum, void *arg) -{ - AFB_DEBUG("controls changed"); - afb_event_push(controls_changed, NULL); -} - -struct value_changed_data -{ - unsigned int change_mask; - struct mixer_control control; -}; - -static void -audiomixer_value_changed_deferred(int signum, void *data) -{ - struct value_changed_data *d = data; - json_object *json; - - AFB_DEBUG("value changed"); - - json = json_object_new_object(); - json_object_object_add(json, "control", - json_object_new_string(d->control.name)); - - if (d->change_mask & MIXER_CONTROL_CHANGE_FLAG_VOLUME) { - json_object_object_add(json, "value", - json_object_new_double(d->control.volume)); - afb_event_push(volume_changed, json); - } else if (d->change_mask & MIXER_CONTROL_CHANGE_FLAG_MUTE) { - json_object_object_add(json, "value", - json_object_new_int(d->control.mute)); - afb_event_push(mute_changed, json); - } - - free(d); -} - -/* called in audiomixer's thread */ -static void -audiomixer_controls_changed(void *data) -{ - afb_api_t api = data; - afb_api_queue_job (api, audiomixer_controls_changed_deferred, NULL, - (void *) 0x1, 0); -} - - -/* called in audiomixer's thread */ -static void -audiomixer_value_changed(void *data, - unsigned int change_mask, - const struct mixer_control *control) -{ - afb_api_t api = data; - struct value_changed_data *d = calloc(1, sizeof(*d)); - - d->change_mask = change_mask; - d->control = *control; - - afb_api_queue_job (api, audiomixer_value_changed_deferred, d, - (void *) 0x1, 0); -} - -static const struct audiomixer_events audiomixer_events = { - .controls_changed = audiomixer_controls_changed, - .value_changed = audiomixer_value_changed, -}; - -static int -cleanup(sd_event_source *s, void *data) -{ - audiomixer_free(audiomixer); - audiomixer = NULL; - return 0; -} - -static int -init(afb_api_t api) -{ - int ret; - - ret = afb_daemon_require_api("signal-composer", 1); - if (ret) { - AFB_WARNING("unable to initialize signal-composer binding"); - } else { - const char **tmp = signalcomposer_events; - json_object *args = json_object_new_object(); - json_object *signals = json_object_new_array(); - - while (*tmp) { - json_object_array_add(signals, json_object_new_string(*tmp++)); - } - json_object_object_add(args, "signal", signals); - if(json_object_array_length(signals)) { - afb_api_call_sync(api, "signal-composer", "subscribe", - args, NULL, NULL, NULL); - } else { - json_object_put(args); - } - } - - sd_event *e = afb_daemon_get_event_loop(); - - controls_changed = afb_api_make_event(api, "controls_changed"); - volume_changed = afb_api_make_event(api, "volume_changed"); - mute_changed = afb_api_make_event(api, "mute_changed"); - - audiomixer = audiomixer_new(); - sd_event_add_exit(e, NULL, cleanup, NULL); - - audiomixer_add_event_listener(audiomixer, &audiomixer_events, api); - - return 0; -} - -static void -list_controls_cb(afb_req_t request) -{ - json_object *ret_json, *nest_json; - const struct mixer_control **ctls; - unsigned int n_controls, i; - - audiomixer_lock(audiomixer); - - if (audiomixer_ensure_controls(audiomixer, 3) < 0) { - AFB_REQ_NOTICE(request, "No mixer controls were exposed " - "in PipeWire after 3 seconds"); - } - - ctls = audiomixer_get_active_controls(audiomixer, &n_controls); - - ret_json = json_object_new_array(); - for (i = 0; i < n_controls; i++) { - nest_json = json_object_new_object(); - json_object_object_add(nest_json, "control", - json_object_new_string(ctls[i]->name)); - json_object_object_add(nest_json, "volume", - json_object_new_double(ctls[i]->volume)); - json_object_object_add(nest_json, "mute", - json_object_new_int(ctls[i]->mute)); - json_object_array_add(ret_json, nest_json); - } - afb_req_success(request, ret_json, NULL); - - audiomixer_unlock(audiomixer); -} - -static void -volume_cb(afb_req_t request) -{ - json_object *ret_json; - const char *control = afb_req_value(request, "control"); - const char *value = afb_req_value(request, "value"); - const struct mixer_control *ctl; - double volume; - - audiomixer_lock(audiomixer); - - if (!control) { - afb_req_fail(request, "failed", - "Invalid arguments: missing 'control'"); - goto unlock; - } - - if (audiomixer_ensure_controls(audiomixer, 3) < 0) { - AFB_REQ_NOTICE(request, "No mixer controls were exposed " - "in PipeWire after 3 seconds"); - } - - ctl = audiomixer_find_control(audiomixer, control); - if (!ctl) { - afb_req_fail(request, "failed", "Could not find control"); - goto unlock; - } - - if(value) { - char *endptr; - volume = strtod(value, &endptr); - if (endptr == value || volume < -0.00001 || volume > 1.00001) { - afb_req_fail(request, "failed", - "Invalid volume value (must be between 0.0 and 1.0)"); - goto unlock; - } - - audiomixer_change_volume(audiomixer, ctl, volume); - } else { - volume = ctl->volume; - } - - ret_json = json_object_new_object(); - json_object_object_add(ret_json, "volume", json_object_new_double(volume)); - afb_req_success(request, ret_json, NULL); - -unlock: - audiomixer_unlock(audiomixer); -} - -static void -mute_cb(afb_req_t request) -{ - json_object *ret_json; - const char *control = afb_req_value(request, "control"); - const char *value = afb_req_value(request, "value"); - const struct mixer_control *ctl; - int mute; - - audiomixer_lock(audiomixer); - - if (!control) { - afb_req_fail(request, "failed", - "Invalid arguments: missing 'control'"); - goto unlock; - } - - if (audiomixer_ensure_controls(audiomixer, 3) < 0) { - AFB_REQ_NOTICE(request, "No mixer controls were exposed " - "in PipeWire after 3 seconds"); - } - - ctl = audiomixer_find_control(audiomixer, control); - if (!ctl) { - afb_req_fail(request, "failed", "Could not find control"); - goto unlock; - } - - if(value) { - char *endptr; - mute = (int) strtol(value, &endptr, 10); - if (endptr == value || mute < 0 || mute > 1) { - afb_req_fail(request, "failed", - "Invalid mute value (must be integer 0 or 1)"); - goto unlock; - } - - audiomixer_change_mute(audiomixer, ctl, mute); - } else { - mute = ctl->mute; - } - - ret_json = json_object_new_object(); - json_object_object_add(ret_json, "mute", json_object_new_int(mute)); - afb_req_success(request, ret_json, NULL); - -unlock: - audiomixer_unlock(audiomixer); -} - -static void -subscribe_cb(afb_req_t request) -{ - const char *eventstr = afb_req_value(request, "event"); - afb_event_t event; - - if (!eventstr) { - afb_req_fail(request, "failed", - "Invalid arguments: missing 'event'"); - return; - } - - if (!strcmp(eventstr, "controls_changed")) - event = controls_changed; - else if (!strcmp(eventstr, "volume_changed")) - event = volume_changed; - else if (!strcmp(eventstr, "mute_changed")) - event = mute_changed; - else { - afb_req_fail(request, "failed", "Invalid event name"); - return; - } - - if (afb_req_subscribe(request, event) != 0) - afb_req_fail(request, "failed", "Failed to subscribe to event"); - else - afb_req_success(request, NULL, "Subscribed"); -} - -static void -unsubscribe_cb(afb_req_t request) -{ - const char *eventstr = afb_req_value(request, "event"); - afb_event_t event; - - if (!eventstr) { - afb_req_fail(request, "failed", - "Invalid arguments: missing 'event'"); - return; - } - - if (!strcmp(eventstr, "controls_changed")) - event = controls_changed; - else if (!strcmp(eventstr, "volume_changed")) - event = volume_changed; - else if (!strcmp(eventstr, "mute_changed")) - event = mute_changed; - else { - afb_req_fail(request, "failed", "Invalid event name"); - return; - } - - if (afb_req_unsubscribe(request, event) != 0) - afb_req_fail(request, "failed", "Failed to unsubscribe from event"); - else - afb_req_success(request, NULL, "Unsubscribed"); -} - -static void -onevent(afb_api_t api, const char *event, struct json_object *object) -{ - const struct mixer_control *ctl; - json_object *tmp = NULL; - const char *uid; - const char *value; - - json_object_object_get_ex(object, "uid", &tmp); - if (tmp == NULL) - return; - - uid = json_object_get_string(tmp); - if (strncmp(uid, "event.volume.", 13)) - return; - - json_object_object_get_ex(object, "value", &tmp); - if (tmp == NULL) - return; - - value = json_object_get_string(tmp); - if (strncmp(value, "true", 4)) - return; - - audiomixer_lock(audiomixer); - - ctl = audiomixer_find_control(audiomixer, "Master Playback"); - if (!ctl) - goto unlock; - - if (!strcmp(uid, "event.volume.mute")) { - audiomixer_change_mute(audiomixer, ctl, !ctl->mute); - } else { - double volume = ctl->volume; - - if (!strcmp(uid, "event.volume.up")) { - volume += 0.05; // up 5% - if (volume > 1.0) - volume = 1.0; // clamp to 100% - } else if (!strcmp(uid, "event.volume.down")) { - volume -= 0.05; // down 5% - if (volume < 0.0) - volume = 0.0; // clamp to 0% - } else { - AFB_WARNING("Unhandled signal-composer uid '%s'", uid); - goto unlock; - } - audiomixer_change_volume(audiomixer, ctl, volume); - } - -unlock: - audiomixer_unlock(audiomixer); -} - -static const afb_verb_t verbs[]= { - { .verb = "list_controls", .callback = list_controls_cb, .info = "List the available controls" }, - { .verb = "volume", .callback = volume_cb, .info = "Get/Set volume" }, - { .verb = "mute", .callback = mute_cb, .info = "Get/Set mute" }, - { .verb = "subscribe", .callback = subscribe_cb, .info = "Subscribe to mixer events" }, - { .verb = "unsubscribe", .callback = unsubscribe_cb, .info = "Unsubscribe from mixer events" }, - { } -}; - -const afb_binding_t afbBindingV3 = { - .api = "audiomixer", - .specification = "AudioMixer API", - .verbs = verbs, - .onevent = onevent, - .init = init, -}; diff --git a/conf.d/cmake/config.cmake b/conf.d/cmake/config.cmake deleted file mode 100644 index 09d91ae..0000000 --- a/conf.d/cmake/config.cmake +++ /dev/null @@ -1,160 +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-audiomixer) -set(PROJECT_PRETTY_NAME "Audio mixer binding service") -set(PROJECT_DESCRIPTION "Expose PipeWire mixer controls through the AGL Framework") -set(PROJECT_VERSION "0.1") -set(PROJECT_URL https://gerrit.automotivelinux.org/gerrit/admin/repos/apps/agl-service-audiomixer) -set(PROJECT_ICON "icon.png") -set(PROJECT_AUTHOR "George Kiagiadakis") -set(PROJECT_AUTHOR_MAIL "george.kiagiadakis@collabora.com") -set(PROJECT_LICENSE "MIT") -set(PROJECT_LANGUAGES,"C") -set(API_NAME "audiomixer") - -# 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 - libpipewire-0.3 - wireplumber-0.4 -) - -# Static constante definition -# ----------------------------- -add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-pthread>) - -# Customize link option -# ----------------------------- -list (APPEND link_libraries -pthread) - -# --------------------------------------------------------------------- -set(INSTALL_PREFIX $ENV{HOME}/opt) - -# 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-audiomixer-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 056c64b..0000000 --- a/conf.d/wgt/config.xml.in +++ /dev/null @@ -1,27 +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" /> - </feature> - - <feature name="urn:AGL:widget:provided-api"> - <param name="audiomixer" value="ws" /> - </feature> - - <feature name="urn:AGL:widget:required-binding"> - <param name="@WIDGET_ENTRY_POINT@" value="local" /> - </feature> - - <feature name="urn:AGL:widget:required-api"> - <param name="signal-composer" value="ws" /> - </feature> -</widget> diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..47e88a4 --- /dev/null +++ b/meson.build @@ -0,0 +1,10 @@ +project('agl-service-audiomixer', + ['cpp', 'c'], + 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/audiomixer-service.cpp b/src/audiomixer-service.cpp new file mode 100644 index 0000000..5787153 --- /dev/null +++ b/src/audiomixer-service.cpp @@ -0,0 +1,129 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "audiomixer-service.hpp" +#include <iostream> +#include <algorithm> + + +AudiomixerService::AudiomixerService(const VisConfig &config, net::io_context& ioc, ssl::context& ctx) : + VisSession(config, ioc, ctx) +{ + m_audiomixer = audiomixer_new(); + if (m_audiomixer) { + // Set up callbacks for WirePlumber events + m_audiomixer_events.controls_changed = audiomixer_control_change_cb; + m_audiomixer_events.value_changed = audiomixer_value_change_cb; + audiomixer_add_event_listener(m_audiomixer, &m_audiomixer_events, this); + + // Drive connecting to PipeWire core and refreshing controls list + audiomixer_lock(m_audiomixer); + audiomixer_ensure_controls(m_audiomixer, 3); + audiomixer_unlock(m_audiomixer); + } else { + std::cerr << "Could not create WirePlumber connection" << std::endl; + } +} + +AudiomixerService::~AudiomixerService() +{ + audiomixer_free(m_audiomixer); +} + +void AudiomixerService::handle_authorized_response(void) +{ + subscribe("Vehicle.Cabin.Infotainment.Media.Volume"); + subscribe("Vehicle.Cabin.SteeringWheel.Switches.VolumeUp"); + subscribe("Vehicle.Cabin.SteeringWheel.Switches.VolumeDown"); + subscribe("Vehicle.Cabin.SteeringWheel.Switches.VolumeMute"); + + // Set initial volume in VSS + // For now a value of 50 matches the default in the homescreen app. + // Ideally there would be some form of persistence scheme to restore + // the last value on restart. + set("Vehicle.Cabin.Infotainment.Media.Volume", "50"); +} + +void AudiomixerService::handle_get_response(std::string &path, std::string &value, std::string ×tamp) +{ + // Placeholder since no gets are performed ATM +} + +void AudiomixerService::handle_notification(std::string &path, std::string &value, std::string ×tamp) +{ + if (!m_audiomixer) { + return; + } + + audiomixer_lock(m_audiomixer); + + const struct mixer_control *ctl = audiomixer_find_control(m_audiomixer, "Master Playback"); + if (!ctl) { + audiomixer_unlock(m_audiomixer); + return; + } + + if (path == "Vehicle.Cabin.Infotainment.Media.Volume") { + try { + int volume = std::stoi(value); + if (volume >= 0 && volume <= 100) { + double v = (double) volume / 100.0; + if (m_config.verbose() > 1) + std::cout << "Setting volume to " << v << std::endl; + audiomixer_change_volume(m_audiomixer, ctl, v); + } + } + catch (std::exception ex) { + // ignore bad value + } + } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeUp" && value == "true") { + double volume = ctl->volume; + volume += 0.05; // up 5% + if (volume > 1.0) + volume = 1.0; // clamp to 100% + if (m_config.verbose() > 1) + std::cout << "Increasing volume to " << volume << std::endl; + audiomixer_change_volume(m_audiomixer, ctl, volume); + + } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeDown" && value == "true") { + double volume = ctl->volume; + volume -= 0.05; // down 5% + if (volume < 0.0) + volume = 0.0; // clamp to 0% + if (m_config.verbose() > 1) + std::cout << "Decreasing volume to " << volume << std::endl; + audiomixer_change_volume(m_audiomixer, ctl, volume); + + } else if (path == "Vehicle.Cabin.SteeringWheel.Switches.VolumeMute" && value == "true") { + if (m_config.verbose() > 1) { + if (ctl->mute) + std::cout << "Unmuting" << std::endl; + else + std::cout << "Muting" << std::endl; + } + audiomixer_change_mute(m_audiomixer, ctl, !ctl->mute); + } + // else ignore + + audiomixer_unlock(m_audiomixer); +} + +void AudiomixerService::handle_control_change(void) +{ + // Ignore for now +} + +void AudiomixerService::handle_value_change(unsigned int change_mask, const struct mixer_control *control) +{ + if (!control) + return; + + if (change_mask & MIXER_CONTROL_CHANGE_FLAG_VOLUME) { + if (std::string(control->name) == "Master Playback") { + // Push change into VIS + std::string value = std::to_string((int) (control->volume * 100.0)); + set("Vehicle.Cabin.Infotainment.Media.Volume", value); + } + } else if (change_mask & MIXER_CONTROL_CHANGE_FLAG_MUTE) { + // For now, do nothing, new state is in control->mute + } +} diff --git a/src/audiomixer-service.hpp b/src/audiomixer-service.hpp new file mode 100644 index 0000000..cb00584 --- /dev/null +++ b/src/audiomixer-service.hpp @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 + +#ifndef _AUDIOMIXER_SERVICE_HPP +#define _AUDIOMIXER_SERVICE_HPP + +#include "vis-session.hpp" +#include "audiomixer.h" + +class AudiomixerService : public VisSession +{ + struct audiomixer *m_audiomixer; + +public: + AudiomixerService(const VisConfig &config, net::io_context& ioc, ssl::context& ctx); + + ~AudiomixerService(); + + static void audiomixer_control_change_cb(void *data) { + if (data) + ((AudiomixerService*) data)->handle_control_change(); + }; + + static void audiomixer_value_change_cb(void *data, + unsigned int change_mask, + const struct mixer_control *control) { + if (data) + ((AudiomixerService*) data)->handle_value_change(change_mask, control); + } + +protected: + struct audiomixer_events m_audiomixer_events; + + 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; + + virtual void handle_control_change(void); + + virtual void handle_value_change(unsigned int change_mask, const struct mixer_control *control); +}; + +#endif // _AUDIOMIXER_SERVICE_HPP diff --git a/binding/audiomixer.c b/src/audiomixer.c index 97ad622..a40a38e 100644 --- a/binding/audiomixer.c +++ b/src/audiomixer.c @@ -433,7 +433,7 @@ do_change_mute (struct action * action) gboolean ret = FALSE; g_variant_builder_add (&b, "{sv}", "mute", - g_variant_new_double (action->change_mute.mute)); + g_variant_new_boolean (action->change_mute.mute)); g_signal_emit_by_name (self->mixer_api, "set-volume", action->change_mute.id, g_variant_builder_end (&b), &ret); if (!ret) diff --git a/binding/audiomixer.h b/src/audiomixer.h index 47bd703..cc67a83 100644 --- a/binding/audiomixer.h +++ b/src/audiomixer.h @@ -5,6 +5,13 @@ * SPDX-License-Identifier: MIT */ +#ifndef _AUDIOMIXER_H +#define _AUDIOMIXER_H + +#ifdef __cplusplus +extern "C" { +#endif + #include <stdbool.h> struct audiomixer; @@ -57,3 +64,8 @@ void audiomixer_change_mute(struct audiomixer *self, const struct mixer_control *control, bool mute); +#ifdef __cplusplus +} +#endif + +#endif // _AUDIOMIXER_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..0960f1b --- /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 "audiomixer-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-audiomixer"); + std::make_shared<AudiomixerService>(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..282d130 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,18 @@ +boost_dep = dependency('boost', + version : '>=1.72', + modules : [ 'thread', 'filesystem', 'program_options', 'log', 'system' ]) +openssl_dep = dependency('openssl') +thread_dep = dependency('threads') +wp_dep = dependency('wireplumber-0.4') + +src = [ 'vis-config.cpp', + 'vis-session.cpp', + 'audiomixer-service.cpp', + 'audiomixer.c', + 'main.cpp' +] +executable('agl-service-audiomixer', + src, + dependencies: [boost_dep, openssl_dep, thread_dep, systemd_dep, wp_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-audiomixer.service b/systemd/agl-service-audiomixer.service new file mode 100644 index 0000000..b6603fa --- /dev/null +++ b/systemd/agl-service-audiomixer.service @@ -0,0 +1,11 @@ +[Unit] +Requires=kuksa-val.service +After=kuksa-val.service + +[Service] +Type=simple +ExecStart=/usr/sbin/agl-service-audiomixer +Restart=on-failure + +[Install] +WantedBy=default.target diff --git a/systemd/meson.build b/systemd/meson.build new file mode 100644 index 0000000..c94ab80 --- /dev/null +++ b/systemd/meson.build @@ -0,0 +1,3 @@ +systemd_system_unit_dir = systemd_dep.get_pkgconfig_variable('systemdsystemunitdir') + +install_data('agl-service-audiomixer.service', install_dir : systemd_system_unit_dir) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt deleted file mode 100644 index 8d8fbdc..0000000 --- a/test/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -########################################################################### -# Copyright 2020 Fujitsu -# -# author: Li Xiaoming <lixm.fnst@cn.fujitsu.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. -########################################################################### - - -# Include any directory not starting with _ -# ----------------------------------------------------- -PROJECT_SUBDIRS_ADD(${PROJECT_SRC_DIR_PATTERN}) - -ADD_TEST(NAME AGL_SERVICE_GPS_TESTS - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - COMMAND afm-test ${CMAKE_BINARY_DIR}/package ${CMAKE_BINARY_DIR}/package-test - ) - diff --git a/test/afb-test/CMakeLists.txt b/test/afb-test/CMakeLists.txt deleted file mode 100644 index 4adafb0..0000000 --- a/test/afb-test/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -########################################################################### -# Copyright 2020 Fujitsu -# -# author: Li Xiaoming <lixm.fnst@cn.fujitsu.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. -########################################################################### - -# Include any directory not starting with _ -# ----------------------------------------------------- -PROJECT_SUBDIRS_ADD(${PROJECT_SRC_DIR_PATTERN}) diff --git a/test/afb-test/etc/CMakeLists.txt b/test/afb-test/etc/CMakeLists.txt deleted file mode 100644 index 12e2efc..0000000 --- a/test/afb-test/etc/CMakeLists.txt +++ /dev/null @@ -1,32 +0,0 @@ -########################################################################### -# Copyright 2020 Fujitsu -# -# author: Li Xiaoming <lixm.fnst@cn.fujitsu.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. -########################################################################### - -################################################## -# audiomixer test configuration files -################################################## -PROJECT_TARGET_ADD(afb-test-config) - - file(GLOB CONF_FILES "*.json") - - add_input_files("${CONF_FILES}") - - SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES - LABELS "TEST-CONFIG" - OUTPUT_NAME ${TARGET_NAME} - ) - diff --git a/test/afb-test/etc/aft-agl-audiomixer.json b/test/afb-test/etc/aft-agl-audiomixer.json deleted file mode 100644 index c1e1c96..0000000 --- a/test/afb-test/etc/aft-agl-audiomixer.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "id": "http://iot.bzh/download/public/schema/json/ctl-schema.json#", - "": "http://iot.bzh/download/public/schema/json/ctl-schema.json#", - "metadata": { - "uid": "Test", - "version": "1.0", - "api": "aft-audiomixer", - "info": "AFB-test binding configuration file to test audiomixer api.", - "require": [ - "audiomixer" - ] - }, - "testVerb": { - "uid": "launch_all_tests", - "info": "Launch all the tests", - "action": "lua://AFT#_launch_test", - "args": { - "trace": "audiomixer", - "files": ["audiomixer.lua"] - } - } -} diff --git a/test/afb-test/tests/CMakeLists.txt b/test/afb-test/tests/CMakeLists.txt deleted file mode 100644 index 4153151..0000000 --- a/test/afb-test/tests/CMakeLists.txt +++ /dev/null @@ -1,31 +0,0 @@ -########################################################################### -# Copyright 2020 Fujitsu -# -# author:Li Xiaoming <lixm.fnst@cn.fujitsu.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. -########################################################################### - -################################################## -# audiomixer Lua Scripts -################################################## -PROJECT_TARGET_ADD(test-files) - - file(GLOB LUA_FILES "*.lua" "*.sh") - add_input_files("${LUA_FILES}") - - SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES - LABELS "TEST-DATA" - OUTPUT_NAME ${TARGET_NAME} - ) - diff --git a/test/afb-test/tests/audiomixer.lua b/test/afb-test/tests/audiomixer.lua deleted file mode 100644 index a978508..0000000 --- a/test/afb-test/tests/audiomixer.lua +++ /dev/null @@ -1,40 +0,0 @@ ---[[ - Copyright 2020 Fujitsu - - author: Li Xiaoming <lixm.fnst@cn.fujitsu.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. ---]] - - - -_AFT.testVerbStatusSuccess("testListControlSuccess","audiomixer","list_controls", {}) - -_AFT.testVerbStatusSuccess("testMuteChange0Success","audiomixer","mute", {control="Master", value="0"}) -_AFT.testVerbStatusSuccess("testMuteChange1Success","audiomixer","mute", {control="Master", value="1"}) -_AFT.testVerbStatusSuccess("testMuteNotChangeSuccess","audiomixer","mute", {control="Master"}) - -_AFT.testVerbStatusSuccess("testVolumeChangeSuccess","audiomixer","volume", {control="Master", value="0.5"}) -_AFT.testVerbStatusSuccess("testVolumeNotChangeSuccess","audiomixer","volume", {control="Master"}) - -_AFT.testVerbStatusSuccess("testSubscribeControls_changedSuccess","audiomixer","subscribe", {event="controls_changed"}) -_AFT.testVerbStatusSuccess("testSubscribeVolume_changedSuccess","audiomixer","subscribe", {event="volume_changed"}) -_AFT.testVerbStatusSuccess("testSubscribeMute_changedSuccess","audiomixer","subscribe", {event="mute_changed"}) - -_AFT.testVerbStatusSuccess("testUnSubscribeControls_changedSuccess","audiomixer","unsubscribe", {event="controls_changed"}) -_AFT.testVerbStatusSuccess("testUnSubscribeVolume_changedSuccess","audiomixer","unsubscribe", {event="volume_changed"}) -_AFT.testVerbStatusSuccess("testUnSubscribeMute_changedSuccess","audiomixer","unsubscribe", {event="mute_changed"}) - - - - |