From 04b8a17a66e7500b82eb913b8ae5588d15bd8466 Mon Sep 17 00:00:00 2001 From: Pawel Wasowski Date: Mon, 6 Jul 2020 20:36:21 +0200 Subject: [PATCH] [Bluetooth] Add BluetoothGATTServer. Implement start, stop and getServer ACR: TWDAPI-263 This commit adds BluetoothGATTServer class in C++ and implements JS APIs: - tizen.bluetooth.getGATTServer() - BluetoothGATTServer.start() - BluetoothGATTServer.stop() Verification: The code tested in the Chrome DevTools console works fine Change-Id: I688df55298be1b5ec997a5ae8ff42877f1bd6a24 Signed-off-by: Pawel Wasowski --- src/bluetooth/bluetooth.gyp | 4 +- src/bluetooth/bluetooth_api.js | 98 ++++++++++++ src/bluetooth/bluetooth_gatt_server.cc | 283 +++++++++++++++++++++++++++++++++ src/bluetooth/bluetooth_gatt_server.h | 61 +++++++ src/bluetooth/bluetooth_instance.cc | 47 +++++- src/bluetooth/bluetooth_instance.h | 12 ++ src/bluetooth/bluetooth_util.cc | 23 ++- src/bluetooth/bluetooth_util.h | 2 + 8 files changed, 524 insertions(+), 6 deletions(-) create mode 100644 src/bluetooth/bluetooth_gatt_server.cc create mode 100644 src/bluetooth/bluetooth_gatt_server.h diff --git a/src/bluetooth/bluetooth.gyp b/src/bluetooth/bluetooth.gyp index 07c310e..73df5d3 100644 --- a/src/bluetooth/bluetooth.gyp +++ b/src/bluetooth/bluetooth.gyp @@ -42,7 +42,9 @@ 'bluetooth_le_device.cc', 'bluetooth_le_device.h', 'uuid.cc', - 'uuid.h' + 'uuid.h', + 'bluetooth_gatt_server.cc', + 'bluetooth_gatt_server.h' ], 'includes': [ '../common/pkg-config.gypi', diff --git a/src/bluetooth/bluetooth_api.js b/src/bluetooth/bluetooth_api.js index 6e55fad..37799e0 100755 --- a/src/bluetooth/bluetooth_api.js +++ b/src/bluetooth/bluetooth_api.js @@ -2715,6 +2715,95 @@ BluetoothAdapter.prototype.getBluetoothProfileHandler = function() { } }; +// class BluetoothGATTServer //////////////////////// +var BluetoothGATTServer = function() { + var services_ = []; + Object.defineProperties(this, { + services: { + get: function() { + return services_; + }, + set: function() {} + } + }); +} + +var AbortError = new WebAPIException('AbortError', 'An unknown error occurred'); + +var BluetoothGATTServer_valid_start_errors = ['InvalidStateError', 'NotSupportedError', 'AbortError']; +var BluetoothGATTServer_valid_start_exceptions = ['InvalidStateError', 'TypeMismatchError', 'SecurityError']; + +BluetoothGATTServer.prototype.start = function() { + privUtils_.log('Entered BluetoothGATTServer.start()'); + var args = AV.validateArgs(arguments, [ + { + name: 'successCallback', + type: AV.Types.FUNCTION, + optional: true, + nullable: true + }, + { + name: 'errorCallback', + type: AV.Types.FUNCTION, + optional: true, + nullable: true + } + ]); + + var callback = function(result) { + if (native.isFailure(result)) { + native.callIfPossible(args.errorCallback, + native.getErrorObjectAndValidate(result, + BluetoothGATTServer_valid_start_errors, AbortError)); + } else { + native.callIfPossible(args.successCallback); + } + }; + + var result = native.call('BluetoothGATTServerStart', {}, callback); + if (native.isFailure(result)) { + throw native.getErrorObjectAndValidate(result, BluetoothGATTServer_valid_start_exceptions, AbortError); + } +} + +var BluetoothGATTServer_valid_stop_errors = ['InvalidStateError', 'NotSupportedError', 'AbortError']; +var BluetoothGATTServer_valid_stop_exceptions = ['InvalidStateError', 'TypeMismatchError', 'SecurityError']; + +BluetoothGATTServer.prototype.stop = function() { + privUtils_.log('Entered BluetoothGATTServer.stop()'); + var args = AV.validateArgs(arguments, [ + { + name: 'successCallback', + type: AV.Types.FUNCTION, + optional: true, + nullable: true + }, + { + name: 'errorCallback', + type: AV.Types.FUNCTION, + optional: true, + nullable: true + } + ]); + + var callback = function(result) { + if (native.isFailure(result)) { + native.callIfPossible(args.errorCallback, + native.getErrorObjectAndValidate(result, + BluetoothGATTServer_valid_stop_errors, AbortError)); + } else { + native.callIfPossible(args.successCallback); + } + }; + + var result = native.call('BluetoothGATTServerStop', {}, callback); + if (native.isFailure(result)) { + throw native.getErrorObjectAndValidate(result, BluetoothGATTServer_valid_stop_exceptions, AbortError); + } +} + +var GATTServer = new BluetoothGATTServer(); + // class BluetoothManager /////////////////////////// var BluetoothManager = function() { Object.defineProperties(this, { @@ -2993,5 +3082,14 @@ BluetoothManager.prototype.uuidsEqual = function(uuid1, uuid2) { ]); return BluetoothManager_UUIDsEqual(args.uuid1, args.uuid2); }; +BluetoothManager.prototype.getGATTServer = function() { + privUtils_.log('Entered BluetoothManager.getGATTServer()'); + return BluetoothManager_getGATTServer(); +}; + +var BluetoothManager_getGATTServer = function() { + return GATTServer; +}; + // exports ////////////////////////////////////////// exports = new BluetoothManager(); diff --git a/src/bluetooth/bluetooth_gatt_server.cc b/src/bluetooth/bluetooth_gatt_server.cc new file mode 100644 index 0000000..255433e --- /dev/null +++ b/src/bluetooth/bluetooth_gatt_server.cc @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd All Rights Reserved + * + * 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 "bluetooth/bluetooth_gatt_server.h" +#include "bluetooth/bluetooth_util.h" + +#include "common/logger.h" +#include "common/tools.h" + +using namespace extension::bluetooth::util; +using namespace common; +using namespace common::tools; + +namespace extension { +namespace bluetooth { + +BluetoothGATTServer::BluetoothGATTServer(const BluetoothInstance& instance) + : instance_{instance}, initialized_{false}, running_{false}, handle_{nullptr} { + ScopeLogger(); + + /* + * TODO: register a callback, that deinitializes a server, unregisters its + * services, destroys handle_ and sets running_ to false + * when Bluetooth is turned off. + */ +} + +BluetoothGATTServer::~BluetoothGATTServer() { + ScopeLogger(); + + UnregisterAllServicesImpl(); + DestroyAllGATTObjects(); + Deinitialize(); +} + +void BluetoothGATTServer::Start(picojson::object& out) { + ScopeLogger(); + + if (running_) { + LoggerD("Server is running"); + ReportSuccess(out); + return; + } + + if (!initialized_) { + /* + * Server is initialized when its services are registered. + * However, the case when server is uninitialized until start is + * also valid. It happens, when user doesn't register any services + * and starts the server with the default ones. + */ + auto result = InitializeAndCreateHandle(); + if (!result) { + result.SetMessage("Couldn't start GATT server"); + ReportError(result, &out); + return; + } + } + + auto ret = bt_gatt_server_start(); + if (BT_ERROR_NONE != ret) { + LoggerE("bt_gatt_server_start(): %d (%s)", ret, get_error_message(ret)); + ReportError(BluetoothErrorToPlatformResult(ret, "Couldn't start GATT server"), &out); + return; + } + LoggerD("bt_gatt_server_start(): success"); + + running_ = true; + ReportSuccess(out); +} + +void BluetoothGATTServer::Stop(picojson::object& out) { + ScopeLogger(); + + if (!running_) { + LoggerD("Server is not running"); + ReportSuccess(out); + return; + } + + // Unregistering all services should not fail. + // Even if it does we just let UnregisterAllSerivcesImpl() log the error + // and proceed with server destruction. + UnregisterAllServicesImpl(); + + // DestroyAllGATTObjects() should not fail + // If it does, it logs the error message. + DestroyAllGATTObjects(); + + // Deinitialize() is the function that actually stops the server, + // thus we return the result, it returns. + auto result = Deinitialize(); + if (!result) { + result.SetMessage("Couldn't stop GATT server"); + ReportError(result, &out); + } else { + ReportSuccess(out); + running_ = false; + } +} + +PlatformResult BluetoothGATTServer::Initialize() { + ScopeLogger(); + + auto ret = bt_gatt_server_initialize(); + if (BT_ERROR_NONE != ret) { + LoggerE("bt_gatt_server_initialize(): %d (%s)", ret, get_error_message(ret)); + return BluetoothErrorToPlatformResult(ret); + } + LoggerD("bt_gatt_server_initialize(): success"); + initialized_ = true; + + return PlatformResult{}; +} + +PlatformResult BluetoothGATTServer::CreateHandle() { + ScopeLogger(); + + if (handle_) { + LoggerE("Server handle is already created"); + return PlatformResult{ErrorCode::INVALID_STATE_ERR}; + } + + auto ret = bt_gatt_server_create(&handle_); + if (BT_ERROR_NONE != ret) { + LoggerE("bt_gatt_server_create(): %d (%s)", ret, get_error_message(ret)); + } else { + LoggerD("bt_gatt_server_create(): success"); + } + + return BluetoothErrorToPlatformResult(ret); +} + +PlatformResult BluetoothGATTServer::Deinitialize() { + ScopeLogger(); + + if (!initialized_) { + LoggerE("Server is not initialized"); + return PlatformResult{ErrorCode::INVALID_STATE_ERR}; + } + + auto ret = bt_gatt_server_deinitialize(); + if (BT_ERROR_NONE != ret) { + LoggerE("bt_gatt_server_deinitialize(): %d (%s)", ret, get_error_message(ret)); + return BluetoothErrorToPlatformResult(ret); + } + + LoggerD("bt_gatt_server_deinitialize(): success"); + initialized_ = false; + + /* + * Undocumented behavior of native API: bt_gatt_server_deinitialize() calls + * bt_gatt_server_destroy() - after Deinitialize() bt_gatt_server_destroy() + * should not be called (a call results in a BT_INVALID_STATE_ERROR). + * handle_ is to be nullified, as it is no longer valid. + */ + handle_ = nullptr; + + return PlatformResult{}; +} + +PlatformResult BluetoothGATTServer::InitializeAndCreateHandle() { + ScopeLogger(); + + auto result = Initialize(); + if (!result) { + return result; + } + + result = CreateHandle(); + if (!result) { + Deinitialize(); + } + return result; +} + +PlatformResult BluetoothGATTServer::UnregisterAllServicesImpl() { + ScopeLogger(); + + if (!handle_) { + LoggerE("Server handle is a nullptr"); + return PlatformResult{ErrorCode::INVALID_STATE_ERR}; + } + + auto ret = bt_gatt_server_unregister_all_services(handle_); + if (BT_ERROR_NONE != ret) { + LoggerE("bt_gatt_server_unregister_all_services(): %d (%s)", ret, get_error_message(ret)); + return BluetoothErrorToPlatformResult(ret); + } else { + LoggerD("bt_gatt_server_unregister_all_services(): success"); + return PlatformResult{}; + } +} + +PlatformResult BluetoothGATTServer::DestroyAllGATTObjects() { + ScopeLogger(); + + if (!handle_) { + LoggerE("Server handle is a nullptr"); + return PlatformResult{ErrorCode::INVALID_STATE_ERR}; + } + + auto ret = bt_gatt_server_foreach_services(handle_, DestroyService, nullptr); + if (BT_ERROR_NONE != ret) { + LoggerE("bt_gatt_server_unregister_all_services(): %d (%s)", ret, get_error_message(ret)); + return BluetoothErrorToPlatformResult(ret); + } else { + LoggerD("bt_gatt_server_unregister_all_services(): success"); + return PlatformResult{}; + } + + /* + * TODO: remove handles from service id -> service handle map + */ +} + +bool BluetoothGATTServer::DestroyService(int total, int index, bt_gatt_h handle, void* user_data) { + ScopeLogger("total: %d, index: %d, handle: %p", total, index, handle); + + auto ret = bt_gatt_service_foreach_included_services(handle, DestroyService, nullptr); + if (BT_ERROR_NONE != ret) { + LoggerE("bt_gatt_service_foreach_included_servics(): %d (%s)", ret, get_error_message(ret)); + } + LoggerD("bt_gatt_service_foreach_included_servics(): success"); + + ret = bt_gatt_service_foreach_characteristics(handle, DestroyCharacteristic, nullptr); + if (BT_ERROR_NONE != ret) { + LoggerE("bt_gatt_service_foreach_characteristics(): %d (%s)", ret, get_error_message(ret)); + } + LoggerD("bt_gatt_service_foreach_characteristics(): success"); + + ret = bt_gatt_service_destroy(handle); + if (BT_ERROR_NONE != ret) { + LoggerE("bt_gatt_service_destroy(): %d (%s)", ret, get_error_message(ret)); + } + + return true; +} + +bool BluetoothGATTServer::DestroyCharacteristic(int total, int index, bt_gatt_h handle, + void* user_data) { + ScopeLogger("total: %d, index: %d, handle: %p", total, index, handle); + + auto ret = bt_gatt_characteristic_foreach_descriptors(handle, DestroyDescriptor, nullptr); + if (BT_ERROR_NONE != ret) { + LoggerE("bt_gatt_characteristic_foreach_descriptors(): %d (%s)", ret, get_error_message(ret)); + } + LoggerD("bt_gatt_characteristic_foreach_descriptors(): success"); + + ret = bt_gatt_characteristic_destroy(handle); + if (BT_ERROR_NONE != ret) { + LoggerE("bt_gatt_characteristic_destroy(): %d (%s)", ret, get_error_message(ret)); + } + + return true; +} + +bool BluetoothGATTServer::DestroyDescriptor(int total, int index, bt_gatt_h handle, + void* user_data) { + ScopeLogger("total: %d, index: %d, handle: %p", total, index, handle); + + auto ret = bt_gatt_descriptor_destroy(handle); + if (BT_ERROR_NONE != ret) { + LoggerE("bt_gatt_descriptor_destroy(): %d (%s)", ret, get_error_message(ret)); + } + return true; +} + +} // namespace bluetooth +} // namespace extension diff --git a/src/bluetooth/bluetooth_gatt_server.h b/src/bluetooth/bluetooth_gatt_server.h new file mode 100644 index 0000000..6c5d83d --- /dev/null +++ b/src/bluetooth/bluetooth_gatt_server.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2020 Samsung Electronics Co., Ltd All Rights Reserved + * + * 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_BLUETOOTH_GATT_SERVER_H_ +#define BLUETOOTH_BLUETOOTH_GATT_SERVER_H_ + +#include + +#include "common/picojson.h" +#include "common/platform_result.h" + +using common::PlatformResult; + +namespace extension { +namespace bluetooth { + +class BluetoothInstance; + +class BluetoothGATTServer { + public: + BluetoothGATTServer(const BluetoothInstance&); + ~BluetoothGATTServer(); + + void Start(picojson::object& out); + void Stop(picojson::object& out); + + static bool DestroyService(int total, int index, bt_gatt_h handle, void* user_data); + static bool DestroyCharacteristic(int total, int index, bt_gatt_h handle, void* user_data); + static bool DestroyDescriptor(int total, int index, bt_gatt_h handle, void* user_data); + + private: + const BluetoothInstance& instance_; + bool initialized_; + bool running_; + bt_gatt_server_h handle_; + + PlatformResult UnregisterAllServicesImpl(); + PlatformResult DestroyAllGATTObjects(); + PlatformResult Initialize(); + PlatformResult CreateHandle(); + PlatformResult Deinitialize(); + PlatformResult InitializeAndCreateHandle(); +}; + +} // namespace bluetooth +} // namespace extension + +#endif // BLUETOOTH_BLUETOOTH_GATT_SERVER_H_ diff --git a/src/bluetooth/bluetooth_instance.cc b/src/bluetooth/bluetooth_instance.cc index 6b68e5f..2ff079e 100644 --- a/src/bluetooth/bluetooth_instance.cc +++ b/src/bluetooth/bluetooth_instance.cc @@ -36,7 +36,9 @@ BluetoothInstance::BluetoothInstance() bluetooth_socket_(bluetooth_adapter_), bluetooth_le_adapter_(*this), bluetooth_gatt_service_(*this), - bluetooth_le_device_(*this, bluetooth_gatt_service_) { + bluetooth_le_device_(*this, bluetooth_gatt_service_), + worker(), + bluetooth_gatt_server_(*this) { ScopeLogger(); using std::placeholders::_1; using std::placeholders::_2; @@ -98,16 +100,21 @@ BluetoothInstance::BluetoothInstance() REGISTER_METHOD(BluetoothGATTServiceAddValueChangeListener); REGISTER_METHOD(BluetoothGATTServiceRemoveValueChangeListener); + REGISTER_METHOD(BluetoothGATTServerStart); + REGISTER_METHOD(BluetoothGATTServerStop); + #undef REGISTER_METHOD } BluetoothInstance::~BluetoothInstance() { ScopeLogger(); + worker.stop(); } namespace { const char* JSON_CALLBACK_ID = "callbackId"; const char* JSON_LISTENER_ID = "listenerId"; +const std::string kPrivilegeBluetooth = "http://tizen.org/privilege/bluetooth"; } // namespace void BluetoothInstance::AsyncResponse(double callback_handle, @@ -435,5 +442,43 @@ void BluetoothInstance::BluetoothGATTServiceRemoveValueChangeListener(const pico bluetooth_gatt_service_.RemoveValueChangeListener(args, out); } +void BluetoothInstance::BluetoothGATTServerStart(const picojson::value& args, + picojson::object& out) { + ScopeLogger(); + + CHECK_PRIVILEGE_ACCESS(kPrivilegeBluetooth, &out); + + double callback_id = args.get(JSON_CALLBACK_ID).get(); + worker.add_job([this, callback_id] { + ScopeLogger("Async call: BluetoothGATTServerStart"); + picojson::value response = picojson::value(picojson::object()); + picojson::object& async_out = response.get(); + async_out[JSON_CALLBACK_ID] = picojson::value(callback_id); + this->bluetooth_gatt_server_.Start(async_out); + this->PostMessage(response.serialize().c_str()); + }); + + ReportSuccess(out); +} + +void BluetoothInstance::BluetoothGATTServerStop(const picojson::value& args, + picojson::object& out) { + ScopeLogger(); + + CHECK_PRIVILEGE_ACCESS(kPrivilegeBluetooth, &out); + + double callback_id = args.get(JSON_CALLBACK_ID).get(); + worker.add_job([this, callback_id] { + ScopeLogger("Async call: BluetoothGATTServerStop"); + picojson::value response = picojson::value(picojson::object()); + picojson::object& async_out = response.get(); + async_out[JSON_CALLBACK_ID] = picojson::value(callback_id); + this->bluetooth_gatt_server_.Stop(async_out); + this->PostMessage(response.serialize().c_str()); + }); + + ReportSuccess(out); +} + } // namespace bluetooth } // namespace extension diff --git a/src/bluetooth/bluetooth_instance.h b/src/bluetooth/bluetooth_instance.h index 0ed4909..0ee235d 100644 --- a/src/bluetooth/bluetooth_instance.h +++ b/src/bluetooth/bluetooth_instance.h @@ -21,6 +21,7 @@ #include "bluetooth/bluetooth_adapter.h" #include "bluetooth/bluetooth_device.h" +#include "bluetooth/bluetooth_gatt_server.h" #include "bluetooth/bluetooth_gatt_service.h" #include "bluetooth/bluetooth_health_application.h" #include "bluetooth/bluetooth_health_channel.h" @@ -30,6 +31,7 @@ #include "bluetooth/bluetooth_service_handler.h" #include "bluetooth/bluetooth_socket.h" #include "bluetooth/bluetooth_util.h" +#include "common/worker.h" namespace extension { namespace bluetooth { @@ -106,6 +108,8 @@ class BluetoothInstance : public common::ParsedInstance { picojson::object& out); void BluetoothGATTServiceRemoveValueChangeListener(const picojson::value& args, picojson::object& out); + void BluetoothGATTServerStart(const picojson::value& args, picojson::object& out); + void BluetoothGATTServerStop(const picojson::value& args, picojson::object& out); BluetoothAdapter bluetooth_adapter_; BluetoothDevice bluetooth_device_; @@ -117,6 +121,14 @@ class BluetoothInstance : public common::ParsedInstance { BluetoothLEAdapter bluetooth_le_adapter_; BluetoothGATTService bluetooth_gatt_service_; BluetoothLEDevice bluetooth_le_device_; + common::Worker worker; + // If all operations on bluetooth_gatt_server_ will be done by the worker, + // no mutex to prevent concurrent access of this object is needed + // - all operations will be done in a single worker thread. + // If any operation on the bluetooth_gatt_server_ object will be done + // synchronously, add a mutex and lock it before each call of server's + // method. + BluetoothGATTServer bluetooth_gatt_server_; }; } // namespace bluetooth diff --git a/src/bluetooth/bluetooth_util.cc b/src/bluetooth/bluetooth_util.cc index 1b378a1..64a3c83 100644 --- a/src/bluetooth/bluetooth_util.cc +++ b/src/bluetooth/bluetooth_util.cc @@ -36,10 +36,13 @@ const picojson::object& GetArguments(const picojson::value& data) { return data.get(); } -PlatformResult GetBluetoothError(int error_code, const std::string& hint) { +PlatformResult BluetoothErrorToPlatformResult(int error_code, const std::string& message) { common::ErrorCode error = ErrorCode::UNKNOWN_ERR; switch (error_code) { + case BT_ERROR_NONE: + error = ErrorCode::NO_ERROR; + break; case BT_ERROR_RESOURCE_BUSY: case BT_ERROR_NOW_IN_PROGRESS: case BT_ERROR_NOT_ENABLED: @@ -58,19 +61,31 @@ PlatformResult GetBluetoothError(int error_code, const std::string& hint) { error = ErrorCode::QUOTA_EXCEEDED_ERR; break; + case BT_ERROR_NOT_SUPPORTED: + error = ErrorCode::NOT_SUPPORTED_ERR; + break; + default: error = ErrorCode::UNKNOWN_ERR; break; } - std::string message = hint + " : " + GetBluetoothErrorMessage(error_code); + return PlatformResult{error, message}; +} + +PlatformResult GetBluetoothError(int error_code, const std::string& hint) { + auto message = hint + ": " + GetBluetoothErrorMessage(error_code); + auto result = BluetoothErrorToPlatformResult(error_code, message); + + LoggerE("%s %d (%s)", message.c_str(), error_code, get_error_message(error_code)); - return LogAndCreateResult(error, message.c_str(), ("%s %d (%s)", message.c_str(), error_code, - get_error_message(error_code))); + return result; } std::string GetBluetoothErrorMessage(int error_code) { switch (error_code) { + case BT_ERROR_NONE: + return "Success"; case BT_ERROR_CANCELLED: return "Operation cancelled"; case BT_ERROR_INVALID_PARAMETER: diff --git a/src/bluetooth/bluetooth_util.h b/src/bluetooth/bluetooth_util.h index 69f5d46..e182b13 100644 --- a/src/bluetooth/bluetooth_util.h +++ b/src/bluetooth/bluetooth_util.h @@ -31,6 +31,8 @@ double GetAsyncCallbackHandle(const picojson::value& data); const picojson::object& GetArguments(const picojson::value& data); +common::PlatformResult BluetoothErrorToPlatformResult(int error_code, + const std::string& message = ""); common::PlatformResult GetBluetoothError(int error_code, const std::string& hint); std::string GetBluetoothErrorMessage(int error_code); -- 2.7.4