/* * Copyright (C) 2018 "IoT.bzh" * Author Loïc Collignon * * 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 #include "ahl-api.hpp" #include "ahl-binding.hpp" /** * @brief Default constructor. */ ahl_binding_t::ahl_binding_t() : handle_{nullptr} { } /** * @brief Creates static verbs. */ void ahl_binding_t::load_static_verbs() { if (afb_api_add_verb( handle_, "get_roles", "Retrieve array of available audio roles", ahl_api_get_roles, nullptr, nullptr, AFB_SESSION_NONE_X2, 0)) { throw std::runtime_error("Failed to add 'get_role' verb to the API."); } if (afb_api_add_verb( handle_, "activerole", "Controls the currently active role", ahl_api_activerole, nullptr, nullptr, AFB_SESSION_NONE_X2, 0)) { throw std::runtime_error("Failed to add 'activerole' verb to the API."); } if (afb_api_add_verb( handle_, "subscribe", "Subscribe to \"volume_changed\" event", ahl_api_subscribe, nullptr, nullptr, AFB_SESSION_NONE_X2, 0)) { throw std::runtime_error("Failed to add 'subscribe' verb to the API."); } if (afb_api_add_verb( handle_, "unsubscribe", "Unsubscribe to \"volume_changed\" event", ahl_api_unsubscribe, nullptr, nullptr, AFB_SESSION_NONE_X2, 0)) { throw std::runtime_error("Failed to add 'unsubscribe' verb to the API."); } } /** * @brief Find all controller's configuration files. */ void ahl_binding_t::load_controller_configs() { char* dir_list = getenv("CONTROL_CONFIG_PATH"); if (!dir_list) dir_list = strdup(CONTROL_CONFIG_PATH); struct json_object* config_files = CtlConfigScan(dir_list, "policy"); if (!config_files) throw std::runtime_error("No config files found!"); // Only one file should be found this way, but read all just in case size_t config_files_count = json_object_array_length(config_files); for(int i = 0; i < config_files_count; ++i) { config_entry_t file {json_object_array_get_idx(config_files, i)}; if(load_controller_config(file.filepath()) < 0) { std::stringstream ss; ss << "Failed to load config file '" << file.filename() << "' from '" << file.fullpath() << "'!"; throw std::runtime_error(ss.str()); } } } /** * @brief Load a controller's configuration. * @param[in] path File to load. * @return Zero on success, non-zero otherwise. */ int ahl_binding_t::load_controller_config(const std::string& path) { CtlConfigT* controller_config; controller_config = CtlLoadMetaData(handle_, path.c_str()); if (!controller_config) { AFB_API_ERROR(handle_, "Failed to load controller from config file!"); return -1; } static CtlSectionT controller_sections[] = { {.key = "plugins", .uid = nullptr, .info = nullptr, .prefix = nullptr, .loadCB = PluginConfig, .handle = nullptr, .actions = nullptr}, {.key = "onload", .uid = nullptr, .info = nullptr, .prefix = nullptr, .loadCB = OnloadConfig, .handle = nullptr, .actions = nullptr}, {.key = "controls", .uid = nullptr, .info = nullptr, .prefix = nullptr, .loadCB = ControlConfig, .handle = nullptr, .actions = nullptr}, {.key = "events", .uid = nullptr, .info = nullptr, .prefix = nullptr, .loadCB = EventConfig, .handle = nullptr, .actions = nullptr}, {.key = "roles", .uid = nullptr, .info = nullptr, .prefix = nullptr, .loadCB = ahl_api_config_roles, .handle = nullptr, .actions = nullptr }, {.key = nullptr, .uid = nullptr, .info = nullptr, .prefix = nullptr, .loadCB = nullptr, .handle = nullptr, .actions = nullptr} }; CtlLoadSections(handle_, controller_config, controller_sections); return 0; } /** * @brief Update audio roles definition by binding to streams. * @return Status code, zero if success. */ int ahl_binding_t::update_streams() { json_object* loaded = nullptr; size_t hals_count = 0, streams_count = 0; if (afb_api_call_sync(handle_, "4a-hal-manager", "loaded", json_object_new_object(), &loaded, nullptr, nullptr)) { AFB_API_ERROR(handle_, "Failed to call 'loaded' verb on '4a-hal-manager' API!"); if (loaded) { AFB_API_ERROR(handle_, "%s", json_object_to_json_string(loaded)); json_object_put(loaded); } return -1; } if (!json_object_is_type(loaded, json_type_array)) { AFB_API_ERROR(handle_, "Expected an array from '4a-hal-manager/loaded', but got something else!"); json_object_put(loaded); return -1; } hals_count = json_object_array_length(loaded); for(int i = 0; i < hals_count; ++i) { json_object* info = nullptr; const char* halname = json_object_get_string(json_object_array_get_idx(loaded, i)); AFB_API_DEBUG(handle_, "Found an active HAL: %s", halname); if (afb_api_call_sync(handle_, halname, "info", json_object_new_object(), &info, nullptr, nullptr)) { AFB_API_ERROR(handle_, "Failed to call 'info' verb on '%s' API!", halname); if (info) { AFB_API_ERROR(handle_, "%s", json_object_to_json_string(info)); json_object_put(info); } json_object_put(loaded); return -1; } json_object* streamsJ = nullptr; json_object_object_get_ex(info, "streams", &streamsJ); streams_count = json_object_array_length(streamsJ); for(int j = 0; j < streams_count; ++j) { json_object * nameJ = nullptr, * cardIdJ = nullptr; json_object * streamJ = json_object_array_get_idx(streamsJ, j); json_object_object_get_ex(streamJ, "name", &nameJ); json_object_object_get_ex(streamJ, "cardId", &cardIdJ); update_stream( halname, json_object_get_string(nameJ), json_object_get_string(cardIdJ) ); } json_object_put(info); } json_object_put(loaded); return 0; } /** * @brief Update the stream info for audio roles. * @param[in] halname The hal on which the stream is. * @param[in] stream The name of the stream. * @param[in] deviceid The device ID to return when opening an audio role. */ void ahl_binding_t::update_stream(std::string halname, std::string stream, std::string deviceid) { for(auto& r : roles_) { if(r.stream() == stream) { if (r.device_uri().size()) AFB_API_WARNING(handle_, "Multiple stream with same name: '%s'.", stream.c_str()); else { r.device_uri(deviceid); r.hal(halname); } } } } /** * @brief Create a verb based on a role. * @param[in] r Role to create a verb for. * @return Zero on success, non-zero otherwise. */ int ahl_binding_t::create_api_verb(role_t* r) { AFB_API_NOTICE(handle_, "New audio role: %s", r->uid().c_str()); if (afb_api_add_verb( handle_, r->uid().c_str(), r->description().c_str(), ahl_api_role, r, nullptr, AFB_SESSION_NONE_X2, 0)) { AFB_API_ERROR(handle_, "Failed to add '%s' verb to the API.", r->uid().c_str()); return -1; } return 0; } role_t* ahl_binding_t::get_active_role() { role_t* active = nullptr; for(auto& r : roles_) { if (r.opened() && (active == nullptr || r.priority() > active->priority())) active = &r; } return active; } /** * @brief Get the singleton instance. * @return The unique instance. */ ahl_binding_t& ahl_binding_t::instance() { static ahl_binding_t s; return s; } /** * @brief Get the api handle. * @return The api handle. */ afb_api_t ahl_binding_t::handle() const { return handle_; } /** * @brief Get the role list. * @return A vector of roles. */ const std::vector ahl_binding_t::roles() const { return roles_; } /** * @brief This method is called during the pre-init phase of loading the binding. * @param[in] handle Handle to the api. * @return Status code, zero if success. */ int ahl_binding_t::preinit(afb_api_t handle) { handle_ = handle; try { load_static_verbs(); load_controller_configs(); if (afb_api_on_event(handle_, ahl_api_on_event)) throw std::runtime_error("Failed to register event handler callback."); if (afb_api_on_init(handle_, ahl_api_init)) throw std::runtime_error("Failed to register init handler callback."); } catch(std::exception& e) { AFB_API_ERROR(handle, "%s", e.what()); return -1; } return 0; } /** * @brief Initialize the API. */ int ahl_binding_t::init() { using namespace std::placeholders; if (afb_api_require_api(handle_, HAL_MGR_API, 1)) { AFB_API_ERROR(handle_, "Failed to require '%s' API!", HAL_MGR_API); return -1; } AFB_API_NOTICE(handle_, "Required '%s' API found!", HAL_MGR_API); afb_api_seal(handle_); AFB_API_NOTICE(handle_, "API is now sealed!"); volume_changed_ = afb_api_make_event(handle_, "volume_changed"); if(!afb_event_is_valid(volume_changed_)) { AFB_API_ERROR(handle_, "Failed to create the \"volume_changed\" event!"); return -2; } if (update_streams()) return -1; return 0; } /** * @brief Called when an event is received by this binding. * @param[in] name Event's name. * @param[in] arg Event's argument. */ void ahl_binding_t::event(std::string name, json_object* arg) { AFB_API_DEBUG(handle_, "Event '%s' received with the following arg: %s", name.c_str(), json_object_to_json_string(arg)); } /** * @brief Emit the 'volume-changed' event. * @param[in] role Role's uid. * @param[in] volume New volume. * @return Zero on success, non-zero otherwise. */ int ahl_binding_t::emit_volume_changed(const std::string& role, int volume) { json_object* data = json_object_new_object(); json_object_object_add(data, "role", json_object_new_string(role.c_str())); json_object_object_add(data, "volume", json_object_new_int(volume)); bool active = false; role_t* activerole = get_active_role(); if (activerole && activerole->uid() == role) active = true; json_object_object_add(data, "active", json_object_new_boolean(active)); return afb_event_push(volume_changed_, data); } /** * @brief Parse the role configuration section. * @param[in] o Role's section. * @return Zero on success, non-zero otherwise. */ int ahl_binding_t::parse_roles_config(json_object* o) { assert(o != nullptr); assert(json_object_is_type(o, json_type_array)); if (roles_.size()) return 0; // Roles already added, ignore. size_t count = json_object_array_length(o); roles_.reserve(count); for(int i = 0; i < count; ++i) { json_object* jr = json_object_array_get_idx(o, i); assert(jr != nullptr); roles_.push_back(role_t(jr)); role_t& r = roles_[roles_.size() - 1]; if(create_api_verb(&r)) return -1; } return 0; } // ---------- Callbacks to verbs ---------- /** * @brief Get the roles list. * @param[in] req Request to answer. */ void ahl_binding_t::get_roles(afb_req_t req) const { json_bool verbose = FALSE; json_object* arg = afb_req_json(req); json_object* jverbose; if (arg != nullptr) { json_bool ret = json_object_object_get_ex(arg, "verbose", &jverbose); if (ret) verbose = json_object_get_boolean(jverbose); } json_object* result = json_object_new_array(); for(const auto& r : roles_) { if (verbose == TRUE || r.device_uri().size()) json_object_array_add(result, json_object_new_string(r.uid().c_str())); } afb_req_success(req, result, nullptr); } /** * @brief Controls the currently active role. * @param[in] req Request to answer. */ void ahl_binding_t::activerole(afb_req_t req) { role_t* role = get_active_role(); if (!role) afb_req_fail(req, "No active role!", nullptr); else role->invoke(req); } /** * @brief Subscribe to events. * @param[in] req Request to answer. */ void ahl_binding_t::subscribe(afb_req_t req) const { if (afb_req_subscribe(req, volume_changed_)) afb_req_fail(req, "Failed to subscribe to \"volume_changed\" event!", nullptr); else afb_req_success(req, nullptr, "Subscribed to \"volume_changed\" event!"); } /** * @brief Unsubscribe to events. * @param[in] req Request to answer. */ void ahl_binding_t::unsubscribe(afb_req_t req) const { if (afb_req_unsubscribe(req, volume_changed_)) afb_req_fail(req, "Failed to unsubscribe from \"volume_changed\" event!", nullptr); else afb_req_success(req, nullptr, "Unsubscribed from \"volume_changed\" event!"); }