From 93f9937300ce2ae1f45d3362d6c1a3180e0743a8 Mon Sep 17 00:00:00 2001 From: Matt Ranostay Date: Tue, 30 Oct 2018 19:41:53 -0700 Subject: binding: bluetooth: initial commit of binding rewrite Bug-AGL: SPEC-1630 Change-Id: I33cfec70283fa29f47b76f9e6be3e8e6cd6a2f54 Signed-off-by: Matt Ranostay --- binding/CMakeLists.txt | 35 ++ binding/bluetooth-agent.c | 272 ++++++++++++ binding/bluetooth-api.c | 1034 ++++++++++++++++++++++++++++++++++++++++++++ binding/bluetooth-api.h | 200 +++++++++ binding/bluetooth-bluez.c | 513 ++++++++++++++++++++++ binding/bluetooth-common.h | 196 +++++++++ binding/bluetooth-rfkill.c | 129 ++++++ binding/bluetooth-util.c | 1026 +++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 3405 insertions(+) create mode 100644 binding/CMakeLists.txt create mode 100644 binding/bluetooth-agent.c create mode 100644 binding/bluetooth-api.c create mode 100644 binding/bluetooth-api.h create mode 100644 binding/bluetooth-bluez.c create mode 100644 binding/bluetooth-common.h create mode 100644 binding/bluetooth-rfkill.c create mode 100644 binding/bluetooth-util.c (limited to 'binding') diff --git a/binding/CMakeLists.txt b/binding/CMakeLists.txt new file mode 100644 index 0000000..a4a8072 --- /dev/null +++ b/binding/CMakeLists.txt @@ -0,0 +1,35 @@ +########################################################################### +# Copyright 2015, 2016, 2017 IoT.bzh +# +# author: Fulup Ar Foll +# contrib: Romain Forlot +# +# 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-bluetooth-binding) + + # Define project Targets + add_library(afm-bluetooth-binding MODULE bluetooth-api.c bluetooth-agent.c bluetooth-rfkill.c bluetooth-util.c bluetooth-bluez.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}) diff --git a/binding/bluetooth-agent.c b/binding/bluetooth-agent.c new file mode 100644 index 0000000..b6ae5fe --- /dev/null +++ b/binding/bluetooth-agent.c @@ -0,0 +1,272 @@ +/* + * Copyright 2018 Konsulko Group + * Author: Pantelis Antoniou + * + * 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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#define AFB_BINDING_VERSION 3 +#include + +#include "bluetooth-api.h" +#include "bluetooth-common.h" + +/* Introspection data for the agent service */ +static const gchar introspection_xml[] = +"" +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +" " +""; + +static void handle_method_call( + GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + struct bluetooth_state *ns = user_data; + struct call_work *cw; + GError *error = NULL; + json_object *jev = NULL; + const gchar *path = NULL; + + /* AFB_INFO("sender=%s", sender_name); + AFB_INFO("object_path=%s", object_path); + AFB_INFO("interface=%s", interface_name); + AFB_INFO("method=%s", method_name); */ + + if (!g_strcmp0(method_name, "RequestConfirmation")) { + int pin; + + g_variant_get(parameters, "(ou)", &path, &pin); + + call_work_lock(ns); + + /* can only occur while a pair is issued */ + cw = call_work_lookup_unlocked(ns, "device", NULL, "RequestConfirmation"); + + /* if nothing is pending return an error */ + /* TODO: allow client side pairing */ + if (!cw) + cw = call_work_create_unlocked(ns, "device", NULL, + "RequestConfirmation", NULL, &error); + + if (!cw) { + call_work_unlock(ns); + g_dbus_method_invocation_return_dbus_error(invocation, + "org.bluez.Error.Rejected", + "No connection pending"); + return; + } + + jev = json_object_new_object(); + json_object_object_add(jev, "action", json_object_new_string("request_confirmation")); + json_object_object_add(jev, "device", json_object_new_string(path)); + json_object_object_add(jev, "pincode", json_object_new_int(pin)); + + cw->agent_data.pin_code = pin; + cw->agent_data.device_path = g_strdup(path); + cw->invocation = invocation; + + call_work_unlock(ns); + + afb_event_push(ns->agent_event, jev); + + return; + } else if (!g_strcmp0(method_name, "AuthorizeService")) { + + /* g_variant_get(parameters, "(os)", &path, &service); + + jev = json_object_new_object(); + json_object_object_add(jev, "action", json_object_new_string("authorize_service")); + json_object_object_add(jev, "path", json_object_new_string(path)); + json_object_object_add(jev, "uuid", json_object_new_string(service)); + + afb_event_push(ns->agent_event, jev); */ + + return g_dbus_method_invocation_return_value(invocation, NULL); + } else if (!g_strcmp0(method_name, "Cancel")) { + + call_work_lock(ns); + + /* can only occur while a pair is issued */ + cw = call_work_lookup_unlocked(ns, "device", NULL, "RequestConfirmation"); + + if (!cw) { + call_work_unlock(ns); + g_dbus_method_invocation_return_dbus_error(invocation, + "org.bluez.Error.Rejected", + "No connection pending"); + return; + } + + jev = json_object_new_object(); + json_object_object_add(jev, "action", json_object_new_string("canceled_pairing")); + + afb_event_push(ns->agent_event, jev); + + call_work_destroy_unlocked(cw); + call_work_unlock(ns); + + return g_dbus_method_invocation_return_value(invocation, NULL); + } + + g_dbus_method_invocation_return_dbus_error(invocation, + "org.freedesktop.DBus.Error.UnknownMethod", + "Uknown method"); +} + +static const GDBusInterfaceVTable interface_vtable = { + .method_call = handle_method_call, + .get_property = NULL, + .set_property = NULL, +}; + +static void on_bus_acquired(GDBusConnection *connection, + const gchar *name, gpointer user_data) +{ + struct init_data *id = user_data; + struct bluetooth_state *ns = id->ns; + GVariant *result; + GError *error = NULL; + + AFB_INFO("agent bus acquired - registering %s", ns->agent_path); + + ns->registration_id = g_dbus_connection_register_object(connection, + ns->agent_path, + ns->introspection_data->interfaces[0], + &interface_vtable, + ns, /* user data */ + NULL, /* user_data_free_func */ + NULL); + + if (!ns->registration_id) { + AFB_ERROR("failed to register agent to dbus"); + goto err_unable_to_register_bus; + + } + + result = agentmanager_call(ns, "RegisterAgent", + g_variant_new("(os)", ns->agent_path, "KeyboardDisplay"), + &error); + if (!result) { + AFB_ERROR("failed to register agent to bluez"); + goto err_unable_to_register_bluez; + } + g_variant_unref(result); + + result = agentmanager_call(ns, "RequestDefaultAgent", + g_variant_new("(o)", ns->agent_path), + &error); + if (!result) { + AFB_ERROR("failed to request default agent to bluez"); + goto err_unable_to_request_default_agent_bluez; + } + g_variant_unref(result); + + ns->agent_registered = TRUE; + + AFB_INFO("agent registered at %s", ns->agent_path); + signal_init_done(id, 0); + + return; + +err_unable_to_request_default_agent_bluez: + agentmanager_call(ns, "UnregisterAgent", + g_variant_new("(o)", ns->agent_path), + &error); + +err_unable_to_register_bluez: + g_dbus_connection_unregister_object(ns->conn, ns->registration_id); + ns->registration_id = 0; + +err_unable_to_register_bus: + signal_init_done(id, -1); +} + +int bluetooth_register_agent(struct init_data *id) +{ + struct bluetooth_state *ns = id->ns; + + ns->agent_path = g_strdup_printf("%s/agent%d", + BLUEZ_PATH, getpid()); + if (!ns->agent_path) { + AFB_ERROR("can't create agent path"); + goto out_no_agent_path; + } + + ns->introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, NULL); + if (!ns->introspection_data) { + AFB_ERROR("can't create introspection data"); + goto out_no_introspection_data; + } + + ns->agent_id = g_bus_own_name(G_BUS_TYPE_SYSTEM, BLUEZ_AGENT_INTERFACE, + G_BUS_NAME_OWNER_FLAGS_REPLACE | + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT, + on_bus_acquired, + NULL, + NULL, + id, + NULL); + if (!ns->agent_id) { + AFB_ERROR("can't create agent bus instance"); + goto out_no_bus_name; + } + + return 0; + +out_no_bus_name: + g_dbus_node_info_unref(ns->introspection_data); +out_no_introspection_data: + g_free(ns->agent_path); +out_no_agent_path: + return -1; +} + +void bluetooth_unregister_agent(struct bluetooth_state *ns) +{ + g_bus_unown_name(ns->agent_id); + g_dbus_node_info_unref(ns->introspection_data); + g_free(ns->agent_path); +} diff --git a/binding/bluetooth-api.c b/binding/bluetooth-api.c new file mode 100644 index 0000000..633bbd6 --- /dev/null +++ b/binding/bluetooth-api.c @@ -0,0 +1,1034 @@ +/* + * Copyright 2018 Konsulko Group + * Author: Matt Ranostay + * Author: Pantelis Antoniou + * + * 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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#define AFB_BINDING_VERSION 3 +#include + +#include "bluetooth-api.h" +#include "bluetooth-common.h" + +/** + * The global thread + */ +static GThread *global_thread; + +static struct bluetooth_state *bluetooth_get_userdata(afb_req_t request) { + afb_api_t api = afb_req_get_api(request); + return afb_api_get_userdata(api); +} + +void call_work_lock(struct bluetooth_state *ns) +{ + g_mutex_lock(&ns->cw_mutex); +} + +void call_work_unlock(struct bluetooth_state *ns) +{ + g_mutex_unlock(&ns->cw_mutex); +} + +struct call_work *call_work_lookup_unlocked( + struct bluetooth_state *ns, + const char *access_type, const char *type_arg, + const char *method) +{ + struct call_work *cw; + GSList *list; + + /* we can only allow a single pending call */ + for (list = ns->cw_pending; list; list = g_slist_next(list)) { + cw = list->data; + if (!g_strcmp0(access_type, cw->access_type) && + !g_strcmp0(type_arg, cw->type_arg) && + !g_strcmp0(method, cw->method)) + return cw; + } + return NULL; +} + +struct call_work *call_work_lookup( + struct bluetooth_state *ns, + const char *access_type, const char *type_arg, + const char *method) +{ + struct call_work *cw; + + g_mutex_lock(&ns->cw_mutex); + cw = call_work_lookup_unlocked(ns, access_type, type_arg, method); + g_mutex_unlock(&ns->cw_mutex); + + return cw; +} + +int call_work_pending_id( + struct bluetooth_state *ns, + const char *access_type, const char *type_arg, + const char *method) +{ + struct call_work *cw; + int id = -1; + + g_mutex_lock(&ns->cw_mutex); + cw = call_work_lookup_unlocked(ns, access_type, type_arg, method); + if (cw) + id = cw->id; + g_mutex_unlock(&ns->cw_mutex); + + return id; +} + +struct call_work *call_work_lookup_by_id_unlocked( + struct bluetooth_state *ns, int id) +{ + struct call_work *cw; + GSList *list; + + /* we can only allow a single pending call */ + for (list = ns->cw_pending; list; list = g_slist_next(list)) { + cw = list->data; + if (cw->id == id) + return cw; + } + return NULL; +} + +struct call_work *call_work_lookup_by_id( + struct bluetooth_state *ns, int id) +{ + struct call_work *cw; + + g_mutex_lock(&ns->cw_mutex); + cw = call_work_lookup_by_id_unlocked(ns, id); + g_mutex_unlock(&ns->cw_mutex); + + return cw; +} + +struct call_work *call_work_create_unlocked(struct bluetooth_state *ns, + const char *access_type, const char *type_arg, + const char *method, const char *bluez_method, + GError **error) +{ + + struct call_work *cw = NULL; + + cw = call_work_lookup_unlocked(ns, access_type, type_arg, method); + if (cw) { + g_set_error(error, NB_ERROR, NB_ERROR_CALL_IN_PROGRESS, + "another call in progress (%s/%s/%s)", + access_type, type_arg, method); + return NULL; + } + + /* no other pending; allocate */ + cw = g_malloc0(sizeof(*cw)); + cw->ns = ns; + do { + cw->id = ns->next_cw_id; + if (++ns->next_cw_id < 0) + ns->next_cw_id = 1; + } while (call_work_lookup_by_id_unlocked(ns, cw->id)); + + cw->access_type = g_strdup(access_type); + cw->type_arg = g_strdup(type_arg); + cw->method = g_strdup(method); + cw->bluez_method = g_strdup(bluez_method); + + ns->cw_pending = g_slist_prepend(ns->cw_pending, cw); + + return cw; +} + +struct call_work *call_work_create(struct bluetooth_state *ns, + const char *access_type, const char *type_arg, + const char *method, const char *bluez_method, + GError **error) +{ + + struct call_work *cw; + + g_mutex_lock(&ns->cw_mutex); + cw = call_work_create_unlocked(ns, + access_type, type_arg, method, bluez_method, + error); + g_mutex_unlock(&ns->cw_mutex); + + return cw; +} + +void call_work_destroy_unlocked(struct call_work *cw) +{ + struct bluetooth_state *ns = cw->ns; + struct call_work *cw2; + + /* verify that it's something we know about */ + cw2 = call_work_lookup_by_id_unlocked(ns, cw->id); + if (cw2 != cw) { + AFB_ERROR("Bad call work to destroy"); + return; + } + + /* remove it */ + ns->cw_pending = g_slist_remove(ns->cw_pending, cw); + + /* agent struct data */ + g_free(cw->agent_data.device_path); + + g_free(cw->access_type); + g_free(cw->type_arg); + g_free(cw->method); + g_free(cw->bluez_method); +} + +void call_work_destroy(struct call_work *cw) +{ + struct bluetooth_state *ns = cw->ns; + + g_mutex_lock(&ns->cw_mutex); + call_work_destroy_unlocked(cw); + g_mutex_unlock(&ns->cw_mutex); +} + +static afb_event_t get_event_from_value(struct bluetooth_state *ns, + const char *value) +{ + if (!g_strcmp0(value, "device_changes")) + return ns->device_changes_event; + + if (!g_strcmp0(value, "agent")) + return ns->agent_event; + + return NULL; +} + +static void bluez_devices_signal_callback( + GDBusConnection *connection, + const gchar *sender_name, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + struct bluetooth_state *ns = user_data; + GVariantIter *array1 = NULL; + GError *error = NULL; + GVariant *var = NULL; + const gchar *path = NULL; + const gchar *key = NULL; + json_object *jresp = NULL, *jobj; + GVariantIter *array; + gboolean is_config, ret; + + /* AFB_INFO("sender=%s", sender_name); + AFB_INFO("object_path=%s", object_path); + AFB_INFO("interface=%s", interface_name); + AFB_INFO("signal=%s", signal_name); */ + + if (!g_strcmp0(signal_name, "InterfacesAdded")) { + + g_variant_get(parameters, "(&oa{sa{sv}})", &path, &array); + + jresp = json_object_new_object(); + + json_object_object_add(jresp, "device", + json_object_new_string(path)); + json_object_object_add(jresp, "action", + json_object_new_string("added")); + + jobj = json_object_new_object(); + + while (g_variant_iter_next(array, "{&s@a{sv}}", &key, &var)) { + const char *name = NULL; + GVariant *val = NULL; + + if (g_strcmp0(key, BLUEZ_DEVICE_INTERFACE) != 0) + continue; + + array1 = g_variant_iter_new(var); + + while (g_variant_iter_next(array1, "{&sv}", &name, &val)) { + ret = device_property_dbus2json(jobj, + name, val, &is_config, &error); + g_variant_unref(val); + if (!ret) { + AFB_WARNING("%s property %s - %s", + "devices", + key, error->message); + g_clear_error(&error); + } + } + g_variant_iter_free(array1); + } + g_variant_iter_free(array); + + if (array1) { + json_object_object_add(jresp, "properties", jobj); + } else { + json_object_put(jresp); + jresp = NULL; + } + + } else if (!g_strcmp0(signal_name, "InterfacesRemoved")) { + + g_variant_get(parameters, "(&oas)", &path, &array); + g_variant_iter_free(array); + + jresp = json_object_new_object(); + + json_object_object_add(jresp, "path", + json_object_new_string(path)); + json_object_object_add(jresp, "action", + json_object_new_string("removed")); + + } else if (!g_strcmp0(signal_name, "PropertiesChanged")) { + + g_variant_get(parameters, "(&sa{sv}as)", &path, &array, &array1); + + if (!g_strcmp0(path, BLUEZ_DEVICE_INTERFACE)) { + + jresp = json_object_new_object(); + + json_object_object_add(jresp, "path", + json_object_new_string(object_path)); + json_object_object_add(jresp, "action", + json_object_new_string("changed")); + + jobj = json_object_new_object(); + + while (g_variant_iter_next(array, "{&sv}", &key, &var)) { + ret = device_property_dbus2json(jobj, + key, var, &is_config, &error); + g_variant_unref(var); + if (!ret) { + AFB_WARNING("%s property %s - %s", + "devices", + key, error->message); + g_clear_error(&error); + } + } + + json_object_object_add(jresp, "properties", jobj); + } + + g_variant_iter_free(array); + g_variant_iter_free(array1); + } + + if (jresp) { + afb_event_push(ns->device_changes_event, jresp); + jresp = NULL; + } + + json_object_put(jresp); +} + +static struct bluetooth_state *bluetooth_init(GMainLoop *loop) +{ + struct bluetooth_state *ns; + GError *error = NULL; + + ns = g_try_malloc0(sizeof(*ns)); + if (!ns) { + AFB_ERROR("out of memory allocating bluetooth state"); + goto err_no_ns; + } + + AFB_INFO("connecting to dbus"); + + ns->loop = loop; + ns->conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); + if (!ns->conn) { + if (error) + g_dbus_error_strip_remote_error(error); + AFB_ERROR("Cannot connect to D-Bus, %s", + error ? error->message : "unspecified"); + g_error_free(error); + goto err_no_conn; + + } + + AFB_INFO("connected to dbus"); + + ns->device_changes_event = + afb_daemon_make_event("device_changes"); + ns->agent_event = + afb_daemon_make_event("agent"); + + if (!afb_event_is_valid(ns->device_changes_event) || + !afb_event_is_valid(ns->agent_event)) { + AFB_ERROR("Cannot create events"); + goto err_no_events; + } + + ns->device_sub = g_dbus_connection_signal_subscribe( + ns->conn, + BLUEZ_SERVICE, + NULL, /* interface */ + NULL, /* member */ + NULL, /* object path */ + NULL, /* arg0 */ + G_DBUS_SIGNAL_FLAGS_NONE, + bluez_devices_signal_callback, + ns, + NULL); + if (!ns->device_sub) { + AFB_ERROR("Unable to subscribe to interface signals"); + goto err_no_device_sub; + } + + g_mutex_init(&ns->cw_mutex); + ns->next_cw_id = 1; + + bluetooth_monitor_init(); + + return ns; + +err_no_device_sub: + /* no way to clear the events */ +err_no_events: + g_dbus_connection_close(ns->conn, NULL, NULL, NULL); +err_no_conn: + g_free(ns); +err_no_ns: + return NULL; +} + +static void bluetooth_cleanup(struct bluetooth_state *ns) +{ + g_dbus_connection_signal_unsubscribe(ns->conn, ns->device_sub); + g_dbus_connection_close(ns->conn, NULL, NULL, NULL); + g_free(ns); +} + +static gpointer bluetooth_func(gpointer ptr) +{ + struct init_data *id = ptr; + struct bluetooth_state *ns; + GMainLoop *loop; + int rc = 0; + + loop = g_main_loop_new(NULL, FALSE); + if (!loop) { + AFB_ERROR("Unable to create main loop"); + goto err_no_loop; + } + + /* real bluetooth init */ + ns = bluetooth_init(loop); + if (!ns) { + AFB_ERROR("bluetooth_init() failed"); + goto err_no_ns; + } + + id->ns = ns; + rc = bluetooth_register_agent(id); + if (rc) { + AFB_ERROR("bluetooth_register_agent() failed"); + goto err_no_agent; + } + + /* note that we wait for agent registration to signal done */ + + afb_api_set_userdata(id->api, ns); + g_main_loop_run(loop); + + g_main_loop_unref(ns->loop); + + bluetooth_unregister_agent(ns); + + bluetooth_cleanup(ns); + afb_api_set_userdata(id->api, NULL); + + return NULL; + +err_no_agent: + bluetooth_cleanup(ns); + +err_no_ns: + g_main_loop_unref(loop); + +err_no_loop: + signal_init_done(id, -1); + + return NULL; +} + +static int init(afb_api_t api) +{ + struct init_data init_data, *id = &init_data; + gint64 end_time; + + memset(id, 0, sizeof(*id)); + id->init_done = FALSE; + id->rc = 0; + id->api = api; + g_cond_init(&id->cond); + g_mutex_init(&id->mutex); + + global_thread = g_thread_new("agl-service-bluetooth", + bluetooth_func, + id); + + AFB_INFO("bluetooth-binding waiting for init done"); + + /* wait maximum 10 seconds for init done */ + end_time = g_get_monotonic_time () + 10 * G_TIME_SPAN_SECOND; + g_mutex_lock(&id->mutex); + while (!id->init_done) { + if (!g_cond_wait_until(&id->cond, &id->mutex, end_time)) + break; + } + g_mutex_unlock(&id->mutex); + + if (!id->init_done) { + AFB_ERROR("bluetooth-binding init timeout"); + return -1; + } + + if (id->rc) + AFB_ERROR("bluetooth-binding init thread returned %d", + id->rc); + else + AFB_INFO("bluetooth-binding operational"); + + return id->rc; +} + +static void bluetooth_subscribe_unsubscribe(afb_req_t request, + gboolean unsub) +{ + struct bluetooth_state *ns = bluetooth_get_userdata(request); + json_object *jresp = json_object_new_object(); + const char *value; + afb_event_t event; + int rc; + + /* if value exists means to set offline mode */ + value = afb_req_value(request, "value"); + if (!value) { + afb_req_fail_f(request, "failed", "Missing \"value\" event"); + return; + } + + event = get_event_from_value(ns, value); + if (!event) { + afb_req_fail_f(request, "failed", "Bad \"value\" event \"%s\"", + value); + return; + } + + if (!unsub) + rc = afb_req_subscribe(request, event); + else + rc = afb_req_unsubscribe(request, event); + if (rc != 0) { + afb_req_fail_f(request, "failed", + "%s error on \"value\" event \"%s\"", + !unsub ? "subscribe" : "unsubscribe", + value); + return; + } + + afb_req_success_f(request, jresp, "Bluetooth %s to event \"%s\"", + !unsub ? "subscribed" : "unsubscribed", + value); +} + +static void bluetooth_subscribe(afb_req_t request) +{ + bluetooth_subscribe_unsubscribe(request, FALSE); +} + +static void bluetooth_unsubscribe(afb_req_t request) +{ + bluetooth_subscribe_unsubscribe(request, TRUE); +} + +static void bluetooth_list(afb_req_t request) +{ + struct bluetooth_state *ns = bluetooth_get_userdata(request); + GError *error = NULL; + json_object *jresp; + + jresp = object_properties(ns, &error); + + afb_req_success(request, jresp, "Bluetooth - managed objects"); +} + +static void bluetooth_state(afb_req_t request) +{ + struct bluetooth_state *ns = bluetooth_get_userdata(request); + GError *error = NULL; + json_object *jresp; + const char *adapter; + + adapter = afb_req_value(request, "adapter"); + if (!adapter) { + afb_req_fail(request, "failed", "No adapter give to return state"); + return; + } + + jresp = adapter_properties(ns, &error, adapter); + if (!jresp) { + afb_req_fail_f(request, "failed", "property %s error %s", + "State", error->message); + return; + } + + afb_req_success(request, jresp, "Bluetooth - adapter state"); +} + +static void bluetooth_adapter(afb_req_t request) +{ + struct bluetooth_state *ns = bluetooth_get_userdata(request); + GError *error = NULL; + const char *adapter, *scan, *discoverable, *powered; + + adapter = afb_req_value(request, "adapter"); + if (!adapter) { + afb_req_fail(request, "failed", "No adapter given to configure"); + return; + } + + scan = afb_req_value(request, "discovery"); + if (scan) { + GVariant *reply = adapter_call(ns, adapter, str2boolean(scan) ? + "StartDiscovery" : "StopDiscovery", NULL, &error); + + if (!reply) { + afb_req_fail_f(request, "failed", + "adapter %s method %s error %s", + scan, "Scan", error->message); + g_error_free(error); + return; + } + g_variant_unref(reply); + } + + discoverable = afb_req_value(request, "discoverable"); + if (discoverable) { + int ret = adapter_set_property(ns, adapter, FALSE, "Discoverable", + json_object_new_boolean(str2boolean(discoverable)), + &error); + if (!ret) { + afb_req_fail_f(request, "failed", + "adapter %s set_property %s error %s", + adapter, "Discoverable", error->message); + g_error_free(error); + return; + } + } + + powered = afb_req_value(request, "powered"); + if (powered) { + int ret = adapter_set_property(ns, adapter, FALSE, "Powered", + json_object_new_boolean(str2boolean(powered)), + &error); + if (!ret) { + afb_req_fail_f(request, "failed", + "adapter %s set_property %s error %s", + adapter, "Powered", error->message); + g_error_free(error); + return; + } + } + + bluetooth_state(request); +} + +static void connect_service_callback(void *user_data, + GVariant *result, GError **error) +{ + struct call_work *cw = user_data; + struct bluetooth_state *ns = cw->ns; + + bluez_decode_call_error(ns, + cw->access_type, cw->type_arg, cw->bluez_method, + error); + + if (error && *error) { + afb_req_fail_f(cw->request, "failed", "Connect error: %s", + (*error)->message); + goto out_free; + } + + if (result) + g_variant_unref(result); + + afb_req_success_f(cw->request, NULL, "Bluetooth - device %s connected", + cw->type_arg); +out_free: + afb_req_unref(cw->request); + call_work_destroy(cw); +} + +static void bluetooth_connect_device(afb_req_t request) +{ + struct bluetooth_state *ns = bluetooth_get_userdata(request); + GError *error = NULL; + const char *device, *uuid; + struct call_work *cw; + + /* first device */ + device = afb_req_value(request, "device"); + if (!device) { + afb_req_fail(request, "failed", "No path given"); + return; + } + + /* optional, connect single profile */ + uuid = afb_req_value(request, "uuid"); + + cw = call_work_create(ns, "device", device, + "connect_service", "Connect", &error); + if (!cw) { + afb_req_fail_f(request, "failed", "can't queue work %s", + error->message); + g_error_free(error); + return; + } + + cw->request = request; + afb_req_addref(request); + + if (uuid) + cw->cpw = bluez_call_async(ns, "device", device, + "ConnectProfile", g_variant_new("(&s)", uuid), &error, + connect_service_callback, cw); + else + cw->cpw = bluez_call_async(ns, "device", device, + "Connect", NULL, &error, + connect_service_callback, cw); + + if (!cw->cpw) { + afb_req_fail_f(request, "failed", "connection error %s", + error->message); + call_work_destroy(cw); + g_error_free(error); + return; + } +} + +static void bluetooth_disconnect_device(afb_req_t request) +{ + struct bluetooth_state *ns = bluetooth_get_userdata(request); + json_object *jresp; + GVariant *reply = NULL; + GError *error = NULL; + const char *device, *uuid; + + /* first device */ + device = afb_req_value(request, "device"); + if (!device) { + afb_req_fail(request, "failed", "No device given to disconnect"); + return; + } + + /* optional, disconnect single profile */ + uuid = afb_req_value(request, "uuid"); + + if (uuid) + reply = device_call(ns, device, "DisconnectProfile", + g_variant_new("(&s)", uuid), &error); + else + reply = device_call(ns, device, "Disconnect", NULL, &error); + + if (!reply) { + afb_req_fail_f(request, "failed", "Disconnect error %s", + error ? error->message : "unspecified"); + g_error_free(error); + return; + } + + g_variant_unref(reply); + + jresp = json_object_new_object(); + afb_req_success_f(request, jresp, "Device - Bluetooth %s disconnected", + device); +} + +static void pair_service_callback(void *user_data, + GVariant *result, GError **error) +{ + struct call_work *cw = user_data; + struct bluetooth_state *ns = cw->ns; + + bluez_decode_call_error(ns, + cw->access_type, cw->type_arg, cw->bluez_method, + error); + + if (error && *error) { + afb_req_fail_f(cw->request, "failed", "Connect error: %s", + (*error)->message); + goto out_free; + } + + if (result) + g_variant_unref(result); + + afb_req_success_f(cw->request, NULL, "Bluetooth - device %s paired", + cw->type_arg); +out_free: + afb_req_unref(cw->request); + call_work_destroy(cw); +} + +static void bluetooth_pair_device(afb_req_t request) +{ + struct bluetooth_state *ns = bluetooth_get_userdata(request); + GError *error = NULL; + const char *device; + struct call_work *cw; + + /* first device */ + device = afb_req_value(request, "device"); + if (!device) { + afb_req_fail(request, "failed", "No path given"); + return; + } + + cw = call_work_create(ns, "device", device, + "pair_device", "Pair", &error); + if (!cw) { + afb_req_fail_f(request, "failed", "can't queue work %s", + error->message); + g_error_free(error); + return; + } + + cw->request = request; + afb_req_addref(request); + + cw->cpw = bluez_call_async(ns, "device", device, "Pair", NULL, &error, + pair_service_callback, cw); + + if (!cw->cpw) { + afb_req_fail_f(request, "failed", "connection error %s", + error->message); + call_work_destroy(cw); + g_error_free(error); + return; + } +} + +static void bluetooth_cancel_pairing(afb_req_t request) +{ + struct bluetooth_state *ns = bluetooth_get_userdata(request); + struct call_work *cw; + GVariant *reply = NULL; + GError *error = NULL; + gchar *device; + + call_work_lock(ns); + + cw = call_work_lookup_unlocked(ns, "device", NULL, "RequestConfirmation"); + + if (!cw) { + call_work_unlock(ns); + afb_req_fail(request, "failed", "No pairing in progress"); + return; + } + + device = cw->agent_data.device_path; + reply = device_call(ns, device, "CancelPairing", NULL, &error); + + if (!reply) { + call_work_unlock(ns); + afb_req_fail_f(request, "failed", + "device %s method %s error %s", + device, "CancelPairing", error->message); + return; + } + + call_work_unlock(ns); + + afb_req_success(request, json_object_new_object(), + "Bluetooth - pairing canceled"); +} + +static void bluetooth_confirm_pairing(afb_req_t request) +{ + struct bluetooth_state *ns = bluetooth_get_userdata(request); + struct call_work *cw; + int pin = -1; + + const char *value = afb_req_value(request, "pincode"); + + if (value) + pin = (int)strtol(value, NULL, 10); + + if (!value || !pin) { + afb_req_fail_f(request, "failed", "No pincode parameter"); + return; + } + + call_work_lock(ns); + + cw = call_work_lookup_unlocked(ns, "device", NULL, "RequestConfirmation"); + + if (!cw) { + call_work_unlock(ns); + afb_req_fail(request, "failed", "No pairing in progress"); + return; + } + + if (pin == cw->agent_data.pin_code) { + g_dbus_method_invocation_return_value(cw->invocation, NULL); + + afb_req_success(request, json_object_new_object(), + "Bluetooth - pairing confimed"); + } else { + g_dbus_method_invocation_return_dbus_error(cw->invocation, + "org.bluez.Error.Rejected", + "No connection pending"); + + afb_req_fail(request, "failed", "Bluetooth - pairing failed"); + } + + call_work_destroy_unlocked(cw); + call_work_unlock(ns); +} + +static void bluetooth_remove_device(afb_req_t request) +{ + struct bluetooth_state *ns = bluetooth_get_userdata(request); + GError *error = NULL; + GVariant *reply; + json_object *jval = NULL; + const char *device, *adapter; + + /* first device */ + device = afb_req_value(request, "device"); + if (!device) { + afb_req_fail(request, "failed", "No path given"); + return; + } + + jval = bluez_get_property(ns, "device", device, FALSE, "Adapter", &error); + + if (!jval) { + afb_req_fail_f(request, "failed", + " adapter not found for device %s error %s", + device, error->message); + + g_error_free(error); + return; + } + + adapter = json_object_get_string(jval); + + reply = adapter_call(ns, adapter, "RemoveDevice", + g_variant_new("(o)", device), &error); + + if (!reply) { + afb_req_fail_f(request, "failed", + " device %s method %s error %s", + device, "RemoveDevice", error->message); + g_error_free(error); + json_object_put(jval); + return; + } + g_variant_unref(reply); + + afb_req_success_f(request, json_object_new_object(), + "Bluetooth - device %s removed", adapter); + + json_object_put(jval); +} + +static const struct afb_verb_v3 bluetooth_verbs[] = { + { + .verb = "subscribe", + .session = AFB_SESSION_NONE, + .callback = bluetooth_subscribe, + .info = "Subscribe to the event of 'value'", + }, { + .verb = "unsubscribe", + .session = AFB_SESSION_NONE, + .callback = bluetooth_unsubscribe, + .info = "Unsubscribe to the event of 'value'", + }, { + .verb = "managed_objects", + .session = AFB_SESSION_NONE, + .callback = bluetooth_list, + .info = "Retrieve managed bluetooth devices" + }, { + .verb = "adapter_state", + .session = AFB_SESSION_NONE, + .callback = bluetooth_adapter, + .info = "Set adapter mode and retrieve properties" + }, { + .verb = "connect", + .session = AFB_SESSION_NONE, + .callback = bluetooth_connect_device, + .info = "Connect device and/or profile" + }, { + .verb = "disconnect", + .session = AFB_SESSION_NONE, + .callback = bluetooth_disconnect_device, + .info = "Disconnect device and/or profile" + }, { + .verb = "pair", + .session = AFB_SESSION_NONE, + .callback = bluetooth_pair_device, + .info = "Pair device" + }, { + .verb = "cancel_pairing", + .session = AFB_SESSION_NONE, + .callback = bluetooth_cancel_pairing, + .info = "Cancel pairing" + }, { + .verb = "confirm_pairing", + .session = AFB_SESSION_NONE, + .callback = bluetooth_confirm_pairing, + .info = "Confirm pairing", + }, { + .verb = "remove_device", + .session = AFB_SESSION_NONE, + .callback = bluetooth_remove_device, + .info = "Removed paired device", + }, + { } /* marker for end of the array */ +}; + +/* + * description of the binding for afb-daemon + */ +const struct afb_binding_v3 afbBindingV3 = { + .api = "bluetooth-manager", + .specification = "bluetooth manager API", + .verbs = bluetooth_verbs, + .init = init, +}; diff --git a/binding/bluetooth-api.h b/binding/bluetooth-api.h new file mode 100644 index 0000000..50bf501 --- /dev/null +++ b/binding/bluetooth-api.h @@ -0,0 +1,200 @@ +/* + * Copyright 2018 Konsulko Group + * Author: Matt Ranostay + * Author: Pantelis Antoniou + * + * 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. + */ + +#ifndef BLUETOOTH_API_H +#define BLUETOOTH_API_H + +#include +#include +#include +#include + +#include + +#include + +#define BLUEZ_SERVICE "org.bluez" +#define BLUEZ_ADAPTER_INTERFACE BLUEZ_SERVICE ".Adapter1" +#define BLUEZ_AGENT_INTERFACE BLUEZ_SERVICE ".Agent1" +#define BLUEZ_AGENTMANAGER_INTERFACE BLUEZ_SERVICE ".AgentManager1" +#define BLUEZ_DEVICE_INTERFACE BLUEZ_SERVICE ".Device1" + +#define BLUEZ_OBJECT_PATH "/" +#define BLUEZ_PATH "/org/bluez" + +#define FREEDESKTOP_INTROSPECT "org.freedesktop.DBus.Introspectable" +#define FREEDESKTOP_PROPERTIES "org.freedesktop.DBus.Properties" +#define FREEDESKTOP_OBJECTMANAGER "org.freedesktop.DBus.ObjectManager" + +#define DBUS_REPLY_TIMEOUT (120 * 1000) +#define DBUS_REPLY_TIMEOUT_SHORT (10 * 1000) + +#define BLUEZ_AT_OBJECT "object" +#define BLUEZ_AT_ADAPTER "adapter" +#define BLUEZ_AT_DEVICE "device" +#define BLUEZ_AT_AGENT "agent" +#define BLUEZ_AT_AGENTMANAGER "agent-manager" + +struct bluetooth_state; + +static inline const char *bluez_strip_path(const char *path) +{ + const char *basename; + + basename = strrchr(path, '/'); + if (!basename) + return NULL; + basename++; + /* at least one character */ + return *basename ? basename : NULL; +} + + +struct call_work *call_work_create_unlocked(struct bluetooth_state *ns, + const char *access_type, const char *type_arg, + const char *method, const char *bluez_method, + GError **error); + +void call_work_destroy_unlocked(struct call_work *cw); + +void call_work_lock(struct bluetooth_state *ns); + +void call_work_unlock(struct bluetooth_state *ns); + +struct call_work *call_work_lookup_unlocked( + struct bluetooth_state *ns, + const char *access_type, const char *type_arg, + const char *method); + +const struct property_info *bluez_get_property_info( + const char *access_type, GError **error); + +gboolean bluez_property_dbus2json(const char *access_type, + json_object *jprop, const gchar *key, GVariant *var, + gboolean *is_config, + GError **error); + +GVariant *bluez_call(struct bluetooth_state *ns, + const char *access_type, const char *type_arg, + const char *method, GVariant *params, GError **error); + +json_object *bluez_get_properties(struct bluetooth_state *ns, + const char *access_type, const char *path, + GError **error); + +json_object *bluez_get_property(struct bluetooth_state *ns, + const char *access_type, const char *type_arg, + gboolean is_json_name, const char *name, GError **error); + +gboolean bluez_set_property(struct bluetooth_state *ns, + const char *access_type, const char *type_arg, + gboolean is_json_name, const char *name, json_object *jval, + GError **error); + +/* convenience access methods */ +static inline gboolean device_property_dbus2json(json_object *jprop, + const gchar *key, GVariant *var, gboolean *is_config, + GError **error) +{ + return bluez_property_dbus2json(BLUEZ_AT_DEVICE, + jprop, key, var, is_config, error); +} + +static inline gboolean agent_property_dbus2json(json_object *jprop, + const gchar *key, GVariant *var, gboolean *is_config, + GError **error) +{ + return bluez_property_dbus2json(BLUEZ_AT_AGENT, + jprop, key, var, is_config, error); +} + +static inline GVariant *device_call(struct bluetooth_state *ns, + const char *device, const char *method, + GVariant *params, GError **error) +{ + return bluez_call(ns, BLUEZ_AT_DEVICE, device, + method, params, error); +} + +static inline GVariant *adapter_call(struct bluetooth_state *ns, + const char *adapter, const char *method, + GVariant *params, GError **error) +{ + return bluez_call(ns, BLUEZ_AT_ADAPTER, adapter, + method, params, error); +} + +static inline GVariant *agentmanager_call(struct bluetooth_state *ns, + const char *method, GVariant *params, GError **error) +{ + return bluez_call(ns, BLUEZ_AT_AGENTMANAGER, NULL, + method, params, error); +} + +static inline gboolean adapter_set_property(struct bluetooth_state *ns, + const char *adapter, gboolean is_json_name, const char *name, + json_object *jval, GError **error) +{ + return bluez_set_property(ns, BLUEZ_AT_ADAPTER, adapter, + is_json_name, name, jval, error); +} + +static inline json_object *adapter_get_property(struct bluetooth_state *ns, + gboolean is_json_name, const char *name, GError **error) +{ + return bluez_get_property(ns, BLUEZ_AT_ADAPTER, NULL, + is_json_name, name, error); +} + +static inline json_object *adapter_properties(struct bluetooth_state *ns, + GError **error, const gchar *adapter) +{ + return bluez_get_properties(ns, + BLUEZ_AT_ADAPTER, adapter, error); +} + +static inline json_object *object_properties(struct bluetooth_state *ns, + GError **error) +{ + return bluez_get_properties(ns, + BLUEZ_AT_OBJECT, BLUEZ_OBJECT_PATH, error); +} + +struct bluez_pending_work { + struct bluetooth_state *ns; + void *user_data; + GCancellable *cancel; + void (*callback)(void *user_data, GVariant *result, GError **error); +}; + +void bluez_cancel_call(struct bluetooth_state *ns, + struct bluez_pending_work *cpw); + +struct bluez_pending_work * +bluez_call_async(struct bluetooth_state *ns, + const char *access_type, const char *type_arg, + const char *method, GVariant *params, GError **error, + void (*callback)(void *user_data, GVariant *result, GError **error), + void *user_data); + +void bluez_decode_call_error(struct bluetooth_state *ns, + const char *access_type, const char *type_arg, + const char *method, + GError **error); + +#endif /* BLUETOOTH_API_H */ diff --git a/binding/bluetooth-bluez.c b/binding/bluetooth-bluez.c new file mode 100644 index 0000000..990a244 --- /dev/null +++ b/binding/bluetooth-bluez.c @@ -0,0 +1,513 @@ +/* + * Copyright 2018 Konsulko Group + * Author: Pantelis Antoniou + * + * 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 +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#define AFB_BINDING_VERSION 3 +#include + +#include "bluetooth-api.h" +#include "bluetooth-common.h" + +static const struct property_info adapter_props[] = { + { .name = "UUIDs", .fmt = "as", }, + { .name = "Discoverable", .fmt = "b", }, + { .name = "Discovering", .fmt = "b", }, + { .name = "Pairable", .fmt = "b", }, + { .name = "Powered", .fmt = "b", }, + { .name = "Address", .fmt = "s", }, + { .name = "AddressType", .fmt = "s", }, + { .name = "DiscoverableTimeout",.fmt = "u", }, + { .name = "PairableTimeout", .fmt = "u", }, + { }, +}; + +static const struct property_info device_props[] = { + { .name = "Address", .fmt = "s", }, + { .name = "AddressType", .fmt = "s", }, + { .name = "Name", .fmt = "s", }, + { .name = "Adapter", .fmt = "s", }, + { .name = "Alias", .fmt = "s", }, + { .name = "Class", .fmt = "u", }, + { .name = "Icon", .fmt = "s", }, + { .name = "Modalias", .fmt = "s", }, + { .name = "Paired", .fmt = "b", }, + { .name = "Trusted", .fmt = "b", }, + { .name = "Blocked", .fmt = "b", }, + { .name = "LegacyPairing", .fmt = "b", }, + { .name = "TxPower", .fmt = "n", }, + { .name = "RSSI", .fmt = "n", }, + { .name = "Connected", .fmt = "b", }, + { .name = "UUIDs", .fmt = "as", }, + { .name = "Adapter", .fmt = "s", }, + { .name = "ServicesResolved", .fmt = "b", }, + { }, +}; + +const struct property_info *bluez_get_property_info( + const char *access_type, GError **error) +{ + const struct property_info *pi = NULL; + + if (!strcmp(access_type, BLUEZ_AT_ADAPTER)) + pi = adapter_props; + else if (!strcmp(access_type, BLUEZ_AT_DEVICE)) + pi = device_props; + else + g_set_error(error, NB_ERROR, NB_ERROR_ILLEGAL_ARGUMENT, + "illegal %s argument", access_type); + return pi; +} + +gboolean bluez_property_dbus2json(const char *access_type, + json_object *jprop, const gchar *key, GVariant *var, + gboolean *is_config, + GError **error) +{ + const struct property_info *pi; + gboolean ret; + + *is_config = FALSE; + pi = bluez_get_property_info(access_type, error); + if (!pi) + return FALSE; + + ret = root_property_dbus2json(jprop, pi, key, var, is_config); + if (!ret) + g_set_error(error, NB_ERROR, NB_ERROR_UNKNOWN_PROPERTY, + "unknown %s property %s", + access_type, key); + + return ret; +} + + +void bluez_decode_call_error(struct bluetooth_state *ns, + const char *access_type, const char *type_arg, + const char *method, + GError **error) +{ + if (!error || !*error) + return; + + if (strstr((*error)->message, + "org.freedesktop.DBus.Error.UnknownObject")) { + + if (!strcmp(method, "Set") || + !strcmp(method, "Get") || + !strcmp(method, "GetAll")) { + + g_clear_error(error); + g_set_error(error, NB_ERROR, + NB_ERROR_UNKNOWN_PROPERTY, + "unknown %s property on %s", + access_type, type_arg); + + } else if (!strcmp(method, "Connect") || + !strcmp(method, "ConnectProfile") || + !strcmp(method, "Disconnect") || + !strcmp(method, "DisconnectProfile") || + !strcmp(method, "Pair") || + !strcmp(method, "Unpair") || + !strcmp(method, "RemoveDevice")) { + + g_clear_error(error); + g_set_error(error, NB_ERROR, + NB_ERROR_UNKNOWN_SERVICE, + "unknown service %s", + type_arg); + + } else if (!strcmp(method, "StartDiscovery") || + !strcmp(method, "StopDiscovery") || + !strcmp(method, "RegisterAgent")) { + + g_clear_error(error); + g_set_error(error, NB_ERROR, + NB_ERROR_UNKNOWN_SERVICE, + "unknown service %s", + type_arg); + } + } +} + +GVariant *bluez_call(struct bluetooth_state *ns, + const char *access_type, const char *path, + const char *method, GVariant *params, GError **error) +{ + const char *interface; + GVariant *reply; + + if (!path && (!strcmp(access_type, BLUEZ_AT_DEVICE) || + !strcmp(access_type, BLUEZ_AT_ADAPTER))) { + g_set_error(error, NB_ERROR, NB_ERROR_MISSING_ARGUMENT, + "missing %s argument", + access_type); + return NULL; + } + + if (!strcmp(access_type, BLUEZ_AT_DEVICE)) { + interface = BLUEZ_DEVICE_INTERFACE; + } else if (!strcmp(access_type, BLUEZ_AT_ADAPTER)) { + interface = BLUEZ_ADAPTER_INTERFACE; + } else if (!strcmp(access_type, BLUEZ_AT_AGENTMANAGER)) { + path = BLUEZ_PATH; + interface = BLUEZ_AGENTMANAGER_INTERFACE; + } else { + g_set_error(error, NB_ERROR, NB_ERROR_ILLEGAL_ARGUMENT, + "illegal %s argument", + access_type); + return NULL; + } + + reply = g_dbus_connection_call_sync(ns->conn, + BLUEZ_SERVICE, path, interface, method, params, + NULL, G_DBUS_CALL_FLAGS_NONE, DBUS_REPLY_TIMEOUT, + NULL, error); + bluez_decode_call_error(ns, access_type, path, method, + error); + if (!reply) { + if (error && *error) + g_dbus_error_strip_remote_error(*error); + AFB_ERROR("Error calling %s%s%s %s method %s", + access_type, + path ? "/" : "", + path ? path : "", + method, + error && *error ? (*error)->message : + "unspecified"); + } + + return reply; +} + +static void bluez_call_async_ready(GObject *source_object, + GAsyncResult *res, gpointer user_data) +{ + struct bluez_pending_work *cpw = user_data; + struct bluetooth_state *ns = cpw->ns; + GVariant *result; + GError *error = NULL; + + result = g_dbus_connection_call_finish(ns->conn, res, &error); + + cpw->callback(cpw->user_data, result, &error); + + g_clear_error(&error); + g_cancellable_reset(cpw->cancel); + g_free(cpw); +} + +void bluez_cancel_call(struct bluetooth_state *ns, + struct bluez_pending_work *cpw) +{ + g_cancellable_cancel(cpw->cancel); +} + +struct bluez_pending_work * +bluez_call_async(struct bluetooth_state *ns, + const char *access_type, const char *type_arg, + const char *method, GVariant *params, GError **error, + void (*callback)(void *user_data, GVariant *result, GError **error), + void *user_data) +{ + const char *path; + const char *interface; + struct bluez_pending_work *cpw; + + if (!type_arg && (!strcmp(access_type, BLUEZ_AT_DEVICE) || + !strcmp(access_type, BLUEZ_AT_ADAPTER))) { + g_set_error(error, NB_ERROR, NB_ERROR_MISSING_ARGUMENT, + "missing %s argument", + access_type); + return NULL; + } + + if (!strcmp(access_type, BLUEZ_AT_DEVICE)) { + path = type_arg; + interface = BLUEZ_DEVICE_INTERFACE; + } else if (!strcmp(access_type, BLUEZ_AT_ADAPTER)) { + path = type_arg; + interface = BLUEZ_ADAPTER_INTERFACE; + } else { + g_set_error(error, NB_ERROR, NB_ERROR_ILLEGAL_ARGUMENT, + "illegal %s argument", + access_type); + return NULL; + } + + cpw = g_malloc(sizeof(*cpw)); + if (!cpw) { + g_set_error(error, NB_ERROR, NB_ERROR_OUT_OF_MEMORY, + "out of memory"); + return NULL; + } + cpw->ns = ns; + cpw->user_data = user_data; + cpw->cancel = g_cancellable_new(); + if (!cpw->cancel) { + g_free(cpw); + g_set_error(error, NB_ERROR, NB_ERROR_OUT_OF_MEMORY, + "out of memory"); + return NULL; + } + cpw->callback = callback; + + g_dbus_connection_call(ns->conn, + BLUEZ_SERVICE, path, interface, method, params, + NULL, /* reply type */ + G_DBUS_CALL_FLAGS_NONE, DBUS_REPLY_TIMEOUT, + cpw->cancel, /* cancellable? */ + bluez_call_async_ready, + cpw); + + return cpw; +} + +json_object *bluez_get_properties(struct bluetooth_state *ns, + const char *access_type, const char *path, + GError **error) +{ + const struct property_info *pi = NULL; + const char *method = NULL; + GVariant *reply = NULL; + GVariantIter *array, *array2, *array3; + GVariant *var = NULL; + const char *interface, *interface2, *path2 = NULL; + const gchar *key = NULL; + json_object *jarray = NULL, *jarray2 = NULL; + json_object *jprop = NULL, *jresp = NULL, *jtype = NULL; + gboolean is_config; + + if (!strcmp(access_type, BLUEZ_AT_DEVICE) || + !strcmp(access_type, BLUEZ_AT_ADAPTER)) { + + pi = bluez_get_property_info(access_type, error); + if (!pi) + return NULL; + + interface = FREEDESKTOP_PROPERTIES; + method = "GetAll"; + } else if (!strcmp(access_type, BLUEZ_AT_OBJECT)) { + interface = FREEDESKTOP_OBJECTMANAGER; + method = "GetManagedObjects"; + } else { + return FALSE; + } + + if (!strcmp(access_type, BLUEZ_AT_DEVICE)) + interface2 = BLUEZ_DEVICE_INTERFACE; + else if (!strcmp(access_type, BLUEZ_AT_ADAPTER)) + interface2 = BLUEZ_ADAPTER_INTERFACE; + else if (!strcmp(access_type, BLUEZ_AT_OBJECT)) + interface2 = NULL; + else + return FALSE; + + if (!method) { + g_set_error(error, NB_ERROR, NB_ERROR_ILLEGAL_ARGUMENT, + "illegal %s argument", + access_type); + return NULL; + } + + reply = g_dbus_connection_call_sync(ns->conn, + BLUEZ_SERVICE, path, interface, method, + interface2 ? g_variant_new("(s)", interface2) : NULL, + NULL, G_DBUS_CALL_FLAGS_NONE, DBUS_REPLY_TIMEOUT, + NULL, error); + + if (!reply) + return NULL; + + if (!strcmp(access_type, BLUEZ_AT_DEVICE) || + !strcmp(access_type, BLUEZ_AT_ADAPTER)) { + jprop = json_object_new_object(); + g_variant_get(reply, "(a{sv})", &array); + while (g_variant_iter_loop(array, "{sv}", &key, &var)) { + root_property_dbus2json(jprop, pi, + key, var, &is_config); + } + g_variant_iter_free(array); + g_variant_unref(reply); + jresp = jprop; + } else if (!strcmp(access_type, BLUEZ_AT_OBJECT)) { + + jarray = json_object_new_array(); + jarray2 = json_object_new_array(); + + jresp = json_object_new_object(); + json_object_object_add(jresp, "adapters", jarray); + json_object_object_add(jresp, "devices", jarray2); + + g_variant_get(reply, "(a{oa{sa{sv}}})", &array); + while (g_variant_iter_loop(array, "{oa{sa{sv}}}", &path2, &array2)) { + + while (g_variant_iter_loop(array2, "{&sa{sv}}", &interface, &array3)) { + json_object *array = NULL; + + if (!strcmp(interface, BLUEZ_ADAPTER_INTERFACE)) { + access_type = BLUEZ_AT_ADAPTER; + array = jarray; + } else if (!strcmp(interface, BLUEZ_DEVICE_INTERFACE)) { + access_type = BLUEZ_AT_DEVICE; + array = jarray2; + } else { + continue; /* TODO: Maybe display other interfaces */ + } + + pi = bluez_get_property_info(access_type, error); + jtype = json_object_new_object(); + + json_object_object_add(jtype, "path", + json_object_new_string(path2)); + + while (g_variant_iter_loop(array3, "{sv}", &key, &var)) { + if (!jprop) + jprop = json_object_new_object(); + + root_property_dbus2json(jprop, pi, + key, var, &is_config); + } + + json_object_object_add(jtype, "properties", jprop); + json_object_array_add(array, jtype); + jprop = NULL; + } + + } + + g_variant_iter_free(array); + g_variant_unref(reply); + } + + if (!jresp) { + g_set_error(error, NB_ERROR, NB_ERROR_ILLEGAL_ARGUMENT, + "No %s", access_type); + } + + return jresp; +} + +json_object *bluez_get_property(struct bluetooth_state *ns, + const char *access_type, const char *path, + gboolean is_json_name, const char *name, GError **error) +{ + const struct property_info *pi; + json_object *jprop, *jval; + + pi = bluez_get_property_info(access_type, error); + if (!pi) + return NULL; + + jprop = bluez_get_properties(ns, access_type, path, error); + if (!jprop) + return NULL; + + jval = get_named_property(pi, is_json_name, name, jprop); + json_object_put(jprop); + + if (!jval) + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property %s on %s%s%s", name, + access_type, + path ? "/" : "", + path ? path : ""); + return jval; +} + +/* NOTE: jval is consumed */ +gboolean bluez_set_property(struct bluetooth_state *ns, + const char *access_type, const char *path, + gboolean is_json_name, const char *name, json_object *jval, + GError **error) +{ + const struct property_info *pi; + GVariant *reply, *arg; + const char *interface; + gboolean is_config; + gchar *propname; + + g_assert(path); + + /* get start of properties */ + pi = bluez_get_property_info(access_type, error); + if (!pi) + return FALSE; + + /* get actual property */ + pi = property_by_name(pi, is_json_name, name, &is_config); + if (!pi) { + g_set_error(error, NB_ERROR, NB_ERROR_UNKNOWN_PROPERTY, + "unknown property with name %s", name); + json_object_put(jval); + return FALSE; + } + + /* convert to gvariant */ + arg = property_json_to_gvariant(pi, NULL, NULL, jval, error); + + /* we don't need this anymore */ + json_object_put(jval); + jval = NULL; + + /* no variant? error */ + if (!arg) + return FALSE; + + if (!is_config) + propname = g_strdup(pi->name); + else + propname = configuration_dbus_name(pi->name); + + if (!strcmp(access_type, BLUEZ_AT_DEVICE)) + interface = BLUEZ_DEVICE_INTERFACE; + else if (!strcmp(access_type, BLUEZ_AT_ADAPTER)) + interface = BLUEZ_ADAPTER_INTERFACE; + else if (!strcmp(access_type, BLUEZ_AT_AGENT)) + interface = BLUEZ_AGENT_INTERFACE; + else + return FALSE; + + reply = g_dbus_connection_call_sync(ns->conn, + BLUEZ_SERVICE, path, FREEDESKTOP_PROPERTIES, "Set", + g_variant_new("(ssv)", interface, propname, arg), + NULL, G_DBUS_CALL_FLAGS_NONE, DBUS_REPLY_TIMEOUT, + NULL, error); + + g_free(propname); + + if (!reply) + return FALSE; + + g_variant_unref(reply); + + return TRUE; +} diff --git a/binding/bluetooth-common.h b/binding/bluetooth-common.h new file mode 100644 index 0000000..159c5f4 --- /dev/null +++ b/binding/bluetooth-common.h @@ -0,0 +1,196 @@ +/* + * Copyright 2018 Konsulko Group + * Author: Matt Ranostay + * Author: Pantelis Antoniou + * + * 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. + */ + +#ifndef BLUETOOTH_COMMON_H +#define BLUETOOTH_COMMON_H + +#include + +#define _GNU_SOURCE +#include +#include +#include +#include + +#include + +#define AFB_BINDING_VERSION 3 +#include + +struct call_work; + +struct bluetooth_state { + GMainLoop *loop; + GDBusConnection *conn; + guint device_sub; + guint autoconnect_sub; + + afb_event_t device_changes_event; + afb_event_t agent_event; + + /* NOTE: single connection allowed for now */ + /* NOTE: needs locking and a list */ + GMutex cw_mutex; + int next_cw_id; + GSList *cw_pending; + struct call_work *cw; + + /* agent */ + GDBusNodeInfo *introspection_data; + guint agent_id; + guint registration_id; + gchar *agent_path; + gboolean agent_registered; +}; + +struct init_data { + GCond cond; + GMutex mutex; + gboolean init_done; + afb_api_t api; + struct bluetooth_state *ns; /* before setting afb_api_set_userdata() */ + int rc; +}; + +struct agent_data { + int pin_code; + gchar *device_path; +}; + +struct call_work { + struct bluetooth_state *ns; + int id; + gchar *access_type; + gchar *type_arg; + gchar *method; + gchar *bluez_method; + struct bluez_pending_work *cpw; + afb_req_t request; + struct agent_data agent_data; + GDBusMethodInvocation *invocation; +}; + +/* init methods in bluetooth-rfkill.c */ + +int bluetooth_monitor_init(void); + +/* agent methods in bluetooth-agent.c */ + +int bluetooth_register_agent(struct init_data *id); + +void bluetooth_unregister_agent(struct bluetooth_state *ns); + +/* utility methods in bluetooth-util.c */ + +extern gboolean auto_lowercase_keys; + +void signal_init_done(struct init_data *id, int rc); + +int str2boolean(const char *value); + +json_object *json_object_copy(json_object *jval); + +gchar *key_dbus_to_json(const gchar *key, gboolean auto_lower); + +json_object *simple_gvariant_to_json(GVariant *var, json_object *parent, + gboolean recurse); + +/** + * Structure for converting from dbus properties to json + * and vice-versa. + * Note this is _not_ a generic dbus json bridge since + * some constructs are not easily mapped. + */ +struct property_info { + const char *name; /* the connman property name */ + const char *json_name; /* the json name (if NULL autoconvert) */ + const char *fmt; + unsigned int flags; + const struct property_info *sub; +}; + +#define PI_CONFIG (1U << 0) + +const struct property_info *property_by_dbus_name( + const struct property_info *pi, + const gchar *dbus_name, + gboolean *is_config); +const struct property_info *property_by_json_name( + const struct property_info *pi, + const gchar *json_name, + gboolean *is_config); +const struct property_info *property_by_name( + const struct property_info *pi, + gboolean is_json_name, const gchar *name, + gboolean *is_config); + +gchar *property_get_json_name(const struct property_info *pi, + const gchar *name); +gchar *property_get_name(const struct property_info *pi, + const gchar *json_name); + +gchar *configuration_dbus_name(const gchar *dbus_name); +gchar *configuration_json_name(const gchar *json_name); + +gchar *property_name_dbus2json(const struct property_info *pi, + gboolean is_config); + +json_object *property_dbus2json( + const struct property_info **pip, + const gchar *key, GVariant *var, + gboolean *is_config); + +gboolean root_property_dbus2json( + json_object *jparent, + const struct property_info *pi, + const gchar *key, GVariant *var, + gboolean *is_config); + +GVariant *property_json_to_gvariant( + const struct property_info *pi, + const char *fmt, /* if NULL use pi->fmt */ + const struct property_info *pi_parent, + json_object *jval, + GError **error); + +typedef enum { + NB_ERROR_BAD_TECHNOLOGY, + NB_ERROR_BAD_SERVICE, + NB_ERROR_OUT_OF_MEMORY, + NB_ERROR_NO_TECHNOLOGIES, + NB_ERROR_NO_SERVICES, + NB_ERROR_BAD_PROPERTY, + NB_ERROR_UNIMPLEMENTED, + NB_ERROR_UNKNOWN_PROPERTY, + NB_ERROR_UNKNOWN_TECHNOLOGY, + NB_ERROR_UNKNOWN_SERVICE, + NB_ERROR_MISSING_ARGUMENT, + NB_ERROR_ILLEGAL_ARGUMENT, + NB_ERROR_CALL_IN_PROGRESS, +} NBError; + +#define NB_ERROR (nb_error_quark()) + +extern GQuark nb_error_quark(void); + +json_object *get_property_collect(json_object *jreqprop, json_object *jprop, + GError **error); +json_object *get_named_property(const struct property_info *pi, + gboolean is_json_name, const char *name, json_object *jprop); + +#endif diff --git a/binding/bluetooth-rfkill.c b/binding/bluetooth-rfkill.c new file mode 100644 index 0000000..5419e8f --- /dev/null +++ b/binding/bluetooth-rfkill.c @@ -0,0 +1,129 @@ +/* + * Copyright 2018 Konsulko Group + * Author: Matt Ranostay + * + * 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 +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include +#include + +#define AFB_BINDING_VERSION 3 +#include + +#define HCIDEVUP _IOW('H', 201, int) +#define BTPROTO_HCI 1 + + + +static int hci_interface_enable(int hdev) +{ + int ctl = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI); + int ret; + + if (ctl < 0) + return ctl; + + ret = ioctl(ctl, HCIDEVUP, hdev); + + close(ctl); + + return ret; +} + +static gboolean bluetooth_rfkill_event(GIOChannel *chan, GIOCondition cond, gpointer ptr) +{ + struct rfkill_event event; + gchar *name, buf[8]; + ssize_t len; + int fd; + + fd = g_io_channel_unix_get_fd(chan); + len = read(fd, &event, sizeof(struct rfkill_event)); + + if (len != sizeof(struct rfkill_event)) + return TRUE; + + if (event.type != RFKILL_TYPE_BLUETOOTH) + return TRUE; + + if (event.op == RFKILL_OP_DEL) + return TRUE; + + name = g_strdup_printf("/sys/class/rfkill/rfkill%u/soft", event.idx); + + fd = g_open(name, O_WRONLY); + len = write(fd, "0", 1); + g_close(fd, NULL); + + g_free(name); + + memset(&buf, 0, sizeof(buf)); + + name = g_strdup_printf("/sys/class/rfkill/rfkill%u/name", event.idx); + fd = g_open(name, O_RDONLY); + len = read(fd, &buf, sizeof(buf) - 1); + + if (len > 0) + { + int idx = 0; + sscanf(buf, "hci%d", &idx); + + /* + * 50 millisecond delay to allow time for rfkill to unblock before + * attempting to bring up HCI interface + */ + g_usleep(50000); + + hci_interface_enable(idx); + } + + g_free(name); + + return TRUE; +} + +int bluetooth_monitor_init(void) +{ + int fd = g_open("/dev/rfkill", O_RDWR); + GIOChannel *chan = NULL; + int ret = -EINVAL; + + if (fd < 0) + { + AFB_ERROR("Cannot open /dev/rfkill"); + return ret; + } + + chan = g_io_channel_unix_new(fd); + g_io_channel_set_close_on_unref(chan, TRUE); + + ret = g_io_add_watch(chan, G_IO_IN, bluetooth_rfkill_event, NULL); + + g_io_channel_unref(chan); + + return 0; +} diff --git a/binding/bluetooth-util.c b/binding/bluetooth-util.c new file mode 100644 index 0000000..0af4898 --- /dev/null +++ b/binding/bluetooth-util.c @@ -0,0 +1,1026 @@ +/* + * Copyright 2018 Konsulko Group + * Author: Pantelis Antoniou + * + * 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 +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#define AFB_BINDING_VERSION 3 +#include + +#include "bluetooth-api.h" +#include "bluetooth-common.h" + +G_DEFINE_QUARK(bluetooth-binding-error-quark, nb_error) + +/* convert dbus key to lower case */ +gboolean auto_lowercase_keys = TRUE; + +void signal_init_done(struct init_data *id, int rc) +{ + g_mutex_lock(&id->mutex); + id->init_done = TRUE; + id->rc = rc; + g_cond_signal(&id->cond); + g_mutex_unlock(&id->mutex); +} + +int str2boolean(const char *value) +{ + if (!strcmp(value, "1") || !g_ascii_strcasecmp(value, "true") || + !g_ascii_strcasecmp(value, "on") || !g_ascii_strcasecmp(value, "enabled") || + !g_ascii_strcasecmp(value, "yes")) + return TRUE; + if (!strcmp(value, "0") || !g_ascii_strcasecmp(value, "false") || + !g_ascii_strcasecmp(value, "off") || !g_ascii_strcasecmp(value, "disabled") || + !g_ascii_strcasecmp(value, "no")) + return FALSE; + return -1; +} + +json_object *json_object_copy(json_object *jval) +{ + json_object *jobj; + int i, len; + + /* handle NULL */ + if (!jval) + return NULL; + + switch (json_object_get_type(jval)) { + case json_type_object: + jobj = json_object_new_object(); + json_object_object_foreach(jval, key, jval2) + json_object_object_add(jobj, key, + json_object_copy(jval2)); + + return jobj; + + case json_type_array: + jobj = json_object_new_array(); + len = json_object_array_length(jval); + for (i = 0; i < len; i++) + json_object_array_add(jobj, + json_object_copy( + json_object_array_get_idx(jval, i))); + return jobj; + + case json_type_null: + return NULL; + + case json_type_boolean: + return json_object_new_boolean( + json_object_get_boolean(jval)); + + case json_type_double: + return json_object_new_double( + json_object_get_double(jval)); + + case json_type_int: + return json_object_new_int64( + json_object_get_int64(jval)); + + case json_type_string: + return json_object_new_string( + json_object_get_string(jval)); + } + + g_assert(0); + /* should never happen */ + return NULL; +} + +gchar *key_dbus_to_json(const gchar *key, gboolean auto_lower) +{ + gchar *lower, *s; + + lower = g_strdup(key); + g_assert(lower); + + if (!auto_lower) + return lower; + + /* convert to lower case */ + for (s = lower; *s; s++) + *s = g_ascii_tolower(*s); + + return lower; +} + +json_object *simple_gvariant_to_json(GVariant *var, json_object *parent, + gboolean recurse) +{ + json_object *obj = NULL, *item; + gint32 i32; + gint64 i64; + guint32 ui32; + guint64 ui64; + GVariantIter iter; + GVariant *key, *value; + gchar *json_key; + gsize nitems; + gboolean is_dict; + + obj = NULL; + + /* AFB_DEBUG("g_variant_classify(var)=%c", g_variant_classify(var)); */ + + /* we only handle simple types */ + switch (g_variant_classify(var)) { + case G_VARIANT_CLASS_BOOLEAN: + obj = json_object_new_boolean(g_variant_get_boolean(var)); + break; + case G_VARIANT_CLASS_INT16: + obj = json_object_new_int(g_variant_get_int16(var)); + break; + case G_VARIANT_CLASS_INT32: + i32 = g_variant_get_int32(var); + obj = json_object_new_int(i32); + break; + case G_VARIANT_CLASS_INT64: + i64 = g_variant_get_int64(var); + if (i64 >= -(1L << 31) && i64 < (1L << 31)) + obj = json_object_new_int((int)i64); + else + obj = json_object_new_int64(i64); + break; + case G_VARIANT_CLASS_BYTE: + obj = json_object_new_int((int)g_variant_get_byte(var)); + break; + case G_VARIANT_CLASS_UINT16: + obj = json_object_new_int((int)g_variant_get_uint16(var)); + break; + case G_VARIANT_CLASS_UINT32: + ui32 = g_variant_get_uint32(var); + if (ui32 < (1U << 31)) + obj = json_object_new_int(ui32); + else + obj = json_object_new_int64(ui32); + break; + case G_VARIANT_CLASS_UINT64: + ui64 = g_variant_get_uint64(var); + if (ui64 < (1U << 31)) + obj = json_object_new_int((int)ui64); + else if (ui64 < (1LLU << 63)) + obj = json_object_new_int64(ui64); + else { + AFB_WARNING("U64 value %llu clamped to %llu", + (unsigned long long)ui64, + (unsigned long long)((1LLU << 63) - 1)); + obj = json_object_new_int64((1LLU << 63) - 1); + } + break; + case G_VARIANT_CLASS_DOUBLE: + obj = json_object_new_double(g_variant_get_double(var)); + break; + case G_VARIANT_CLASS_OBJECT_PATH: + case G_VARIANT_CLASS_STRING: + obj = json_object_new_string(g_variant_get_string(var, NULL)); + break; + + case G_VARIANT_CLASS_ARRAY: + + if (!recurse) + break; + + /* detect dictionaries which are arrays of dict entries */ + g_variant_iter_init(&iter, var); + + nitems = g_variant_iter_n_children(&iter); + /* remove completely empty arrays */ + if (nitems == 0) + break; + + is_dict = nitems > 0; + while (is_dict && (value = g_variant_iter_next_value(&iter))) { + is_dict = g_variant_classify(value) == G_VARIANT_CLASS_DICT_ENTRY; + g_variant_unref(value); + } + + if (is_dict) + obj = json_object_new_object(); + else + obj = json_object_new_array(); + + g_variant_iter_init(&iter, var); + while ((value = g_variant_iter_next_value(&iter))) { + + item = simple_gvariant_to_json(value, obj, TRUE); + if (!is_dict && item) + json_object_array_add(obj, item); + + g_variant_unref(value); + } + break; + + case G_VARIANT_CLASS_DICT_ENTRY: + + if (!recurse) + break; + + if (!parent) { + AFB_WARNING("#### dict new object without a parent"); + break; + } + + g_variant_iter_init(&iter, var); + while ((key = g_variant_iter_next_value(&iter))) { + + value = g_variant_iter_next_value(&iter); + if (!value) { + AFB_WARNING("Out of values with a key"); + g_variant_unref(key); + break; + } + + json_key = key_dbus_to_json( + g_variant_get_string(key, NULL), + auto_lowercase_keys); + + /* only handle dict values with string keys */ + if (g_variant_classify(key) == G_VARIANT_CLASS_STRING) { + item = simple_gvariant_to_json(value, obj, TRUE); + if (item) + json_object_object_add(parent, json_key, item); + + } else + AFB_WARNING("Can't handle non-string key"); + + g_free(json_key); + + g_variant_unref(value); + g_variant_unref(key); + } + break; + + case G_VARIANT_CLASS_VARIANT: + + /* NOTE: recurse allowed because we only allow a single encapsulated variant */ + + g_variant_iter_init(&iter, var); + nitems = g_variant_iter_n_children(&iter); + if (nitems != 1) { + AFB_WARNING("Can't handle variants with more than one children (%lu)", nitems); + break; + } + + while ((value = g_variant_iter_next_value(&iter))) { + obj = simple_gvariant_to_json(value, parent, TRUE); + g_variant_unref(value); + break; + } + break; + + default: + AFB_WARNING("############ class is %c", g_variant_classify(var)); + obj = NULL; + break; + } + + return obj; +} + +gchar *property_name_dbus2json(const struct property_info *pi, + gboolean is_config) +{ + gchar *json_name; + gchar *cfgname; + + if (pi->json_name) + json_name = g_strdup(pi->json_name); + else + json_name = key_dbus_to_json(pi->name, auto_lowercase_keys); + + if (!json_name) + return NULL; + + if (!is_config) + return json_name; + + cfgname = g_strdup_printf("%s.%configuration", + json_name, + auto_lowercase_keys ? 'c' : 'C'); + g_free(json_name); + return cfgname; +} + +json_object *property_dbus2json( + const struct property_info **pip, + const gchar *key, GVariant *var, + gboolean *is_config) +{ + const struct property_info *pi = *pip, *pi2, *pi_sub; + GVariantIter iter, iter2; + json_object *obj = NULL, *obji; + const char *fmt; + GVariant *value, *dict_value, *dict_key; + const gchar *sub_key; + gchar *json_key; + gboolean is_subconfig; + + if (key) { + pi = property_by_dbus_name(pi, key, is_config); + if (!pi) + return NULL; + *pip = pi; + } + + fmt = pi->fmt; + + obj = simple_gvariant_to_json(var, NULL, FALSE); + if (obj) { + /* TODO check fmt for matching type */ + return obj; + } + + switch (*fmt) { + case 'a': /* array */ + obj = json_object_new_array(); + + g_variant_iter_init(&iter, var); + while ((value = g_variant_iter_next_value(&iter))) { + pi2 = pi; + obji = property_dbus2json(&pi2, NULL, value, + &is_subconfig); + if (obji) + json_object_array_add(obj, obji); + + g_variant_unref(value); + } + break; + case '{': + /* we only support {sX} */ + + /* there must be a sub property entry */ + g_assert(pi->sub); + + obj = json_object_new_object(); + + g_variant_iter_init(&iter, var); + while ((value = g_variant_iter_next_value(&iter))) { + + if (g_variant_classify(value) != G_VARIANT_CLASS_DICT_ENTRY) { + AFB_WARNING("Expecting dict got '%c'", g_variant_classify(value)); + g_variant_unref(value); + break; + } + + g_variant_iter_init(&iter2, value); + while ((dict_key = g_variant_iter_next_value(&iter2))) { + if (g_variant_classify(dict_key) != G_VARIANT_CLASS_STRING) { + AFB_WARNING("Can't handle non-string dict keys '%c'", + g_variant_classify(dict_key)); + g_variant_unref(dict_key); + g_variant_unref(value); + continue; + } + + dict_value = g_variant_iter_next_value(&iter2); + if (!dict_value) { + AFB_WARNING("Out of values with a dict_key"); + g_variant_unref(dict_key); + g_variant_unref(value); + break; + } + + sub_key = g_variant_get_string(dict_key, NULL); + + pi_sub = pi->sub; + while (pi_sub->name) { + if (!g_strcmp0(sub_key, pi_sub->name)) + break; + pi_sub++; + } + + if (pi_sub->name) { + pi2 = pi_sub; + obji = property_dbus2json(&pi2, + sub_key, dict_value, + &is_subconfig); + if (obji) { + json_key = property_name_dbus2json(pi2, FALSE); + json_object_object_add(obj, json_key, obji); + g_free(json_key); + } + } else + AFB_INFO("Unhandled %s/%s property", key, sub_key); + + g_variant_unref(dict_value); + g_variant_unref(dict_key); + } + + g_variant_unref(value); + } + + break; + } + + if (!obj) + AFB_INFO("# %s not a type we can handle", key ? key : ""); + + return obj; +} + +const struct property_info *property_by_dbus_name( + const struct property_info *pi, + const gchar *dbus_name, + gboolean *is_config) +{ + const struct property_info *pit; + const gchar *suffix; + gchar *tmpname; + size_t len; + + /* direct match first */ + pit = pi; + while (pit->name) { + if (!g_strcmp0(dbus_name, pit->name)) { + if (is_config) + *is_config = FALSE; + return pit; + } + pit++; + } + + /* try to see if a matching config property exists */ + suffix = strrchr(dbus_name, '.'); + if (!suffix || g_ascii_strcasecmp(suffix, ".Configuration")) + return NULL; + + /* it's a (possible) .config property */ + len = suffix - dbus_name; + tmpname = alloca(len + 1); + memcpy(tmpname, dbus_name, len); + tmpname[len] = '\0'; + + /* match with config */ + pit = pi; + while (pit->name) { + if (!(pit->flags & PI_CONFIG)) { + pit++; + continue; + } + if (!g_strcmp0(tmpname, pit->name)) { + if (is_config) + *is_config = TRUE; + return pit; + } + pit++; + } + + return NULL; +} + +const struct property_info *property_by_json_name( + const struct property_info *pi, + const gchar *json_name, + gboolean *is_config) +{ + const struct property_info *pit; + gchar *this_json_name; + const gchar *suffix; + gchar *tmpname; + size_t len; + + /* direct match */ + pit = pi; + while (pit->name) { + this_json_name = property_name_dbus2json(pit, FALSE); + if (!g_strcmp0(this_json_name, json_name)) { + g_free(this_json_name); + if (is_config) + *is_config = FALSE; + return pit; + } + g_free(this_json_name); + pit++; + } + + /* try to see if a matching config property exists */ + suffix = strrchr(json_name, '.'); + if (!suffix || g_ascii_strcasecmp(suffix, ".configuration")) + return NULL; + + /* it's a (possible) .config property */ + len = suffix - json_name; + tmpname = alloca(len + 1); + memcpy(tmpname, json_name, len); + tmpname[len] = '\0'; + + /* match with config */ + pit = pi; + while (pit->name) { + if (!(pit->flags & PI_CONFIG)) { + pit++; + continue; + } + this_json_name = property_name_dbus2json(pit, FALSE); + if (!g_strcmp0(this_json_name, tmpname)) { + g_free(this_json_name); + if (is_config) + *is_config = TRUE; + return pit; + } + g_free(this_json_name); + pit++; + } + + return NULL; +} + +const struct property_info *property_by_name( + const struct property_info *pi, + gboolean is_json_name, const gchar *name, + gboolean *is_config) +{ + return is_json_name ? + property_by_json_name(pi, name, is_config) : + property_by_dbus_name(pi, name, is_config); +} + +gchar *property_get_json_name(const struct property_info *pi, + const gchar *name) +{ + gboolean is_config; + + pi = property_by_dbus_name(pi, name, &is_config); + if (!pi) + return NULL; + return property_name_dbus2json(pi, is_config); +} + +gchar *configuration_dbus_name(const gchar *dbus_name) +{ + return g_strdup_printf("%s.Configuration", dbus_name); +} + +gchar *configuration_json_name(const gchar *json_name) +{ + return g_strdup_printf("%s.configuration", json_name); +} + +gchar *property_get_name(const struct property_info *pi, + const gchar *json_name) +{ + gboolean is_config; + + pi = property_by_json_name(pi, json_name, &is_config); + if (!pi) + return NULL; + + return !is_config ? g_strdup(pi->name) : + configuration_dbus_name(pi->name); +} + +gboolean root_property_dbus2json( + json_object *jparent, + const struct property_info *pi, + const gchar *key, GVariant *var, + gboolean *is_config) +{ + json_object *obj; + gchar *json_key; + + obj = property_dbus2json(&pi, key, var, is_config); + if (!obj) + return FALSE; + + switch (json_object_get_type(jparent)) { + case json_type_object: + json_key = property_name_dbus2json(pi, *is_config); + json_object_object_add(jparent, json_key, obj); + g_free(json_key); + break; + case json_type_array: + json_object_array_add(jparent, obj); + break; + default: + json_object_put(obj); + return FALSE; + } + + return TRUE; +} + +static const GVariantType *type_from_fmt(const char *fmt) +{ + switch (*fmt) { + case 'b': /* gboolean */ + return G_VARIANT_TYPE_BOOLEAN; + case 'y': /* guchar */ + return G_VARIANT_TYPE_BYTE; + case 'n': /* gint16 */ + return G_VARIANT_TYPE_INT16; + case 'q': /* guint16 */ + return G_VARIANT_TYPE_UINT16; + case 'h': + return G_VARIANT_TYPE_HANDLE; + case 'i': /* gint32 */ + return G_VARIANT_TYPE_INT32; + case 'u': /* guint32 */ + return G_VARIANT_TYPE_UINT32; + case 'x': /* gint64 */ + return G_VARIANT_TYPE_INT64; + case 't': /* gint64 */ + return G_VARIANT_TYPE_UINT64; + case 'd': /* double */ + return G_VARIANT_TYPE_DOUBLE; + case 's': /* string */ + return G_VARIANT_TYPE_STRING; + case 'o': /* object */ + return G_VARIANT_TYPE_OBJECT_PATH; + case 'g': /* signature */ + return G_VARIANT_TYPE_SIGNATURE; + case 'v': /* variant */ + return G_VARIANT_TYPE_VARIANT; + } + /* nothing complex */ + return NULL; +} + +GVariant *property_json_to_gvariant( + const struct property_info *pi, + const char *fmt, + const struct property_info *pi_parent, + json_object *jval, + GError **error) +{ + const struct property_info *pi_sub; + GVariant *arg, *item; + GVariantBuilder builder; + json_object *jitem; + json_bool b; + gchar *dbus_name; + int64_t i64; + double d; + const char *jvalstr, *str; + char c; + int i, len; + gboolean is_config; + + if (!fmt) + fmt = pi->fmt; + + if (!jval) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "can't encode json NULL type"); + return NULL; + } + + jvalstr = json_object_to_json_string(jval); + + arg = NULL; + + b = FALSE; + i64 = 0; + d = 0.0; + str = NULL; + + /* conversion and type check */ + c = *fmt++; + if (c == 'a') { + if (!json_object_is_type(jval, json_type_array)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property \"%s\" (not an array)", + jvalstr); + return NULL; + } + + len = json_object_array_length(jval); + /* special case for empty array */ + if (!len) { + arg = g_variant_new_array(type_from_fmt(fmt), NULL, 0); + if (!arg) + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Can't create empty array on \"%s\"", + jvalstr); + return arg; + } + + g_variant_builder_init(&builder, G_VARIANT_TYPE_ARRAY); + for (i = 0; i < len; i++) { + jitem = json_object_array_get_idx(jval, i); + item = property_json_to_gvariant(pi, fmt, NULL, jitem, error); + if (!item) { + g_variant_builder_clear(&builder); + return NULL; + } + g_variant_builder_add_value(&builder, item); + } + + arg = g_variant_builder_end(&builder); + + if (!arg) + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Can't handle array on \"%s\"", + jvalstr); + + return arg; + } + if (c == '{') { + g_assert(pi->sub); + + c = *fmt++; + /* we only handle string keys */ + if (c != 's') { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Can't handle non-string keys on \"%s\"", + jvalstr); + return NULL; + } + c = *fmt++; + + /* this is arguably wrong */ + g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); + pi_sub = pi->sub; + json_object_object_foreach(jval, key_o, jval_o) { + pi_sub = property_by_json_name(pi->sub, key_o, &is_config); + if (!pi_sub) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Unknown sub-property %s in \"%s\"", + key_o, json_object_to_json_string(jval_o)); + return NULL; + } + item = property_json_to_gvariant(pi_sub, NULL, pi, jval_o, error); + if (!item) + return NULL; + + dbus_name = property_get_name(pi->sub, key_o); + g_assert(dbus_name); /* can't fail; but check */ + + g_variant_builder_add(&builder, pi->fmt, dbus_name, item); + + g_free(dbus_name); + } + + arg = g_variant_builder_end(&builder); + + if (!arg) + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Can't handle object on \"%s\"", + jvalstr); + return arg; + } + + switch (c) { + case 'b': /* gboolean */ + if (!json_object_is_type(jval, json_type_boolean)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property \"%s\" (not a boolean)", + jvalstr); + return NULL; + } + b = json_object_get_boolean(jval); + break; + case 'y': /* guchar */ + case 'n': /* gint16 */ + case 'q': /* guint16 */ + case 'h': + case 'i': /* gint32 */ + case 'u': /* guint32 */ + case 'x': /* gint64 */ + case 't': /* gint64 */ + if (!json_object_is_type(jval, json_type_int)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property \"%s\" (not an integer)", + jvalstr); + return NULL; + } + /* note unsigned 64 bit values shall be truncated */ + i64 = json_object_get_int64(jval); + break; + + case 'd': /* double */ + if (!json_object_is_type(jval, json_type_double)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property \"%s\" (not a double)", + jvalstr); + return NULL; + } + d = json_object_get_double(jval); + break; + case 's': /* string */ + case 'o': /* object */ + case 'g': /* signature */ + if (!json_object_is_type(jval, json_type_string)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property \"%s\" (not a string)", + jvalstr); + return NULL; + } + str = json_object_get_string(jval); + break; + case 'v': /* variant */ + AFB_WARNING("Can't handle variant yet"); + break; + } + + /* build gvariant */ + switch (c) { + case 'b': /* gboolean */ + arg = g_variant_new_boolean(b); + break; + case 'y': /* guchar */ + if (i64 < 0 || i64 > 255) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property %s (out of byte range)", + jvalstr); + return FALSE; + } + arg = g_variant_new_byte((guchar)i64); + break; + case 'n': /* gint16 */ + if (i64 < -(1LL << 15) || i64 >= (1LL << 15)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property %s (out of int16 range)", + jvalstr); + return FALSE; + } + arg = g_variant_new_int16((gint16)i64); + break; + case 'q': /* guint16 */ + if (i64 < 0 || i64 >= (1LL << 16)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property %s (out of uint16 range)", + jvalstr); + return FALSE; + } + arg = g_variant_new_uint16((guint16)i64); + break; + case 'h': + case 'i': /* gint32 */ + if (i64 < -(1LL << 31) || i64 >= (1LL << 31)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property %s (out of int32 range)", + jvalstr); + return FALSE; + } + arg = g_variant_new_int32((gint32)i64); + break; + case 'u': /* guint32 */ + if (i64 < 0 || i64 >= (1LL << 32)) { + g_set_error(error, NB_ERROR, NB_ERROR_BAD_PROPERTY, + "Bad property %s (out of uint32 range)", + jvalstr); + return FALSE; + } + arg = g_variant_new_uint32((guint32)i64); + break; + case 'x': /* gint64 */ + arg = g_variant_new_int64(i64); + break; + case 't': /* gint64 */ + arg = g_variant_new_uint64(i64); + break; + case 'd': /* double */ + arg = g_variant_new_double(d); + break; + case 's': /* string */ + arg = g_variant_new_string(str); + break; + case 'o': /* object */ + arg = g_variant_new_object_path(str); + break; + case 'g': /* signature */ + arg = g_variant_new_signature(str); + break; + case 'v': /* variant */ + AFB_WARNING("Can't handle variant yet"); + break; + } + + return arg; +} + +json_object *get_property_collect(json_object *jreqprop, json_object *jprop, + GError **error) +{ + int i, len; + json_object *jkey, *jval, *jobj = NULL, *jobjval; + const char *key; + + + /* printf("jreqprop=%s\n", json_object_to_json_string_ext(jreqprop, + JSON_C_TO_STRING_SPACED)); + printf("jprop=%s\n", json_object_to_json_string_ext(jprop, + JSON_C_TO_STRING_SPACED)); */ + + /* get is an array of strings (or an object for subtype */ + g_assert(json_object_get_type(jreqprop) == json_type_array); + + len = json_object_array_length(jreqprop); + if (len == 0) + return NULL; + + for (i = 0; i < len; i++) { + jkey = json_object_array_get_idx(jreqprop, i); + + /* string key */ + if (json_object_is_type(jkey, json_type_string)) { + key = json_object_get_string(jkey); + if (!json_object_object_get_ex(jprop, key, &jval)) { + g_set_error(error, + NB_ERROR, NB_ERROR_BAD_PROPERTY, + "can't find key %s", key); + json_object_put(jobj); + return NULL; + } + + if (!jobj) + jobj = json_object_new_object(); + + json_object_object_add(jobj, key, + json_object_copy(jval)); + + } else if (json_object_is_type(jkey, json_type_object)) { + /* recursing into an object */ + + json_object_object_foreach(jkey, key_o, jval_o) { + if (!json_object_object_get_ex(jprop, key_o, + &jval)) { + g_set_error(error, NB_ERROR, + NB_ERROR_BAD_PROPERTY, + "can't find key %s", key_o); + json_object_put(jobj); + return NULL; + } + + /* jval_o is on jreqprop */ + /* jval is on jprop */ + + jobjval = get_property_collect(jval_o, jval, + error); + + if (!jobjval && error && *error) { + json_object_put(jobj); + return NULL; + } + + if (jobjval) { + if (!jobj) + jobj = json_object_new_object(); + + json_object_object_add(jobj, key_o, jobjval); + } + } + } + } + + /* if (jobj) + printf("jobj=%s\n", json_object_to_json_string_ext(jobj, + JSON_C_TO_STRING_SPACED)); */ + + return jobj; +} + +json_object *get_named_property(const struct property_info *pi, + gboolean is_json_name, const char *name, json_object *jprop) +{ + json_object *jret = NULL; + gchar *json_name = NULL; + + if (!is_json_name) { + json_name = property_get_json_name(pi, name); + if (!json_name) + return NULL; + name = json_name; + } + + json_object_object_foreach(jprop, key, jval) { + if (!g_strcmp0(key, name)) { + jret = json_object_copy(jval); + break; + } + } + + g_free(json_name); + + return jret; +} -- cgit 1.2.3-korg