From patchwork Fri Oct 16 22:09:32 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Peter Mullen X-Patchwork-Id: 285280 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-8.2 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID, HEADER_FROM_DIFFERENT_DOMAINS, INCLUDES_PATCH, MAILING_LIST_MULTI, SPF_HELO_NONE, SPF_PASS, USER_AGENT_SANE_1 autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 83F87C433DF for ; Fri, 16 Oct 2020 22:09:35 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id F307D2065D for ; Fri, 16 Oct 2020 22:09:34 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=tcd-ie.20150623.gappssmtp.com header.i=@tcd-ie.20150623.gappssmtp.com header.b="gwhDZMwh" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S2388841AbgJPWJe (ORCPT ); Fri, 16 Oct 2020 18:09:34 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:40864 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S2388807AbgJPWJe (ORCPT ); Fri, 16 Oct 2020 18:09:34 -0400 Received: from mail-pj1-x1043.google.com (mail-pj1-x1043.google.com [IPv6:2607:f8b0:4864:20::1043]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id DE9C8C061755 for ; Fri, 16 Oct 2020 15:09:33 -0700 (PDT) Received: by mail-pj1-x1043.google.com with SMTP id hk7so2249502pjb.2 for ; Fri, 16 Oct 2020 15:09:33 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=tcd-ie.20150623.gappssmtp.com; s=20150623; h=from:subject:to:message-id:date:user-agent:mime-version :content-language:content-transfer-encoding; bh=n1+giSsolW0YEWNRCDDe69POzBrY4Vg661V09byvQ1M=; b=gwhDZMwhkvBD1mpqF9lT0hEJgN7e1RoMkyYGvcX8xMH/qKU/jnReOh86d82s8/tKil Zzw8qxn+e0Edfi14Hr4FN+Q5tqBqZzsIaa3dDESNVoZQsaYY4ZfcZRLKLBG2blH4tpiI a1FTn/4cpkWkaXd2wPQrVDFncaL7xA3e24oKnGQU1B7FdQUO/E+1wVhofbWwvcl/CGxX OiaqMSYc77Lc0mAVIrhQk5kor0i/GqING+s4EIqQklmmH2pp8a6tS77jvBADg5h9ioIz iyqY0okw5DVWgjfl/htMpv3kdCIuSve0KxhgiMePMTm/C7A59E3Fv11gOJ8XnHTP46lP rwVA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:subject:to:message-id:date:user-agent :mime-version:content-language:content-transfer-encoding; bh=n1+giSsolW0YEWNRCDDe69POzBrY4Vg661V09byvQ1M=; b=L8hm0cZrTBY5k9abtgy8OxpsbDo1eBVbAo3rwJ4ew7i41rei8/sZNg9Dg3A+Y18gZA AyBpTuJn7SPKm5SA5pJPSewPrRvdxsiCQZ62o/LyVkHKmsK++udPdhxssOYNQau62cwr J/oyynqdmWyn/9RMW0dFCUTUdGNqN3GuQ6yuLcY4i3M4vhPCcDXxqT7Fp1YktGaosU0E 5TBBw7uX3LGJYgVTsucfsEE+VSElPkrPgXI77EMNxx7vCfzQZh9CFEdvzW7VzSADZRFX vv429EDScY/OuXTB17P/PXGRuk20G4W3OLaPMjJ28PsWaITE9wyUlX2u7lo6pxGIS7JH 2i4w== X-Gm-Message-State: AOAM532keU6o6alJc2OU73xMrldAiGXFOvUe0ibbM+YhxdCeMbaPFARr lWCp5CB/yG+v9EG/zwDK8en1KBgItgR3wqEE X-Google-Smtp-Source: ABdhPJx3m2gdq/oHubGKAGvyLOgNrR+OM7K4jaCDkoKgAwRuqOlE6ORNIWrywQ5BCiLiYBQ8lXKcQg== X-Received: by 2002:a17:902:8348:b029:d3:781a:5857 with SMTP id z8-20020a1709028348b02900d3781a5857mr5940234pln.2.1602886173218; Fri, 16 Oct 2020 15:09:33 -0700 (PDT) Received: from [192.168.2.14] (216-19-182-159.dyn.novuscom.net. [216.19.182.159]) by smtp.gmail.com with ESMTPSA id z18sm3780604pfn.158.2020.10.16.15.09.32 for (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128); Fri, 16 Oct 2020 15:09:32 -0700 (PDT) From: Peter Mullen Subject: [PATCH BlueZ 1/3] profile: Add Vendor/Product ID profile matching To: "linux-bluetooth@vger.kernel.org" Message-ID: Date: Fri, 16 Oct 2020 15:09:32 -0700 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Thunderbird/68.12.0 MIME-Version: 1.0 Content-Language: en-US Precedence: bulk List-ID: X-Mailing-List: linux-bluetooth@vger.kernel.org Extend the process for matching profiles to devices by adding vid/pid fields to btd_profile. If no remote_uuid is supplied, attempt to match based on the new fields. If only vid field is set, ignore pid during matching. --- src/device.c | 37 ++++++++++++++++++++++++++----------- src/profile.h | 3 +++ 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/device.c b/src/device.c index a5ef46730..2cb2ceb2e 100644 --- a/src/device.c +++ b/src/device.c @@ -329,7 +329,8 @@ static GSList *find_service_with_uuid(GSList *list, char *uuid) struct btd_service *service = l->data; struct btd_profile *profile = btd_service_get_profile(service); - if (bt_uuid_strcmp(profile->remote_uuid, uuid) == 0) + if (profile->remote_uuid && + bt_uuid_strcmp(profile->remote_uuid, uuid) == 0) return l; } @@ -1901,11 +1902,19 @@ static struct btd_service *find_connectable_service(struct btd_device *dev, struct btd_service *service = l->data; struct btd_profile *p = btd_service_get_profile(service); - if (!p->connect || !p->remote_uuid) + if (!p->connect) continue; - if (strcasecmp(uuid, p->remote_uuid) == 0) + if (p->remote_uuid && strcasecmp(uuid, p->remote_uuid) == 0) return service; + + if (p->vendor_id && p->vendor_id == + btd_device_get_vendor(dev)) { + if (p->product_id && p->product_id != + btd_device_get_product(dev)) + continue; + return service; + } } return NULL; @@ -3800,14 +3809,18 @@ static bool device_match_profile(struct btd_device *device, struct btd_profile *profile, GSList *uuids) { - if (profile->remote_uuid == NULL) - return false; - - if (g_slist_find_custom(uuids, profile->remote_uuid, - bt_uuid_strcmp) == NULL) - return false; + if (profile->remote_uuid && g_slist_find_custom(uuids, + profile->remote_uuid, bt_uuid_strcmp) != NULL) + return true; - return true; + if (profile->vendor_id && profile->vendor_id == + btd_device_get_vendor(device)) { + if (profile->product_id && profile->product_id != + btd_device_get_product(device)) + return false; + return true; + } + return false; } static void add_gatt_service(struct gatt_db_attribute *attr, void *user_data) @@ -6729,6 +6742,8 @@ void btd_device_set_record(struct btd_device *device, const char *uuid, const sdp_record_t *btd_device_get_record(struct btd_device *device, const char *uuid) { + if (uuid == NULL) + return NULL; /* Load records from storage if there is nothing in cache */ if (!device->tmp_records) { device->tmp_records = read_device_records(device); @@ -6881,7 +6896,7 @@ struct btd_service *btd_device_get_service(struct btd_device *dev, struct btd_service *service = l->data; struct btd_profile *p = btd_service_get_profile(service); - if (g_str_equal(p->remote_uuid, remote_uuid)) + if (p->remote_uuid && g_str_equal(p->remote_uuid, remote_uuid)) return service; } diff --git a/src/profile.h b/src/profile.h index 6827f8481..fe882a843 100644 --- a/src/profile.h +++ b/src/profile.h @@ -21,6 +21,9 @@ struct btd_profile { const char *local_uuid; const char *remote_uuid; + uint16_t vendor_id; + uint16_t product_id; + bool auto_connect; /* Some profiles are considered safe to be handled internally and also * be exposed in the GATT API. This flag give such profiles exception -- 2.28.0 From patchwork Fri Oct 16 22:09:48 2020 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Peter Mullen X-Patchwork-Id: 285279 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org X-Spam-Level: X-Spam-Status: No, score=-8.2 required=3.0 tests=BAYES_00,DKIM_SIGNED, DKIM_VALID, HEADER_FROM_DIFFERENT_DOMAINS, INCLUDES_PATCH, MAILING_LIST_MULTI, SPF_HELO_NONE, SPF_PASS, URIBL_BLOCKED, USER_AGENT_SANE_1 autolearn=ham autolearn_force=no version=3.4.0 Received: from mail.kernel.org (mail.kernel.org [198.145.29.99]) by smtp.lore.kernel.org (Postfix) with ESMTP id 8E7E2C433DF for ; Fri, 16 Oct 2020 22:09:52 +0000 (UTC) Received: from vger.kernel.org (vger.kernel.org [23.128.96.18]) by mail.kernel.org (Postfix) with ESMTP id 23A5B20829 for ; Fri, 16 Oct 2020 22:09:52 +0000 (UTC) Authentication-Results: mail.kernel.org; dkim=pass (2048-bit key) header.d=tcd-ie.20150623.gappssmtp.com header.i=@tcd-ie.20150623.gappssmtp.com header.b="qnsBs4i6" Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S2388912AbgJPWJv (ORCPT ); Fri, 16 Oct 2020 18:09:51 -0400 Received: from lindbergh.monkeyblade.net ([23.128.96.19]:40910 "EHLO lindbergh.monkeyblade.net" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S2388807AbgJPWJv (ORCPT ); Fri, 16 Oct 2020 18:09:51 -0400 Received: from mail-pl1-x644.google.com (mail-pl1-x644.google.com [IPv6:2607:f8b0:4864:20::644]) by lindbergh.monkeyblade.net (Postfix) with ESMTPS id 6C46FC061755 for ; Fri, 16 Oct 2020 15:09:50 -0700 (PDT) Received: by mail-pl1-x644.google.com with SMTP id d6so1976220plo.13 for ; Fri, 16 Oct 2020 15:09:50 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=tcd-ie.20150623.gappssmtp.com; s=20150623; h=from:subject:to:message-id:date:user-agent:mime-version :content-language:content-transfer-encoding; bh=grk8HjhASsXnzA5vVkzFWsweZv2u3f+WqEoMPNLzS0Y=; b=qnsBs4i6hgIod/eY0l1TrlEUoUXOeMPhrvE9BJ4r6oML7PSoHhRz+u1DkEOgee0mZW EOZ/uvrH1UoupQcZYmdMlgE13Tnf1+BYm96a0r6fiONwxsCY5b+4H5+ZAl7Y/YVeHKq1 bH4Hg2PCaFi4L24f5eJVOGVF+THoaL2V7FwnWEVDHRrCatYcrwv+E5q8hf4eB3x+rlnD SLPe2ay9vAeK8oiCNYYPvVzQdIp7RWbJdX1Wo+0EymxQSQ4SdUKbYQ4OQHs+3rbgI7Ko 12nqqWupKzPoa/laUkDvEqOUIsRItbpkuY9NA8LwWc7cjb+TcHgG3xB5p1HeWbWRrL9Q Qf0Q== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:from:subject:to:message-id:date:user-agent :mime-version:content-language:content-transfer-encoding; bh=grk8HjhASsXnzA5vVkzFWsweZv2u3f+WqEoMPNLzS0Y=; b=h/HDkDANSaNOJuuwqGaJw9Sz/f0te8rTXjnDtZq8QML8mpCKK9g1gEQth+4z0VbN2i GpCDaWGSCbo71gTwU9XN1dHVlYfE8p9YIsD7zIlE3Y9HCQfvFWz8EmaHg0/LNi5DwX+O q7cMU/2f1z/Ubk9nWl6v44DwdT/7VIretXASXESZ7xfBgCxDJpzugZYlYfR/8k1q17M1 MN/JXBOWqVXcbYVvW/uyYI2Ok2KyuQUyGTrM3dDzi3La9MnrmUFld+RpSzEcn6Fhp3OM j8XKx64suweKVxDAw0KxkyV51pWbWLM0ZKD+tosdK4OHnO5BG2o7rzDRWC3SPsb0k5aN /gGA== X-Gm-Message-State: AOAM532M/eqoyYR+8JR9EK1Vs650aXVT9/0f+5laaGREQIqZp7Sj19eI 9Fhup4/57aYGussUez0NdRzmR35Kan1FbHVO X-Google-Smtp-Source: ABdhPJxpjceVoeu0LXMimQqsCoTdq3Vunc5q2tL/wpC3Hl3h6434e9gnyUxsbmPidBvhup9rO5Y5iA== X-Received: by 2002:a17:90a:1bc3:: with SMTP id r3mr6252479pjr.196.1602886189318; Fri, 16 Oct 2020 15:09:49 -0700 (PDT) Received: from [192.168.2.14] (216-19-182-159.dyn.novuscom.net. [216.19.182.159]) by smtp.gmail.com with ESMTPSA id r8sm4658773pgn.30.2020.10.16.15.09.48 for (version=TLS1_3 cipher=TLS_AES_128_GCM_SHA256 bits=128/128); Fri, 16 Oct 2020 15:09:48 -0700 (PDT) From: Peter Mullen Subject: [PATCH BlueZ 3/3] plugins: Add Bose QC35 plugin To: "linux-bluetooth@vger.kernel.org" Message-ID: <2ffaa215-fc4b-c38b-e15c-d1286f070acb@tcd.ie> Date: Fri, 16 Oct 2020 15:09:48 -0700 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Thunderbird/68.12.0 MIME-Version: 1.0 Content-Language: en-US Precedence: bulk List-ID: X-Mailing-List: linux-bluetooth@vger.kernel.org The Bose QC35 headphones use a non-standard interface for handling battery level reporting. This plugin handles the rfcomm communication required for accessing extra controls/information from the device, including the battery level. The battery level is reported over the same DBus interface as for the standard battery characteristic for integration with existing systems. The same rfcomm communication allows control over the device's Active Noise Cancelling (ANC) level. The plugin registers an additional new dbus interface, org.bluez.Anc1, to allow external control over the ANC level. --- Makefile.plugins | 5 + configure.ac | 4 + plugins/bose-qc35.c | 623 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 632 insertions(+) create mode 100644 plugins/bose-qc35.c +{ + btd_profile_unregister(&bose_profile); +} + +BLUETOOTH_PLUGIN_DEFINE(bose_qc35, VERSION, BLUETOOTH_PLUGIN_PRIORITY_DEFAULT, + bose_init, bose_exit) diff --git a/Makefile.plugins b/Makefile.plugins index 3b673bf85..61b9d2275 100644 --- a/Makefile.plugins +++ b/Makefile.plugins @@ -112,3 +112,8 @@ plugins_sixaxis_la_LDFLAGS = $(AM_LDFLAGS) -module -avoid-version \ plugins_sixaxis_la_LIBADD = $(UDEV_LIBS) plugins_sixaxis_la_CFLAGS = $(AM_CFLAGS) -fvisibility=hidden endif + +if BOSE_QC35 +builtin_modules += bose_qc35 +builtin_sources += plugins/bose-qc35.c +endif diff --git a/configure.ac b/configure.ac index d6347c098..09ca43096 100644 --- a/configure.ac +++ b/configure.ac @@ -346,6 +346,10 @@ AC_ARG_ENABLE(sixaxis, AC_HELP_STRING([--enable-sixaxis], AM_CONDITIONAL(SIXAXIS, test "${enable_sixaxis}" = "yes" && test "${enable_udev}" != "no") +AC_ARG_ENABLE(bose_qc35, AC_HELP_STRING([--enable-bose-qc35], + [enable bose-qc35 plugin]), [enable_bose_qc35=${enableval}]) +AM_CONDITIONAL(BOSE_QC35, test "${enable_bose_qc35}" = "yes" ) + AC_ARG_ENABLE(hid2hci, AC_HELP_STRING([--enable-hid2hci], [enable hid2hci tool]), [enable_hid2hci=${enableval}]) AM_CONDITIONAL(HID2HCI, test "${enable_hid2hci}" = "yes" && diff --git a/plugins/bose-qc35.c b/plugins/bose-qc35.c new file mode 100644 index 000000000..4bece875f --- /dev/null +++ b/plugins/bose-qc35.c @@ -0,0 +1,623 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2020 Peter Mullen + * + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include + +#include "btio/btio.h" + +#include "gdbus/gdbus.h" + +#include "lib/bluetooth.h" +#include "lib/uuid.h" + +#include "src/adapter.h" +#include "src/dbus-common.h" +#include "src/device.h" +#include "src/plugin.h" +#include "src/profile.h" +#include "src/log.h" +#include "src/service.h" + +#define IAP_UUID "00000000-deca-fade-deca-deafdecacaff" +#define BATTERY_INTERFACE "org.bluez.Battery1" +#define ANC_INTERFACE "org.bluez.Anc1" +#define BOSE_ANC_RANGE 2 +#define BOSE_RFCOMM_CHANNEL 8 +#define BOSE_VENDOR_ID 158 +#define BOSE_DEVICE_CLASS 2360344 +#define BOSE_QC35_PRODUCT_ID 16396 + +/* + * The following resource was used as a starting reference for this plugin: + * https://blog.davidventura.com.ar/reverse-engineering-the-bose-qc35-bluetooth-protocol.html + */ + +enum bose_command { + BOSE_COMMAND_CONNECT = 0, + BOSE_COMMAND_QUERY_DEVICE_STATUS, + BOSE_COMMAND_QUERY_BATTERY, + BOSE_COMMAND_SET_ANC, + BOSE_COMMAND_END +}; + +#define BOSE_HEADER_LEN 4 +static const guint8 bose_command_codes[BOSE_COMMAND_END][BOSE_HEADER_LEN] = { + {0x00, 0x01, 0x01, 0x00}, /* CONNECT */ + {0x01, 0x01, 0x05, 0x00}, /* QUERY_DEVICE_STATUS */ + {0x02, 0x02, 0x01, 0x00}, /* QUERY_BATTERY */ + {0x01, 0x06, 0x02, 0x01}, /* SET_ANC */ +}; + +enum bose_message { + BOSE_MESSAGE_CONNECT_ACK = 0, + BOSE_MESSAGE_ANC_LEVEL, + BOSE_MESSAGE_BATTERY_LEVEL, + BOSE_MESSAGE_END +}; + +#define BOSE_OPCODE_LEN 3 +static const guint8 bose_message_codes[BOSE_MESSAGE_END][BOSE_OPCODE_LEN] = { + {0x00, 0x01, 0x03}, /* CONNECT_ACK */ + {0x01, 0x06, 0x03}, /* ANC_LEVEL */ + {0x02, 0x02, 0x03}, /* BATTERY_LEVEL */ +}; + +struct bose_session { + GIOChannel *io; + guint io_id; + char *path; + enum { + BOSE_CONNECTION_STATE_DISCONNECTED, + BOSE_CONNECTION_STATE_CONNECTING, + BOSE_CONNECTION_STATE_CONNECTED + } connection_state; + + uint8_t battery_level; + uint8_t anc_level; + uint8_t anc_level_set; +}; + +static int service_cb_id; +static GSList *bose_services; + +static void bose_reset(struct bose_session *session) +{ + if (session->io) { + g_io_channel_shutdown(session->io, FALSE, NULL); + g_io_channel_unref(session->io); + session->io = NULL; + } + if (session->io_id) { + g_source_remove(session->io_id); + session->io_id = 0; + } + if (session->path) { + g_dbus_unregister_interface(btd_get_dbus_connection(), + session->path, BATTERY_INTERFACE); + g_dbus_unregister_interface(btd_get_dbus_connection(), + session->path, ANC_INTERFACE); + g_free(session->path); + session->path = NULL; + } + + session->connection_state = BOSE_CONNECTION_STATE_DISCONNECTED; +} + +static void bose_session_destroy(struct btd_service *service, int err) +{ + struct bose_session *session = btd_service_get_user_data(service); + + if (!session) + return; + + int connection_state = session->connection_state; + + bose_reset(session); + + if (session->connection_state == BOSE_CONNECTION_STATE_CONNECTING) + btd_service_connecting_complete(service, err); + else if (session->connection_state == BOSE_CONNECTION_STATE_CONNECTED) + btd_service_disconnecting_complete(service, 0); +} + +static int bose_disconnect(struct btd_service *service) +{ + struct bose_session *session = btd_service_get_user_data(service); + + if (session->connection_state == BOSE_CONNECTION_STATE_DISCONNECTED) + return -EAGAIN; + + DBG("Bose profile disconnect"); + + bose_session_destroy(service, 0); + return 0; +} + +static int bose_free(struct btd_service *service) +{ + struct bose_session *session = btd_service_get_user_data(service); + + if (g_slist_find(bose_services, service) != NULL) { + bose_services = g_slist_remove(bose_services, service); + + if (g_slist_length(bose_services) == 0 && service_cb_id > 0) { + btd_service_remove_state_cb(service_cb_id); + service_cb_id = 0; + } + } + + if (!session) + return -ENOTCONN; + + bose_session_destroy(service, 0); + + g_free(session); + btd_service_set_user_data(service, NULL); + + return 0; +} + +static bool parse_anc_level(struct bose_session *session) +{ + guint8 msg[2]; + uint8_t new_level; + int fd = g_io_channel_unix_get_fd(session->io); + + if (recv(fd, &msg, sizeof(msg), MSG_DONTWAIT) < (ssize_t) sizeof(msg)) { + error("Failed to read ANC level payload"); + return false; + } + + new_level = msg[0]; + if (new_level == 3) + new_level = 1; + else if (new_level == 1) + new_level = 2; + else if (new_level != 0) { + warn("Received invalid ANC level"); + goto end; + } + + if (new_level != session->anc_level) { + session->anc_level = new_level; + g_dbus_emit_property_changed(btd_get_dbus_connection(), + session->path, ANC_INTERFACE, + "Level"); + } + +end: + session->anc_level_set = session->anc_level; + return true; +} + +static bool parse_battery_level(struct bose_session *session) +{ + uint8_t battery_level; + int fd = g_io_channel_unix_get_fd(session->io); + + if (recv(fd, &battery_level, sizeof(battery_level), MSG_DONTWAIT) != + (ssize_t) sizeof(battery_level)) { + error("Failed to read battery level payload"); + return false; + } + + if (battery_level != session->battery_level) { + session->battery_level = battery_level; + g_dbus_emit_property_changed(btd_get_dbus_connection(), + session->path, BATTERY_INTERFACE, + "Percentage"); + } + + return true; +} + +static void discard_data(int fd, ssize_t len) +{ + uint8_t buffer[64]; + + for (; len > 0; len -= sizeof(buffer)) { + ssize_t to_read = MIN(len, (ssize_t) sizeof(buffer)); + + if (to_read != recv(fd, buffer, to_read, MSG_DONTWAIT)) + return; + } +} + +static void parse_message(int fd, const uint8_t header[BOSE_HEADER_LEN], + struct bose_session *session) +{ + enum bose_message message; + bool payload_consumed = false; + + for (message = 0; message < BOSE_MESSAGE_END; ++message) { + if (memcmp(header, bose_message_codes[message], + BOSE_OPCODE_LEN) == 0) + break; + } + + switch (message) { + case BOSE_MESSAGE_BATTERY_LEVEL: + if (header[3] == 1) + payload_consumed = parse_battery_level(session); + break; + case BOSE_MESSAGE_CONNECT_ACK: + session->connection_state = BOSE_CONNECTION_STATE_CONNECTED; + break; + case BOSE_MESSAGE_ANC_LEVEL: + if (header[3] == 2) + payload_consumed = parse_anc_level(session); + break; + case BOSE_MESSAGE_END: + default: + DBG("Failed to determine packet type (invalid/unimplemented)"); + } + + if (!payload_consumed && header[3] > 0) + discard_data(fd, (ssize_t) header[3]); +} + +gboolean on_data_cb(GIOChannel *source, GIOCondition condition, gpointer data) +{ + uint8_t message_header[BOSE_HEADER_LEN]; + ssize_t len; + int fd; + struct btd_service *service = data; + struct bose_session *session = btd_service_get_user_data(service); + + if (condition & (G_IO_HUP | G_IO_ERR)) { + DBG("Received HUP/ERR"); + bose_session_destroy(service, -EIO); + return FALSE; + } + + if (!(G_IO_IN & condition)) + return TRUE; + + fd = g_io_channel_unix_get_fd(source); + while ((len = recv(fd, message_header, sizeof(message_header), + MSG_DONTWAIT)) >= 0) { + if (len != (ssize_t) sizeof(message_header)) { + error("Received corrupted packet"); + return TRUE; + } + + parse_message(fd, message_header, session); + } + return TRUE; +} + +static int send_command(GIOChannel *io, enum bose_command command, uint8_t data) +{ + uint8_t commandMsg[5]; + ssize_t msgLen; + int fd; + + if (command < 0 || command >= BOSE_COMMAND_END) { + error("Invalid command"); + return -EINVAL; + } + + memcpy(commandMsg, bose_command_codes[command], BOSE_HEADER_LEN); + msgLen = (ssize_t) (commandMsg[3] + BOSE_HEADER_LEN); + + if (msgLen > 5) { + error("Cannot handle message payload"); + return -EINVAL; + } + + if (msgLen == 5) + commandMsg[4] = data; + + fd = g_io_channel_unix_get_fd(io); + + if (write(fd, commandMsg, msgLen) != msgLen) { + error("Failed to send command"); + return -EIO; + } + + return 0; +} + +static gboolean property_get_percentage(const GDBusPropertyTable *property, + DBusMessageIter *iter, void *data) +{ + struct btd_service *service = data; + struct bose_session *session = btd_service_get_user_data(service); + + if (send_command(session->io, BOSE_COMMAND_QUERY_BATTERY, 0) >= 0) + on_data_cb(session->io, G_IO_IN, service); + + dbus_message_iter_append_basic(iter, DBUS_TYPE_BYTE, + &session->battery_level); + + return TRUE; +} + +static gboolean property_get_anc(const GDBusPropertyTable *property, + DBusMessageIter *iter, void *data) +{ + struct btd_service *service = data; + struct bose_session *session = btd_service_get_user_data(service); + + dbus_message_iter_append_basic(iter, DBUS_TYPE_BYTE, + &session->anc_level); + return TRUE; +} + +static void property_set_anc(const GDBusPropertyTable *property, + DBusMessageIter *value, + GDBusPendingPropertySet id, void *data) +{ + struct btd_service *service; + struct bose_session *session; + uint8_t new_level; + + if (dbus_message_iter_get_arg_type(value) != DBUS_TYPE_BYTE) + return; + + dbus_message_iter_get_basic(value, &new_level); + + service = data; + session = btd_service_get_user_data(service); + + if (new_level == session->anc_level_set) + return; + + session->anc_level_set = new_level; + + if (new_level == 1) + new_level = 3; + else if (new_level == 2) + new_level = 1; + else if (new_level != 0) + return; + + if (!send_command(session->io, BOSE_COMMAND_SET_ANC, new_level)) + on_data_cb(session->io, G_IO_IN, service); +} + +static gboolean property_get_range(const GDBusPropertyTable *property, + DBusMessageIter *iter, void *data) +{ + const uint8_t range = BOSE_ANC_RANGE; + + dbus_message_iter_append_basic(iter, DBUS_TYPE_BYTE, &range); + return TRUE; +} + +static const GDBusPropertyTable battery_properties[] = { + { "Percentage", "y", property_get_percentage }, + { } +}; + +static const GDBusPropertyTable anc_properties[] = { + { "Level", "y", property_get_anc, property_set_anc }, + { "Range", "y", property_get_range }, + { } +}; + +static void rfcomm_connect_cb(GIOChannel *io, GError *err, gpointer user_data) +{ + GIOCondition watch_cond; + struct btd_service *service; + struct bose_session *session; + struct btd_device *device; + + if (err) { + error("%s", err->message); + goto failed; + } + + if (send_command(io, BOSE_COMMAND_CONNECT, 0) || + send_command(io, BOSE_COMMAND_QUERY_BATTERY, 0) || + send_command(io, BOSE_COMMAND_QUERY_DEVICE_STATUS, 0)) + goto failed; + + service = user_data; + session = btd_service_get_user_data(service); + device = btd_service_get_device(service); + + session->path = g_strdup(device_get_path(device)); + + if (g_dbus_register_interface(btd_get_dbus_connection(), + session->path, BATTERY_INTERFACE, + NULL, NULL, + battery_properties, service, + NULL) == FALSE) { + error("Unable to register %s interface for %s", + BATTERY_INTERFACE, session->path); + + goto failed; + } + + if (g_dbus_register_interface(btd_get_dbus_connection(), + session->path, ANC_INTERFACE, + NULL, NULL, + anc_properties, service, + NULL) == FALSE) { + error("Unable to register %s interface for %s", + ANC_INTERFACE, session->path); + goto failed; + } + + on_data_cb(io, G_IO_IN, service); + + watch_cond = G_IO_HUP | G_IO_ERR | G_IO_NVAL | G_IO_IN; + session->io_id = g_io_add_watch(io, watch_cond, on_data_cb, service); + + session->connection_state = BOSE_CONNECTION_STATE_CONNECTED; + btd_service_connecting_complete(service, 0); + return; + +failed: + bose_session_destroy(service, -EIO); +} + +static int bose_connect(struct btd_service *service) +{ + struct btd_device *device; + struct btd_adapter *adapter; + struct bose_session *session = btd_service_get_user_data(service); + GError *err = NULL; + + if (session == NULL) + return -EINVAL; + + if (session->connection_state != BOSE_CONNECTION_STATE_DISCONNECTED) + return -EALREADY; + + device = btd_service_get_device(service); + adapter = device_get_adapter(device); + + session->io = bt_io_connect(rfcomm_connect_cb, service, + NULL, &err, + BT_IO_OPT_SOURCE_BDADDR, + btd_adapter_get_address(adapter), + BT_IO_OPT_DEST_BDADDR, + device_get_address(device), + BT_IO_OPT_CHANNEL, BOSE_RFCOMM_CHANNEL, + BT_IO_OPT_SEC_LEVEL, BT_IO_SEC_MEDIUM, + BT_IO_OPT_INVALID); + + session->connection_state = BOSE_CONNECTION_STATE_CONNECTING; + + if (!session->io || err) { + error("%s", err->message); + g_error_free(err); + bose_session_destroy(service, -EIO); + return -EIO; + } + + return 0; +} + +static void service_cb(struct btd_service *service, + btd_service_state_t old_state, + btd_service_state_t new_state, + void *user_data) +{ + struct btd_profile *profile = btd_service_get_profile(service); + struct btd_device *service_device = btd_service_get_device(service); + GSList *l; + + if (new_state != BTD_SERVICE_STATE_DISCONNECTED && + new_state != BTD_SERVICE_STATE_CONNECTED) + return; + + if (profile->remote_uuid == NULL || + !g_str_equal(profile->remote_uuid, AVRCP_REMOTE_UUID)) + return; + + for (l = bose_services; l != NULL; l = l->next) { + struct btd_service *service = l->data; + + if (btd_service_get_device(service) != service_device) + continue; + + switch (new_state) { + case BTD_SERVICE_STATE_DISCONNECTED: + bose_disconnect(service); + return; + case BTD_SERVICE_STATE_CONNECTED: + bose_connect(service); + return; + case BTD_SERVICE_STATE_CONNECTING: + case BTD_SERVICE_STATE_DISCONNECTING: + case BTD_SERVICE_STATE_UNAVAILABLE: + default: + return; + } + } +} + +static bool is_valid_bose_device(struct btd_device *device) +{ + if (btd_device_get_vendor(device) != BOSE_VENDOR_ID || + btd_device_get_class(device) != BOSE_DEVICE_CLASS || + btd_device_get_product(device) != BOSE_QC35_PRODUCT_ID) + return false; + + return true; +} + +static int bose_probe(struct btd_service *service) +{ + struct btd_device *device = btd_service_get_device(service); + struct bose_session *session = btd_service_get_user_data(service); + char addr[18]; + + if (!is_valid_bose_device(device)) + return -EINVAL; + + ba2str(device_get_address(device), addr); + DBG("Bose profile probe (%s)", addr); + + if (session) { + error("Profile probed twice for the same device!"); + return -EALREADY; + } + + session = g_new0(struct bose_session, 1); + if (!session) + return -ENOMEM; + + btd_service_set_user_data(service, session); + session->connection_state = BOSE_CONNECTION_STATE_DISCONNECTED; + session->path = NULL; + session->io = NULL; + session->io_id = 0; + + if (g_slist_length(bose_services) == 0 && service_cb_id == 0) + service_cb_id = btd_service_add_state_cb(service_cb, NULL); + + bose_services = g_slist_append(bose_services, service); + return 0; +} + + +static void bose_remove(struct btd_service *service) +{ + struct btd_device *device = btd_service_get_device(service); + struct bose_session *session = btd_service_get_user_data(service); + char addr[18]; + + ba2str(device_get_address(device), addr); + DBG("Bose profile remove (%s)", addr); + + if (bose_free(service) < 0) + error("Bose service not handled by profile"); +} + +static struct btd_profile bose_profile = { + .name = "bose-qc35-profile", + .vendor_id = BOSE_VENDOR_ID, + .product_id = BOSE_QC35_PRODUCT_ID, + .device_probe = bose_probe, + .device_remove = bose_remove, + .disconnect = bose_disconnect +}; + +static int bose_init(void) +{ + return btd_profile_register(&bose_profile); +} + +static void bose_exit(void)